Introduction
In the base conforming container the ability to register multiple implementations of the same interface and request specific versions at runtime is not really possible without a lot of hoop jumping and pain. This is due to the nature of the underlying DI system and not all containers which can be plumbed in have the functionality or implement it in a similar fashion. With AutoFac it is more straight forward.
Keyed or Named dependencies allow for the same interface to be implemented by a number of different types and registered with the AutoFac container to then be requested by name or key when the dependency graph is constructed.
But can this functionality be used with Azure Functions?
TL;DR - Yes and no, and yes again. In this post I’ll show how to do it with minimal constructs but directly is possible. You’ll have to subscribe to my blog for a future post on how to do that!
Show me the code
The example code for this can be found in the named-depenencies branch.
Registering Dependencies
First we need to register the dependencies. In this example we are going to implement a simple interface which speaks a greeting.
public interface IGreeting
{
string Speak();
}
And then create two implementations of this interface.
First up HelloService
.
public class HelloService : IGreeting
{
public string Speak() => "Hello!";
}
And secondly a GoodByeService
.
public class GoodByeService : IGreeting
{
public string Speak() => "Goodbye!";
}
These now need to be registered with the container as part of the host builder process.
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterType<HelloService>().Keyed<IGreeting>("hello");
builder.RegisterType<GoodByeService>().Keyed<IGreeting>("goodbye");
})
Now they are registered we need some where to use them.
Consuming Service
As part of the consuming service definition we need to have two dependencies of IGreeting
but as we have two registrations we need to specify which to use. As we registered the dependencies with a Keyed
value we can use the KeyFilterAttribute
to decorate the constructor parameters.
public class MyService : IMyService
{
private readonly IGreeting _hello;
private readonly IGreeting _goodbye;
public MyService(
[KeyFilter("hello")] IGreeting hello,
[KeyFilter("goodbye")] IGreeting goodbye
)
{
_hello = hello;
_goodbye = goodbye;
}
public string Speak() => $"{_hello.Speak()} and {_goodbye.Speak()}";
}
We now have a service definition which is expecting two dependencies of the same interface but different registrations and hence implementations.
Calling Function
We can now use the IMyService
interface and inject that into the Azure Function to be able to call both greetings.
public class GetWelcome
{
private readonly IMyService _myService;
private readonly ILogger _logger;
public GetWelcome(IMyService myService, ILoggerFactory loggerFactory)
{
_myService = myService;
_logger = loggerFactory.CreateLogger<GetWelcome>();
}
[Function("GetWelcome")]
public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequestData req)
{
** snip for brevity **
response.WriteString($"{_myService.Speak()} Welcome to Azure Functions!");
return response;
}
}
We also need to register the service with the DI container. If you register the MyService
with the ServiceCollection directly and try and run the code you will recieve an AutoFac error saying it failed to activate MyService
.
[2021-11-19T07:47:46.931Z] Executed 'Functions.GetWelcome' (Failed, Id=78b79743-c0fb-4336-8556-f23ed45e2445, Duration=170ms)
[2021-11-19T07:47:46.933Z] System.Private.CoreLib: Exception while executing function: Functions.GetWelcome. System.Private.CoreLib: Result: Failure
Exception: Autofac.Core.DependencyResolutionException: An exception was thrown while activating Api.MyService.
[2021-11-19T07:47:46.934Z] ---> Autofac.Core.DependencyResolutionException: None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'Api.MyService' can be invoked with the available services and parameters:
[2021-11-19T07:47:46.934Z] Cannot resolve parameter 'Api.IGreeting hello' of constructor 'Void .ctor(Api.IGreeting, Api.IGreeting)'.
This is due to the fact we still haven’t told the container how to use the KeyFilter
attribute on the dependencies.
Attribute Filtering
As we have specified that the service has dependencies which are filtered we need to tell AutoFac that for the MyService
registration there needs to be a little more work done when registering the service. AutoFac allows for adding additional metadata to registrations to tell them more needs to be done. This is not added as default as depending on what you are doing it can be a performance hit. In my experience of using this functionality with ASP.NET Core it is not usually noticable and I’ve used it with some pretty big dependency graphs (don’t ask!). Due to this we need to specify the service registration as well as WithAttributeFiltering()
extension method to add that additional data to use.
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterType<HelloService>().Keyed<IGreeting>("hello");
builder.RegisterType<GoodByeService>().Keyed<IGreeting>("goodbye");
builder.RegisterType<MyService>().As<IMyService>().WithAttributeFiltering();
})
Full Program.cs
Here is the full Program class and registrations of all the services for completion.
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<MyService>().As<IMyService>().WithAttributeFiltering();
})
.ConfigureServices(services =>
{
})
.Build();
host.Run();
}
}
On running this the Azure Function will be resolved and executed with the MyService
instance resolved with both the hello and goodbye versions of IGreeting
and the output in a browser window will be:
Hello! and Goodbye! Welcome to Azure Functions!
Conclusion
In this post we have reviewed using Keyed registrations with AutoFac and how to use them with a .NET6 isolated mode Azure Function. We’ve had to put in an additional service layer, MyService
, for the functionality to work as expected which is a valid way of doing it. In a future post I will show how this can be done directly in an Azure Function so please subscribe to my RSS feed.
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.