In this article, we will explain how to use a factory pattern with dependency injection in .NET.
Let’s dive in.
VIDEO: Using Factory Pattern With Dependency Injection.
Factory Pattern and Dependency Injection
The Factory Pattern is a widely recognized design pattern aimed at dissociating the creation of objects from their use. This pattern often encompasses a factory class or method which is tasked with the generation and provision of object instances. The main advantage of this technique is the abstraction of the object composition mechanism. This contributes to a more loosely coupled system and enhanced modularity.
That’s how the Factory Pattern helps us align with the Inversion of Control (IoC) principle.
When we talk about IoC, Dependency Injection (DI) comes into the discussion naturally. In the context of DI, the Factory Pattern provides separation of concerns by encapsulating the creation of complex dependencies. We can inject a factory object that creates and provides instances of the dependencies instead of explicitly injecting the dependencies.
The Factory Pattern in conjunction with DI offers many other benefits like managing object life cycles, dynamic dependency resolution, dynamic object composition, dependency optimization, etc. We are going to explore such capabilities in the realm of .NET Core dependency injection.
Parameterized Dependency Injection With Factory Pattern
Let’s assume we want to use a service class from a third-party library that generates dynamic labels for products, like so:
public class LabelGenService { public string Prefix { get; } public string Suffix { get; } public LabelGenService(string prefix, string suffix) { Prefix = prefix; Suffix = suffix; } public string Generate() { return $"{Prefix}{DateTime.Now:yyyyMMddHHmmssfff}{Suffix}"; } }
As we can see, LabelGenService
requires two constructor parameters – Prefix
and Suffix
. Let’s assume these values are consistent throughout our application and come from a configuration file. Such a service is a perfect candidate for singleton registration in the DI pipeline.
However, such simple constructor parameters are not meant for dependency injection as they don’t provide additional functionality beyond their immediate values. And since this is a third-party library, we can’t inject our own configuration dependency here.
So, how can we wire up such services in the DI pipeline? This is where Factory Pattern comes into play.
Entry Point to DI Pipeline
Let’s prepare our entry point to the DI pipeline in a helper class:
public class Helper { public static IServiceProvider CreateServiceProvider() { var host = Host.CreateDefaultBuilder() .ConfigureServices(ConfigureServices) .Build(); return host.Services; } private static void ConfigureServices(IServiceCollection services) { // TODO } }
We’ll utilize an IServiceProvider
container, which serves as the central component for working with dependency injection. To achieve this, we create a platform-agnostic default host builder. This builder instance provides a ConfigureServices
delegate, allowing us to register our services. As we construct the host instance, we gain access to the IServiceProvider
container.
Inside the ConfigureServices()
method, we register our services as required. First and foremost, we need a way to read desired parameter values from a configuration file (appsettings.json):
{ "LabelGenOptions": { "Prefix": "CD", "Suffix": "MZ" } }
We achieve this by binding this piece of the configuration (in the form of LabelGenOptions
) using the options pattern:
private static void ConfigureServices(IServiceCollection services) { services.AddOptions<LabelGenOptions>().BindConfiguration("LabelGenOptions"); }
Service Injection Using Factory Delegate
It’s time to register the LabelGenService
as a singleton:
private static void ConfigureServices(IServiceCollection services) { services.AddOptions<LabelGenOptions>().BindConfiguration("LabelGenOptions"); services.AddSingleton<LabelGenService>(serviceProvider => { var options = serviceProvider .GetService<IOptions<LabelGenOptions>>()!.Value; return new LabelGenService( options.Prefix, options.Suffix); }); }
We choose an overload of the AddSingleton
method that accepts a factory delegate. This delegate supplies a IServiceProvider
parameter and from there, we can easily resolve a LabelGenOptions
object and construct the LabelGenService
with the supplied values.
All set. Let’s generate a label using our DI-resolved LabelGenService
instance:
var service = _serviceProvider.GetService<LabelGenService>()!; var label = service.Generate(); Assert.StartsWith("CD", label); Assert.EndsWith("MZ", label);
Nothing stops the resolution of the service through DI! Also, the generated label perfectly reflects our pre-configured Prefix
and Suffix
values.
Abstraction Over Third Party Service Using a Factory Class
The factory delegate offers a quick approach to setting up a parameterized service. However, a cleaner and better alternative is to use a wrapper factory class, like so:
public class LabelGenServiceFactory { private readonly LabelGenService _labelGenService; public LabelGenServiceFactory(IOptions<LabelGenOptions> options) { var value = options.Value; _labelGenService = new(value.Prefix, value.Suffix); } public LabelGenService GetLabelGenService() => _labelGenService; }
And let’s complete its registration inside ConfigureServices
:
services.AddSingleton<LabelGenServiceFactory>();
In this factory class, we inject IOptions<LabelGenOptions>
in the constructor and instantiate a LabelGenService
instance with the values from options
. On every GetLabelGenService()
request to the factory class, we serve this local instance. This approach offers better encapsulation and a decoupled architecture.
By doing this, we get rid of the direct dependency on a third-party service. Instead, we now can inject/resolve our own factory dependency and get access to that service as needed:
var service = _serviceProvider.GetService<LabelGenServiceFactory>()! .GetLabelGenService(); var label = service.Generate(); Assert.StartsWith("CD", label); Assert.EndsWith("MZ", label);
We get the same output as before.
Conditional Object Instantiation
A factory class is also handy for conditionally creating objects based on runtime parameters.
Let’s consider a scenario where we have multiple device models, each associated with a specific DeviceType
value:
public class DeviceFactory : IDeviceFactory { private readonly LabelGenServiceFactory _labelFactory; public DeviceFactory(LabelGenServiceFactory labelFactory) { _labelFactory = labelFactory; } public Device CreateDevice(DeviceType deviceType) { var label = _labelFactory.GetLabelGenService().Generate(); return deviceType switch { DeviceType.Watch => new Watch(label), DeviceType.Phone => new Phone(label), DeviceType.Laptop => new Laptop(label), _ => throw new NotImplementedException() }; } }
We have three device types – Watch
, Phone
, and Laptop
– and each one inherits from Device
record. They all take on a Label
value that we provide through LabelGenServiceFactory
injection.
At its core, the DeviceFactory
class encapsulates the logic of switching between device instantiations based on the deviceType
parameter.
By registering this factory in the DI pipeline, we can seamlessly create different device records just by supplying the type:
var factory = _serviceProvider.GetService<DeviceFactory>()!; Assert.IsType<Laptop>(factory.CreateDevice(DeviceType.Laptop)); Assert.IsType<Watch>(factory.CreateDevice(DeviceType.Watch));
Conditional Service Resolution With Factory Pattern
Another potential use case of a factory class is to conditionally switch between different concrete implementations of a service. As an example, let’s consider we have three different implementations of IRelayService
.
The SandboxRelayService
:
public class SandboxRelayService : IRelayService { public RelayMode RelayMode => RelayMode.Sandbox; public string Relay(string message) => $"Sandbox: {message}"; }
The LiveRelayService
:
public class LiveRelayService : IRelayService { public RelayMode RelayMode => RelayMode.Live; public string Relay(string message) => $"Live: {message}"; }
And the OfflineRelayService
:
public class OfflineRelayService : IRelayService { public RelayMode RelayMode => RelayMode.Offline; public string Relay(string message) => $"Offline: {message}"; }
Each of these services defines logic for a specific relay mode. Although we have simplistic implementations here, in reality, they can be full-fledged services with their own dependencies. That also means they may need explicit registration in the DI pipeline:
private static void ConfigureServices(IServiceCollection services) { // omitted for brevity services.AddTransient<IRelayService, SandboxRelayService>(); services.AddTransient<IRelayService, LiveRelayService>(); services.AddTransient<IRelayService, OfflineRelayService>(); services.AddTransient<RelayServiceFactory>(); }
Right after the registration of all relay services, we wire up a factory class that aids us in resolving IRelayService
for a certain RelayMode
:
public class RelayServiceFactory { private readonly IEnumerable<IRelayService> _relayServices; public RelayServiceFactory(IEnumerable<IRelayService> relayServices) { _relayServices = relayServices; } public IRelayService GetRelayService(RelayMode relayMode) { return _relayServices.FirstOrDefault(e => e.RelayMode == relayMode) ?? throw new NotSupportedException(); } }
Interestingly, in the factory class, we can simply inject a IEnumerable<IRelayService>
which will automatically pull all IRelayService
implementations from the DI container. Consequently, the resolution of the target IRelayService
is just a matter of a simple lookup within this collection.
We can now pick any variant of IRelayService
on-demand:
var factory = _serviceProvider.GetService<RelayServiceFactory>()!; var liveRelay = factory.GetRelayService(RelayMode.Live); var sandboxRelay = factory.GetRelayService(RelayMode.Sandbox); Assert.Equal("Live: Demo", liveRelay.Relay("Demo")); Assert.Equal("Sandbox: Demo", sandboxRelay.Relay("Demo"));
Encapsulate Service Initialization
A service may need special initialization before it can be used, to free up locked resources, establish a database connection, etc.
Let’s take a simple example of a RecorderService
:
public class RecorderService { private bool _isDeviceReady = false; public void Initialize() { // Initialization of hardware connection goes here _isDeviceReady = true; } public string Record(string message) { if (!_isDeviceReady) throw new InvalidOperationException("Device is not ready"); return $"Recorded: {message}"; } }
Establishing the hardware connection is a different concern than the recording itself, but recording cannot continue until the device is ready. As a result, we have to initialize the device before recording. Such hardware initialization is not a good fit inside the constructor, therefore we expose a separate Initialize()
method. However, that also means that whenever we inject this service anywhere, we need to call Initialize()
explicitly from that calling code.
A factory class effectively abstracts away such responsibility from the client code:
public class RecorderServiceFactory { public RecorderService CreateRecorderService() { var service = new RecorderService(); service.Initialize(); return service; } }
The factory returns us an initialized instance of the RecorderService
, and as a result, we no longer need to worry about initialization from the calling code:
var factory = _serviceProvider.GetService<RecorderServiceFactory>()!; var recorder = factory.CreateRecorderService(); Assert.Equal("Recorded: Demo", recorder.Record("Demo"));
Abstract Factory With Dependency Injection
A factory may act as an abstraction over other factories, which we refer to as the Abstract Factory Pattern. This might be the case when we deal with groups of similar objects from different families, for example.
Let’s recall our DeviceFactory
class which lets us retrieve three kinds of devices – Laptop
, Phone
and Watch
. In addition, we now have the second generation of these products – SmartLaptop
, SmartPhone
and SmartWatch
.
That means we need a new factory:
public class SmartDeviceFactory : IDeviceFactory { private readonly LabelGenServiceFactory _labelFactory; public SmartDeviceFactory(LabelGenServiceFactory labelFactory) { _labelFactory = labelFactory; } public Device CreateDevice(DeviceType deviceType) { var label = _labelFactory.GetLabelGenService().Generate(); return deviceType switch { DeviceType.Watch => new SmartWatch(label), DeviceType.Phone => new SmartPhone(label), DeviceType.Laptop => new SmartLaptop(label), _ => throw new NotImplementedException() }; } }
This is nothing different from the previous DeviceFactory
, except that we now provide newer models. Both factories employ the same core operations, so we introduce a common interface of IDeviceFactory
. This interface is our abstraction over all such factory variants.
Let’s register them in the DI container:
private static void ConfigureServices(IServiceCollection services) { // omitted for brevity services.AddTransient<IDeviceFactory, DeviceFactory>(); services.AddTransient<IDeviceFactory, SmartDeviceFactory>(); services.AddTransient<MasterDeviceFactory>(); // omitted for brevity }
And bring them under the umbrella of a master factory:
public class MasterDeviceFactory { private readonly IEnumerable<IDeviceFactory> _deviceFactories; public MasterDeviceFactory(IEnumerable<IDeviceFactory> deviceFactories) { _deviceFactories = deviceFactories; } public IDeviceFactory GetClassicFactory() { return _deviceFactories.OfType<DeviceFactory>() .FirstOrDefault()!; } public IDeviceFactory GetSmartFactory() { return _deviceFactories.OfType<SmartDeviceFactory>() .FirstOrDefault()!; } }
We expose two separate methods to resolve the classic and smart variants independently.
We register this master factory in the DI container, and as a result, we are able to create a IDeviceFactory
variant as needed:
var masterFactory = _serviceProvider.GetService<MasterDeviceFactory>()!; var classic = masterFactory.GetClassicFactory(); var smart = masterFactory.GetSmartFactory(); Assert.IsType<Laptop>(classic.CreateDevice(DeviceType.Laptop)); Assert.IsType<SmartLaptop>(smart.CreateDevice(DeviceType.Laptop));
As expected, both device factories work in the same fashion but provide device instances from their own family line.
Conclusion
In this post, we have explored a few Factory Pattern use cases with dependency injection. The key purpose of the Factory Pattern is object creation, but in the context of DI, it also promotes loose coupling, encapsulation, and extensibility.