A:网络请求
This commit is contained in:
1 parent
c73c6f1380
commit
956169dbb4
16 files changed
+593
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NetClient.Abstractions;
|
||||||
|
|
||||||
|
public interface IAccessTokenProvider
|
||||||
|
{
|
||||||
|
Task<string?> GetTokenAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NetClient.Abstractions;
|
||||||
|
|
||||||
|
public interface IHttpClient
|
||||||
|
{
|
||||||
|
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<T?> GetAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default);
|
||||||
|
Task<string> GetStringAsync(string path, object? query = null, CancellationToken cancellationToken = default);
|
||||||
|
Task<byte[]> GetBytesAsync(string path, object? query = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default);
|
||||||
|
Task<TResponse?> PutAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default);
|
||||||
|
Task<TResponse?> PatchAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteAsync(string path, object? query = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<TResponse?> PostMultipartAsync<TResponse>(string path, System.Action<System.Net.Http.MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NetClient.Abstractions;
|
||||||
|
|
||||||
|
public interface IHttpContentSerializer
|
||||||
|
{
|
||||||
|
string ContentType { get; }
|
||||||
|
Task SerializeAsync<T>(Stream stream, T? value, CancellationToken cancellationToken = default);
|
||||||
|
Task<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace NetClient.Abstractions;
|
||||||
|
|
||||||
|
public interface INetClientFactory
|
||||||
|
{
|
||||||
|
IHttpClient Create(string name);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace NetClient
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NetClient.Abstractions;
|
||||||
|
using NetClient.Handlers;
|
||||||
|
using NetClient.Implementations;
|
||||||
|
using NetClient.Internals.Serialization;
|
||||||
|
using NetClient.Options;
|
||||||
|
using Polly;
|
||||||
|
using Polly.Contrib.WaitAndRetry;
|
||||||
|
using Polly.Extensions.Http;
|
||||||
|
|
||||||
|
namespace NetClient.DependencyInjection;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddNetClientCore(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.TryAddTransient<LoggingHandler>();
|
||||||
|
services.TryAddTransient<BearerTokenAuthenticationHandler>();
|
||||||
|
services.TryAddSingleton<IHttpContentSerializer>(sp => new SystemTextJsonContentSerializer(new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web)));
|
||||||
|
services.TryAddSingleton<INetClientFactory, NetClientFactory>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHttpClientBuilder AddNetClient(this IServiceCollection services, Action<NetClientOptions> configure)
|
||||||
|
{
|
||||||
|
services.AddNetClientCore();
|
||||||
|
var options = new NetClientOptions();
|
||||||
|
configure(options);
|
||||||
|
|
||||||
|
var builder = services.AddHttpClient<IHttpClient, DefaultNetClient>(client => ApplyClientOptions(client, options));
|
||||||
|
|
||||||
|
if (options.EnableLogging)
|
||||||
|
builder.AddHttpMessageHandler<LoggingHandler>();
|
||||||
|
|
||||||
|
if (options.IncludeCorrelationId)
|
||||||
|
builder.AddHttpMessageHandler(() => new CorrelationIdHandler(options.CorrelationIdHeaderName));
|
||||||
|
|
||||||
|
if (options.UseBearerToken)
|
||||||
|
builder.AddHttpMessageHandler<BearerTokenAuthenticationHandler>();
|
||||||
|
|
||||||
|
if (options.Retry.Enabled)
|
||||||
|
builder.AddPolicyHandler(CreateRetryPolicy(options));
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHttpClientBuilder AddNamedNetClient(this IServiceCollection services, string name, Action<NetClientOptions> configure)
|
||||||
|
{
|
||||||
|
services.AddNetClientCore();
|
||||||
|
var options = new NetClientOptions();
|
||||||
|
configure(options);
|
||||||
|
|
||||||
|
var builder = services.AddHttpClient(name, client => ApplyClientOptions(client, options));
|
||||||
|
|
||||||
|
if (options.EnableLogging)
|
||||||
|
builder.AddHttpMessageHandler<LoggingHandler>();
|
||||||
|
|
||||||
|
if (options.IncludeCorrelationId)
|
||||||
|
builder.AddHttpMessageHandler(() => new CorrelationIdHandler(options.CorrelationIdHeaderName));
|
||||||
|
|
||||||
|
if (options.UseBearerToken)
|
||||||
|
builder.AddHttpMessageHandler<BearerTokenAuthenticationHandler>();
|
||||||
|
|
||||||
|
if (options.Retry.Enabled)
|
||||||
|
builder.AddPolicyHandler(CreateRetryPolicy(options));
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyClientOptions(HttpClient client, NetClientOptions options)
|
||||||
|
{
|
||||||
|
if (options.BaseAddress is not null)
|
||||||
|
client.BaseAddress = options.BaseAddress;
|
||||||
|
|
||||||
|
client.Timeout = options.Timeout;
|
||||||
|
|
||||||
|
foreach (var header in options.DefaultRequestHeaders)
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Remove(header.Key);
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IAsyncPolicy<HttpResponseMessage> CreateRetryPolicy(NetClientOptions options)
|
||||||
|
{
|
||||||
|
var retry = options.Retry;
|
||||||
|
|
||||||
|
var delay = Backoff.ExponentialBackoff(retry.FirstDelay, retry.MaxRetryAttempts, fastFirst: false);
|
||||||
|
|
||||||
|
var policyBuilder = HttpPolicyExtensions
|
||||||
|
.HandleTransientHttpError(); // 5xx + HttpRequestException + 408
|
||||||
|
|
||||||
|
if (retry.Handle429)
|
||||||
|
{
|
||||||
|
policyBuilder = policyBuilder.OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry.HandleRequestTimeout)
|
||||||
|
{
|
||||||
|
policyBuilder = policyBuilder.OrResult(msg => msg.StatusCode == HttpStatusCode.RequestTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
return policyBuilder.WaitAndRetryAsync(delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace NetClient.Exceptions;
|
||||||
|
|
||||||
|
public class ApiException : Exception
|
||||||
|
{
|
||||||
|
public HttpStatusCode StatusCode { get; }
|
||||||
|
public string? ResponseContent { get; }
|
||||||
|
public HttpResponseMessage Response { get; }
|
||||||
|
|
||||||
|
public ApiException(string message, HttpResponseMessage response, string? content = null, Exception? inner = null)
|
||||||
|
: base(message, inner)
|
||||||
|
{
|
||||||
|
Response = response;
|
||||||
|
StatusCode = response.StatusCode;
|
||||||
|
ResponseContent = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NetClient.Abstractions;
|
||||||
|
|
||||||
|
namespace NetClient.Handlers;
|
||||||
|
|
||||||
|
public class BearerTokenAuthenticationHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly IAccessTokenProvider _tokenProvider;
|
||||||
|
|
||||||
|
public BearerTokenAuthenticationHandler(IAccessTokenProvider tokenProvider)
|
||||||
|
{
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.Headers.Authorization is null)
|
||||||
|
{
|
||||||
|
var token = await _tokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace NetClient.Handlers;
|
||||||
|
|
||||||
|
public class CorrelationIdHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly string _headerName;
|
||||||
|
|
||||||
|
public CorrelationIdHandler(string headerName)
|
||||||
|
{
|
||||||
|
_headerName = headerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!request.Headers.Contains(_headerName))
|
||||||
|
{
|
||||||
|
request.Headers.Add(_headerName, Guid.NewGuid().ToString("N"));
|
||||||
|
}
|
||||||
|
return base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace NetClient.Handlers;
|
||||||
|
|
||||||
|
public class LoggingHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger<LoggingHandler> _logger;
|
||||||
|
|
||||||
|
public LoggingHandler(ILogger<LoggingHandler> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri);
|
||||||
|
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
sw.Stop();
|
||||||
|
_logger.LogDebug("HTTP {Method} {Uri} -> {Status} in {Elapsed} ms", request.Method, request.RequestUri, (int)response.StatusCode, sw.ElapsedMilliseconds);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
_logger.LogError(ex, "HTTP {Method} {Uri} failed after {Elapsed} ms", request.Method, request.RequestUri, sw.ElapsedMilliseconds);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NetClient.Abstractions;
|
||||||
|
using NetClient.Exceptions;
|
||||||
|
using NetClient.Internals.Url;
|
||||||
|
|
||||||
|
namespace NetClient.Implementations;
|
||||||
|
|
||||||
|
public class DefaultNetClient : IHttpClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly IHttpContentSerializer _serializer;
|
||||||
|
private readonly ILogger<DefaultNetClient> _logger;
|
||||||
|
|
||||||
|
public DefaultNetClient(HttpClient httpClient, IHttpContentSerializer serializer, ILogger<DefaultNetClient> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_serializer = serializer;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
|
||||||
|
=> _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<T?> GetAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, query);
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||||
|
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||||
|
await EnsureSuccess(resp).ConfigureAwait(false);
|
||||||
|
await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await _serializer.DeserializeAsync<T>(stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetStringAsync(string path, object? query = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, query);
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||||
|
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||||
|
await EnsureSuccess(resp).ConfigureAwait(false);
|
||||||
|
return await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> GetBytesAsync(string path, object? query = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, query);
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||||
|
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||||
|
await EnsureSuccess(resp).ConfigureAwait(false);
|
||||||
|
return await resp.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default)
|
||||||
|
=> SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Post, path, body, cancellationToken);
|
||||||
|
|
||||||
|
public Task<TResponse?> PutAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default)
|
||||||
|
=> SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Put, path, body, cancellationToken);
|
||||||
|
|
||||||
|
public Task<TResponse?> PatchAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default)
|
||||||
|
=> SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Patch, path, body, cancellationToken);
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string path, object? query = null, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, query);
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Delete, uri);
|
||||||
|
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||||
|
await EnsureSuccess(resp).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse?> PostMultipartAsync<TResponse>(string path, Action<MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, null);
|
||||||
|
using var content = new MultipartFormDataContent();
|
||||||
|
buildContent(content);
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content };
|
||||||
|
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||||
|
await EnsureSuccess(resp).ConfigureAwait(false);
|
||||||
|
await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await _serializer.DeserializeAsync<TResponse>(stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildUri(string path, object? query)
|
||||||
|
{
|
||||||
|
var q = QueryStringHelper.Build(query);
|
||||||
|
if (string.IsNullOrWhiteSpace(q)) return path;
|
||||||
|
return path.Contains('?') ? $"{path}&{q}" : $"{path}?{q}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureSuccess(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
if (response.IsSuccessStatusCode) return;
|
||||||
|
string? content = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
content = response.Content is null ? null : await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed reading error response content");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiException($"Request failed with status {(int)response.StatusCode}", response, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TResponse?> SendWithBodyAsync<TRequest, TResponse>(HttpMethod method, string path, TRequest body, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var uri = BuildUri(path, null);
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
await _serializer.SerializeAsync(ms, body, cancellationToken).ConfigureAwait(false);
|
||||||
|
ms.Position = 0;
|
||||||
|
using var content = new StreamContent(ms);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue(_serializer.ContentType) { CharSet = Encoding.UTF8.WebName };
|
||||||
|
using var req = new HttpRequestMessage(method, uri) { Content = content };
|
||||||
|
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
|
||||||
|
await EnsureSuccess(resp).ConfigureAwait(false);
|
||||||
|
await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return await _serializer.DeserializeAsync<TResponse>(stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NetClient.Abstractions;
|
||||||
|
|
||||||
|
namespace NetClient.Implementations;
|
||||||
|
|
||||||
|
public class NetClientFactory : INetClientFactory
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
|
||||||
|
public NetClientFactory(IServiceProvider services)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IHttpClient Create(string name)
|
||||||
|
{
|
||||||
|
var httpFactory = _services.GetRequiredService<System.Net.Http.IHttpClientFactory>();
|
||||||
|
var client = httpFactory.CreateClient(name);
|
||||||
|
var serializer = _services.GetRequiredService<IHttpContentSerializer>();
|
||||||
|
var logger = _services.GetRequiredService<ILogger<DefaultNetClient>>();
|
||||||
|
return new DefaultNetClient(client, serializer, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NetClient.Abstractions;
|
||||||
|
|
||||||
|
namespace NetClient.Internals.Serialization;
|
||||||
|
|
||||||
|
internal class SystemTextJsonContentSerializer : IHttpContentSerializer
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
public SystemTextJsonContentSerializer(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ContentType => "application/json";
|
||||||
|
|
||||||
|
public async Task SerializeAsync<T>(Stream stream, T? value, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(stream, value, _options, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NetClient.Internals.Url;
|
||||||
|
|
||||||
|
internal static class QueryStringHelper
|
||||||
|
{
|
||||||
|
public static string Build(object? query)
|
||||||
|
{
|
||||||
|
if (query is null) return string.Empty;
|
||||||
|
|
||||||
|
if (query is IEnumerable<KeyValuePair<string, object?>> pairs)
|
||||||
|
{
|
||||||
|
return BuildFromPairs(pairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dict = new Dictionary<string, object?>();
|
||||||
|
foreach (var prop in query.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||||
|
{
|
||||||
|
if (!prop.CanRead) continue;
|
||||||
|
var val = prop.GetValue(query);
|
||||||
|
dict[prop.Name] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildFromPairs(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildFromPairs(IEnumerable<KeyValuePair<string, object?>> pairs)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
void AppendPair(string key, string value)
|
||||||
|
{
|
||||||
|
if (sb.Length > 0) sb.Append('&');
|
||||||
|
sb.Append(Uri.EscapeDataString(key));
|
||||||
|
sb.Append('=');
|
||||||
|
sb.Append(Uri.EscapeDataString(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kv in pairs)
|
||||||
|
{
|
||||||
|
var key = kv.Key;
|
||||||
|
var val = kv.Value;
|
||||||
|
if (val is null) continue;
|
||||||
|
|
||||||
|
if (val is string s)
|
||||||
|
{
|
||||||
|
AppendPair(key, s);
|
||||||
|
}
|
||||||
|
else if (val is IEnumerable enumerable && val is not byte[])
|
||||||
|
{
|
||||||
|
foreach (var item in enumerable)
|
||||||
|
{
|
||||||
|
if (item is null) continue;
|
||||||
|
AppendPair(key, ConvertToString(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendPair(key, ConvertToString(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ConvertToString(object value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
DateTime dt => dt.ToString("o", CultureInfo.InvariantCulture),
|
||||||
|
DateTimeOffset dto => dto.ToString("o", CultureInfo.InvariantCulture),
|
||||||
|
bool b => b ? "true" : "false",
|
||||||
|
IFormattable f => f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty,
|
||||||
|
_ => value.ToString() ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace NetClient.Options;
|
||||||
|
|
||||||
|
public class NetClientOptions
|
||||||
|
{
|
||||||
|
public Uri? BaseAddress { get; set; }
|
||||||
|
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||||
|
public Dictionary<string, string> DefaultRequestHeaders { get; set; } = new();
|
||||||
|
|
||||||
|
public bool EnableLogging { get; set; } = true;
|
||||||
|
public bool IncludeCorrelationId { get; set; } = true;
|
||||||
|
public string CorrelationIdHeaderName { get; set; } = "X-Correlation-Id";
|
||||||
|
public bool UseBearerToken { get; set; } = false;
|
||||||
|
|
||||||
|
public RetryOptions Retry { get; set; } = new();
|
||||||
|
|
||||||
|
public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
WriteIndented = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NetClient.Options;
|
||||||
|
|
||||||
|
public class RetryOptions
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public int MaxRetryAttempts { get; set; } = 3;
|
||||||
|
public TimeSpan FirstDelay { get; set; } = TimeSpan.FromMilliseconds(500);
|
||||||
|
public int MaxJitterMs { get; set; } = 100;
|
||||||
|
public bool Handle429 { get; set; } = true;
|
||||||
|
public bool HandleRequestTimeout { get; set; } = true; // 408
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user