dotnet学习笔记-专题04-配置的读取和写入-01

配置的读取和写入

读取配置的类,包括手动从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);
    }
}
posted @ 2024-11-22 23:28  random_d  阅读(46)  评论(0)    收藏  举报