C# 11 introduces the concept of required members. Any code instantiating the containing class must initialize these members; otherwise, the compiler will issue an error while compiling it.
In this article, let’s learn how to use this feature to prevent bugs and catch them early in our code.
Let’s start.
How to Use Required Members in C#
To mark a member as required, we add the required
modifier keyword to a property or field declaration.
Let’s see how we typically made classes that require us to initialize a property before required members existed:
public class OriginalClass { public string RequiredString { get; set; } public OriginalClass(string requiredString) { RequiredString = requiredString; } }
To ensure that we initialize theRequiredString
property upon instantiation, this class does not expose a default constructor. Instead, it takes the desired value of the property as a parameter and assigns it to RequiredString
.
We had to repeat this single property four times: twice referencing the property itself, and twice referencing the associated parameter.
Using the Default Constructor
Let’s rewrite the class with the same required property using the required
modifier:
public class ClassWithRequiredProperty { public required string RequiredString { get; set; } }
We are able to drop the explicit constructor whilst still ensuring that we initialize RequiredString
. That’s because now when we instantiate this class, the compiler forces us to initialize the RequiredString
property.
What’s more, even though we never initialize the property in the class, we don’t get an uninitialized non-nullable property warning. This is because the compiler knows it’s not possible to create an instance of this class without initializing the property.
Let’s instantiate it now using an object initializer:
var classWithRequiredProperty = new ClassWithRequiredProperty() { RequiredString = "Hello World" };
If we forget to initialize RequiredString
in the initializer, the compiler will issue an error.
Constructors that Set Required Members
The compiler doesn’t inspect constructors to check whether they initialize required members or not.
This means that even if we initialize a required property in a constructor, the compiler will still require the instantiating code to explicitly initialize it.
Let’s keep the default constructor in our class but bring back the original constructor as well:
public class ClassWithRequiredPropertyAndCustomConstructor { public required string RequiredString { get; set; } public ClassWithRequiredPropertyAndCustomConstructor() { } public ClassWithRequiredPropertyAndCustomConstructor(string requiredString) { RequiredString = requiredString; } }
If we try to create a new instance of this class, even if we specify an argument for the requiredString
parameter, the compiler will refuse to compile our code unless we explicitly assign RequiredString
using an object initializer:
var classWithRequiredPropertyAndCustomConstructor = new ClassWithRequiredPropertyAndCustomConstructor("Hello World") { RequiredString = "Hello World" };
To solve this, we can annotate the constructor with SetsRequiredMembersAttribute
in the System.Diagnostics.CodeAnalysis
namespace:
public class ClassWithRequiredPropertyAndAnnotatedCustomConstructor { public required string RequiredString { get; set; } public ClassWithRequiredPropertyAndAnnotatedCustomConstructor() { } [SetsRequiredMembers] public ClassWithRequiredPropertyAndAnnotatedCustomConstructor(string requiredString) { RequiredString = requiredString; } }
This will tell the compiler that the constructor sets all required members. Accordingly, it will not require the instantiating code to initialize the required members:
var classWithRequiredPropertyAndAnnotatedCustomConstructor = new ClassWithRequiredPropertyAndAnnotatedCustomConstructor("Hello World");
It’s important to keep in mind that the compiler doesn’t verify if a constructor actually sets all required members. If we forget to set a required member in the constructor, our code will still compile. The member will remain uninitialized.
Problems that Required Members Solve
As we saw earlier in this article, required members allow us to avoid repeating our code. This is good because, over time, our classes can grow large with many members. Eventually, the constructor can get out of sync with the actual class definition.
In addition to that, they also help us avoid warnings about uninitialized non-nullable properties without having to initialize them in each constructor.
Moreover, because nullable reference types and required members work in conjunction to statically determine whether a variable may be null at any given place in our code, they ensure we write null-safe code that will not throw a NullReferenceException
or other unexpected null-related errors.
Another related benefit is that they keep us from unintentionally creating classes with non-nullable value-typed properties set to their default value (e.g. 0
for an int
) if we forget to set such a property.
Required Members in Database Model Classes
The problems that required members solve are most apparent when we’re writing code that uses an ORM, like Entity Framework Core.
Instead of either marking all reference-typed properties as nullable or having to repeat all the properties in a constructor, we can simply mark them with required
.
With this, we can rely better on features like reflecting the nullability of a property in the associated database column. That’s because we no longer have to mark them as nullable just to suppress the uninitialized non-nullable parameter warning.
While one common workaround was to initialize the property with null!
, this meant that code creating an instance of the model class which forgot to initialize these variables would not result in an error or warning. This can lead to runtime database exceptions when trying to write to a non-nullable column.
Limitations of Required Members
While required members force instantiating code to initialize them, we are free to initialize them to any value—including null
.
In a nullable context (which is the default for new projects with the latest .NET version), absent the null-suppression operator !
, this will throw a warning. However, our code will still compile. Furthermore, if the null-suppression operator is present, there won’t even be a warning.
Because of this, it’s possible for a non-nullable, required property to nonetheless be null
.
Another case where required members aren’t necessarily fully initialized is in constructors annotated with SetsRequiredMembersAttribute
. Since the compiler doesn’t inspect the constructor to ensure that it really does set the required members, it’s possible for the member to remain uninitialized.
It’s also not possible for us to only set some required members in a constructor whilst leaving the rest to the instantiating code; if it’s marked with SetsRequiredMembersAttribute
, the instantiating code won’t be required to initialize any member, and if it isn’t, it will be required to initialize every member.
Conclusion
The ability to define required members in C# is a useful new feature that helps us write safer code with fewer lines. We must keep some limitations in mind, but this is a powerful feature that can significantly improve our code’s reliability. This is even more so the case when working with database models.