dotnet学习笔记-专题04-配置的读取和写入-01
1.dotnet学习笔记-专题03-RabbitMQ-01
2.dotnet学习笔记-专题04-配置的读取和写入-01
3.dotnet学习笔记-专题01-异步与多线程-014.dotnet学习笔记-专题06-过滤器和中间件-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>
从数据库读取配置
说明
- 依据Zack.AnyDBConfigProvider项目,按照自己的编码习惯重新构建。
- 该项目可从数据库中读取配置信息
重点内容
- 任何自定义的ConfigurationProvider都要实现IConfigurationProvider接口,由于.NET中的抽象类ConfigurationProvider已经实现了IConfigurationProvider接口,所以一般的做法是继承ConfigurationProvider,然后override抽象类ConfigurationProvider中的方法。
- Load方法用于加载配置数据,加载的数据按照键值对的形式保存到Data属性中。
- Data属性是IDictionary<string,string>类型,其中键为配置的名字。
- 如果配置项发生变化,则需要调用OnReload方法通知订阅者配置项发生改变。
DbConfigurationProvider类的构造方法
- 如果启用了ReloadOnChange选项,那么将一个委托方法送入队列,等到线程池有线程可用时执行该委托方法(ThreadPool.QueueUserWorkItem)。
- 该委托方法在方法体内每间隔一段时间(通过Thread.Sleep实现)执行一次Load方法,直到DbConfigurationProvider类的实例被释放。
Load方法
- Load方法首先创建了一个Data属性的副本clonedData,用于稍后比较数据是否修改了。
- 读取配置的代码最终会调用TryGet方法读取配置,为了避免TryGet读取到Load加载一半的数据,使用读写锁控制读写同步。
- 由于读的频率高于写的频率,为了避免使用普通的锁造成性能问题,这里使用ReaderWriteLockSlim类(.NET自带)实现“只允许一个线程写入,允许多个线程读取”。
- 为了实现3的写锁,需要把“将配置项写入Data属性“的代码放到EnterWriteLock和ExitWriteLock之间。
- 同时一定要把OnReload方法放到ExitWriteLock之后。这是因为OnReload方法中调用了TryGet方法,TryGet方法中有读锁,写锁中嵌套读锁是不被允许的。
- Load中调用的DoLoad方法从数据库中读取配置,然后将数据加载到Data属性中。DoLoad方法遵循”多层级数据扁平化规则“来解析和加载数据。
- 在6之后调用DataIsChanged方法将旧数据和从数据库中读取的新数据比较,如果发现数据有变化就返回true,否则返回false。
- 如果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); } }
合集:
dotnet学习笔记-专题-xx
标签:
学习笔记
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端