配置的讀取和寫入
讀取配置的類,包括手動從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);
}
}