1. 程式人生 > >[ASP.NET Core 3框架揭祕] Options[2]: 配置選項的正確使用方式[下篇]

[ASP.NET Core 3框架揭祕] Options[2]: 配置選項的正確使用方式[下篇]

四、直接初始化Options物件

前面演示的幾個例項具有一個共同的特徵,即都採用配置系統來提供繫結Options物件的原始資料,實際上,Options框架具有一個完全獨立的模型,可以稱為Options模型。這個獨立的Options模型本身並不依賴於配置系統,讓配置系統來提供配置資料僅僅是通過Options模型的一個擴充套件點實現的。在很多情況下,可能並不需要將應用的配置選項定義在配置檔案中,在應用啟動時直接初始化可能是一種更方便快捷的方式。

class Program
{
    static void Main()
    {
        var profile = new ServiceCollection()
            .AddOptions()
            .Configure<Profile>(it =>
            {
                it.Gender = Gender.Male;
                it.Age = 18;
                it.ContactInfo = new ContactInfo
                {
                    PhoneNo = "123456789",
                    EmailAddress = "[email protected]"
                };
            })
            .BuildServiceProvider()
            .GetRequiredService<IOptions<Profile>>()
            .Value;

        Console.WriteLine($"Gender: {profile.Gender}");
        Console.WriteLine($"Age: {profile.Age}");
        Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}");
        Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n");
    }
}

我們依然沿用前面演示的應用場景,現在摒棄配置檔案,轉而採用程式設計的方式直接對使用者資訊進行初始化,所以需要對程式做如上改寫。在呼叫IServiceCollection介面的Configure<Profile>擴充套件方法時,不需要再指定一個IConfiguration物件,而是利用一個Action<Profile>型別的委託對作為引數的Profile物件進行初始化。程式執行後會在控制檯上產生下圖所示的輸出結果。

具名Options同樣可以採用類似的方式進行初始化。如果需要根據指定的名稱對Options進行初始化,那麼呼叫方法時就需要指定一個Action<TOptions,String>型別的委託物件,該委託物件的第二個引數表示Options的名稱。在如下所示的程式碼片段中,我們通過類似的方式設定了兩個使用者(foo和bar)的資訊,然後利用IOptionsSnapshot<TOptions>服務將它們分別提取出來。

class Program
{
    static void Main()
    {
        var optionsAccessor = new ServiceCollection()
            .AddOptions()
            .Configure<Profile>("foo", it =>
            {
                it.Gender = Gender.Male;
                it.Age = 18;
                it.ContactInfo = new ContactInfo
                {
                    PhoneNo = "123",
                    EmailAddress = "[email protected]"
                };
            })
            .Configure<Profile>("bar", it =>
            {
                it.Gender = Gender.Female;
                it.Age = 25;
                it.ContactInfo = new ContactInfo
                {
                    PhoneNo = "456",
                    EmailAddress = "[email protected]"
                };
            })
            .BuildServiceProvider()
            .GetRequiredService<IOptionsSnapshot<Profile>>();

        Print(optionsAccessor.Get("foo"));
        Print(optionsAccessor.Get("bar"));

        static void Print(Profile profile)
        {
            Console.WriteLine($"Gender: {profile.Gender}");
            Console.WriteLine($"Age: {profile.Age}");
            Console.WriteLine($"Email Address: {profile.ContactInfo.EmailAddress}");
            Console.WriteLine($"Phone No: {profile.ContactInfo.PhoneNo}\n");
        };
    }
}

該程式執行後會在控制檯上產生下圖所示的輸出結果。在前面的演示中,我們利用依賴注入框架提供IOptions<TOptions>服務、IOptionsSnapshot<TOptions>服務和IOptionsMonitor<TOptions>服務,然後進一步利用它們來提供對應的Options物件。既然作為依賴注入容器的IServiceProvider物件能夠提供這3個物件,我們就能夠將它們注入消費Options物件的型別中。所謂的Options模式就是通過注入這3個服務來提供對應Options物件的程式設計模式。

五、根據依賴服務的Options設定

在很多情況下需要針對某個依賴的服務動態地初始化Options的設定,比較典型的就是根據當前的承載環境(開發、預發和產品)對Options做動態設定。《上篇》演示了一系列針對時間日期輸出格式的配置,下面沿用這個場景演示如何根據當前的承載環境設定對應的Options。將DateTimeFormatOptions的定義進行簡化,只保留如下所示的表示日期和時間格式的兩個屬性。

public class DateTimeFormatOptions
{
    public string DatePattern { get; set; }
    public string TimePattern { get; set; }
    public override string ToString()  => $"Date: {DatePattern}; Time: {TimePattern}";
}

如下所示的程式碼片段是整個演示例項的完整定義。我們利用第6章介紹的配置系統來設定當前的承載環境,具體採用的是基於命令列引數的配置源。.NET Core的承載系統通過IHostEnvironment介面表示承載環境,具體實現型別為HostingEnvironment。如下面的程式碼片段所示,我們利用獲取的環境名稱建立了一個HostingEnvironment物件,並針對IHostEnvironment介面採用Singleton生命週期做了相應的註冊。

class Program
{
    public static void Main(string[] args)
    {
        var environment = new ConfigurationBuilder()
            .AddCommandLine(args)
            .Build()["env"];

        var services = new ServiceCollection();
        services
            .AddSingleton<IHostEnvironment>(new HostingEnvironment { EnvironmentName = environment })
            .AddOptions<DateTimeFormatOptions>().Configure<IHostEnvironment>( (options, env) => {
            if (env.IsDevelopment())
            {
                options.DatePattern = "dddd, MMMM d, yyyy";
                options.TimePattern = "M/d/yyyy";
            }
            else
            {
                options.DatePattern = "M/d/yyyy";
                options.TimePattern = "h:mm tt";
            }
        });

        var options = services
            .BuildServiceProvider()
            .GetRequiredService<IOptions<DateTimeFormatOptions>>().Value;
        Console.WriteLine(options);
    }
}

上面呼叫IServiceCollection介面的AddOptions<DateTimeFormatOptions>擴充套件方法完成了針對Options模型核心服務的註冊和針對DateTimeFormatOptions的設定。該方法返回的是一個封裝了IServiceCollection集合的OptionsBuilder<DateTimeFormatOptions>物件,可以呼叫其Configure<IHostEnvironment>方法利用提供的Action<DateTimeFormatOptions, IHostEnvironment>委託物件針對依賴的IHostEnvironment服務對DateTimeFormatOptions做相應的設定。具體來說,我們針對開發環境和非開發環境設定了不同的日期時間格式。如果採用命令列的方式啟動這個應用程式,並利用命令列引數設定不同的環境名稱,就可以在控制檯上看到下圖所示的針對DateTimeFormatOptions的不同設定。

六、驗證Options的有效性

由於配置選項是整個應用的全域性設定,為了儘可能避免錯誤的設定造成的影響,最好能夠對內容進行有效性驗證。接下來我們將上面的程式做了如下改動,從而演示如何對設定的日期和時間格式做最後的有效性驗證。

class Program
{
    public static void Main(string[] args)
    {
        var config = new ConfigurationBuilder()
            .AddCommandLine(args)
            .Build();
        var datePattern = config["date"];
        var timePattern = config["time"];

        var services = new ServiceCollection();
        services.AddOptions<DateTimeFormatOptions>()
            .Configure(options =>
            {
                options.DatePattern = datePattern;
                options.TimePattern = timePattern;
            })
            .Validate(options => Validate(options.DatePattern) && Validate(options.TimePattern),"Invalid Date or Time pattern.");
        try
        {
            var options = services
                .BuildServiceProvider()
                .GetRequiredService<IOptions<DateTimeFormatOptions>>().Value;
            Console.WriteLine(options);
        }
        catch (OptionsValidationException ex)
        {
            Console.WriteLine(ex.Message);
        }

        static bool Validate(string format)
        {
            var time = new DateTime(1981, 8, 24,2,2,2);
            var formatted = time.ToString(format);
            return DateTimeOffset.TryParseExact(formatted, format,   null, DateTimeStyles.None, out var value)  && (value.Date == time.Date || value.TimeOfDay == time.TimeOfDay);
        }
    }
}

上述演示例項藉助配置系統以命令列的形式提供了日期和時間格式化字串。在建立了OptionsBuilder<DateTimeFormatOptions>物件並對DateTimeFormatOptions做了相應設定之後,我們呼叫Validate<DateTimeFormatOptions>方法利用提供的Func<DateTimeFormatOptions,bool>委託物件對最終的設定進行驗證。執行該程式並按照下圖所示的方式指定不同的格式化字串,系統會根據我們指定的規則來驗證其有效性。

[ASP.NET Core 3框架揭祕] Options[1]: 配置選項的正確使用方式[上篇]
[ASP.NET Core 3框架揭祕] Options[2]: 配置選項的正確使用方式[下篇]
[ASP.NET Core 3框架揭祕] Options[3]: Options模型[上篇]
[ASP.NET Core 3框架揭祕] Options[4]: Options模型[下篇]
[ASP.NET Core 3框架揭祕] Options[5]: 依賴注入
[ASP.NET Core 3框架揭祕] Options[6]: 擴充套件與定製
[ASP.NET Core 3框架揭祕] Options[7]: 與配置系統的整合