1. 程式人生 > >[ASP.NET Core 3框架揭祕] 配置[1]:讀取配置資料[上篇]

[ASP.NET Core 3框架揭祕] 配置[1]:讀取配置資料[上篇]

提到“配置”二字,我想絕大部分.NET開發人員腦海中會立即浮現出兩個特殊檔案的身影,那就是我們再熟悉不過的app.config和web.config,多年以來我們已經習慣了將結構化的配置定義在這兩個XML格式的檔案之中。到了.NET Core的時代,很多我們習以為常的東西都發生了改變,其中就包括定義配置的方式。總的來說,新的配置系統顯得更加輕量級,並且具有更好的擴充套件性,其最大的特點就是支援多樣化的資料來源。我們可以採用記憶體的變數作為配置的資料來源,也可以將配置定義在持久化的檔案甚至資料庫中。在對配置系統進行系統介紹之前,我們先從程式設計的角度來體驗一下全新的配置讀取方式。

一、配置程式設計模型三要素

就程式設計層面來講,.NET Core的配置系統由如下圖所示的三個核心物件構成。讀取出來的配置資訊最終會轉換成一個IConfiguration物件供應用程式使用。IConfigurationBuilder是IConfiguration物件的構建者,而IConfigurationSource則代表配置資料最原始的來源。

在讀取配置的時候,我們根據配置的定義方式(資料來源)建立相應的IConfigurationSource物件,並將其註冊到IConfigurationBuilder物件上。提供配置的最初來源可能不止一個,我們可以註冊多個相同或者不同型別的IConfigurationSource物件到同一個IConfigurationBuilder物件上。IConfigurationBuilder物件正是利用註冊的這些IConfigurationSource物件提供的資料構建出我們在程式中使用的IConfiguration物件。

這裡介紹的IConfiguration、IConfigurationSource和IConfigurationBuilder介面以及其他一些基礎型別均定義在NuGet包“Microsoft.Extensions.Configuration.Abstractions”中。對這些介面的預設實現,則大多定義在“Microsoft.Extensions.Configuration”這個NuGet包中。

二、以鍵值對的形式讀取配置

雖然大部分情況下的配置從整體來說都具有結構化層次關係,但是“原子”配置項都以體現為最簡單的“鍵值對”形式,並且鍵和值通常都是字串。接下來我們會通過一個簡單的例項來演示如何以鍵值對的形式來讀取配置。

假設我們的應用程式需要通過配置來設定日期/時間的顯示格式,為此我們將相關的配置資訊定義在如下所示的這個DateTimeFormatOptions類中,它的四個屬性體現了針對DateTime物件的四種顯示格式(分別為長日期/時間和短日期/時間)。

public class DateTimeFormatOptions
{
    ...
    public string LongDatePattern { get; set; }
    public string LongTimePattern { get; set; }
    public string ShortDatePattern { get; set; }
    public string ShortTimePattern { get; set; }
}

我們希望通過配置的形式來控制由DateTimeFormatOptions的四個屬性所體現的顯示格式,所以我們為它定義了一個建構函式。如下面的程式碼片段所示,該建構函式具有一個IConfiguration介面型別的引數。鍵值對是配置的基本表現形式,所以IConfiguration物件提供了索引使我們可以根據配置項的Key得到配置項的值,下面的程式碼正是以索引的方式得到對應配置資訊的。

public class DateTimeFormatOptions
{
    ...
    public DateTimeFormatOptions (IConfiguration config)
    {
        LongDatePattern = config["LongDatePattern"];
        LongTimePattern = config["LongTimePattern"];
        ShortDatePattern = config["ShortDatePattern"];
        ShortTimePattern = config ["ShortTimePattern"];
    }
}

要建立一個體現當前配置的DateTimeFormatOptions物件,我們必須提供這個承載相關配置資訊的IConfiguration物件。正如我們前面所說,IConfiguration物件是由IConfigurationBuilder物件建立的,而原始的配置資訊則是通過相應的IConfigurationSource物件來提供,所以建立一個IConfiguration物件的正確程式設計方式是:建立一個ConfigurationBuilder(IConfigurationBuilder介面的預設實現型別)物件併為之註冊一個或者多個IConfigurationSource物件,最後利用它來建立我們需要的IConfiguration物件。

我們通過如下的程式來讀取配置並將其轉換成一個DateTimeFormatOptions物件。簡單起見,我們採用的IConfigurationSource實現型別為MemoryConfigurationSource,它直接利用一個儲存在記憶體中的字典物件作為最初的配置來源。如下面的程式碼片段所示,我們在為MemoryConfigurationSource提供的字典物件中設定了四種類型的日期/時間顯示格式。

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["longDatePattern"] = "dddd, MMMM d, yyyy",
            ["longTimePattern"] = "h:mm:ss tt",
            ["shortDatePattern"] = "M/d/yyyy",
            ["shortTimePattern"] = "h:mm tt"
        };

        var config = new ConfigurationBuilder()
            .Add(new MemoryConfigurationSource { InitialData = source })
            .Build();

        var options = new DateTimeFormatOptions(config);
        Console.WriteLine($"LongDatePattern: {options.LongDatePattern}");
        Console.WriteLine($"LongTimePattern: {options.LongTimePattern}");
        Console.WriteLine($"ShortDatePattern: {options.ShortDatePattern}");
        Console.WriteLine($"ShortTimePattern: {options.ShortTimePattern}");
    }
}

在上面的程式碼片段中,我們建立了一個ConfigurationBuilder物件,並在它上面註冊一個根據記憶體字典建立的MemoryConfigurationSource物件。我們接下來呼叫ConfigurationBuilder的Build方法創建出IConfiguration物件,並利用它創建出了DateTimeFormatOptions物件。為了驗證該Options物件是否與原始的配置一致,我們將它的四個屬性列印在控制檯上。程式執行之後,控制檯上將會產生如下所示的輸出結果。

三、 讀取結構化的配置

真實專案中涉及的配置大都具有結構化的層次結構,所以IConfiguration物件同樣具有這樣的結構。由於配置具有一個樹形層次結構,我們不妨將其稱之為“配置樹”,一個IConfiguration物件對應著這棵配置樹的某個節點,而整棵配置樹自然可以由根節點對應的IConfiguration物件來表示。以鍵值對體現的“原子配置項”對應著配置樹中不具有子節點的“葉子節點”。

接下來我們同樣以例項的方式來演示如何定義並讀取具有層次結構的配置資料。我們依然沿用上面的應用場景,不過現在我們不僅僅需要設定日期/時間的格式,還需要設定其他資料型別的格式,比如表示貨幣的Decimal型別。為此我們定義瞭如下一個CurrencyDecimalFormatOptions類,它的屬性Digits和Symbol分別表示小數位數和貨幣符號,一個CurrencyDecimalFormatOptions物件依然是利用一個IConfiguration物件來建立的。

public class CurrencyDecimalFormatOptions
{
    public int Digits { get; set; }
    public string Symbol { get; set; }

    public CurrencyDecimalFormatOptions (IConfiguration config)
    {
        Digits = int.Parse(config["Digits"]);
        Symbol = config["Symbol"];
    }
}

我們定義了另一個名為FormatOptions的型別來表示針對不同資料型別的格式設定。如下面的程式碼片段所示,它的兩個屬性DateTime和CurrencyDecimal分別表示針對日期/時間和貨幣數字的格式設定。FormatOptions依然具有一個引數型別為IConfiguration的建構函式,它的兩個屬性均在此建構函式中被初始化。值得注意的是初始化這兩個屬性採用的是當前IConfiguration的“子配置節”,我們通過呼叫GetSection方法根據指定的名稱(“DateTime”和“CurrencyDecimal”)獲得這兩個子配置節。

public class FormatOptions
{
    public DateTimeFormatOptions DateTime { get; set; }
    public CurrencyDecimalFormatOptions CurrencyDecimal { get; set; }

    public FormatOptions (IConfiguration config)
    {
        DateTime = new DateTimeFormatOptions ( config.GetSection("DateTime"));
        CurrencyDecimal = new CurrencyDecimalFormatOptions (config.GetSection("CurrencyDecimal"));
    }
}

FormatOptions型別體現的配置具有如圖6-3所示的樹形層次結構。在我們前面演示的例項中,我們使用一個MemoryConfigurationSource物件來提供原始的配置資訊。由於承載原始配置資訊的是一個元素型別為KeyValuePair<string, string>的集合,它在物理儲存上並不具有樹形化的層次結構,那麼它如何能夠提供一個結構化的IConfiguration物件承載的資料呢?

解決方案其實很簡單,對於一棵完整的配置樹,具體的配置資訊最終是通過葉子節點來承載的,所以MemoryConfigurationSource只需要在配置字典中儲存葉子節點的資料即可。除此之外,為了描述配置樹的結構,配置字典需要將對應葉子節點在配置樹中的路徑作為Key。所以MemoryConfigurationSource可以採用下表6-1所示的配置字典對配置樹進行“扁平化”,作為Key的路徑採用冒號(“:”)作為分隔符。

Key

Value
Format:DateTime:LongDatePattern dddd, MMMM d, yyyy
Format:DateTime:LongTimePattern h:mm:ss tt
Format:DateTime:ShortDatePattern M/d/yyyy
Format:DateTime:ShortTimePattern h:mm tt
Format:CurrencyDecimal:Digits 2
Format:CurrencyDecimal:Symbol $

如下面的程式碼片段所示,我們按照表6-1所示的結構建立了一個Dictionary<string, string>物件,並利用它創建出MemoryConfigurationSource物件。在利用ConfigurationBuilder得到IConfiguration物件之後,我們呼叫其GetSection方法得到名稱為“Format”的配置節,並利用後者建立一個FormatOptions。

public class Program
{
    public static void Main()
    {
        var source = new Dictionary<string, string>
        {
            ["format:dateTime:longDatePattern"] = "dddd, MMMM d, yyyy",
            ["format:dateTime:longTimePattern"] = "h:mm:ss tt",
            ["format:dateTime:shortDatePattern"] = "M/d/yyyy",
            ["format:dateTime:shortTimePattern"] = "h:mm tt",

            ["format:currencyDecimal:digits"] = "2",
            ["format:currencyDecimal:symbol"] = "$",
        };
        var configuration = new ConfigurationBuilder()
                .Add(new MemoryConfigurationSource { InitialData = source })
                .Build();

        var options = new FormatOptions(configuration.GetSection("Format"));
        var dateTime = options.DateTime;
        var currencyDecimal = options.CurrencyDecimal;

        Console.WriteLine("DateTime:");
        Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}");
        Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}");
        Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}");
        Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}");

        Console.WriteLine("CurrencyDecimal:");
        Console.WriteLine($"\tDigits:{currencyDecimal.Digits}");
        Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}");
    }
}

在得到利用讀取的配置建立的 FormatOptions物件之後,為了驗證該物件與原始配置資料是否一致,我們依然將它的相關屬性列印在控制檯上。這個程式執行之後會在控制檯上呈現如下所示的輸出結果。