ASP.NET Core 學習筆記 第五篇 ASP.NET Core 中的選項

故人與貓發表於2021-11-21

前言

還記得上一篇文章中所說的配置嗎?本篇文章算是上一篇的延續吧。在 .NET Core 中讀取配置檔案大多數會為配置選項繫結一個POCO(Plain Old CLR Object)物件,並通過依賴注入使用它。而這種使用方式稱之為選項模式。而選項模式使是用類來提供對相關設定組的強型別訪問,同時選項還提供驗證配置資料的機制,是不是很強大,讓我們一點點揭開它的神祕面紗。

ASP.NET Core 6.0 Web API簡要說明

首先開發工具上需要VS2022了,這裡非常推薦大家下載使用,在編碼上真的越來越符合我們開發者的使用了,提示也更加智慧化,VS2022詳細說明,下載以及註冊碼可以參考我的上一篇部落格,這裡就不做詳細說明了VS2022傳送門(含註冊碼)
.NET 6.0 已經出來有一段時間了,為了跟進技術的進步,筆者後續的系列筆記也將使用.NET 6.0作為目標框架,那麼.NET 6.0 ASP.NET Core Web API 中帶了了哪些變化呢?這裡我做下簡要說明。

  1. 開啟Visual Studio 2022,建立新專案,選擇“ASP.NET Core Web API”專案模板:
    image
    這裡可以看見Use controllers選項預設選中的,取消該選項,則會建立最小Web API。
  2. 結構變化
    image
    這裡可以看下Program.cs檔案的變化
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

可以發現我們熟知的Startup.cs不見了,現在,全部都在Program.cs中實現:

  • 在WebApplicationBuilder例項上使用Addxxx方法向DI容器註冊特定服務,類似Startup類的ConfigureServices方法實現。
  • 在WebApplication例項上使用Usexxx方法將一系列中介軟體加入到HTTP管道,類似Startup類的Configure方法實現。
  • DTO使用record定義,而且也放在同一個Program.cs檔案中。
    說明: Visual Studio 2022預設使用 Kestrel Web伺服器,而不是IIS Express。

配置繫結

首先建立一個json格式的配置檔案

{
  "JsonConfig": {
    "Title": "MyTitle",
    "Name": "Tom"
  }
}

建立選項類

    public class MyOptions
    {
        public const string JsonConfig = "JsonConfig";

        public string? Title { get; set; }

        public string? Name { get; set; }
    }

選項類說明:

  • 必須是包含公共無引數建構函式的非抽象類。
  • 型別的所有公共讀寫屬性都已繫結。
  • 不會繫結欄位。 在上面的程式碼中,Position 未繫結。 由於使用了 Position 屬性,因此在將類繫結到配置提供程式時,不需要在應用中對字串 "Position" 進行硬編碼。

使用 ConfigurationBinder 繫結

新建一個控制器來讀取配置檔案。

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class OptionsController : ControllerBase
    {
        private readonly IConfiguration _configuration;

        public OptionsController(IConfiguration configuration)
        {
            _configuration=configuration;
        }

        public ContentResult GetOptions()
        {
            var options = new MyOptions();

            //方式1
            _configuration.GetSection(MyOptions.JsonConfig).Bind(options);

            //方式2
            _configuration.GetSection(MyOptions.JsonConfig).Get<MyOptions>();

            return Content($"Title:{options.Title}\n Name:{options.Name}");
        }
    }

說明:
這裡的型別繫結有兩種方式分別為ConfigurationBinder.BindConfigurationBinder.Get ,
前者相對於後者更方便一些,在開發過程當中比較推薦使用後者做型別繫結。

依賴注入繫結

服務容器中繫結配置。

using OptionsTest.Model;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Configuration.AddJsonFile("MyJsonConfig.json", optional: true, reloadOnChange: true);

//注入繫結
builder.Services.Configure<MyOptions>(builder.Configuration.GetSection(MyOptions.JsonConfig));

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

新建一個控制器來讀取配置檔案。

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class Options2Controller : ControllerBase
    {
        private readonly MyOptions _options;

        public Options2Controller(IOptions<MyOptions> options)
        {
            _options = options.Value;
        }

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

選項介面

IOptions:

  • 不支援:
    • 在應用啟動後讀取配置資料。
    • 命名選項
    • 註冊為單一例項且可以注入到任何服務生存期。

IOptionsSnapshot:

  • 在每次請求時應重新計算選項的方案中有用。 有關詳細資訊,請參閱使用 IOptionsSnapshot 讀取已更新的資料。
  • 註冊為範圍內,因此無法注入到單一例項服務。
  • 支援命名選項

IOptionsMonitor:

  • 用於檢索選項並管理 TOptions 例項的選項通知。
  • 註冊為單一例項且可以注入到任何服務生存期。
    • 支援:
    • 更改通知
    • 命名選項
    • 可過載配置
    • 選擇性選項失效 (IOptionsMonitorCache)

IOptionsSnapshot

在專案中如何在改變配置檔案後,在不重啟專案的前提下自動載入配置檔案呢?IOptionsSnapshot就支援讀取已更新的配置值。
在前面專案的基礎上略做改變就能做到,MyOptions.cs、MyJsonConfig.json、Program.cs不需要改變。在OptionsController中改用IOptionsSnapshot注入就可以了,程式碼如下:

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class Options2Controller : ControllerBase
    {
        private readonly MyOptions _options;

        public Options2Controller(IOptionsSnapshot<MyOptions> options)
        {
            _options = options.Value;
        }

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

是不是很簡單,這樣每次改完配置檔案就不需要重啟程式了。

IOptionsMonitor

MyOptions.cs、MyJsonConfig.json、Program.cs不需要改變。在OptionsController中改用IOptionsMonitor注入就可以了,程式碼如下:

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class OptionsController : ControllerBase
    {
        private readonly MyOptions _options;

        public Options2Controller(IOptionsMonitor<MyOptions> options)
        {
            _options = options.Value;
        }

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

說明:

  • IOptionsMonitor 是一種單一示例服務,可隨時檢索當前選項值,這在單一例項依賴項中尤其有用。
  • IOptionsSnapshot 是一種作用域服務,並在構造 IOptionsSnapshot 物件時提供選項的快照。 選項快照旨在用於暫時性和有作用域的依賴項。

命名選項

命名選項:

  • 當多個配置節繫結到同一屬性時有用。
  • 區分大小寫。
    新建json配置檔案
{
  "JsonConfig": {
    "Title": "MyTitle",
    "Name": "Tom"
  },
  "JsonConfig2": {
    "Title": "MyTitle2",
    "Name": "Jerry"
  }
}

在屬性相同的情況下,不需要建立兩個類,我們增加一個欄位就可以了。

    public class MyOptions
    {
        public const string JsonConfig = "JsonConfig";

        public const string JsonConfig2 = "JsonConfig2";

        public string? Title { get; set; }

        public string? Name { get; set; }
    }

Program.cs中配置命名選項。

using OptionsTest3.Model;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Configuration.AddJsonFile("MyJsonConfig.json", optional: true, reloadOnChange: true);

//注入繫結
builder.Services.Configure<MyOptions>(MyOptions.JsonConfig,builder.Configuration.GetSection(MyOptions.JsonConfig));
builder.Services.Configure<MyOptions>(MyOptions.JsonConfig2, builder.Configuration.GetSection(MyOptions.JsonConfig2));
var app = builder.Build();


// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

在Controller中讀取配置。

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class Options3Controller : ControllerBase
    {
        private readonly MyOptions _options;

        private readonly MyOptions _options2;

        public Options3Controller(IOptionsSnapshot<MyOptions> options)
        {
            _options = options.Get(MyOptions.JsonConfig);

            _options2 = options.Get(MyOptions.JsonConfig2);
        }

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

選項驗證

在配置檔案中對於配置項的驗證是必不可少的。這樣可以保證程式配置的正確性。具體如何實現呢,且聽筆者慢慢道來。
首先建立一個簡單的Json配置檔案。

{
  "JsonConfig": {
    "ID": 10000000,
    "Title": "MyTitle",
    "Name": "Tom"
  }
}

建立我們配置項的驗證Model。

    public class MyOptions
    {
        public const string JsonConfig = "JsonConfig";

        [Range(0, 1000,
        ErrorMessage = "值 {0}必須在 {1} 和 {2}之間。")]
        public int ID { get; set; }

        [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
        public string? Title { get; set; }

        public string? Name { get; set; }
    }

在Program.cs中配置我們的選項和驗證。

using OptionValidtate.Model;
using System.Configuration;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Configuration.AddJsonFile("MyJsonConfig.json", optional: true, reloadOnChange: true);
builder.Services.AddOptions<MyOptions>().Bind(builder.Configuration.GetSection(MyOptions.JsonConfig)).ValidateDataAnnotations();


var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

說明:注意到和之前的程式碼有什麼不同了嗎?在我們正是通過呼叫ValidateDataAnnotations 以使用 DataAnnotations 來完成驗證。
在我們的Controller中讀取我們的配置。這裡用到了ILogger,可以先不用關心,在後續筆者會詳細講解。

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class OptionsController : ControllerBase
    {
        private readonly ILogger<OptionsController> _logger;
        private readonly IOptions<MyOptions> _options;

        public OptionsController(IOptions<MyOptions> options, ILogger<OptionsController> logger)
        {
            _options = options;
            _logger = logger;
            try
            {
               var optionsValue = _options.Value;
            }
            catch (OptionsValidationException e)
            {
                foreach (var failure in e.Failures)
                {
                    _logger.LogError(failure);
                }
            }
        }

        public ContentResult GetOptions()
        {
            string msg;

            try
            {
                msg=$"ID:{_options.Value.ID}\nTitle:{_options.Value.Title}\nName:{_options.Value.Name}\n";
            }
            catch (OptionsValidationException e)
            {
                return Content(e.Message);

            }

            return Content(msg);
        }
    }

還記得我們們配置的ID驗證規則嗎?啟動程式看下我們的驗證已經生效了。
image

IValidateOptions複雜驗證的實現

如果上面的驗證還不滿足要求的話,沒關係我對上面的程式碼略作修改,通過IValidateOptions就可以實現更加的複雜驗證。
新建MyValidateOptions類繼承IValidateOptions介面並實現。

    public class MyValidateOptions : IValidateOptions<MyOptions>
    {
          public MyOptions _options { get; private set; }

        public MyValidateOptions(IConfiguration config)
        {
            _options = config.GetSection(MyOptions.JsonConfig).Get<MyOptions>();
        }
        public ValidateOptionsResult Validate(string name, MyOptions options)
        {
            string msg = null;
            var rxValidate = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
            if (options.ID<0||options.ID>1000)
            {
                msg = $"{options.ID}必須在0-1000之間。";
            }
            if (string.IsNullOrEmpty(options.Title))
            {
                msg = $"{options.Title}必須符合正則要求。";
            }
            if (msg !=null)
            {
                return ValidateOptionsResult.Fail(msg);
            }

            return ValidateOptionsResult.Success;
        }
    }

在Program.cs中配置我們的選項和驗證。

using Microsoft.Extensions.Options;
using OptionValidtate.Model;
using System.Configuration;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Configuration.AddJsonFile("MyJsonConfig.json", optional: true, reloadOnChange: true);
builder.Services.AddOptions<MyOptions>().Bind(builder.Configuration.GetSection(MyOptions.JsonConfig));
builder.Services.AddSingleton<IValidateOptions<MyOptions>,MyValidateOptions>();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

好了配置完畢,執行程式可以看到我們配置的驗證已經生效了。

image

相關文章