With the release of .NET 5.0 and C# 9 coming up in the next month, I have been updating my Dependency Injection talk to incorporate the latest features of the framework and language.
One of the topics of the talk is using the “Gang of Four” Factory pattern to help with the creation of class instances where some of the data is coming from the DI container and some from the caller.
In this post, I walk through using the Factory Pattern to apply the same principles to creating instances of C# 9 records.
So How Do Records Differ from Classes?
Before getting down into the weeds of using the Factory Pattern, a quick overview of how C#9 record types differ from classes.
There are plenty of articles, blog posts and videos available on the Internet that will tell you this, so I am not going to go into much depth here. However, here are a couple of resources I found useful in understanding records vs classes.
Firstly, I really like the video from Microsoft’s Channel 9 ‘On.NET’ show where Jared Parsons explains the benefits of records over classes.
Secondly, a shout out to Anthony Giretti for his blog post https://anthonygiretti.com/2020/06/17/introducing-c-9-records/ that goes into detail about the syntax of using record types.
From watching/reading these (and other bits and bobs around the Internet), my take on record types vs. classes are as follows:
- Record types are ideal for read-only ‘value types’ such as Data Transfer Objects (DTOs) as immutable by default (especially if using positional declaration syntax)
- Record types automatically provide (struct-like) value equality implementation so no need to write your own boiler plate for overriding default (object reference) equality behaviour
- Record types automatically provide a deconstruction implementation so you don’t need to write your own boiler plate for that.
- There is no need to explicitly write constructors if positional parameters syntax is used as object initialisers can be used instead
The thing that really impressed me when watching the video is the way that the record syntax is able to replace a 40 line class definition with all the aforementioned boiler plate code with a single declaration
Why Would I Need A Factory?
In most cases where you may choose to use a record type over using a class type, you won’t need a factory.
Typically you will have some framework doing the work for you, be it a JSON deserialiser, Entity Framework or some code generation tool to create the boilerplate code for you (such as NSwag for creating client code from Swagger/OpenAPI definitions or possibly the new C#9 Source Generators).
Similarly, if all the properties of your record type can be derived from dependency injection, you can register your record type with the container with an appropriate lifetime (transient or singleton).
However, in some cases you may have a need to create a new record instance as part of a user/service interaction by requiring data from a combination of
- user/service input;
- other objects you currently have in context;
- objects provided by dependency injection from the container.
You could instantiate a new instance in your code, but to use Steve ‘Ardalis’ Smith‘s phrase, “new is glue” and things become complicated if you need to make changes to the record type (such as adding additional mandatory properties).
In these cases, try to keep in line with the Don’t Repeat Yourself (DRY) principle and Single Responsibility Principle (SRP) from SOLID. This is where a factory class comes into its own as it becomes a single place to take all these inputs from multiple sources and use them together to create the new instance, providing a simpler interaction for your code to work with.
Creating the Record
When looking at the properties required for your record type, consider
- Which parameters can be derived by dependency injection from the container and therefore do not need to be passed in from the caller
- Which parameters are only known by the caller and cannot be derived from the container
- Which parameters need to be created by performing some function or computation using these inputs that are not directly consumed by the record.
For example, a Product type may have the following properties
- A product name – from some user input
- A SKU value – generated by some SKU generation function
- A created date/time value – generated at the time the record is created
- The name of the person who has created the value – taken from some identity source.
The record is declared as follows
You may have several places in your application where a product can be created, so adhering to the DRY principle, we want to encapsulate the creation process into a single process.
In addition, the only property coming from actual user input is the product name, so we don’t want to drag all these other dependencies around the application.
This is where the factory can be a major help.
Building a Record Factory
I have created an example at https://github.com/stevetalkscode/RecordFactory that you may want to download and follow along with for the rest of the post.
In my example, I am making the assumption that
- the SKU generation is provided by a custom delegate function registered with the container (as it is a single function and therefore does not necessarily require a class with a method to represent it – if you are unfamiliar with this approach within dependency injection, have a look at my previous blog about using delegates with DI
- the current date and time are generated by a class instance (registered as a singleton in the container) that may have several methods to choose from as to which is most appropriate (though again, for a single method, this could be represented by a delegate)
- the user’s identity is provided by an ‘accessor’ type that is a singleton registered with the container that can retrieve user information from the current AsyncLocal context (E.g. in an ASP.NET application, via IHttpContextAccessor’s HttpContext.User).
- All of the above can be injected from the container, leaving the Create method only needing the single parameter of the product name
(You may notice that there is not an implementation of GetCurrentTimeUtc. This is provided directly in the service registration process in the StartUp class below).
In order to make this all work in a unit test, the date/time generation and user identity generation are provided by mock implementations that are registered with the service collection. The service registrations in the StartUp class will only be applied if these registrations have not already taken place by making user of the TryAddxxx extension methods.
Keeping the Record Simple
As the record itself is a value type, it does not need to know how to obtain the information from these participants as it is effectively just a read-only Data Transfer Object (DTO).
Therefore, the factory will need to provide the following in order to create a record instance:
- consume the SKU generator function referenced in its constructor and make a call to it when creating a new Product instance to get a new SKU
- consume the DateTime abstraction in the constructor and call a method to get the current UTC date and time when creating a new Product instance
- consume the user identity abstraction to get the user’s details via a delegate.
The factory will have one method Create() that will take a single parameter of productName (as all the other inputs are provided from within the factory class)
Hiding the Service Locator Pattern
Over the years, the Service Locator pattern has come to be recognised as an anti-pattern, especially when it requires that the caller has some knowledge of the container.
For example, it would be easy for the ProductFactory class to take a single parameter of IServiceProvider and make use of the GetRequiredService<T> extension method to obtain an instance from the container. If the factory is a public class, this would tie the implementation to the container technology (in this case, the Microsoft .NET Dependency Injection ‘conforming’ container).
In some cases, there may be no way of getting around this due to some problem in resolving an dependency. In particular, with factory classes, you may encounter difficulties where the factory is registered as a singleton but one or more dependencies are scoped or transient.
In these scenarios, there is a danger of the shorter-lived dependencies (transient and scoped) being resolved when the singleton is created and becoming ‘captured dependencies’ that are trapped for the lifetime of the singleton and not using the correctly scoped value when the Create method is called.
You may need to make these dependencies parameters of the Create method (see warning about scoped dependencies below), but in some cases, this then (mis)places a responsibility onto the caller of the method to obtain those dependencies (and thus creating an unnecessary dependency chain through the application).
There are two approaches that can be used in the scenario where a singleton has dependencies on lesser scoped instances.
Approach 1- Making the Service Locator Private
The first approach is to adopt the service locator pattern (by requiring the IServiceProvider as described above), but making the factory a private class within the StartUp class.
This hiding of the factory within the StartUp class also hides the service locator pattern from the outside world as the only way of instantiating the factory is through the container. The outside world will only be aware of the abstracted interface through which it has been registered and can be consumed in the normal manner from the container.
Approach 2 – Redirect to a Service Locator Embedded Within the Service Registration
The second way of getting around this is to add a level of indirection by using a custom delegate.
The extension methods for IServiceCollection allow for a service locator to be embedded within a service registration by using an expression that has access to the IServiceProvider.
To avoid the problem of a ‘captured dependency’ when injecting transient dependencies, we can register a delegate signature that wraps the service locator thus moving the service locator up into the registration process itself (as seen here) and leaving our factory unaware of the container technology.
At this point the factory may be made public (or left as private) as the custom delegate takes care of obtaining the values from the container when the Create method is called and not when the (singleton) factory is created, thus avoiding capturing the dependency.
Special warning about scoped dependencies
Wrapping access to transient registered dependencies with a singleton delegate works as both singleton and transient instances are resolved from the root provider. However, this does not work for dependencies registered as scoped lifetimes which are resolved from a scoped provider which the singleton does not have access to.
If you use the root provider, you will end up with a captured instance of the scoped type that is created when the singleton is created (as the container will not throw an exception unless scope checking is turned on).
Unfortunately, for these dependencies, you will still need to pass these to the Create method in most cases.
If you are using ASP.NET Core, there is a workaround (already partially illustrated above with accessing the User’s identity).
IHttpContextAcessor.HttpContext.RequestServices exposes the scoped container but is accessible from singleton services as IHttpContextAcessor is registered as a singleton service. Therefore, you could write a delegate that uses this to access the scoped dependencies via a delegate. My advice is to approach this with caution as you may find it hard to debug unexpected behaviour.
Go Create a Product
In the example, the factory class is registered as a singleton to avoid the cost of recreating it every time a new Product is needed.
The three dependencies are injected into the constructor, but as already mentioned the transient values are not captured at this point – we are only capturing the function pointers.
It is within the Create method that we call the delegate functions to obtain the real-time transient values that are then used with the caller provided productName parameter to then instantiate a Product instance.
I’ll leave things there, as the best way to understand the above is to step through the code at https://github.com/stevetalkscode/RecordFactory that accompanies this post.
Whilst a factory is not required for the majority of scenarios where you use a record type, it can be of help when you need to encapsulate the logic for gathering all the dependencies that are used to create an instance without polluting the record declaration itself.