Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# 8.0 non-nullable reference types and options pattern

Tl;dr: I want an options class that uses non-nullable types for its members with no defaults.

C# 8.0 introduces Nullable Reference Types.

I've found that using nullable reference types with the ASP.Net Options Pattern is rather difficult, incomplete, or that I am missing something. I am experiencing the same issue described in this stack over flow post.

  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)

I want an options class that uses non-nullable types for its members with no defaults. The answers in that post end up using nullable types anyway (which I am trying to avoid) or defaults (which I am also trying to avoid).

The comments about the options validation bring up good points and look promising, but it turns out that the Validate method still needs an options object to validate, which defeats the purpose if you already have to pass the options object into it.

public ValidateOptionsResult Validate(string name, MyOptions options)
 // Pointless if MyOptions options is being passed in here

This is pointless because I have determined that the only way to enforce an options class with all non-nullable members and no defaults is to have a constructor. Take the code sample below for example.

namespace SenderServer.Options
{
    using System;
    using Microsoft.Extensions.Configuration;

    /// <summary>
    /// Configuration options for json web tokens.
    /// </summary>
    public class JwtOptions
    {
        /// <summary>
        /// The secret used for signing the tokens.
        /// </summary>
        public String Secret { get; }

        /// <summary>
        /// The length of time in minutes tokens should last for.
        /// </summary>
        public Int32 TokenExpirationInMinutes { get; }

        /// <summary>
        /// Configuration options for json web tokens.
        /// </summary>
        /// <param name="secret"> The secret used for signing the tokens.</param>
        /// <param name="tokenExpirationInMinutes">The length of time in minutes tokens should last for.</param>
        public JwtOptions(String secret, Int32 tokenExpirationInMinutes)
        {
            Secret = secret;
            TokenExpirationInMinutes = tokenExpirationInMinutes;
        }

        /// <summary>
        /// Create a JwtOptions instance from a configuration section.
        /// </summary>
        /// <param name="jwtConfiguration">The configuration section.</param>
        /// <returns>A validated JwtOptions instance.</returns>
        public static JwtOptions FromConfiguration(IConfiguration jwtConfiguration)
        {
            // Validate the secret
            String? secret = jwtConfiguration[nameof(Secret)];
            if (secret == null)
            {
                throw new ArgumentNullException(nameof(Secret));
            }

            // Validate the expiration length
            if (!Int32.TryParse(jwtConfiguration[nameof(TokenExpirationInMinutes)], out Int32 tokenExpirationInMinutes))
            {
                throw new ArgumentNullException(nameof(TokenExpirationInMinutes));
            }

            if (tokenExpirationInMinutes < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(TokenExpirationInMinutes));
            }

            return new JwtOptions(secret, tokenExpirationInMinutes);
        }
    }
}

So if I need a constructor with the parameters for the class, then I can instantiate it on my own with something like:

// Configure the JWT options
IConfiguration jwtConfiguration = Configuration.GetSection("Authentication:JwtOptions");
JwtOptions jwtOptions = JwtOptions.FromConfiguration(jwtConfiguration); // This performs validation as well

but then where do I put the jwtOptions? None of the services.Configure<JwtOptions>(jwtOptions); and variants just take in an already-instantiated object (or at least none that I've seen). And lastly, even if they did, you can't use a dependency-injected options class that doesn't have a public parameter-less constructor.

public JwtService(IOptions<JwtOptions> jwtOptions)
like image 761
Montana Avatar asked Apr 25 '20 04:04

Montana


1 Answers

I want an options class that uses non-nullable types for its members with no defaults.

Then unfortunately, Microsoft.Extensions.Options simply isn’t for you. The way the Options works is by having a configuration pipeline of multiple sources, actions, and validators that all work with the same options object. Since there is no explicit beginning of this pipeline, and any configuration source can be at any position in the pipeline, the construction of the options object is handled by the framework and comes before any of the configuration sources is invoked.

This is strictly necessary in order for Options to allow the different kind of use cases it has: You can configure options from configuration (Microsoft.Extensions.Configuration), you can configure them through configuration actions, you can configure them through services that have additional dependencies, etc. And all of those can run in any order.

So since the construction of the object happens by the framework, there also need to be defaults that the options object gets created with: Usually, these are just the type’s default value but you can also choose different defaults through the object’s constructor.

If you want to enforce that specific parameters have been configured after the pipeline, you can use post-configure actions to enforce a configuration, or options validation to validate the configured options. But since this all runs in the pipeline, you need to have defaults.

So basically, if you need to have non-nullable properties without default values, then you cannot use Options. At least not out of the box. If you want to do this in order to safely reference the options in your services, then there would be a different way to approach this: Instead of injecting IOptions<T>, inject a non-nullable options object T directly. And have that provided through a factory:

services.AddSingleton<MySafeOptions>(sp =>
{
    var options = sp.GetService<IOptions<MyUnsafeOptions>>();
    return new MySafeOptions(options.Value);
});
services.Configure<MyUnsafeOptions>(Configuration.GetSection("MyOptions"));
like image 110
poke Avatar answered Oct 21 '22 09:10

poke