Abstract Factory Support for Microsoft .NET Dependency Injection
.NET Core’s built-in dependency injection support is easy to use and well-supported, but it’s quite feature-thin compared to some other IoC containers. To fill one important gap, I’ve implemented an abstract factory facility that works with MS DI, and published it via NuGet.
In cases where a service might need to create new instances of a dependency on the fly, rather than just use one injected instance throughout its lifetime, being able to inject a Factory for that dependency is the “right” way to do this while maintaining the paradigm of inversion of control. Without the ability to inject a factory, a service either needs to create new instances by calling the appropriate constructor (violating inversion of control), or the service needs to inject the service provider itself, giving it the ability to resolve any service it wants (violating the principle that types should explicitly declare their dependencies).
MS DI does not provide factory support out of the box, so to help with that I’ve implemented an abstract factory facility which can be added during application startup with a single extension method call, giving the service container the ability to resolve factories for any of its registered services, subject to some simple rules that I’ll go into below.
To get started in two steps, you first need to install the package from NuGet:
install-package Ariadne.Extensions.ServiceCollection
Then, you need to add a call to serviceCollection.AddFactoryFacility()
, but after all of your services have been registered.
Again, just to re-iterate, you need to add the factory facility after all your services - any services added after the factory facility won’t be resolvable via factories.
Using the Abstract Factory Facility
Now you’re ready to use abstract factory services, and you can see from the example below that they can be injected just like any other service dependency:
In the same way as injecting a regular dependency into the constructor, we can now inject IFactory<TService>
instead, then call New()
to return an instance of type TService
each time it is called. This means that where a service needs multiple instances of a dependency (such as for a connection that could only be used for one request), it can simply call New()
as many times as needed during the lifetime of the service.
If the dependency needs to be initialised with parameter values, another factory interfaceIFactory<T, TService>
is available to use. In this case, New(T arg)
takes a single argument (which could perhaps be a connection string or the id of a resource to connect to), and again the service can call this method as many times and with as many different values as it needs to during its lifetime.
Further factory interfaces for two and three arguments are also available: IFactory<T1, T2, TService>
and IFactory<T1, T2, T3, TService>
, these work in much the same way as the parameterised factory of one argument.
How It Works
For the full details of how the factory facility works you can browse the source code on GitHub, but here’s how IFactory<TService>
behaves:
Whenever you call New()
, the internal type that implements IFactory<TService>
resolves an instance of TService
from the main container. This is exactly as though you had called IServiceProvider.GetRequiredService<TService>()
, but the factory by design will only resolve its own return type. This means that as with other service injections, you can simply inspect the constructor parameters to understand what factory services a type uses.
There’s a little more to howIFactory<T, TService>
and the other parameterised factory interfaces work as we need a convention on how to pass the argument value to the constructor of the service. The simple rule I’ve fixed on is that TService
must have a constructor whose last parameter is of type T
. Having found a matching constructor, any other parameters are treated as dependencies and injected as they would be for any other service.
In the same way, for parameterised factories of 2 or more arguments, there must be a constructor whose last parameters match the argument types (in the correct order), with remaining parameters being resolved as dependencies.
Help and Support
The factory facility is designed to throw exceptions with useful messages when it can’t resolve a factory service for one reason or another, which will typically be either that a constructor matching the parameter types could not be found or that services for the remaining constructor parameters have not been registered.
Have fun, I hope this is of use, and let me know either here or on GitHub if you find any issues or want to suggest enhancements!