A:配置读取

This commit is contained in:
jimmy committed 2025-11-13 18:43:23 +08:00
1 parent 859814514b
commit 727c83da2d
13 files changed
+671 -32

No files matched your search

+1
View File
@@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageReference Include="Tomlyn" Version="0.16.2" /> <PackageReference Include="Tomlyn" Version="0.16.2" />
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.0" /> <PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.0" />
+1 -2
View File
@@ -1,6 +1,6 @@
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
namespace Sharp.Config; namespace Config;
public static class ConfigBuilder public static class ConfigBuilder
{ {
@@ -37,4 +37,3 @@ public static class ConfigBuilder
}); });
} }
} }
+1 -2
View File
@@ -1,4 +1,4 @@
namespace Sharp.Config; namespace Config;
public enum ConfigFormat public enum ConfigFormat
{ {
@@ -7,4 +7,3 @@ public enum ConfigFormat
Xml, Xml,
Toml Toml
} }
+6 -6
View File
@@ -3,10 +3,10 @@ using System.Text;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Sharp.Config.Serialization; using Config.Serialization;
using Tomlyn.Extensions.Configuration; using Tomlyn.Extensions.Configuration;
namespace Sharp.Config; namespace Config;
public sealed class ConfigManager public sealed class ConfigManager
{ {
@@ -47,7 +47,8 @@ public sealed class ConfigManager
builder.AddXmlFile(src.Path, src.Optional, options.ReloadOnChange); builder.AddXmlFile(src.Path, src.Optional, options.ReloadOnChange);
break; break;
case ConfigFormat.Toml: case ConfigFormat.Toml:
builder.AddToml(src.Path, src.Optional, options.ReloadOnChange); // Tomlyn.Extensions.Configuration uses AddTomlFile in recent versions
builder.AddTomlFile(src.Path, src.Optional, options.ReloadOnChange);
break; break;
default: default:
throw new NotSupportedException($"Unsupported format: {src.Format}"); throw new NotSupportedException($"Unsupported format: {src.Format}");
@@ -154,8 +155,8 @@ public sealed class ConfigManager
// Reload notifications // Reload notifications
public IDisposable OnReload(Action callback) public IDisposable OnReload(Action callback)
{ {
IChangeToken token = Configuration.GetReloadToken(); // 始终基于最新的 ReloadToken 订阅,以支持连续热更新
return ChangeToken.OnChange(() => token, callback); return ChangeToken.OnChange(() => Configuration.GetReloadToken(), callback);
} }
} }
@@ -180,4 +181,3 @@ public sealed class ConfigurationValidationException : Exception
Errors = errors; Errors = errors;
} }
} }
+7 -8
View File
@@ -1,20 +1,19 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
namespace Sharp.Config; namespace Config;
public sealed class ConfigOptions public sealed class ConfigOptions
{ {
public string? BasePath { get; init; } public string? BasePath { get; set; }
public IList<ConfigSource> Sources { get; } = new List<ConfigSource>(); public IList<ConfigSource> Sources { get; } = new List<ConfigSource>();
public bool ReloadOnChange { get; init; } = true; public bool ReloadOnChange { get; set; } = true;
public bool IncludeEnvironmentVariables { get; init; } = true; public bool IncludeEnvironmentVariables { get; set; } = true;
public string? EnvironmentPrefix { get; init; } public string? EnvironmentPrefix { get; set; }
public IFileProvider? FileProvider { get; init; } public IFileProvider? FileProvider { get; set; }
// Hook: allow external modules (如 Common) 自定义构建 // Hook: allow external modules (如 Common) 自定义构建
public Func<IConfigurationBuilder, IConfigurationBuilder>? ConfigureBuilderHook { get; init; } public Func<IConfigurationBuilder, IConfigurationBuilder>? ConfigureBuilderHook { get; set; }
} }
+1 -2
View File
@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace Sharp.Config; namespace Config;
public sealed class ConfigSource public sealed class ConfigSource
{ {
@@ -28,4 +28,3 @@ public sealed class ConfigSource
return true; return true;
} }
} }
@@ -1,6 +1,6 @@
using System.Text; using System.Text;
namespace Sharp.Config.Serialization; namespace Config.Serialization;
public interface IConfigSerializer public interface IConfigSerializer
{ {
@@ -9,4 +9,3 @@ public interface IConfigSerializer
void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null); void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null);
T? DeserializeFromFile<T>(string path, Encoding? encoding = null); T? DeserializeFromFile<T>(string path, Encoding? encoding = null);
} }
@@ -1,6 +1,6 @@
using System.Text; using System.Text;
namespace Sharp.Config.Serialization; namespace Config.Serialization;
public sealed class IniConfigSerializer : IConfigSerializer public sealed class IniConfigSerializer : IConfigSerializer
{ {
@@ -1,7 +1,7 @@
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
namespace Sharp.Config.Serialization; namespace Config.Serialization;
public sealed class JsonConfigSerializer : IConfigSerializer public sealed class JsonConfigSerializer : IConfigSerializer
{ {
@@ -41,4 +41,3 @@ public sealed class JsonConfigSerializer : IConfigSerializer
return Deserialize<T>(text); return Deserialize<T>(text);
} }
} }
@@ -1,19 +1,21 @@
using System.Text; using System.Text;
using Tomlyn; using Tomlyn;
namespace Sharp.Config.Serialization; namespace Config.Serialization;
public sealed class TomlConfigSerializer : IConfigSerializer public sealed class TomlConfigSerializer : IConfigSerializer
{ {
public string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null) public string Serialize<T>(T instance, bool indented = true, Encoding? encoding = null)
{ {
// Tomlyn writes with indentation by default return Toml.FromModel(instance!);
return Toml.FromModel(instance!, new TomlModelOptions { ConvertIndentation = true });
} }
public T? Deserialize<T>(string content) public T? Deserialize<T>(string content)
{ {
return Toml.ToModel<T>(content); // To keep dependencies light and constraints simple across value types,
// prefer using the configuration builder + binder path for TOML.
// Direct generic mapping with Toml.ToModel<T> requires T : class.
throw new NotSupportedException("Use ConfigManager with AddTomlFile + binder for typed TOML binding.");
} }
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null) public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
@@ -30,4 +32,3 @@ public sealed class TomlConfigSerializer : IConfigSerializer
return Deserialize<T>(text); return Deserialize<T>(text);
} }
} }
@@ -1,7 +1,7 @@
using System.Text; using System.Text;
using System.Xml.Serialization; using System.Xml.Serialization;
namespace Sharp.Config.Serialization; namespace Config.Serialization;
public sealed class XmlConfigSerializer : IConfigSerializer public sealed class XmlConfigSerializer : IConfigSerializer
{ {
@@ -36,4 +36,3 @@ public sealed class XmlConfigSerializer : IConfigSerializer
return Deserialize<T>(text); return Deserialize<T>(text);
} }
} }
+643
View File
@@ -0,0 +1,643 @@
namespace Common.Test;
/// <summary>
/// Global 轻量级 DI 容器测试
/// 展示了依赖注入容器的所有功能和使用场景
/// </summary>
[TestClass]
public class GlobalTests
{
/// <summary>
/// 每个测试方法执行前清除所有注册的服务,确保测试隔离
/// </summary>
[TestInitialize]
public void Setup()
{
Global.Clear();
}
#region 基础注册和获取测试
/// <summary>
/// 测试:注册并获取单例服务实例
/// 演示最基本的服务注册和获取
/// </summary>
[TestMethod]
public void RegisterSingleton_WithInstance_CanGetSameInstance()
{
// Arrange: 创建一个配置对象
var config = new AppConfig { AppName = "MyApp", Version = "1.0" };
// Act: 注册为单例
Global.RegisterSingleton(config);
var retrieved = Global.Get<AppConfig>();
// Assert: 应该返回同一个实例
Assert.AreSame(config, retrieved);
Assert.AreEqual("MyApp", retrieved.AppName);
}
/// <summary>
/// 测试:使用工厂方法注册单例
/// 演示延迟初始化,只有在第一次 Get 时才会调用工厂方法
/// </summary>
[TestMethod]
public void RegisterSingleton_WithFactory_CreatesInstanceOnFirstGet()
{
// Arrange: 创建一个计数器来跟踪工厂方法被调用的次数
var factoryCallCount = 0;
// Act: 使用工厂方法注册
Global.RegisterSingleton(() =>
{
factoryCallCount++;
return new AppConfig { AppName = "FactoryApp" };
});
// Assert: 工厂方法还未被调用
Assert.AreEqual(0, factoryCallCount);
// Act: 第一次获取
var first = Global.Get<AppConfig>();
Assert.AreEqual(1, factoryCallCount); // 工厂方法被调用了一次
// Act: 第二次获取
var second = Global.Get<AppConfig>();
Assert.AreEqual(1, factoryCallCount); // 工厂方法没有再次调用
Assert.AreSame(first, second); // 返回的是同一个实例
}
/// <summary>
/// 测试:注册瞬态服务,每次获取都创建新实例
/// </summary>
[TestMethod]
public void RegisterTransient_CreatesNewInstanceEachTime()
{
// Arrange & Act: 注册瞬态服务
Global.RegisterTransient(() => new RequestContext
{
RequestId = Guid.NewGuid().ToString(),
Timestamp = DateTime.Now
});
// Act: 多次获取
var first = Global.Get<RequestContext>();
var second = Global.Get<RequestContext>();
var third = Global.Get<RequestContext>();
// Assert: 每次都是不同的实例
Assert.AreNotSame(first, second);
Assert.AreNotSame(second, third);
Assert.AreNotEqual(first.RequestId, second.RequestId);
}
/// <summary>
/// 测试:使用通用 Register 方法指定生命周期
/// </summary>
[TestMethod]
public void Register_WithLifetime_CreatesCorrectLifetime()
{
// Arrange & Act: 使用通用方法注册单例
Global.Register(() => new CacheService(), ServiceLifetime.Singleton);
var cache1 = Global.Get<CacheService>();
var cache2 = Global.Get<CacheService>();
// Assert: 单例应该是同一个实例
Assert.AreSame(cache1, cache2);
// Arrange & Act: 使用通用方法注册瞬态
Global.Register(() => new OperationContext(), ServiceLifetime.Transient);
var op1 = Global.Get<OperationContext>();
var op2 = Global.Get<OperationContext>();
// Assert: 瞬态应该是不同实例
Assert.AreNotSame(op1, op2);
}
#endregion
#region 服务获取测试
/// <summary>
/// 测试:获取未注册的服务会抛出异常
/// </summary>
[TestMethod]
public void Get_UnregisteredService_ThrowsException()
{
// Act & Assert: 尝试获取未注册的服务应该抛出异常
var exception = Assert.ThrowsException<InvalidOperationException>(() =>
{
Global.Get<ILogger>();
});
Assert.IsTrue(exception.Message.Contains("ILogger"));
}
/// <summary>
/// 测试:TryGet 安全地尝试获取服务
/// </summary>
[TestMethod]
public void TryGet_UnregisteredService_ReturnsFalse()
{
// Act: 尝试获取未注册的服务
var success = Global.TryGet<ILogger>(out var logger);
// Assert: 应该返回 false 且 logger 为 null
Assert.IsFalse(success);
Assert.IsNull(logger);
}
/// <summary>
/// 测试:TryGet 成功获取已注册的服务
/// </summary>
[TestMethod]
public void TryGet_RegisteredService_ReturnsTrue()
{
// Arrange: 注册服务
var expectedLogger = new ConsoleLogger();
Global.RegisterSingleton<ILogger>(expectedLogger);
// Act: 尝试获取
var success = Global.TryGet<ILogger>(out var logger);
// Assert: 应该成功获取
Assert.IsTrue(success);
Assert.IsNotNull(logger);
Assert.AreSame(expectedLogger, logger);
}
/// <summary>
/// 测试:GetOrDefault 获取服务或返回默认值
/// </summary>
[TestMethod]
public void GetOrDefault_UnregisteredService_ReturnsDefault()
{
// Arrange: 创建默认值
var defaultLogger = new ConsoleLogger();
// Act: 获取未注册的服务
var logger = Global.GetOrDefault(defaultLogger);
// Assert: 应该返回默认值
Assert.AreSame(defaultLogger, logger);
}
/// <summary>
/// 测试:GetOrDefault 获取已注册的服务而不是默认值
/// </summary>
[TestMethod]
public void GetOrDefault_RegisteredService_ReturnsService()
{
// Arrange: 注册服务
var registeredLogger = new ConsoleLogger();
Global.RegisterSingleton<ILogger>(registeredLogger);
var defaultLogger = new ConsoleLogger();
// Act: 获取服务
var logger = Global.GetOrDefault(defaultLogger);
// Assert: 应该返回注册的服务,而不是默认值
Assert.AreSame(registeredLogger, logger);
Assert.AreNotSame(defaultLogger, logger);
}
#endregion
#region 服务更新和变更通知测试
/// <summary>
/// 测试:更新单例服务实例
/// </summary>
[TestMethod]
public void Update_SingletonService_UpdatesInstance()
{
// Arrange: 注册初始配置
var oldConfig = new AppConfig { AppName = "OldApp", Version = "1.0" };
Global.RegisterSingleton(oldConfig);
// Act: 更新配置
var newConfig = new AppConfig { AppName = "NewApp", Version = "2.0" };
Global.Update(newConfig);
// Assert: 获取的应该是新配置
var retrieved = Global.Get<AppConfig>();
Assert.AreSame(newConfig, retrieved);
Assert.AreEqual("NewApp", retrieved.AppName);
}
/// <summary>
/// 测试:更新瞬态服务会抛出异常
/// </summary>
[TestMethod]
public void Update_TransientService_ThrowsException()
{
// Arrange: 注册瞬态服务
Global.RegisterTransient(() => new RequestContext());
// Act & Assert: 尝试更新瞬态服务应该抛出异常
Assert.ThrowsException<InvalidOperationException>(() =>
{
Global.Update(new RequestContext());
});
}
/// <summary>
/// 测试:订阅服务变更通知
/// 演示如何监听服务变更事件
/// </summary>
[TestMethod]
public void Subscribe_ServiceChange_ReceivesNotification()
{
// Arrange: 设置事件处理器
ServiceChangedEventArgs<AppConfig>? receivedEventArgs = null;
var callCount = 0;
Global.Subscribe<AppConfig>(e =>
{
receivedEventArgs = e;
callCount++;
});
// Act: 注册初始服务
var oldConfig = new AppConfig { AppName = "OldApp", Version = "1.0" };
Global.RegisterSingleton(oldConfig);
// 第一次注册不会触发通知(因为没有旧值)
Assert.AreEqual(0, callCount);
// Act: 更新服务
var newConfig = new AppConfig { AppName = "NewApp", Version = "2.0" };
Global.Update(newConfig);
// Assert: 应该收到通知
Assert.AreEqual(1, callCount);
Assert.IsNotNull(receivedEventArgs);
Assert.AreSame(oldConfig, receivedEventArgs.OldValue);
Assert.AreSame(newConfig, receivedEventArgs.NewValue);
Assert.AreEqual("OldApp", receivedEventArgs.OldValue?.AppName);
Assert.AreEqual("NewApp", receivedEventArgs.NewValue.AppName);
}
/// <summary>
/// 测试:多个订阅者都能收到通知
/// </summary>
[TestMethod]
public void Subscribe_MultipleSubscribers_AllReceiveNotifications()
{
// Arrange: 创建多个订阅者
var subscriber1Called = false;
var subscriber2Called = false;
var subscriber3Called = false;
Global.Subscribe<AppConfig>(e => subscriber1Called = true);
Global.Subscribe<AppConfig>(e => subscriber2Called = true);
Global.Subscribe<AppConfig>(e => subscriber3Called = true);
// Act: 注册并更新服务
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "1.0" });
Global.Update(new AppConfig { AppName = "App", Version = "2.0" });
// Assert: 所有订阅者都应该收到通知
Assert.IsTrue(subscriber1Called);
Assert.IsTrue(subscriber2Called);
Assert.IsTrue(subscriber3Called);
}
/// <summary>
/// 测试:取消订阅后不再收到通知
/// </summary>
[TestMethod]
public void Unsubscribe_RemovesSubscriber()
{
// Arrange: 订阅通知
var callCount = 0;
void Handler(ServiceChangedEventArgs<AppConfig> e) => callCount++;
Global.Subscribe<AppConfig>(Handler);
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "1.0" });
// Act: 更新一次(应该收到通知)
Global.Update(new AppConfig { AppName = "App", Version = "2.0" });
Assert.AreEqual(1, callCount);
// Act: 取消订阅
Global.Unsubscribe<AppConfig>(Handler);
// Act: 再次更新(不应该收到通知)
Global.Update(new AppConfig { AppName = "App", Version = "3.0" });
Assert.AreEqual(1, callCount); // 计数没有增加
}
/// <summary>
/// 测试:注册时启用 notifyIfExists 会触发通知
/// </summary>
[TestMethod]
public void RegisterSingleton_WithNotifyIfExists_TriggersNotification()
{
// Arrange: 订阅通知
var notificationCount = 0;
Global.Subscribe<AppConfig>(e => notificationCount++);
// Act: 第一次注册
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "1.0" });
Assert.AreEqual(0, notificationCount); // 首次注册不触发
// Act: 使用 notifyIfExists = true 再次注册
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "2.0" }, notifyIfExists: true);
// Assert: 应该触发通知
Assert.AreEqual(1, notificationCount);
}
#endregion
#region 服务管理测试
/// <summary>
/// 测试:检查服务是否已注册
/// </summary>
[TestMethod]
public void IsRegistered_ChecksServiceRegistration()
{
// Assert: 初始未注册
Assert.IsFalse(Global.IsRegistered<AppConfig>());
// Act: 注册服务
Global.RegisterSingleton(new AppConfig());
// Assert: 现在已注册
Assert.IsTrue(Global.IsRegistered<AppConfig>());
}
/// <summary>
/// 测试:注销服务
/// </summary>
[TestMethod]
public void Unregister_RemovesService()
{
// Arrange: 注册服务
Global.RegisterSingleton(new AppConfig { AppName = "TestApp" });
Assert.IsTrue(Global.IsRegistered<AppConfig>());
// Act: 注销服务
var result = Global.Unregister<AppConfig>();
// Assert: 服务已被移除
Assert.IsTrue(result);
Assert.IsFalse(Global.IsRegistered<AppConfig>());
// 尝试获取应该失败
Assert.ThrowsException<InvalidOperationException>(() =>
{
Global.Get<AppConfig>();
});
}
/// <summary>
/// 测试:清除所有服务
/// </summary>
[TestMethod]
public void Clear_RemovesAllServices()
{
// Arrange: 注册多个服务
Global.RegisterSingleton(new AppConfig());
Global.RegisterSingleton<ILogger>(new ConsoleLogger());
Global.RegisterTransient(() => new RequestContext());
Assert.AreEqual(3, Global.Count);
// Act: 清除所有服务
Global.Clear();
// Assert: 所有服务都被移除
Assert.AreEqual(0, Global.Count);
Assert.IsFalse(Global.IsRegistered<AppConfig>());
Assert.IsFalse(Global.IsRegistered<ILogger>());
}
/// <summary>
/// 测试:获取已注册服务的数量
/// </summary>
[TestMethod]
public void Count_ReturnsCorrectNumber()
{
// Assert: 初始为 0
Assert.AreEqual(0, Global.Count);
// Act: 注册服务
Global.RegisterSingleton(new AppConfig());
Assert.AreEqual(1, Global.Count);
Global.RegisterSingleton<ILogger>(new ConsoleLogger());
Assert.AreEqual(2, Global.Count);
Global.RegisterTransient(() => new RequestContext());
Assert.AreEqual(3, Global.Count);
// Act: 注销一个服务
Global.Unregister<AppConfig>();
Assert.AreEqual(2, Global.Count);
}
#endregion
#region 线程安全测试
/// <summary>
/// 测试:多线程并发获取单例服务,确保只创建一次
/// 演示容器的线程安全性
/// </summary>
[TestMethod]
public void Get_ConcurrentAccess_CreatesSingletonOnce()
{
// Arrange: 使用计数器追踪工厂方法调用次数
var factoryCallCount = 0;
var createdInstances = new HashSet<int>();
Global.RegisterSingleton(() =>
{
Interlocked.Increment(ref factoryCallCount);
Thread.Sleep(50); // 模拟复杂初始化
return new CacheService();
});
// Act: 多线程并发获取
var tasks = Enumerable.Range(0, 20).Select(i =>
Task.Run(() =>
{
var service = Global.Get<CacheService>();
lock (createdInstances)
{
createdInstances.Add(service.GetHashCode());
}
})
).ToArray();
Task.WaitAll(tasks);
// Assert: 工厂方法只被调用一次,所有线程获取的是同一个实例
Assert.AreEqual(1, factoryCallCount);
Assert.AreEqual(1, createdInstances.Count);
}
/// <summary>
/// 测试:多线程并发订阅和更新
/// </summary>
[TestMethod]
public void Subscribe_ConcurrentSubscribeAndUpdate_ThreadSafe()
{
// Arrange: 注册服务
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "1.0" });
var notificationCounts = new System.Collections.Concurrent.ConcurrentBag<int>();
// Act: 多线程并发订阅
var subscribeTasks = Enumerable.Range(0, 10).Select(i =>
Task.Run(() =>
{
var count = 0;
Global.Subscribe<AppConfig>(e => Interlocked.Increment(ref count));
notificationCounts.Add(count);
})
).ToArray();
Task.WaitAll(subscribeTasks);
// Act: 触发更新
Global.Update(new AppConfig { AppName = "App", Version = "2.0" });
// 等待所有通知处理完成
Thread.Sleep(100);
// Assert: 不应该抛出异常,所有订阅者都应该收到通知
Assert.AreEqual(10, notificationCounts.Count);
}
#endregion
#region 实际应用场景测试
/// <summary>
/// 场景测试:应用配置热更新
/// 演示实时配置更新的完整流程
/// </summary>
[TestMethod]
public void Scenario_LiveConfigurationUpdate()
{
// Arrange: 模拟配置变更历史
var configHistory = new List<string>();
// 订阅配置变更
Global.Subscribe<AppConfig>(e =>
{
configHistory.Add($"v{e.OldValue?.Version} -> v{e.NewValue.Version}");
});
// Act: 注册初始配置
Global.RegisterSingleton(new AppConfig { AppName = "MyApp", Version = "1.0" });
// 模拟配置文件更新
Global.Update(new AppConfig { AppName = "MyApp", Version = "1.1" });
Global.Update(new AppConfig { AppName = "MyApp", Version = "1.2" });
Global.Update(new AppConfig { AppName = "MyApp", Version = "2.0" });
// Assert: 应该记录所有变更
Assert.AreEqual(3, configHistory.Count);
Assert.AreEqual("v1.0 -> v1.1", configHistory[0]);
Assert.AreEqual("v1.1 -> v1.2", configHistory[1]);
Assert.AreEqual("v1.2 -> v2.0", configHistory[2]);
// 当前配置应该是最新的
var currentConfig = Global.Get<AppConfig>();
Assert.AreEqual("2.0", currentConfig.Version);
}
/// <summary>
/// 场景测试:用户会话管理
/// 演示多组件监听用户会话变更
/// </summary>
[TestMethod]
public void Scenario_UserSessionManagement()
{
// Arrange: 模拟不同组件
var uiUpdates = new List<string>();
var logEntries = new List<string>();
// UI 组件订阅
Global.Subscribe<UserSession>(e =>
{
uiUpdates.Add($"Display: {e.NewValue.Username}");
});
// 日志组件订阅
Global.Subscribe<UserSession>(e =>
{
logEntries.Add($"Session changed at {e.ChangedAt:HH:mm:ss}");
});
// Act: 用户登录
Global.RegisterSingleton(new UserSession
{
Username = "user1",
LoginTime = DateTime.Now
});
// 用户切换
Thread.Sleep(10);
Global.Update(new UserSession
{
Username = "user2",
LoginTime = DateTime.Now
});
// Assert: 各组件都应该收到通知
Assert.AreEqual(1, uiUpdates.Count);
Assert.AreEqual("Display: user2", uiUpdates[0]);
Assert.AreEqual(1, logEntries.Count);
Assert.IsTrue(logEntries[0].StartsWith("Session changed at"));
}
#endregion
#region 测试辅助类型
private class AppConfig
{
public string AppName { get; set; } = "";
public string Version { get; set; } = "";
}
private class RequestContext
{
public string RequestId { get; set; } = Guid.NewGuid().ToString();
public DateTime Timestamp { get; set; } = DateTime.Now;
}
private interface ILogger
{
void Log(string message);
}
private class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"[LOG] {message}");
}
private class UserSession
{
public string Username { get; set; } = "";
public DateTime LoginTime { get; set; }
}
private class CacheService
{
private readonly Dictionary<string, object> _cache = new();
}
private class OperationContext
{
public Guid OperationId { get; } = Guid.NewGuid();
}
#endregion
}
+1
View File
@@ -0,0 +1 @@
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]