. NET Core Options

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

Most of the following content comes from official documents. I'm just a translator or a porter!

Introducing Options extension package

PM>Package-install Microsoft.Extensions.Options

Binding hierarchy 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 with a public parameterless constructor.
  • All public read-write properties of type are bound.
  • Fields are not bound. In the above code, Position is not bound. Because the Position attribute is used, there is no need to hard code the string "Position" in the application when binding the class to the 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.Get May be better than using ConfigurationBinder.Bind More convenient.

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

Dependency injection service container

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

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

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

Option interface

Beginners will find that the framework has three main consumer facing interfaces: IOptions, IOptions monitor, and IOptions snapshot.

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

  1. IOptions
  • I won't support it
    • Read configuration data after application startup.
    • Naming options
  • Registered as a single instance, it can be injected into any service lifetime.
  1. IOptionsSnapshot
  • Scope container configuration hot update using it
  • Cannot be injected into a single instance service because it is registered in scope
  • Support for naming options
  1. IOptionsMonitor
  • Option notifications for retrieving options and managing instances of options.
  • Registered as a single instance and can be injected into any service lifetime.
  • support
    • Change notice
    • Naming options
    • Reloadable configuration
    • Optional option failure

Read the updated data using ioptionsnapshot

The differences between IOptionsMonitor and ioptionsnapshot are as follows:

  • IOptionsMonitor is a single sample service that can retrieve the current option values at any time, which is particularly useful in a single instance dependency.
  • IOptionsSnapshot is a scope service that provides a snapshot of options when the IOptionsSnapshot object is constructed. Option snapshots are intended for transient and scoped dependencies.
public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

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

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

IOptionsMonitor

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> 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>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(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<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }
}

Use DI service configuration options

When you configure options, you can access services through dependency injection in two ways:

  • Pass configuration delegation to Configure on OptionsBuilder
services.AddOptions<MyOptions>("optionalName")
    .Configure<Service1, Service2, Service3, Service4, Service5>(
        (o, s, s2, s3, s4, s5) => 
            o.Property = DoSomethingWith(s, s2, s3, s4, s5));
  • Create a type that implements IConfigureOptions or IConfigureNamedOptions and register the type as a service

It is recommended that you pass configuration delegation to Configure because creating services is complex. When you call Configure, the creation type is equivalent to the operation performed by the framework. Calling Configure registers the temporary generic IConfigureNamedOptions with a constructor that accepts the specified generic service type.

Option validation

appsettings.json file

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

The following classes bind 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 DataAnnotations validation
public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations();

    services.AddControllersWithViews();
}

More complex configuration with IValidateOptions

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

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

    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 classes.

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

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

Options post configuration

Use IPostConfigureOptions to set up post configuration. Run post configuration after all IConfigureOptions are configured

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

Use PostConfigureAll to post configure all configuration instances

Access options during startup

IOptions and IOptionsMonitor are available for Startup.Configure Because the service was built before the configure method was executed.

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

conclusion

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

Ioptionsmonitor < > is also a single example, but it can be updated with the configuration file through ioptionschaetokensource < > and can also change the value by code.

Ioptionsnapshot < > is a range, so its value will be updated in the next access of profile update, but it can't change the value by code across the range, and it can only be valid in the current range (request).

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

One thing to note is that ASP.NET IOptionsMonitor in core application may lead to inconsistent values of options in the same request, which may cause some strange bug s when you are modifying the configuration file.

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

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

Ioptions < > is the simplest, maybe a good choice.

Tags: JSON snapshot Attribute hot update

Posted on Sun, 21 Jun 2020 04:55:16 -0400 by urb