The use of options in. Net core option mode

Time:2020-12-1

ASP.NET The core introduces the options mode, which uses classes to represent related settings groups. To put it simply, we use strongly typed classes to express configuration items, which brings a lot of benefits. The dependency injection of the system is utilized, and the configuration system can also be used. It allows us to directly use a bound poco object, called options object, by using dependency injection. It can also be called a configuration object.

Most of the following contents are from official documents. I’m just a translator or porter!

Introducing options expansion pack

PM>Package-install Microsoft.Extensions.Options

Binding hierarchical configuration

stay appsetting.json Add the following configuration to the file

"Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Create the following positionoptions class:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}
Option class:
  • Must be a non abstract class that contains a public parameterless constructor.
  • All public read-write properties of type are bound.
  • Fields are not bound. In the above code, position is unbound. Since the position property is used, there is no need to hard code the string “position” in the application when binding a class to a configuration provider.
Class binding

Call ConfigurationBinder.Bind Bind the positionoptions class to the position section. Then it can be used. Of course, this method is not commonly used in the development of. Net core. Generally, dependency injection is used.

var positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

use ConfigurationBinder.GetMay be better than using ConfigurationBinder.Bind More convenient.

positionOptions = Configuration.GetSection(PositionOptions.Position).Get();

Dependency injection service container

  • Modify configureservices method
public void ConfigureServices(IServiceCollection services)
{
    services.Configure(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}
  • Using the previous code, the following code reads the location options:
public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

Options interface

Beginners will find that the framework has three main consumer oriented interfaces: ioptions、IOptionsMonitorAnd ioptions snapshot。

These three interfaces look very similar at first, so it is easy to cause confusion. Which interface should be used in what scenario?

  1. IOptions
  • I won’t support it
    • Read the configuration data after the application starts.
    • Naming options
  • Registered as a single instance, it can be injected into any service lifetime.
  1. IOptionsSnapshot
  • The scope container is configured for hot updates to use it
  • Registered as in scope and therefore cannot be injected into a single instance service
  • Support naming options
  1. IOptionsMonitor
  • Option notifications for retrieving options and managing options instances.
  • It is registered as a single instance and can be injected into any service lifetime.
  • support
    • Change notice
    • Naming options
    • Reloadable configuration
    • Selective option is invalid

Read updated data using ioptionsnapshot

The differences between ioptionsmonitor and ioptionsnapshot are as follows:

  • Ioptionsmonitor is a single instance service that can retrieve current option values at any time, which is particularly useful in single instance dependencies.
  • Ioptionsnapshot is a scoped service, and the ioptionsnapshot is constructedObject provides a snapshot of the options. Option snapshots are intended for transient and scoped dependencies.
public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

IOptionsMonitor

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Naming options support iconfigurenamedoptions

Naming options:

  • Useful when multiple configuration sections are bound to the same property.
  • Case sensitive.

appsettings.json file

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Instead of creating two classes to bind, the following classes are used for each section TopItem:Month And TopItem:Year

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

Dependency injection container

public void ConfigureServices(IServiceCollection services)
{
    services.Configure(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

Service application

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }
}

Use Di services configuration options

When configuring options, you can access services through dependency injection in the following two ways:

  • Pass the configuration delegate to optionsbuilderConfigure on
services.AddOptions("optionalName")
    .Configure(
        (o, s, s2, s3, s4, s5) => 
            o.Property = DoSomethingWith(s, s2, s3, s4, s5));
  • Create and implement iconfigureoptionsOr iconfigurenamedoptionsAnd register the type as a service

It is recommended to pass the configuration delegate to configure because it is complex to create a service. When you call configure, creating a type is equivalent to what the framework performs. Calling configure registers the temporary generic iconfigurenamedoptionsThat has a constructor that takes the specified generic service type.

Option validation

appsettings.json file

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

The following classes are bound to the “myconfig” configuration section and apply several dataannotations rules:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}
  • Enable data annotations validation
public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations();

    services.AddControllersWithViews();
}

More complex configuration using ivalidateoptions

public class MyConfigValidation : IValidateOptions
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

Ivalidateoptions allows validation code to be moved out of startup and into a class.

Using the previous code, use the following code in the Startup.ConfigureServices Enable authentication in

public void ConfigureServices(IServiceCollection services)
{
    services.Configure(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton, MyConfigValidation>());
    services.AddControllersWithViews();
}

Option post configuration

Using ipostconfigureoptionsSet up post configuration. Perform all iconfigureoptionsRun post configuration after configuration

services.PostConfigure(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Use postconfigureall to post configure all configuration instances

Access options during startup

IOptionsAnd ioptionsmonitorCan be used for Startup.Configure Because the service was generated before the configure method was executed.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

conclusion

Ioptions < > is a singleton, so once generated, its value will not be updated unless it is changed by code.

Ioptionsmonitor < > is also a singleton, but it can be updated with the configuration file through ioptionschangetokensource < > and the value can be changed by code.

Ioptionsnapshot < > is a range, so its value will be updated the next time the configuration file is updated. However, it cannot change the value through code across the scope, and it can only be valid within the current scope (request).

So you should choose which one of the three should be used according to your actual use scenarios.
Generally speaking, if you rely on configuration files, you should first consider ioptionsmonitor < >, if not, then consider ioptionsnapshot < >, and finally consider ioptions < >.

One thing to note is that ASP.NET Ioptionsmonitor in core applications may cause inconsistent values of options in the same request, which may cause some strange bugs when you are modifying the configuration file.

If this is important to you, please use ioptions snapshot, which can guarantee consistency in the same request, but it may cause slight performance loss.
If you construct your own options when the app starts (for example, in the startup class), the

services.Configure(opt => opt.Name = "Test");

Ioptions < > is the simplest and probably a good choice.