From d8cd22a424a02e5786f7a2e9071e52e7f1861464 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:40:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(configuration):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 IConfigurationManager 接口定义配置管理契约 - 创建 ConfigurationManager 类提供线程安全的配置存储和访问 - 添加配置的增删改查功能支持泛型类型转换 - 实现配置变更监听机制和取消注册功能 - 提供 JSON 格式导入导出和文件读写功能 - 添加完整的单元测试覆盖并发场景和边界条件 - 实现 ConfigWatcherUnRegister 类处理监听器注销逻辑 --- .../configuration/IConfigurationManager.cs | 99 +++++ .../ConfigurationManagerTests.cs | 377 ++++++++++++++++++ .../configuration/ConfigWatcherUnRegister.cs | 21 + .../configuration/ConfigurationManager.cs | 324 +++++++++++++++ 4 files changed, 821 insertions(+) create mode 100644 GFramework.Core.Abstractions/configuration/IConfigurationManager.cs create mode 100644 GFramework.Core.Tests/configuration/ConfigurationManagerTests.cs create mode 100644 GFramework.Core/configuration/ConfigWatcherUnRegister.cs create mode 100644 GFramework.Core/configuration/ConfigurationManager.cs diff --git a/GFramework.Core.Abstractions/configuration/IConfigurationManager.cs b/GFramework.Core.Abstractions/configuration/IConfigurationManager.cs new file mode 100644 index 0000000..d75fff5 --- /dev/null +++ b/GFramework.Core.Abstractions/configuration/IConfigurationManager.cs @@ -0,0 +1,99 @@ +using GFramework.Core.Abstractions.events; +using GFramework.Core.Abstractions.utility; + +namespace GFramework.Core.Abstractions.configuration; + +/// +/// 配置管理器接口,提供类型安全的配置存储和访问 +/// 线程安全:所有方法都是线程安全的 +/// +public interface IConfigurationManager : IUtility +{ + /// + /// 获取配置数量 + /// + int Count { get; } + + /// + /// 获取指定键的配置值 + /// + /// 配置值类型 + /// 配置键 + /// 配置值,如果不存在则返回类型默认值 + T? GetConfig(string key); + + /// + /// 获取指定键的配置值,如果不存在则返回默认值 + /// + /// 配置值类型 + /// 配置键 + /// 默认值 + /// 配置值或默认值 + T GetConfig(string key, T defaultValue); + + /// + /// 设置指定键的配置值 + /// + /// 配置值类型 + /// 配置键 + /// 配置值 + void SetConfig(string key, T value); + + /// + /// 检查指定键的配置是否存在 + /// + /// 配置键 + /// 如果存在返回 true,否则返回 false + bool HasConfig(string key); + + /// + /// 移除指定键的配置 + /// + /// 配置键 + /// 如果成功移除返回 true,否则返回 false + bool RemoveConfig(string key); + + /// + /// 清空所有配置 + /// + void Clear(); + + /// + /// 监听指定键的配置变化 + /// + /// 配置值类型 + /// 配置键 + /// 配置变化时的回调,参数为新值 + /// 取消注册接口 + IUnRegister WatchConfig(string key, Action onChange); + + /// + /// 从 JSON 字符串加载配置 + /// + /// JSON 字符串 + void LoadFromJson(string json); + + /// + /// 将配置保存为 JSON 字符串 + /// + /// JSON 字符串 + string SaveToJson(); + + /// + /// 从文件加载配置 + /// + /// 文件路径 + void LoadFromFile(string path); + + /// + /// 将配置保存到文件 + /// + /// 文件路径 + void SaveToFile(string path); + + /// + /// 获取所有配置键 + /// + /// 配置键集合 + IEnumerable GetAllKeys(); +} \ No newline at end of file diff --git a/GFramework.Core.Tests/configuration/ConfigurationManagerTests.cs b/GFramework.Core.Tests/configuration/ConfigurationManagerTests.cs new file mode 100644 index 0000000..45a38dd --- /dev/null +++ b/GFramework.Core.Tests/configuration/ConfigurationManagerTests.cs @@ -0,0 +1,377 @@ +using System.IO; +using GFramework.Core.configuration; +using NUnit.Framework; + +namespace GFramework.Core.Tests.configuration; + +/// +/// ConfigurationManager 功能测试类 +/// +[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("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("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.key"), Is.EqualTo(42)); + Assert.That(_configManager.GetConfig("string.key"), Is.EqualTo("hello")); + Assert.That(_configManager.GetConfig("bool.key"), Is.True); + Assert.That(_configManager.GetConfig("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("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("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("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("test.key", _ => count1++); + _configManager.WatchConfig("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("key1"), Is.EqualTo("value1")); + Assert.That(newManager.GetConfig("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("key1"), Is.EqualTo("value1")); + Assert.That(newManager.GetConfig("key2"), Is.EqualTo(42)); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [Test] + public void LoadFromFile_Should_Throw_When_File_Not_Exists() + { + Assert.Throws(() => { _configManager.LoadFromFile("nonexistent.json"); }); + } + + [Test] + public void SetConfig_Should_Throw_When_Key_Is_Null_Or_Empty() + { + Assert.Throws(() => _configManager.SetConfig(null!, "value")); + Assert.Throws(() => _configManager.SetConfig("", "value")); + Assert.Throws(() => _configManager.SetConfig(" ", "value")); + } + + [Test] + public void GetConfig_Should_Throw_When_Key_Is_Null_Or_Empty() + { + Assert.Throws(() => _configManager.GetConfig(null!)); + Assert.Throws(() => _configManager.GetConfig("")); + Assert.Throws(() => _configManager.GetConfig(" ")); + } + + [Test] + public void WatchConfig_Should_Throw_When_Parameters_Invalid() + { + Assert.Throws(() => _configManager.WatchConfig(null!, _ => { })); + Assert.Throws(() => _configManager.WatchConfig("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(); + + 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(); + + 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("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(); + var callCounts = new int[threadCount]; + + for (var i = 0; i < threadCount; i++) + { + var threadId = i; + tasks[i] = Task.Run(() => + { + try + { + _configManager.WatchConfig("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)); + } +} \ No newline at end of file diff --git a/GFramework.Core/configuration/ConfigWatcherUnRegister.cs b/GFramework.Core/configuration/ConfigWatcherUnRegister.cs new file mode 100644 index 0000000..78b6199 --- /dev/null +++ b/GFramework.Core/configuration/ConfigWatcherUnRegister.cs @@ -0,0 +1,21 @@ +using GFramework.Core.Abstractions.events; + +namespace GFramework.Core.Abstractions.configuration; + +/// +/// 配置监听取消注册接口 +/// +internal sealed class ConfigWatcherUnRegister : IUnRegister +{ + private readonly Action _unRegisterAction; + + public ConfigWatcherUnRegister(Action unRegisterAction) + { + _unRegisterAction = unRegisterAction ?? throw new ArgumentNullException(nameof(unRegisterAction)); + } + + public void UnRegister() + { + _unRegisterAction(); + } +} \ No newline at end of file diff --git a/GFramework.Core/configuration/ConfigurationManager.cs b/GFramework.Core/configuration/ConfigurationManager.cs new file mode 100644 index 0000000..3bf749c --- /dev/null +++ b/GFramework.Core/configuration/ConfigurationManager.cs @@ -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; + +/// +/// 配置管理器实现,提供线程安全的配置存储和访问 +/// 线程安全:所有公共方法都是线程安全的 +/// +public class ConfigurationManager : IConfigurationManager +{ + /// + /// Key 参数验证错误消息常量 + /// + private const string KeyCannotBeNullOrEmptyMessage = "Key cannot be null or whitespace."; + + /// + /// Path 参数验证错误消息常量 + /// + private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace."; + + /// + /// JSON 参数验证错误消息常量 + /// + private const string JsonCannotBeNullOrEmptyMessage = "JSON cannot be null or whitespace."; + + /// + /// 配置存储字典(线程安全) + /// + private readonly ConcurrentDictionary _configs = new(); + + /// + /// 用于保护监听器列表的锁 + /// + private readonly object _watcherLock = new(); + + /// + /// 配置监听器字典(线程安全) + /// 键:配置键,值:监听器列表 + /// + private readonly ConcurrentDictionary> _watchers = new(); + + /// + /// 获取配置数量 + /// + public int Count => _configs.Count; + + /// + /// 获取指定键的配置值 + /// + public T? GetConfig(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key)); + + if (_configs.TryGetValue(key, out var value)) + { + return ConvertValue(value); + } + + return default; + } + + /// + /// 获取指定键的配置值,如果不存在则返回默认值 + /// + public T GetConfig(string key, T defaultValue) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key)); + + if (_configs.TryGetValue(key, out var value)) + { + return ConvertValue(value); + } + + return defaultValue; + } + + /// + /// 设置指定键的配置值 + /// + public void SetConfig(string key, T value) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key)); + + var oldValue = _configs.AddOrUpdate(key, value!, (_, _) => value!); + + // 触发监听器 + if (!EqualityComparer.Default.Equals(oldValue, value)) + { + NotifyWatchers(key, value); + } + } + + /// + /// 检查指定键的配置是否存在 + /// + public bool HasConfig(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException(KeyCannotBeNullOrEmptyMessage, nameof(key)); + + return _configs.ContainsKey(key); + } + + /// + /// 移除指定键的配置 + /// + 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; + } + + /// + /// 清空所有配置 + /// + public void Clear() + { + _configs.Clear(); + + // 清空监听器(使用锁保护) + lock (_watcherLock) + { + _watchers.Clear(); + } + } + + /// + /// 监听指定键的配置变化 + /// + public IUnRegister WatchConfig(string key, Action 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(); + _watchers[key] = watchers; + } + + watchers.Add(onChange); + } + + return new ConfigWatcherUnRegister(() => UnwatchConfig(key, onChange)); + } + + /// + /// 从 JSON 字符串加载配置 + /// + public void LoadFromJson(string json) + { + if (string.IsNullOrWhiteSpace(json)) + throw new ArgumentException(JsonCannotBeNullOrEmptyMessage, nameof(json)); + + var dict = JsonSerializer.Deserialize>(json); + if (dict == null) + return; + + foreach (var kvp in dict) + { + _configs[kvp.Key] = kvp.Value; + } + } + + /// + /// 将配置保存为 JSON 字符串 + /// + public string SaveToJson() + { + var dict = _configs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + return JsonSerializer.Serialize(dict, new JsonSerializerOptions + { + WriteIndented = true + }); + } + + /// + /// 从文件加载配置 + /// + 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); + } + + /// + /// 将配置保存到文件 + /// + 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); + } + + /// + /// 获取所有配置键 + /// + public IEnumerable GetAllKeys() + { + return _configs.Keys.ToList(); + } + + /// + /// 取消监听指定键的配置变化 + /// + private void UnwatchConfig(string key, Action onChange) + { + lock (_watcherLock) + { + if (_watchers.TryGetValue(key, out var watchers)) + { + watchers.Remove(onChange); + + if (watchers.Count == 0) + { + _watchers.TryRemove(key, out _); + } + } + } + } + + /// + /// 通知监听器配置已变化 + /// + private void NotifyWatchers(string key, T newValue) + { + List? watchersCopy = null; + + lock (_watcherLock) + { + if (_watchers.TryGetValue(key, out var watchers)) + { + watchersCopy = new List(watchers); + } + } + + if (watchersCopy == null) + return; + + foreach (var watcher in watchersCopy) + { + try + { + if (watcher is Action typedWatcher) + { + typedWatcher(newValue); + } + } + catch (Exception ex) + { + // 防止监听器异常影响其他监听器 + Console.Error.WriteLine( + $"[ConfigurationManager] Error in config watcher for key '{key}': {ex.Message}"); + } + } + } + + /// + /// 转换配置值到目标类型 + /// + private static T ConvertValue(object value) + { + if (value is T typedValue) + { + return typedValue; + } + + // 处理 JsonElement 类型 + if (value is JsonElement jsonElement) + { + return JsonSerializer.Deserialize(jsonElement.GetRawText())!; + } + + // 尝试类型转换 + try + { + return (T)Convert.ChangeType(value, typeof(T)); + } + catch + { + return default!; + } + } +} \ No newline at end of file