dotnet學習筆記-專題04-配置的讀取和寫入-01

random_d發表於2024-11-22

配置的讀取和寫入

讀取配置的類,包括手動從json中讀取配置、將json配置與配置類繫結、從控制檯讀取配置、從環境變數讀取配置

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

namespace LearnConfigurationSystem;

public static class ReadConfig
{
    public static void ReadConfigManually()
    {
        // 獲取ConfigurationBuilder例項
        var configurationBuilder = new ConfigurationBuilder();
        // configurationBuilder配置(path: 配置檔案路徑,optional: 配置檔案是否可選,reloadOnChange: 配置檔案改變時是否重新讀取配置)
        configurationBuilder.AddJsonFile(path: "config.json", optional: true, reloadOnChange: false);

        // 從ConfigurationBuilder例項構建實現了IConfigurationRoot介面的例項
        IConfigurationRoot config = configurationBuilder.Build();

        // 從配置檔案中讀取資料,讀取到的資料均用string?型別表示,即使對應資料在json中不是字串型別,如果沒有獲取到指定名稱的資料則用null表示
        string? user = config["userName"];
        string? proxyAddress = config.GetSection("proxy:address").Value;
        string? port = config["proxy:port"];

        Debug.Assert(user != null);
        Debug.Assert(proxyAddress != null);
        Debug.Assert(port != null);

        Console.WriteLine($"{user} - {proxyAddress}:{port}");
    }

    public static void ReadConfigurationThenMapToConfigurationModels()
    {
        var configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddJsonFile("config.json", optional: false, reloadOnChange: true);
        IConfigurationRoot config = configurationBuilder.Build();

        // 使用依賴注入容器管理依賴注入
        var service = new ServiceCollection();

        service.AddOptions()
            // 將config.json中的dataBaseConfig繫結到配置類DataBaseSettings(自動小駝峰->大駝峰轉換,配置類中的屬性大駝峰,json中寫小駝峰就行)
            .Configure<DataBaseSettings>(e => config.GetSection("dataBaseConfig").Bind(e))
            // 將config.json中的smtpConfig繫結到配置類SmtpSettings(自動小駝峰->大駝峰轉換,配置類中的屬性大駝峰,json中寫小駝峰就行)
            .Configure<SmtpSettings>(e => config.GetSection("smtpConfig").Bind(e));

        // 將Demo註冊為瞬態服務
        service.AddTransient<Demo>();

        // 建立ServiceProvider服務
        using var serviceProvider = service.BuildServiceProvider();

        // 迴圈3次,方便測試(更改配置檔案後檢視輸出是否變換)
        /* 注:修改和編譯得到的exe檔案處在同一資料夾下的config.json,不要修改原始碼下的config.json(程式讀取的不是這個config.json) */
        for (int i = 0; i < 3; i++)
        {
            // 建立Scope(每次迴圈一個Scope,防止上次迴圈的環境干擾)
            using var scope = serviceProvider.CreateScope();
            var serviceProviderOfScope = scope.ServiceProvider;
            // 從serviceProvider獲取對應的服務,這一服務只在當前scope範圍內生效
            var demo = serviceProviderOfScope.GetRequiredService<Demo>();
            demo.Test();
            Console.WriteLine("可以改配置了");
            Console.ReadKey();
        }
    }

    // args從Main函式傳遞
    public static void ReadConfigurationFromCommandLine(string[] args)
    {
        if (args.Length == 0)
        {
            Console.WriteLine($"{nameof(args)} is null. 沒有從控制檯接收額外的配置引數");
            return;
        }

        // 顯示從控制檯傳入的所有引數
        for (var index = 0; index < args.Length; index++)
        {
            var arg = args[index];
            Console.WriteLine($"ArgumentFromConsole: No.{index}, Content: {arg}");
        }

        var configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddCommandLine(args);
        var config = configurationBuilder.Build();
        var serverAddress = config["serverAddress"];
        Console.WriteLine($"serverAddress:{serverAddress ?? "null"}");
    }

    public static void ReadConfigurationFromEnvironmentVariables()
    {
        var configBuilder = new ConfigurationBuilder();
        // 讀取所有環境變數(不推薦,環境變數太多,全部讀入浪費資源,並且容易和其他程式的環境變數衝突)
        //configBuilder.AddEnvironmentVariables();
        // 讀取字首為 PROCESSOR_ 的環境變數
        configBuilder.AddEnvironmentVariables("PROCESSOR_");
        IConfigurationRoot config = configBuilder.Build();

        // 獲取環境變數PROCESSOR_IDENTIFIER
        // 注意:去除字首,例如:PROCESSOR_IDENTIFIER去除字首PROCESSOR_後變為IDENTIFIER
        var processorIdentifier = config["IDENTIFIER"];
        Console.WriteLine($"{nameof(processorIdentifier)}: {processorIdentifier ?? "null"}");
    }

    // 模型類
    public class DataBaseSettings
    {
        public string? DataBaseType { get; set; }
        public string? ConnectionString { get; set; }
    }

    public class SmtpSettings
    {
        public string? Server { get; set; }
        public string? UserName { get; set; }
        public string? Password { get; set; }
    }
 
    // 用於測試讀取配置的Demo類
    public class Demo
    {
        /* 類似的介面有:
         * 1. IOption<T>: 在配置改變後無法自動讀取新值,除非重啟程式
         * 2. IOptionMonitor<T>: 配置改變後,新值會立即生效,可能會造成資料不一致,
         *  例如:A、B先後讀取某個配置,配置在A執行後改變,A使用了舊值,B使用了新值
         * 3. IOptionSnapshot<T>: 配置改變後,新值會在下次進入這個範圍時生效,
         *  例如:A、B先後讀取某個配置,配置在A執行後改變,A使用了舊值,B也使用了舊值,但下次A、B再讀取配置時,讀取的都是新值,保證資料的一致性
         */
        /* 綜上,這三個介面優先使用IOptionSnapshot<T>介面 */
        private readonly IOptionsSnapshot<DataBaseSettings> _optionDataBaseSettings;
        private readonly IOptionsSnapshot<SmtpSettings> _optionSmtpSettings;

        public Demo(IOptionsSnapshot<DataBaseSettings> optionDataBaseSettings,
            IOptionsSnapshot<SmtpSettings> optionSmtpSettings)
        {
            _optionDataBaseSettings = optionDataBaseSettings;
            _optionSmtpSettings = optionSmtpSettings;
        }

        public void Test()
        {
            var db = _optionDataBaseSettings.Value;
            var smtp = _optionSmtpSettings.Value;
            Console.WriteLine($"Database: {db.DataBaseType ?? "null"}, {db.ConnectionString ?? "null"}");
            Console.WriteLine($"Smtp: {smtp.Server ?? "null"}, {smtp.UserName ?? "null"}, {smtp.Password ?? "null"}");
        }
    }
}

專案檔案(LearnConfigurationSystem.csproj)

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <!-- 程式在執行時預設載入EXE檔案同資料夾下的配置檔案,而不是專案中的config.json檔案。
             所以需要設定這一屬性,在生成專案時自動將config.json檔案複製到生成目錄。
        -->
        <None Update="config.json">
            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
    </ItemGroup>

    <ItemGroup>
        <!-- 讀取配置依賴的安裝包,其中:
            Microsoft.Extensions.Configuration是基礎包,
            Microsoft.Extensions.Configuration.Json用於讀取Json配置
            Microsoft.Extensions.Configuration.CommandLine用於從命令列讀取配置
            Microsoft.Extensions.Configuration.EnvironmentVariables用於從環境變數讀取配置
            Microsoft.Extensions.Options用於對映配置項,它可以輔助處理容器生命週期、配置重新整理等。
            Microsoft.Extensions.DependencyInjection用於依賴注入,配合Microsoft.Extensions.Options使用
            Microsoft.Extensions.Configuration.Binder用於將配置項與配置類繫結(對映)
        -->
        <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
        <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
        <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
    </ItemGroup>

</Project>

從資料庫讀取配置

說明

  1. 依據Zack.AnyDBConfigProvider專案,按照自己的編碼習慣重新構建。
  2. 該專案可從資料庫中讀取配置資訊

重點內容

  1. 任何自定義的ConfigurationProvider都要實現IConfigurationProvider介面,由於.NET中的抽象類ConfigurationProvider已經實現了IConfigurationProvider介面,所以一般的做法是繼承ConfigurationProvider,然後override抽象類ConfigurationProvider中的方法。
  2. Load方法用於載入配置資料,載入的資料按照鍵值對的形式儲存到Data屬性中。
  3. Data屬性是IDictionary<string,string>型別,其中鍵為配置的名字。
  4. 如果配置項發生變化,則需要呼叫OnReload方法通知訂閱者配置項發生改變。

DbConfigurationProvider類的構造方法

  1. 如果啟用了ReloadOnChange選項,那麼將一個委託方法送入佇列,等到執行緒池有執行緒可用時執行該委託方法(ThreadPool.QueueUserWorkItem)。
  2. 該委託方法在方法體內每間隔一段時間(透過Thread.Sleep實現)執行一次Load方法,直到DbConfigurationProvider類的例項被釋放。

Load方法

  1. Load方法首先建立了一個Data屬性的副本clonedData,用於稍後比較資料是否修改了。
  2. 讀取配置的程式碼最終會呼叫TryGet方法讀取配置,為了避免TryGet讀取到Load載入一半的資料,使用讀寫鎖控制讀寫同步。
  3. 由於讀的頻率高於寫的頻率,為了避免使用普通的鎖造成效能問題,這裡使用ReaderWriteLockSlim類(.NET自帶)實現“只允許一個執行緒寫入,允許多個執行緒讀取”。
  4. 為了實現3的寫鎖,需要把“將配置項寫入Data屬性“的程式碼放到EnterWriteLock和ExitWriteLock之間。
  5. 同時一定要把OnReload方法放到ExitWriteLock之後。這是因為OnReload方法中呼叫了TryGet方法,TryGet方法中有讀鎖,寫鎖中巢狀讀鎖是不被允許的
  6. Load中呼叫的DoLoad方法從資料庫中讀取配置,然後將資料載入到Data屬性中。DoLoad方法遵循”多層級資料扁平化規則“來解析和載入資料。
  7. 在6之後呼叫DataIsChanged方法將舊資料和從資料庫中讀取的新資料比較,如果發現資料有變化就返回true,否則返回false。
  8. 如果7中的DataIsChanged方法返回true就呼叫OnReload方法向訂閱者通知資料的變化。

程式碼實現

ConfigurationBuilderInterfaceExtension.cs

using System.Data;
using Microsoft.Extensions.Configuration;

namespace ReadConfigurationsFromDatabase;

// 擴充套件IConfigurationBuilder介面,提供AddDbConfiguration方法
public static class ConfigurationBuilderInterfaceExtension
{
    public static IConfigurationBuilder AddDbConfiguration(this IConfigurationBuilder builder,
        DbConfigOptions options) => builder.Add(new DbConfigurationSource(options));

    public static IConfigurationBuilder AddDbConfiguration(this IConfigurationBuilder builder,
        Func<IDbConnection> createDbConnection, string tableName = "T_DbConfigurations", bool reloadOnChange = false,
        TimeSpan? reloadInterval = null)
    {
        return AddDbConfiguration(builder, new DbConfigOptions(createDbConnection)
        {
            TableName = tableName,
            ReloadOnChange = reloadOnChange,
            ReloadInterval = reloadInterval
        });
    }
}

DbConfigOptions.cs

using System.Data;

namespace ReadConfigurationsFromDatabase;

public sealed class DbConfigOptions
{
    public DbConfigOptions(Func<IDbConnection> createDbConnection)
    {
        CreateDbConnection = createDbConnection;
    }

    // Func委託不能為空,又沒有預設值,所以必須使用建構函式初始化
    public Func<IDbConnection> CreateDbConnection { get; }
    public string TableName { get; init; } = "T_DbConfigurations";
    public bool ReloadOnChange { get; init; } = false;
    public TimeSpan? ReloadInterval { get; init; }
}

DbConfigurationProvider.cs

using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.Configuration;

namespace ReadConfigurationsFromDatabase;

public class DbConfigurationProvider : ConfigurationProvider,
    IDisposable, IAsyncDisposable
{
    private readonly DbConfigOptions _dbConfigOptions;

    // 預設值為false,可以不用顯式賦值
    private bool _isDisposed = false;

    // 讀寫鎖
    private readonly ReaderWriterLockSlim _lockObj = new();

    # region Constructor and DisposePattern

    public DbConfigurationProvider(DbConfigOptions dbConfigOptions)
    {
        _dbConfigOptions = dbConfigOptions;
        // 如果option中沒有設定“在配置改變時重新載入,那麼直接返回”
        if (!_dbConfigOptions.ReloadOnChange) return;

        // 預設reload的時間間隔
        var interval = TimeSpan.FromSeconds(3);
        // 如果option設定了reload的時間間隔,那麼就應用這一間隔
        if (_dbConfigOptions.ReloadInterval != null) interval = _dbConfigOptions.ReloadInterval.Value;

        // 將委託扔進執行緒池佇列,執行緒池有空閒執行緒就執行
        ThreadPool.QueueUserWorkItem(_ =>
        {
            // 如果資源被回收了,直接返回
            if (_isDisposed) return;

            // 透過執行緒休眠的方式定時載入
            Load();
            Thread.Sleep(interval);
        });
    }

    public void Dispose()
    {
        if (_isDisposed) return;
        _lockObj.Dispose();
        _isDisposed = true;
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        if (_isDisposed) return;
        await Task.Run(Dispose);
        GC.SuppressFinalize(this);
    }

    ~DbConfigurationProvider()
    {
        Dispose();
    }

    #endregion Constructor and DisposePattern

    # region override Methods in ConfigurationProvider

    public override IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string? parentPath)
    {
        _lockObj.EnterReadLock();
        try
        {
            return base.GetChildKeys(earlierKeys, parentPath);
        }
        finally
        {
            _lockObj.ExitReadLock();
        }
    }

    public override bool TryGet(string key, out string? value)
    {
        _lockObj.EnterReadLock();
        try
        {
            return base.TryGet(key, out value);
        }
        finally
        {
            _lockObj.ExitReadLock();
        }
    }

    public override void Load()
    {
        _lockObj.EnterWriteLock();
        var tableName = _dbConfigOptions.TableName;
        IDictionary<string, string?> clonedData = Data.Clone();
        Data.Clear();

        try
        {
            using var dbConnection = _dbConfigOptions.CreateDbConnection.Invoke();
            dbConnection.Open();
            DoLoad(tableName, dbConnection);
        }
        catch (DbException)
        {
            Data = clonedData;
            throw;
        }
        finally
        {
            _lockObj.ExitWriteLock();
        }

        // 如果資料改變,則發出通知
        if (DataIsChanged(clonedData, Data)) OnReload();
    }

    private static bool DataIsChanged(IDictionary<string, string?> oldData, IDictionary<string, string?> newData)
    {
        if (ReferenceEquals(oldData, newData) || oldData.Count != newData.Count) return true;
        foreach (var (oldKey, oldValue) in oldData)
        {
            if (!newData.ContainsKey(oldKey)) return true;
            if (newData[oldKey] != oldData[oldKey]) return true;
        }

        return false;
    }

    private void DoLoad(string tableName, IDbConnection dbConnection)
    {
        using var dbCommand = dbConnection.CreateCommand();
        dbCommand.CommandText =
            // 子查詢的作用是透過Id篩選最新的配置資訊
            $"select name,value from {tableName} where id in (select MAX(id) from {tableName} group by name)";
        using var dbReader = dbCommand.ExecuteReader();

        while (dbReader.Read())
        {
            // 索引對應的列和查詢語句(dbCommand.CommandText)有關,這裡第一列是"名稱",第二列是"值"
            var name = dbReader.GetString(0);
            var value = dbReader.GetString(1);

            // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
            if (value is null)
            {
                Data[name] = value;
                continue;
            }

            // 去除多餘空格
            value = value.Trim();

            // 處理value是json的情況
            if (value.StartsWith("[") && value.EndsWith("]") || value.StartsWith("{") && value.EndsWith("}"))
            {
                TryLoadAsJson(name, value);
                continue;
            }

            Data[name] = value;
        }
    }

    private void TryLoadAsJson(string name, string value)
    {
        var jsonOptions = new JsonDocumentOptions
        {
            // 允許json列表或陣列末尾存在額外逗號(json預設的行為是不能存在逗號)
            AllowTrailingCommas = true,
            // 允許json存在註釋並跳過這些註釋不做任何處理(json的預設行為是不允許註釋)
            CommentHandling = JsonCommentHandling.Skip
        };
        try
        {
            // 將字串的值解析為JsonDocument,並獲取JsonDocument的根元素
            var jsonRoot = JsonDocument.Parse(value, jsonOptions).RootElement;
            if (jsonRoot.ValueKind is not (JsonValueKind.Array or JsonValueKind.Object))
            {
                Data[name] = GetValueForConfig(jsonRoot);
                return;
            }

            var traceStack = new Stack<KeyValuePair<string, JsonElement>>();
            traceStack.Push(new KeyValuePair<string, JsonElement>(name, jsonRoot));
            while (traceStack.Count > 0) LoadJsonElement(traceStack);
        }
        catch (JsonException e)
        {
            // 如果不能轉換為json字串,將該字串當作原始字串對待
            Data[name] = value;
            Debug.WriteLine($"將{value}轉換為json時出現異常,異常資訊:{e}");
        }
    }

    private void LoadJsonElement(Stack<KeyValuePair<string, JsonElement>> traceStack)
    {
        var (name, jsonRoot) = traceStack.Pop();
        // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
        switch (jsonRoot.ValueKind)
        {
            case JsonValueKind.Array:
            {
                int index = 0;
                foreach (var item in jsonRoot.EnumerateArray())
                {
                    string path = name + ConfigurationPath.KeyDelimiter + index;
                    traceStack.Push(new KeyValuePair<string, JsonElement>(path, item));
                    index++;
                }

                break;
            }
            case JsonValueKind.Object:
            {
                foreach (var jsonObj in jsonRoot.EnumerateObject())
                {
                    string pathOfObj = name + ConfigurationPath.KeyDelimiter + jsonObj.Name;
                    traceStack.Push(new KeyValuePair<string, JsonElement>(pathOfObj, jsonObj.Value));
                }

                break;
            }
            default:
                Data[name] = GetValueForConfig(jsonRoot);
                break;
        }
    }

    private static string? GetValueForConfig(JsonElement jsonRoot) => jsonRoot.ValueKind switch
    {
        JsonValueKind.String =>
            //remove the quotes, "ab"-->ab
            jsonRoot.GetString(),
        JsonValueKind.Null =>
            //remove the quotes, "null"-->null
            null,
        JsonValueKind.Undefined =>
            //remove the quotes, "null"-->null
            null,
        _ => jsonRoot.GetRawText()
    };

    #endregion override Methods in ConfigurationProvider
}

DbConfigurationSource.cs

using Microsoft.Extensions.Configuration;

namespace ReadConfigurationsFromDatabase;

// 宣告如何建立實現了IConfigurationProvider介面的物件DbConfigurationProvider
// DbConfigurationSource類似於:IConfigurationProvider系列產品中DbConfigurationProvider產品的生產說明
public sealed class DbConfigurationSource : IConfigurationSource
{
    private readonly DbConfigOptions _dbConfigOptions;

    public DbConfigurationSource(DbConfigOptions dbConfigOptions) => _dbConfigOptions = dbConfigOptions;

    // 這裡引入IConfigurationBuilder型別的形參builder的作用可類比於:生產本產品需要其他工廠生產出來的其他產品。
    // 不過這裡沒有用到其他產品,所以沒有正在使用形參builder(和產品生產線一樣,要預留些可擴充部件,方便產線的升級改造)
    public IConfigurationProvider Build(IConfigurationBuilder builder) => new DbConfigurationProvider(_dbConfigOptions);
}

DictionaryInterfaceExtension.cs

namespace ReadConfigurationsFromDatabase;

public static class DictionaryInterfaceExtension
{
    public static IDictionary<string, string?> Clone(this IDictionary<string, string?> dictionary) =>
        dictionary.ToDictionary(item => item.Key, item => item.Value);
}

測試程式碼如下:

using Microsoft.Extensions.Configuration;
using MySql.Data.MySqlClient;
using ReadConfigurationsFromDatabase;
using Xunit.Abstractions;

namespace ConfigurationSystem.Test;

public class ReadConfigurationsFromDatabaseTest
{
    private const string ConnectionString =
        "Data Source=localhost;Database=Test;User ID=root;Password=Aa123456+;pooling=true";

    private readonly ITestOutputHelper _output;

    public ReadConfigurationsFromDatabaseTest(ITestOutputHelper output)
    {
        _output = output;
    }

    [Fact]
    public void DbConnectionTest()
    {
        using var connection = new MySqlConnection(ConnectionString);
        connection.Open();

        using var command = connection.CreateCommand();
        command.CommandText = "select * from Test.config";
        using var dbReader = command.ExecuteReader();

        while (dbReader.Read())
        {
            var name = dbReader.GetString(1);
            var value = dbReader.GetString(2);
            _output.WriteLine($"{name ?? "null"}: {value ?? "null"}");
        }

        connection.Close();
    }

    [Fact]
    public void ReadConfigFromDb()
    {
        var configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddDbConfiguration(() =>
        {
            var connection = new MySqlConnection(ConnectionString);
            return connection;
        }, tableName: "config", reloadOnChange: false);

        var config = configurationBuilder.Build();
        var userName = config["userName"];
        Assert.Equal("dbUserName", userName ?? "null");
        var connectionString = config["databaseConfig:connectionString"];
        Assert.Equal("mysqlConnectionString", connectionString ?? "null");
    }
}

多配置源問題

.NET Core中的配置系統支援“可覆蓋的配置”,可以向ConfigurationBuilder中註冊多個配置提供程式,後新增的配置提供程式可以覆蓋先新增的配置提供程式

現在從多個配置源讀取配置,配置順序如下:

新增順序 配置來源(右邊的列是配置內容) server userName password port
1 資料庫 smtpFromDb.example.com dbUserName 80
2 JSON檔案 smtp.example.com userNameFromJson passwordFromJson
3 命令列 passwordFromFromCommandLine

按照順序讀取配置後,各個配置項的實際值如下(後新增的會覆蓋先新增的):

  • server=smtp.example.com
  • userName=userNameFromJson
  • password=passwordFromFromCommandLine
  • port=80

程式碼如下:

using Microsoft.Extensions.Configuration;
using MySql.Data.MySqlClient;
using ReadConfigurationsFromDatabase;

namespace GetConfigUsingMultipleWaysSimultaneously;

public static class ReadConfigFromMultipleSource
{
    private const string ConnectionString =
        "Data Source=localhost;Database=Test;User ID=root;Password=Aa123456+;pooling=true";

    public static IConfigurationRoot GetConfigurationRoot(string[] args)
    {
        var configurationBuilder = new ConfigurationBuilder();
        configurationBuilder
            // 配置來源:資料庫
            .AddDbConfiguration(() =>
            {
                var connection = new MySqlConnection(ConnectionString);
                return connection;
            }, tableName: "config", reloadOnChange: false)
            // 配置來源Json
            .AddJsonFile("config.json")
            // 配置來源CommandLine
            .AddCommandLine(args);
        return configurationBuilder.Build();
    }
}

其中,config.json內容如下:

{
  "server": "smtp.example.com",
  "userName": "userNameFromJson",
  "password": "passwordFromJson"
}

測試程式碼如下:

using GetConfigUsingMultipleWaysSimultaneously;

namespace ConfigurationSystem.Test;

public class GetConfigUsingMultipleWaysSimultaneouslyTest
{
    [Fact]
    public void Test()
    {
        string[] args = new[] { "password=passwordFromFromCommandLine" };
        var configRoot = ReadConfigFromMultipleSource.GetConfigurationRoot(args);
        var server = configRoot["server"] ?? "null";
        var userName = configRoot["userName"] ?? "null";
        var password = configRoot["password"] ?? "null";
        var port = configRoot["port"] ?? "null";

        Assert.Equal("smtp.example.com", server);
        Assert.Equal("userNameFromJson", userName);
        Assert.Equal("passwordFromFromCommandLine", password);
        Assert.Equal("80", port);
    }
}

相關文章