Optional dependencies, is it a thing?
I was asked at work the other day if it was possible for a dependency to be optional. Now my default position is probably but I’d have to try and work it out. But also, why would you want to do that? So turns out that there is a valid reason.
The reason is the service or configuration which is being loaded in could be configurable depending on how/where the service is being used. If null is passed in, or an empty settings object, then everything is set one way. If you load in the optional dependency then the functionality could change. So potentially for client 1 you could specify X but for client 2 you could specify Y. The rest of the code is exactly the same.
So is it possible? Let’s find out! (Spoiler, yes it is!)
Setting the Scene
Let’s start with a simple service which has a dependency.
public class MyService
{
public MyService(IMyServiceDependency myServiceDependency)
{
Service = myServiceDependency;
}
public IMyServiceDependency Service { get; }
}
public interface IMyServiceDependency { }
public class MyServiceDependency : IMyServiceDependency { }
Let’s write a unit test to prove what we are trying to do and see if it works with explicit registration.
[Fact]
public void Resolution_Optional()
{
// Arrange
var builder = new ContainerBuilder();
builder.RegisterType<MyServiceDependency>().As<IMyServiceDependency>();
builder.RegisterType<MyService>();
var container = builder.Build();
// Act
var sut = container.Resolve<MyService>();
// Assert
sut.Should().NotBeNull();
sut.Service.Should().NotBeNull();
}
Now if we remove the explicit registration of IMyServiceDependency
and run the unit test again we will get the following exception and stack trace thrown.
Autofac.Core.DependencyResolutionException : An exception was thrown while activating XUnitTestProject2.MyService.
---- Autofac.Core.DependencyResolutionException : None of the constructors found with 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder' on type 'XUnitTestProject2.MyService' can be invoked with the available services and parameters:
Cannot resolve parameter 'XUnitTestProject2.IMyServiceDependency myServiceDependency' of constructor 'Void .ctor(XUnitTestProject2.IMyServiceDependency)'.
at Autofac.Core.Resolving.InstanceLookup.CreateInstance(IEnumerable`1 parameters)
at Autofac.Core.Resolving.InstanceLookup.Execute()
Essentially Autofac is saying “I’ve been told this constructor wants something which implements this interface but I’ve not been told about it”.
This is an issue.
Allowing for an optional dependency
So the short version is; this is totally possible. But how?
Allowing for optional dependencies are possible using more advance features of Autofac. Autofac as many extension points to allow for different dependency resolution workflows to be applied. A lot of these are based on a concept called AttributeFiltering
.
What is attribute filtering?
When setting up your dependencies for a service class the constructor parameters can be decorated with out of the box attributes or custom attributes depending on your requirements. This tells the Autofac container to work through additional constructs to find the derived dependencies. This is our way in.
Creating our Attribute
To allow for us to mess with the resolution “manually” we need to create a new Attribute deriving from ParameterFilterAttribute
. This construct allows us to determine if the attribute should allow for resolving the parameter from the details provided and then actually providing the details.
As we always want this parameter to execute then we return true
from the CanResolveParameter
. This method override is new in version 5.x of Autofac.
[AttributeUsage(AttributeTargets.Parameter)]
public class OptionalAttribute : ParameterFilterAttribute
{
public override object ResolveParameter(ParameterInfo parameter, IComponentContext context)
{
if (context.TryResolve(parameter.ParameterType, out object instance))
{
return instance;
}
return null;
}
public override bool CanResolveParameter(ParameterInfo parameter, IComponentContext context)
{
return true;
}
}
In the ResolveParameter
method we need to try and resolve the type of the parameter. If this is not possible then returning null
at this point is perfectly fine in this instance.
Once we have the attribute we need to do more steps.
- Decorate the parameter in the constructor which we want to be marked as optional.
public class MyService
{
public MyService([Optional] IMyServiceDependency myServiceDependency)
{
Service = myServiceDependency;
}
public IMyServiceDependency Service { get; }
}
- Specify when the service is registered with the Autofac container that it needs the additional
WithAttributeFiltering
extension method applied. This tells the Autofac container that additional work with the parameters will need to be done when resolving the service.
Let’s update our unit test to show this.
[Fact]
public void Resolution_Optional()
{
// Arrange
var builder = new ContainerBuilder();
// this applies the parameter attribute filtering
builder.RegisterType<MyService>().WithAttributeFiltering();
var container = builder.Build();
// Act
var sut = container.Resolve<MyService>();
// Assert
sut.Should().NotBeNull();
sut.Service.Should().BeNull();
}
This test now passes. We have the system under test instantiated with a null service injected.
Why would you want a dependency to be optional?
As I mentioned at the beginning of the post you may publish a system which runs with default values but can be configured by plugging in optional settings and services. You may have a core system but can be configurable per client due to different business rules for example.
As Autofac allows for registering dependencies through configuration without recompiling the application let’s take a look at what needs to be done to allow this.
Configuration based example
First we need some configuration. This example is quite light as there a number of different options which can be set through configuration. This is a basic example of how to register the same dependency as earlier but through configuration. This could easily be from a different project/dll which implements the required interface.
{
"autofac": {
"components": [
{
"type": "XUnitTestProject2.MyServiceDependency, XUnitTestProject2",
"services": [
{
"type": "XUnitTestProject2.IMyServiceDependency, XUnitTestProject2"
}
],
"autoActivate": true,
"injectProperties": true,
"instanceScope": "per-dependency"
}
]
}
}
To use this we need to load the configuration into a new ConfigurationModule
and register this with the Autofac container builder. This will run through the configuration specified and add the registrations to the container builder before the IoC container is built.
[Fact]
public void Resolution_Registered_ThroughConfig()
{
// Arrange
IConfigurationBuilder config = new ConfigurationBuilder();
config.AddJsonFile($"{Directory.GetCurrentDirectory()}\\OptionalInjectionTests.json");
var configuration = config.Build();
var builder = new ContainerBuilder();
builder.RegisterType<MyService>().WithAttributeFiltering();
ConfigurationModule configModule = new ConfigurationModule(configuration.GetSection("autofac"));
builder.RegisterModule(configModule);
var container = builder.Build();
// Act
var sut = container.Resolve<MyService>();
// Assert
sut.Should().NotBeNull();
sut.Service.Should().NotBeNull();
}
As you can see in the unit test above we load the configuration from the json file. If this is in an ASP.NET Core web application this can be added to the appsettings.json file and it will be loaded into the IConfiguration
which can be referenced and used in the ConfigureContainer
optional method you can add to Startup
.
Conclusion
In this post we have looked at how to make a dependency optional in Autofac without the resolution exception being raised. We have achieved this through creating a simple Autofac ParameterFilterAttribute
. And finally we looked at how to define the optional type through configuration so it could be used with different implementations of the interface which are only known at runtime.
Any questions/comments then please contact me on Twitter @WestDiscGolf