ASP.NET Core 中的选项模式

前言:

选项模式使用的 Microsoft.Extensions.Options.ConfigurationExtensions 包已在 ASP.NET Core 应用中隐式引用。
全文中我们全部使用一个名称为 MyOptions 的类:

    public class MyOptions : IDisposable
    {
    
    
        public MyOptions()
        {
    
    
            Option1 = "value_ctor";
        }

        public string Option1 {
    
     get; set; }
        
		[Range(0, int.MaxValue)]
        public int Option2 {
    
     get; set; } = 5;
        
        public void Dispose()
        {
    
    
            Console.WriteLine($"MyOptions Disposed:{GetHashCode()}");
        }
    }

配置文件 appsettings.json 的内容:

    {
    
    
      "option1": "value_json",
      "option2": -1,
  	  "subsection": {
    
    
      	 "option1": "subvalue_json",
         "option2": 55555
      },
      "Logging": {
    
    
        "LogLevel": {
    
    
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*"
    }

本人在进行测试时,发现 选项模式的实例并不会走 Dispose() 方法

一、依赖注入的生命周期

在选项模式中实例的生命周期由注入时使用的接口决定,
IOptionsIOptionsMonitorIOptionsSnapshot共三个,
其中 IOptionsIOptionsMonitor 的生命周期是单例模式,生命周期和 AddSingleton 类似,
IOptions 没有数据热更新,读取的值永远不会变。
IOptionsMonitorIOptionsSnapshot 有数据热更新,会保持最新值。
剩下的 IOptionsSnapshot 是作用域服务(快照),生命周期和 AddScoped 类似。

一、常规选项配置

MyOptions 类已通过 Configure 添加到服务容器并绑定到配置:

#region ConfigureServices
	services.Configure<MyOptions>(Configuration);
#endregion

#region [ApiController] 
    private readonly MyOptions _options;
    public HomeController(IOptionsMonitor<MyOptions> options)
    {
    
    
        _options = options.CurrentValue;
    }
    
    [HttpGet]
    public MyOptions Index() {
    
     return _options; }
#endregion

访问 /Home/Index 得到的值为:

{
    
    "option1":"value_json","option2":-1}

可以看到,配置文件中的值已将实例的默认值覆盖了。

备注

使用自定义 ConfigurationBuilder 从设置文件加载选项配置:

#region ConfigureServices
   var configBuilder = new ConfigurationBuilder()
       .SetBasePath(Directory.GetCurrentDirectory())
       .AddJsonFile("appsettings.json", optional: true);
   var config = configBuilder.Build();
   services.Configure<MyOptions>(config);
#endregion

二、通过委托配置简单选项

使用委托设置选项值:

#region ConfigureServices
    services.Configure<MyOptions>(myOptions =>
    {
    
    
        myOptions.Option1 = "value_delegate";
        myOptions.Option2 = 2333;
    });
#endregion

访问 /Home/Index 得到的值为:

	{
    
    "option1":"value_delegate","option2":2333}

可以看到,MyOptions 使用委托配置绑定覆盖了默认值,
当启用多个配置服务时,指定的最后一个配置源优于其他源 。

三、子选项配置

需要配置值的部分应用时可以使用子选项配置

#region ConfigureServices
    services.Configure<MyOptions>(Configuration.GetSection("subsection"));
#endregion

访问 /Home/Index 得到的值为:

	{
    
    "option1":"subvalue_json","option2":55555}

四、重新加载配置数据

使用选项框架时,配置文件默认是热更新的。
我们运行 子选项配置 中的代码
访问 /Home/Index 得到的值为:

	{
    
    "option1":"subvalue_json","option2":55555}

程序运行时,我们将 appsettings.json 文件中的 subsection 节点改为如下:

  "subsection": {
    
    
    "option1": "subvalue_json new",
    "option2": 12345
  },

再次访问 /Home/Index 得到的值为:

	{
    
    "option1":"subvalue_json new","option2":12345}

五、命名选项

命名选项支持允许应用在命名选项配置之间进行区分

#region ConfigureServices
    services.Configure<MyOptions>("options_1", Configuration);
    
    services.Configure<MyOptions>("options_2", myOptions =>
    {
    
    
        myOptions.Option1 = "options_2_value1_action";
    });
#endregion

#region [ApiController] 
    private readonly MyOptions _options;
    private readonly MyOptions _options_1;
    private readonly MyOptions _options_2;
    
    public HomeController(IOptionsMonitor<MyOptions> options)
    {
    
    
        _options = options.CurrentValue;
        _options_1 = options.Get("options_1");
        _options_2 = options.Get("options_2");
    }
    
    [HttpGet]
    public List<MyOptions> Index()
    {
    
    
        var result = new List<MyOptions>() {
    
    
            _options,_options_1,_options_2
        };
        return result;
    }
#endregion

访问 /Home/Index 得到的值为:

	[
		{
    
    "option1":"value_ctor","option2":5},
		{
    
    "option1":"value_json","option2":-1},
		{
    
    "option1":"options_2_value1_action","option2":5}
	]

可以看到,各个MyOptions 实例的不同,
无命名的取的是默认值,options_1 取的是配置文件的值,options_2 取的是委托配置。

六、配置所有选项

可以使用 ConfigureAll 方法配置所有选项实例。

#region ConfigureServices
    services.Configure<MyOptions>("options_1", Configuration);
    
    services.Configure<MyOptions>("options_2", myOptions =>
    {
    
    
        myOptions.Option1 = "options_2_value1_action";
    });
    
    services.ConfigureAll<MyOptions>(myOptions =>
    {
    
    
        myOptions.Option1 = "ConfigureAll value";
    });
#endregion

访问 /Home/Index 得到的值为:

	[
		{
    
    "option1":"ConfigureAll value","option2":5},
		{
    
    "option1":"ConfigureAll value","option2":-1},
		{
    
    "option1":"ConfigureAll value","option2":5}
	]

可以看到每个MyOptions实例的 option1 都被覆盖了

七、OptionsBuilder API

OptionsBuilder 简化了创建命名选项的过程,

#region ConfigureServices
    services.AddOptions<MyOptions>().Configure(o => o.Option1 = "options_builder");
    
    services.AddOptions<MyOptions>("options_1").Configure(o =>
    {
    
    
    	Configuration.Bind(o);
        // o.Option1 = "default value"; 
        // o.Option2 = 2000;
    });
#endregion

访问 /Home/Index 得到的值为:

	[
		{
    
    "option1":"options_builder","option2":5},
		{
    
    "option1":"default value","option2":2000},
		{
    
    "option1":"value_ctor","option2":5}
	]

八、使用 DI 服务配置选项

可以使用DI服务进行配置

#region ConfigEntity
	public class AppConfig {
    
     public string Option1 {
    
     get; set; } = "SAP"; }

    public class PrjConfig {
    
     public int Option2 {
    
     get; set; } = 69; }
#endregion

#region ConfigureServices
    services.AddScoped<AppConfig>();
    services.AddScoped<PrjConfig>();
    
    services.AddOptions<MyOptions>("optionalName")
        .Configure<AppConfig, PrjConfig>((o, s, b) =>
        {
    
    
            o.Option1 = s.Option1;
            o.Option2 = b.Option2;
        });
#endregion

#region [ApiController]
	private readonly MyOptions _options;
    public HomeController(IOptionsSnapshot<MyOptions> options)
    {
    
    
        _options = options.Get("optionalName");
    }
    
    [HttpGet]
    public MyOptions Index() {
    
     return _options; }
#endregion

访问 /Home/Index 得到的值为:

	{
    
    "option1":"SAP","option2":69}

九、后期配置

使用 IPostConfigureOptions<TOptions> 设置后期配置

#region ConfigureServices
	//  PostConfigure 可用于后期配置
    services.PostConfigure<MyOptions>(myOptions =>
    {
    
    
        myOptions.Option1 = "post_configured_option1_value";
    });
    //  PostConfigure 可用于对命名选项进行后期配置
    services.PostConfigure<MyOptions>("named_options_1", myOptions =>
    {
    
    
        myOptions.Option1 = "post_configured_option1_value";
    });
    //  PostConfigureAll 对所有配置实例进行后期配置
    services.PostConfigureAll<MyOptions>(myOptions =>
    {
    
    
        myOptions.Option1 = "post_configured_option1_value";
    });
#endregion

十、选项验证

添加验证的时候只能用 OptionsBuilder API
现在将 HomeController 的 ctor 修改为如下:

#region [ApiController]
    private readonly MyOptions _options;
    public HomeController(IOptionsSnapshot<MyOptions> options)
    {
    
    
        _options = options.Value;
    }
    
    [HttpGet]
    public MyOptions Index() {
    
     return _options; }
#endregion

添加选项验证有三种方法:

  • 直接注册验证函数,调用 Validate
  • 使用 Microsoft.Extensions.Options.DataAnnotations 基于数据注释验证
  • 实现 IValidateOptions<TOptions> 函数

1) 使用Validate 方法验证

#region ConfigureServices
    services.AddOptions<MyOptions>().Configure(options =>
    {
    
    
        Configuration.Bind(options);
    }).Validate(options =>
    {
    
    
        return options.Option2 > 0;
    }, "Option2 必须大于0");
#endregion

因为配置文件中 Option2 等于 -1 所以当我们访问 /Home/Index 时:
ValidateErrorImg
可以看到报错的内容为我们填写的 Validate 的第二个参数。

2) 基于数据注释的验证

#region ConfigureServices
    services.AddOptions<MyOptions>().Configure(options =>
    {
    
    
        Configuration.Bind(options);
    }).ValidateDataAnnotations();
#endregion

#region MyOptions Class
		[Range(0, int.MaxValue)]
        public int Option2 {
    
     get; set; } = 5;
#endregion

因为配置文件中 Option2 等于 -1 所以当我们访问 /Home/Index 时:
AttributeErrorImg

3) 实现 IValidateOptions 进行验证

首先我们需要实现 IValidateOptions :

#region IValidateOptions
    public class MyValidateOptions : IValidateOptions<MyOptions>
    {
    
    
        public ValidateOptionsResult Validate(string name, MyOptions options)
        {
    
    
            if (options.Option2 <= 0)
            {
    
    
                return ValidateOptionsResult.Fail("Option2 必须大于0");
            }
            else
            {
    
    
                return ValidateOptionsResult.Success;
            }
        }
    }
#endregion

#region ConfigureServices
    services.AddOptions<MyOptions>().Configure(options =>
    {
    
    
        Configuration.Bind(options);
    }).Services.AddSingleton<IValidateOptions<MyOptions>, MyValidateOptions>();
#endregion

因为配置文件中 Option2 等于 -1 所以当我们访问 /Home/Index 时:
IValidateOptionsErrorImg

十一、在启动期间访问选项

可用于 Startup.Configure 中,因为在 Configure 方法执行之前已生成服务

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


参考文档

ASP.NET Core 中的选项模式

猜你喜欢

转载自blog.csdn.net/Upgrader/article/details/104785213