理解ASP.NET Core - 配置(Configuration)


注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄

配置提供程序

在.NET中,配置是通過多種配置提供程序來提供的,包括以下幾種:

  • 文件配置提供程序
  • 環境變量配置提供程序
  • 命令行配置提供程序
  • Azure應用配置提供程序
  • Azure Key Vault 配置提供程序
  • Key-per-file配置提供程序
  • 內存配置提供程序
  • 應用機密(機密管理器)
  • 自定義配置提供程序

為了方便大家后續了解配置,這里先簡單提一下選項(Options),它是用於以強類型的方式對程序配置信息進行訪問的一種方式。接下來的示例中,我會添加一個簡單的配置Book,結構如下:

public class BookOptions
{
    public const string Book = "Book";

    public string Name { get; set; }

    public BookmarkOptions Bookmark { get; set; }

    public List<string> Authors { get; set; }
}

public class BookmarkOptions
{
    public string Remarks { get; set; }
}

然后我們在Startup.ConfigureServices中使用IConfiguration進行配置的讀取,並顯示在控制台中,如下:

public void ConfigureServices(IServiceCollection services)
{
    var book = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
    Console.WriteLine($"Book Name: {book.Name}" +
        $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
        $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
}

接下來,就挑幾個常用的配置提供程序來詳細講解一下。

文件配置提供程序

顧名思義,就是從文件中加載配置。文件細分為

  • JSON配置提供程序(JsonConfigurationProvider)
  • XML配置提供程序(XmlConfigurationProvider)
  • INI配置提供程序(IniConfigurationProvider)

以上這些配置提供程序,均繼承於抽象類FileConfigurationProvider

另外,所有文件配置提供程序都支持提供兩個配置參數:

  • optionalbool類型,指示該文件是否是可選的。如果該參數為false,但是指定的文件又不存在,則會報錯。
  • reloadOnChangebool類型,指示該文件發生更改時,是否要重新加載配置。

JSON配置提供程序

通過JsonConfigurationProvider在運行時從Json文件中加載配置。

Install-Package Microsoft.Extensions.Configuration.Json

使用方式非常簡單,只需要調用AddJsonFile擴展方法添加用於保存配置的Json文件即可:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            // 清空所有配置提供程序
            config.Sources.Clear();

            var env = context.HostingEnvironment;

            // 添加 appsettings.json 和 appsettings.{env.EnvironmentName}.json 兩個json文件
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
        });

你可以在 appsetting.json 中添加如下配置:

{
  "Book": {
    "Name": "appsettings.json book name",
    "Authors": [
      "appsettings.json author name A",
      "appsettings.json author name B"
    ],
    "Bookmark": {
      "Remarks": "appsettings.json bookmark remarks"
    }
  }
}

XML配置提供程序

通過XmlConfigurationProvider在運行時從Xml文件中加載配置。

Install-Package Microsoft.Extensions.Configuration.Xml

同樣的,只需調用AddXmlFile擴展方法添加Xml文件即可:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true);
        });

你可以在 appsettings.xml 中添加如下配置:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <Book>
    <Name>appsettings.xml book name</Name>
    <Authors name="0">appsettings.xml author name A</Authors>
    <Authors name="1">appsettings.xml author name B</Authors>
     <Bookmark>
      <Remarks>appsettings.xml bookmark remarks</Remarks>
    </Bookmark>
  </Book>
</configuration>

在 .NET 6 中,我們就不用手動添加 name 屬性來指定索引了,它會自動進行索引編號。

INI配置提供程序

通過IniConfigurationProvider在運行時從Ini文件中加載配置。

Install-Package Microsoft.Extensions.Configuration.Ini

同樣的,只需調用AddIniFile擴展方法添加Ini文件即可:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            config.AddIniFile("appsettings.ini", optional: true, reloadOnChange: true);
        });

你可以在 appsettings.ini 中添加如下配置

[Book]
Name=appsettings.ini book name
Authors:0=appsettings.ini book author A
Authors:1=appsettings.ini book author B

[Book:Bookmark]
Remarks=appsettings.ini bookmark remarks

環境變量配置提供程序

通過EnvironmentVariablesConfigurationProvider在運行時從環境變量中加載配置。

Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables

同樣的,只需調用AddEnvironmentVariables擴展方法添加環境變量即可:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            // 添加前綴為 My_ 的環境變量
            config.AddEnvironmentVariables(prefix: "My_");
        });

在添加環境變量時,通過指定參數prefix,只讀取限定前綴的環境變量。不過在讀取環境變量時,會將前綴刪除。如果不指定參數prefix,那么會讀取所有環境變量。

當創建默認通用主機(Host)時,默認就已經添加了前綴為DOTNET_的環境變量,加載應用配置時,也添加了未限定前綴的環境變量。另外,在 ASP.NET Core 中,配置 Web主機時,默認添加了前綴為ASPNETCORE_的環境變量。

需要注意的是,由於環境變量的分層鍵:並不受所有平台支持,而雙下划線(__)是全平台支持的,所以要使用雙下划線(__)來代替冒號(:)。

在 Windows 平台下,可以通過setsetx命令進行環境變量配置,不過:

  • set命令設置的環境變量是臨時的,僅在當前進程有效,這個進程就是當前cmd窗口啟動的。也就是說,當你打開一個cmd窗口時,通過set命令設置了環境變量,然后通過dotnet xxx.dll啟動了你的應用程序,是可以讀取到環境變量的,但是在該cmd窗口之外,例如通過VS啟動應用程序,是無法讀取到該環境變量的。
  • setx命令設置的環境變量是持久化的。可選的添加/M開關,表示將該環境變量配置到系統環境中(需要管理員權限),否則,將添加到用戶環境中。

我更喜歡通過setx去設置環境變量(記得以管理員身份運行哦):

# 注意,這里的 My_ 是前綴
setx My_Book__Name "Environment variables book name" /M
setx My_Book__Authors__0 "Environment variables book author A" /M
setx My_Book__Authors__1 "Environment variables book author B" /M
setx My_Book__Bookmark__Remarks "Environment variables bookmark remakrs" /M

配置完環境變量后,一定要記得重啟VS或cmd窗口,否則是無法讀取到最新的環境變量值的

連接字符串前綴的特殊處理

當沒有向AddEnvironmentVariables傳入前綴時,默認也會針對含有以下前綴的環境變量進行特殊處理:

前綴 環境變量Key 配置Key 配置提供程序
MYSQLCONNSTR_ MYSQLCONNSTR_{KEY} ConnectionStrings:{KEY} MySQL
SQLCONNSTR_ SQLCONNSTR_{KEY} ConnectionStrings:{KEY} SQL Server
SQLAZURECONNSTR_ SQLAZURECONNSTR_{KEY} ConnectionStrings:{KEY} Azure SQL
CUSTOMCONNSTR_ CUSTOMCONNSTR_{KEY} ConnectionStrings:{KEY} 自定義配置提供程序

在 launchSettings.json 中配置環境變量

在 ASP.NET Core 模板項目中,會生成一個 launchSettings.json 文件,我們也可以在該文件中配置環境變量。

需要注意的是,launchSettings.json 中的配置只用於開發環境,並且在該文件中設置的環境變量會覆蓋在系統環境中設置的變量。

{
  "WebApplication": {
    "commandName": "Project",
    "dotnetRunMessages": "true",
    "launchBrowser": true,
    "launchUrl": "swagger",
    "applicationUrl": "http://localhost:5000",      // 設置環境變量 ASPNETCORE_URLS
    "environmentVariables": {
      "ASPNETCORE_ENVIRONMENT": "Development",
      "My_Book__Name": "launchSettings.json Environment variables book name",
      "My_Book__Authors__0": "launchSettings.json Environment variables book author A",
      "My_Book__Authors__1": "launchSettings.json Environment variables book author B",
      "My_Book__Bookmark__Remarks": "launchSettings.json Environment variables bookmark remarks"
    }
  }
}

雖然說在 launchSettings.json 中配置環境變量時可以使用冒號(:)作為分層鍵,但是我在測試過程中,發現當同時配置了系統環境變量時,程序讀取到的環境變量值會發生錯亂(一部分是系統環境變量,一部分是該文件中的環境變量)。所以建議大家還是使用雙下划線(__)作為分層鍵。

在Linux平台,當設置的環境變量為URL時,需要設置為轉義后的URL。可以使用systemd-escaple工具:

$ systemd-escape http://localhost:5001
http:--localhost:5001

命令行配置提供程序

通過CommandLineConfigurationProvider在運行時從命令行參數鍵值對中加載配置。

Install-Package Microsoft.Extensions.Configuration.CommandLine

通過調用AddCommandLine擴展方法,並傳入參數args

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            config.AddCommandLine(args);
        });

有三種設置命令行參數的方式:

使用=

dotnet run Book:Name="Command line book name" Book:Authors:0="Command line book author A" Book:Authors:1="Command line book author B" Book:Bookmark:Remarks="Command line bookmark remarks"

使用/

dotnet run /Book:Name "Command line book name" /Book:Authors:0 "Command line book author A" /Book:Authors:1 "Command line book author B"  /Book:Bookmark:Remarks "Command line bookmark remarks"

使用--

dotnet WebApplication5.dll --Book:Name "Command line book name" --Book:Authors:0 "Command line book author A" --Book:Authors:1 "Command line book author B" --Book:Bookmark:Remarks "Command line bookmark remarks"

交換映射

該功能是針對命令行配置參數進行key映射的,如你可以將n映射為Name,要求:

  • 交換映射key必須以---開頭。當使用-開頭時,命令行參數書寫時也要以-開頭,當使用--開頭時,命令行參數書寫時可以以--/開頭。
  • 交換映射字典中的key不區分大小寫,不能包含重復key。如不能同時出現-n-N,但可以同時出現-n--n

接下來我們來映射一下:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            var switchMappings = new Dictionary<string, string>
            {
                ["--bn"] = "Book:Name",
                ["-ba0"] = "Book:Authors:0",
                ["--ba1"] = "Book:Authors:1",
                ["--bmr"] = "Book:Bookmark:Remarks"
            };
            config.AddCommandLine(args, switchMappings);
        });

然后以命令行命令啟動:

dotnet run --bn "Command line book name" -ba0 "Command line book author A" /ba1 "Command line book author B" --bmr="Command line bookmark remarks"

內存配置提供程序

通過MemoryConfigurationProvider在運行時從內存中的集合中加載配置。

Install-Package Microsoft.Extensions.Configuration

通過調用AddInMemoryCollection添加內存配置:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string>
            {
                ["Book:Name"] = "Memmory book name",
                ["Book:Authors:0"] = "Memory book author A",
                ["Book:Authors:1"] = "Memory book author B",
                ["Book:Bookmark:Remarks"] = "Memory bookmark remarks"
            });
        });

主機(Host)中的默認配置優先級

約定:越后添加的配置提供程序優先級越高,優先級高的配置值會覆蓋優先級低的配置值

在 主機(Host)中,我們介紹了Host的啟動流程,根據默認的配置提供程序的添加順序,默認的優先級從低到高為(我順便將WebHost默認配置的也加進來了):

  1. 內存配置提供程序 環境變量配置提供程序(prefix: DOTNET_)
  2. 環境變量配置提供程序(prefix: ASPNETCORE_)
  3. JSON配置提供程序(appsettings.json)
  4. JSON配置提供程序(appsettings.{Environment}.json)
  5. 機密管理器(僅Windows)
  6. 環境變量配置提供程序(未限定前綴)
  7. 命令行配置提供程序

完整的配置提供程序列表可以通過 IConfigurationRoot.Providers 來查看。

如果想要添加額外配置文件,但是仍然想要環境變量或命令行參數優先,則可以類似這樣做:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) =>
        {
            config.AddJsonFile("my.json", optional: true, reloadOnChange: true);
            
            config.AddEnvironmentVariables();
            config.AddCommandLine(args);
        });

配置體系

上面我們已經了解了幾種常用的配置提供程序,這是微軟已經提供的。如果你看過某個配置提供程序的源碼的話,一定見過IConfigurationSourceIConfigurationProvider等接口。

IConfigurationSource

IConfigurationSource負責創建IConfigurationProvider實現的實例。它的定義很簡單,就一個Build方法,返回IConfigurationProvider實例:

public interface IConfigurationSource
{
    IConfigurationProvider Build(IConfigurationBuilder builder);
}

IConfigurationProvider

IConfigurationProvider負責實現配置的設置、讀取、重載等功能,並以鍵值對形式提供配置。

所有配置提供程序均建議繼承於抽象類ConfigurationProvider,該類實現了接口IConfigurationProvider

public interface IConfigurationProvider
{
    // 獲取指定父路徑下的直接子節點Key,然后 Concat(earlierKeys) 一同返回
    IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
    
    // 當該配置提供程序支持更改追蹤(change tracking)時,會返回 change token
    // 否則,返回 null
    IChangeToken GetReloadToken();

    // 加載配置
    void Load();

    // 設置 key:value
    void Set(string key, string value);

    // 嘗試獲取指定 key 的 value
    bool TryGet(string key, out string value);
}

public abstract class ConfigurationProvider : IConfigurationProvider
{
    // 包含了該配置提供程序的所有葉子節點的配置項
    protected IDictionary<string, string> Data { get; set; }

    protected ConfigurationProvider() { }

    // 從 Data 中查找指定父路徑下的直接子節點Key,然后 Concat(earlierKeys) 一同返回
    public virtual IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath) { }

    public IChangeToken GetReloadToken() { }

    // 將配置項賦值到 Data 中
    public virtual void Load() { }

    protected void OnReload() { }

    // 設置 Data key:value
    public virtual void Set(string key, string value) { }

    public override string ToString() { }
    
    // 嘗試從 Data 中獲取指定 key 的 value
    public virtual bool TryGet(string key, out string value) { }
}

Data包含了該配置提供程序的所有葉子節點的配置項。拿上方的Book示例來說,該Data包含“Book:Name”、“Book:Authors:0”、“Book:Authors:1”和“Book:Bookmark:Remarks”這4個Key。

另外,你可能還會見到一個名為ChainedConfigurationProvider的配置提供程序,它可以將一個已存在的IConfiguration實例,作為配置提供程序添加到另一個IConfiguration中。例如HostConfiguration流轉到AppConfiguration就使用了這個。

IConfigurationBuilder

public interface IConfigurationBuilder
{
    // 存放用於該 Builder 的 Sources 列表中各個元素的共享字典
    IDictionary<string, object> Properties { get; }

    // 已注冊的 IConfigurationSource 列表
    IList<IConfigurationSource> Sources { get; }

    // 將 IConfigurationSource 添加到 Sources 中
    IConfigurationBuilder Add(IConfigurationSource source);

    // 通過 Sources 構建配置提供程序實例,並創建 IConfigurationRoot 實例
    IConfigurationRoot Build();
}

ConfigurationBuilder實現了IConfigurationBuilder接口:

public class ConfigurationBuilder : IConfigurationBuilder
{
    public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();

    public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

    public IConfigurationBuilder Add(IConfigurationSource source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }

        Sources.Add(source);
        return this;
    }

    public IConfigurationRoot Build()
    {
        var providers = new List<IConfigurationProvider>();
        foreach (IConfigurationSource source in Sources)
        {
            IConfigurationProvider provider = source.Build(this);
            providers.Add(provider);
        }
        return new ConfigurationRoot(providers);
    }
}

IConfiguration

public interface IConfiguration
{
    // 獲取或設置指定配置 key 的 value
    string this[string key] { get; set; }
    
    // 獲取當前配置節點的 直接 子節點列表
    IEnumerable<IConfigurationSection> GetChildren();

    // 獲取監控配置發生更改的 token
    IChangeToken GetReloadToken();
    
    // 獲取指定Key的配置子節點
    IConfigurationSection GetSection(string key);
}

GetValue

通過IConfiguration的擴展方法ConfigurationBinder.GetValue ,可以以類似字典的方式,讀取某個Key對應的Value。

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var bookName = Configuration.GetValue<string>("Book:Name", defaultValue: "Unknown");
        Console.WriteLine(bookName);
    }
}

該擴展的實質(默認實現)是在底層通過調用IConfigurationProvider.TryGet方法,讀取ConfigurationProvider.Data字典中的鍵值對。所以,只能通過該擴展方法讀取葉子節點的配置值。

GetSection

通過IConfiguration.GetSection方法,可以獲取到指定Key的配置子節點:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 返回的 section 永遠不會為 null
        IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);

        IConfigurationSection bookmarkSection = bookSection.GetSection("Bookmark");
        // or
        //IConfigurationSection bookmarkSection = Configuration.GetSection("Book:Bookmark");

        var remarks = bookmarkSection["Remarks"];
        Console.WriteLine(remarks);
    }
}

GetChildren

通過IConfiguration.GetChildren方法,可以獲取到當前配置節點的直接子節點列表

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // children 包含了 Name、Bookmark、Authors
        var children = Configuration.GetSection(BookOptions.Book).GetChildren();
        foreach (var child in children)
        {
            Console.WriteLine($"Key: {child.Key}\tValue: {child.Value}");
        }
    }
}

Exists

前面提到了,Configuration.GetSection永遠不會返回null,那么我們如何判斷該 Section 是否真的存在呢?這就要用到擴展方法ConfigurationExtensions.Exists了:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);
        if (bookSection.Exists())
        {
            var notExistSection = bookSection.GetSection("NotExist");
            if (!notExistSection.Exists())
            {
                Console.WriteLine("Book:NotExist");
            }
        }
    }
}

這里分析一下Exists的源碼:

public static class ConfigurationExtensions
{
    public static bool Exists(this IConfigurationSection section)
    {
        if (section == null)
        {
            return false;
        }
        return section.Value != null || section.GetChildren().Any();
    }
}

因此,在這里補充一下:假設存在某個子節點(ConfigurationSection),若該子節點為葉子節點,那么其Value一定不為null,若該子節點非葉子節點,則該子節點的子節點一定不為空

Get

通過ConfigurationBinder.Get 方法,可以將配置以強類型的方式綁定到選項對象上:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var book = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
        Console.WriteLine($"Book Name: {book.Name}" +
        $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
        $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
    }
}

Bind

與上方Get方法類似,通過ConfigurationBinder.Bind 方法,可以將配置以強類型的方式綁定到已存在的選項對象上:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var book = new BookOptions();
        Configuration.GetSection(BookOptions.Book).Bind(book);
        Console.WriteLine($"Book Name: {book.Name}" +
        $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
        $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
    }
}

IConfigurationRoot

IConfigurationRoot表示配置的,相應的,下面要提到的IConfigurationSection則表示配置的子節點。舉個例子,XML格式的文檔都會有一個根節點(如上方示例中的<configuration>),還可以包含多個子節點(如上方示例中的<Book><Name>等)。

public interface IConfigurationRoot : IConfiguration
{
    // 存放了當前應用程序的所有配置提供程序
    IEnumerable<IConfigurationProvider> Providers { get; }

    // 強制從配置提供程序中重載配置
    void Reload();
}

ConfigurationRoot實現了IConfigurationRoot接口,下面就着重看一下Reload方法的實現:

Startup構造函數中注入的IConfiguration其實就是ConfigurationRoot的實例。

public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
    private readonly IList<IConfigurationProvider> _providers;
    
    public ConfigurationRoot(IList<IConfigurationProvider> providers)
    {
        // 該構造函數內代碼有刪減
    
        _providers = providers;
        foreach (IConfigurationProvider p in providers)
        {
            p.Load();
        }
    }
    
    public void Reload()
    {
        foreach (IConfigurationProvider provider in _providers)
        {
            provider.Load();
        }
        
        // 此處刪減了部分代碼
    }
}

IConfigurationSection

IConfigurationSection表示配置的子節點。

public interface IConfigurationSection : IConfiguration
{
    // 該子節點在其父節點中所表示的 key
    string Key { get; }

    // 該子節點在配置中的全路徑(從根節點開始,到當前節點的路徑)
    string Path { get; }

    // 該子節點的 value。如果該子節點下存在孩子節點,則其始終為 null
    string Value { get; set; }
}

借用上方的數據舉個例子,假設配置提供程序為內存:

  • 當我們通過Configuration.GetSection("Book:Name")獲取到子節點時,Key為“Name”,Path為“Book:Name”,Value則為“Memmory book name”
  • 當我們通過Configuration.GetSection("Book:Bookmark")獲取到子節點時,Key為“Bookmark”,Path為“Book:Name”,Value則為null

實現自定義配置提供程序

既然我們已經理解了.NET中的配置體系,那我們完全可以自己動手實踐一下了,現在就來實現一個自定義的配置提供程序來玩玩。

日常使用的配置中心客戶端,如Apollo等,都是通過實現自定義配置提供程序來提供配置的。

咱們不搞那么復雜,就基於ORM框架EF Core來實現一個自定義配置提供程序,具體邏輯是這樣的:數據庫中有一個JsonConfiguration數據集,專門用來存放Json格式的配置。該表有KeyValue兩個字段,Key對應例子中的“Book”,而Value則是“Book”對應值的Json字符串。

首先,裝一下Nuget包:

Install-Package Microsoft.EntityFrameworkCore.InMemory

然后定義自己的DbContext——AppDbContext

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions options) 
        : base(options) { }

    public virtual DbSet<JsonConfiguration> JsonConfigurations { get; set; }
}

public class JsonConfiguration
{
    [Key]
    public string Key { get; set; }

    public string Value { get; set; }
}

接下來,通過EFConfigurationSource來構建EFConfigurationProvider實例:

public class EFConfigurationSource : IConfigurationSource
{
    private readonly Action<DbContextOptionsBuilder> _optionsAction;

    public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)
    {
        _optionsAction = optionsAction;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new EFConfigurationProvider(_optionsAction);
    }
}

接着,就是EFConfigurationProvider的實現了,邏輯類似於Json文件配置提供程序,只不過配置來源於EF而不是Json文件:

public class EFConfigurationProvider : ConfigurationProvider
{
    public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
    {
        OptionsAction = optionsAction;
    }

    Action<DbContextOptionsBuilder> OptionsAction { get; }

    public override void Load()
    {
        var builder = new DbContextOptionsBuilder<AppDbContext>();

        OptionsAction(builder);

        using var dbContext = new AppDbContext(builder.Options);

        dbContext.Database.EnsureCreated();

        // 如果沒有任何配置則添加默認配置
        if (!dbContext.JsonConfigurations.Any())
        {
            CreateAndSaveDefaultValues(dbContext);
        }

        // 將配置項轉換為鍵值對(key和value均為字符串類型)
        Data = EFJsonConfigurationParser.Parse(dbContext.JsonConfigurations);
    }

    private static void CreateAndSaveDefaultValues(AppDbContext dbContext)
    {
        dbContext.JsonConfigurations.AddRange(new[]
        {
            new JsonConfiguration
            {
                Key = "Book",
                Value = JsonSerializer.Serialize(
                new BookOptions()
                {
                    Name = "ef configuration book name",
                    Authors = new List<string>
                    {
                        "ef configuration book author A",
                        "ef configuration book author B"
                    },
                    Bookmark = new BookmarkOptions
                    {
                        Remarks = "ef configuration bookmark Remarks"
                    }
                })
            }
        });

        dbContext.SaveChanges();
    }
}

internal class EFJsonConfigurationParser
{
    private EFJsonConfigurationParser() { }

    private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    private readonly Stack<string> _context = new();
    private string _currentPath;

    public static IDictionary<string, string> Parse(DbSet<JsonConfiguration> inputs)
        => new EFJsonConfigurationParser().ParseJsonConfigurations(inputs);

    private IDictionary<string, string> ParseJsonConfigurations(DbSet<JsonConfiguration> inputs)
    {
        _data.Clear();

        if(inputs?.Any() != true)
        {
            return _data;
        }

        var jsonDocumentOptions = new JsonDocumentOptions
        {
            CommentHandling = JsonCommentHandling.Skip,
            AllowTrailingCommas = true,
        };

        foreach (var input in inputs)
        {
            ParseJsonConfiguration(input, jsonDocumentOptions);
        }

        return _data;
    }

    private void ParseJsonConfiguration(JsonConfiguration input, JsonDocumentOptions options)
    {
        if (string.IsNullOrWhiteSpace(input.Key))
            throw new FormatException($"The key {input.Key} is invalid.");

        var jsonValue = $"{{\"{input.Key}\": {input.Value}}}";
        using var doc = JsonDocument.Parse(jsonValue, options);

        if (doc.RootElement.ValueKind != JsonValueKind.Object)
            throw new FormatException($"Unsupported JSON token '{doc.RootElement.ValueKind}' was found.");

        VisitElement(doc.RootElement);
    }

    private void VisitElement(JsonElement element)
    {
        foreach (JsonProperty property in element.EnumerateObject())
        {
            EnterContext(property.Name);
            VisitValue(property.Value);
            ExitContext();
        }
    }

    private void VisitValue(JsonElement value)
    {
        switch (value.ValueKind)
        {
            case JsonValueKind.Object:
                VisitElement(value);
                break;

            case JsonValueKind.Array:
                var index = 0;
                foreach (var arrayElement in value.EnumerateArray())
                {
                    EnterContext(index.ToString());
                    VisitValue(arrayElement);
                    ExitContext();
                    index++;
                }
                break;

            case JsonValueKind.Number:
            case JsonValueKind.String:
            case JsonValueKind.True:
            case JsonValueKind.False:
            case JsonValueKind.Null:
                var key = _currentPath;
                if (_data.ContainsKey(key))
                    throw new FormatException($"A duplicate key '{key}' was found.");

                _data[key] = value.ToString();
                break;

            default:
                throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
        }
    }

    private void EnterContext(string context)
    {
        _context.Push(context);
        _currentPath = ConfigurationPath.Combine(_context.Reverse());
    }

    private void ExitContext()
    {
        _context.Pop();
        _currentPath = ConfigurationPath.Combine(_context.Reverse());
    }
}

其中,EFJsonConfigurationParser是我借鑒JsonConfigurationFileParser而實現的,這也是學習優秀設計的一種方式!

接着,我們按照AddXXX的格式將該配置提供程序的添加封裝為擴展方法:

public static class EntityFrameworkExtensions
{
    public static IConfigurationBuilder AddEFConfiguration(
        this IConfigurationBuilder builder,
        Action<DbContextOptionsBuilder> optionsAction)
    {
        return builder.Add(new EFConfigurationSource(optionsAction));
    }
}

這時,我們就可以使用擴展方法添加EFConfigurationProvider了:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, config) => 
        {
            config.AddEFConfiguration(options => options.UseInMemoryDatabase("configdb"));
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

最后,你可以試着讀取一下Book配置了,看看是不是如咱們所期望的那樣,讀取到EF中的配置呢?這里,我就不再演示了。

其他

查看所有配置項

通過擴展方法ConfigurationExtensions.AsEnumerable,來查看所有配置項:

public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();

    var config = host.Services.GetRequiredService<IConfiguration>();

    foreach (var c in config.AsEnumerable())
    {
        Console.WriteLine(c.Key + " = " + c.Value);
    }
    host.Run();
}

通過委托配置選項

除了可以通過配置提供程序來提供配置外,也可以通過委托來提供配置:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<BookOptions>(book =>
    {
        book.Name = "delegate book name";
        book.Authors = new List<string> { "delegate book author A", "delegate book author A" };
        book.Bookmark = new BookmarkOptions { Remarks = "delegate bookmark reamarks" };
    });
}

關於選項的更多理解,將在后續章節進行詳細講解。

注意事項

配置Key

  • 不區分大小寫。例如Namename被視為等效的。
  • 配置提供程序有很多種,如果在多個提供程序中添加了某個配置項,那么,只有在最后一個提供程序中配置的才會生效。
  • 分層鍵:
    • 在環境變量中,由於冒號(:)無法適用於所有平台,所以要使用全平台均支持的雙下划線(__),它會在程序中自動轉換為冒號(:
    • 在其他類型的配置中,一般均使用冒號(:)分隔符即可
  • ConfigurationPath類提供了一些輔助方法。

配置Value

  • 均被保存為字符串


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM