Skip to content

Instantly share code, notes, and snippets.

@mt89vein
Last active November 25, 2024 18:32
Show Gist options
  • Select an option

  • Save mt89vein/fe67f6c811f20d4e150d0f77926ada93 to your computer and use it in GitHub Desktop.

Select an option

Save mt89vein/fe67f6c811f20d4e150d0f77926ada93 to your computer and use it in GitHub Desktop.
/// <summary>
/// Example of typed http client.
/// </summary>
public class MyHttpClient
{
/// <summary>
/// Request policy name.
/// </summary>
public const string GoogleRequestPolicy = nameof(MyHttpClient) + ":" + nameof(MakeGoogleRequestAsync);
/// <summary>
/// Http client factory.
/// </summary>
private readonly IHttpClientFactory _httpClientFactory;
/// <summary>
/// Creates new instance of <see cref="MyHttpClient"/>.
/// </summary>
/// <param name="httpClientFactory">HTTP client factory.</param>
public MyHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
/// <summary>
/// Executes request to google.
/// </summary>
/// <returns>Some response.</returns>
public async Task<string> MakeGoogleRequestAsync()
{
using var ctx = OutgoingHttpRequestContext.Create();
ctx.WithResiliencePipeline(GoogleRequestPolicy);
var httpClient = _httpClientFactory.CreateClient(nameof(MyHttpClient));
using var response = await httpClient.GetAsync(new Uri("https://google.com"));
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
/// <summary>
/// Outgoing http request context.
/// Use it to configure request.
/// </summary>
public sealed record OutgoingHttpRequestContext : IDisposable
{
/// <summary>
/// Parent context.
/// </summary>
private readonly OutgoingHttpRequestContext? _parentRequestContext;
/// <summary>
/// Current outgoing http request context.
/// </summary>
private static readonly AsyncLocal<OutgoingHttpRequestContext?> _currentRequestContext = new();
/// <summary>
/// Returns current outgoing http request context.
/// </summary>
public static OutgoingHttpRequestContext? Current => _currentRequestContext.Value;
/// <summary>
/// Any other additional props attached to outgoing request.
/// </summary>
public IDictionary<string, object?> Properties { get; }
/// <summary>
/// Creates <see cref="OutgoingHttpRequestContext"/>.
/// </summary>
/// <param name="properties">Any other additional props attached to outgoing request.</param>
/// <param name="parent">Parent context.</param>
private OutgoingHttpRequestContext(IDictionary<string, object?>? properties, OutgoingHttpRequestContext? parent)
{
Properties = properties ?? new Dictionary<string, object?>();
_parentRequestContext = parent;
}
/// <summary>
/// Creates <see cref="OutgoingHttpRequestContext"/>.
/// </summary>
public static OutgoingHttpRequestContext Create(IDictionary<string, object?>? properties = null)
{
return _currentRequestContext.Value = new OutgoingHttpRequestContext(properties, _currentRequestContext.Value);
}
/// <summary>
/// Adds property to context.
/// </summary>
public OutgoingHttpRequestContext WithProperty(string key, object? value)
{
Properties.TryAdd(key, value);
return this;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
_currentRequestContext.Value = _parentRequestContext;
}
}
/// <summary>
/// Extensions for <see cref="OutgoingHttpRequestContext"/>.
/// </summary>
public static class OutgoingHttpRequestContextExtensions
{
/// <summary>
/// The key where stored resilience pipeline name.
/// </summary>
private const string ResiliencePipelineKey = "ResiliencePipeline";
/// <summary>
/// Sets resilience pipeline for request.
/// </summary>
/// <param name="ctx">Outgoing http request context.</param>
/// <param name="pipelineName">The name of resilience pipeline.</param>
/// <returns>Outgoing http request context.</returns>
public static OutgoingHttpRequestContext WithResiliencePipeline(
this OutgoingHttpRequestContext ctx,
string pipelineName
)
{
return ctx.WithProperty(ResiliencePipelineKey, pipelineName);
}
/// <summary>
/// Returns resilience pipeline for request.
/// </summary>
/// <param name="ctx">Outgoing http request context.</param>
/// <param name="pipelineName">The name of resilience pipeline.</param>
/// <returns>True, if resilience pipeline configured.</returns>
public static bool TryGetResiliencePipeline(
this OutgoingHttpRequestContext ctx,
[NotNullWhen(returnValue: true)] out string? pipelineName
)
{
if (ctx.Properties.TryGetValue(ResiliencePipelineKey, out var value) && value is string s)
{
pipelineName = s;
return true;
}
pipelineName = null;
return false;
}
}
/// <summary>
/// Executes resilience pipeline by name.
/// </summary>
public sealed class PipelineExecuteDelegatingHandler : DelegatingHandler
{
/// <summary>
/// Resilience pipelines provider.
/// </summary>
private readonly ResiliencePipelineProvider<string> _provider;
/// <summary>
/// Creates new instance of <see cref="PipelineExecuteDelegatingHandler"/>.
/// </summary>
/// <param name="provider">Resilience pipelines provider.</param>
public PipelineExecuteDelegatingHandler(ResiliencePipelineProvider<string> provider)
{
_provider = provider;
}
/// <summary>
/// Executes resillience pipeline by name.
/// </summary>
/// <param name="request">Outgoing HTTP request.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
)
{
if (OutgoingHttpRequestContext.Current is null ||
!OutgoingHttpRequestContext.Current.TryGetResiliencePipeline(out var pipelineName))
{
return base.SendAsync(request, cancellationToken);
}
var pipeline = _provider.GetPipeline(pipelineName);
return pipeline
.ExecuteAsync((r, ct) => new ValueTask<HttpResponseMessage>(base.SendAsync(r, ct)), request,
cancellationToken)
.AsTask();
}
}
using Microsoft.AspNetCore.Mvc;
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using Sstv.HttpClient;
using Sstv.HttpClient.Example;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<MyHttpClient>();
var clientBuilder = builder.Services.AddHttpClient(nameof(MyHttpClient));
builder.Services.AddSingleton<PipelineExecuteDelegatingHandler>();
clientBuilder.AddHttpMessageHandler<PipelineExecuteDelegatingHandler>();
builder.Services.AddResiliencePipeline(MyHttpClient.GoogleRequestPolicy, b =>
{
var overallTimeout = TimeSpan.FromSeconds(20);
var attemptTimeout = TimeSpan.FromSeconds(5);
var attemptsCount = 2;
var concurrencyLimiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions { PermitLimit = 1 });
b.AddRateLimiter(concurrencyLimiter)
.AddTimeout(overallTimeout)
.AddRetry(
new RetryStrategyOptions
{
Name = "MyResiliencePipeline.Retry",
Delay = TimeSpan.FromMilliseconds(100),
MaxRetryAttempts = attemptsCount,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
MaxDelay = TimeSpan.FromMilliseconds(300),
})
.AddCircuitBreaker(
new CircuitBreakerStrategyOptions
{
Name = "MyResiliencePipeline.CircuitBreaker",
BreakDuration = TimeSpan.FromSeconds(5),
FailureRatio = 0.5,
MinimumThroughput = 10,
SamplingDuration = overallTimeout,
})
.AddTimeout(attemptTimeout);
});
var app = builder.Build();
app.MapGet("/", async ([FromServices] MyHttpClient httpClient) => Results.Ok(await httpClient.MakeGoogleRequestAsync()));
app.Run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment