I have an HTTP message handler named AddHeadersHandler, which extends System.Net.Http.DelegatingHandler and I need it to be added to all current and future HttpClient instances, including typed, named and non-named clients.
I know I can add a handler using .AddHttpMessageHandler<AddHeadersHandler>() for a specific client, but how do I add it to all clients?
// AddHeadersHandler.cs
public class AddHeadersHandler: DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.TryAddWithoutValidation("X-Correlation-Id", Guid.NewGuid.ToString());
return base.SendAsync(request, cancellationToken);
}
}
// Startup.cs
services
.AddHttpContextAccessor()
.AddTransient<AddHeadersHandler>();
services
.AddHttpClient<MyClient>()
.AddHttpMessageHandler<AddHeadersHandler>(); // I don't want to specify this for each client.
// MyClient.cs
public class MyClient
{
public HttpClient HttpClient { get; }
public MyClient(HttpClient httpClient)
{
HttpClient = httpClient;
}
public async Task GetTest()
{
await HttpClient.GetAsync("https://localhost:5001/test"); // This should have headers attached.
}
}
It can be done by configuring HttpClientFactoryOptions for all named options. We need to provide a delegate in HttpMessageHandlerBuilderActions, which will include your handler to AdditionalHandlers property list.
There are multiple ways of doing this using the Options pattern.
1. Using .AddSingleton() ❌
If your handler has any dependencies (ex. IHttpContextAccessor to get current correlation id), we would like to use dependency injection to resolve it.
We could use OptionsBuilder API to get a required handler using dependency injection. Unfortunately, OptionsBuilder API does not provide a method to configure options for all named instances like .ConfigureAll does.
Luckily, we can get what we need by registering a factory method for IConfigureOptions<HttpClientFactoryOptions> like so:
// Startup.cs
services.AddSingleton<IConfigureOptions<HttpClientFactoryOptions>>(provider =>
{
// When name is null, it will be used for all configurations.
return new ConfigureNamedOptions<HttpClientFactoryOptions>(name: null, options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
// Here we have access to ServiceProvider to get an instance of the handler.
builder.AdditionalHandlers.Add(provider.GetRequiredService<AddHeadersHandler>());
});
});
});
2. Using .ConfigureAll() ✔️
The following improved answer was inspired by LostInComputer.
Add .ConfigureAll in your Startup.cs and use IServiceProvider through builder object like so:
services.ConfigureAll<HttpClientFactoryOptions>(options =>
{
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
builder.AdditionalHandlers.Add(builder.Services.GetRequiredService<AddHeadersHandler>());
});
});
Related
I'm working on a Blazor WebAssembly application in .NET 6.0.
I'm using MediatR requests and handlers.
public class DummyRequest : IRequest<string>
{
public Guid Test { get; } = new Guid("e9f41a5d-5da6-4aad-b118-83476b7f40f4");
}
public class DummyHandler : IRequestHandler<DummyRequest, string>
{
private readonly HttpClient _httpClient;
public DummyHandler(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<string> Handle(DummyRequest request, CancellationToken cancellationToken)
{
// This should be the value configured in Program.cs
string baseAddress = _httpClient.BaseAddress?.AbsoluteUri ?? string.Empty;
// But it's always blank, so we can't make any calls with the HttpClient
await Task.CompletedTask;
return "foobar";
}
}
I'm configuring a different HttpClient for each request handler in Program.cs, then I'm adding MediatR:
builder.Services.AddHttpClient<DummyHandler>((client) => { client.BaseAddress = new Uri("https://api.somewhere.com"); });
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());
I have also tried reversing those calls, so that I add MediatR first, and register the HttpClient for the DummyHandler type afterwards.
At runtime, after that Handler has been instantiated, it should have an _httpClient with a BaseAddress property set to "https://api.somewhere.com".
However, it always gets an HttpClient with a null BaseUri, so the Handler can't use the HttpClient in any operations.
Can anybody see what's gone wrong please?
It seems that MediatR registers interface-implemetation pair so you need to follow the same pattern for the typed client registration. Try the following:
services.AddHttpClient<IRequestHandler<DummyRequest, string>, DummyHandler>((client) => { client.BaseAddress = new Uri("https://api.somewhere.com"); });
Gist with full test code.
Instead of a typed httpclient, you could use a named httpclient.
Thus register as
builder.Services.AddHttpClient("somename", client => { client.BaseAddress = new Uri("https://api.somewhere.com"); });
And in the constructor, inject the httpclientfactory instead:
public DummyHandler(HttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient("somename");
}
I suggest you to create the wrapper class around your Http client and register it instead.It hides implementation of your connection type and can be extended by other logic or other realization if you need.
Example:
class ApiConnection : IConnection
{
private readonly HttpClient _client;
public ApiConnection(...)
{
_client = new HttpClient();
}
// some other logic
}
Add this class to your Handler (IConnection connection) and use it in handler.
Register as: services.AddSingleton<IConnection, APIConnection>();
In case you are using the IRequestHandler<SomeCommand> alternative where there is no response, MediatR internally converts it to to IRequestHandler<SomeCommand, Unit>, which is what you will need to use to properly inject the HTTP client in your DI:
serviceCollection
.AddHttpClient<IRequestHandler<SomeCommand, Unit>, SomeCommandHandler>((httpClient) =>
{
...
});
I am working on a Blazor project, and to make the question I have easier to understand, we can say that I am using two different services that handles the Authentication part. Those are registered in the configureservices startup method together with a named httpclient.
services.AddHttpClient("XBOWServicesApi", c =>
{
c.BaseAddress = new Uri(XBOWServicesApi);
});
services.AddSingleton<IService1, Service1>();
services.AddSingleton<IService2, Service2>();
Service 1: Wraps all functionality available in a REST Api. It uses an http client which is set in the constructor via an instanciated httpclientfactory. This needs to be set with a baseurl and an Auth-header to work.
public Service1(IHttpClientFactory clientFactory)
{
this.httpClient = clientFactory.CreateClient("XBOWServicesApi");
}
Service 2: Handles the login/logout functionality using a custom AuthenticationStateProvider. It has its own httpclient, so that I can set the Auth Header for the http client. The constructor works in the same way as for Service 1.
public Service2(IHttpClientFactory clientFactory)
{
this.httpClient = clientFactory.CreateClient("XBOWServicesApi");
}
The reason for this build up is of course that I like to share the same http client, so when it is set in the login/logout methods, service 1 will have the correct auth header when communicating with the api.
However, the client factory provides a new instance everytime, so this will never work.
Any ideas how to handle this?
/Henrik
You can use named client:
services.AddHttpClient("github", c =>
{
c.BaseAddress = new Uri("https://api.github.com/");
// Github API versioning
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
// Github requires a user-agent
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
Afterwards, just call CreateClient method with corresponding name parameter.
var client = _clientFactory.CreateClient("github");
Each time CreateClient is called:
A new instance of HttpClient is created.
The configuration action is
called.
You can find more details in Microsoft documentation here.
When I read through the Microsoft IHttpClientFactory docs:
Each time you get an HttpClient object from the IHttpClientFactory, a
new instance is returned. But each HttpClient uses an
HttpMessageHandler that's pooled and reused by the IHttpClientFactory
to reduce resource consumption, as long as the HttpMessageHandler's
lifetime hasn't expired.
Does that answer your question?
You can share scoped services between transient HttpClients by using HttpMessageHandlers.
IHttpClient.CreateClient returns a new instance every time, but you can register a HttpMessageHandler as shown below:
services.AddScoped<HandlerData>();
services.AddTransient<HeaderHandler>();
services.AddHttpClient("XBOWServicesApi", c =>
{
c.BaseAddress = new Uri(XBOWServicesApi);
}).AddHttpMessageHandler<HeaderHandler>();
HeaderHandler Class:
public class HeaderHandler : DelegatingHandler
{
private readonly IHttpContextAccessor httpContextAccessor;
public HeaderHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken )
{
var Data= this.httpContextAccessor.HttpContext.RequestServices.GetRequiredService<HandlerData>();
request.Headers.Add(Data.HeaderName, Data.HeaderValue);
return base.SendAsync(request, cancellationToken);
}
}
HandlerData Class:
public class HandlerData
{
public string HeaderName { get; set; }
public string HeaderValue { get; set; }
}
ServicesCode:
public Service1(IHttpClientFactory clientFactory, HandlerData data)
{
data.HeaderName = "Header1";
data.HeaderValue = "Value";
this.httpClient = clientFactory.CreateClient("XBOWServicesApi");
}
public Service2(IHttpClientFactory clientFactory)
{
//This will contain the same headers as Service1 as HandlerData is Scoped Service
this.httpClient = clientFactory.CreateClient("XBOWServicesApi");
}
Alternatively, you can also use new IHttpMessageHandlerFactory if you need to create handlers that live in the same DI scope as you request:
Reference: https://github.com/aspnet/HttpClientFactory/issues/166
With the new HttpClientFactory in ASP.NET Core 2.1, it's quite easy to configure custom HTTP clients with things like base urls, default headers etc.
However, I haven't found a way to centralize configuration that lets me inject headers from the current request context. For example, consider a service called with an Authorization header, which I wish to pass on to all the underlying services as well. It would be awesome to be able to configure this in the .AddHttpClient() call on services in the Startup class, but I can't figure out how to get at the request context from there.
Any ideas?
Working on this answer lead me to multiple answers. I think the first approach is what you are looking for, the second is a good alternative.
In order to configure multiple clients you can use named clients. These clients are registered as transient. Use DI to get the service that has access to the request context.
For that we need IHttpContextAccessor. In this case you don't have to register it yourself, because Identity already does that for you.
Otherwise add the following line in startup:
services.AddHttpContextAccessor();
Next we can configure the named client "github":
services.AddHttpClient("github", c =>
{
// access the DI container
var serviceProvider = services.BuildServiceProvider();
// Find the HttpContextAccessor service
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
// Get the bearer token from the request context (header)
var bearerToken = httpContextAccessor.HttpContext.Request
.Headers["Authorization"]
.FirstOrDefault(h => h.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase));
// Add authorization if found
if (bearerToken != null)
c.DefaultRequestHeaders.Add("Authorization", bearerToken);
// Other settings
c.BaseAddress = new Uri("https://api.github.com/");
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
});
Call the client like this:
public class MyController : ControllerBase
{
private readonly IHttpClientFactory _clientFactory;
public MyController(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<ActionResult> StartCall()
{
var client = _clientFactory.CreateClient("github");
var response = await client.GetAsync("/repos/aspnet/docs/issues");
}
}
Another option is to use Typed clients. Here's a short example. For a full example check the link.
Register IHttpContextAccessor:
services.AddHttpContextAccessor();
Create a typed client. I've added two options to add settings. One through the request context and one through a singleton class:
public class GitHubService
{
public HttpClient Client { get; }
public GitHubService(HttpClient client, HttpClientSettings httpClientSettings, IHttpContextAccessor httpContextAccessor)
{
var bearerToken = httpContextAccessor.HttpContext.Request
.Headers["Authorization"]
.FirstOrDefault(h => h.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase));
// Add authorization if found
if (bearerToken != null)
client.DefaultRequestHeaders.Add("Authorization", bearerToken);
// Or the value from httpClientSettings:
client.DefaultRequestHeaders.Add("Authorization", httpClientSettings.BearerToken);
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning
client.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-agent
Client = client;
}
}
Register the client:
// The typed client is registered as transient with DI.
services.AddHttpClient<GitHubService>();
Please note, the code below is just an example. Since the token can't be persisted in the client, you can use the shared HttpClientSettings instead:
services.AddSingleton<HttpClientSettings>();
Where HttpClientSettings is:
public class HttpClientSettings
{
public string BearerToken { get; set; }
}
You can use the client like this:
public class MyController : ControllerBase
{
private readonly GitHubService _gitHubService;
public MyController(GitHubService gitHubService)
{
_gitHubService = gitHubService;
}
public async Task<ActionResult> StartCall()
{
var response = await _gitHubService.Client.GetAsync("/repos/aspnet/docs/issues");
}
}
Since .NET Core 3.0 you can use HeaderPropagation.
ConfigureServices in Startup.cs
services.AddHeaderPropagation(o =>
{
o.Headers.Add("Authorization");
});
services.AddHttpClient<YourTypedHttpClient>().AddHeaderPropagation();
Configure in Startup.cs
app.UseHeaderPropagation();
And this will automatically propagate Authorization header. You can also use it for other headers as well.
AutoRest generated client don't have suitable constructor for use it with services.AddHttpClient() method. So how can we get around this?
Now we have public constructor with such signature.
public Client(ServiceClientCredentials credentials, HttpClient httpClient, bool disposeHttpClient) : this(httpClient, disposeHttpClient)
But becouse it have bool disposeHttpClient argument we can't use it direct within AddHttpClient() method to configure client service into DI.
HttpClientFactory, to my deep regret, does not contain an override version of a method AddHttpClient with such a signature:
AddHttpClient<IClient>(Func<IServiceProvider, HttpClietn, IClient> configClient)
You'll need to use a named client, rather than a typed client, and then you'll need to register your AutoRest client using the factory overload.
services.AddHttpClient("MyAutoRestClient", c =>
{
// configure your HttpClient instance
});
services.AddScoped<MyAutoRestClient>(p =>
{
var httpClient = p.GetRequiredService<IHttpClientFactory>().GetClient("MyAutoRestClient");
// get or create any other dependencies
// set disposeHttpClient to false, since it's owned by the service collection
return new MyAutoRestClient(credentials, httpClient, false);
});
There is one more way that we can achieve. We can inherit from generated class and define for DI and AddHttpClient() constructor. See code below.
public partial class MyAutoRestClientExtended: MyAutoRestClient
{
public MyAutoRestClientExtended(HttpClient httpClient, IOptions<SomeOptions> options)
: base(new EmptyServiceClientCredentials(), httpClient, false)
{
var optionsValue = options.Value ?? throw new ArgumentNullException(nameof(options));
BaseUri = optionsValue .Url;
}
}
Now we can use AddHttpClient() method for configure typed client via fluent builder with all its benefits like Polly policies and HttpHandler defining.
services.AddHttpClient<MyAutoRestClientExtended>()
.ConfigureHttpClient((sp, httpClient) =>
{
httpClient.Timeout = TimeSpan.FromSeconds(30);
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.ConfigurePrimaryHttpMessageHandler(x => new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate })
.AddHttpMessageHandler(sp => sp.GetService<AuthenticationHandlerFactory>().CreateAuthHandler())
.AddPolicyHandlerFromRegistry(PollyPolicyName.HttpRetry)
.AddPolicyHandlerFromRegistry(PollyPolicyName.HttpCircuitBreaker);
Finally, define singleton service for service contract usage.
I want to find out how Polly retry polly configured via Startup.ConfigureServices() can be tested.
ConfigureServices
Polly policy is configured within it
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<IHttpClientService, HttpClientService>()
.SetWaitAndRetryPolicy1();
}
}
Below is the Polly policy:
public static class IServiceCollectionExtension
{
public static void SetWaitAndRetryPolicy1(this IHttpClientBuilder clientBuilder)
{
clientBuilder.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)),
onRetry: (outcome, timespan, retryCount, context) =>
{
service.GetService<ILog>().Error("Delaying for {delay}ms, then making retry {retry}.",
timespan.TotalMilliseconds, retryCount);
}
)
);
}
}
Below is what I tried:
Integration test
The Polly policy is configured within the test.
public class RetryPolicyTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public RetryPolicyTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("http://localhost:1234/api/v1/car/")]
public async Task Test3(string url)
{
// Arrange
var client = _factory.WithWebHostBuilder(whb =>
{
whb.ConfigureServices((bc, sc) =>
{
sc.AddOptions();
sc.AddHttpClient("test")
.SetWaitAndRetryPolicy1(); //Test the Polly policy
sc.BuildServiceProvider();
});
})
.CreateClient(); //cannot get a named or typed HttpClient
// Act
var body = "{}";
using (var content = new StringContent(body, Encoding.UTF8, "application/json"))
{
var response = await client.PostAsync(url, content);
}
//Assert: somewhy assert it
}
}
}
The problem is that
I cannot retrieve the HttpClient that has been configured with the Polly polly. Because WebApplicationFactory.CreateClient() has no overloads that returns a named or typed HttpClient:
Any idea?
Is there a better way to testing it?
ASPS.NET Core API 2.2
To modify your posted code minimally to obtain the named or typed HttpClient configured on HttpClientFactory, build the IServiceProvider, obtain the IHttpClientFactory and then obtain the configured client from IHttpClientFactory.
var configuredClient = sc.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient("test");
Many people consider the use of IServiceProvider like this to be a service-locator anti-pattern in production code; perhaps it is ok here in a test, to pull the specific item you want to unit-test out of the default app configuration. However, there are also shorter ways for a test to get a sample HttpClient configured on HttpClientFactory, without using a full WebApplicationFactory (see last part of answer).
For a full end-to-end integration test, testing how your app uses the configured policy, using WebApplicationFactory to exercise some endpoint of your app like http://localhost:1234/api/v1/car/:
You could - within the integration test - use a tool like Mountebank for .NET or HttpClientInterception to stub out the calls that the configured HttpClient makes, so that those calls return errors which you expect the policy to handle.
You could use the ability of WebHostBuilder.ConfigureServices(...) to modify the normal startup of your app, to make it easy to assert something to prove the policy was called. For example, you could configure a mock/fake ILog implementation, and assert that the ILog.Error(...) call in your onRetry delegate takes place.
For the shortest-possible, self-contained unit test to test a Polly policy configured on a given HttpClient configuration on HttpClientFactory, you could use a code pattern like below. This only uses IHttpClientFactory and the standard Microsoft DI infrastructure; no web host from ASP.NET.
public class HttpClientFactory_Polly_Policy_Test
{
[Fact]
public async Task Given_a_retry_policy_configured_on_a_named_client_When_call_via_the_named_client_Then_the_policy_is_used()
{
// Given / Arrange
IServiceCollection services = new ServiceCollection();
bool retryCalled = false;
HttpStatusCode codeHandledByPolicy = HttpStatusCode.InternalServerError;
const string TestClient = "TestClient";
services.AddHttpClient(TestClient)
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError()
.RetryAsync(3, onRetry: (_, __) => retryCalled = true))
.AddHttpMessageHandler(() => new StubDelegatingHandler(codeHandledByPolicy));
HttpClient configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient(TestClient);
// When / Act
var result = await configuredClient.GetAsync("https://www.doesnotmatterwhatthisis.com/");
// Then / Assert
Assert.Equal(codeHandledByPolicy, result.StatusCode);
Assert.True(retryCalled);
}
}
public class StubDelegatingHandler : DelegatingHandler
{
private readonly HttpStatusCode stubHttpStatusCode;
public StubDelegatingHandler(HttpStatusCode stubHttpStatusCode) => this.stubHttpStatusCode = stubHttpStatusCode;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(stubHttpStatusCode));
}
If the declarations of policies are pulled out to methods (like SetWaitAndRetryPolicy1() in your posted code), an approach like above provides a more unit-test-focused way to test them.