-
Notifications
You must be signed in to change notification settings - Fork 53
Description
While converting my durable function to isolated in .NET7, I ran into a scenario that I'm pretty sure is caused by the fact that function class instances are constructed using something like ActivatorUtilities.CreateInstance instead of resolving the type from the container.
I have a simple activity that performs a DELETE Http call to one of my APIs:
[DurableTask]
public sealed class DeleteReservationTask : TaskActivityBase<Guid, HttpResponseMessage>
{
private readonly HttpClient httpClient;
public DeleteReservationTask(HttpClient httpClient)
=> this.httpClient = httpClient;
protected override async Task<HttpResponseMessage?> OnRunAsync(TaskActivityContext context, Guid input)
=> await this.httpClient.DeleteAsync($"Reservations/{input}");
}To configure this client, I'm using the AddHttpClient method like this:
services
.AddHttpClient<DeleteReservationTask>()
.ConfigureHttpClient((provider, client) =>
{
var settings = provider.GetRequiredService<IOptions<MyApiSettings>>();
client.BaseAddress = settings.Value.BaseAddress;
})When running the integration however, I get a service activation exception:
[2022-11-25T14:53:24.673Z] System.Private.CoreLib: Exception while executing function: Functions.DeleteReservationTask. System.Private.CoreLib: Result: Failure
Exception: System.AggregateException: One or more errors occurred. (Unable to resolve service for type 'System.Net.Http.HttpClient' while attempting to activate 'MyNamespace.DeleteReservationTask'.)
This is happening because AddHttpClient registers a named HttpClient instance that is resolved only when requesting the service it was registered with. Since the functions runtime is not taking the type from the container, and instead is building it up using ActivatorUtilities, it will ignore that registration and attempt to resolve a "nameless" HttpClient from the container when building the type, which will obviously never work.
This makes sense as a default, but I'd like to propose an alternative like the one used by MVC controllers that provide you with a AddControllersAsServices() extension that registers all controllers in the container directly, causing the controller activation logic to request the controller type from the container instead of building the type up manually with ActivatorUtilities.CreateInstance/CreateFactory. You could call it AddFuncitonsAsServices() or similar, for consistency. Once registered, function classes should then be resolved from the container instead of built-up on the fly, which would then respect whatever registrations are tied to it.
This of course affects other types of patterns other than the typed HttpClient registrations. Things such as decorators would also be impossible to register and use today if they target the function class type directly.
Another benefit of something like AddControllersAsServices is that it allows the DI container's built-in self-checking mechanism (enabled in debugging by default) to be able to detect missing dependencies before having to run each controller: when they are not registered as services, the container is oblivious to them and cannot perform this check for controller classes. The same benefit would exist for function classes as well.
As for workarounds if anyone is interested, I'll probably just inject the IHttpClientFactory and manually fetch the HttpClient from it using the class name (which is what happens automatically when the mechanism works as intended). Another workaround of course would be to create an indirection to a second WhateverService, that then relies on HttpClient and is injected into the function class.