From c4aa2e7c63580f427c8878876dcd9f9924d08b09 Mon Sep 17 00:00:00 2001 From: Denis-Cosmin Nutiu Date: Sat, 18 Apr 2020 18:23:57 +0300 Subject: [PATCH] NUC-39: Create wrapper over System.Net.Http HttpClient. --- NucuCar.Common/HttpClient.cs | 241 ++++++++++++++++++ NucuCar.Common/NucuCar.Common.csproj | 4 + .../TelemetryPublisherFirestore.cs | 3 +- 3 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 NucuCar.Common/HttpClient.cs diff --git a/NucuCar.Common/HttpClient.cs b/NucuCar.Common/HttpClient.cs new file mode 100644 index 0000000..dcb0c6f --- /dev/null +++ b/NucuCar.Common/HttpClient.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using sNetHttp = System.Net.Http; +using sNetHttpHeaders = System.Net.Http.Headers; +using Microsoft.Extensions.Logging; + +namespace NucuCar.Common +{ + /// + /// A simple HttpClient wrapper designed to make it easier to work with web requests with media type application/json. + /// It implements a simple retry mechanism. + /// + public class HttpClient : IDisposable + { + #region Fields + + public ILogger Logger; + + // ReSharper disable InconsistentNaming + protected int maxRetries; + + protected int timeout; + // ReSharper restore InconsistentNaming + + private readonly sNetHttp.HttpClient _httpClient; + + + public int MaxRetries + { + get => maxRetries; + set + { + if (value < 0 || value > 10) + { + throw new ArgumentOutOfRangeException($"Maximum retries allowed value is between 0 and 10!"); + } + + maxRetries = value; + } + } + + public int Timeout + { + get => timeout; + set + { + if (value < 0 || value > 10000) + { + throw new ArgumentOutOfRangeException($"Timeout allowed value is between 0 and 10000!"); + } + + timeout = value; + } + } + + #endregion + + #region Constructors + + protected HttpClient() + { + _httpClient = new sNetHttp.HttpClient(); + maxRetries = 3; + timeout = 10000; + Logger = null; + } + + protected HttpClient(string baseAddress) : this() + { + _httpClient.BaseAddress = new Uri(baseAddress); + // TODO: Setup logging. + } + + protected HttpClient(string baseAddress, int maxRetries) : this(baseAddress) + { + MaxRetries = maxRetries; + } + + #endregion + + + #region Public Methods + + public void Authorization(string scheme, string token) + { + _httpClient.DefaultRequestHeaders.Authorization = + new sNetHttpHeaders.AuthenticationHeaderValue(scheme, token); + } + + public void Authorization(string token) + { + Authorization("Bearer", token); + } + + public async Task GetAsync(string path) + { + var request = _makeRequest(sNetHttp.HttpMethod.Get, path); + return await SendAsync(request); + } + + public async Task PostAsync(string path, Dictionary data) + { + var request = _makeRequest(sNetHttp.HttpMethod.Post, path); + request.Content = _makeContent(data); + return await SendAsync(request); + } + + public async Task PutAsync(string path, Dictionary data) + { + var request = _makeRequest(sNetHttp.HttpMethod.Put, path); + request.Content = _makeContent(data); + return await SendAsync(request); + } + + public async Task DeleteAsync(string path, Dictionary data) + { + var request = _makeRequest(sNetHttp.HttpMethod.Delete, path); + request.Content = _makeContent(data); + return await SendAsync(request); + } + + /// + /// Makes a request with timeout and retry support. + /// + /// The request to make. + /// + public virtual async Task SendAsync(sNetHttp.HttpRequestMessage requestMessage) + { + var currentRetry = 0; + sNetHttp.HttpResponseMessage responseMessage = null; + + while (currentRetry < maxRetries) + { + try + { + responseMessage = await _sendAsync(requestMessage); + break; + } + catch (TaskCanceledException) + { + Logger?.LogError($"Request timeout for {requestMessage.RequestUri}!"); + } + catch (sNetHttp.HttpRequestException e) + { + // The request failed due to an underlying issue such as network connectivity, DNS failure, + // server certificate validation or timeout. + Logger?.LogError($"HttpRequestException timeout for {requestMessage.RequestUri}!"); + Logger?.LogError($"{e.Message}"); + } + finally + { + currentRetry += 1; + } + } + + return responseMessage; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region NonPublic Methods + + /// + /// Creates a StringContent with media type of application.json and encodes it with UTF8. + /// + /// A dictionary representing JSON data. + /// + private sNetHttp.StringContent _makeContent(Dictionary data) + { + return new sNetHttp.StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json"); + } + + /// + /// Creates a HttpRequestMessage, applies the auth header and constructs the uri. + /// + /// The HttpMethod to use + /// The path, whether it is relative to the base or a new one. + /// + private sNetHttp.HttpRequestMessage _makeRequest(sNetHttp.HttpMethod method, string path) + { + var requestUri = path.ToLowerInvariant().StartsWith("http://") + ? new Uri(path) + : new Uri(_httpClient.BaseAddress, path); + + var requestMessage = new sNetHttp.HttpRequestMessage + { + Method = method, + RequestUri = requestUri + }; + requestMessage.Headers.Authorization = _httpClient.DefaultRequestHeaders.Authorization; + + return requestMessage; + } + + /// + /// Makes a request which gets cancelled after Timeout. + /// + /// + /// + private async Task _sendAsync(sNetHttp.HttpRequestMessage requestMessage) + { + var cts = new CancellationTokenSource(); + sNetHttp.HttpResponseMessage response; + + // Make sure we cancel after a certain timeout. + cts.CancelAfter(timeout); + try + { + response = await _httpClient.SendAsync(requestMessage, + sNetHttp.HttpCompletionOption.ResponseContentRead, + cts.Token); + } + finally + { + cts.Dispose(); + } + + return response; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _httpClient.Dispose(); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/NucuCar.Common/NucuCar.Common.csproj b/NucuCar.Common/NucuCar.Common.csproj index 42cf6aa..7c20e9d 100644 --- a/NucuCar.Common/NucuCar.Common.csproj +++ b/NucuCar.Common/NucuCar.Common.csproj @@ -4,4 +4,8 @@ netcoreapp3.0;netcoreapp3.1 + + + + diff --git a/NucuCar.Telemetry/TelemetryPublisherFirestore.cs b/NucuCar.Telemetry/TelemetryPublisherFirestore.cs index a86d778..38949f1 100644 --- a/NucuCar.Telemetry/TelemetryPublisherFirestore.cs +++ b/NucuCar.Telemetry/TelemetryPublisherFirestore.cs @@ -64,8 +64,7 @@ namespace NucuCar.Telemetry var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" + $"databases/(default)/documents/{firestoreCollection}/"; - _httpClient = new HttpClient(); - _httpClient.BaseAddress = new Uri(requestUrl); + _httpClient = new HttpClient {BaseAddress = new Uri(requestUrl)}; Logger?.LogInformation($"Initialized {nameof(TelemetryPublisherFirestore)}"); Logger?.LogInformation($"ProjectId: {firestoreProjectId}; CollectionName: {firestoreCollection}."); }