Search code examples
c#asp.net-coreflurl

How to set HttpMessageHandler on Flurl calls globally without calling Configure()


I'm building a small library for internal use that relies on Flurl for handling all outgoing HTTP calls.

I want to provide the ability for consumers of this library to opt-in to our HTTP call tracing infrastrucure like so:

Startup.cs:

... 

public void ConfigureServices(IServiceCollection services)
{
    ....
    services.AddTracing(options => ...);
    ....
}

...

My current implementation of AddTracing() looks like this:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddTracing(this IServiceCollection services, Action<TracingOptions> configureOptions)
    {
        var tracingOptions = new TracingOptions();
        configureOptions(tracingOptions);

        // Make these options available in DI container
        services.AddSingleton(tracingOptions);

        FlurlHttp.Configure(settings =>
        {
            settings.HttpClientFactory = new TracingHttpClientFactory(tracingOptions.ApplicationName);
        });

        return services;
    }
}

And the current implementation of TracingHttpClientFactory looks like this:

public class TracingHttpClientFactory : DefaultHttpClientFactory
{
    private readonly string _applicationName;

    public TracingHttpClientFactory(string applicationName)
    {
        _applicationName = applicationName;
    }

    // override to customize how HttpMessageHandler is created/configured
    public override HttpMessageHandler CreateMessageHandler()
    {
        var tracingHandler = new TracingHandler(_applicationName, base.CreateMessageHandler());

        return tracingHandler;
    }
}

This works, but the issue I'm facing is that the documentation for Configure() reads: Should only be called once at application startup.

As a result, I've "wasted" my call to Configure() by adding tracing (which is optional). In scenarios where tracing is used, I also still need to call Configure() afterwards.

An example of when I might need to call configure afterwards would be in Startup.cs:

... 

public void ConfigureServices(IServiceCollection services)
{
    ....
    // Configure() is being called inside AddTracing()
    services.AddTracing(options => ...);
    ....
    // This is a second call to Configure()
    FlurlHttp.Configure(settings => {
        var jsonSettings = new JsonSerializerSettings
        {
            ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
        };
        settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
    });
}
...

Point being - each consumer of AddTracing() should be able to configure Flurl as they see fit. The point of AddTracing() is to simply "supercharge" Flurl with some extra, optional functionality. It isn't meant to take over Configure() - it's meant to extend it.

I've been reading through the docs here, and despite there being a number of places where configuration can occur, I can't work out a way to get my TracingHandler (which is a HttpMessageHander) into every request without calling Configure() somewhere.

Is there a suitable implementation for the scenario I've described?


Solution

  • The reason for the "call once at startup" advice is it touches global scope. If you start messing with the global settings in lots of different places, perhaps on different threads (e.g. in a controller action method), you could end up with weird race conditions and other unpredictable behavior.

    In your case, yes, you're calling Configure twice, but the calls are sequential, non-conflicting, and properly done where "startup" code belongs in an ASP.NET Core app. Most importantly, they're done before any calls are made with Flurl. So what you've done here is fine.