Options pattern in ASP.NET Core: Advance scenario

ยท

5 min read

Options pattern in ASP.NET Core:  Advance scenario

In this article we are going to cover some advance scenarios where Options pattern is very useful.

We have covered the basic understanding of Options pattern into Options pattern in ASP.NET Core: Introduction article. Please go through it if not familiar with it.

We will cover below scenarios which are handled using Options pattern:

  • Validation of configuration settings using data annotation.

  • Validation of configuration settings using custom validation logic.

  • Validation of configuration settings during application startup.

  • Handling command line arguments or environment variables using options pattern.

  • Monitoring the changes of configuration settings.

Let's dive directly into code to understand all how Options pattern is used to handle all these scenarios.

GitHub repo link for the code sample https://github.com/dkj468/OptionsPatternDemo

Validation of configuration settings using data annotation

Options pattern allows to bind the DataAnnotations rules to class which represents the section of application settings. This is powerful mechanism to validate for non empty, range and many more things.

consider the FileOptions class from our first article :

    public class FileOptions {
        public const string Key = "file";

        [Required(AllowEmptyStrings =false)]
        public string MaxSize { get; set; }
        public string FileType { get; set; }
        public bool CanModify { get; set; }
        [Range(minimum:2, maximum:6)]
        public int MaxFileCount { get; set; }
    }

We have applied some validation rules to class properties which represents the configuration setting.

Now we need to modify Program.cs file to validate these data annotations.

builder.Services
       .AddOptions<FileOptions>().Bind(builder.Configuration.GetSection("file"))
       .ValidateDataAnnotations();  // validate the annotations

With above code changes, application is ready to validate annotation rule when user tries to access the configuration settings via Options pattern.

if you just remove the MaxSize property from appsettings.json file and try to access this inside controller then Microsoft.Extensions.Options.OptionsValidationException exception will be thrown with proper message.

Microsoft.Extensions.Options.OptionsValidationException: DataAnnotation validation failed for 'FileOptions' members: 'MaxSize' with the error: 'The MaxSize field is required.'.

๐Ÿ’ก
You could provide custom error message while defining the annotation rules.
 [Required(AllowEmptyStrings =false, ErrorMessage ="MaxSize cannot be empty")]
 public string MaxSize { get; set; }

Custom validation

You could write custom validation logic to validate configuration settings value.

builder.Services
       .AddOptions<FileOptions>().Bind(builder.Configuration.GetSection("file"))
       .Validate(fileOptions => {
           if (string.IsNullOrEmpty(fileOptions.FileType) {
               return false;
           }
           return true;
       });

Validate on application startup

builder.Services
       .AddOptions<FileOptions>().Bind(builder.Configuration.GetSection("file"))
       .ValidateDataAnnotations()
       .ValidateOnStart();

With .NET 8 , you could combine this with AddOptions method.

builder.Services
    .AddOptionsWithValidateOnStart<FileOptions>()
    .Bind(builder.Configuration.GetSection("file"));

Handling command line arguments or environment variables

To manage command line arguments or environment variables using options pattern, we somehow need a way to use ASP.NET Core dependency injection mechanism to access the required data.

So far we have used AddOptions method to tell ASP.NET Core to bind the C# class with a specific section of the appsettings.json file. So this method only allows us to bind the configurations which are part of appsettings.json file. We need a way to bind the configuration from other sources like command line, environment variables, database, xml or ini files etc.

To achieve this, we will set options pattern using IConfigurationOptions . This is two step process:

  • Create a class which implements IConfigureOptions interface, this class will allow us to access the other configuration sources via dependency injection.

  • Modify Program.cs to use the class created in above step.

let's see this with code:

First, we will modify the FileOptions class to include two new properties: FileMode and Version.

let's create class with implementation of IConfigureOptions

    public class FileOptionsSetup : IConfigureOptions<FileOptions> {
        private readonly IConfiguration _config;
        public FileOptionsSetup(IConfiguration config) {
            _config = config;
        }
        public void Configure(FileOptions options) {
            _config.GetSection("file").Bind(options);
            // bind the environment variable
            options.filemode = Environment.GetEnvironmentVariable("filemode");
            // bind the command line argument
            options.version = _config.GetValue<string>("version"); 
        }
    }

In above code, we first bind the appsettings.json section to FileOptions object and then read the configuration from environment variable and command line respectively.

We could extend above code to include configurations from almost any source

Now let's tell our program.cs to use above class for configurations.

builder.Services.ConfigureOptions<FileOptionsSetup>();

With all above code, our code is now powered to handle configuration from sources other than appsettings.json

Monitoring the changes in configuration settings

Let's recall how our code uses the IOptions interface for injecting the FileOptions class into controller constructor. There are two more interfaces which could be used to inject the options classes vai dependency injection into ASP.NET Core application.

Let's understand more about these in details :

IOptions

  • After running the program, IOptions doesn't monitor for changes into application configurations. It always gives the same value.

  • This works as singleton and can be injected in any service lifetime.

IOptionsSnapshot

  • This is useful in scenario where configuration needs to be read for each request - It works as a scoped service.

  • This can be injected into a scoped or transient service but not in singleton service.

  • appsettings.json is read for each request.

IOptionsMonitor

  • This works as singleton and can be injected in any service lifetime.

  • this interface has an OnChnage method which is triggered if there are changes in appsettings.json

              public FileController(IConfiguration config, 
                                  IOptionsMonitor<FileOptions> optionsMonitor) {
                  _config = config;
                  _optionsMonitor = optionsMonitor;
                  _fileOptions = _optionsMonitor.CurrentValue; // notice here
                  _optionsMonitor.OnChange(options => { // onchange method
                      _fileOptions = options;
                  });
              }
    

๐Ÿš€ Use IOptions<T> when you are not expecting changes in appsettings content.

๐Ÿš€ Use IOptionsSnapshot<T> when values are expected to change but want it to be consistent during a request.

๐Ÿš€ Use IOptionsMonitor<T> when real time value is expected.

Conclusion

In this article we have learned about some advance use cases where options pattern is quite useful. All such scenarios are difficult to manage using IConfiguration method.

Thanks for reading.

Did you find this article valuable?

Support Deepak Kumar Jain by becoming a sponsor. Any amount is appreciated!