How to fully initialize types in their constructor with C# nullable using the async factory pattern
Here at endjin, we've recently enabled the new nullable references features in all of our projects. This has led me to a realisation that I often don't think as much as I should about object initialisation patterns. A couple of weeks ago I ran into an issue where I was trying to initalise an object, but the initialisation involved asynchronous operations so couldn't be done in a constructor.
When I first created the class I did this:
public class Thing
{
public string AString { get; private set; }
public static async Task<Thing> CreateThingAsync()
{
return new Thing()
{
AString = await GetStringAsync()
};
}
}
Where the asynchronous inititialisation is done via a factory method. However, this means that a set
function is required for the property, and also, when nullable references are enabled you get this warning:
This is because the AString
property is not initialised in the constructor and is therefore technically able to be null. It would be better to be able to make the property truly readonly, and to ensure that it is always initialised as non-null.
To do this we can use the async factory method pattern:
public class Thing
{
private Thing(string aString)
{
this.AString = aString;
}
public string AString { get; }
public static async Task<Thing> CreateThingAsync()
{
var aString = await GetStringAsync();
return new Thing(aString);
}
}
Here we have a constructor which takes a string
, which we use in the initialiser to produce a fully populated object. Also, as the constructor is private we know that the objects can only be initalisated from inside the Thing
class. And, as the constructor takes a non-nullable string, we will get a warning if we try and pass anything that could be null in.
For example, if the GetStringAsync
method returned a nullable string:
public static async Task<string?> GetStringAsync()
{
await Task.Delay(1).ConfigureAwait(false);
return null;
}
Then we would see this warning from the compiler when we try to pass it into the consructor:
This is a simple pattern but one that I think is incredibly useful in avoiding partial instantiation and null reference exceptions!