I have written a C# wrapper library that targets .net6,.net7,.net8. I want to add the DI support so that users can configure it by calling the services extension method AddMyApp.
namespace Project1
{
public class FooHttpClientOptions : IBasicAuthHttpClientOptions
{
public Uri Url { get; set; }
public BasicAuthCredential Credentials { get; set; }
}
public interface IBasicAuthHttpClientOptions
{
Uri Url { get; set; }
BasicAuthCredential Credentials { get; set; }
}
public class BasicAuthCredential
{
public const string Scheme = "x-api-key";
public string ApiKey { get; set; } = string.Empty;
}
public static class MyAppExtensions
{
public static IServiceCollection AddMyApp<THttpClient, THttpClientOptions>(
this IServiceCollection services,
Action<THttpClientOptions> configure) where THttpClient : class where THttpClientOptions : class, IBasicAuthHttpClientOptions
{
services
.AddOptions<THttpClientOptions>()
.Configure(configure)
.Validate(options => string.IsNullOrEmpty(options.Credentials.ApiKey)
|| string.IsNullOrWhiteSpace(options.Credentials.ApiKey), MyAppExceptionMessages.InvalidApiKeyMessage)
.ValidateOnStart();
services.AddTransient<BasicAuthenticationHandler<THttpClientOptions>>();
services.AddHttpClient<THttpClient>((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<THttpClientOptions>>().Value;
client.BaseAddress = options.Url;
})
.AddHttpMessageHandler<BasicAuthenticationHandler<THttpClientOptions>>();
services.AddTransient<ICustomHttpClient, CustomHttpClient>();
services.AddTransient<ICarsService, CarsService>();
return services;
}
}
public class BasicAuthenticationHandler<TOptions> : DelegatingHandler
where TOptions : class, IBasicAuthHttpClientOptions
{
private readonly TOptions _options;
public BasicAuthenticationHandler(IOptions<TOptions> options)
{
_options = options.Value;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue(BasicAuthCredential.Scheme, _options.Credentials.ApiKey);
return await base.SendAsync(request, cancellationToken);
}
}
public class MyAppExceptionMessages
{
public const string InvalidApiKeyMessage = "Your API key is invalid.";
}
}
Here is the CustomHttpClient class. I have applied the breakpoint in its constructor but the httpClient object does not contain BaseUrl and the request headers are empty as well.
public class CustomHttpClient : ICustomHttpClient
{
private readonly HttpClient _httpClient;
public CustomHttpClient (HttpClient httpClient)
{
_httpClient = httpClient;
}
}
Here's how I am calling the extension method:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IBasicAuthHttpClientOptions, FooHttpClientOptions>();
builder.Services.AddMyApp<CustomHttpClient, FooHttpClientOptions>(option =>
{
option.Url = new Uri("xxxxxxxxxxxxxx");
option.Credentials = new BasicAuthCredential
{
ApiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};
});
var app = builder.Build();
Why aren't the provided Url and ApiKey values reaching the httpClient object inside the CustomHttpClient?
You were pretty close to getting it right, yet two things were holding you back:
1. Registering
HttpClientfor service using implementation typeWhen this line gets executed:
a typed client is registered within the service collection under the name of the provided type.
Additionally, when you hover your mouse over
AddHttpClient()one of the sentences says:So, the conclusion we can draw is that your custom
HttpClientwill be bound with the implementation type because that's the type you are passing when calling:But, your
CustomHttpClientimplements aICustomHttpClientinterface and that's the type you are using when resolving it (which I assume). But noHttpClientis bound withICustomHttpClient, only withCustomHttpClient. You can fix that by adding a third type parameter for the interface and pass it down toAddHttpClient()extension method:2. Duplicated registration of
CustomHttpClientNow, at this point this might work, but it's highly probable you'll fall into a second pitfall. In the official documentation about
IHttpClientFactorythere is a registration example:with an important note:
That means that by calling
services.AddHttpClient<TService, THttpClient>()the extension method already registersTServicewith implementation beingTHttpClientfor you. So, if you call your own custom extension method:you'll end up with two registrations for
CustomHttpClient(orICustomHttpClientif you use the modified version from above). Below is a view of service collection from debugger:Registering the same service is allowed, but when resolving a single service, the last registration will be used and the last registration is not bound with
HttpClient. To fix it, remove this line:I said before this might work, because if you use a different client, it won't happen because the type passed will be different from
CustomHttpClient, but I suspect that you were testing that extension withCustomHttpClientlike this:Here's the final code of extension method:
PS I inverted API key validation logic in options, unless you want it to throw an exception when API key is null or empty.