I have a client that is created from an OpenApi definition that is generated using NSwagStudio. The code that is generated has an interface and a class. It takes an HttpClient in the constructor. I set this up for the DI engine like this.
services.AddScoped<IConsumerAdapterClient, ConsumerAdapterClient>(sp =>
{
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
var baseUrl = Configuration.GetSection("ConsumerAdapterConfig:EndpointBase").Value;
return new ConsumerAdapterClient(baseUrl,httpClientFactory.CreateClient());
});
But this doesn't seem quite right to me. After doing some searching, I found this article. Now that same code looks like this.
services.AddHttpClient<IConsumerAdapterClient, ConsumerAdapterClient>(client =>
{
var baseUrl = Configuration.GetSection("ConsumerAdapterConfig:EndpointBase").Value;
client.BaseAddress = new Uri(baseUrl);
});
Ok, that's better. But now I have to add security to the mix. To do that, I have a class called ClientCredentialProvider which does the work of getting an access token using client credential flow. I didn't want to modify the generated code (that's just asking for maintenance problems) so instead, I can take advantage of the fact that NSwagStudio generates classes as a partial class. This means I can create a new constructor and inject the security element into the class where I can then use it in the prepare method hooks.
public partial class ConsumerAdapterClient
{
private readonly IClientCredentialProvider _clientCredentialProvider;
public ConsumerAdapterClient(HttpClient httpClient, IClientCredentialProvider clientCredentialProvider)
{
_httpClient = httpClient;
_clientCredentialProvider = clientCredentialProvider;
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
}
protected async Task PrepareRequestAsync(HttpClient client, HttpRequestMessage request, StringBuilder url)
{
await PrepareRequestAsync(client, request, url.ToString());
}
protected async Task PrepareRequestAsync(HttpClient client, HttpRequestMessage request, string url)
{
if (_clientCredentialProvider != null)
{
var accessToken = await _clientCredentialProvider.GetAccessToken();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
protected async Task ProcessResponseAsync(HttpClient client, HttpResponseMessage response, CancellationToken cancellationToken)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
// nothing to do here.
}
}
That created a new problem in that it now complains about multiple constructors and it doesn't know which one to pick. After deleting the constructor from the generated code, it worked as expected.
So there were three problems here.
One was I had to remove the constructor from the generated code. This will create a maintenance issue but it's not a horrible one and trying to run a test will quickly reveal it.
Second, because I had to remove the constructor from the generated code, I had to move the initialization for _settings and copy it to the constructor in the partial class. That's a one-time thing so I can live with it.
The third is that I did this using a partial class. While that in and of itself is not a big deal, I have a lot of these client classes and would need to create the same thing over and over for each one.
So how can I achieve implementing this class without creating boilerplate code over and over again?
I took the partial class and turned it into a base class. I then told NSwagStudio about my base class so when the code got generated it would use the base class. The only problem I had now was that I couldn't overload the constructer. Now instead of injecting the ClientCredentialProvider, I was back to doing things the way I started but setting a property for the ClientCredentialProvider.
public class ClientServicesBase
{
private IClientCredentialProvider _clientCredentialProvider;
public IClientCredentialProvider ClientCredentialProvider
{
set => _clientCredentialProvider = value;
}
// The rest of the code is the same as above.
}
So to set it up, it looks like this.
services.AddScoped<IConsumerAdapterClient, ConsumerAdapterClient>(sp =>
{
var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
var clientCredentialProvider = sp.GetRequiredService<IClientCredentialProvider>();
var baseUrl = Configuration.GetSection("ConsumerAdapterConfig:EndpointBase").Value;
return new ConsumerAdapterClient(baseUrl,httpClientFactory.CreateClient())
{
ClientCredentialProvider = clientCredentialProvider
};
});
So this works and it does what I want it to do but it's not done in a way that represents Microsoft's recommended solution.
How do I:
Add the security element using the DI engine without having to extract the parameters from the service container and new'ing up a new instance of the class?