2020-04-18 15:23:57 +00:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
2021-08-02 18:57:52 +00:00
|
|
|
using System.Net.Http;
|
|
|
|
using System.Net.Http.Headers;
|
2020-04-18 15:23:57 +00:00
|
|
|
using System.Text;
|
|
|
|
using System.Text.Json;
|
|
|
|
using System.Threading;
|
|
|
|
using System.Threading.Tasks;
|
2020-08-01 13:03:06 +00:00
|
|
|
using Microsoft.Extensions.Logging;
|
2020-04-18 15:23:57 +00:00
|
|
|
|
2021-08-02 18:57:52 +00:00
|
|
|
namespace NucuCar.Core.Http
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
/// <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>
|
2020-11-25 18:49:15 +00:00
|
|
|
public class MinimalHttpClient : IDisposable
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
#region Fields
|
|
|
|
|
2020-04-19 09:26:56 +00:00
|
|
|
public ILogger Logger { get; set; }
|
2020-04-18 15:23:57 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-19 09:26:56 +00:00
|
|
|
// ReSharper disable InconsistentNaming
|
|
|
|
protected int maxRetries;
|
|
|
|
|
|
|
|
protected int timeout;
|
|
|
|
// ReSharper restore InconsistentNaming
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
private readonly HttpClient _httpClient;
|
2020-04-19 09:26:56 +00:00
|
|
|
|
2020-04-18 15:23:57 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Constructors
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public MinimalHttpClient()
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
_httpClient = new HttpClient();
|
2020-04-18 15:23:57 +00:00
|
|
|
maxRetries = 3;
|
|
|
|
timeout = 10000;
|
|
|
|
Logger = null;
|
|
|
|
}
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public MinimalHttpClient(string baseAddress) : this()
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
_httpClient.BaseAddress = new Uri(baseAddress);
|
|
|
|
}
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public MinimalHttpClient(string baseAddress, int maxRetries) : this(baseAddress)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
MaxRetries = maxRetries;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Public Methods
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public void ClearAuthorizationHeader()
|
|
|
|
{
|
|
|
|
_httpClient.DefaultRequestHeaders.Authorization = null;
|
|
|
|
}
|
|
|
|
|
2020-04-18 15:23:57 +00:00
|
|
|
public void Authorization(string scheme, string token)
|
|
|
|
{
|
|
|
|
_httpClient.DefaultRequestHeaders.Authorization =
|
2020-11-25 18:49:15 +00:00
|
|
|
new AuthenticationHeaderValue(scheme, token);
|
2020-04-18 15:23:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public void Authorization(string token)
|
|
|
|
{
|
|
|
|
Authorization("Bearer", token);
|
|
|
|
}
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public async Task<HttpResponseMessage> GetAsync(string path)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
var request = _makeRequest(HttpMethod.Get, path);
|
2020-04-18 15:23:57 +00:00
|
|
|
return await SendAsync(request);
|
|
|
|
}
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public async Task<HttpResponseMessage> PostAsync(string path, Dictionary<string, object> data)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
var request = _makeRequest(HttpMethod.Post, path);
|
2020-04-18 15:23:57 +00:00
|
|
|
request.Content = _makeContent(data);
|
|
|
|
return await SendAsync(request);
|
|
|
|
}
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public async Task<HttpResponseMessage> PutAsync(string path, Dictionary<string, object> data)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
var request = _makeRequest(HttpMethod.Put, path);
|
2020-04-18 15:23:57 +00:00
|
|
|
request.Content = _makeContent(data);
|
|
|
|
return await SendAsync(request);
|
|
|
|
}
|
|
|
|
|
2020-11-25 18:49:15 +00:00
|
|
|
public async Task<HttpResponseMessage> DeleteAsync(string path, Dictionary<string, object> data)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
var request = _makeRequest(HttpMethod.Delete, path);
|
2020-04-18 15:23:57 +00:00
|
|
|
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>
|
2020-11-25 18:49:15 +00:00
|
|
|
public virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage requestMessage)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
var currentRetry = 0;
|
2020-11-25 18:49:15 +00:00
|
|
|
HttpResponseMessage responseMessage = null;
|
2020-04-18 15:23:57 +00:00
|
|
|
|
|
|
|
while (currentRetry < maxRetries)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
// We need a request copy because we can't send the same request multiple times.
|
|
|
|
var requestCopy = new HttpRequestMessage(requestMessage.Method, requestMessage.RequestUri);
|
|
|
|
requestCopy.Headers.Authorization = requestMessage.Headers.Authorization;
|
|
|
|
requestCopy.Content = requestMessage.Content;
|
|
|
|
|
|
|
|
responseMessage = await _sendAsync(requestCopy);
|
2020-04-18 15:23:57 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
catch (TaskCanceledException)
|
|
|
|
{
|
2021-08-01 17:22:25 +00:00
|
|
|
Logger?.LogError("Request timeout for {Uri}!", requestMessage.RequestUri);
|
2020-04-18 15:23:57 +00:00
|
|
|
}
|
2020-11-25 18:49:15 +00:00
|
|
|
catch (HttpRequestException e)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
// The request failed due to an underlying issue such as network connectivity, DNS failure,
|
|
|
|
// server certificate validation or timeout.
|
2021-08-01 17:22:25 +00:00
|
|
|
Logger?.LogError("HttpRequestException timeout for {Uri}!", requestMessage.RequestUri);
|
|
|
|
Logger?.LogError("{ErrorMessage}", e.Message);
|
2020-04-18 15:23:57 +00:00
|
|
|
}
|
|
|
|
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>
|
2020-11-25 18:49:15 +00:00
|
|
|
private StringContent _makeContent(Dictionary<string, object> data)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-11-25 18:49:15 +00:00
|
|
|
return new StringContent(JsonSerializer.Serialize(data), Encoding.UTF8, "application/json");
|
2020-04-18 15:23:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <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>
|
2020-11-25 18:49:15 +00:00
|
|
|
private HttpRequestMessage _makeRequest(HttpMethod method, string path)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
2020-04-20 15:19:32 +00:00
|
|
|
var uri = _httpClient.BaseAddress == null ? new Uri(path) : new Uri(_httpClient.BaseAddress, path);
|
2020-11-25 18:49:15 +00:00
|
|
|
|
|
|
|
var requestMessage = new HttpRequestMessage
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
Method = method,
|
2020-04-20 15:19:32 +00:00
|
|
|
RequestUri = uri
|
2020-04-18 15:23:57 +00:00
|
|
|
};
|
|
|
|
requestMessage.Headers.Authorization = _httpClient.DefaultRequestHeaders.Authorization;
|
|
|
|
|
|
|
|
return requestMessage;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Makes a request which gets cancelled after Timeout.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="requestMessage"></param>
|
|
|
|
/// <returns></returns>
|
2020-11-25 18:49:15 +00:00
|
|
|
private async Task<HttpResponseMessage> _sendAsync(HttpRequestMessage requestMessage)
|
2020-04-18 15:23:57 +00:00
|
|
|
{
|
|
|
|
var cts = new CancellationTokenSource();
|
2020-11-25 18:49:15 +00:00
|
|
|
HttpResponseMessage response;
|
2020-04-18 15:23:57 +00:00
|
|
|
|
|
|
|
// Make sure we cancel after a certain timeout.
|
|
|
|
cts.CancelAfter(timeout);
|
|
|
|
try
|
|
|
|
{
|
|
|
|
response = await _httpClient.SendAsync(requestMessage,
|
2020-11-25 18:49:15 +00:00
|
|
|
HttpCompletionOption.ResponseContentRead,
|
2020-04-18 15:23:57 +00:00
|
|
|
cts.Token);
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
{
|
|
|
|
cts.Dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
{
|
|
|
|
if (disposing)
|
|
|
|
{
|
|
|
|
_httpClient.Dispose();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
}
|
2020-04-19 10:08:34 +00:00
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// HttpClientResponseMessageExtension provides extensions methods for the HttpResponseMessage class.
|
|
|
|
/// </summary>
|
|
|
|
public static class HttpResponseMessageExtension
|
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// Extension used to deserialize the body of a HttpResponseMessage into Json.
|
|
|
|
/// </summary>
|
2020-11-25 18:49:15 +00:00
|
|
|
/// <param name="responseMessage">The HttpResponseMessage message. <see cref="HttpResponseMessage"/></param>
|
2020-04-19 10:08:34 +00:00
|
|
|
/// <returns>A JsonElement. <see cref="JsonElement"/></returns>
|
2020-11-25 18:49:15 +00:00
|
|
|
public static async Task<JsonElement> GetJson(this HttpResponseMessage responseMessage)
|
2020-04-19 10:08:34 +00:00
|
|
|
{
|
|
|
|
return JsonSerializer.Deserialize<JsonElement>(await responseMessage.Content.ReadAsStringAsync());
|
|
|
|
}
|
|
|
|
}
|
2020-04-18 15:23:57 +00:00
|
|
|
}
|