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