U:调整所有代码格式

This commit is contained in:
jimmy committed 2025-12-30 09:17:25 +08:00
1 parent d133bacf55
commit 155b67e831
342 files changed
+9431 -9868

No files matched your search

+8 -11
View File
@@ -118,9 +118,9 @@ public class CollectionExpandTests
// Act
var tree = items.ToTree(
idSelector: x => x.Id,
parentIdSelector: x => x.ParentId,
rootParentId: null
x => x.Id,
x => x.ParentId,
null
);
// Assert
@@ -243,10 +243,7 @@ public class CollectionExpandTests
var processedBatches = new List<int>();
// Act
items.ForEachBatch(10, batch =>
{
processedBatches.Add(batch.Count());
});
items.ForEachBatch(10, batch => { processedBatches.Add(batch.Count()); });
// Assert
Assert.AreEqual(3, processedBatches.Count);
@@ -618,9 +615,9 @@ public class CollectionExpandTests
// Act: 数据清洗流水线
var cleanedData = rawData
.WhereNotNull() // 去除 null
.DistinctBy(x => x) // 去重
.Where(x => x != "D") // 过滤不需要的数据
.WhereNotNull() // 去除 null
.DistinctBy(x => x) // 去重
.Where(x => x != "D") // 过滤不需要的数据
.ToList();
// Assert
@@ -629,4 +626,4 @@ public class CollectionExpandTests
}
#endregion
}
}
+10 -19
View File
@@ -67,10 +67,7 @@ public class CryptoHelperTests
var encrypted = CryptoHelper.AesEncrypt(plainText, correctPassword);
// Act & Assert
Assert.ThrowsException<CryptographicException>(() =>
{
CryptoHelper.AesDecrypt(encrypted, wrongPassword);
});
Assert.ThrowsException<CryptographicException>(() => { CryptoHelper.AesDecrypt(encrypted, wrongPassword); });
}
/// <summary>
@@ -79,10 +76,7 @@ public class CryptoHelperTests
[TestMethod]
public void AesEncrypt_EmptyString_ThrowsException()
{
Assert.ThrowsException<ArgumentNullException>(() =>
{
CryptoHelper.AesEncrypt("", "password");
});
Assert.ThrowsException<ArgumentNullException>(() => { CryptoHelper.AesEncrypt("", "password"); });
}
#endregion
@@ -155,10 +149,7 @@ public class CryptoHelperTests
var encrypted = CryptoHelper.RsaEncrypt(plainText, publicKey1);
// Act & Assert
Assert.ThrowsException<CryptographicException>(() =>
{
CryptoHelper.RsaDecrypt(encrypted, privateKey2);
});
Assert.ThrowsException<CryptographicException>(() => { CryptoHelper.RsaDecrypt(encrypted, privateKey2); });
}
#endregion
@@ -396,7 +387,7 @@ public class CryptoHelperTests
var password = "test";
// Act
var hash = CryptoHelper.HashPassword(password, saltSize: 32, iterations: 50000, hashSize: 64);
var hash = CryptoHelper.HashPassword(password, 32, 50000, 64);
var isValid = CryptoHelper.VerifyPassword(password, hash);
// Assert
@@ -514,7 +505,7 @@ public class CryptoHelperTests
// Arrange & Act
var str10 = CryptoHelper.GenerateRandomString(10);
var str20 = CryptoHelper.GenerateRandomString(20);
var strWithSpecial = CryptoHelper.GenerateRandomString(15, useSpecialChars: true);
var strWithSpecial = CryptoHelper.GenerateRandomString(15, true);
// Assert
Assert.AreEqual(10, str10.Length);
@@ -548,10 +539,10 @@ public class CryptoHelperTests
// Act
var password = CryptoHelper.GeneratePassword(
12,
requireDigit: true,
requireLowercase: true,
requireUppercase: true,
requireSpecialChar: false
true,
true,
true,
false
);
// Assert
@@ -745,4 +736,4 @@ public class CryptoHelperTests
}
#endregion
}
}
+6 -6
View File
@@ -413,8 +413,8 @@ public class DateTimeExpandTests
{
// Arrange
var saturday = new DateTime(2024, 3, 16); // 周六
var sunday = new DateTime(2024, 3, 17); // 周日
var monday = new DateTime(2024, 3, 18); // 周一
var sunday = new DateTime(2024, 3, 17); // 周日
var monday = new DateTime(2024, 3, 18); // 周一
// Act & Assert
Assert.IsTrue(saturday.IsWeekend());
@@ -502,7 +502,7 @@ public class DateTimeExpandTests
// Act: 舍入到 15 分钟间隔
var ceiling = dt.CeilingToMinutes(15); // 向上舍入到 14:30
var floor = dt.FloorToMinutes(15); // 向下舍入到 14:15
var floor = dt.FloorToMinutes(15); // 向下舍入到 14:15
// Assert
Assert.AreEqual(30, ceiling.Minute);
@@ -559,8 +559,8 @@ public class DateTimeExpandTests
{
// Arrange: 营业时间是工作日 9:00-18:00
var workdayMorning = new DateTime(2024, 3, 15, 10, 30, 0); // 周五 10:30
var weekend = new DateTime(2024, 3, 16, 10, 30, 0); // 周六 10:30
var afterHours = new DateTime(2024, 3, 15, 19, 30, 0); // 周五 19:30
var weekend = new DateTime(2024, 3, 16, 10, 30, 0); // 周六 10:30
var afterHours = new DateTime(2024, 3, 15, 19, 30, 0); // 周五 19:30
// Act & Assert
Assert.IsTrue(workdayMorning.IsWorkDay());
@@ -629,4 +629,4 @@ public class DateTimeExpandTests
}
#endregion
}
}
+3 -3
View File
@@ -410,7 +410,7 @@ public class EnumHelperTests
public void ToEnum_ConvertsIntToEnum()
{
// Arrange
int value = 3;
var value = 3;
// Act
var status = value.ToEnum<OrderStatus>();
@@ -517,7 +517,7 @@ public class EnumHelperTests
public void GetRandomExcept_ExcludesSpecifiedValues()
{
// Act: 多次获取随机值,确保不包含排除的值
for (int i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
var random = EnumHelper.GetRandomExcept(OrderStatus.Pending, OrderStatus.Cancelled);
@@ -631,4 +631,4 @@ public class EnumHelperTests
}
#endregion
}
}
+4 -13
View File
@@ -104,10 +104,7 @@ public class ExceptionExpandTests
public void Scenario_UseInBusinessLogic()
{
// Act: 执行可能失败的业务操作
var exception = Assert.ThrowsException<ExceptionExpand>(() =>
{
ProcessBusinessOperation(-1);
});
var exception = Assert.ThrowsException<ExceptionExpand>(() => { ProcessBusinessOperation(-1); });
// Assert
Assert.AreEqual(4001L, exception.ErrorCode);
@@ -132,10 +129,7 @@ public class ExceptionExpandTests
);
// Act: 捕获并检查异常链
var exception = Assert.ThrowsException<ExceptionExpand>(() =>
{
throw serviceException;
});
var exception = Assert.ThrowsException<ExceptionExpand>(() => { throw serviceException; });
// Assert: 验证异常链
Assert.AreEqual(5001L, exception.ErrorCode);
@@ -179,10 +173,7 @@ public class ExceptionExpandTests
/// </summary>
private void ProcessBusinessOperation(int userId)
{
if (userId <= 0)
{
throw new ExceptionExpand(4001L, "Invalid user ID", "User ID must be positive");
}
if (userId <= 0) throw new ExceptionExpand(4001L, "Invalid user ID", "User ID must be positive");
// 正常处理逻辑...
}
@@ -201,4 +192,4 @@ public class ExceptionExpandTests
}
#endregion
}
}
+17 -28
View File
@@ -125,10 +125,7 @@ public class GlobalTests
public void Get_UnregisteredService_ThrowsException()
{
// Act & Assert: 尝试获取未注册的服务应该抛出异常
var exception = Assert.ThrowsException<InvalidOperationException>(() =>
{
Global.Get<ILogger>();
});
var exception = Assert.ThrowsException<InvalidOperationException>(() => { Global.Get<ILogger>(); });
Assert.IsTrue(exception.Message.Contains("ILogger"));
}
@@ -235,10 +232,7 @@ public class GlobalTests
Global.RegisterTransient(() => new RequestContext());
// Act & Assert: 尝试更新瞬态服务应该抛出异常
Assert.ThrowsException<InvalidOperationException>(() =>
{
Global.Update(new RequestContext());
});
Assert.ThrowsException<InvalidOperationException>(() => { Global.Update(new RequestContext()); });
}
/// <summary>
@@ -311,7 +305,11 @@ public class GlobalTests
{
// Arrange: 订阅通知
var callCount = 0;
void Handler(ServiceChangedEventArgs<AppConfig> e) => callCount++;
void Handler(ServiceChangedEventArgs<AppConfig> e)
{
callCount++;
}
Global.Subscribe<AppConfig>(Handler);
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "1.0" });
@@ -343,7 +341,7 @@ public class GlobalTests
Assert.AreEqual(0, notificationCount); // 首次注册不触发
// Act: 使用 notifyIfExists = true 再次注册
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "2.0" }, notifyIfExists: true);
Global.RegisterSingleton(new AppConfig { AppName = "App", Version = "2.0" }, true);
// Assert: 应该触发通知
Assert.AreEqual(1, notificationCount);
@@ -387,10 +385,7 @@ public class GlobalTests
Assert.IsFalse(Global.IsRegistered<AppConfig>());
// 尝试获取应该失败
Assert.ThrowsException<InvalidOperationException>(() =>
{
Global.Get<AppConfig>();
});
Assert.ThrowsException<InvalidOperationException>(() => { Global.Get<AppConfig>(); });
}
/// <summary>
@@ -528,10 +523,7 @@ public class GlobalTests
var configHistory = new List<string>();
// 订阅配置变更
Global.Subscribe<AppConfig>(e =>
{
configHistory.Add($"v{e.OldValue?.Version} -> v{e.NewValue.Version}");
});
Global.Subscribe<AppConfig>(e => { configHistory.Add($"v{e.OldValue?.Version} -> v{e.NewValue.Version}"); });
// Act: 注册初始配置
Global.RegisterSingleton(new AppConfig { AppName = "MyApp", Version = "1.0" });
@@ -564,16 +556,10 @@ public class GlobalTests
var logEntries = new List<string>();
// UI 组件订阅
Global.Subscribe<UserSession>(e =>
{
uiUpdates.Add($"Display: {e.NewValue.Username}");
});
Global.Subscribe<UserSession>(e => { uiUpdates.Add($"Display: {e.NewValue.Username}"); });
// 日志组件订阅
Global.Subscribe<UserSession>(e =>
{
logEntries.Add($"Session changed at {e.ChangedAt:HH:mm:ss}");
});
Global.Subscribe<UserSession>(e => { logEntries.Add($"Session changed at {e.ChangedAt:HH:mm:ss}"); });
// Act: 用户登录
Global.RegisterSingleton(new UserSession
@@ -620,7 +606,10 @@ public class GlobalTests
private class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine($"[LOG] {message}");
public void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
private class UserSession
@@ -640,4 +629,4 @@ public class GlobalTests
}
#endregion
}
}
+35 -35
View File
@@ -30,7 +30,7 @@ public class JsonHelperTests
var json = JsonHelper.ToJson(person);
// Assert
Assert.IsTrue(json.Contains("\"name\": \"John Doe\"")); // camelCase
Assert.IsTrue(json.Contains("\"name\": \"John Doe\"")); // camelCase
Assert.IsTrue(json.Contains("\"age\": 30"));
Assert.IsTrue(json.Contains("\"email\": \"john@example.com\""));
}
@@ -54,7 +54,7 @@ public class JsonHelperTests
// Assert: 属性名应该是 camelCase
Assert.IsTrue(json.Contains("\"appName\""));
Assert.IsTrue(json.Contains("\"maxConnections\""));
Assert.IsFalse(json.Contains("\"AppName\"")); // 不应该是 PascalCase
Assert.IsFalse(json.Contains("\"AppName\"")); // 不应该是 PascalCase
}
/// <summary>
@@ -85,12 +85,12 @@ public class JsonHelperTests
{
// Arrange
var json = """
{
"name": "Alice",
"age": 28,
"email": "alice@example.com"
}
""";
{
"name": "Alice",
"age": 28,
"email": "alice@example.com"
}
""";
// Act
var person = JsonHelper.FromJson<Person>(json);
@@ -110,11 +110,11 @@ public class JsonHelperTests
{
// Arrange: JSON 使用 camelCase
var json = """
{
"appName": "TestApp",
"maxConnections": 50
}
""";
{
"appName": "TestApp",
"maxConnections": 50
}
""";
// Act
var config = JsonHelper.FromJson<AppConfig>(json);
@@ -148,7 +148,7 @@ public class JsonHelperTests
// Assert: 枚举应该序列化为字符串(camelCase)
Assert.IsTrue(json.Contains("\"active\""));
Assert.IsTrue(json.Contains("\"high\""));
Assert.IsFalse(json.Contains("0")); // 不应该是数字
Assert.IsFalse(json.Contains("0")); // 不应该是数字
Assert.IsFalse(json.Contains("1"));
}
@@ -160,11 +160,11 @@ public class JsonHelperTests
{
// Arrange
var json = """
{
"status": "active",
"priority": "low"
}
""";
{
"status": "active",
"priority": "low"
}
""";
// Act
var data = JsonHelper.FromJson<DataWithEnum>(json);
@@ -204,11 +204,11 @@ public class JsonHelperTests
{
// Arrange
var json = """
{
"serverIp": "192.168.1.1",
"clientIp": "10.0.0.5"
}
""";
{
"serverIp": "192.168.1.1",
"clientIp": "10.0.0.5"
}
""";
// Act
var networkInfo = JsonHelper.FromJson<NetworkInfo>(json);
@@ -374,16 +374,16 @@ public class JsonHelperTests
// 模拟 API 调用后收到响应
var responseJson = """
{
"success": true,
"data": {
"name": "New User",
"age": 25,
"email": "newuser@example.com"
},
"message": "User created successfully"
}
""";
{
"success": true,
"data": {
"name": "New User",
"age": 25,
"email": "newuser@example.com"
},
"message": "User created successfully"
}
""";
var response = JsonHelper.FromJson<ApiResponse<Person>>(responseJson);
@@ -507,4 +507,4 @@ public class JsonHelperTests
}
#endregion
}
}
+5 -20
View File
@@ -186,19 +186,13 @@ public class ReflectionHelperTests
// Act: 使用 ReflectionHelper(有缓存)
var sw1 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
_ = ReflectionHelper.GetPropertyValue(user, nameof(User.Name));
}
for (var i = 0; i < iterations; i++) _ = ReflectionHelper.GetPropertyValue(user, nameof(User.Name));
sw1.Stop();
// Act: 使用原生反射(无缓存)
var propertyInfo = typeof(User).GetProperty(nameof(User.Name))!;
var sw2 = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
_ = propertyInfo.GetValue(user);
}
for (var i = 0; i < iterations; i++) _ = propertyInfo.GetValue(user);
sw2.Stop();
// Assert: ReflectionHelper 应该比直接反射快(由于缓存和表达式树)
@@ -377,7 +371,7 @@ public class ReflectionHelperTests
var target = new UserDto();
// Act
ReflectionHelper.CopyProperties(source, target, ignoreCase: true);
ReflectionHelper.CopyProperties(source, target, true);
// Assert
Assert.AreEqual("Alice", target.Name);
@@ -565,12 +559,8 @@ public class ReflectionHelperTests
// Act: 实例化并执行插件
foreach (var pluginType in plugins)
{
if (ReflectionHelper.CreateInstance(pluginType) is ITestService plugin)
{
plugin.Execute(); // 输出:Executing test service
}
}
}
[TestMethod]
@@ -610,9 +600,7 @@ public class ReflectionHelperTests
// Act: 动态更新属性
foreach (var (propertyName, value) in propertyUpdates)
{
ReflectionHelper.SetPropertyValue(user, propertyName, value);
}
// Assert
Assert.AreEqual("Updated Bob", user.Name);
@@ -631,10 +619,7 @@ public class ReflectionHelperTests
foreach (var property in properties)
{
var description = ReflectionHelper.GetAttribute<System.ComponentModel.DescriptionAttribute>(property);
if (description != null)
{
validationMessages[property.Name] = $"{description.Description}不能为空";
}
if (description != null) validationMessages[property.Name] = $"{description.Description}不能为空";
}
// Assert
@@ -671,4 +656,4 @@ public class ReflectionHelperTests
}
#endregion
}
}
+14 -26
View File
@@ -125,10 +125,7 @@ public class ResultTests
var executed = false;
// Act
var result = Result.Try(() =>
{
executed = true;
});
var result = Result.Try(() => { executed = true; });
// Assert
Assert.IsTrue(result.IsSuccess);
@@ -142,10 +139,7 @@ public class ResultTests
public void Try_FailingAction_ReturnsFailure()
{
// Act
var result = Result.Try(() =>
{
throw new InvalidOperationException("Operation failed");
});
var result = Result.Try(() => { throw new InvalidOperationException("Operation failed"); });
// Assert
Assert.IsTrue(result.IsFailure);
@@ -188,10 +182,7 @@ public class ResultTests
public async Task TryAsync_SuccessfulTask_ReturnsSuccess()
{
// Act
var result = await Result.TryAsync(async () =>
{
await Task.Delay(10);
});
var result = await Result.TryAsync(async () => { await Task.Delay(10); });
// Assert
Assert.IsTrue(result.IsSuccess);
@@ -310,10 +301,7 @@ public class ResultTests
var result = Result.Failure<int>(exception);
// Act & Assert
var thrown = Assert.ThrowsException<InvalidOperationException>(() =>
{
result.GetOrThrow();
});
var thrown = Assert.ThrowsException<InvalidOperationException>(() => { result.GetOrThrow(); });
Assert.AreSame(exception, thrown);
}
@@ -566,8 +554,8 @@ public class ResultTests
// Act
var message = result.Match(
onSuccess: value => $"Success: {value}",
onFailure: (ex, msg) => $"Error: {msg}"
value => $"Success: {value}",
(ex, msg) => $"Error: {msg}"
);
// Assert
@@ -585,8 +573,8 @@ public class ResultTests
// Act
var message = result.Match(
onSuccess: value => $"Success: {value}",
onFailure: (ex, msg) => $"Error: {msg}"
value => $"Success: {value}",
(ex, msg) => $"Error: {msg}"
);
// Assert
@@ -609,9 +597,9 @@ public class ResultTests
// Act: 使用 LINQ 查询语法
var combined = from x in result1
from y in result2
where x > 0 && y > 0
select x + y;
from y in result2
where x > 0 && y > 0
select x + y;
// Assert
Assert.IsTrue(combined.IsSuccess);
@@ -630,8 +618,8 @@ public class ResultTests
// Act
var combined = from x in result1
from y in result2
select x + y;
from y in result2
select x + y;
// Assert
Assert.IsTrue(combined.IsFailure);
@@ -877,4 +865,4 @@ public class ResultTests
}
#endregion
}
}
+16 -19
View File
@@ -69,17 +69,14 @@ public class StringExpandTests
var invalidUrls = new[]
{
"not a url",
"example.com", // 缺少协议
"http://", // 不完整
"", // 空字符串
"://invalid" // 无效格式
"example.com", // 缺少协议
"http://", // 不完整
"", // 空字符串
"://invalid" // 无效格式
};
// Act & Assert
foreach (var url in invalidUrls)
{
Assert.IsFalse(url.IsValidUrl(), $"'{url}' should be invalid");
}
foreach (var url in invalidUrls) Assert.IsFalse(url.IsValidUrl(), $"'{url}' should be invalid");
}
#endregion
@@ -281,22 +278,22 @@ public class StringExpandTests
{
["Port"] = "8080",
["Timeout"] = "30",
["MaxRetries"] = "invalid", // 无效值
["MaxRetries"] = "invalid", // 无效值
["Price"] = "99.99",
["Discount"] = "0.15"
};
// Act: 解析配置值
var port = config["Port"].ToInt(80); // 默认 80
var timeout = config["Timeout"].ToInt(60); // 默认 60
var maxRetries = config["MaxRetries"].ToInt(3); // 无效值,使用默认 3
var port = config["Port"].ToInt(80); // 默认 80
var timeout = config["Timeout"].ToInt(60); // 默认 60
var maxRetries = config["MaxRetries"].ToInt(3); // 无效值,使用默认 3
var price = config["Price"].ToDouble(0.0);
var discount = config["Discount"].ToDouble(0.0);
// Assert
Assert.AreEqual(8080, port);
Assert.AreEqual(30, timeout);
Assert.AreEqual(3, maxRetries); // 使用了默认值
Assert.AreEqual(3, maxRetries); // 使用了默认值
Assert.AreEqual(99.99, price);
Assert.AreEqual(0.15, discount);
}
@@ -332,8 +329,8 @@ public class StringExpandTests
public void Scenario_SafelyParseUserInput()
{
// Arrange: 模拟用户输入(可能无效)
string? userAgeInput = "25";
string? userWeightInput = "invalid";
var userAgeInput = "25";
var userWeightInput = "invalid";
string? userHeightInput = null;
// Act: 安全解析
@@ -344,8 +341,8 @@ public class StringExpandTests
// Assert
Assert.IsNotNull(age);
Assert.AreEqual(25, age.Value);
Assert.IsNull(weight); // 无效输入
Assert.IsNull(height); // null 输入
Assert.IsNull(weight); // 无效输入
Assert.IsNull(height); // null 输入
// 使用默认值的版本
var ageWithDefault = userAgeInput.ToInt(18);
@@ -353,7 +350,7 @@ public class StringExpandTests
var heightWithDefault = userHeightInput.ToDouble(170.0);
Assert.AreEqual(25, ageWithDefault);
Assert.AreEqual(70.0, weightWithDefault); // 使用默认值
Assert.AreEqual(70.0, weightWithDefault); // 使用默认值
Assert.AreEqual(170.0, heightWithDefault); // 使用默认值
}
@@ -389,4 +386,4 @@ public class StringExpandTests
}
#endregion
}
}
+6 -17
View File
@@ -132,7 +132,7 @@ public class SystemHelperTests
var allocatedPorts = new List<int>();
// Act: 为每个服务器分配端口
for (int i = 0; i < serverCount; i++)
for (var i = 0; i < serverCount; i++)
{
var port = SystemHelper.GetAvailablePort();
allocatedPorts.Add(port);
@@ -162,11 +162,9 @@ public class SystemHelperTests
void PrivilegedOperation()
{
if (!SystemHelper.IsAdministrator())
{
throw new UnauthorizedAccessException(
"This operation requires administrator privileges"
);
}
// 执行需要管理员权限的操作...
Console.WriteLine("Executing privileged operation");
@@ -182,10 +180,7 @@ public class SystemHelperTests
else
{
// 如果不是管理员,应该抛出异常
Assert.ThrowsException<UnauthorizedAccessException>(() =>
{
PrivilegedOperation();
});
Assert.ThrowsException<UnauthorizedAccessException>(() => { PrivilegedOperation(); });
}
}
@@ -202,7 +197,7 @@ public class SystemHelperTests
try
{
// Act: 启动多个测试服务器
for (int i = 0; i < 3; i++)
for (var i = 0; i < 3; i++)
{
var port = SystemHelper.GetAvailablePort();
var server = new TestServer($"TestServer{i + 1}", port);
@@ -223,10 +218,7 @@ public class SystemHelperTests
finally
{
// Cleanup: 停止所有测试服务器
foreach (var server in testServers)
{
server.Stop();
}
foreach (var server in testServers) server.Stop();
}
}
@@ -242,10 +234,7 @@ public class SystemHelperTests
var ports = new List<int>();
// Act: 收集端口样本
for (int i = 0; i < sampleSize; i++)
{
ports.Add(SystemHelper.GetAvailablePort());
}
for (var i = 0; i < sampleSize; i++) ports.Add(SystemHelper.GetAvailablePort());
// Assert: 验证所有端口都在合理范围内
// 动态端口范围通常是 49152-65535
@@ -333,4 +322,4 @@ public class SystemHelperTests
}
#endregion
}
}
+28 -55
View File
@@ -24,10 +24,7 @@ public class ValidationHelperTests
};
// Act & Assert
foreach (var email in validEmails)
{
Assert.IsTrue(ValidationHelper.IsValidEmail(email), $"Failed for: {email}");
}
foreach (var email in validEmails) Assert.IsTrue(ValidationHelper.IsValidEmail(email), $"Failed for: {email}");
}
[TestMethod]
@@ -46,9 +43,7 @@ public class ValidationHelperTests
// Act & Assert
foreach (var email in invalidEmails)
{
Assert.IsFalse(ValidationHelper.IsValidEmail(email), $"Should fail for: {email}");
}
}
[TestMethod]
@@ -97,9 +92,7 @@ public class ValidationHelperTests
// Act & Assert
foreach (var mobile in validMobiles)
{
Assert.IsTrue(ValidationHelper.IsValidChinaMobile(mobile), $"Failed for: {mobile}");
}
}
[TestMethod]
@@ -109,17 +102,15 @@ public class ValidationHelperTests
var invalidMobiles = new[]
{
"",
"1234567890", // 10 位
"123456789012", // 12 位
"12812345678", // 第二位是 2
"abcdefghijk" // 非数字
"1234567890", // 10 位
"123456789012", // 12 位
"12812345678", // 第二位是 2
"abcdefghijk" // 非数字
};
// Act & Assert
foreach (var mobile in invalidMobiles)
{
Assert.IsFalse(ValidationHelper.IsValidChinaMobile(mobile), $"Should fail for: {mobile}");
}
}
[TestMethod]
@@ -150,9 +141,7 @@ public class ValidationHelperTests
// Act & Assert
foreach (var mobile in validMobiles)
{
Assert.IsTrue(ValidationHelper.IsValidInternationalMobile(mobile), $"Failed for: {mobile}");
}
}
#endregion
@@ -165,8 +154,8 @@ public class ValidationHelperTests
// Arrange: 测试有效的身份证号(包含正确的校验码)
var validIdCards = new[]
{
"11010519491231002X", // 北京,1949年12月31日,校验码 X
"110105194912310021" // 北京,1949年12月31日,校验码 1
"11010519491231002X", // 北京,1949年12月31日,校验码 X
"110105194912310021" // 北京,1949年12月31日,校验码 1
};
// Act & Assert
@@ -184,9 +173,9 @@ public class ValidationHelperTests
var invalidIdCards = new[]
{
"",
"12345", // 太短
"123456789012345678", // 格式不对(日期无效)
"11010519491399002X" // 无效日期(99号)
"12345", // 太短
"123456789012345678", // 格式不对(日期无效)
"11010519491399002X" // 无效日期(99号)
};
// Act & Assert
@@ -217,8 +206,8 @@ public class ValidationHelperTests
public void ExtractGenderFromIdCard_ValidIdCard_ReturnsCorrectGender()
{
// Arrange
var maleIdCard = "110105199001150011"; // 倒数第二位是 1(奇数)
var femaleIdCard = "110105199001150024"; // 倒数第二位是 2(偶数)
var maleIdCard = "110105199001150011"; // 倒数第二位是 1(奇数)
var femaleIdCard = "110105199001150024"; // 倒数第二位是 2(偶数)
// Act
var maleGender = ValidationHelper.ExtractGenderFromIdCard(maleIdCard);
@@ -246,10 +235,7 @@ public class ValidationHelperTests
};
// Act & Assert
foreach (var url in validUrls)
{
Assert.IsTrue(ValidationHelper.IsValidUrl(url), $"Failed for: {url}");
}
foreach (var url in validUrls) Assert.IsTrue(ValidationHelper.IsValidUrl(url), $"Failed for: {url}");
}
[TestMethod]
@@ -260,16 +246,13 @@ public class ValidationHelperTests
{
"",
"not-a-url",
"ftp://example.com", // 不是 HTTP/HTTPS
"ftp://example.com", // 不是 HTTP/HTTPS
"//example.com",
"example.com" // 缺少协议
"example.com" // 缺少协议
};
// Act & Assert
foreach (var url in invalidUrls)
{
Assert.IsFalse(ValidationHelper.IsValidUrl(url), $"Should fail for: {url}");
}
foreach (var url in invalidUrls) Assert.IsFalse(ValidationHelper.IsValidUrl(url), $"Should fail for: {url}");
}
[TestMethod]
@@ -389,9 +372,7 @@ public class ValidationHelperTests
// Act & Assert
foreach (var password in strongPasswords)
{
Assert.IsTrue(ValidationHelper.IsStrongPassword(password), $"Failed for: {password}");
}
}
[TestMethod]
@@ -400,18 +381,16 @@ public class ValidationHelperTests
// Arrange
var weakPasswords = new[]
{
"12345678", // 只有数字
"abcdefgh", // 只有小写
"ABCDEFGH", // 只有大写
"Abc123", // 太短
"Abcdefgh", // 缺少数字和特殊字符
"12345678", // 只有数字
"abcdefgh", // 只有小写
"ABCDEFGH", // 只有大写
"Abc123", // 太短
"Abcdefgh" // 缺少数字和特殊字符
};
// Act & Assert
foreach (var password in weakPasswords)
{
Assert.IsFalse(ValidationHelper.IsStrongPassword(password), $"Should fail for: {password}");
}
}
[TestMethod]
@@ -447,10 +426,7 @@ public class ValidationHelperTests
};
// Act & Assert
foreach (var ip in validIPs)
{
Assert.IsTrue(ValidationHelper.IsValidIPv4(ip), $"Failed for: {ip}");
}
foreach (var ip in validIPs) Assert.IsTrue(ValidationHelper.IsValidIPv4(ip), $"Failed for: {ip}");
}
[TestMethod]
@@ -460,17 +436,14 @@ public class ValidationHelperTests
var invalidIPs = new[]
{
"",
"256.1.1.1", // 超出范围
"192.168.1", // 不完整
"192.168.1.1.1", // 太多段
"256.1.1.1", // 超出范围
"192.168.1", // 不完整
"192.168.1.1.1", // 太多段
"not.an.ip.address"
};
// Act & Assert
foreach (var ip in invalidIPs)
{
Assert.IsFalse(ValidationHelper.IsValidIPv4(ip), $"Should fail for: {ip}");
}
foreach (var ip in invalidIPs) Assert.IsFalse(ValidationHelper.IsValidIPv4(ip), $"Should fail for: {ip}");
}
#endregion
@@ -499,7 +472,7 @@ public class ValidationHelperTests
public void ValidateAll_OneFails_ReturnsFailure()
{
// Arrange
var email = "ab"; // 太短
var email = "ab"; // 太短
// Act
var result = ValidationHelper.ValidateAll(
@@ -621,4 +594,4 @@ public class ValidationHelperTests
}
#endregion
}
}
+2 -2
View File
@@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
<PackageReference Include="MSTest.TestFramework" Version="3.6.4" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.4"/>
</ItemGroup>
<ItemGroup>
@@ -18,7 +18,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Config\Config.csproj" />
<ProjectReference Include="..\..\src\Sharp\Config\Config.csproj"/>
</ItemGroup>
</Project>
+302 -300
View File
@@ -7,319 +7,321 @@ using System.Text;
// - 使用 MSTest。请确保已在测试项目中添加对 src\Sharp\Config\Config.csproj 的 ProjectReference。
// - 约定:配置项的“后添加覆盖先添加”(Configuration 的默认行为)。环境变量位于最末,优先级最高。
namespace Config.Test
namespace Config.Test;
// 为避免文件监听与并发写入导致的互相影响,禁用并行执行
[TestClass]
[DoNotParallelize]
public class ConfigTests
{
// 为避免文件监听与并发写入导致的互相影响,禁用并行执行
[TestClass]
[DoNotParallelize]
public class ConfigTests
// 简单的配置模型,涵盖:基础字段、嵌套对象、DataAnnotations 验证
public class AppSettings
{
// 简单的配置模型,涵盖:基础字段、嵌套对象、DataAnnotations 验证
public class AppSettings
public string? Name { get; set; }
public int Port { get; set; }
[Range(1, 65535)] public int ValidPort { get; set; }
[Required] public string? RequiredField { get; set; }
public DatabaseSettings Database { get; set; } = new();
}
public class DatabaseSettings
{
[Required] public string? Host { get; set; }
public string? User { get; set; }
public string? Password { get; set; }
}
// 创建临时目录的轻量封装,便于测试隔离
private sealed class TempDirScope : IDisposable
{
public string Dir { get; }
public TempDirScope()
{
public string? Name { get; set; }
public int Port { get; set; }
[Range(1, 65535)] public int ValidPort { get; set; }
[Required] public string? RequiredField { get; set; }
public DatabaseSettings Database { get; set; } = new();
Dir = Path.Combine(Path.GetTempPath(), "ConfigTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Dir);
}
public class DatabaseSettings
public string PathOf(string file)
{
[Required] public string? Host { get; set; }
public string? User { get; set; }
public string? Password { get; set; }
return Path.Combine(Dir, file);
}
// 创建临时目录的轻量封装,便于测试隔离
private sealed class TempDirScope : IDisposable
public void Dispose()
{
public string Dir { get; }
public TempDirScope()
{
Dir = Path.Combine(Path.GetTempPath(), "ConfigTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Dir);
}
public string PathOf(string file) => System.IO.Path.Combine(Dir, file);
public void Dispose()
{
try
{
Directory.Delete(Dir, recursive: true);
}
catch
{
/* 文件监视可能暂占用,忽略 */
}
}
}
[TestMethod]
public void Build_FromFiles_Binds_Json_Ini_Xml_Toml_WithValidation()
{
using var scope = new TempDirScope();
// 1. 构造多格式配置文件,分别写入不同字段
var jsonPath = scope.PathOf("appsettings.json");
var iniPath = scope.PathOf("settings.ini");
var xmlPath = scope.PathOf("extra.xml");
var tomlPath = scope.PathOf("feature.toml");
File.WriteAllText(jsonPath, /* UTF-8 无 BOM */
"{\n" +
" \"Name\": \"fromJson\",\n" +
" \"Port\": 10\n" +
"}", new UTF8Encoding(false));
File.WriteAllText(iniPath,
// INI 文件:根键与一层嵌套(Database 节)
"User=iniUser\n" +
"[Database]\n" +
"Host=iniHost\n",
new UTF8Encoding(false));
File.WriteAllText(xmlPath,
// XML:顶层键直接映射
"<settings>\n" +
" <ValidPort>100</ValidPort>\n" +
"</settings>", new UTF8Encoding(false));
File.WriteAllText(tomlPath,
"RequiredField = \"fromToml\"\n",
new UTF8Encoding(false));
// 2. 构建配置(顺序:JSON -> INI -> XML -> TOML),最后添加环境变量
var cm = ConfigBuilder.BuildFromFiles(
files: new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
reloadOnChange: true,
includeEnvironment: true,
environmentPrefix: "APP_",
basePath: scope.Dir
);
// 3. 强类型绑定 + 验证(DataAnnotations)
var settings = cm.Get<AppSettings>(validate: true);
Assert.AreEqual("fromJson", settings.Name);
Assert.AreEqual(10, settings.Port);
Assert.AreEqual(100, settings.ValidPort);
Assert.AreEqual("iniHost", settings.Database.Host);
Assert.AreEqual("iniUser", settings.Database.User);
Assert.AreEqual("fromToml", settings.RequiredField);
}
[TestMethod]
public void ConfigDefaults_Fluent_WithToml_AndOtherFormats_Works()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"default\", \"Port\": 20 }",
new UTF8Encoding(false));
File.WriteAllText(scope.PathOf("settings.ini"),
"[Database]\nHost=iniHost\nUser=iniUser\n",
new UTF8Encoding(false));
File.WriteAllText(scope.PathOf("extra.xml"),
"<settings><ValidPort>8080</ValidPort></settings>",
new UTF8Encoding(false));
File.WriteAllText(scope.PathOf("feature.toml"),
"RequiredField = \"fromToml\"\n",
new UTF8Encoding(false));
var settings = ConfigDefaults.Load<AppSettings>(defaults =>
{
defaults.BasePath = scope.Dir;
defaults.IncludeEnvironmentVariables = false;
defaults.WithIni("settings.ini")
.WithXml("extra.xml")
.WithToml("feature.toml");
});
Assert.AreEqual("default", settings.Name);
Assert.AreEqual(20, settings.Port);
Assert.AreEqual(8080, settings.ValidPort);
Assert.AreEqual("iniHost", settings.Database.Host);
Assert.AreEqual("iniUser", settings.Database.User);
Assert.AreEqual("fromToml", settings.RequiredField);
}
[TestMethod]
public void Validation_Fails_On_Missing_Required()
{
using var scope = new TempDirScope();
// 缺少 RequiredField 与 Database.Host,期望验证失败
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"x\", \"Port\": 5, \"ValidPort\": 200 }",
new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, basePath: scope.Dir);
Assert.ThrowsException<ConfigurationValidationException>(() => { cm.Get<AppSettings>(validate: true); });
}
[TestMethod]
public void EnvironmentVariables_Override_FileValues()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fromFile\", \"Database\": { \"Host\": \"fileHost\" } }",
new UTF8Encoding(false));
// 使用前缀 APP_,配置键用双下划线分隔层级
Environment.SetEnvironmentVariable("APP__Name", "fromEnv");
Environment.SetEnvironmentVariable("APP__Database__Host", "envHost");
try
{
var cm = ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json" },
basePath: scope.Dir,
includeEnvironment: true,
environmentPrefix: "APP_"
);
var settings = cm.Get<AppSettings>();
Assert.AreEqual("fromEnv", settings.Name);
Assert.AreEqual("envHost", settings.Database.Host);
Directory.Delete(Dir, true);
}
finally
catch
{
// 清理环境变量,避免影响其他测试
Environment.SetEnvironmentVariable("APP__Name", null);
Environment.SetEnvironmentVariable("APP__Database__Host", null);
/* 文件监视可能暂占用,忽略 */
}
}
[TestMethod]
[Timeout(15000)]
public async Task HotReload_FileChange_Triggers_Callback_And_Rebind()
{
using var scope = new TempDirScope();
var json = scope.PathOf("hot.json");
File.WriteAllText(json, "{ \"Port\": 1 }", new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(
new[] { "hot.json" },
reloadOnChange: true,
basePath: scope.Dir
);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using var sub = cm.OnReload(() => tcs.TrySetResult(true));
// 初始读取
var s1 = cm.Get<AppSettings>();
Assert.AreEqual(1, s1.Port);
// 修改内容,触发文件监控
await Task.Delay(250); // 稍等,确保监控挂上
File.WriteAllText(json, "{ \"Port\": 2 }", new UTF8Encoding(false));
// 等待回调触发
var fired = await Task.WhenAny(tcs.Task, Task.Delay(5000)) == tcs.Task;
Assert.IsTrue(fired, "未在期望时间内触发热更新回调");
// 变更后再取配置
var s2 = cm.Get<AppSettings>();
Assert.AreEqual(2, s2.Port);
}
[TestMethod]
public void Save_And_Load_Json_Xml_Ini_Toml()
{
using var scope = new TempDirScope();
var settings = new AppSettings
{
Name = "save",
Port = 1234,
ValidPort = 80,
RequiredField = "req",
Database = new DatabaseSettings { Host = "db", User = "u" }
};
var cm = ConfigManager.Build(new ConfigOptions()); // 用于调用 Save/Load
// JSON
var json = scope.PathOf("a.json");
cm.Save(settings, json, ConfigFormat.Json);
var jsonBack = cm.LoadFromFile<AppSettings>(json, ConfigFormat.Json);
Assert.AreEqual("save", jsonBack!.Name);
Assert.AreEqual(1234, jsonBack.Port);
Assert.AreEqual("db", jsonBack.Database.Host);
// XML
var xml = scope.PathOf("a.xml");
cm.Save(settings, xml, ConfigFormat.Xml);
var xmlBack = cm.LoadFromFile<AppSettings>(xml, ConfigFormat.Xml);
Assert.AreEqual(80, xmlBack!.ValidPort);
// INI(支持简单与一层嵌套的序列化回读)
var ini = scope.PathOf("a.ini");
cm.Save(settings, ini, ConfigFormat.Ini);
var iniBack = cm.LoadFromFile<AppSettings>(ini, ConfigFormat.Ini);
Assert.AreEqual("u", iniBack!.Database.User);
Assert.AreEqual("db", iniBack.Database.Host);
// TOML:当前不支持直接泛型反序列化(LoadFromFile),需通过配置管线加载
var toml = scope.PathOf("a.toml");
cm.Save(settings, toml, ConfigFormat.Toml);
Assert.ThrowsException<NotSupportedException>(() =>
{
_ = cm.LoadFromFile<AppSettings>(toml, ConfigFormat.Toml);
});
var cmToml = ConfigBuilder.BuildFromFiles(
new[] { "a.toml" },
basePath: scope.Dir,
includeEnvironment: false);
var tomlBack = cmToml.Get<AppSettings>();
Assert.AreEqual("save", tomlBack.Name);
Assert.AreEqual("db", tomlBack.Database.Host);
}
[TestMethod]
public void ConfigureBuilderHook_Appends_Providers_And_Overrides()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fileName\" }",
new UTF8Encoding(false));
var cm = ConfigBuilder.Build(opt =>
{
opt.BasePath = scope.Dir;
opt.Sources.Add(new ConfigSource
{
Path = "appsettings.json", Format = ConfigFormat.Json, Optional
= false
});
// 通过 Hook 追加内存配置,位于最后,覆盖前面的值
opt.ConfigureBuilderHook = builder =>
{
var kv = new[] { new KeyValuePair<string, string?>("Name", "hookName") };
builder.AddInMemoryCollection(kv);
return builder;
};
});
var s = cm.Get<AppSettings>();
Assert.AreEqual("hookName", s.Name, "Hook 未按预期覆盖前序配置源");
}
}
}
[TestMethod]
public void Build_FromFiles_Binds_Json_Ini_Xml_Toml_WithValidation()
{
using var scope = new TempDirScope();
// 1. 构造多格式配置文件,分别写入不同字段
var jsonPath = scope.PathOf("appsettings.json");
var iniPath = scope.PathOf("settings.ini");
var xmlPath = scope.PathOf("extra.xml");
var tomlPath = scope.PathOf("feature.toml");
File.WriteAllText(jsonPath, /* UTF-8 无 BOM */
"{\n" +
" \"Name\": \"fromJson\",\n" +
" \"Port\": 10\n" +
"}", new UTF8Encoding(false));
File.WriteAllText(iniPath,
// INI 文件:根键与一层嵌套(Database 节)
"User=iniUser\n" +
"[Database]\n" +
"Host=iniHost\n",
new UTF8Encoding(false));
File.WriteAllText(xmlPath,
// XML:顶层键直接映射
"<settings>\n" +
" <ValidPort>100</ValidPort>\n" +
"</settings>", new UTF8Encoding(false));
File.WriteAllText(tomlPath,
"RequiredField = \"fromToml\"\n",
new UTF8Encoding(false));
// 2. 构建配置(顺序:JSON -> INI -> XML -> TOML),最后添加环境变量
var cm = ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
true,
true,
"APP_",
scope.Dir
);
// 3. 强类型绑定 + 验证(DataAnnotations)
var settings = cm.Get<AppSettings>(validate: true);
Assert.AreEqual("fromJson", settings.Name);
Assert.AreEqual(10, settings.Port);
Assert.AreEqual(100, settings.ValidPort);
Assert.AreEqual("iniHost", settings.Database.Host);
Assert.AreEqual("iniUser", settings.Database.User);
Assert.AreEqual("fromToml", settings.RequiredField);
}
[TestMethod]
public void ConfigDefaults_Fluent_WithToml_AndOtherFormats_Works()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"default\", \"Port\": 20 }",
new UTF8Encoding(false));
File.WriteAllText(scope.PathOf("settings.ini"),
"[Database]\nHost=iniHost\nUser=iniUser\n",
new UTF8Encoding(false));
File.WriteAllText(scope.PathOf("extra.xml"),
"<settings><ValidPort>8080</ValidPort></settings>",
new UTF8Encoding(false));
File.WriteAllText(scope.PathOf("feature.toml"),
"RequiredField = \"fromToml\"\n",
new UTF8Encoding(false));
var settings = ConfigDefaults.Load<AppSettings>(defaults =>
{
defaults.BasePath = scope.Dir;
defaults.IncludeEnvironmentVariables = false;
defaults.WithIni("settings.ini")
.WithXml("extra.xml")
.WithToml("feature.toml");
});
Assert.AreEqual("default", settings.Name);
Assert.AreEqual(20, settings.Port);
Assert.AreEqual(8080, settings.ValidPort);
Assert.AreEqual("iniHost", settings.Database.Host);
Assert.AreEqual("iniUser", settings.Database.User);
Assert.AreEqual("fromToml", settings.RequiredField);
}
[TestMethod]
public void Validation_Fails_On_Missing_Required()
{
using var scope = new TempDirScope();
// 缺少 RequiredField 与 Database.Host,期望验证失败
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"x\", \"Port\": 5, \"ValidPort\": 200 }",
new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, basePath: scope.Dir);
Assert.ThrowsException<ConfigurationValidationException>(() => { cm.Get<AppSettings>(validate: true); });
}
[TestMethod]
public void EnvironmentVariables_Override_FileValues()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fromFile\", \"Database\": { \"Host\": \"fileHost\" } }",
new UTF8Encoding(false));
// 使用前缀 APP_,配置键用双下划线分隔层级
Environment.SetEnvironmentVariable("APP__Name", "fromEnv");
Environment.SetEnvironmentVariable("APP__Database__Host", "envHost");
try
{
var cm = ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json" },
basePath: scope.Dir,
includeEnvironment: true,
environmentPrefix: "APP_"
);
var settings = cm.Get<AppSettings>();
Assert.AreEqual("fromEnv", settings.Name);
Assert.AreEqual("envHost", settings.Database.Host);
}
finally
{
// 清理环境变量,避免影响其他测试
Environment.SetEnvironmentVariable("APP__Name", null);
Environment.SetEnvironmentVariable("APP__Database__Host", null);
}
}
[TestMethod]
[Timeout(15000)]
public async Task HotReload_FileChange_Triggers_Callback_And_Rebind()
{
using var scope = new TempDirScope();
var json = scope.PathOf("hot.json");
File.WriteAllText(json, "{ \"Port\": 1 }", new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(
new[] { "hot.json" },
true,
basePath: scope.Dir
);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using var sub = cm.OnReload(() => tcs.TrySetResult(true));
// 初始读取
var s1 = cm.Get<AppSettings>();
Assert.AreEqual(1, s1.Port);
// 修改内容,触发文件监控
await Task.Delay(250); // 稍等,确保监控挂上
File.WriteAllText(json, "{ \"Port\": 2 }", new UTF8Encoding(false));
// 等待回调触发
var fired = await Task.WhenAny(tcs.Task, Task.Delay(5000)) == tcs.Task;
Assert.IsTrue(fired, "未在期望时间内触发热更新回调");
// 变更后再取配置
var s2 = cm.Get<AppSettings>();
Assert.AreEqual(2, s2.Port);
}
[TestMethod]
public void Save_And_Load_Json_Xml_Ini_Toml()
{
using var scope = new TempDirScope();
var settings = new AppSettings
{
Name = "save",
Port = 1234,
ValidPort = 80,
RequiredField = "req",
Database = new DatabaseSettings { Host = "db", User = "u" }
};
var cm = ConfigManager.Build(new ConfigOptions()); // 用于调用 Save/Load
// JSON
var json = scope.PathOf("a.json");
cm.Save(settings, json, ConfigFormat.Json);
var jsonBack = cm.LoadFromFile<AppSettings>(json, ConfigFormat.Json);
Assert.AreEqual("save", jsonBack!.Name);
Assert.AreEqual(1234, jsonBack.Port);
Assert.AreEqual("db", jsonBack.Database.Host);
// XML
var xml = scope.PathOf("a.xml");
cm.Save(settings, xml, ConfigFormat.Xml);
var xmlBack = cm.LoadFromFile<AppSettings>(xml, ConfigFormat.Xml);
Assert.AreEqual(80, xmlBack!.ValidPort);
// INI(支持简单与一层嵌套的序列化回读)
var ini = scope.PathOf("a.ini");
cm.Save(settings, ini, ConfigFormat.Ini);
var iniBack = cm.LoadFromFile<AppSettings>(ini, ConfigFormat.Ini);
Assert.AreEqual("u", iniBack!.Database.User);
Assert.AreEqual("db", iniBack.Database.Host);
// TOML:当前不支持直接泛型反序列化(LoadFromFile),需通过配置管线加载
var toml = scope.PathOf("a.toml");
cm.Save(settings, toml, ConfigFormat.Toml);
Assert.ThrowsException<NotSupportedException>(() =>
{
_ = cm.LoadFromFile<AppSettings>(toml, ConfigFormat.Toml);
});
var cmToml = ConfigBuilder.BuildFromFiles(
new[] { "a.toml" },
basePath: scope.Dir,
includeEnvironment: false);
var tomlBack = cmToml.Get<AppSettings>();
Assert.AreEqual("save", tomlBack.Name);
Assert.AreEqual("db", tomlBack.Database.Host);
}
[TestMethod]
public void ConfigureBuilderHook_Appends_Providers_And_Overrides()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fileName\" }",
new UTF8Encoding(false));
var cm = ConfigBuilder.Build(opt =>
{
opt.BasePath = scope.Dir;
opt.Sources.Add(new ConfigSource
{
Path = "appsettings.json", Format = ConfigFormat.Json, Optional
= false
});
// 通过 Hook 追加内存配置,位于最后,覆盖前面的值
opt.ConfigureBuilderHook = builder =>
{
var kv = new[] { new KeyValuePair<string, string?>("Name", "hookName") };
builder.AddInMemoryCollection(kv);
return builder;
};
});
var s = cm.Get<AppSettings>();
Assert.AreEqual("hookName", s.Name, "Hook 未按预期覆盖前序配置源");
}
}
+5 -7
View File
@@ -53,9 +53,9 @@ public class _00_QuickStart
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"AuditDemo_{Guid.NewGuid()}")
.UseAudit(true) // 启用审计
.UseSoftDelete(true) // 启用软删除
.UseCurrentUser("admin"); // 设置当前用户
.UseAudit(true) // 启用审计
.UseSoftDelete(true) // 启用软删除
.UseCurrentUser("admin"); // 设置当前用户
using var db = builder.Build();
var repo = db.Repository<Product>();
@@ -96,15 +96,13 @@ public class _00_QuickStart
var repo = db.Repository<Product>();
// 添加测试数据
for (int i = 1; i <= 25; i++)
{
for (var i = 1; i <= 25; i++)
await repo.AddAsync(new Product
{
Name = $"产品{i}",
Price = i * 10m,
Stock = i
});
}
await db.SaveChangesAsync();
// 分页查询:第2页,每页10条
@@ -181,4 +179,4 @@ public class _00_QuickStart
Assert.AreEqual(1, count);
Console.WriteLine($"✓ 可以直接使用 EF Core,完全兼容!");
}
}
}
+9 -9
View File
@@ -69,12 +69,12 @@ public class _01_BasicUsageTests
// 链式配置所有功能
builder.UseInMemoryDatabase("FullConfigDemo")
.UseAudit(true) // 启用审计功能
.UseSoftDelete(true) // 启用软删除功能
.UseAutoMigration(true) // 启动时自动迁移数据库
.UseCurrentUser("admin") // 设置当前用户(用于审计)
.EnableDetailedErrors(true) // 启用详细错误信息
.EnableSensitiveDataLogging(true); // 启用敏感数据日志(仅开发环境)
.UseAudit(true) // 启用审计功能
.UseSoftDelete(true) // 启用软删除功能
.UseAutoMigration(true) // 启动时自动迁移数据库
.UseCurrentUser("admin") // 设置当前用户(用于审计)
.EnableDetailedErrors(true) // 启用详细错误信息
.EnableSensitiveDataLogging(true); // 启用敏感数据日志(仅开发环境)
using var db = builder.Build();
@@ -126,8 +126,8 @@ public class _01_BasicUsageTests
// 只启用审计,不启用软删除
builder.UseInMemoryDatabase("SelectiveDemo")
.UseAudit(true) // 启用审计
.UseSoftDelete(false); // 不启用软删除
.UseAudit(true) // 启用审计
.UseSoftDelete(false); // 不启用软删除
using var db = builder.Build();
@@ -269,4 +269,4 @@ public class _01_BasicUsageTests
builder.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}");
return builder.Build();
}
}
}
+16 -40
View File
@@ -89,10 +89,7 @@ public class _02_RepositoryCRUDTests
await _db.SaveChangesAsync();
Console.WriteLine($"✓ 成功添加 {products.Count} 个产品");
foreach (var product in products)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
foreach (var product in products) Console.WriteLine($" - {product.Name}: {product.Price:C}");
// 验证
var allProducts = await repo.GetAllAsync();
@@ -146,10 +143,7 @@ public class _02_RepositoryCRUDTests
Assert.AreEqual(3, allProducts.Count);
Console.WriteLine($"✓ 获取所有数据成功,共 {allProducts.Count} 条");
foreach (var p in allProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
foreach (var p in allProducts) Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
/// <summary>
@@ -174,10 +168,7 @@ public class _02_RepositoryCRUDTests
Assert.AreEqual(2, appleProducts.Count);
Console.WriteLine("✓ 条件查询成功,找到包含'苹果'的产品:");
foreach (var p in appleProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
foreach (var p in appleProducts) Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
/// <summary>
@@ -265,25 +256,23 @@ public class _02_RepositoryCRUDTests
await _db.SaveChangesAsync();
// 获取可查询对象,进行复杂查询
var query = repo.Query(trackChanges: false); // trackChanges: false 提升查询性能
var query = repo.Query(false); // trackChanges: false 提升查询性能
var result = await query
.Where(p => p.Price > 2000) // 价格大于2000
.OrderByDescending(p => p.Price) // 按价格降序
.Select(p => new // 投影到匿名类型
.Where(p => p.Price > 2000) // 价格大于2000
.OrderByDescending(p => p.Price) // 按价格降序
.Select(p => new // 投影到匿名类型
{
p.Name,
p.Price,
p.Stock,
TotalValue = p.Price * p.Stock // 计算总价值
TotalValue = p.Price * p.Stock // 计算总价值
})
.ToListAsync();
Console.WriteLine("✓ 复杂查询成功,价格>2000的产品(按价格降序):");
foreach (var item in result)
{
Console.WriteLine($" - {item.Name}: {item.Price:C} x {item.Stock} = {item.TotalValue:C}");
}
Assert.AreEqual(3, result.Count);
}
@@ -345,19 +334,13 @@ public class _02_RepositoryCRUDTests
await _db.SaveChangesAsync();
// 批量涨价 10%
foreach (var product in products)
{
product.Price *= 1.10m;
}
foreach (var product in products) product.Price *= 1.10m;
repo.UpdateRange(products);
await _db.SaveChangesAsync();
Console.WriteLine("✓ 批量涨价 10% 成功:");
var updatedProducts = await repo.GetAllAsync();
foreach (var p in updatedProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
foreach (var p in updatedProducts) Console.WriteLine($" - {p.Name}: {p.Price:C}");
// 验证
Assert.AreEqual(110m, updatedProducts[0].Price);
@@ -377,7 +360,7 @@ public class _02_RepositoryCRUDTests
[TestMethod]
public async Task Demo_DeleteEntity()
{
var repo = _db.Repository<Category>(); // Category 没有软删除,是真实删除
var repo = _db.Repository<Category>(); // Category 没有软删除,是真实删除
// 添加分类
var category = new Category { Name = "测试分类" };
@@ -483,10 +466,7 @@ public class _02_RepositoryCRUDTests
.ToListAsync();
Console.WriteLine("✓ 按价格升序:");
foreach (var p in ascProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
foreach (var p in ascProducts) Console.WriteLine($" - {p.Name}: {p.Price:C}");
// 按价格降序
var descProducts = await repo.Query()
@@ -494,10 +474,7 @@ public class _02_RepositoryCRUDTests
.ToListAsync();
Console.WriteLine("✓ 按价格降序:");
foreach (var p in descProducts)
{
Console.WriteLine($" - {p.Name}: {p.Price:C}");
}
foreach (var p in descProducts) Console.WriteLine($" - {p.Name}: {p.Price:C}");
Assert.AreEqual("A产品", ascProducts[0].Name);
Assert.AreEqual("C产品", descProducts[0].Name);
@@ -537,12 +514,11 @@ public class _02_RepositoryCRUDTests
Console.WriteLine("✓ 按价格区间分组统计:");
foreach (var group in priceGroups)
{
Console.WriteLine($" - {group.PriceRange}: {group.Count}个产品, 平均价格 {group.AvgPrice:C}, 总库存 {group.TotalStock}");
}
Console.WriteLine(
$" - {group.PriceRange}: {group.Count}个产品, 平均价格 {group.AvgPrice:C}, 总库存 {group.TotalStock}");
Assert.AreEqual(3, priceGroups.Count);
}
#endregion
}
}
+17 -17
View File
@@ -27,8 +27,8 @@ public class _03_AuditFeatureTests
// 创建启用审计功能的 DataContext
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"AuditCreateDemo_{Guid.NewGuid()}")
.UseAudit(true) // 启用审计
.UseCurrentUser("zhang.san"); // 设置当前用户
.UseAudit(true) // 启用审计
.UseCurrentUser("zhang.san"); // 设置当前用户
using var db = builder.Build();
var repo = db.Repository<Product>();
@@ -49,7 +49,7 @@ public class _03_AuditFeatureTests
Console.WriteLine($" CreatedBy: {product.CreatedBy ?? "(null)"}");
await repo.AddAsync(product);
await db.SaveChangesAsync(); // SaveChanges 时自动填充审计信息
await db.SaveChangesAsync(); // SaveChanges 时自动填充审计信息
var afterCreate = DateTime.UtcNow;
@@ -63,7 +63,7 @@ public class _03_AuditFeatureTests
// 验证
Assert.IsTrue(product.CreatedAt >= beforeCreate && product.CreatedAt <= afterCreate);
Assert.AreEqual("zhang.san", product.CreatedBy);
Assert.IsNull(product.UpdatedAt); // 创建时 UpdatedAt 为 null
Assert.IsNull(product.UpdatedAt); // 创建时 UpdatedAt 为 null
Assert.IsNull(product.UpdatedBy);
}
@@ -108,7 +108,7 @@ public class _03_AuditFeatureTests
product.Name = "更新后的产品";
product.Price = 200m;
repo.Update(product);
await db.SaveChangesAsync(); // SaveChanges 时自动填充 UpdatedAt 和 UpdatedBy
await db.SaveChangesAsync(); // SaveChanges 时自动填充 UpdatedAt 和 UpdatedBy
Console.WriteLine("\n更新后的审计信息:");
Console.WriteLine($" CreatedBy: {product.CreatedBy} (保持不变)");
@@ -117,11 +117,11 @@ public class _03_AuditFeatureTests
Console.WriteLine($" ✓ UpdatedAt: {product.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
// 验证
Assert.AreEqual(createdBy, product.CreatedBy); // 创建人不变
Assert.AreEqual(createdAt, product.CreatedAt); // 创建时间不变
Assert.AreEqual("editor", product.UpdatedBy); // 更新人是新用户
Assert.AreEqual(createdBy, product.CreatedBy); // 创建人不变
Assert.AreEqual(createdAt, product.CreatedAt); // 创建时间不变
Assert.AreEqual("editor", product.UpdatedBy); // 更新人是新用户
Assert.IsNotNull(product.UpdatedAt);
Assert.IsTrue(product.UpdatedAt > product.CreatedAt); // 更新时间晚于创建时间
Assert.IsTrue(product.UpdatedAt > product.CreatedAt); // 更新时间晚于创建时间
}
/// <summary>
@@ -167,9 +167,9 @@ public class _03_AuditFeatureTests
Console.WriteLine($"第2次更新: {product.UpdatedBy} 于 {product.UpdatedAt:HH:mm:ss}");
// 验证
Assert.AreEqual("user1", product.CreatedBy); // 创建人始终不变
Assert.AreEqual("user3", product.UpdatedBy); // 最后更新人
Assert.IsTrue(product.UpdatedAt > firstUpdateTime); // 更新时间在变化
Assert.AreEqual("user1", product.CreatedBy); // 创建人始终不变
Assert.AreEqual("user3", product.UpdatedBy); // 最后更新人
Assert.IsTrue(product.UpdatedAt > firstUpdateTime); // 更新时间在变化
}
/// <summary>
@@ -215,8 +215,8 @@ public class _03_AuditFeatureTests
await db.SaveChangesAsync();
Console.WriteLine($"✓ 更新人: {product.UpdatedBy}");
Assert.AreEqual("zhang.san", product.CreatedBy); // 创建人不变
Assert.AreEqual("li.si", product.UpdatedBy); // 更新人改变
Assert.AreEqual("zhang.san", product.CreatedBy); // 创建人不变
Assert.AreEqual("li.si", product.UpdatedBy); // 更新人改变
}
/// <summary>
@@ -263,7 +263,7 @@ public class _03_AuditFeatureTests
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"NoAuditDemo_{Guid.NewGuid()}")
.UseAudit(true) // 即使启用了审计功能
.UseAudit(true) // 即使启用了审计功能
.UseCurrentUser("admin");
using var db = builder.Build();
@@ -290,7 +290,7 @@ public class _03_AuditFeatureTests
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DisableAuditDemo_{Guid.NewGuid()}")
.UseAudit(false) // 禁用审计功能
.UseAudit(false) // 禁用审计功能
.UseCurrentUser("admin");
using var db = builder.Build();
@@ -368,4 +368,4 @@ public class _03_AuditFeatureTests
Console.WriteLine($" - 谁最后修改:{product.UpdatedBy}");
Console.WriteLine($" - 何时修改:{product.UpdatedAt:yyyy-MM-dd HH:mm:ss}");
}
}
}
+7 -18
View File
@@ -29,7 +29,7 @@ public class _04_SoftDeleteTests
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"SoftDeleteDemo_{Guid.NewGuid()}")
.UseAudit(true)
.UseSoftDelete(true) // 启用软删除
.UseSoftDelete(true) // 启用软删除
.UseCurrentUser("admin");
using var db = builder.Build();
@@ -71,7 +71,7 @@ public class _04_SoftDeleteTests
// 但数据仍在数据库中
var allIncludingDeleted = await db.Context.Set<Product>()
.IgnoreQueryFilters() // 忽略软删除过滤器
.IgnoreQueryFilters() // 忽略软删除过滤器
.Where(p => p.Id == productId)
.FirstOrDefaultAsync();
@@ -147,15 +147,13 @@ public class _04_SoftDeleteTests
var repo = db.Repository<Product>();
// 添加 5 个产品
for (int i = 1; i <= 5; i++)
{
for (var i = 1; i <= 5; i++)
await repo.AddAsync(new Product
{
Name = $"产品{i}",
Price = i * 100m,
Stock = i * 10
});
}
await db.SaveChangesAsync();
Console.WriteLine("✓ 创建了 5 个产品");
@@ -264,15 +262,13 @@ public class _04_SoftDeleteTests
// 添加多个产品
var products = new List<Product>();
for (int i = 1; i <= 10; i++)
{
for (var i = 1; i <= 10; i++)
products.Add(new Product
{
Name = $"产品{i}",
Price = i * 100m,
Stock = i * 10
});
}
await repo.AddRangeAsync(products);
await db.SaveChangesAsync();
@@ -290,10 +286,7 @@ public class _04_SoftDeleteTests
Console.WriteLine($"✓ 剩余可见产品: {remaining.Count} 个");
Assert.AreEqual(5, remaining.Count);
foreach (var p in expensiveProducts)
{
Assert.IsTrue(p.IsDeleted);
}
foreach (var p in expensiveProducts) Assert.IsTrue(p.IsDeleted);
}
/// <summary>
@@ -343,7 +336,7 @@ public class _04_SoftDeleteTests
{
var builder = new DataContextBuilder<TestDbContext>();
builder.UseInMemoryDatabase($"DisableSoftDelete_{Guid.NewGuid()}")
.UseSoftDelete(false) // 禁用软删除
.UseSoftDelete(false) // 禁用软删除
.UseCurrentUser("admin");
using var db = builder.Build();
@@ -371,12 +364,8 @@ public class _04_SoftDeleteTests
.FirstOrDefaultAsync(p => p.Id == productId);
if (found != null)
{
Console.WriteLine($" 结果:软删除 (IsDeleted={found.IsDeleted})");
}
else
{
Console.WriteLine($" 结果:物理删除");
}
}
}
}
+37 -47
View File
@@ -30,8 +30,7 @@ public class _05_PaginationTests
// 添加测试数据:100 个产品
var repo = _db.Repository<Product>();
var products = new List<Product>();
for (int i = 1; i <= 100; i++)
{
for (var i = 1; i <= 100; i++)
products.Add(new Product
{
Name = $"产品{i:D3}",
@@ -39,7 +38,6 @@ public class _05_PaginationTests
Stock = i * 5,
Description = i % 2 == 0 ? "热门产品" : "普通产品"
});
}
await repo.AddRangeAsync(products);
await _db.SaveChangesAsync();
@@ -62,8 +60,8 @@ public class _05_PaginationTests
// 查询第 1 页,每页 10 条
var page1 = await repo.GetPagedAsync(
page: 1,
pageSize: 10);
1,
10);
Console.WriteLine($"第 1 页:");
Console.WriteLine($" 当前页: {page1.PageIndex}");
@@ -83,10 +81,7 @@ public class _05_PaginationTests
Assert.IsTrue(page1.HasNextPage);
Console.WriteLine($"\n产品列表:");
foreach (var product in page1.Items)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
foreach (var product in page1.Items) Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
/// <summary>
@@ -96,8 +91,8 @@ public class _05_PaginationTests
public async Task Demo_IterateAllPages()
{
var repo = _db.Repository<Product>();
int pageSize = 15;
int currentPage = 1;
var pageSize = 15;
var currentPage = 1;
Console.WriteLine($"=== 遍历所有页(每页 {pageSize} 条)===\n");
@@ -107,7 +102,8 @@ public class _05_PaginationTests
pageResult = await repo.GetPagedAsync(currentPage, pageSize);
Console.WriteLine($"第 {currentPage} 页: {pageResult.Items.Count} 条记录");
Console.WriteLine($" 范围: {(currentPage - 1) * pageSize + 1} - {Math.Min(currentPage * pageSize, pageResult.TotalCount)}");
Console.WriteLine(
$" 范围: {(currentPage - 1) * pageSize + 1} - {Math.Min(currentPage * pageSize, pageResult.TotalCount)}");
// 处理当前页数据
foreach (var product in pageResult.Items)
@@ -116,11 +112,10 @@ public class _05_PaginationTests
}
currentPage++;
} while (pageResult.HasNextPage);
Console.WriteLine($"\n✓ 总共遍历了 {currentPage - 1} 页");
Assert.AreEqual(7, currentPage - 1); // 100 / 15 = 7 页
Assert.AreEqual(7, currentPage - 1); // 100 / 15 = 7 页
}
/// <summary>
@@ -133,9 +128,9 @@ public class _05_PaginationTests
// 查询价格 > 500 的产品,第 1 页,每页 10 条
var result = await repo.GetPagedAsync(
page: 1,
pageSize: 10,
predicate: p => p.Price > 500m);
1,
10,
p => p.Price > 500m);
Console.WriteLine($"查询条件: 价格 > 500");
Console.WriteLine($" 总记录数: {result.TotalCount}");
@@ -160,23 +155,18 @@ public class _05_PaginationTests
// 按价格降序排列
var result = await repo.GetPagedAsync(
page: 1,
pageSize: 10,
predicate: null,
orderBy: q => q.OrderByDescending(p => p.Price));
1,
10,
null,
q => q.OrderByDescending(p => p.Price));
Console.WriteLine($"排序: 按价格降序");
Console.WriteLine($"\n前 10 个最贵的产品:");
foreach (var product in result.Items)
{
Console.WriteLine($" - {product.Name}: {product.Price:C}");
}
foreach (var product in result.Items) Console.WriteLine($" - {product.Name}: {product.Price:C}");
// 验证排序
for (int i = 0; i < result.Items.Count - 1; i++)
{
for (var i = 0; i < result.Items.Count - 1; i++)
Assert.IsTrue(result.Items[i].Price >= result.Items[i + 1].Price);
}
}
/// <summary>
@@ -189,10 +179,10 @@ public class _05_PaginationTests
// 查询热门产品,价格 > 300,按库存降序,第 2 页,每页 5 条
var result = await repo.GetPagedAsync(
page: 2,
pageSize: 5,
predicate: p => p.Description == "热门产品" && p.Price > 300m,
orderBy: q => q.OrderByDescending(p => p.Stock));
2,
5,
p => p.Description == "热门产品" && p.Price > 300m,
q => q.OrderByDescending(p => p.Stock));
Console.WriteLine($"复杂查询:");
Console.WriteLine($" 条件: 热门产品 AND 价格>300");
@@ -234,8 +224,8 @@ public class _05_PaginationTests
Console.WriteLine($"有下一页: {result.HasNextPage}");
// 计算记录范围
int startIndex = (result.PageIndex - 1) * result.PageSize + 1;
int endIndex = Math.Min(result.PageIndex * result.PageSize, result.TotalCount);
var startIndex = (result.PageIndex - 1) * result.PageSize + 1;
var endIndex = Math.Min(result.PageIndex * result.PageSize, result.TotalCount);
Console.WriteLine($"\n记录范围: {startIndex} - {endIndex}");
// 验证
@@ -256,9 +246,9 @@ public class _05_PaginationTests
// 查询不存在的条件
var result = await repo.GetPagedAsync(
page: 1,
pageSize: 10,
predicate: p => p.Price > 99999m);
1,
10,
p => p.Price > 99999m);
Console.WriteLine($"查询不存在的记录:");
Console.WriteLine($" 总记录数: {result.TotalCount}");
@@ -306,9 +296,9 @@ public class _05_PaginationTests
var repo = _db.Repository<Product>();
// 模拟用户请求
int requestedPage = 3;
int pageSize = 20;
string? searchTerm = "产品0"; // 用户搜索
var requestedPage = 3;
var pageSize = 20;
var searchTerm = "产品0"; // 用户搜索
Console.WriteLine($"=== UI 分页场景 ===");
Console.WriteLine($"用户请求: 第 {requestedPage} 页, 每页 {pageSize} 条");
@@ -316,12 +306,12 @@ public class _05_PaginationTests
// 执行分页查询
var result = await repo.GetPagedAsync(
page: requestedPage,
pageSize: pageSize,
predicate: string.IsNullOrEmpty(searchTerm)
requestedPage,
pageSize,
string.IsNullOrEmpty(searchTerm)
? null
: p => p.Name.Contains(searchTerm),
orderBy: q => q.OrderBy(p => p.Name));
q => q.OrderBy(p => p.Name));
// 构建返回给前端的数据
var uiData = new
@@ -387,8 +377,8 @@ public class _05_PaginationTests
.OrderBy(p => p.Name);
// 手动实现分页逻辑
int page = 2;
int pageSize = 10;
var page = 2;
var pageSize = 10;
var totalCount = await query.CountAsync();
var items = await query
@@ -412,4 +402,4 @@ public class _05_PaginationTests
Console.WriteLine($"\n✓ 可以直接使用 IQueryable 和 PagedResult 进行自定义分页");
}
}
}
+5 -10
View File
@@ -118,10 +118,7 @@ public class _06_CustomEntityTests
// 列出所有地区
var allRegions = await regions.ToListAsync();
Console.WriteLine($"\n所有地区:");
foreach (var region in allRegions)
{
Console.WriteLine($" [{region.Code}] {region.Name}");
}
foreach (var region in allRegions) Console.WriteLine($" [{region.Code}] {region.Name}");
}
/// <summary>
@@ -155,7 +152,7 @@ public class _06_CustomEntityTests
foreach (var tag in tags)
{
Console.WriteLine($" ID: {tag.Id}, Name: {tag.Name}, Color: {tag.Color}");
Assert.IsTrue(tag.Id > 0); // 自增 Id
Assert.IsTrue(tag.Id > 0); // 自增 Id
}
// 查询
@@ -306,8 +303,7 @@ public class _06_CustomEntityTests
var repo = db.Repository<CustomProduct>();
// 添加测试数据
for (int i = 1; i <= 20; i++)
{
for (var i = 1; i <= 20; i++)
await repo.AddAsync(new CustomProduct
{
Id = Guid.NewGuid(),
@@ -315,7 +311,6 @@ public class _06_CustomEntityTests
Price = i * 100m,
CreatedDate = DateTime.UtcNow.AddDays(-i)
});
}
await db.SaveChangesAsync();
Console.WriteLine("=== 自定义实体的查询 ===\n");
@@ -326,7 +321,7 @@ public class _06_CustomEntityTests
// 2. 使用业务属性查询
var veryExpensive = await repo.Query()
.Where(p => p.IsExpensive) // 使用计算属性
.Where(p => p.IsExpensive) // 使用计算属性
.ToListAsync();
Console.WriteLine($"2. 使用 IsExpensive 属性: {veryExpensive.Count} 个");
@@ -382,4 +377,4 @@ public class _06_CustomEntityTests
Console.WriteLine(" 你可以完全自定义,仍然享受仓储模式的便利");
Console.WriteLine(" 灵活性和便利性可以兼得!");
}
}
}
+12 -19
View File
@@ -209,7 +209,7 @@ public class _07_EFCoreDirectUsageTests
var product3 = new Product { Name = "产品3", Price = 300m, Stock = 30 };
await productRepo.AddAsync(product2);
await productRepo.AddAsync(product3);
await db.SaveChangesAsync(); // 两个产品作为一个事务提交
await db.SaveChangesAsync(); // 两个产品作为一个事务提交
Console.WriteLine($" ✓ 隐式事务添加了 2 个产品");
@@ -233,15 +233,13 @@ public class _07_EFCoreDirectUsageTests
Console.WriteLine("1. 批量插入(推荐方式):");
var products = new List<Product>();
for (int i = 1; i <= 1000; i++)
{
for (var i = 1; i <= 1000; i++)
products.Add(new Product
{
Name = $"产品{i}",
Price = i * 10m,
Stock = i
});
}
var startTime = DateTime.UtcNow;
@@ -288,15 +286,13 @@ public class _07_EFCoreDirectUsageTests
var repo = db.Repository<Product>();
// 添加测试数据
for (int i = 1; i <= 100; i++)
{
for (var i = 1; i <= 100; i++)
await repo.AddAsync(new Product
{
Name = $"产品{i}",
Price = i * 10m,
Stock = i
});
}
await db.SaveChangesAsync();
Console.WriteLine("=== AsNoTracking 优化 ===\n");
@@ -310,14 +306,14 @@ public class _07_EFCoreDirectUsageTests
Console.WriteLine("\n2. AsNoTracking 查询(不跟踪):");
var untrackedProducts = await db.Context.Set<Product>()
.AsNoTracking() // 不跟踪实体变化
.AsNoTracking() // 不跟踪实体变化
.Take(10)
.ToListAsync();
Console.WriteLine($" ✓ 查询 {untrackedProducts.Count} 条,实体不被跟踪");
Console.WriteLine($" 适用于: 只读查询、数据展示");
Console.WriteLine("\n3. 仓储默认支持:");
var query = repo.Query(trackChanges: false); // 指定不跟踪
var query = repo.Query(false); // 指定不跟踪
var products = await query.Take(10).ToListAsync();
Console.WriteLine($" ✓ 仓储的 Query() 方法支持 trackChanges 参数");
@@ -354,21 +350,22 @@ public class _07_EFCoreDirectUsageTests
var entry = db.Context.Entry(product);
Console.WriteLine($"1. 新增状态:");
Console.WriteLine($" EntityState: {entry.State}"); // Added
Console.WriteLine($" EntityState: {entry.State}"); // Added
Assert.AreEqual(EntityState.Added, entry.State);
await db.SaveChangesAsync();
Console.WriteLine($"\n2. 保存后状态:");
Console.WriteLine($" EntityState: {entry.State}"); // Unchanged
Console.WriteLine($" EntityState: {entry.State}"); // Unchanged
Assert.AreEqual(EntityState.Unchanged, entry.State);
// 修改实体
product.Price = 200m;
Console.WriteLine($"\n3. 修改后状态:");
Console.WriteLine($" EntityState: {entry.State}"); // Modified
Console.WriteLine($" 修改的属性: {string.Join(", ", entry.Properties.Where(p => p.IsModified).Select(p => p.Metadata.Name))}");
Console.WriteLine($" EntityState: {entry.State}"); // Modified
Console.WriteLine(
$" 修改的属性: {string.Join(", ", entry.Properties.Where(p => p.IsModified).Select(p => p.Metadata.Name))}");
Assert.AreEqual(EntityState.Modified, entry.State);
// 查看原始值和当前值
@@ -396,8 +393,7 @@ public class _07_EFCoreDirectUsageTests
var repo = db.Repository<Product>();
// 添加测试数据
for (int i = 1; i <= 50; i++)
{
for (var i = 1; i <= 50; i++)
await repo.AddAsync(new Product
{
Name = $"产品{i}",
@@ -405,7 +401,6 @@ public class _07_EFCoreDirectUsageTests
Stock = i * 2,
Description = i % 2 == 0 ? "热门" : "普通"
});
}
await db.SaveChangesAsync();
Console.WriteLine("=== 高级查询技巧 ===\n");
@@ -439,9 +434,7 @@ public class _07_EFCoreDirectUsageTests
.ToListAsync();
foreach (var stat in groupStats)
{
Console.WriteLine($" {stat.Category}: {stat.Count}个, 均价 {stat.AvgPrice:C}, 最大库存 {stat.MaxStock}");
}
// 3. 子查询
Console.WriteLine("\n3. 子查询:");
@@ -493,4 +486,4 @@ public class _07_EFCoreDirectUsageTests
Console.WriteLine(" 两者可以自由组合使用,根据场景选择");
Console.WriteLine(" 底层完全共用同一个 DbContext,无缝协作");
}
}
}
+3 -3
View File
@@ -17,12 +17,12 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Data\Data.csproj" />
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj" />
<ProjectReference Include="..\..\src\Sharp\Data\Data.csproj"/>
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0"/>
</ItemGroup>
</Project>
+20 -20
View File
@@ -6,17 +6,17 @@
## 测试文件说明
| 文件 | 说明 | 主要演示内容 |
|------|------|------------|
| `TestEntities.cs` | 实体定义演示 | 6种不同的实体定义方式 |
| `TestDbContext.cs` | DbContext 配置演示 | 如何配置各种实体 |
| `01_BasicUsageTests.cs` | 基础使用 | 3行代码快速开始、各种配置选项 |
| `02_RepositoryCRUDTests.cs` | 仓储 CRUD | 增删改查、批量操作、复杂查询 |
| `03_AuditFeatureTests.cs` | 审计功能 | 自动记录创建/修改信息 |
| `04_SoftDeleteTests.cs` | 软删除功能 | 逻辑删除 vs 物理删除 |
| `05_PaginationTests.cs` | 分页查询 | 各种分页场景 |
| `06_CustomEntityTests.cs` | 自定义实体 | 不使用基类的灵活方式 |
| `07_EFCoreDirectUsageTests.cs` | EF Core 直接使用 | 原生 SQL、事务、高级查询 |
| 文件 | 说明 | 主要演示内容 |
|--------------------------------|----------------|-----------------|
| `TestEntities.cs` | 实体定义演示 | 6种不同的实体定义方式 |
| `TestDbContext.cs` | DbContext 配置演示 | 如何配置各种实体 |
| `01_BasicUsageTests.cs` | 基础使用 | 3行代码快速开始、各种配置选项 |
| `02_RepositoryCRUDTests.cs` | 仓储 CRUD | 增删改查、批量操作、复杂查询 |
| `03_AuditFeatureTests.cs` | 审计功能 | 自动记录创建/修改信息 |
| `04_SoftDeleteTests.cs` | 软删除功能 | 逻辑删除 vs 物理删除 |
| `05_PaginationTests.cs` | 分页查询 | 各种分页场景 |
| `06_CustomEntityTests.cs` | 自定义实体 | 不使用基类的灵活方式 |
| `07_EFCoreDirectUsageTests.cs` | EF Core 直接使用 | 原生 SQL、事务、高级查询 |
## 运行测试
@@ -165,19 +165,19 @@ using (var dbB = CreateDataContext("userB"))
## 最佳实践
1. **根据需求选择实体定义方式**
- 需要审计+软删除:使用 `AuditableEntity`
- 只需要基础 CRUD:使用 `Entity` 或完全自定义
- 已有实体模型:保持原样,直接使用
- 需要审计+软删除:使用 `AuditableEntity`
- 只需要基础 CRUD:使用 `Entity` 或完全自定义
- 已有实体模型:保持原样,直接使用
2. **性能优化**
- 只读查询使用 `AsNoTracking()`
- 批量操作使用 `AddRange/UpdateRange/RemoveRange`
- 避免在循环中调用 `SaveChangesAsync`
- 只读查询使用 `AsNoTracking()`
- 批量操作使用 `AddRange/UpdateRange/RemoveRange`
- 避免在循环中调用 `SaveChangesAsync`
3. **灵活使用**
- 简单场景用仓储
- 复杂查询直接用 EF Core
- 两者可以无缝混用
- 简单场景用仓储
- 复杂查询直接用 EF Core
- 两者可以无缝混用
## 问题反馈
+1 -1
View File
@@ -180,4 +180,4 @@ public class TestDbContext : DbContext
entity.HasIndex(e => e.Email);
});
}
}
}
+1 -1
View File
@@ -156,4 +156,4 @@ public class Address
public string Country { get; set; } = string.Empty;
}
#endregion
#endregion
+9 -9
View File
@@ -12,16 +12,16 @@
<PackageReference Include="MSTest" Version="3.6.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Log\Log.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Log\Log.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="8.0.10"/>
</ItemGroup>
</Project>
+4 -2
View File
@@ -82,7 +82,7 @@ public sealed class Test1
{
var temp = CreateTempDir();
var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder();
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
builder.Logging.ClearProviders();
builder.Logging.AddSharpLog(opt =>
@@ -130,8 +130,10 @@ public sealed class Test1
found = files[0];
break;
}
Thread.Sleep(50);
}
return found;
}
}
}
+10 -10
View File
@@ -7,17 +7,17 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="MSTest" Version="3.6.4"/>
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\NetClient\NetClient.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\NetClient\NetClient.csproj"/>
</ItemGroup>
</Project>
@@ -13,6 +13,8 @@ public sealed class StaticTokenProvider : IAccessTokenProvider
_token = token;
}
public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default) => Task.FromResult(_token);
}
public Task<string?> GetTokenAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(_token);
}
}
@@ -18,15 +18,21 @@ public sealed class TestHttpMessageHandler : HttpMessageHandler
_handler = handler;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
Interlocked.Increment(ref _calls);
return await _handler(request);
}
public static HttpResponseMessage Json(HttpStatusCode code, string json)
=> new(code) { Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") };
{
return new HttpResponseMessage(code)
{ Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") };
}
public static HttpResponseMessage Text(HttpStatusCode code, string text)
=> new(code) { Content = new StringContent(text) };
}
{
return new HttpResponseMessage(code) { Content = new StringContent(text) };
}
}
@@ -18,8 +18,12 @@ public sealed class TestJsonSerializer : IHttpContentSerializer
public string ContentType => "application/json";
public Task SerializeAsync<T>(Stream stream, T? value, CancellationToken cancellationToken = default)
=> JsonSerializer.SerializeAsync(stream, value, _options, cancellationToken);
{
return JsonSerializer.SerializeAsync(stream, value, _options, cancellationToken);
}
public Task<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default)
=> JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken).AsTask();
}
{
return JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken).AsTask();
}
}
@@ -15,23 +15,19 @@ public class NamedClientFactoryTests
{
var services = new ServiceCollection();
services.AddNamedNetClient("catalog", opt =>
{
opt.BaseAddress = new Uri("https://catalog.test");
}).ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
{
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://catalog.test");
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "catalog"));
}));
services.AddNamedNetClient("catalog", opt => { opt.BaseAddress = new Uri("https://catalog.test"); })
.ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
{
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://catalog.test");
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "catalog"));
}));
services.AddNamedNetClient("order", opt =>
{
opt.BaseAddress = new Uri("https://order.test");
}).ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
{
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://order.test");
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "order"));
}));
services.AddNamedNetClient("order", opt => { opt.BaseAddress = new Uri("https://order.test"); })
.ConfigurePrimaryHttpMessageHandler(() => new TestDoubles.TestHttpMessageHandler(req =>
{
StringAssert.StartsWith(req.RequestUri!.AbsoluteUri, "https://order.test");
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "order"));
}));
var sp = services.BuildServiceProvider();
var factory = sp.GetRequiredService<INetClientFactory>();
@@ -45,5 +41,4 @@ public class NamedClientFactoryTests
Assert.AreEqual("catalog", cRes);
Assert.AreEqual("order", oRes);
}
}
}
@@ -34,10 +34,12 @@ public class NetClientBasicTests
var handler = new TestHttpMessageHandler(req =>
{
Assert.AreEqual(HttpMethod.Get, req.Method);
Assert.AreEqual("https://example.com/api/echo", $"{req.RequestUri!.Scheme}://{req.RequestUri.Host}{req.RequestUri.AbsolutePath}");
Assert.AreEqual("https://example.com/api/echo",
$"{req.RequestUri!.Scheme}://{req.RequestUri.Host}{req.RequestUri.AbsolutePath}");
var q = req.RequestUri!.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
CollectionAssert.IsSubsetOf(new[] { "id=1", "name=foo" }, q);
var json = $"{{\"method\":\"{req.Method}\",\"path\":\"{req.RequestUri!.AbsolutePath}\",\"query\":\"{req.RequestUri!.Query.TrimStart('?')}\",\"name\":\"foo\"}}";
var json =
$"{{\"method\":\"{req.Method}\",\"path\":\"{req.RequestUri!.AbsolutePath}\",\"query\":\"{req.RequestUri!.Query.TrimStart('?')}\",\"name\":\"foo\"}}";
return Task.FromResult(TestHttpMessageHandler.Json(HttpStatusCode.OK, json));
});
@@ -77,7 +79,8 @@ public class NetClientBasicTests
[TestMethod]
public async Task Error_Throws_ApiException_With_Content()
{
var handler = new TestHttpMessageHandler(req => Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.InternalServerError, "oops")));
var handler = new TestHttpMessageHandler(req =>
Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.InternalServerError, "oops")));
var client = CreateClient(handler);
try
@@ -91,4 +94,4 @@ public class NetClientBasicTests
StringAssert.Contains(ex.ResponseContent ?? string.Empty, "oops");
}
}
}
}
@@ -50,5 +50,4 @@ public class NetClientDiTests
Assert.IsNotNull(capturedHandler);
Assert.AreEqual(1, capturedHandler!.Calls);
}
}
}
@@ -25,7 +25,8 @@ public class NetClientResultsTests
public async Task GetResultAsync_Success_Returns_Result_With_Value()
{
var json = "{\"id\":1}";
var handler = new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Json(HttpStatusCode.OK, json)));
var handler =
new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Json(HttpStatusCode.OK, json)));
var client = CreateClient(handler);
var res = await client.GetResultAsync<Obj>("/obj");
@@ -36,7 +37,8 @@ public class NetClientResultsTests
[TestMethod]
public async Task GetResultAsync_Failure_Maps_StatusCode_And_Message()
{
var handler = new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.BadRequest, "bad req")));
var handler = new TestHttpMessageHandler(_ =>
Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.BadRequest, "bad req")));
var client = CreateClient(handler);
var res = await client.GetResultAsync<Obj>("/obj");
@@ -48,7 +50,8 @@ public class NetClientResultsTests
[TestMethod]
public async Task DeleteResultAsync_Success_Returns_Success_Result()
{
var handler = new TestHttpMessageHandler(_ => Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.NoContent, "")));
var handler = new TestHttpMessageHandler(_ =>
Task.FromResult(TestHttpMessageHandler.Text(HttpStatusCode.NoContent, "")));
var client = CreateClient(handler);
var res = await client.DeleteResultAsync("/obj/1");
@@ -56,5 +59,4 @@ public class NetClientResultsTests
}
private sealed record Obj(int id);
}
}
@@ -30,9 +30,7 @@ public class NetClientRetryTests
{
attempts++;
if (attempts == 1)
{
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.TooManyRequests, "busy"));
}
return Task.FromResult(TestDoubles.TestHttpMessageHandler.Text(HttpStatusCode.OK, "ok"));
}));
@@ -44,5 +42,4 @@ public class NetClientRetryTests
Assert.AreEqual("ok", result);
Assert.AreEqual(2, attempts);
}
}
}
@@ -48,7 +48,7 @@ public class AuthController : BaseController
var token = GenerateSimpleToken(username);
// 设置 Cookie
SetCookie(context, "auth_token", token, expiresInSeconds: 3600);
SetCookie(context, "auth_token", token, 3600);
await WriteSuccessResponseAsync(context, new
{
@@ -156,7 +156,7 @@ public class AuthController : BaseController
}
var newToken = GenerateSimpleToken(username);
SetCookie(context, "auth_token", newToken, expiresInSeconds: 3600);
SetCookie(context, "auth_token", newToken, 3600);
await WriteSuccessResponseAsync(context, new
{
@@ -199,8 +199,8 @@ public class SimpleAuthenticationProvider : IAuthenticationProvider
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role)
new(ClaimTypes.Name, username),
new(ClaimTypes.Role, role)
};
var identity = new ClaimsIdentity(claims, "SimpleAuth");
@@ -216,4 +216,4 @@ public class SimpleAuthenticationProvider : IAuthenticationProvider
return Task.FromResult<ClaimsPrincipal?>(null);
}
}
}
@@ -16,7 +16,7 @@ public class ProductController : BaseController
new Product { Id = 1, Name = "笔记本电脑", Price = 5999.00m, Stock = 50, Category = "电子产品" },
new Product { Id = 2, Name = "无线鼠标", Price = 99.00m, Stock = 200, Category = "电子产品" },
new Product { Id = 3, Name = "机械键盘", Price = 399.00m, Stock = 100, Category = "电子产品" },
new Product { Id = 4, Name = "显示器", Price = 1299.00m, Stock = 30, Category = "电子产品" },
new Product { Id = 4, Name = "显示器", Price = 1299.00m, Stock = 30, Category = "电子产品" }
};
/// <summary>
@@ -109,14 +109,12 @@ public class ProductController : BaseController
var stockUpdate = await ReadJsonBodyAsync<Dictionary<string, object>>(context);
if (stockUpdate != null && stockUpdate.TryGetValue("quantity", out var quantityObj))
{
if (int.TryParse(quantityObj.ToString(), out var quantity))
{
product.Stock = quantity;
await WriteSuccessResponseAsync(context, product, "库存更新成功");
return;
}
}
await BadRequestAsync(context, "无效的库存数量");
}
@@ -190,4 +188,4 @@ public class Product
public decimal Price { get; set; }
public int Stock { get; set; }
public string Category { get; set; } = string.Empty;
}
}
@@ -168,4 +168,4 @@ public class User
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Role { get; set; } = "User";
}
}
@@ -47,7 +47,8 @@ public class ExampleProgram
// 配置服务 - 注册认证提供者
builder.ConfigureServices(services =>
{
services.AddSingleton<Sharp.NetServer.Abstractions.IAuthenticationProvider, SimpleAuthenticationProvider>();
services
.AddSingleton<Sharp.NetServer.Abstractions.IAuthenticationProvider, SimpleAuthenticationProvider>();
});
// 启用限流 - 防止滥用
@@ -225,4 +226,4 @@ public class ExampleProgram
Console.WriteLine(" - Token 有效期: 1小时");
Console.WriteLine(" - 某些接口有限流保护");
}
}
}
@@ -142,10 +142,7 @@ public class ServerIntegrationTests
var tasks = new List<Task<HttpResponseMessage>>();
// Act - 发送 10 个并发请求
for (int i = 0; i < 10; i++)
{
tasks.Add(client.GetAsync($"api/integration/concurrent?id={i}"));
}
for (var i = 0; i < 10; i++) tasks.Add(client.GetAsync($"api/integration/concurrent?id={i}"));
var responses = await Task.WhenAll(tasks);
@@ -201,4 +198,4 @@ internal class IntegrationTestController : BaseController
await Task.Delay(50); // 模拟一些处理时间
await WriteSuccessResponseAsync(context, new { id, processed = true });
}
}
}
+2 -2
View File
@@ -22,8 +22,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\NetServer\NetServer.csproj" />
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj" />
<ProjectReference Include="..\..\src\Sharp\NetServer\NetServer.csproj"/>
<ProjectReference Include="..\..\src\Sharp\Common\Common.csproj"/>
</ItemGroup>
</Project>
+56 -39
View File
@@ -3,12 +3,14 @@
## 🎉 项目完成状态
### ✅ 核心框架 (Sharp.NetServer)
- **状态**: 完全实现并编译成功
- **文件数**: 26 个 C# 源文件
- **代码行数**: 约 2,578 行
- **编译状态**: ✅ 0 错误,0 警告
### ✅ 测试项目 (NetServer.Test)
- **状态**: 完全实现并测试通过
- **文件数**: 8 个 C# 源文件
- **测试数量**: 16 个测试
@@ -16,6 +18,7 @@
- **测试时间**: 6.2 秒
### 📊 总体统计
- **总文件数**: 34 个 C# 文件
- **总代码量**: 约 3,500+ 行
- **测试覆盖**: 核心功能全覆盖
@@ -27,47 +30,48 @@
### 核心组件
1. **HTTP 服务器** (`Server/`)
- HttpServer.cs - 基于 HttpListener 的服务器实现
- HttpServerOptions.cs - 服务器配置选项
- HttpServer.cs - 基于 HttpListener 的服务器实现
- HttpServerOptions.cs - 服务器配置选项
2. **路由系统** (`Routing/`)
- RouteTable.cs - 路由注册和管理
- RouteInfo.cs - 路由元数据
- RouteTable.cs - 路由注册和管理
- RouteInfo.cs - 路由元数据
3. **中间件系统** (`Middleware/`)
- MiddlewarePipeline.cs - 中间件管道
- ExceptionHandlingMiddleware.cs - 全局异常处理
- LoggingMiddleware.cs - 请求日志
- CorsMiddleware.cs - CORS 处理
- AuthenticationMiddleware.cs - 身份认证
- AuthorizationMiddleware.cs - 授权检查
- RateLimitMiddleware.cs - 限流
- ResponseCacheMiddleware.cs - 响应缓存
- MiddlewarePipeline.cs - 中间件管道
- ExceptionHandlingMiddleware.cs - 全局异常处理
- LoggingMiddleware.cs - 请求日志
- CorsMiddleware.cs - CORS 处理
- AuthenticationMiddleware.cs - 身份认证
- AuthorizationMiddleware.cs - 授权检查
- RateLimitMiddleware.cs - 限流
- ResponseCacheMiddleware.cs - 响应缓存
4. **控制器** (`Controllers/`)
- BaseController.cs - 控制器基类(提供丰富的辅助方法)
- BaseController.cs - 控制器基类(提供丰富的辅助方法)
5. **特性系统** (`Attributes/`)
- RouteAttribute.cs - 路由特性
- HttpMethodAttributes.cs - HTTP 方法特性
- AuthorizeAttribute.cs - 授权特性
- RateLimitAttribute.cs - 限流特性
- CacheAttribute.cs - 缓存特性
- CorsAttribute.cs - CORS 特性
- RouteAttribute.cs - 路由特性
- HttpMethodAttributes.cs - HTTP 方法特性
- AuthorizeAttribute.cs - 授权特性
- RateLimitAttribute.cs - 限流特性
- CacheAttribute.cs - 缓存特性
- CorsAttribute.cs - CORS 特性
6. **模型** (`Models/`)
- HttpContext.cs - HTTP 上下文封装
- ResponseModel.cs - 标准响应模型
- HttpContext.cs - HTTP 上下文封装
- ResponseModel.cs - 标准响应模型
7. **抽象接口** (`Abstractions/`)
- IMiddleware.cs - 中间件接口
- IAuthenticationProvider.cs - 认证提供者接口
- IMiddleware.cs - 中间件接口
- IAuthenticationProvider.cs - 认证提供者接口
---
## ✨ 核心功能
### 1. 特性路由系统
```csharp
[ApiController]
[Route("api/users")]
@@ -81,6 +85,7 @@ public class UserController : BaseController
```
### 2. 中间件管道
- 全局异常处理
- 请求日志记录
- CORS 支持
@@ -90,6 +95,7 @@ public class UserController : BaseController
- 自定义中间件
### 3. 限流(Rate Limiting)
```csharp
[RateLimit(100, RateLimitScope.Ip)] // 每个 IP 每分钟 100 次
[RateLimit(50, RateLimitScope.User)] // 每个用户每分钟 50 次
@@ -97,12 +103,14 @@ public class UserController : BaseController
```
### 4. 响应缓存
```csharp
[ResponseCache(60)] // 缓存 60 秒
[ResponseCache(300, VaryByQueryKeys = true)] // 根据查询参数缓存
```
### 5. 认证授权
```csharp
[Authorize] // 需要认证
[Authorize(Roles = "Admin")] // 需要角色
@@ -111,12 +119,14 @@ public class UserController : BaseController
```
### 6. CORS
```csharp
[EnableCors(Origins = "http://localhost:3000", AllowCredentials = true)]
[DisableCors]
```
### 7. 控制器辅助方法
```csharp
// 读取请求
var query = GetQueryParam(context, "name");
@@ -140,19 +150,20 @@ var value = GetCookie(context, "name");
### 单元测试
1. **路由系统测试** (7 个测试)
- 路由注册和发现
- 路由匹配
- 路径参数支持
- 特性解析
- 路由注册和发现
- 路由匹配
- 路径参数支持
- 特性解析
2. **中间件测试** (3 个测试)
- 执行顺序
- 多中间件支持
- 短路机制
- 执行顺序
- 多中间件支持
- 短路机制
### 集成测试
**服务器集成测试** (6 个测试)
- HTTP 请求处理 (GET/POST)
- 查询参数解析
- CORS 支持
@@ -212,51 +223,57 @@ curl -X POST http://localhost:5000/api/users \
### 已创建的文档
1. **框架文档**
- [README.md](../../src/Sharp/NetServer/README.md) - 快速入门和 API 文档
- [ARCHITECTURE.md](../../src/Sharp/NetServer/ARCHITECTURE.md) - 架构设计
- [README.md](../../src/Sharp/NetServer/README.md) - 快速入门和 API 文档
- [ARCHITECTURE.md](../../src/Sharp/NetServer/ARCHITECTURE.md) - 架构设计
2. **测试文档**
- [README.md](../../test/NetServer.Test/README.md) - 测试和示例说明
- [SUMMARY.md](../../test/NetServer.Test/SUMMARY.md) - 项目完成总结
- [README.md](../../test/NetServer.Test/README.md) - 测试和示例说明
- [SUMMARY.md](../../test/NetServer.Test/SUMMARY.md) - 项目完成总结
3. **代码示例**
- DemoController.cs - 基础示例
- UserController.cs - 用户管理完整示例
- ProductController.cs - 产品管理完整示例
- AuthController.cs - 认证完整示例
- ExampleProgram.cs - 可运行的完整程序
- DemoController.cs - 基础示例
- UserController.cs - 用户管理完整示例
- ProductController.cs - 产品管理完整示例
- AuthController.cs - 认证完整示例
- ExampleProgram.cs - 可运行的完整程序
---
## 🎯 项目亮点
### 1. 简单易用
- 类似 Spring Boot 的开发体验
- 基于特性的声明式配置
- 丰富的辅助方法
### 2. 功能完整
- 路由、中间件、认证、授权
- 限流、缓存、CORS
- 日志、异常处理
### 3. 高性能
- 基于 HttpListener
- 异步 I/O
- 并发请求处理
- 响应缓存
### 4. 可扩展
- 自定义中间件
- 自定义认证提供者
- 依赖注入支持
### 5. 测试完善
- 16 个自动化测试
- 完整的集成测试
- 真实业务场景示例
### 6. 文档齐全
- 详细的 API 文档
- 架构设计文档
- 使用示例和教程
+20
View File
@@ -67,14 +67,18 @@ cd bin/Debug/net8.0
### 1. 单元测试 (UnitTests/)
#### RouteTableTests.cs
测试路由系统的核心功能:
- ✅ 路由注册和发现
- ✅ 路由匹配(包括路径参数)
- ✅ 特性解析(授权、限流、缓存等)
- ✅ 无效路由处理
#### MiddlewarePipelineTests.cs
测试中间件管道:
- ✅ 中间件执行顺序
- ✅ 多个中间件的链式调用
- ✅ 中间件短路机制
@@ -82,7 +86,9 @@ cd bin/Debug/net8.0
### 2. 集成测试 (IntegrationTests/)
#### ServerIntegrationTests.cs
测试完整的服务器功能:
- ✅ HTTP GET/POST 请求处理
- ✅ 查询参数解析
- ✅ 404 错误处理
@@ -96,6 +102,7 @@ cd bin/Debug/net8.0
### 1. 用户管理示例 (UserController.cs)
展示了完整的 CRUD 操作:
- `GET /api/users` - 获取所有用户(需要 Admin 角色)
- `GET /api/users/{id}` - 获取单个用户(带缓存)
- `POST /api/users` - 创建用户(需要 Admin 角色)
@@ -104,6 +111,7 @@ cd bin/Debug/net8.0
- `GET /api/users/search` - 搜索用户(允许匿名)
**特性展示**:
- 角色授权 (`[Authorize(Roles = "Admin")]`)
- 限流保护 (`[RateLimit]`)
- 响应缓存 (`[ResponseCache]`)
@@ -112,6 +120,7 @@ cd bin/Debug/net8.0
### 2. 产品管理示例 (ProductController.cs)
展示了产品管理功能:
- `GET /api/products` - 获取所有产品(公开接口,带缓存和限流)
- `GET /api/products/{id}` - 获取产品详情(带缓存)
- `POST /api/products` - 创建产品(需要 Admin)
@@ -120,6 +129,7 @@ cd bin/Debug/net8.0
- `GET /api/products/search` - 搜索产品(支持价格范围)
**特性展示**:
- 查询参数处理
- 部分更新 (PATCH)
- 复杂搜索条件
@@ -128,6 +138,7 @@ cd bin/Debug/net8.0
### 3. 认证示例 (AuthController.cs)
展示了完整的认证流程:
- `POST /api/auth/login` - 用户登录
- `POST /api/auth/logout` - 用户登出
- `GET /api/auth/me` - 获取当前用户信息
@@ -135,6 +146,7 @@ cd bin/Debug/net8.0
- `POST /api/auth/refresh` - 刷新 Token
**特性展示**:
- 简单的 Token 认证实现
- Cookie 操作
- 密码验证
@@ -143,6 +155,7 @@ cd bin/Debug/net8.0
### 4. 完整示例程序 (ExampleProgram.cs)
一个完整的可运行示例,包含:
- ✅ 服务器配置和启动
- ✅ CORS 配置
- ✅ 认证授权配置
@@ -173,6 +186,7 @@ curl -X POST http://localhost:5000/api/auth/login \
```
响应示例:
```json
{
"success": true,
@@ -239,25 +253,30 @@ curl -X PATCH http://localhost:5000/api/products/1/stock \
## 📊 功能特性展示
### 认证授权
- ✅ 基于 Token 的认证
- ✅ Cookie 支持
- ✅ 角色验证
- ✅ 权限验证
### 限流
- ✅ 基于 IP 的限流
- ✅ 基于用户的限流
- ✅ 防暴力破解
### 缓存
- ✅ 内存响应缓存
- ✅ 基于查询参数的缓存变化
### CORS
- ✅ 跨域资源共享
- ✅ 预检请求处理
### 日志
- ✅ 请求日志记录
- ✅ 错误日志记录
- ✅ 性能监控(慢请求警告)
@@ -278,6 +297,7 @@ curl -X PATCH http://localhost:5000/api/products/1/stock \
## 🚧 扩展建议
基于这些示例,你可以:
- 添加数据库支持(Entity Framework Core)
- 实现真正的 JWT 认证
- 添加 Swagger/OpenAPI 文档
+19 -10
View File
@@ -20,6 +20,7 @@
#### 单元测试 (10 个)
**路由系统测试** (RouteTableTests.cs):
- ✅ RegisterController_ShouldDiscoverRoutes - 路由注册和发现
- ✅ FindRoute_ShouldMatchCorrectRoute - 路由匹配
- ✅ FindRoute_WithParameter_ShouldMatch - 路径参数匹配
@@ -29,6 +30,7 @@
- ✅ RegisterController_ShouldCaptureRateLimit - 限流配置捕获
**中间件管道测试** (MiddlewarePipelineTests.cs):
- ✅ Pipeline_ShouldExecuteMiddlewaresInOrder - 中间件执行顺序
- ✅ Pipeline_ShouldSupportMultipleMiddlewares - 多个中间件支持
- ✅ Pipeline_CanShortCircuit - 中间件短路机制
@@ -36,6 +38,7 @@
#### 集成测试 (6 个)
**服务器集成测试** (ServerIntegrationTests.cs):
- ✅ Server_ShouldRespondToGetRequest - GET 请求处理
- ✅ Server_ShouldHandleQueryParameters - 查询参数解析
- ✅ Server_ShouldHandlePostRequest - POST 请求处理
@@ -71,6 +74,7 @@ NetServer.Test/
### 1. 用户管理 API (UserController.cs)
提供完整的 CRUD 操作:
- `GET /api/users` - 获取所有用户 (需要 Admin 角色)
- `GET /api/users/{id}` - 获取单个用户 (带 30 秒缓存)
- `POST /api/users` - 创建用户 (需要 Admin 角色)
@@ -79,6 +83,7 @@ NetServer.Test/
- `GET /api/users/search?keyword=xxx` - 搜索用户 (公开)
**特性展示**:
- ✅ 角色授权 (`[Authorize(Roles = "Admin")]`)
- ✅ 限流 (管理员接口 10次/分钟,普通接口 50次/分钟)
- ✅ 响应缓存
@@ -94,6 +99,7 @@ NetServer.Test/
- `GET /api/products/search?q=xxx&minPrice=100&maxPrice=500` - 搜索
**特性展示**:
- ✅ 查询参数过滤
- ✅ 部分更新 (PATCH)
- ✅ 复杂搜索条件
@@ -108,6 +114,7 @@ NetServer.Test/
- `POST /api/auth/refresh` - 刷新 Token
**特性展示**:
- ✅ 简单 Token 认证实现 (可替换为 JWT)
- ✅ Cookie 支持
- ✅ 防暴力破解限流 (登录接口 10次/分钟)
@@ -116,6 +123,7 @@ NetServer.Test/
### 4. 完整示例程序 (ExampleProgram.cs)
一个完整的可运行 HTTP 服务器,展示:
- ✅ 服务器配置和启动
- ✅ CORS 配置
- ✅ 认证授权配置
@@ -200,24 +208,25 @@ curl "http://localhost:5000/api/users/search?keyword=张"
基于这些测试和示例,可以:
1. **扩展功能**:
- 添加数据库集成 (Entity Framework Core)
- 实现真正的 JWT 认证
- 添加 Swagger/OpenAPI 文档
- 添加数据库集成 (Entity Framework Core)
- 实现真正的 JWT 认证
- 添加 Swagger/OpenAPI 文档
2. **增强测试**:
- 添加性能测试
- 添加安全测试
- 提高代码覆盖率
- 添加性能测试
- 添加安全测试
- 提高代码覆盖率
3. **生产部署**:
- 配置 HTTPS
- 添加健康检查端点
- 集成分布式缓存 (Redis)
- 添加监控和日志收集
- 配置 HTTPS
- 添加健康检查端点
- 集成分布式缓存 (Redis)
- 添加监控和日志收集
## 🎓 学习价值
这个测试项目可以帮助开发者:
- 理解如何使用 Sharp.NetServer 框架
- 学习 RESTful API 设计最佳实践
- 掌握中间件、认证、授权等概念
@@ -29,10 +29,7 @@ public class MiddlewarePipelineTests
executionOrder.Add("Second-After");
});
pipeline.Use(next => async context =>
{
executionOrder.Add("Third");
});
pipeline.Use(next => async context => { executionOrder.Add("Third"); });
// Act
await pipeline.ExecuteAsync(null!); // 简化测试,不需要真实 context
@@ -53,14 +50,12 @@ public class MiddlewarePipelineTests
var pipeline = new MiddlewarePipeline();
var counter = 0;
for (int i = 0; i < 5; i++)
{
for (var i = 0; i < 5; i++)
pipeline.Use(next => async context =>
{
counter++;
await next(context);
});
}
// Act
await pipeline.ExecuteAsync(null!);
@@ -94,4 +89,4 @@ public class MiddlewarePipelineTests
// Assert
Assert.IsFalse(reachedEnd, "Pipeline should be short-circuited");
}
}
}
@@ -152,4 +152,4 @@ internal class TestController : BaseController
{
await WriteSuccessResponseAsync(context, new { message = "Limited" });
}
}
}