54

How can we use non-nullable reference types in combination with the Options pattern?

Let's say we have an options model named MyOptions.

The services requiring those options are getting IOptions<MyOptions> options injected into the constructor.

Configuring the options happens on the IServiceCollection like this:

services
    .AddOptions<MyOptions>()
    .Configure(options =>
    {
        options.Name = "ABC";
    });

Now, the problem is in the definition of MyOptions:

public sealed class MyOptions
{
    public string Name { get; set; }
}

which generates the warning:

CS8618 Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.

  1. We don't want to make Name nullable as then we need to place traditional null checks everywhere (which is against the purpose of non-nullable reference types)
  2. We can't create a constructor to enforce the MyOptions class to be created with a non-nullable name value as the Configure method construct the options instance for us
  3. We can't use the null-forgiving operator trick (public string name { get; set; } = null!;) as then we can't ensure the Name property is set and we can end up with a null in the Name property where this would not be expected (inside the services)

Any other option I forgot to consider?

18
  • 3
    Is string.Empty a viable option for your use case? I'm assuming your check for a populated value is to use string.IsNullOrWhiteSpace. Commented Sep 24, 2019 at 19:15
  • 3
    have you checked into IValidateOptions<TOptions>? learn.microsoft.com/en-us/dotnet/api/… Commented Sep 24, 2019 at 19:20
  • 2
    You're right, we still need to validate for empty strings (or other constraints). And I just found out we can chain an additional .Validate() call to the options registration method which might serve our needs. Thank you. Commented Sep 24, 2019 at 19:21
  • 2
    Yes, I agree. I think they were trying to maintain consistency with the existing "nullable value types" feature, but IMHO that was less important a goal than having a name that was a more accurate description of what the feature actually does. :) Commented Sep 24, 2019 at 19:31
  • 3
    @huysentruitw the question isn't really about the pattern, it's about its initialization mechanism which is no different than any deserializer. Configuration has the same issue - both mechanisms use property-based initialization and thus generate nullability errors. Both of them would have to use constructor-based initialization to avoid the problem. Commented Sep 25, 2019 at 9:03

4 Answers 4

11

It seems, that you have two possible options here. First one is to initialize an Options properties using empty string (instead of null value) to avoid null checks

public sealed class MyOptions
{
    public string Name { get; set; } = "";
}

Second one is to make all of the properties a nullable ones and decorate them using DisallowNull precondition and NotNull postcondition.

DisallowNull means that nullable input argument should never be null, NotNull - a nullable return value will never be null. But these attributes only affect nullable analysis for the callers of members that are annotated with them. So, you are indicating that your property can never return null or be set to null, despite nullable declaration

public sealed class MyOptions
{
    [NotNull, DisallowNull]public string? Name { get; set; }
}

and the usage example

var options = new MyOptions();
options.Name = null; //warning CS8625: Cannot convert null literal to non-nullable reference type.
options.Name = "test";

But the next example doesn't show a warning, because nullable analysis doesn't work properly in object initializers yet, see GitHub issue 40127 in Roslyn repository.

var options = new MyOptions { Name = null }; //no warning

(Edit: This issue was fixed already, shipped in version 16.5 in March, 2020 and should go away after updating a VS to the latest version.)

The same picture for property getter, the following sample doesn't show any warnings, because you indicated that nullable return type can't be null

var options = new MyOptions();
string test = options.Name.ToLower();

but attempting to set a null value and get it generates a warning (compiler is smart enough to detect such scenarios)

var options = new MyOptions() { Name = null };
string test = options.Name.ToLower(); //warning CS8602: Dereference of a possibly null reference.
Sign up to request clarification or add additional context in comments.

1 Comment

With NotNull attribute it rises "CS8603: Possible null reference return".
8

You should go with option 3). It does not matter whether the non-nullable atribute is null during the initialization process. What matters is the point of view of the consumer of the options instance later.

We can ensure that the options attribute will not be null by annotating it with a [Required] attribute and then calling ValidateDataAnnotations() on the options builder, such as:

public class MyOptions {
    [Required] public string MyRequiredText { get; set; } = null!;
    public string? MyOptionalText { get; set; };
}

services.AddOptions<MyOptions>()
    .Bind(Configuration.GetSection("MySettings"))
    .Configure(o => arbitrary configuration action here...)
    .ValidateDataAnnotations();

// When options are consumed from DI by `IOptions` or similar interfaces, 
// it is certain that MyRequiredText will not be null - in such case, exception will be thrown instead

When options are requested from DI and framework firstly creates the instance, it validates all attributes on properties after all registered configuration handlers are executed. If validation fails (e.g. required attribute is null or empty string), exception is thrown, which is what you should be after.

2 Comments

Just tried this and it works beautifully. Accidental misconfiguration is significantly reduced with this validation in place.
This answer with the addition of AddOptionsWithValidateOnStart instead of AddOptions works great.
5

C# 11 introduced the required keyword modifier as another, more elegant solution to this issue. It causes a compile-time check that ensures the field or property is initialized, thus removing the warning generated by having the property as a non-nullable reference type.

Your example code would simply become:

public sealed class MyOptions
{
    public required string Name { get; set; }
}

See the following links for more detailed information:

1 Comment

Options factory uses Activator under the hood, which appears not to respect required. So it's the same as the null forgiving operator in that it makes the compiler happy but doesn't actually verify the setter is called.
4

If the expected behavior of the property is that it may initially contain null but should never be set to null, then try using DisallowNullAttribute.

#nullable enable

using System.Diagnostics.CodeAnalysis;

public sealed class MyOptions
{
    [DisallowNull]
    public string? Name { get; set; }

    public static void Test()
    {
        var options = new MyOptions();
        options.Name = null; // warning
        options.Name = "Hello"; // ok
    }

    public static void Test2()
    {
        var options = new MyOptions();
        options.Name.Substring(1); // warning on dereference
    }
}

1 Comment

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.