I have a custom HttpMessageHandler implementation:
public class MyHandler : DelegatingHandler
{
private readonly HttpClient _httpClient;
private readonly MyHandlerOptions _config;
public MyHandler(
HttpClient httpClient,
IOptions<MyHandlerOptions> options)
{
_httpClient = httpClient;
_config = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = await GetAccessToken()
return await base.SendAsync(request, cancellationToken);
}
private async Task<string> GetAccessToken()
{
//some logic to get access token using _httpClient and _config
}
}
It requires confiuration object MyHandlerOptions. Its form is not so important here. It basically contains clientId, clientSecret, etc. that are needed for the handler to know how to get the access token.
I have a few services (typed http clients) that need to use MyHandler:
//registration of MyHandler itself
builder.Services.AddHttpClient<MyHandler>();
//configuration of MyHandler
builder.Services.AddOptions<MyHandlerOptions>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandlerOptions").Bind(config);
});
//Services that need to use MyHandler:
services.AddHttpClient<Service1>()
.AddHttpMessageHandler<MyHandler>();
services.AddHttpClient<Service2>()
.AddHttpMessageHandler<MyHandler>();
services.AddHttpClient<Service3>()
.AddHttpMessageHandler<MyHandler>();
The problem is that the MyHandlerOptions instance that I registered is valid only when used with Service1. However, Service2 and Service3 require other configuration (different clientId, clientSecret, etc.). How can I achieve it?
The possible solution that comes to my mind:
Create a new service:
public class AccessTokenGetter
{
Task<string> GetAccessToken(AccessTokenConfig config)
{
//get the access token...
}
}
Create separate HttpMessageHandlers for each case where configuration is different:
public class MyHandler1 : DelegatingHandler
{
private readonly MyHandler1Options _config;
private readonly AccessTokenGetter _accessTokenGetter;
public MyHandler(AccessTokenGetter accessTokenGetter, IOptions<MyHandlerOptions1> options)
{
_accessTokenGetter = accessTokenGetter;
_config = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//somehow convert _config to AccessTokenConfig
request.Headers.Authorization = await _accessTokenGetter.GetAccessToken(_config)
return await base.SendAsync(request, cancellationToken);
}
}
public class MyHandler2 : DelegatingHandler
{
//same implementation as MyHandler1, just use MyHandler2Options instead
}
Register my services:
//configurations
builder.Services.AddOptions<MyHandler1Options>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandler1Options").Bind(config);
});
builder.Services.AddOptions<MyHandler2Options>()
.Configure<IConfiguration>((config, configuration) =>
{
configuration.GetSection("MyHandler2Options").Bind(config);
});
//AccessTokenGetter
services.AddHttpClient<AccessTokenGetter>()
//Services that need to use MyHandlers:
services.AddHttpClient<Service1>()
.AddHttpMessageHandler<MyHandler1>();
services.AddHttpClient<Service2>()
.AddHttpMessageHandler<MyHandler2>();
services.AddHttpClient<Service3>()
.AddHttpMessageHandler<MyHandler2>();
Is there a better solution? I am not a great fan of my idea, it is not very flexible.
services.AddHttpClient<Service1>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetRequiredService<MyHandler>();
handler.Foo = "Bar";
return handler;
});
services.AddHttpClient<Service2>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetRequiredService<MyHandler>();
handler.Foo = "Baz";
return handler;
});
Related
I've been trying to follow the directions from this blog post to pass an ILogger to my retry policy in order to log information about the errors being retried.
The code in the blog doesn't work out of the box as we're using Refit for client generation. Based on the refit docs it should just be a matter of adding a property to my method signatures, but haven't been able to get it to actually work.
Even though I've added the property to my method signature:
Task<UserSubscriptions> GetUserSubscriptions(string userId, [Property("PollyExecutionContext")] Polly.Context context);
I've captured logger management in extension methods:
private static readonly string LoggerKey = "LoggerKey";
public static Context WithLogger(this Context context, ILogger logger)
{
context[LoggerKey] = logger;
return context;
}
public static ILogger GetLogger(this Context context)
{
if (context.TryGetValue(LoggerKey, out object logger))
{
return logger as ILogger;
}
return null;
}
I create a new context when executing the method:
public Context GetPollyContext() => new Context().WithLogger(logger);
public Task<UserSubscriptions> GetUserSubscriptions(UserId userId) {
return restClient.GetUserSubscriptions(userId.UserIdString, GetPollyContext());
}
And try to access the logger as part of the retry action:
return Policy
.Handle<Exception>()
.OrResult<HttpResponseMessage>(r => CodesToRetry.Contains(r.StatusCode))
.WaitAndRetryAsync(3, retryCount => TimeSpan.FromSeconds(1), (result, timeSpan, retryCount, context) =>
{
var logger = context.GetLogger();
if (logger == null) return;
// do some logging
}
});
When I set a break point in the retry action the context that I see is a new empty context and not the one I created with the attached logger.
Per GitHub issues, there was a typo, the property is PolicyExecutionContext, not PollyExecutionContext.
Though given I don't need to generate a unique context per request, the better pattern is to use delegate injection.
Extension methods
private static readonly string LoggerKey = "LoggerKey";
public static Context WithLogger(this Context context, ILogger logger)
{
context[LoggerKey] = logger;
return context;
}
public static ILogger GetLogger(this Context context)
{
if (context.TryGetValue(LoggerKey, out object logger))
{
return logger as ILogger;
}
return null;
}
Delegate definition
public class PollyContextInjectingDelegatingHandler<T> : DelegatingHandler
{
private readonly ILogger<T> _logger;
public PollyContextInjectingDelegatingHandler(ILogger<T> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
var pollyContext = new Context().WithLogger(_logger);
request.SetPolicyExecutionContext(pollyContext);
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
Then add the delegate to the client definition
services
.AddTransient<ISubscriptionApi, SubscriptionApi>()
.AddTransient<PollyContextInjectingDelegatingHandler<SubscriptionApi>>()
.AddRefitClient<ISubscriptionApiRest>(EightClientFactory.GetRefitSettings())
.ConfigureHttpClient((s, c) =>
{
...
})
.AddHttpMessageHandler<PollyContextInjectingDelegatingHandler<SubscriptionApi>>()
.ApplyTransientRetryPolicy(retryCount, timeout);
I've an httpclient which uses Polly policy :
services.AddHttpClient("MyClient", (serviceProvider, client) =>
{
var X = External_PARAMETER;
client.BaseAddress = new Uri(settings.BaseUrl);
.....
})
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler((provider, httpRequestMessage) => GetTokenPolicy(provider, httpRequestMessage));
When creating the clienthttp from this named client, I want to dynamically pass some paramters to it like:
var httpClient = httpClientFactory.CreateClient("MyClient", External_PARAMETER);
I know that one solution is actually to use a singleton service like this and pass values:
As far as I know you can't use a transient or scoped service inside AddHttpClient definition. If I use a non-sington one I get this error (Cannot resolve scoped service 'MyApp.IMyService' from root provider.))
services.AddHttpClient("MyClient", (serviceProvider, client) =>
{
var passedServer = serviceProvider.GetService<IMyService>();
var X = passedServer.GetMyParameter();
client.BaseAddress = new Uri(settings.BaseUrl);
.....
})
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler((provider, httpRequestMessage) => GetTokenPolicy(provider, httpRequestMessage));
services.AddSingleton<IMyService, MyService>();
.
.
.
myServiceInstance.SetMyParameter("A_VALUE");
var httpClient = httpClientFactory.CreateClient("MyClient");
But how can I achieve this without using a singleton service like MyService?
There are cases where the HTTP request should create an HttpClient and having a sington service means all request will have access to this singleton service which is not ideal.
As far as I know, we could try to set the DelegatingHandler for httpclient and this DelegatingHandler could be registered as the Transient.
More details, you could refer to below example:
Handler:
public class TraceLogHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<TraceLogHandler> _logger;
private readonly bool _canLog;
private StringValues ipAddress;
public TraceLogHandler(IHttpContextAccessor httpContextAccessor, ILogger<TraceLogHandler> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public TraceLogHandler(IHttpContextAccessor httpContextAccessor, ILogger<TraceLogHandler> logger, bool canLog)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_canLog = canLog;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// here you could modify the http request
int i = 0;
return new HttpResponseMessage();
//blah blah code for custom logging
}
}
Startup.cs:
services.AddTransient<TraceLogHandler>();
services.AddHttpClient<ICountryRepositoryClient, CountryRepositoryClientV2>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://localhost"))
.AddHttpMessageHandler((services) =>
{
return new TraceLogHandler(services.GetRequiredService<IHttpContextAccessor>(),
services.GetRequiredService<ILogger<TraceLogHandler>>());
});
What you could try is Options pattern in .NET.
Instead of creating one singleton service providing the value, it enables you to create and access a set of named singleton settings, so you could create one setting per named HTTP client:
class MyHttpClientOptions
{
public string BaseUri { get; set; }
}
services.Configure<MyHttpClientOptions>("MyClient", opt =>
{
opt.BaseUri = "https://url1";
});
services.Configure<MyHttpClientOptions>("MyClient2", opt =>
{
opt.BaseUri = "https://url2";
});
You can access the named settings when creating the HttpClient:
services.AddHttpClient("MyClient", (serviceProvider, client) =>
{
var options = serviceProvider
.GetRequiredService<IOptionsMonitor<MyHttpClientOptions>>()
.Get("MyClient");
client.BaseAddress = new Uri(options.BaseUri);
});
UPDATE
If some values really have to be fetched from the request's context, and can't be set to the client from where it gets created, you could add a custom builder which uses IHttpContextAccessor:
services.AddHttpClient("MyClient1");
services.AddOptions<HttpClientFactoryOptions>("MyClient1")
.Configure<IHttpContextAccessor>((opt, httpContextAccessor) =>
{
opt.HttpClientActions.Add(client =>
{
var httpContext = httpContextAccessor.HttpContext;
// If you have scoped service to create in the request context
httpContext.RequestServices.GetRequiredService<MyScopedService>();
// Shared key value pairs
httpContext.Items["some key"]
});
});
I have the below Generic Class & Interface implementations
public interface IHttpClient<T> where T : class
{
public Task<List<T>> GetJsonAsync(string url);
}
public class HttpClient<T> : IHttpClient<T> where T:class
{
private readonly IHttpClientFactory _clientFactory;
public HttpClient(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
public async Task<List<T>> GetJsonAsync(string url)
{
var request = new HttpRequestMessage(HttpMethod.Get,url);
var client = _clientFactory.CreateClient();
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<T>>(result);
}
return null;
}
}
and this is how I try to register them in the startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped(typeof(IHttpClient<>), typeof(HttpClient<>));
}
My Controller:
private readonly IHttpClient<Feed> _feedClient;
public HomeController( IHttpClient<Feed> _client)
{
_feedClient = _client;
}
and this is the error I'm getting
InvalidOperationException: Unable to resolve service for type 'System.Net.Http.IHttpClientFactory' while attempting to activate...
what is it that I'm missing? any help is very appreciated..
You should register HttpClient in startup class like this
//register
services.AddHttpClient();
use
public YourController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
var client = _httpClientFactory.CreateClient();
Another options
//register
services.AddHttpClient("YourClientName", c =>
{
c.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
c.BaseAddress = new Uri("https://yoururl");
});
use
public YourController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
var client = _httpClientFactory.CreateClient("YourClientName");
I have a SessionService that has HttpClient injected into it and is registered as a Typed Client
See Microsoft example here.
I want to be able to write an integration test that I can control the responses made when the HttpClient is used.
I think that passing in a HttpMessageHandler to the HttpClient will allow me to intercept the request and control the response.
The problem I have is that I can't seem to add the HttpMessageHandler to the existing HttpClientFactory registration
// My client
public class SessionService
{
private readonly HttpClient httpClient;
public SessionService(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public async Task<Session> GetAsync(string id)
{
var httpResponseMessage = await this.httpClient.GetAsync($"session/{id}");
var responseJson = await httpResponseMessage.Content?.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Session>(responseJson);
}
}
// Live registrations
public static class HttpModule
{
public static IServiceCollection AddHttpModule(this IServiceCollection serviceCollection) =>
serviceCollection
.AddHttpClient<SessionService>()
.Services;
}
// My HttpMessageHandler which controls the response
public class FakeHttpMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
// customise response
return response;
}
}
If I try re-register the Typed Client so I can add the HttpMessageHandler it tells me I've already registered the client and can't register it again.
public static class TestHttpModule
{
public static IServiceCollection AddTestHttpModule(this IServiceCollection serviceCollection) =>
serviceCollection
.AddHttpClient<SessionService>() // <== Errors
.AddHttpMessageHandler<FakeHttpMessageHandler>()
.Services;
}
Any ideas?
The problem is because when you register a HttpClient using Dependency Injection, it adds it to an internal HttpClientMappingRegistry class. The fix was to remove the registration for the registry class. This allows me to re-add the Typed client and specify a HttpMessageHandler
public static class TestHttpModule
{
public static IServiceCollection AddTestHttpModule(this IServiceCollection serviceCollection) =>
serviceCollection
.AddSingleton(typeof(FakeHttpMessageHandler))
.RemoveHttpClientRegistry()
.AddHttpClient<SessionService>()
.AddHttpMessageHandler<FakeHttpMessageHandler>()
.Services;
private static IServiceCollection RemoveHttpClientRegistry(this IServiceCollection serviceCollection)
{
var registryType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.SingleOrDefault(t => t.Name == "HttpClientMappingRegistry");
var descriptor = serviceCollection.SingleOrDefault(x => x.ServiceType == registryType);
if (descriptor != null)
{
serviceCollection.Remove(descriptor);
}
return serviceCollection;
}
}
I'm using dependency injection with HttpClient and I'm trying to figure out how to set a baseurl, but can't seem to figure it out.
I'm doing it this way:
public static async Task<HttpResponseMessage> PostUser(User user) {
var services = new ServiceCollection();
services.UseServices();
var serviceProvider = services.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<IUserService>();
return await service.PostUser(user);
}
class UserService : IUserService
{
private readonly HttpClient _httpClient;
public UserService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<HttpResponseMessage> PostUser(User user)
{
HttpResponseMessage response = await _httpClient.PostAsJsonAsync(BASEURL, user);
return response;
}
}
I register in this way:
public static class Bootstrapper
{
public static void UseServices(this IServiceCollection services)
{
services.AddHttpClient<IUserService, UserService>();
}
}
So I want to use the BASEURL in the above, but how can I pass it with the httpClient?
This should work:
public static class Bootstrapper
{
public static void UseServices(this IServiceCollection services)
{
services.AddHttpClient<IUserServicee, UserService>(
client => client.BaseAddress = new Uri("YOUR_BASE_ADDRESS"));
}
}
Method overload takes Action<HttpClient> as an argument so it's void and you can mutate your HttpClient instance in the way you want.