Introduction
As part of the last post we looked at how we can use the Keyed dependency functionality in Isolated .NET 6 Azure Functions. The implementation required a service abstraction which took the Keyed dependencies and then the Azure Function took a dependency on the service. This is a valid way of implementing this functionality with minimal fuss.
However as this requires an additional service class to be defined it could be seen as additional complexity to use the functionality. So is it possible to do it directly in an Azure Function without the need for an extra class?
In short; yes. Although as we will see moving through there are different hoops which need to be jumped through instead to make it work.
Show me the code
The example code for this can be found in the named-directly-in-function branch.
What is the issue?
Let’s take a step back and determine what the underlying problem is. We have an Azure Function definition which is defined as a Http end point due to the usage of the HttpTriggerAttribute
in the function parameter list. The worker runtime needs to find all of the functions and then instantiate them with their dependency tree when requested. This is done through an IFunctionActivator
implementation and out of the box this is done by the DefaultFunctionActivator
source here.
internal class DefaultFunctionActivator : IFunctionActivator
{
public object? CreateInstance(Type instanceType, FunctionContext context)
{
if (instanceType is null)
{
throw new ArgumentNullException(nameof(instanceType));
}
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return ActivatorUtilities.CreateInstance(context.InstanceServices, instanceType, Array.Empty<object>());
}
}
As you can see from the source it all comes down to the ActivatorUtilities.CreateInstance
call to create the instance of the function. This is a special entry point to resolve the dependency tree of the target instanceType
. This in our case is the target Azure function class. Due to the nature of this it uses the default Ioc container which in our context is not Autofac.
So how do we solve this?
ServiceBasedFunctionActivator
As part of the Dotnet Worker functionality it is all based on interfaces which are in most cases public. This allows for customizations as required by the use case. The functionality, as we can see above in the DefaultFunctionActivator
, has default implementations but we can swap them as required.
So what do we need to swap out and what do we need to implement?
Well actually after navigating around the Dotnet Worker repo linked above this isn’t as complicated as I was expecting. There are concepts of function invokers, function executors, function invoker factories etc. but after debugging through the code what it comes down to is implementing a different IFunctionActivator
version and replacing the default version at application start up.
public class ServiceBasedFunctionActivator : IFunctionActivator
{
public object? CreateInstance(Type instanceType, FunctionContext context)
{
_ = instanceType ?? throw new ArgumentNullException(nameof(instanceType));
_ = context ?? throw new ArgumentNullException(nameof(context));
return context.InstanceServices.GetService(instanceType);
}
}
As we can see from the DefaultFunctionActivator
implementation we already have access to the InstanceServices
property on the FunctionContext
instance passed in. This is used in the original implementation. What isn’t obvious from the name of the property is that it is the IServiceProvider
instance where as on first glace I had mistakenly taken it for the IServiceCollection
registrations. This makes a lot more sense and makes it easier to implement.
As we saw in “Using AutoFac with .NET 6 Isolated Azure Functions” once the Autofac Service Provider factory has been registered with the host builder the working implementation of IServiceProvider
is the Autofac one.
var host = new HostBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
As we have access to the service provider we can request the target service type, the actual Azure function class in this case, directly from the provider in our new ServiceBasedFunctionActivator
implementation.
context.InstanceServices.GetService(instanceType);
Registering Dependencies
Now we have the new ServiceBasedFunctionActivator
implementation we now need to register it with the Ioc container so that it is used instead of the default version.
This is done as part of the ConfigureService
registration process in the Host Builder setup.
.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Singleton<IFunctionActivator, ServiceBasedFunctionActivator>());
}
The Replace()
method is quite handy in that it will look at the target service type, IFunctionActivator
in this case, and it will find the first implementation of that type in the services collection and replace it with the specified service descriptor provided. This is very handy when wanting to do this type of replace default functionality without having to iterate over the collection manually etc.
Registering Azure Function with Autofac Container
This isn’t the end of the story. If you now ran the application it would still not resolve the Azure Function class as it would still not know about how to resolve the configured Keyed depencencies. So what are we missing?
ASP.NET Core has a similar issue with resolving Controllers. To resolve this issue there is an extension method AddControllersAsServices
on the IMvcBuilder
which is called at Startup. This method tells the builder to register all the controller classes with the Ioc container as transient services. Once this is done a service based controller activitor is used to resolve the controllers through the IServiceProvider
like any other dependency.
This is the same for Azure Functions however there is no helper extension method, and I’ve not figured out yet how it is possible to auto discover the functions, to register them. So for now we have to do this manually.
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterType<HelloService>().Keyed<IGreeting>("hello");
builder.RegisterType<GoodByeService>().Keyed<IGreeting>("goodbye");
builder.RegisterType<GetWelcome>().WithAttributeFiltering();
})
As in the earlier posts this registers the Keyed IGreeting
implementations and now also registers the Azure Function implementation with the Autofac builder and applies the WithAttributeFiltering()
extension method to the registration as we previously did with the MyService
registration previously.
Full Code
Program.cs
public class Program
{
public static void Main()
{
var host = new HostBuilder()
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureFunctionsWorkerDefaults()
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterType<HelloService>().Keyed<IGreeting>("hello");
builder.RegisterType<GoodByeService>().Keyed<IGreeting>("goodbye");
builder.RegisterType<GetWelcome>().WithAttributeFiltering();
})
.ConfigureServices(services =>
{
services.Replace(ServiceDescriptor.Singleton<IFunctionActivator, ServiceBasedFunctionActivator>());
})
.Build();
host.Run();
}
}
Azure Function
public class GetWelcome
{
private readonly IGreeting _hello;
private readonly IGreeting _goodbye;
private readonly ILogger _logger;
public GetWelcome(
[KeyFilter("hello")] IGreeting hello,
[KeyFilter("goodbye")] IGreeting goodbye,
ILoggerFactory loggerFactory)
{
_hello = hello;
_goodbye = goodbye;
_logger = loggerFactory.CreateLogger<GetWelcome>();
}
[Function("GetWelcome")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
var response = req.CreateResponse(HttpStatusCode.OK);
response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
response.WriteString($"{_hello.Speak()} and then {_goodbye.Speak()}");
return response;
}
Conclusion
In this post we have looked at how we can use Autofac’s Keyed dependencies directly in an Azure Function. We have looked at how this is possible by creating a custom implementation of IFunctionActivator
and how to register it.
I personally think Autofac is a brilliant Ioc container which is easy to plug into Azure Functions v4 Isolated model as well as ASP.NET Core. This allows for ease of customization for complex scenarios. It also allows for using the same common code structures and concepts between Azure Functions and ASP.NET Core applications if that is your thing.
Due to how easy it was to implement a ServiceBasedFunctionActivator
and swap it in as part of the service registration process I can see why it’s not part of the out of the box experience. I would like to see if a similar concept to AddControllersAsServices
is possible in the future though and this is something I want to investigate. I think an AddFunctionsAsServices
extension method would be very helpful.
What are you thoughts about using a different container than the built in one in Azure Functions? Is it overhead or welcomed configurability? Let me know on Twitter @WestDiscGolf as I’d be interested in hearing your thoughts.