Required Keyword

C# 11 introduces a new keyword: required. This keyword can be applied onto fields or properties to make them required to be set during initialization. In this post I will focus on using required properties.

In the past developers could create init properties that could be set only at initialization time by the type's constructor or object initializer. However, the compiler did not require the developer to explicitly set a value to a property.

Required properties make object initializers issue a compiler error when the developer does not set a required property.

In this post I will explore the following topics for required properties:

  • Required Property Example

  • Order of Initialization Precedence

  • Nullability

  • Use-Cases and Limitations

  • Constructors and Derived Types

  • Serialization

  • IL Under the Hood

Required Property Example

A regular init (non-required) property could look like the following in C#:

public class Dto
{
  public string Value { get; init; }
}

The required counterpart of this property adds the required keyword to the declaration of the property:

public class Dto
{
  public required string Value { get; init; }
}

This will instruct the compiler to issue a warning if the developer would not set the property. For example, the following case omits setting the Value property:

Console.WriteLine(new Dto().Value);

The code above returns a compile error: Error CS9035 Required member 'Dto.Value' must be set in the object initializer or attribute constructor.. This compile error is also shown when the developer of Dto type provides a constructor to set the Value property inside the constructor. However, I will revisit setting required properties in constructors later in this post.

Order of Initialization Precedence

One may ponder about the question: what the order of precedence is when setting a value for required properties in constructor and object initializer as well. The following example answers the question:

Console.WriteLine(new Derived() { Value = "4" }.Value);

public class Dto
{
  public Dto()
  {
    Console.WriteLine(Value);
    Value = "2";
    Console.WriteLine(Value);
  }

  public required string Value { get; init; } = "1";
}

public class Derived : Dto
{
  public Derived()
  {
    Value = "3";
    Console.WriteLine(Value);
  }
}

The code snippet above prints the numbers in order: 1, 2, 3, 4 to the console. A type or a derived type may set a value in its constructor, but that will be overwritten by the value set in the object initializer.

Notice, that the property is init, which means we cannot change its value outside the initialization phase, but it changes its value 4 times during it. This could have negative performance implications in certain cases.

Nullability

Required properties do not enforce to have a non-null value set for the property. For reference types a developer can still set null, which satisfies the compiler. If the field is non-nullable reference type, the default behavior of the compiler is to issue a warning. To escalate nullability warnings to errors increase the severity of nullability warnings to errors by setting

<WarningsAsErrors>Nullable</WarningsAsErrors>

in the project's csproj file.

Use-Cases and Limitations

Typical use-cases for required types could be types that may prefer not to use constructors but still require properties to be set during initialization phase. DTO classes and entities could be good candidates. However, using required properties become increasingly useful along with init properties and non-nullable reference types, when nullability warnings are treated as errors.

I would particularly find useful to have types with required properties for IOption<T> from Microsoft.Extensions.Options namespace. However, IOption<T> has new() generic parameter constraint which is not supported by types with required properties.

Constructors and Derived Types

A type's constructor may be attributed with [SetsRequiredMembers]. This turns off required properties check when using this given constructor (and derived type's constructors using this constructor). However, such a constructor might be evil and omit setting some required properties. Another concern to notice: currently there is no way to tell that only some of the required properties would be set by a constructor attributed with [SetsRequiredMembers].

As noted above, when a constructor is not attributed with [SetsRequiredMembers], setting a value for a required property in the constructor will not satisfy the compiler, the developer will be still required to set a value when the object is initialized.

Serialization

As required properties is a new feature, not all serializers consider the required quality of a property. System.Text.Json serializer is aware of required properties. We can safely serialize and deserialize objects as:

var serialized = JsonSerializer.Serialize(new Dto() { Value = "3" });
Console.WriteLine(serialized);
var data = JsonSerializer.Deserialize<Dto>(serialized);
Console.WriteLine(data.Value);

For example, the above code snippet will correctly serialize and deserialize the object while printing the following text to the console:

{"Value":"3"}
3

When a message to be deserialized does not have a required property, an exception is thrown:

JsonSerializer.Deserialize<Dto>("""{"OtherValue":"3"}""");

The above code snippet results a JsonException: JSON deserialization for type 'Dto' was missing required properties, including the following: Value at runtime.

Using the Newtonsoft.Json serializer, no exception will be thrown, and the value of the property will be the default value, or any value set in a corresponding constructor of the object:

var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dto>("""{"OtherValue":"3"}""");

// Prints out the default value of the Value property.
Console.WriteLine(data.Value);

IL Under the Hood

How does required properties feature work? Is it a C# compiler feature or is it backed up by the CLR? So far, the observations suggest that it is a C# compiler feature:

  • [SetsRequiredMembers] attribute turns off validation completely at compile time

  • different serializers behave differently when there is no value passed for a required field

Decompiling the generated IL for Dto type results the following C#:

[RequiredMember]
public class Dto
{
    [RequiredMember]
    public string Value { get; init; }

    [Obsolete("Constructors of types with required members are not supported in this version of your compiler.", true)]
    [CompilerFeatureRequired("RequiredMembers")]
    public Dto() { }
}

A few interesting points compared to type without required properties:

  • Both the type and the property are attributed with [RequiredMember]

  • The default constructor is attributed with [Obsolete] which generated a compiler error when being used: this may tell existing tooling (that may be unaware of the required properties feature), that this constructor may not be used the regular way. This also prevents older compilers using these constructors incorrectly.

  • The default constructor is also attributed with [CompilerFeatureRequired("RequiredMembers")] which tells the compiler that this constructor must be used with the RequiredMembers feature during type initialization.

Can required properties make the code execute faster? In general, as this is a compiler feature, it should not. However, when comparing it with a type DtoRegular, which sets a string.Empty for the Value property to avoid the nullability warning/error, we may get surprising results.

public class DtoRegular
{
    public string Value { get; init; } = string.Empty;
}

Executing the following benchmarks, the results show that Dto with the required property executes faster:

    [Benchmark]
    public Dto Required() => new Dto() { Value = "3" };

    [Benchmark]
    public DtoRegular NotRequired() => new DtoRegular() { Value = "3" };
|      Method |     Mean |     Error |    StdDev |
|------------ |---------:|----------:|----------:|
|    Required | 3.461 ns | 0.0389 ns | 0.0364 ns |
| NotRequired | 4.442 ns | 0.1162 ns | 0.1030 ns |

Further investigation shows that a sizable portion of this difference is caused by having to set the string.Empty value for DtoRegular in its constructor, then the property needs to be set again by the object initializer.

When the string.Empty default property value is omitted for DtoRegular, the optimized (tier-1) JIT compilation results the same generated assembly code for both Dto and DtoRegular types.