U:调整所有代码格式
This commit is contained in:
1 parent
d133bacf55
commit
155b67e831
342 files changed
+9431
-9868
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 未按预期覆盖前序配置源");
|
||||
}
|
||||
}
|
||||
@@ -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,完全兼容!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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($" 结果:物理删除");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 进行自定义分页");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(" 灵活性和便利性可以兼得!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,无缝协作");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
- 两者可以无缝混用
|
||||
|
||||
## 问题反馈
|
||||
|
||||
|
||||
@@ -180,4 +180,4 @@ public class TestDbContext : DbContext
|
||||
entity.HasIndex(e => e.Email);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,4 +156,4 @@ public class Address
|
||||
public string Country { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 文档
|
||||
- 架构设计文档
|
||||
- 使用示例和教程
|
||||
|
||||
@@ -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 文档
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user