NUC-39: Create wrapper over System.Net.Http HttpClient.
This commit is contained in:
parent
6f100abc1e
commit
c4aa2e7c63
3 changed files with 246 additions and 2 deletions
241
NucuCar.Common/HttpClient.cs
Normal file
241
NucuCar.Common/HttpClient.cs
Normal 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
|
||||
}
|
||||
}
|
|
@ -4,4 +4,8 @@
|
|||
<TargetFrameworks>netcoreapp3.0;netcoreapp3.1</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -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}.");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue