mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
feat(configuration): 添加配置管理器功能
- 实现了 IConfigurationManager 接口定义配置管理契约 - 创建 ConfigurationManager 类提供线程安全的配置存储和访问 - 添加配置的增删改查功能支持泛型类型转换 - 实现配置变更监听机制和取消注册功能 - 提供 JSON 格式导入导出和文件读写功能 - 添加完整的单元测试覆盖并发场景和边界条件 - 实现 ConfigWatcherUnRegister 类处理监听器注销逻辑
This commit is contained in:
parent
ffaace4538
commit
d8cd22a424
@ -0,0 +1,99 @@
|
||||
using GFramework.Core.Abstractions.events;
|
||||
using GFramework.Core.Abstractions.utility;
|
||||
|
||||
namespace GFramework.Core.Abstractions.configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 配置管理器接口,提供类型安全的配置存储和访问
|
||||
/// 线程安全:所有方法都是线程安全的
|
||||
/// </summary>
|
||||
public interface IConfigurationManager : IUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取配置数量
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定键的配置值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">配置值类型</typeparam>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <returns>配置值,如果不存在则返回类型默认值</returns>
|
||||
T? GetConfig<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定键的配置值,如果不存在则返回默认值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">配置值类型</typeparam>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="defaultValue">默认值</param>
|
||||
/// <returns>配置值或默认值</returns>
|
||||
T GetConfig<T>(string key, T defaultValue);
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定键的配置值
|
||||
/// </summary>
|
||||
/// <typeparam name="T">配置值类型</typeparam>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="value">配置值</param>
|
||||
void SetConfig<T>(string key, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定键的配置是否存在
|
||||
/// </summary>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <returns>如果存在返回 true,否则返回 false</returns>
|
||||
bool HasConfig(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定键的配置
|
||||
/// </summary>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <returns>如果成功移除返回 true,否则返回 false</returns>
|
||||
bool RemoveConfig(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有配置
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 监听指定键的配置变化
|
||||
/// </summary>
|
||||
/// <typeparam name="T">配置值类型</typeparam>
|
||||
/// <param name="key">配置键</param>
|
||||
/// <param name="onChange">配置变化时的回调,参数为新值</param>
|
||||
/// <returns>取消注册接口</returns>
|
||||
IUnRegister WatchConfig<T>(string key, Action<T> onChange);
|
||||
|
||||
/// <summary>
|
||||
/// 从 JSON 字符串加载配置
|
||||
/// </summary>
|
||||
/// <param name="json">JSON 字符串</param>
|
||||
void LoadFromJson(string json);
|
||||
|
||||
/// <summary>
|
||||
/// 将配置保存为 JSON 字符串
|
||||
/// </summary>
|
||||
/// <returns>JSON 字符串</returns>
|
||||
string SaveToJson();
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载配置
|
||||
/// </summary>
|
||||
/// <param name="path">文件路径</param>
|
||||
void LoadFromFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 将配置保存到文件
|
||||
/// </summary>
|
||||
/// <param name="path">文件路径</param>
|
||||
void SaveToFile(string path);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有配置键
|
||||
/// </summary>
|
||||
/// <returns>配置键集合</returns>
|
||||
IEnumerable<string> GetAllKeys();
|
||||
}
|
||||
377
GFramework.Core.Tests/configuration/ConfigurationManagerTests.cs
Normal file
377
GFramework.Core.Tests/configuration/ConfigurationManagerTests.cs
Normal file
@ -0,0 +1,377 @@
|
||||
using System.IO;
|
||||
using GFramework.Core.configuration;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GFramework.Core.Tests.configuration;
|
||||
|
||||
/// <summary>
|
||||
/// ConfigurationManager 功能测试类
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ConfigurationManagerTests
|
||||
{
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_configManager = new ConfigurationManager();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
_configManager.Clear();
|
||||
}
|
||||
|
||||
private ConfigurationManager _configManager = null!;
|
||||
|
||||
[Test]
|
||||
public void SetConfig_And_GetConfig_Should_Work()
|
||||
{
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
|
||||
var value = _configManager.GetConfig<int>("test.key");
|
||||
|
||||
Assert.That(value, Is.EqualTo(42));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetConfig_With_DefaultValue_Should_Return_DefaultValue_When_Key_Not_Exists()
|
||||
{
|
||||
var value = _configManager.GetConfig("nonexistent.key", 100);
|
||||
|
||||
Assert.That(value, Is.EqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetConfig_Should_Return_Default_When_Key_Not_Exists()
|
||||
{
|
||||
var value = _configManager.GetConfig<int>("nonexistent.key");
|
||||
|
||||
Assert.That(value, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasConfig_Should_Return_True_When_Key_Exists()
|
||||
{
|
||||
_configManager.SetConfig("test.key", "value");
|
||||
|
||||
Assert.That(_configManager.HasConfig("test.key"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HasConfig_Should_Return_False_When_Key_Not_Exists()
|
||||
{
|
||||
Assert.That(_configManager.HasConfig("nonexistent.key"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemoveConfig_Should_Remove_Existing_Key()
|
||||
{
|
||||
_configManager.SetConfig("test.key", "value");
|
||||
|
||||
var removed = _configManager.RemoveConfig("test.key");
|
||||
|
||||
Assert.That(removed, Is.True);
|
||||
Assert.That(_configManager.HasConfig("test.key"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RemoveConfig_Should_Return_False_When_Key_Not_Exists()
|
||||
{
|
||||
var removed = _configManager.RemoveConfig("nonexistent.key");
|
||||
|
||||
Assert.That(removed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clear_Should_Remove_All_Configs()
|
||||
{
|
||||
_configManager.SetConfig("key1", "value1");
|
||||
_configManager.SetConfig("key2", "value2");
|
||||
|
||||
_configManager.Clear();
|
||||
|
||||
Assert.That(_configManager.Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Count_Should_Return_Correct_Number()
|
||||
{
|
||||
_configManager.SetConfig("key1", "value1");
|
||||
_configManager.SetConfig("key2", "value2");
|
||||
_configManager.SetConfig("key3", "value3");
|
||||
|
||||
Assert.That(_configManager.Count, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAllKeys_Should_Return_All_Keys()
|
||||
{
|
||||
_configManager.SetConfig("key1", "value1");
|
||||
_configManager.SetConfig("key2", "value2");
|
||||
|
||||
var keys = _configManager.GetAllKeys().ToList();
|
||||
|
||||
Assert.That(keys, Has.Count.EqualTo(2));
|
||||
Assert.That(keys, Contains.Item("key1"));
|
||||
Assert.That(keys, Contains.Item("key2"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetConfig_Should_Support_Different_Types()
|
||||
{
|
||||
_configManager.SetConfig("int.key", 42);
|
||||
_configManager.SetConfig("string.key", "hello");
|
||||
_configManager.SetConfig("bool.key", true);
|
||||
_configManager.SetConfig("double.key", 3.14);
|
||||
|
||||
Assert.That(_configManager.GetConfig<int>("int.key"), Is.EqualTo(42));
|
||||
Assert.That(_configManager.GetConfig<string>("string.key"), Is.EqualTo("hello"));
|
||||
Assert.That(_configManager.GetConfig<bool>("bool.key"), Is.True);
|
||||
Assert.That(_configManager.GetConfig<double>("double.key"), Is.EqualTo(3.14).Within(0.001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WatchConfig_Should_Trigger_When_Config_Changes()
|
||||
{
|
||||
var callCount = 0;
|
||||
var receivedValue = 0;
|
||||
|
||||
_configManager.WatchConfig<int>("test.key", value =>
|
||||
{
|
||||
callCount++;
|
||||
receivedValue = value;
|
||||
});
|
||||
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
|
||||
Assert.That(callCount, Is.EqualTo(1));
|
||||
Assert.That(receivedValue, Is.EqualTo(42));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WatchConfig_Should_Not_Trigger_When_Value_Not_Changed()
|
||||
{
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
|
||||
var callCount = 0;
|
||||
_configManager.WatchConfig<int>("test.key", _ => callCount++);
|
||||
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
|
||||
Assert.That(callCount, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnRegister_Should_Stop_Watching()
|
||||
{
|
||||
var callCount = 0;
|
||||
|
||||
var unRegister = _configManager.WatchConfig<int>("test.key", _ => callCount++);
|
||||
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
Assert.That(callCount, Is.EqualTo(1));
|
||||
|
||||
unRegister.UnRegister();
|
||||
|
||||
_configManager.SetConfig("test.key", 100);
|
||||
Assert.That(callCount, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Multiple_Watchers_Should_All_Be_Triggered()
|
||||
{
|
||||
var count1 = 0;
|
||||
var count2 = 0;
|
||||
|
||||
_configManager.WatchConfig<int>("test.key", _ => count1++);
|
||||
_configManager.WatchConfig<int>("test.key", _ => count2++);
|
||||
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
|
||||
Assert.That(count1, Is.EqualTo(1));
|
||||
Assert.That(count2, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveToJson_And_LoadFromJson_Should_Work()
|
||||
{
|
||||
_configManager.SetConfig("key1", "value1");
|
||||
_configManager.SetConfig("key2", 42);
|
||||
|
||||
var json = _configManager.SaveToJson();
|
||||
|
||||
var newManager = new ConfigurationManager();
|
||||
newManager.LoadFromJson(json);
|
||||
|
||||
Assert.That(newManager.GetConfig<string>("key1"), Is.EqualTo("value1"));
|
||||
Assert.That(newManager.GetConfig<int>("key2"), Is.EqualTo(42));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveToFile_And_LoadFromFile_Should_Work()
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
_configManager.SetConfig("key1", "value1");
|
||||
_configManager.SetConfig("key2", 42);
|
||||
|
||||
_configManager.SaveToFile(tempFile);
|
||||
|
||||
var newManager = new ConfigurationManager();
|
||||
newManager.LoadFromFile(tempFile);
|
||||
|
||||
Assert.That(newManager.GetConfig<string>("key1"), Is.EqualTo("value1"));
|
||||
Assert.That(newManager.GetConfig<int>("key2"), Is.EqualTo(42));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LoadFromFile_Should_Throw_When_File_Not_Exists()
|
||||
{
|
||||
Assert.Throws<FileNotFoundException>(() => { _configManager.LoadFromFile("nonexistent.json"); });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetConfig_Should_Throw_When_Key_Is_Null_Or_Empty()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _configManager.SetConfig<string>(null!, "value"));
|
||||
Assert.Throws<ArgumentException>(() => _configManager.SetConfig("", "value"));
|
||||
Assert.Throws<ArgumentException>(() => _configManager.SetConfig(" ", "value"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetConfig_Should_Throw_When_Key_Is_Null_Or_Empty()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _configManager.GetConfig<string>(null!));
|
||||
Assert.Throws<ArgumentException>(() => _configManager.GetConfig<string>(""));
|
||||
Assert.Throws<ArgumentException>(() => _configManager.GetConfig<string>(" "));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WatchConfig_Should_Throw_When_Parameters_Invalid()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => _configManager.WatchConfig<int>(null!, _ => { }));
|
||||
Assert.Throws<ArgumentNullException>(() => _configManager.WatchConfig<int>("key", null!));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Concurrent_SetConfig_Should_Be_Thread_Safe()
|
||||
{
|
||||
const int threadCount = 10;
|
||||
const int iterationsPerThread = 100;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
for (var i = 0; i < threadCount; i++)
|
||||
{
|
||||
var threadId = i;
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var j = 0; j < iterationsPerThread; j++)
|
||||
{
|
||||
_configManager.SetConfig($"key.{threadId}", j);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (exceptions)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
Assert.That(exceptions, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Concurrent_GetConfig_And_SetConfig_Should_Be_Thread_Safe()
|
||||
{
|
||||
const int threadCount = 5;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
for (var i = 0; i < threadCount; i++)
|
||||
{
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
for (var j = 0; j < 100; j++)
|
||||
{
|
||||
if (j % 2 == 0)
|
||||
{
|
||||
_configManager.SetConfig("shared.key", j);
|
||||
}
|
||||
else
|
||||
{
|
||||
_configManager.GetConfig<int>("shared.key");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (exceptions)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
Assert.That(exceptions, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Concurrent_WatchConfig_Should_Be_Thread_Safe()
|
||||
{
|
||||
const int threadCount = 10;
|
||||
var tasks = new Task[threadCount];
|
||||
var exceptions = new List<Exception>();
|
||||
var callCounts = new int[threadCount];
|
||||
|
||||
for (var i = 0; i < threadCount; i++)
|
||||
{
|
||||
var threadId = i;
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_configManager.WatchConfig<int>("test.key",
|
||||
_ => { Interlocked.Increment(ref callCounts[threadId]); });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (exceptions)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
_configManager.SetConfig("test.key", 42);
|
||||
|
||||
Assert.That(exceptions, Is.Empty);
|
||||
Assert.That(callCounts.Sum(), Is.EqualTo(threadCount));
|
||||
}
|
||||
}
|
||||
21
GFramework.Core/configuration/ConfigWatcherUnRegister.cs
Normal file
21
GFramework.Core/configuration/ConfigWatcherUnRegister.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using GFramework.Core.Abstractions.events;
|
||||
|
||||
namespace GFramework.Core.Abstractions.configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 配置监听取消注册接口
|
||||
/// </summary>
|
||||
internal sealed class ConfigWatcherUnRegister : IUnRegister
|
||||
{
|
||||
private readonly Action _unRegisterAction;
|
||||
|
||||
public ConfigWatcherUnRegister(Action unRegisterAction)
|
||||
{
|
||||
_unRegisterAction = unRegisterAction ?? throw new ArgumentNullException(nameof(unRegisterAction));
|
||||
}
|
||||
|
||||
public void UnRegister()
|
||||
{
|
||||
_unRegisterAction();
|
||||
}
|
||||
}
|
||||
324
GFramework.Core/configuration/ConfigurationManager.cs
Normal file
324
GFramework.Core/configuration/ConfigurationManager.cs
Normal file
@ -0,0 +1,324 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using GFramework.Core.Abstractions.configuration;
|
||||
using GFramework.Core.Abstractions.events;
|
||||
|
||||
namespace GFramework.Core.configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 配置管理器实现,提供线程安全的配置存储和访问
|
||||
/// 线程安全:所有公共方法都是线程安全的
|
||||
/// </summary>
|
||||
public class ConfigurationManager : IConfigurationManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Key 参数验证错误消息常量
|
||||
/// </summary>
|
||||
private const string KeyCannotBeNullOrEmptyMessage = "Key cannot be null or whitespace.";
|
||||
|
||||
/// <summary>
|
||||
/// Path 参数验证错误消息常量
|
||||
/// </summary>
|
||||
private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace.";
|
||||
|
||||
/// <summary>
|
||||
/// JSON 参数验证错误消息常量
|
||||
/// </summary>
|
||||
private const string JsonCannotBeNullOrEmptyMessage = "JSON cannot be null or whitespace.";
|
||||
|
||||
/// <summary>
|
||||
/// 配置存储字典(线程安全)
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, object> _configs = new();
|
||||
|
||||
/// <summary>
|
||||
/// 用于保护监听器列表的锁
|
||||
/// </summary>
|
||||
private readonly object _watcherLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 配置监听器字典(线程安全)
|
||||
/// 键:配置键,值:监听器列表
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, List<Delegate>> _watchers = new();
|
||||
|
||||
/// <summary>
|
||||
/// 获取配置数量
|
||||
/// </summary>
|
||||
public int Count => _configs.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定键的配置值
|
||||
/// </summary>
|
||||
public T? GetConfig<T>(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key));
|
||||
|
||||
if (_configs.TryGetValue(key, out var value))
|
||||
{
|
||||
return ConvertValue<T>(value);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定键的配置值,如果不存在则返回默认值
|
||||
/// </summary>
|
||||
public T GetConfig<T>(string key, T defaultValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key));
|
||||
|
||||
if (_configs.TryGetValue(key, out var value))
|
||||
{
|
||||
return ConvertValue<T>(value);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置指定键的配置值
|
||||
/// </summary>
|
||||
public void SetConfig<T>(string key, T value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key));
|
||||
|
||||
var oldValue = _configs.AddOrUpdate(key, value!, (_, _) => value!);
|
||||
|
||||
// 触发监听器
|
||||
if (!EqualityComparer<object>.Default.Equals(oldValue, value))
|
||||
{
|
||||
NotifyWatchers(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定键的配置是否存在
|
||||
/// </summary>
|
||||
public bool HasConfig(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key));
|
||||
|
||||
return _configs.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 移除指定键的配置
|
||||
/// </summary>
|
||||
public bool RemoveConfig(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key));
|
||||
|
||||
var removed = _configs.TryRemove(key, out _);
|
||||
|
||||
if (removed)
|
||||
{
|
||||
// 移除该键的所有监听器(使用锁保护)
|
||||
lock (_watcherLock)
|
||||
{
|
||||
_watchers.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有配置
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_configs.Clear();
|
||||
|
||||
// 清空监听器(使用锁保护)
|
||||
lock (_watcherLock)
|
||||
{
|
||||
_watchers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 监听指定键的配置变化
|
||||
/// </summary>
|
||||
public IUnRegister WatchConfig<T>(string key, Action<T> onChange)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key));
|
||||
|
||||
if (onChange == null)
|
||||
throw new ArgumentNullException(nameof(onChange));
|
||||
|
||||
lock (_watcherLock)
|
||||
{
|
||||
if (!_watchers.TryGetValue(key, out var watchers))
|
||||
{
|
||||
watchers = new List<Delegate>();
|
||||
_watchers[key] = watchers;
|
||||
}
|
||||
|
||||
watchers.Add(onChange);
|
||||
}
|
||||
|
||||
return new ConfigWatcherUnRegister(() => UnwatchConfig(key, onChange));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 JSON 字符串加载配置
|
||||
/// </summary>
|
||||
public void LoadFromJson(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
throw new ArgumentException(JsonCannotBeNullOrEmptyMessage, nameof(json));
|
||||
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
|
||||
if (dict == null)
|
||||
return;
|
||||
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
_configs[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将配置保存为 JSON 字符串
|
||||
/// </summary>
|
||||
public string SaveToJson()
|
||||
{
|
||||
var dict = _configs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
return JsonSerializer.Serialize(dict, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载配置
|
||||
/// </summary>
|
||||
public void LoadFromFile(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path));
|
||||
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"Configuration file not found: {path}");
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
LoadFromJson(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将配置保存到文件
|
||||
/// </summary>
|
||||
public void SaveToFile(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException(PathCannotBeNullOrEmptyMessage, nameof(path));
|
||||
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = SaveToJson();
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有配置键
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetAllKeys()
|
||||
{
|
||||
return _configs.Keys.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消监听指定键的配置变化
|
||||
/// </summary>
|
||||
private void UnwatchConfig<T>(string key, Action<T> onChange)
|
||||
{
|
||||
lock (_watcherLock)
|
||||
{
|
||||
if (_watchers.TryGetValue(key, out var watchers))
|
||||
{
|
||||
watchers.Remove(onChange);
|
||||
|
||||
if (watchers.Count == 0)
|
||||
{
|
||||
_watchers.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知监听器配置已变化
|
||||
/// </summary>
|
||||
private void NotifyWatchers<T>(string key, T newValue)
|
||||
{
|
||||
List<Delegate>? watchersCopy = null;
|
||||
|
||||
lock (_watcherLock)
|
||||
{
|
||||
if (_watchers.TryGetValue(key, out var watchers))
|
||||
{
|
||||
watchersCopy = new List<Delegate>(watchers);
|
||||
}
|
||||
}
|
||||
|
||||
if (watchersCopy == null)
|
||||
return;
|
||||
|
||||
foreach (var watcher in watchersCopy)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (watcher is Action<T> typedWatcher)
|
||||
{
|
||||
typedWatcher(newValue);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 防止监听器异常影响其他监听器
|
||||
Console.Error.WriteLine(
|
||||
$"[ConfigurationManager] Error in config watcher for key '{key}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换配置值到目标类型
|
||||
/// </summary>
|
||||
private static T ConvertValue<T>(object value)
|
||||
{
|
||||
if (value is T typedValue)
|
||||
{
|
||||
return typedValue;
|
||||
}
|
||||
|
||||
// 处理 JsonElement 类型
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(jsonElement.GetRawText())!;
|
||||
}
|
||||
|
||||
// 尝试类型转换
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user