NUC-39: Create wrapper over System.Net.Http HttpClient.

This commit is contained in:
Denis-Cosmin Nutiu 2020-04-18 18:23:57 +03:00
parent 6f100abc1e
commit c4aa2e7c63
3 changed files with 246 additions and 2 deletions

View file

@ -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
{
/// <summary>
/// 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.
/// </summary>
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<dynamic> GetAsync(string path)
{
var request = _makeRequest(sNetHttp.HttpMethod.Get, path);
return await SendAsync(request);
}
public async Task<dynamic> PostAsync(string path, Dictionary<string, object> data)
{
var request = _makeRequest(sNetHttp.HttpMethod.Post, path);
request.Content = _makeContent(data);
return await SendAsync(request);
}
public async Task<dynamic> PutAsync(string path, Dictionary<string, object> data)
{
var request = _makeRequest(sNetHttp.HttpMethod.Put, path);
request.Content = _makeContent(data);
return await SendAsync(request);
}
public async Task<dynamic> DeleteAsync(string path, Dictionary<string, object> data)
{
var request = _makeRequest(sNetHttp.HttpMethod.Delete, path);
request.Content = _makeContent(data);
return await SendAsync(request);
}
/// <summary>
/// Makes a request with timeout and retry support.
/// </summary>
/// <param name="requestMessage">The request to make.</param>
/// <returns></returns>
public virtual async Task<sNetHttp.HttpResponseMessage> 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
/// <summary>
/// Creates a StringContent with media type of application.json and encodes it with UTF8.
/// </summary>
/// <param name="data">A dictionary representing JSON data.</param>
/// <returns></returns>
private sNetHttp.StringContent _makeContent(Dictionary<string, object> data)
{
return new sNetHttp.StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
}
/// <summary>
/// Creates a HttpRequestMessage, applies the auth header and constructs the uri.
/// </summary>
/// <param name="method">The HttpMethod to use</param>
/// <param name="path">The path, whether it is relative to the base or a new one.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Makes a request which gets cancelled after Timeout.
/// </summary>
/// <param name="requestMessage"></param>
/// <returns></returns>
private async Task<sNetHttp.HttpResponseMessage> _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
}
}

View file

@ -4,4 +4,8 @@
<TargetFrameworks>netcoreapp3.0;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.1" />
</ItemGroup>
</Project>

View file

@ -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}.");
}