跟我一起學.NetCore之選項(Options)核心型別簡介

Code綜藝圈發表於2020-08-24

前言

來啦!來啦!上一節一堆程式碼,是不是感覺甚是無味啊?沒關係,這裡結合上一節內容專注舉例演示,絕不廢話!走起~~~~~

正文

老規矩,一個WebApi專案走起,專案結構如下:

img

上一節中提到,Options是基於依賴注入的,所以我們需要將相關服務進行註冊,如下:

img

註冊完成之後就可以直接用啦,這裡新建了一個ComputerController進行測試:

img

執行走起:

img

通常在使用的時候,相關服務的配置會放到配置檔案中,比如資料庫連線字串等,所以在註冊的時候將初始化的內容從配置中獲取即可,如下:

img

跑起來~~~

img

通過以上的例項演示,可能會好奇為啥不直接用Configuration,我是這樣理解的:通過Options的話,服務不限制使用初始化方式,根據需求選擇,如果使用配置檔案取值,服務也和配置沒有直接關係,從而使得服務的使用沒有過多限制;另外服務內部使用也比較便捷,就單純操作Options物件;

哎呀,又來這一套,使用太簡單了,來點真貨唄,此時應該有小夥伴按捺不住了問:Options的相關服務是怎麼註冊的,初始化和上一節講的初始化有啥關係? 來來來,不急,以下慢慢品~~~

那從哪開始呢?

上一小節說的TOptions物件建立的三大步,建立內部直接New了(忘了的可以回顧一下上一節:跟我一起學.NetCore之選項(Options)核心型別簡介),那這裡就從註冊初始化的地方下手:

img

Services.Configure方法中肯定有事,不然一句程式碼咋就讓後面使用如此便捷呢,來,直接看原始碼:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

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

namespace Microsoft.Extensions.DependencyInjection
{
    /// <summary>
    /// Extension methods for adding configuration related options services to the DI container.
    /// </summary>
    public static class OptionsConfigurationServiceCollectionExtensions
    {
       // 挨著的這三個方法都是IServiceCollection 的擴充套件方法
        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config) where TOptions : class
            => services.Configure<TOptions>(Options.Options.DefaultName, config);
        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config) where TOptions : class
            => services.Configure<TOptions>(name, config, _ => { }); 
        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, IConfiguration config, Action<BinderOptions> configureBinder)
            where TOptions : class
            => services.Configure<TOptions>(Options.Options.DefaultName, config, configureBinder);
        // 關鍵方法
        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
            where TOptions : class
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            if (config == null)
            {
                throw new ArgumentNullException(nameof(config));
            }
             // 註冊Options相關服務,這是關鍵方法
            services.AddOptions();
            // IOptionsChangeTokenSource 註冊,後續用於通知
            services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
            // 註冊從配置系統中獲取配置值的服務
            return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
        }
    }
}

上面程式碼中services.AddOptions()是關鍵方法,再翻程式碼瞅瞅:

  public static class OptionsServiceCollectionExtensions
 {
        // 恍然大悟吧,就是這個擴充套件方法,把服務都註冊好了,所以使用的時候才直接注入即可
        public static IServiceCollection AddOptions(this IServiceCollection services)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }
            // 使用TryAdd的注入方式,為了對應服務型別只註冊一次
            // 註冊IOptions<>,實現類是OptionsManager<>
            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
            // 註冊IOptionsSnapshot<>,實現類是OptionsManager<>
            services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
            // 註冊IOptionsMonitor<>,實現類是OptionsMonitor<>
            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
            // 註冊IOptionsFactory<> ,實現類是OptionsFactory<>
            services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
            // 註冊IOptionsMonitorCache<>, 實現類是OptionsCache<>
            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
            return services;
        }
        // 對應建立TOption 初始化的兩小步,步驟1Configure
        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
            where TOptions : class
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            if (configureOptions == null)
            {
                throw new ArgumentNullException(nameof(configureOptions));
            }

            services.AddOptions();
            services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
            return services;
        }
        // 對應建立TOption 初始化的兩小步,步驟2 PostConfigure
        public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
            where TOptions : class
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            if (configureOptions == null)
            {
                throw new ArgumentNullException(nameof(configureOptions));
            }

            services.AddOptions();
            services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions));
            return services;
        }
        ......省略了其他方法....
 }

看了以上程式碼,是不是豁然開朗了,Options的相關服務註冊都是在AddOptions擴充套件方法中完成的,至於其中的Configure和PostConfigure實則對應著上一節的建立Options初始化的過程;以上註冊的核心型別的生命週期需要了解一下,後面舉例會關聯到:

  • IOptions<>:Singleton - 單例模式

  • IOptionsSnapshot<>:Scoped - 作用域範圍模式

  • IOptionsMonitor<>: Singleton - 單例模式

Configure是註冊時進行初始化,而PostConfigure一般會用在讀取到配置資料之後對資料進行加工,如下:

img

接下來說熱更新,也就是配置改變,服務對應的Options能獲取最新的值,程式碼稍微優化一下:

img

第一次訪問,第二次訪問前進行配置檔案修改,然後在訪問,執行結果如下:

img

可以看到,配置資料改變時,IOptionsSnapshot<>和IOptionsMonitor<>都能獲取到最新的值,IOptions<>沒有獲取到,簡單分析一下:

IOptons<>和IOptionsSnapshot<>其實內部是一樣的,只是注入的生命週期不一樣,前者是單例,後者是Scoped,這樣就使得後者每次請求都是不同的IOptionsSnapshot物件,從而就能獲取最新的值;而單例IOptions物件一致不變;那為什麼單例的IOptionsMonitor型別能改變呢,那是因為IOptionsMonitor提供了監聽改變的功能,上一節有簡單說明;

如果在配置資料改變時,需要通知Option怎麼辦呢?通過IOptionsMonitor的OnChange監聽改變,如下:

img

執行看結果,首先請求一次,然後修改配置檔案,就能看到實時監控了:

img

最後來說說Options驗證(也就是建立TOptions的第三步),主要是避免不合法的配置,導致程式在執行時業務邏輯出錯才能發現錯誤,從而導致程式迭代週期頻繁,使用者體驗差;驗證有以下三種方式:

  • 直接註冊驗證函式

  • 實現IValidateOptions進行驗證

  • 使用註解(Microsoft.Extensions.Options.DataAnnotations)

這裡用三種方式分別依次驗證Name,Cores,MemorySize 三個配置項,如下:

  • 直接註冊驗證函式

    img

  • 實現IValidateOptions進行驗證

    img

  • 使用註解(Microsoft.Extensions.Options.DataAnnotations)

    img

三種驗證方式是相互獨立的,可以單獨使用,也可以組合使用;以上每一種方式對應圖片內容;

執行訪問不合法就報錯,這樣就避免配置不合法,導致業務邏輯不合法,如下

img

總結

Options(選項)的常規用法暫時就說這麼多了,結合上一小節是不是感覺清晰多了~~~下一節說說日誌(ILogger)

----------------------------------------------

一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~

相關文章