简体   繁体   中英

How can I use RateLimiter's HttpClient DelegatingHandler with dependency injection?

I've been using RateLimiter ( github ) successfully with my project for a while now. I've recently discovered dependency injection and am attempting to migrate my code as-is to use this but I'm stuck on RateLimiter.

Normal usage from the docs is

var handler = TimeLimiter
        .GetFromMaxCountByInterval(25, TimeSpan.FromMinutes(1))
        .AsDelegatingHandler();
var Client = new HttpClient(handler)

However if I try to replicate that during dependency injection

    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddHttpClient<IMyApiClient, MyApiClient>(client => client.BaseAddress = new Uri("https://api.myapi.com/"))
               .AddPolicyHandler(GetRetryPolicy())
               .AddHttpMessageHandler(() => TimeLimiter.GetFromMaxCountByInterval(25, TimeSpan.FromMinutes(1)).AsDelegatingHandler());
     }

I receive the error:

[2021-05-17T21:58:00.116Z] Microsoft.Extensions.Http: The 'InnerHandler' property must be null. 'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached.
[2021-05-17T21:58:00.117Z] Handler: 'ComposableAsync.DispatcherDelegatingHandler'.
[2021-05-17T21:58:00.122Z] An unhandled host error has occurred.

My class client structure (mostly for the sake of unit testing) looks like this:

using System.Net.Http;
using System.Threading.Tasks;

public class MyApiClient : HumbleHttpClient, IMyApiClient
{
    public MyApiClient(HttpClient client)
        : base(client)
    {
    }
}

public class HumbleHttpClient : IHttpClient
{
    public HumbleHttpClient(HttpClient httpClient)
    {
        this.Client = httpClient;
    }

    public HttpClient Client { get; }

    public Task<HttpResponseMessage> GetAsync(string requestUri)
    {
        return this.Client.GetAsync(requestUri);
    }

    public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
    {
        return this.Client.PostAsync(requestUri, content);
    }
}

public interface IHttpClient
{
    Task<HttpResponseMessage> GetAsync(string requestUri);
    Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
    HttpClient Client { get; }
}

These are the docs I've been trying to follow: https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests

I don't know if this is a bug or a feature, but to fix this I believe you need to add the delegating handler as Transient as well.

So, in a typical scenario, you'd set up a DelegatingHandler as so:

public sealed class MyDelegatingHandler : DelegatingHandler
{
    // ...
}

Then you need to register it; But here's the rub... You need to register the handler as Transient as well:

public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.AddHttpClient<IMyHttpService, MyHttpService>()
        .AddHttpMessageHandler<MyDelegatingHandler>();

    // this needs to be added:
    builder.Services.AddTransient<MyDelegatingHandler>();
}

With that being said, you are in a unique situation as the handler you want to use is created at runtime from a 3rd party. You can make this work... Not 100% if it's correct, but it should work:

public override void Configure(IFunctionsHostBuilder builder)
{
    var handler = TimeLimiter
        .GetFromMaxCountByInterval(25, TimeSpan.FromMinutes(1))
        .AsDelegatingHandler();

    builder.Services.AddHttpClient<IMyApiClient, MyApiClient>(
            client => client.BaseAddress = new Uri("https://api.myapi.com/"))
        .AddPolicyHandler(GetRetryPolicy())
        .AddHttpMessageHandler(() => handler);

    builder.Services.AddTransient(_ => handler);
}

Edited to Add...

The issue you are seeing is with the ComposableAsync Nuget package. Specifically this line . The code should not be setting this property. ASP.NET Core should/will do it. Luckily for us it's just a simple extension method.

Let's write our own extension that does it the right way:

using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace YourNamespace
{
    public static class DispatcherExtension
    {
        private sealed class DispatcherDelegatingHandler : DelegatingHandler
        {
            private readonly ComposableAsync.IDispatcher _Dispatcher;

            public DispatcherDelegatingHandler(ComposableAsync.IDispatcher dispatcher)
            {
                _Dispatcher = dispatcher;
            }

            protected override Task<HttpResponseMessage> SendAsync(
                HttpRequestMessage request,
                CancellationToken cancellationToken)
            {
                return _Dispatcher.Enqueue(() =>
                    base.SendAsync(request, cancellationToken), cancellationToken);
            }
        }

        public static DelegatingHandler AsDelegatingHandler(
            this ComposableAsync.IDispatcher dispatcher)
        {
            return new DispatcherDelegatingHandler(dispatcher);
        }
    }
}

Make sure to change your code to how I have it above with registering the hander. Also, make sure it's using the new extension instead of the one from the library.

I tested it and you can see it gets set properly:

处理程序注入

I would also consider opening a bug in the ComposableAsync github page.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM