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 中的選項模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章