扩展开发
本指南介绍如何开发自定义配置源和扩展功能。
配置源接口
ICfgSource 接口
所有配置源必须实现的基础接口:
csharp
public interface ICfgSource
{
/// <summary>
/// 配置源名称(同一层级内唯一)
/// </summary>
string Name { get; set; }
/// <summary>
/// 配置源层级,数值越大优先级越高
/// </summary>
int Level { get; }
/// <summary>
/// 配置源类型名称
/// </summary>
string Type { get; }
/// <summary>
/// 是否支持写入
/// </summary>
bool IsWriteable { get; }
/// <summary>
/// 是否为主写入源(同层级只能有一个)
/// </summary>
bool IsPrimaryWriter { get; }
/// <summary>
/// 配置项数量(所有叶子节点的总数)
/// </summary>
int KeyCount { get; }
/// <summary>
/// 顶级配置键数量(只统计第一层节点)
/// </summary>
int TopLevelKeyCount { get; }
/// <summary>
/// 构建 Microsoft.Extensions.Configuration 配置源
/// </summary>
IConfigurationSource BuildSource();
/// <summary>
/// 获取该配置源的所有配置值
/// </summary>
IEnumerable<KeyValuePair<string, string?>> GetAllValues();
}IWritableCfgSource 接口
可写配置源需要额外实现的接口:
csharp
public interface IWritableCfgSource : ICfgSource
{
/// <summary>
/// 应用配置更改
/// </summary>
/// <param name="changes">要应用的配置更改</param>
/// <param name="cancellationToken">取消令牌</param>
Task ApplyChangesAsync(IReadOnlyDictionary<string, string?> changes, CancellationToken cancellationToken);
}开发自定义配置源
示例:自定义 HTTP API 配置源
1. 定义配置选项
csharp
public class HttpApiCfgOptions
{
public string BaseUrl { get; set; } = "http://localhost:5000";
public string Endpoint { get; set; } = "/api/config";
public string? ApiKey { get; set; }
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1);
public bool EnableHotReload { get; set; } = true;
}2. 实现配置源
csharp
public class HttpApiCfgSource : ICfgSource, IWritableCfgSource, IDisposable
{
private readonly HttpApiCfgOptions _options;
private readonly int _level;
private readonly bool _isPrimaryWriter;
private readonly HttpClient _httpClient;
private readonly ConcurrentDictionary<string, string?> _data = new();
private Timer? _pollTimer;
private Action<Dictionary<string, string?>>? _onReload;
public HttpApiCfgSource(HttpApiCfgOptions options, int level, bool isPrimaryWriter = false)
{
_options = options;
_level = level;
_isPrimaryWriter = isPrimaryWriter;
_httpClient = new HttpClient
{
BaseAddress = new Uri(options.BaseUrl),
Timeout = options.Timeout
};
if (!string.IsNullOrEmpty(options.ApiKey))
{
_httpClient.DefaultRequestHeaders.Add("X-Api-Key", options.ApiKey);
}
}
// ICfgSource 实现
public string Name { get; set; } = "HttpApi";
public int Level => _level;
public string Type => "HttpApi";
public bool IsWriteable => true;
public bool IsPrimaryWriter => _isPrimaryWriter;
public int KeyCount => _data.Count;
public int TopLevelKeyCount => _data.Keys
.Select(k => k.Split(':')[0])
.Distinct()
.Count();
public IConfigurationSource BuildSource()
{
// 初始加载配置
LoadConfigAsync().GetAwaiter().GetResult();
// 启动热重载
if (_options.EnableHotReload)
{
StartPolling();
}
return new HttpApiConfigurationSource(this);
}
public IEnumerable<KeyValuePair<string, string?>> GetAllValues() => _data;
private async Task LoadConfigAsync()
{
try
{
var response = await _httpClient.GetAsync(_options.Endpoint);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var config = JsonSerializer.Deserialize<Dictionary<string, string?>>(json);
if (config != null)
{
_data.Clear();
foreach (var kvp in config)
{
_data[kvp.Key] = kvp.Value;
}
}
}
catch (Exception ex)
{
// 记录错误,但不抛出异常
Console.WriteLine($"加载配置失败: {ex.Message}");
}
}
private void StartPolling()
{
_pollTimer = new Timer(async _ =>
{
var oldData = new Dictionary<string, string?>(_data);
await LoadConfigAsync();
// 检测变更
if (HasChanges(oldData, _data))
{
_onReload?.Invoke(new Dictionary<string, string?>(_data));
}
}, null, _options.PollInterval, _options.PollInterval);
}
private bool HasChanges(IDictionary<string, string?> oldData, IDictionary<string, string?> newData)
{
if (oldData.Count != newData.Count) return true;
foreach (var kvp in newData)
{
if (!oldData.TryGetValue(kvp.Key, out var oldValue) || oldValue != kvp.Value)
return true;
}
return false;
}
// IWritableCfgSource 实现
public async Task ApplyChangesAsync(IReadOnlyDictionary<string, string?> changes, CancellationToken cancellationToken)
{
// 更新本地缓存
foreach (var kvp in changes)
{
if (kvp.Value == null)
_data.TryRemove(kvp.Key, out _);
else
_data[kvp.Key] = kvp.Value;
}
// 保存到远程
var json = JsonSerializer.Serialize(changes);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(_options.Endpoint, content, cancellationToken);
response.EnsureSuccessStatusCode();
}
internal IReadOnlyDictionary<string, string?> GetData() => _data;
internal void SetOnReload(Action<Dictionary<string, string?>> onReload)
{
_onReload = onReload;
}
public void Dispose()
{
_pollTimer?.Dispose();
_httpClient.Dispose();
}
}3. 实现 IConfigurationSource
csharp
internal class HttpApiConfigurationSource : IConfigurationSource
{
private readonly HttpApiCfgSource _cfgSource;
public HttpApiConfigurationSource(HttpApiCfgSource cfgSource)
{
_cfgSource = cfgSource;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new HttpApiConfigurationProvider(_cfgSource);
}
}4. 实现 IConfigurationProvider
csharp
internal class HttpApiConfigurationProvider : ConfigurationProvider
{
private readonly HttpApiCfgSource _cfgSource;
public HttpApiConfigurationProvider(HttpApiCfgSource cfgSource)
{
_cfgSource = cfgSource;
// 订阅重载事件
_cfgSource.SetOnReload(newData =>
{
Data = new Dictionary<string, string?>(newData, StringComparer.OrdinalIgnoreCase);
OnReload();
});
}
public override void Load()
{
Data = new Dictionary<string, string?>(_cfgSource.GetData(), StringComparer.OrdinalIgnoreCase);
}
}5. 添加扩展方法
csharp
public static class HttpApiCfgBuilderExtensions
{
public static CfgBuilder AddHttpApi(
this CfgBuilder builder,
Action<HttpApiCfgOptions> configure,
int level,
bool isPrimaryWriter = false)
{
var options = new HttpApiCfgOptions();
configure(options);
var source = new HttpApiCfgSource(options, level, isPrimaryWriter);
return builder.AddSource(source);
}
public static CfgBuilder AddHttpApi(
this CfgBuilder builder,
string baseUrl,
string endpoint,
int level,
bool enableHotReload = true)
{
return builder.AddHttpApi(options =>
{
options.BaseUrl = baseUrl;
options.Endpoint = endpoint;
options.EnableHotReload = enableHotReload;
}, level);
}
}6. 使用自定义配置源
csharp
var cfg = new CfgBuilder()
.AddJsonFile("config.json", level: 0, writeable: false)
.AddHttpApi(options =>
{
options.BaseUrl = "http://config-server:5000";
options.Endpoint = "/api/config/myapp";
options.ApiKey = "secret-key";
options.PollInterval = TimeSpan.FromMinutes(5);
options.EnableHotReload = true;
}, level: 10, isPrimaryWriter: true)
.Build();
// 读取配置
var value = cfg["SomeKey"];
// 写入配置
cfg.SetValue("NewKey", "NewValue");
await cfg.SaveAsync();开发文件配置源
继承 FileCfgSourceBase
对于文件类型的配置源,可以继承基类简化开发:
csharp
public class CustomFileCfgSource : FileCfgSourceBase
{
public CustomFileCfgSource(
string filePath,
int level,
bool writeable = false,
bool isPrimaryWriter = false,
bool reloadOnChange = false,
EncodingOptions? encoding = null)
: base(filePath, level, writeable, isPrimaryWriter, reloadOnChange, encoding)
{
}
protected override Dictionary<string, string?> ParseContent(string content)
{
// 实现自定义解析逻辑
var result = new Dictionary<string, string?>();
foreach (var line in content.Split('\n'))
{
var parts = line.Split('=', 2);
if (parts.Length == 2)
{
result[parts[0].Trim()] = parts[1].Trim();
}
}
return result;
}
protected override string SerializeContent(Dictionary<string, string?> data)
{
// 实现自定义序列化逻辑
var sb = new StringBuilder();
foreach (var kvp in data)
{
sb.AppendLine($"{kvp.Key}={kvp.Value}");
}
return sb.ToString();
}
}开发远程配置源
继承 RemoteCfgSourceBase
对于远程配置中心,可以继承基类:
csharp
public abstract class RemoteCfgSourceBase : ICfgSource, IWritableCfgSource, IDisposable
{
protected readonly ConcurrentDictionary<string, string?> Data = new();
protected Action<Dictionary<string, string?>>? OnReloadCallback;
// ICfgSource 实现
public string Name { get; set; } = "";
public abstract int Level { get; }
public abstract string Type { get; }
public abstract bool IsWriteable { get; }
public abstract bool IsPrimaryWriter { get; }
public int KeyCount => Data.Count;
public int TopLevelKeyCount => Data.Keys
.Select(k => k.Split(':')[0])
.Distinct()
.Count();
public abstract IConfigurationSource BuildSource();
public IEnumerable<KeyValuePair<string, string?>> GetAllValues() => Data;
protected abstract Task ConnectAsync();
protected abstract Task DisconnectAsync();
protected abstract Task LoadDataAsync();
protected abstract Task SaveDataAsync(IReadOnlyDictionary<string, string?> changes);
protected abstract void SetupWatcher();
// IWritableCfgSource 实现
public async Task ApplyChangesAsync(IReadOnlyDictionary<string, string?> changes, CancellationToken cancellationToken)
{
// 更新本地缓存
foreach (var kvp in changes)
{
if (kvp.Value == null)
Data.TryRemove(kvp.Key, out _);
else
Data[kvp.Key] = kvp.Value;
}
// 保存到远程
await SaveDataAsync(changes);
}
protected void NotifyReload()
{
OnReloadCallback?.Invoke(new Dictionary<string, string?>(Data));
}
public virtual void Dispose()
{
DisconnectAsync().GetAwaiter().GetResult();
}
}自定义编码映射
添加自定义编码规则
csharp
var cfg = new CfgBuilder()
.ConfigureEncodingMapping(config =>
{
// 完整路径映射
config.AddReadMapping(
@"C:\legacy\old-config.ini",
EncodingMappingType.ExactPath,
Encoding.GetEncoding("GB2312"),
priority: 100);
// 通配符映射
config.AddWriteMapping(
"*.ps1",
EncodingMappingType.Wildcard,
new UTF8Encoding(true), // UTF-8 with BOM
priority: 50);
// 正则表达式映射
config.AddReadMapping(
@"logs[/\\].*\.log$",
EncodingMappingType.Regex,
Encoding.Unicode,
priority: 30);
// 清除默认规则
config.ClearReadMappings();
config.ClearWriteMappings();
})
.AddJsonFile("config.json", level: 0, writeable: false)
.Build();自定义编码检测
csharp
var cfg = new CfgBuilder()
// 设置编码检测置信度阈值
.WithEncodingConfidenceThreshold(0.8f)
// 启用编码检测日志
.WithEncodingDetectionLogging(result =>
{
Console.WriteLine($"文件: {result.FilePath}");
Console.WriteLine($"检测编码: {result.DetectedEncoding?.EncodingName}");
Console.WriteLine($"置信度: {result.Confidence:P0}");
Console.WriteLine($"检测方法: {result.DetectionMethod}");
})
.AddJsonFile("config.json", level: 0)
.Build();自定义类型转换
注册自定义类型转换器
csharp
// 自定义类型
public class ConnectionString
{
public string Server { get; set; } = "";
public string Database { get; set; } = "";
public string UserId { get; set; } = "";
public string Password { get; set; } = "";
public static ConnectionString Parse(string value)
{
var result = new ConnectionString();
var parts = value.Split(';');
foreach (var part in parts)
{
var kv = part.Split('=', 2);
if (kv.Length == 2)
{
switch (kv[0].Trim().ToLower())
{
case "server": result.Server = kv[1].Trim(); break;
case "database": result.Database = kv[1].Trim(); break;
case "user id": result.UserId = kv[1].Trim(); break;
case "password": result.Password = kv[1].Trim(); break;
}
}
}
return result;
}
public override string ToString()
{
return $"Server={Server};Database={Database};User Id={UserId};Password={Password}";
}
}
// 使用
var connStr = cfg.GetValue<ConnectionString>("Database:ConnectionString");开发 CfgBuilder 扩展
添加便捷方法
csharp
public static class CfgBuilderExtensions
{
/// <summary>
/// 添加标准的多环境配置
/// </summary>
public static CfgBuilder AddStandardConfig(
this CfgBuilder builder,
string environment,
string basePath = "")
{
var path = string.IsNullOrEmpty(basePath) ? "" : basePath + "/";
return builder
.AddJsonFile($"{path}config.json", level: 0)
.AddJsonFile($"{path}config.{environment}.json", level: 1, optional: true)
.AddJsonFile($"{path}config.local.json", level: 2, optional: true, writeable: true)
.AddEnvironmentVariables(prefix: "APP_", level: 10);
}
/// <summary>
/// 添加开发环境配置
/// </summary>
public static CfgBuilder AddDevelopmentConfig(this CfgBuilder builder)
{
return builder
.AddJsonFile("config.json", level: 0)
.AddJsonFile("config.Development.json", level: 1, optional: true)
.AddEnvFile(".env", level: 2, optional: true)
.AddEnvFile(".env.local", level: 3, optional: true)
.AddEnvironmentVariables(prefix: "APP_", level: 10);
}
/// <summary>
/// 添加生产环境配置(带远程配置中心)
/// </summary>
public static CfgBuilder AddProductionConfig(
this CfgBuilder builder,
string consulAddress,
string serviceName)
{
return builder
.AddJsonFile("config.json", level: 0)
.AddJsonFile("config.Production.json", level: 1, optional: true)
.AddConsul(options =>
{
options.Address = consulAddress;
options.KeyPrefix = $"services/{serviceName}/";
options.EnableHotReload = true;
}, level: 10)
.AddEnvironmentVariables(prefix: "APP_", level: 20);
}
}测试自定义配置源
单元测试示例
csharp
public class HttpApiCfgSourceTests
{
[Fact]
public async Task Should_Load_Config_From_Api()
{
// Arrange
using var server = new MockHttpServer();
server.SetupGet("/api/config", new Dictionary<string, string?>
{
["Key1"] = "Value1",
["Key2"] = "Value2"
});
var cfg = new CfgBuilder()
.AddHttpApi(options =>
{
options.BaseUrl = server.BaseUrl;
options.Endpoint = "/api/config";
}, level: 0)
.Build();
// Act
var value1 = cfg["Key1"];
var value2 = cfg["Key2"];
// Assert
Assert.Equal("Value1", value1);
Assert.Equal("Value2", value2);
}
[Fact]
public async Task Should_Save_Config_To_Api()
{
// Arrange
using var server = new MockHttpServer();
server.SetupGet("/api/config", new Dictionary<string, string?>());
server.SetupPost("/api/config");
var cfg = new CfgBuilder()
.AddHttpApi(options =>
{
options.BaseUrl = server.BaseUrl;
options.Endpoint = "/api/config";
}, level: 0, isPrimaryWriter: true)
.Build();
// Act
cfg.SetValue("NewKey", "NewValue");
await cfg.SaveAsync();
// Assert
var savedData = server.GetLastPostData<Dictionary<string, string?>>();
Assert.Equal("NewValue", savedData["NewKey"]);
}
[Fact]
public async Task Should_Detect_Config_Changes()
{
// Arrange
using var server = new MockHttpServer();
var initialData = new Dictionary<string, string?> { ["Key1"] = "Value1" };
server.SetupGet("/api/config", initialData);
var changeDetected = false;
var cfg = new CfgBuilder()
.AddHttpApi(options =>
{
options.BaseUrl = server.BaseUrl;
options.Endpoint = "/api/config";
options.PollInterval = TimeSpan.FromMilliseconds(100);
options.EnableHotReload = true;
}, level: 0)
.Build();
cfg.OnChange += (key, oldValue, newValue) =>
{
changeDetected = true;
};
// Act - 更新服务器数据
server.SetupGet("/api/config", new Dictionary<string, string?>
{
["Key1"] = "UpdatedValue"
});
// 等待轮询
await Task.Delay(200);
// Assert
Assert.True(changeDetected);
Assert.Equal("UpdatedValue", cfg["Key1"]);
}
}集成测试示例
csharp
public class HttpApiCfgSourceIntegrationTests : IClassFixture<ConfigServerFixture>
{
private readonly ConfigServerFixture _fixture;
public HttpApiCfgSourceIntegrationTests(ConfigServerFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Should_Work_With_Real_Server()
{
// Arrange
var cfg = new CfgBuilder()
.AddHttpApi(options =>
{
options.BaseUrl = _fixture.ServerUrl;
options.Endpoint = "/api/config";
options.ApiKey = _fixture.ApiKey;
}, level: 0, isPrimaryWriter: true)
.Build();
// Act
cfg.SetValue("IntegrationTest:Key", "IntegrationTest:Value");
await cfg.SaveAsync();
// 重新加载验证
var newCfg = new CfgBuilder()
.AddHttpApi(options =>
{
options.BaseUrl = _fixture.ServerUrl;
options.Endpoint = "/api/config";
options.ApiKey = _fixture.ApiKey;
}, level: 0)
.Build();
// Assert
Assert.Equal("IntegrationTest:Value", newCfg["IntegrationTest:Key"]);
}
}发布自定义配置源
创建 NuGet 包
- 创建项目文件:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<PackageId>Apq.Cfg.HttpApi</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>HTTP API configuration source for Apq.Cfg</Description>
<PackageTags>configuration;apq;http;api</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/yourname/Apq.Cfg.HttpApi</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Apq.Cfg" Version="1.1.*" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>- 创建 README.md:
markdown
# Apq.Cfg.HttpApi
HTTP API configuration source for Apq.Cfg.
## Installation
```bash
dotnet add package Apq.Cfg.HttpApiUsage
csharp
var cfg = new CfgBuilder()
.AddHttpApi(options =>
{
options.BaseUrl = "http://config-server:5000";
options.Endpoint = "/api/config";
options.EnableHotReload = true;
}, level: 10)
.Build();
3. 打包发布:
```bash
dotnet pack -c Release
dotnet nuget push bin/Release/Apq.Cfg.HttpApi.1.0.0.nupkg -s https://api.nuget.org/v3/index.json -k YOUR_API_KEY最佳实践
配置源开发建议
| 建议 | 说明 |
|---|---|
| 实现 IDisposable | 正确释放资源(HTTP 客户端、定时器等) |
| 线程安全 | 使用 ConcurrentDictionary 存储数据 |
| 错误处理 | 捕获异常,记录日志,不影响应用启动 |
| 可选配置 | 支持 optional 参数,允许配置源不存在 |
| 热重载 | 实现变更检测和通知机制 |
| 单元测试 | 编写完整的测试用例 |
性能优化建议
- 缓存数据:避免频繁读取远程配置
- 批量保存:合并多次写入操作
- 异步操作:使用异步方法避免阻塞
- 连接池:复用 HTTP 客户端等资源