U:调整所有代码格式

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

No files matched your search

+2 -2
View File
@@ -15,8 +15,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+4 -13
View File
@@ -16,10 +16,7 @@ public static class CacheExtensions
Func<T> factory, Func<T> factory,
TimeSpan? expiration = null) TimeSpan? expiration = null)
{ {
if (cache.TryGet<T>(key, out var cached)) if (cache.TryGet<T>(key, out var cached)) return cached!;
{
return cached!;
}
var value = factory(); var value = factory();
cache.Set(key, value, expiration); cache.Set(key, value, expiration);
@@ -35,10 +32,7 @@ public static class CacheExtensions
Func<Task<T>> factory, Func<Task<T>> factory,
TimeSpan? expiration = null) TimeSpan? expiration = null)
{ {
if (cache.TryGet<T>(key, out var cached)) if (cache.TryGet<T>(key, out var cached)) return cached!;
{
return cached!;
}
var value = await factory(); var value = await factory();
cache.Set(key, value, expiration); cache.Set(key, value, expiration);
@@ -54,10 +48,7 @@ public static class CacheExtensions
Func<T> factory, Func<T> factory,
TimeSpan slidingExpiration) TimeSpan slidingExpiration)
{ {
if (cache.TryGet<T>(key, out var cached)) if (cache.TryGet<T>(key, out var cached)) return cached!;
{
return cached!;
}
var value = factory(); var value = factory();
cache.SetSliding(key, value, slidingExpiration); cache.SetSliding(key, value, slidingExpiration);
@@ -81,4 +72,4 @@ public static class CacheExtensions
var prefix = pattern.TrimEnd('*'); var prefix = pattern.TrimEnd('*');
cache.RemoveByPrefix(prefix); cache.RemoveByPrefix(prefix);
} }
} }
@@ -36,4 +36,4 @@ public static class CacheServiceExtensions
return services; return services;
} }
} }
+40 -49
View File
@@ -57,11 +57,11 @@ public interface ICacheService
/// <summary> /// <summary>
/// 基于 MemoryCache 的缓存服务实现 /// 基于 MemoryCache 的缓存服务实现
/// </summary> /// </summary>
public class MemoryCacheService : ICacheService public class MemoryCacheService : ICacheService
{ {
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly HashSet<string> _keys = new(); // 跟踪所有键(用于 Clear) private readonly HashSet<string> _keys = new(); // 跟踪所有键(用于 Clear)
private readonly object _lockObj = new(); private readonly object _lockObj = new();
public MemoryCacheService(IMemoryCache cache) public MemoryCacheService(IMemoryCache cache)
{ {
@@ -134,50 +134,41 @@ public interface ICacheService
} }
} }
public void Clear() public void Clear()
{
lock (_lockObj)
{ {
lock (_lockObj) foreach (var key in _keys.ToList()) _cache.Remove(key);
{ _keys.Clear();
foreach (var key in _keys.ToList())
{
_cache.Remove(key);
}
_keys.Clear();
}
}
public void RemoveByPrefix(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
{
return;
}
List<string> targets;
lock (_lockObj)
{
targets = _keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
}
foreach (var key in targets)
{
Remove(key);
}
}
public IReadOnlyCollection<string> Keys
{
get
{
lock (_lockObj)
{
return _keys.ToList().AsReadOnly();
}
}
}
public bool Exists(string key)
{
return _cache.TryGetValue(key, out _);
} }
} }
public void RemoveByPrefix(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix)) return;
List<string> targets;
lock (_lockObj)
{
targets = _keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
}
foreach (var key in targets) Remove(key);
}
public IReadOnlyCollection<string> Keys
{
get
{
lock (_lockObj)
{
return _keys.ToList().AsReadOnly();
}
}
}
public bool Exists(string key)
{
return _cache.TryGetValue(key, out _);
}
}
+29
View File
@@ -9,6 +9,7 @@
### 场景对比 ### 场景对比
#### ❌ 不使用 Cache - 重复查询 #### ❌ 不使用 Cache - 重复查询
```csharp ```csharp
public class ProductService { public class ProductService {
private readonly IRepository<Category> _categoryRepo; private readonly IRepository<Category> _categoryRepo;
@@ -29,6 +30,7 @@ public class ProductService {
``` ```
#### ✅ 使用 Cache - 智能缓存 #### ✅ 使用 Cache - 智能缓存
```csharp ```csharp
public class ProductService { public class ProductService {
private readonly IRepository<Category> _categoryRepo; private readonly IRepository<Category> _categoryRepo;
@@ -53,6 +55,7 @@ public class ProductService {
``` ```
**效果**: **效果**:
- 第一次:查询数据库(耗时 50ms) - 第一次:查询数据库(耗时 50ms)
- 后续访问:从内存读取(耗时 < 1ms) - 后续访问:从内存读取(耗时 < 1ms)
- 用户体验:界面秒开,不再等待 - 用户体验:界面秒开,不再等待
@@ -72,6 +75,7 @@ public class ProductService {
## 🚀 快速开始(未来) ## 🚀 快速开始(未来)
### 1. 注册服务 ### 1. 注册服务
```csharp ```csharp
// App.xaml.cs // App.xaml.cs
protected override void OnStartup(StartupEventArgs e) { protected override void OnStartup(StartupEventArgs e) {
@@ -85,6 +89,7 @@ protected override void OnStartup(StartupEventArgs e) {
``` ```
### 2. 注入使用 ### 2. 注入使用
```csharp ```csharp
public class WeatherService { public class WeatherService {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -111,6 +116,7 @@ public class WeatherService {
## 📚 核心 API 设计 ## 📚 核心 API 设计
### ICacheService 接口 ### ICacheService 接口
```csharp ```csharp
public interface ICacheService { public interface ICacheService {
/// <summary> /// <summary>
@@ -157,6 +163,7 @@ public interface ICacheService {
``` ```
### 扩展方法(最常用) ### 扩展方法(最常用)
```csharp ```csharp
public static class CacheExtensions { public static class CacheExtensions {
/// <summary> /// <summary>
@@ -200,6 +207,7 @@ public static class CacheExtensions {
## 💡 典型使用场景 ## 💡 典型使用场景
### 场景1:字典数据缓存(最常见) ### 场景1:字典数据缓存(最常见)
```csharp ```csharp
public class UserViewModel : ViewModelBase { public class UserViewModel : ViewModelBase {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -224,6 +232,7 @@ public class UserViewModel : ViewModelBase {
--- ---
### 场景2:API响应缓存 ### 场景2:API响应缓存
```csharp ```csharp
public class WeatherService { public class WeatherService {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -247,6 +256,7 @@ public class WeatherService {
--- ---
### 场景3:计算结果缓存 ### 场景3:计算结果缓存
```csharp ```csharp
public class ReportService { public class ReportService {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -277,6 +287,7 @@ public class ReportService {
--- ---
### 场景4:用户权限缓存 ### 场景4:用户权限缓存
```csharp ```csharp
public class AuthorizationService { public class AuthorizationService {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -300,6 +311,7 @@ public class AuthorizationService {
--- ---
### 场景5:配置数据缓存 ### 场景5:配置数据缓存
```csharp ```csharp
public class SystemConfigService { public class SystemConfigService {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -325,6 +337,7 @@ public class SystemConfigService {
## 🔧 高级功能 ## 🔧 高级功能
### 1. 缓存键约定 ### 1. 缓存键约定
```csharp ```csharp
// 推荐的缓存键命名规范 // 推荐的缓存键命名规范
public static class CacheKeys { public static class CacheKeys {
@@ -346,6 +359,7 @@ var user = await _cache.GetOrSetAsync(
--- ---
### 2. 缓存依赖(级联清除) ### 2. 缓存依赖(级联清除)
```csharp ```csharp
public class ProductService { public class ProductService {
// 当产品更新时,清除相关缓存 // 当产品更新时,清除相关缓存
@@ -367,6 +381,7 @@ public class ProductService {
--- ---
### 3. 缓存预热 ### 3. 缓存预热
```csharp ```csharp
public class CacheWarmupService { public class CacheWarmupService {
private readonly ICacheService _cache; private readonly ICacheService _cache;
@@ -406,6 +421,7 @@ protected override async void OnStartup(StartupEventArgs e) {
--- ---
### 4. 缓存统计(可选) ### 4. 缓存统计(可选)
```csharp ```csharp
public interface ICacheStatistics { public interface ICacheStatistics {
long TotalHits { get; } // 命中次数 long TotalHits { get; } // 命中次数
@@ -446,12 +462,14 @@ services.AddMemoryCache(options => {
## 🏗️ 实现方案 ## 🏗️ 实现方案
### 基于 Microsoft.Extensions.Caching.Memory ### 基于 Microsoft.Extensions.Caching.Memory
- **内置库**:无需额外依赖 - **内置库**:无需额外依赖
- **高性能**:基于 ConcurrentDictionary - **高性能**:基于 ConcurrentDictionary
- **线程安全**:完全线程安全 - **线程安全**:完全线程安全
- **自动清理**:后台定期清理过期项 - **自动清理**:后台定期清理过期项
### 架构设计 ### 架构设计
``` ```
ICacheService(接口) ICacheService(接口)
↓ ↓
@@ -474,6 +492,7 @@ Microsoft.Extensions.Caching.Memory.IMemoryCache
## 🔄 过期策略对比 ## 🔄 过期策略对比
### 绝对过期 (Absolute Expiration) ### 绝对过期 (Absolute Expiration)
```csharp ```csharp
// 从设置时开始,10分钟后过期(无论是否访问) // 从设置时开始,10分钟后过期(无论是否访问)
_cache.Set("key", value, TimeSpan.FromMinutes(10)); _cache.Set("key", value, TimeSpan.FromMinutes(10));
@@ -485,6 +504,7 @@ _cache.Set("key", value, TimeSpan.FromMinutes(10));
``` ```
### 滑动过期 (Sliding Expiration) ### 滑动过期 (Sliding Expiration)
```csharp ```csharp
// 最后访问后30分钟无访问则过期 // 最后访问后30分钟无访问则过期
_cache.SetSliding("key", value, TimeSpan.FromMinutes(30)); _cache.SetSliding("key", value, TimeSpan.FromMinutes(30));
@@ -496,6 +516,7 @@ _cache.SetSliding("key", value, TimeSpan.FromMinutes(30));
``` ```
### 组合使用 ### 组合使用
```csharp ```csharp
// 滑动30分钟,但最多缓存2小时 // 滑动30分钟,但最多缓存2小时
services.AddMemoryCache(); services.AddMemoryCache();
@@ -511,6 +532,7 @@ _cache.Set("key", value, new MemoryCacheEntryOptions {
## 💾 缓存大小管理 ## 💾 缓存大小管理
### 设置项大小 ### 设置项大小
```csharp ```csharp
_cache.Set("large-data", value, new MemoryCacheEntryOptions { _cache.Set("large-data", value, new MemoryCacheEntryOptions {
Size = 1, // 占用1个单位(Size单位由SizeLimit定义) Size = 1, // 占用1个单位(Size单位由SizeLimit定义)
@@ -519,6 +541,7 @@ _cache.Set("large-data", value, new MemoryCacheEntryOptions {
``` ```
### 优先级 ### 优先级
```csharp ```csharp
// 低优先级:内存不足时优先清除 // 低优先级:内存不足时优先清除
_cache.Set("low-priority", value, new MemoryCacheEntryOptions { _cache.Set("low-priority", value, new MemoryCacheEntryOptions {
@@ -541,6 +564,7 @@ _cache.Set("never-remove", value, new MemoryCacheEntryOptions {
## 📝 最佳实践 ## 📝 最佳实践
### 1. 缓存键命名规范 ### 1. 缓存键命名规范
```csharp ```csharp
✅ 好的命名 ✅ 好的命名
"user:profile:123" "user:profile:123"
@@ -554,6 +578,7 @@ _cache.Set("never-remove", value, new MemoryCacheEntryOptions {
``` ```
### 2. 合理设置过期时间 ### 2. 合理设置过期时间
```csharp ```csharp
// 根据数据变化频率设置 // 根据数据变化频率设置
字典数据:1小时 - 1天 字典数据:1小时 - 1天
@@ -563,6 +588,7 @@ API响应:5分钟 - 15分钟
``` ```
### 3. 避免缓存过大对象 ### 3. 避免缓存过大对象
```csharp ```csharp
❌ 不推荐:缓存整个DataTable ❌ 不推荐:缓存整个DataTable
_cache.Set("all-products", largeDataTable); _cache.Set("all-products", largeDataTable);
@@ -572,6 +598,7 @@ _cache.Set("all-products", productDtos);
``` ```
### 4. 及时清除失效缓存 ### 4. 及时清除失效缓存
```csharp ```csharp
public async Task UpdateUserAsync(User user) { public async Task UpdateUserAsync(User user) {
await _repository.UpdateAsync(user); await _repository.UpdateAsync(user);
@@ -582,6 +609,7 @@ public async Task UpdateUserAsync(User user) {
``` ```
### 5. 使用Result<T>模式 ### 5. 使用Result<T>模式
```csharp ```csharp
public async Task<Result<Product>> GetProductAsync(int id) { public async Task<Result<Product>> GetProductAsync(int id) {
return await _cache.GetOrSetAsync( return await _cache.GetOrSetAsync(
@@ -602,6 +630,7 @@ public async Task<Result<Product>> GetProductAsync(int id) {
## 🚀 未来扩展 ## 🚀 未来扩展
### 可能的扩展方向 ### 可能的扩展方向
1. **分布式缓存**(基于 Redis) 1. **分布式缓存**(基于 Redis)
2. **多级缓存**(L1: Memory, L2: Redis) 2. **多级缓存**(L1: Memory, L2: Redis)
3. **缓存预加载**(后台任务) 3. **缓存预加载**(后台任务)
+15 -15
View File
@@ -1,22 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- 程序集元数据 --> <!-- 程序集元数据 -->
<Description>Sharp Framework 核心库 - 提供 Result&lt;T&gt; 模式、Global 容器、系统配置等基础功能</Description> <Description>Sharp Framework 核心库 - 提供 Result&lt;T&gt; 模式、Global 容器、系统配置等基础功能</Description>
<!-- NuGet 包元数据 --> <!-- NuGet 包元数据 -->
<PackageTags>sharp;common;result;railway-oriented;functional;utilities</PackageTags> <PackageTags>sharp;common;result;railway-oriented;functional;utilities</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -15,11 +15,9 @@ public sealed class SystemConfig
[RegularExpression(@"^([0-9]{1,3}\.){3}[0-9]{1,3}$|^localhost$|^[a-zA-Z0-9_.-]+$")] [RegularExpression(@"^([0-9]{1,3}\.){3}[0-9]{1,3}$|^localhost$|^[a-zA-Z0-9_.-]+$")]
public string HTTPIp { get; set; } = "127.0.0.1"; public string HTTPIp { get; set; } = "127.0.0.1";
[Range(1, 65535)] [Range(1, 65535)] public int HttpPort { get; set; } = 8080;
public int HttpPort { get; set; } = 8080;
[Range(1, 600)] [Range(1, 600)] public int TimeoutSeconds { get; set; } = 30;
public int TimeoutSeconds { get; set; } = 30;
public string Scheme { get; set; } = "http"; public string Scheme { get; set; } = "http";
} }
@@ -21,20 +21,14 @@ public static class QuickStartExtensions
// 1. 配置管理(如果没传就用默认) // 1. 配置管理(如果没传就用默认)
// Note: ConfigBuilder.CreateDefault() 在 Config 模块中 // Note: ConfigBuilder.CreateDefault() 在 Config 模块中
// 这里假设用户已经传入 configuration 或在外部创建 // 这里假设用户已经传入 configuration 或在外部创建
if (configuration != null) if (configuration != null) services.AddSingleton(configuration);
{
services.AddSingleton(configuration);
}
// 2. 缓存服务(使用 Cache 模块的扩展方法) // 2. 缓存服务(使用 Cache 模块的扩展方法)
// Note: AddSharpCache() 在 Cache.DependencyInjection 中定义 // Note: AddSharpCache() 在 Cache.DependencyInjection 中定义
services.AddMemoryCache(); services.AddMemoryCache();
// 3. 全局服务定位器(兼容性) // 3. 全局服务定位器(兼容性)
if (configuration != null) if (configuration != null) RegisterGlobalServices(services, configuration);
{
RegisterGlobalServices(services, configuration);
}
return services; return services;
} }
@@ -51,4 +45,4 @@ public static class QuickStartExtensions
Global.RegisterSingleton(configuration); Global.RegisterSingleton(configuration);
Global.RegisterSingleton(provider); Global.RegisterSingleton(provider);
} }
} }
@@ -33,6 +33,4 @@ public class ExceptionExpand : Exception
{ {
return $"Error Code: {ErrorCode}\nMessage: {Message}\nSuggestion: {Suggestion}\n{base.ToString()}"; return $"Error Code: {ErrorCode}\nMessage: {Message}\nSuggestion: {Suggestion}\n{base.ToString()}";
} }
} }
+11 -38
View File
@@ -171,26 +171,17 @@ public static class CollectionExpand
var nodes = list.Select(item => new TreeNode<T>(item, idSelector(item), parentIdSelector(item))).ToList(); var nodes = list.Select(item => new TreeNode<T>(item, idSelector(item), parentIdSelector(item))).ToList();
var lookup = nodes.ToLookup(n => n.ParentId); var lookup = nodes.ToLookup(n => n.ParentId);
foreach (var node in nodes) foreach (var node in nodes) node.Children = lookup[node.Id].ToList();
{
node.Children = lookup[node.Id].ToList();
}
// 构建层级深度 // 构建层级深度
void SetLevel(TreeNode<T> node, int level) void SetLevel(TreeNode<T> node, int level)
{ {
node.Level = level; node.Level = level;
foreach (var child in node.Children) foreach (var child in node.Children) SetLevel(child, level + 1);
{
SetLevel(child, level + 1);
}
} }
var rootNodes = nodes.Where(n => Equals(n.ParentId, rootParentId)).ToList(); var rootNodes = nodes.Where(n => Equals(n.ParentId, rootParentId)).ToList();
foreach (var root in rootNodes) foreach (var root in rootNodes) SetLevel(root, 0);
{
SetLevel(root, 0);
}
return rootNodes; return rootNodes;
} }
@@ -210,10 +201,7 @@ public static class CollectionExpand
foreach (var node in nodes) foreach (var node in nodes)
{ {
result.Add(node.Data); result.Add(node.Data);
if (node.Children.Any()) if (node.Children.Any()) Traverse(node.Children);
{
Traverse(node.Children);
}
} }
} }
@@ -231,10 +219,7 @@ public static class CollectionExpand
foreach (var node in nodes) foreach (var node in nodes)
{ {
action(node); action(node);
if (node.Children.Any()) if (node.Children.Any()) Traverse(node.Children);
{
Traverse(node.Children);
}
} }
} }
@@ -270,10 +255,7 @@ public static class CollectionExpand
} }
} }
if (batch.Count > 0) if (batch.Count > 0) yield return batch;
{
yield return batch;
}
} }
/// <summary> /// <summary>
@@ -281,10 +263,7 @@ public static class CollectionExpand
/// </summary> /// </summary>
public static void ForEachBatch<T>(this IEnumerable<T> source, int batchSize, Action<IEnumerable<T>> action) public static void ForEachBatch<T>(this IEnumerable<T> source, int batchSize, Action<IEnumerable<T>> action)
{ {
foreach (var batch in source.Batch(batchSize)) foreach (var batch in source.Batch(batchSize)) action(batch);
{
action(batch);
}
} }
#endregion #endregion
@@ -333,10 +312,7 @@ public static class CollectionExpand
/// </summary> /// </summary>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action) public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{ {
foreach (var item in source) foreach (var item in source) action(item);
{
action(item);
}
} }
/// <summary> /// <summary>
@@ -344,11 +320,8 @@ public static class CollectionExpand
/// </summary> /// </summary>
public static void ForEach<T>(this IEnumerable<T> source, Action<T, int> action) public static void ForEach<T>(this IEnumerable<T> source, Action<T, int> action)
{ {
int index = 0; var index = 0;
foreach (var item in source) foreach (var item in source) action(item, index++);
{
action(item, index++);
}
} }
/// <summary> /// <summary>
@@ -475,4 +448,4 @@ public static class CollectionExpand
} }
#endregion #endregion
} }
+13 -22
View File
@@ -238,7 +238,7 @@ public static class DateTimeExpand
/// </summary> /// </summary>
public static DateTime StartOfWeek(this DateTime dt, DayOfWeek firstDayOfWeek = DayOfWeek.Monday) public static DateTime StartOfWeek(this DateTime dt, DayOfWeek firstDayOfWeek = DayOfWeek.Monday)
{ {
int diff = (7 + (dt.DayOfWeek - firstDayOfWeek)) % 7; var diff = (7 + (dt.DayOfWeek - firstDayOfWeek)) % 7;
return dt.AddDays(-diff).Date; return dt.AddDays(-diff).Date;
} }
@@ -255,17 +255,14 @@ public static class DateTimeExpand
/// </summary> /// </summary>
public static DateTime AddWorkDays(this DateTime dt, int workDays) public static DateTime AddWorkDays(this DateTime dt, int workDays)
{ {
int direction = workDays < 0 ? -1 : 1; var direction = workDays < 0 ? -1 : 1;
int daysToAdd = Math.Abs(workDays); var daysToAdd = Math.Abs(workDays);
DateTime result = dt; var result = dt;
while (daysToAdd > 0) while (daysToAdd > 0)
{ {
result = result.AddDays(direction); result = result.AddDays(direction);
if (result.DayOfWeek != DayOfWeek.Saturday && result.DayOfWeek != DayOfWeek.Sunday) if (result.DayOfWeek != DayOfWeek.Saturday && result.DayOfWeek != DayOfWeek.Sunday) daysToAdd--;
{
daysToAdd--;
}
} }
return result; return result;
@@ -279,14 +276,10 @@ public static class DateTimeExpand
if (startDate > endDate) if (startDate > endDate)
(startDate, endDate) = (endDate, startDate); (startDate, endDate) = (endDate, startDate);
int workDays = 0; var workDays = 0;
for (DateTime date = startDate; date <= endDate; date = date.AddDays(1)) for (var date = startDate; date <= endDate; date = date.AddDays(1))
{
if (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday) if (date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday)
{
workDays++; workDays++;
}
}
return workDays; return workDays;
} }
@@ -297,12 +290,10 @@ public static class DateTimeExpand
public static int CalculateAge(this DateTime birthDate, DateTime? referenceDate = null) public static int CalculateAge(this DateTime birthDate, DateTime? referenceDate = null)
{ {
var reference = referenceDate ?? DateTime.Today; var reference = referenceDate ?? DateTime.Today;
int age = reference.Year - birthDate.Year; var age = reference.Year - birthDate.Year;
if (reference.Month < birthDate.Month || (reference.Month == birthDate.Month && reference.Day < birthDate.Day)) if (reference.Month < birthDate.Month ||
{ (reference.Month == birthDate.Month && reference.Day < birthDate.Day)) age--;
age--;
}
return age; return age;
} }
@@ -320,8 +311,8 @@ public static class DateTimeExpand
/// </summary> /// </summary>
public static DateTime StartOfQuarter(this DateTime dt) public static DateTime StartOfQuarter(this DateTime dt)
{ {
int quarter = dt.GetQuarter(); var quarter = dt.GetQuarter();
int month = (quarter - 1) * 3 + 1; var month = (quarter - 1) * 3 + 1;
return new DateTime(dt.Year, month, 1, 0, 0, 0, dt.Kind); return new DateTime(dt.Year, month, 1, 0, 0, 0, dt.Kind);
} }
@@ -527,4 +518,4 @@ public static class DateTimeExpand
} }
#endregion #endregion
} }
+13 -24
View File
@@ -277,10 +277,7 @@ public static class StringExpand
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString)) if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString))
return str ?? string.Empty; return str ?? string.Empty;
while (str.StartsWith(trimString)) while (str.StartsWith(trimString)) str = str.Substring(trimString.Length);
{
str = str.Substring(trimString.Length);
}
return str; return str;
} }
@@ -293,10 +290,7 @@ public static class StringExpand
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString)) if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(trimString))
return str ?? string.Empty; return str ?? string.Empty;
while (str.EndsWith(trimString)) while (str.EndsWith(trimString)) str = str.Substring(0, str.Length - trimString.Length);
{
str = str.Substring(0, str.Length - trimString.Length);
}
return str; return str;
} }
@@ -344,7 +338,7 @@ public static class StringExpand
var result = new StringBuilder(); var result = new StringBuilder();
result.Append(words[0].ToLowerInvariant()); result.Append(words[0].ToLowerInvariant());
for (int i = 1; i < words.Length; i++) for (var i = 1; i < words.Length; i++)
{ {
result.Append(char.ToUpperInvariant(words[i][0])); result.Append(char.ToUpperInvariant(words[i][0]));
if (words[i].Length > 1) if (words[i].Length > 1)
@@ -620,10 +614,7 @@ public static class StringExpand
return string.Empty; return string.Empty;
var sb = new StringBuilder(str.Length * count); var sb = new StringBuilder(str.Length * count);
for (int i = 0; i < count; i++) for (var i = 0; i < count; i++) sb.Append(str);
{
sb.Append(str);
}
return sb.ToString(); return sb.ToString();
} }
@@ -686,23 +677,21 @@ public static class StringExpand
if (string.IsNullOrEmpty(target)) if (string.IsNullOrEmpty(target))
return source.Length; return source.Length;
int[,] distance = new int[source.Length + 1, target.Length + 1]; var distance = new int[source.Length + 1, target.Length + 1];
for (int i = 0; i <= source.Length; i++) for (var i = 0; i <= source.Length; i++)
distance[i, 0] = i; distance[i, 0] = i;
for (int j = 0; j <= target.Length; j++) for (var j = 0; j <= target.Length; j++)
distance[0, j] = j; distance[0, j] = j;
for (int i = 1; i <= source.Length; i++) for (var i = 1; i <= source.Length; i++)
for (var j = 1; j <= target.Length; j++)
{ {
for (int j = 1; j <= target.Length; j++) var cost = target[j - 1] == source[i - 1] ? 0 : 1;
{ distance[i, j] = Math.Min(
int cost = (target[j - 1] == source[i - 1]) ? 0 : 1; Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1),
distance[i, j] = Math.Min( distance[i - 1, j - 1] + cost);
Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1),
distance[i - 1, j - 1] + cost);
}
} }
return distance[source.Length, target.Length]; return distance[source.Length, target.Length];
+3 -20
View File
@@ -121,10 +121,7 @@ public static class Global
_services[typeof(T)] = descriptor; _services[typeof(T)] = descriptor;
// 如果是更新且需要通知,触发变更事件 // 如果是更新且需要通知,触发变更事件
if (isUpdate && notifyIfExists) if (isUpdate && notifyIfExists) NotifyChanged(oldValue, instance);
{
NotifyChanged(oldValue, instance);
}
} }
/// <summary> /// <summary>
@@ -237,15 +234,10 @@ public static class Global
{ {
// 双重检查锁定模式,保证单例只创建一次 // 双重检查锁定模式,保证单例只创建一次
if (descriptor.SingletonInstance == null) if (descriptor.SingletonInstance == null)
{
lock (descriptor.Lock) lock (descriptor.Lock)
{ {
if (descriptor.SingletonInstance == null) if (descriptor.SingletonInstance == null) descriptor.SingletonInstance = descriptor.Factory();
{
descriptor.SingletonInstance = descriptor.Factory();
}
} }
}
return descriptor.SingletonInstance; return descriptor.SingletonInstance;
} }
@@ -326,10 +318,7 @@ public static class Global
lock (lockObj) lock (lockObj)
{ {
if (_subscribers.TryGetValue(type, out var subscribers)) if (_subscribers.TryGetValue(type, out var subscribers)) subscribers.Remove(handler);
{
subscribers.Remove(handler);
}
} }
} }
@@ -344,20 +333,15 @@ public static class Global
// 获取订阅者列表的快照,避免在通知过程中锁定 // 获取订阅者列表的快照,避免在通知过程中锁定
List<Delegate> subscribersCopy; List<Delegate> subscribersCopy;
if (_subscriberLocks.TryGetValue(type, out var lockObj)) if (_subscriberLocks.TryGetValue(type, out var lockObj))
{
lock (lockObj) lock (lockObj)
{ {
subscribersCopy = new List<Delegate>(subscribers); subscribersCopy = new List<Delegate>(subscribers);
} }
}
else else
{
subscribersCopy = new List<Delegate>(subscribers); subscribersCopy = new List<Delegate>(subscribers);
}
// 通知所有订阅者 // 通知所有订阅者
foreach (var subscriber in subscribersCopy) foreach (var subscriber in subscribersCopy)
{
try try
{ {
(subscriber as Action<ServiceChangedEventArgs<T>>)?.Invoke(eventArgs); (subscriber as Action<ServiceChangedEventArgs<T>>)?.Invoke(eventArgs);
@@ -367,7 +351,6 @@ public static class Global
// 忽略订阅者抛出的异常,避免影响其他订阅者 // 忽略订阅者抛出的异常,避免影响其他订阅者
// 在实际应用中,可以记录日志 // 在实际应用中,可以记录日志
} }
}
} }
#endregion #endregion
+16 -15
View File
@@ -749,33 +749,33 @@ dotnet test
### DI 容器 ### DI 容器
1. **生命周期选择** 1. **生命周期选择**
- 使用 **Singleton** 用于:配置对象、数据库连接池、缓存服务 - 使用 **Singleton** 用于:配置对象、数据库连接池、缓存服务
- 使用 **Transient** 用于:请求上下文、操作上下文 - 使用 **Transient** 用于:请求上下文、操作上下文
2. **变更通知** 2. **变更通知**
- 订阅者应该是轻量级的,避免长时间阻塞 - 订阅者应该是轻量级的,避免长时间阻塞
- 订阅者中不要抛出异常(已被容器捕获和忽略) - 订阅者中不要抛出异常(已被容器捕获和忽略)
3. **线程安全** 3. **线程安全**
- 容器本身是线程安全的 - 容器本身是线程安全的
- 单例服务的初始化是线程安全的(双重检查锁定) - 单例服务的初始化是线程安全的(双重检查锁定)
### Result 模式 ### Result 模式
1. **何时使用 Result** 1. **何时使用 Result**
- 可预期的业务错误(验证失败、资源不存在) - 可预期的业务错误(验证失败、资源不存在)
- 外部 API 调用 - 外部 API 调用
- 需要传递错误码的场景 - 需要传递错误码的场景
2. **何时使用异常** 2. **何时使用异常**
- 不可预期的程序错误(NullReferenceException、OutOfMemoryException) - 不可预期的程序错误(NullReferenceException、OutOfMemoryException)
- 系统级错误 - 系统级错误
3. **链式操作** 3. **链式操作**
- 使用 `Map` 转换成功值 - 使用 `Map` 转换成功值
- 使用 `Bind` 链接返回 Result 的操作 - 使用 `Bind` 链接返回 Result 的操作
- 使用 `Ensure` 进行验证 - 使用 `Ensure` 进行验证
- 失败会自动传播,不需要手动检查 - 失败会自动传播,不需要手动检查
## 项目结构 ## 项目结构
@@ -812,6 +812,7 @@ Sharp/Common/
## 版本历史 ## 版本历史
### v1.0.0 ### v1.0.0
- ✅ 轻量级 DI 容器(支持生命周期和变更通知) - ✅ 轻量级 DI 容器(支持生命周期和变更通知)
- ✅ Result 模式(完整的函数式错误处理) - ✅ Result 模式(完整的函数式错误处理)
- ✅ 自定义异常(带错误码) - ✅ 自定义异常(带错误码)
+70 -38
View File
@@ -14,10 +14,10 @@ public abstract class Result
{ {
if (isSuccess && exception is not null) if (isSuccess && exception is not null)
throw new ArgumentException("Success result cannot have an exception", nameof(exception)); throw new ArgumentException("Success result cannot have an exception", nameof(exception));
if (isSuccess && errorCode.HasValue) if (isSuccess && errorCode.HasValue)
throw new ArgumentException("Success result cannot have an error code", nameof(errorCode)); throw new ArgumentException("Success result cannot have an error code", nameof(errorCode));
if (!isSuccess && exception is null && string.IsNullOrEmpty(errorMessage) && !errorCode.HasValue) if (!isSuccess && exception is null && string.IsNullOrEmpty(errorMessage) && !errorCode.HasValue)
throw new ArgumentException("Failed result must have either an exception, error message, or error code"); throw new ArgumentException("Failed result must have either an exception, error message, or error code");
@@ -25,33 +25,60 @@ public abstract class Result
Exception = exception; Exception = exception;
ErrorMessage = errorMessage ?? exception?.Message; ErrorMessage = errorMessage ?? exception?.Message;
ErrorCode = errorCode; ErrorCode = errorCode;
// If we have an ExceptionExpand, extract the error code from it // If we have an ExceptionExpand, extract the error code from it
if (exception is ExceptionExpand exceptionExpand && !errorCode.HasValue) if (exception is ExceptionExpand exceptionExpand && !errorCode.HasValue) ErrorCode = exceptionExpand.ErrorCode;
{
ErrorCode = exceptionExpand.ErrorCode;
}
} }
public static Result Success() => new SimpleResult(true); public static Result Success()
{
public static Result<T> Success<T>(T value) => new Result<T>(value); return new SimpleResult(true);
}
public static Result Failure(string errorMessage) => new SimpleResult(false, null, errorMessage);
public static Result<T> Success<T>(T value)
public static Result Failure(Exception exception) => new SimpleResult(false, exception); {
return new Result<T>(value);
public static Result Failure(long errorCode, string errorMessage) => new SimpleResult(false, null, errorMessage, errorCode); }
public static Result Failure(long errorCode, string errorMessage, Exception exception) => new SimpleResult(false, exception, errorMessage, errorCode); public static Result Failure(string errorMessage)
{
public static Result<T> Failure<T>(string errorMessage) => new Result<T>(default, false, null, errorMessage); return new SimpleResult(false, null, errorMessage);
}
public static Result<T> Failure<T>(Exception exception) => new Result<T>(default, false, exception);
public static Result Failure(Exception exception)
public static Result<T> Failure<T>(long errorCode, string errorMessage) => new Result<T>(default, false, null, errorMessage, errorCode); {
return new SimpleResult(false, exception);
public static Result<T> Failure<T>(long errorCode, string errorMessage, Exception exception) => new Result<T>(default, false, exception, errorMessage, errorCode); }
public static Result Failure(long errorCode, string errorMessage)
{
return new SimpleResult(false, null, errorMessage, errorCode);
}
public static Result Failure(long errorCode, string errorMessage, Exception exception)
{
return new SimpleResult(false, exception, errorMessage, errorCode);
}
public static Result<T> Failure<T>(string errorMessage)
{
return new Result<T>(default, false, null, errorMessage);
}
public static Result<T> Failure<T>(Exception exception)
{
return new Result<T>(default, false, exception);
}
public static Result<T> Failure<T>(long errorCode, string errorMessage)
{
return new Result<T>(default, false, null, errorMessage, errorCode);
}
public static Result<T> Failure<T>(long errorCode, string errorMessage, Exception exception)
{
return new Result<T>(default, false, exception, errorMessage, errorCode);
}
public static Result Try(Action action) public static Result Try(Action action)
{ {
@@ -128,17 +155,18 @@ public abstract class Result
public Result<T> Match<T>(Func<T> onSuccess, Func<Exception?, string?, long?, T> onFailure) public Result<T> Match<T>(Func<T> onSuccess, Func<Exception?, string?, long?, T> onFailure)
{ {
return IsSuccess ? Result.Success(onSuccess()) : Result.Failure<T>(Exception ?? new Exception(ErrorMessage)); return IsSuccess ? Success(onSuccess()) : Failure<T>(Exception ?? new Exception(ErrorMessage));
} }
public Result<T> Match<T>(Func<T> onSuccess, Func<Exception?, string?, T> onFailure) public Result<T> Match<T>(Func<T> onSuccess, Func<Exception?, string?, T> onFailure)
{ {
return IsSuccess ? Result.Success(onSuccess()) : Result.Failure<T>(Exception ?? new Exception(ErrorMessage)); return IsSuccess ? Success(onSuccess()) : Failure<T>(Exception ?? new Exception(ErrorMessage));
} }
private class SimpleResult : Result private class SimpleResult : Result
{ {
public SimpleResult(bool isSuccess, Exception? exception = null, string? errorMessage = null, long? errorCode = null) public SimpleResult(bool isSuccess, Exception? exception = null, string? errorMessage = null,
long? errorCode = null)
: base(isSuccess, exception, errorMessage, errorCode) : base(isSuccess, exception, errorMessage, errorCode)
{ {
} }
@@ -149,31 +177,35 @@ public class Result<T> : Result
{ {
public T? Value { get; } public T? Value { get; }
public Result(T? value, bool isSuccess = true, Exception? exception = null, string? errorMessage = null, long? errorCode = null) public Result(T? value, bool isSuccess = true, Exception? exception = null, string? errorMessage = null,
long? errorCode = null)
: base(isSuccess, exception, errorMessage, errorCode) : base(isSuccess, exception, errorMessage, errorCode)
{ {
Value = value; Value = value;
} }
public static implicit operator Result<T>(T value) => new Result<T>(value); public static implicit operator Result<T>(T value)
{
return new Result<T>(value);
}
public Result<TNew> Map<TNew>(Func<T, TNew> mapper) public Result<TNew> Map<TNew>(Func<T, TNew> mapper)
{ {
if (IsFailure) if (IsFailure)
{ {
if (ErrorCode.HasValue) if (ErrorCode.HasValue)
return Result.Failure<TNew>(ErrorCode.Value, ErrorMessage ?? "Mapping failed", Exception); return Failure<TNew>(ErrorCode.Value, ErrorMessage ?? "Mapping failed", Exception);
return Result.Failure<TNew>(Exception ?? new Exception(ErrorMessage)); return Failure<TNew>(Exception ?? new Exception(ErrorMessage));
} }
try try
{ {
var mappedValue = mapper(Value!); var mappedValue = mapper(Value!);
return Result.Success(mappedValue); return Success(mappedValue);
} }
catch (Exception ex) catch (Exception ex)
{ {
return Result.Failure<TNew>(ex); return Failure<TNew>(ex);
} }
} }
@@ -182,8 +214,8 @@ public class Result<T> : Result
if (IsFailure) if (IsFailure)
{ {
if (ErrorCode.HasValue) if (ErrorCode.HasValue)
return Result.Failure<TNew>(ErrorCode.Value, ErrorMessage ?? "Binding failed", Exception); return Failure<TNew>(ErrorCode.Value, ErrorMessage ?? "Binding failed", Exception);
return Result.Failure<TNew>(Exception ?? new Exception(ErrorMessage)); return Failure<TNew>(Exception ?? new Exception(ErrorMessage));
} }
try try
@@ -192,7 +224,7 @@ public class Result<T> : Result
} }
catch (Exception ex) catch (Exception ex)
{ {
return Result.Failure<TNew>(ex); return Failure<TNew>(ex);
} }
} }
+28 -15
View File
@@ -57,10 +57,13 @@ public static class ResultExtensions
public static T GetOrElse<T>(this Result<T> result, Func<Exception?, string?, long?, T> alternativeProvider) public static T GetOrElse<T>(this Result<T> result, Func<Exception?, string?, long?, T> alternativeProvider)
{ {
return result.IsSuccess ? result.Value! : alternativeProvider(result.Exception, result.ErrorMessage, result.ErrorCode); return result.IsSuccess
? result.Value!
: alternativeProvider(result.Exception, result.ErrorMessage, result.ErrorCode);
} }
public static Result<T> Where<T>(this Result<T> result, Func<T, bool> predicate, string errorMessage = "Predicate not satisfied") public static Result<T> Where<T>(this Result<T> result, Func<T, bool> predicate,
string errorMessage = "Predicate not satisfied")
{ {
if (result.IsFailure) if (result.IsFailure)
return result; return result;
@@ -68,7 +71,8 @@ public static class ResultExtensions
return predicate(result.Value!) ? result : Result.Failure<T>(errorMessage); return predicate(result.Value!) ? result : Result.Failure<T>(errorMessage);
} }
public static Result<T> Where<T>(this Result<T> result, Func<T, bool> predicate, long errorCode, string errorMessage = "Predicate not satisfied") public static Result<T> Where<T>(this Result<T> result, Func<T, bool> predicate, long errorCode,
string errorMessage = "Predicate not satisfied")
{ {
if (result.IsFailure) if (result.IsFailure)
return result; return result;
@@ -76,7 +80,8 @@ public static class ResultExtensions
return predicate(result.Value!) ? result : Result.Failure<T>(errorCode, errorMessage); return predicate(result.Value!) ? result : Result.Failure<T>(errorCode, errorMessage);
} }
public static Result<T> Where<T>(this Result<T> result, Func<T, bool> predicate, Func<T, Exception> exceptionProvider) public static Result<T> Where<T>(this Result<T> result, Func<T, bool> predicate,
Func<T, Exception> exceptionProvider)
{ {
if (result.IsFailure) if (result.IsFailure)
return result; return result;
@@ -89,7 +94,8 @@ public static class ResultExtensions
return result.Bind(selector); return result.Bind(selector);
} }
public static Result<V> SelectMany<T, U, V>(this Result<T> result, Func<T, Result<U>> selector, Func<T, U, V> resultSelector) public static Result<V> SelectMany<T, U, V>(this Result<T> result, Func<T, Result<U>> selector,
Func<T, U, V> resultSelector)
{ {
return result.Bind(t => selector(t).Map(u => resultSelector(t, u))); return result.Bind(t => selector(t).Map(u => resultSelector(t, u)));
} }
@@ -120,7 +126,8 @@ public static class ResultExtensions
return result; return result;
} }
public static Result<T> DoOnBoth<T>(this Result<T> result, Action<T> onSuccess, Action<Exception?, string?> onFailure) public static Result<T> DoOnBoth<T>(this Result<T> result, Action<T> onSuccess,
Action<Exception?, string?> onFailure)
{ {
if (result.IsSuccess) if (result.IsSuccess)
onSuccess(result.Value!); onSuccess(result.Value!);
@@ -129,7 +136,8 @@ public static class ResultExtensions
return result; return result;
} }
public static Result<T> DoOnBoth<T>(this Result<T> result, Action<T> onSuccess, Action<Exception?, string?, long?> onFailure) public static Result<T> DoOnBoth<T>(this Result<T> result, Action<T> onSuccess,
Action<Exception?, string?, long?> onFailure)
{ {
if (result.IsSuccess) if (result.IsSuccess)
onSuccess(result.Value!); onSuccess(result.Value!);
@@ -140,12 +148,12 @@ public static class ResultExtensions
public static Result Ignore<T>(this Result<T> result) public static Result Ignore<T>(this Result<T> result)
{ {
if (result.IsSuccess) if (result.IsSuccess)
return Result.Success(); return Result.Success();
if (result.ErrorCode.HasValue) if (result.ErrorCode.HasValue)
return Result.Failure(result.ErrorCode.Value, result.ErrorMessage ?? "Operation failed", result.Exception); return Result.Failure(result.ErrorCode.Value, result.ErrorMessage ?? "Operation failed", result.Exception);
return Result.Failure(result.Exception ?? new Exception(result.ErrorMessage)); return Result.Failure(result.Exception ?? new Exception(result.ErrorMessage));
} }
@@ -172,7 +180,8 @@ public static class ResultExtensions
return result; return result;
} }
public static async Task<Result<T>> DoOnFailureAsync<T>(this Task<Result<T>> resultTask, Func<Exception?, string?, Task> asyncAction) public static async Task<Result<T>> DoOnFailureAsync<T>(this Task<Result<T>> resultTask,
Func<Exception?, string?, Task> asyncAction)
{ {
var result = await resultTask; var result = await resultTask;
if (result.IsFailure) if (result.IsFailure)
@@ -197,7 +206,8 @@ public static class ResultExtensions
} }
} }
public static async Task<Result<U>> BindAsync<T, U>(this Task<Result<T>> resultTask, Func<T, Task<Result<U>>> asyncBinder) public static async Task<Result<U>> BindAsync<T, U>(this Task<Result<T>> resultTask,
Func<T, Task<Result<U>>> asyncBinder)
{ {
var result = await resultTask; var result = await resultTask;
if (result.IsFailure) if (result.IsFailure)
@@ -221,7 +231,8 @@ public static class ResultExtensions
return predicate(result.Value!) ? result : Result.Failure<T>(errorMessage); return predicate(result.Value!) ? result : Result.Failure<T>(errorMessage);
} }
public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, long errorCode, string errorMessage) public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, long errorCode,
string errorMessage)
{ {
if (result.IsFailure) if (result.IsFailure)
return result; return result;
@@ -229,7 +240,8 @@ public static class ResultExtensions
return predicate(result.Value!) ? result : Result.Failure<T>(errorCode, errorMessage); return predicate(result.Value!) ? result : Result.Failure<T>(errorCode, errorMessage);
} }
public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate, Func<T, Exception> exceptionFactory) public static Result<T> Ensure<T>(this Result<T> result, Func<T, bool> predicate,
Func<T, Exception> exceptionFactory)
{ {
if (result.IsFailure) if (result.IsFailure)
return result; return result;
@@ -247,7 +259,8 @@ public static class ResultExtensions
return result.IsSuccess ? result : alternativeProvider(result.Exception, result.ErrorMessage); return result.IsSuccess ? result : alternativeProvider(result.Exception, result.ErrorMessage);
} }
public static Result<T> OrElse<T>(this Result<T> result, Func<Exception?, string?, long?, Result<T>> alternativeProvider) public static Result<T> OrElse<T>(this Result<T> result,
Func<Exception?, string?, long?, Result<T>> alternativeProvider)
{ {
return result.IsSuccess ? result : alternativeProvider(result.Exception, result.ErrorMessage, result.ErrorCode); return result.IsSuccess ? result : alternativeProvider(result.Exception, result.ErrorMessage, result.ErrorCode);
} }
+44 -47
View File
@@ -25,12 +25,12 @@ public static class CryptoHelper
throw new ArgumentNullException(nameof(password)); throw new ArgumentNullException(nameof(password));
// 生成随机 Salt // 生成随机 Salt
byte[] salt = RandomNumberGenerator.GetBytes(16); var salt = RandomNumberGenerator.GetBytes(16);
// 从密码派生密钥 // 从密码派生密钥
using var deriveBytes = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256); using var deriveBytes = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256);
byte[] key = deriveBytes.GetBytes(32); // 256 bit var key = deriveBytes.GetBytes(32); // 256 bit
byte[] iv = deriveBytes.GetBytes(16); // 128 bit var iv = deriveBytes.GetBytes(16); // 128 bit
// AES 加密 // AES 加密
using var aes = Aes.Create(); using var aes = Aes.Create();
@@ -38,11 +38,11 @@ public static class CryptoHelper
aes.IV = iv; aes.IV = iv;
using var encryptor = aes.CreateEncryptor(); using var encryptor = aes.CreateEncryptor();
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText); var plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
// 组合 Salt + 密文 // 组合 Salt + 密文
byte[] result = new byte[salt.Length + cipherBytes.Length]; var result = new byte[salt.Length + cipherBytes.Length];
Buffer.BlockCopy(salt, 0, result, 0, salt.Length); Buffer.BlockCopy(salt, 0, result, 0, salt.Length);
Buffer.BlockCopy(cipherBytes, 0, result, salt.Length, cipherBytes.Length); Buffer.BlockCopy(cipherBytes, 0, result, salt.Length, cipherBytes.Length);
@@ -62,20 +62,20 @@ public static class CryptoHelper
if (string.IsNullOrEmpty(password)) if (string.IsNullOrEmpty(password))
throw new ArgumentNullException(nameof(password)); throw new ArgumentNullException(nameof(password));
byte[] fullCipher = Convert.FromBase64String(cipherText); var fullCipher = Convert.FromBase64String(cipherText);
// 提取 Salt // 提取 Salt
byte[] salt = new byte[16]; var salt = new byte[16];
Buffer.BlockCopy(fullCipher, 0, salt, 0, 16); Buffer.BlockCopy(fullCipher, 0, salt, 0, 16);
// 提取密文 // 提取密文
byte[] cipherBytes = new byte[fullCipher.Length - 16]; var cipherBytes = new byte[fullCipher.Length - 16];
Buffer.BlockCopy(fullCipher, 16, cipherBytes, 0, cipherBytes.Length); Buffer.BlockCopy(fullCipher, 16, cipherBytes, 0, cipherBytes.Length);
// 从密码派生密钥 // 从密码派生密钥
using var deriveBytes = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256); using var deriveBytes = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256);
byte[] key = deriveBytes.GetBytes(32); var key = deriveBytes.GetBytes(32);
byte[] iv = deriveBytes.GetBytes(16); var iv = deriveBytes.GetBytes(16);
// AES 解密 // AES 解密
using var aes = Aes.Create(); using var aes = Aes.Create();
@@ -83,7 +83,7 @@ public static class CryptoHelper
aes.IV = iv; aes.IV = iv;
using var decryptor = aes.CreateDecryptor(); using var decryptor = aes.CreateDecryptor();
byte[] plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
return Encoding.UTF8.GetString(plainBytes); return Encoding.UTF8.GetString(plainBytes);
} }
@@ -121,8 +121,8 @@ public static class CryptoHelper
using var rsa = RSA.Create(); using var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _); rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKey), out _);
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText); var plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] cipherBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256); var cipherBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.OaepSHA256);
return Convert.ToBase64String(cipherBytes); return Convert.ToBase64String(cipherBytes);
} }
@@ -143,8 +143,8 @@ public static class CryptoHelper
using var rsa = RSA.Create(); using var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out _); rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKey), out _);
byte[] cipherBytes = Convert.FromBase64String(cipherText); var cipherBytes = Convert.FromBase64String(cipherText);
byte[] plainBytes = rsa.Decrypt(cipherBytes, RSAEncryptionPadding.OaepSHA256); var plainBytes = rsa.Decrypt(cipherBytes, RSAEncryptionPadding.OaepSHA256);
return Encoding.UTF8.GetString(plainBytes); return Encoding.UTF8.GetString(plainBytes);
} }
@@ -164,8 +164,8 @@ public static class CryptoHelper
throw new ArgumentNullException(nameof(input)); throw new ArgumentNullException(nameof(input));
using var md5 = MD5.Create(); using var md5 = MD5.Create();
byte[] inputBytes = Encoding.UTF8.GetBytes(input); var inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes); var hashBytes = md5.ComputeHash(inputBytes);
return BytesToHex(hashBytes); return BytesToHex(hashBytes);
} }
@@ -181,8 +181,8 @@ public static class CryptoHelper
throw new ArgumentNullException(nameof(input)); throw new ArgumentNullException(nameof(input));
using var sha256 = SHA256.Create(); using var sha256 = SHA256.Create();
byte[] inputBytes = Encoding.UTF8.GetBytes(input); var inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = sha256.ComputeHash(inputBytes); var hashBytes = sha256.ComputeHash(inputBytes);
return BytesToHex(hashBytes); return BytesToHex(hashBytes);
} }
@@ -198,8 +198,8 @@ public static class CryptoHelper
throw new ArgumentNullException(nameof(input)); throw new ArgumentNullException(nameof(input));
using var sha512 = SHA512.Create(); using var sha512 = SHA512.Create();
byte[] inputBytes = Encoding.UTF8.GetBytes(input); var inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = sha512.ComputeHash(inputBytes); var hashBytes = sha512.ComputeHash(inputBytes);
return BytesToHex(hashBytes); return BytesToHex(hashBytes);
} }
@@ -223,7 +223,7 @@ public static class CryptoHelper
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
throw new ArgumentNullException(nameof(input)); throw new ArgumentNullException(nameof(input));
byte[] bytes = Encoding.UTF8.GetBytes(input); var bytes = Encoding.UTF8.GetBytes(input);
return Convert.ToBase64String(bytes); return Convert.ToBase64String(bytes);
} }
@@ -237,7 +237,7 @@ public static class CryptoHelper
if (string.IsNullOrEmpty(input)) if (string.IsNullOrEmpty(input))
throw new ArgumentNullException(nameof(input)); throw new ArgumentNullException(nameof(input));
byte[] bytes = Convert.FromBase64String(input); var bytes = Convert.FromBase64String(input);
return Encoding.UTF8.GetString(bytes); return Encoding.UTF8.GetString(bytes);
} }
@@ -293,11 +293,11 @@ public static class CryptoHelper
throw new ArgumentNullException(nameof(password)); throw new ArgumentNullException(nameof(password));
// 生成随机 Salt // 生成随机 Salt
byte[] salt = RandomNumberGenerator.GetBytes(saltSize); var salt = RandomNumberGenerator.GetBytes(saltSize);
// 使用 PBKDF2 计算哈希 // 使用 PBKDF2 计算哈希
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256); using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256);
byte[] hash = pbkdf2.GetBytes(hashSize); var hash = pbkdf2.GetBytes(hashSize);
// 格式: $pbkdf2-sha256$迭代次数$Salt$Hash // 格式: $pbkdf2-sha256$迭代次数$Salt$Hash
return $"$pbkdf2-sha256${iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}"; return $"$pbkdf2-sha256${iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
@@ -323,13 +323,13 @@ public static class CryptoHelper
if (parts.Length != 5 || parts[1] != "pbkdf2-sha256") if (parts.Length != 5 || parts[1] != "pbkdf2-sha256")
return false; return false;
int iterations = int.Parse(parts[2]); var iterations = int.Parse(parts[2]);
byte[] salt = Convert.FromBase64String(parts[3]); var salt = Convert.FromBase64String(parts[3]);
byte[] storedHash = Convert.FromBase64String(parts[4]); var storedHash = Convert.FromBase64String(parts[4]);
// 计算输入密码的哈希 // 计算输入密码的哈希
using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256); using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256);
byte[] computedHash = pbkdf2.GetBytes(storedHash.Length); var computedHash = pbkdf2.GetBytes(storedHash.Length);
// 常量时间比较,防止时序攻击 // 常量时间比较,防止时序攻击
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash); return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
@@ -357,11 +357,11 @@ public static class CryptoHelper
if (string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(secret))
throw new ArgumentNullException(nameof(secret)); throw new ArgumentNullException(nameof(secret));
byte[] keyBytes = Encoding.UTF8.GetBytes(secret); var keyBytes = Encoding.UTF8.GetBytes(secret);
byte[] messageBytes = Encoding.UTF8.GetBytes(message); var messageBytes = Encoding.UTF8.GetBytes(message);
using var hmac = new HMACSHA256(keyBytes); using var hmac = new HMACSHA256(keyBytes);
byte[] hashBytes = hmac.ComputeHash(messageBytes); var hashBytes = hmac.ComputeHash(messageBytes);
return BytesToHex(hashBytes); return BytesToHex(hashBytes);
} }
@@ -399,11 +399,11 @@ public static class CryptoHelper
if (string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(secret))
throw new ArgumentNullException(nameof(secret)); throw new ArgumentNullException(nameof(secret));
byte[] keyBytes = Encoding.UTF8.GetBytes(secret); var keyBytes = Encoding.UTF8.GetBytes(secret);
byte[] messageBytes = Encoding.UTF8.GetBytes(message); var messageBytes = Encoding.UTF8.GetBytes(message);
using var hmac = new HMACSHA512(keyBytes); using var hmac = new HMACSHA512(keyBytes);
byte[] hashBytes = hmac.ComputeHash(messageBytes); var hashBytes = hmac.ComputeHash(messageBytes);
return BytesToHex(hashBytes); return BytesToHex(hashBytes);
} }
@@ -449,10 +449,7 @@ public static class CryptoHelper
chars += special; chars += special;
var result = new char[length]; var result = new char[length];
for (int i = 0; i < length; i++) for (var i = 0; i < length; i++) result[i] = chars[RandomNumberGenerator.GetInt32(chars.Length)];
{
result[i] = chars[RandomNumberGenerator.GetInt32(chars.Length)];
}
return new string(result); return new string(result);
} }
@@ -490,16 +487,19 @@ public static class CryptoHelper
chars += lowercase; chars += lowercase;
password.Add(lowercase[RandomNumberGenerator.GetInt32(lowercase.Length)]); password.Add(lowercase[RandomNumberGenerator.GetInt32(lowercase.Length)]);
} }
if (requireUppercase) if (requireUppercase)
{ {
chars += uppercase; chars += uppercase;
password.Add(uppercase[RandomNumberGenerator.GetInt32(uppercase.Length)]); password.Add(uppercase[RandomNumberGenerator.GetInt32(uppercase.Length)]);
} }
if (requireDigit) if (requireDigit)
{ {
chars += digits; chars += digits;
password.Add(digits[RandomNumberGenerator.GetInt32(digits.Length)]); password.Add(digits[RandomNumberGenerator.GetInt32(digits.Length)]);
} }
if (requireSpecialChar) if (requireSpecialChar)
{ {
chars += special; chars += special;
@@ -507,15 +507,12 @@ public static class CryptoHelper
} }
// 填充剩余长度 // 填充剩余长度
for (int i = password.Count; i < length; i++) for (var i = password.Count; i < length; i++) password.Add(chars[RandomNumberGenerator.GetInt32(chars.Length)]);
{
password.Add(chars[RandomNumberGenerator.GetInt32(chars.Length)]);
}
// 随机打乱 // 随机打乱
for (int i = password.Count - 1; i > 0; i--) for (var i = password.Count - 1; i > 0; i--)
{ {
int j = RandomNumberGenerator.GetInt32(i + 1); var j = RandomNumberGenerator.GetInt32(i + 1);
(password[i], password[j]) = (password[j], password[i]); (password[i], password[j]) = (password[j], password[i]);
} }
@@ -523,4 +520,4 @@ public static class CryptoHelper
} }
#endregion #endregion
} }
+10 -19
View File
@@ -87,13 +87,9 @@ public static class EnumHelper
return null; return null;
foreach (var field in typeof(TEnum).GetFields()) foreach (var field in typeof(TEnum).GetFields())
{
if (field.GetCustomAttribute<DescriptionAttribute>() is { } attribute) if (field.GetCustomAttribute<DescriptionAttribute>() is { } attribute)
{
if (attribute.Description.Equals(description, StringComparison.OrdinalIgnoreCase)) if (attribute.Description.Equals(description, StringComparison.OrdinalIgnoreCase))
return (TEnum)field.GetValue(null)!; return (TEnum)field.GetValue(null)!;
}
}
return null; return null;
} }
@@ -229,8 +225,8 @@ public static class EnumHelper
/// </summary> /// </summary>
public static TEnum AddFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum public static TEnum AddFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
{ {
int intValue = Convert.ToInt32(value); var intValue = Convert.ToInt32(value);
int intFlag = Convert.ToInt32(flag); var intFlag = Convert.ToInt32(flag);
return (TEnum)(object)(intValue | intFlag); return (TEnum)(object)(intValue | intFlag);
} }
@@ -239,8 +235,8 @@ public static class EnumHelper
/// </summary> /// </summary>
public static TEnum RemoveFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum public static TEnum RemoveFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
{ {
int intValue = Convert.ToInt32(value); var intValue = Convert.ToInt32(value);
int intFlag = Convert.ToInt32(flag); var intFlag = Convert.ToInt32(flag);
return (TEnum)(object)(intValue & ~intFlag); return (TEnum)(object)(intValue & ~intFlag);
} }
@@ -249,8 +245,8 @@ public static class EnumHelper
/// </summary> /// </summary>
public static TEnum ToggleFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum public static TEnum ToggleFlag<TEnum>(this TEnum value, TEnum flag) where TEnum : struct, Enum
{ {
int intValue = Convert.ToInt32(value); var intValue = Convert.ToInt32(value);
int intFlag = Convert.ToInt32(flag); var intFlag = Convert.ToInt32(flag);
return (TEnum)(object)(intValue ^ intFlag); return (TEnum)(object)(intValue ^ intFlag);
} }
@@ -259,11 +255,9 @@ public static class EnumHelper
/// </summary> /// </summary>
public static IEnumerable<TEnum> GetFlags<TEnum>(this TEnum value) where TEnum : struct, Enum public static IEnumerable<TEnum> GetFlags<TEnum>(this TEnum value) where TEnum : struct, Enum
{ {
foreach (TEnum flag in Enum.GetValues<TEnum>()) foreach (var flag in Enum.GetValues<TEnum>())
{
if (value.HasFlag(flag)) if (value.HasFlag(flag))
yield return flag; yield return flag;
}
} }
/// <summary> /// <summary>
@@ -271,11 +265,8 @@ public static class EnumHelper
/// </summary> /// </summary>
public static TEnum CombineFlags<TEnum>(params TEnum[] flags) where TEnum : struct, Enum public static TEnum CombineFlags<TEnum>(params TEnum[] flags) where TEnum : struct, Enum
{ {
int result = 0; var result = 0;
foreach (var flag in flags) foreach (var flag in flags) result |= Convert.ToInt32(flag);
{
result |= Convert.ToInt32(flag);
}
return (TEnum)(object)result; return (TEnum)(object)result;
} }
@@ -399,4 +390,4 @@ public static class EnumHelper
} }
#endregion #endregion
} }
+7 -15
View File
@@ -276,13 +276,11 @@ public static class FileHelper
// 递归复制子目录 // 递归复制子目录
if (recursive) if (recursive)
{
foreach (var subDir in dir.GetDirectories()) foreach (var subDir in dir.GetDirectories())
{ {
var targetDirPath = Path.Combine(destDir, subDir.Name); var targetDirPath = Path.Combine(destDir, subDir.Name);
CopyDirectory(subDir.FullName, targetDirPath, true); CopyDirectory(subDir.FullName, targetDirPath, true);
} }
}
} }
/// <summary> /// <summary>
@@ -351,10 +349,7 @@ public static class FileHelper
return Enumerable.Empty<string>(); return Enumerable.Empty<string>();
var files = new List<string>(); var files = new List<string>();
foreach (var pattern in searchPatterns) foreach (var pattern in searchPatterns) files.AddRange(GetFiles(directoryPath, pattern, recursive));
{
files.AddRange(GetFiles(directoryPath, pattern, recursive));
}
return files.Distinct(); return files.Distinct();
} }
@@ -664,7 +659,8 @@ public static class FileHelper
public static bool IsTextFile(string filePath) public static bool IsTextFile(string filePath)
{ {
var extension = GetExtension(filePath).ToLowerInvariant(); var extension = GetExtension(filePath).ToLowerInvariant();
string[] textExtensions = { ".txt", ".cs", ".xml", ".json", ".html", ".css", ".js", ".ts", ".md", ".log", ".csv", ".sql" }; string[] textExtensions =
{ ".txt", ".cs", ".xml", ".json", ".html", ".css", ".js", ".ts", ".md", ".log", ".csv", ".sql" };
return textExtensions.Contains(extension); return textExtensions.Contains(extension);
} }
@@ -756,16 +752,14 @@ public static class FileHelper
return; return;
var files = GetFiles(directoryPath, searchPattern, recursive); var files = GetFiles(directoryPath, searchPattern, recursive);
foreach (var file in files) foreach (var file in files) DeleteFile(file);
{
DeleteFile(file);
}
} }
/// <summary> /// <summary>
/// 删除超过指定天数的文件 /// 删除超过指定天数的文件
/// </summary> /// </summary>
public static int DeleteOldFiles(string directoryPath, int daysOld, string searchPattern = "*", bool recursive = false) public static int DeleteOldFiles(string directoryPath, int daysOld, string searchPattern = "*",
bool recursive = false)
{ {
if (string.IsNullOrWhiteSpace(directoryPath)) if (string.IsNullOrWhiteSpace(directoryPath))
throw new ArgumentNullException(nameof(directoryPath)); throw new ArgumentNullException(nameof(directoryPath));
@@ -778,13 +772,11 @@ public static class FileHelper
var deletedCount = 0; var deletedCount = 0;
foreach (var file in files) foreach (var file in files)
{
if (GetLastWriteTime(file) < cutoffDate) if (GetLastWriteTime(file) < cutoffDate)
{ {
DeleteFile(file); DeleteFile(file);
deletedCount++; deletedCount++;
} }
}
return deletedCount; return deletedCount;
} }
@@ -870,4 +862,4 @@ public static class FileHelper
} }
#endregion #endregion
} }
+18 -28
View File
@@ -17,7 +17,7 @@ public static class JsonHelper
/// <summary> /// <summary>
/// 驼峰命名选项(默认格式化输出) /// 驼峰命名选项(默认格式化输出)
/// </summary> /// </summary>
public static readonly JsonSerializerOptions CamelCaseOptions = new JsonSerializerOptions public static readonly JsonSerializerOptions CamelCaseOptions = new()
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true, WriteIndented = true,
@@ -25,14 +25,14 @@ public static class JsonHelper
Converters = Converters =
{ {
new IpAddressConverter(), new IpAddressConverter(),
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false) new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false)
}, }
}; };
/// <summary> /// <summary>
/// 驼峰命名选项(紧凑格式,不格式化输出) /// 驼峰命名选项(紧凑格式,不格式化输出)
/// </summary> /// </summary>
public static readonly JsonSerializerOptions CamelCaseCompactOptions = new JsonSerializerOptions public static readonly JsonSerializerOptions CamelCaseCompactOptions = new()
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false, WriteIndented = false,
@@ -41,14 +41,14 @@ public static class JsonHelper
Converters = Converters =
{ {
new IpAddressConverter(), new IpAddressConverter(),
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, allowIntegerValues: false) new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false)
}, }
}; };
/// <summary> /// <summary>
/// 蛇形命名选项(snake_case) /// 蛇形命名选项(snake_case)
/// </summary> /// </summary>
public static readonly JsonSerializerOptions SnakeCaseOptions = new JsonSerializerOptions public static readonly JsonSerializerOptions SnakeCaseOptions = new()
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true, WriteIndented = true,
@@ -56,14 +56,14 @@ public static class JsonHelper
Converters = Converters =
{ {
new IpAddressConverter(), new IpAddressConverter(),
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, allowIntegerValues: false) new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, false)
}, }
}; };
/// <summary> /// <summary>
/// 默认选项(保留原始命名,格式化输出) /// 默认选项(保留原始命名,格式化输出)
/// </summary> /// </summary>
public static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions public static readonly JsonSerializerOptions DefaultOptions = new()
{ {
WriteIndented = true, WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
@@ -71,7 +71,7 @@ public static class JsonHelper
{ {
new IpAddressConverter(), new IpAddressConverter(),
new JsonStringEnumConverter(allowIntegerValues: false) new JsonStringEnumConverter(allowIntegerValues: false)
}, }
}; };
#endregion #endregion
@@ -191,7 +191,8 @@ public static class JsonHelper
/// <summary> /// <summary>
/// 序列化对象到流 /// 序列化对象到流
/// </summary> /// </summary>
public static async Task SerializeToStreamAsync<T>(Stream stream, T value, bool useCompact = false, CancellationToken cancellationToken = default) public static async Task SerializeToStreamAsync<T>(Stream stream, T value, bool useCompact = false,
CancellationToken cancellationToken = default)
{ {
var options = useCompact ? CamelCaseCompactOptions : CamelCaseOptions; var options = useCompact ? CamelCaseCompactOptions : CamelCaseOptions;
await JsonSerializer.SerializeAsync(stream, value, options, cancellationToken); await JsonSerializer.SerializeAsync(stream, value, options, cancellationToken);
@@ -200,7 +201,8 @@ public static class JsonHelper
/// <summary> /// <summary>
/// 从流反序列化对象 /// 从流反序列化对象
/// </summary> /// </summary>
public static async Task<T?> DeserializeFromStreamAsync<T>(Stream stream, CancellationToken cancellationToken = default) public static async Task<T?> DeserializeFromStreamAsync<T>(Stream stream,
CancellationToken cancellationToken = default)
{ {
return await JsonSerializer.DeserializeAsync<T>(stream, CamelCaseOptions, cancellationToken); return await JsonSerializer.DeserializeAsync<T>(stream, CamelCaseOptions, cancellationToken);
} }
@@ -287,16 +289,10 @@ public static class JsonHelper
var element = doc.RootElement; var element = doc.RootElement;
foreach (var key in path.Split('.')) foreach (var key in path.Split('.'))
{
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(key, out var property)) if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty(key, out var property))
{
element = property; element = property;
}
else else
{
return null; return null;
}
}
return element.ToString(); return element.ToString();
} }
@@ -345,16 +341,10 @@ public static class JsonHelper
var merged = new Dictionary<string, JsonElement>(); var merged = new Dictionary<string, JsonElement>();
// 添加第一个 JSON 的所有属性 // 添加第一个 JSON 的所有属性
foreach (var property in doc1.RootElement.EnumerateObject()) foreach (var property in doc1.RootElement.EnumerateObject()) merged[property.Name] = property.Value.Clone();
{
merged[property.Name] = property.Value.Clone();
}
// 用第二个 JSON 的属性覆盖 // 用第二个 JSON 的属性覆盖
foreach (var property in doc2.RootElement.EnumerateObject()) foreach (var property in doc2.RootElement.EnumerateObject()) merged[property.Name] = property.Value.Clone();
{
merged[property.Name] = property.Value.Clone();
}
return JsonSerializer.Serialize(merged, CamelCaseOptions); return JsonSerializer.Serialize(merged, CamelCaseOptions);
} }
@@ -403,7 +393,7 @@ public class IpAddressConverter : JsonConverter<IPAddress>
{ {
public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
var ipString = reader.GetString() ?? String.Empty; var ipString = reader.GetString() ?? string.Empty;
return IPAddress.Parse(ipString); return IPAddress.Parse(ipString);
} }
+1 -5
View File
@@ -332,17 +332,13 @@ public static class ReflectionHelper
); );
foreach (var sourceProp in sourceProperties) foreach (var sourceProp in sourceProperties)
{
if (targetProperties.TryGetValue(sourceProp.Name, out var targetProp)) if (targetProperties.TryGetValue(sourceProp.Name, out var targetProp))
{
// 检查类型是否兼容 // 检查类型是否兼容
if (targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType)) if (targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{ {
var value = sourceProp.GetValue(source); var value = sourceProp.GetValue(source);
targetProp.SetValue(target, value); targetProp.SetValue(target, value);
} }
}
}
} }
/// <summary> /// <summary>
@@ -522,4 +518,4 @@ public static class ReflectionHelper
} }
#endregion #endregion
} }
+2 -4
View File
@@ -14,9 +14,9 @@ public static class SystemHelper
// todo: 2025/6/24 15:53 开发过程中使用固定端口 // todo: 2025/6/24 15:53 开发过程中使用固定端口
// return 8267; // return 8267;
TcpListener listener = new TcpListener(IPAddress.Loopback, 0); // 端口号为0表示由系统自动分配 var listener = new TcpListener(IPAddress.Loopback, 0); // 端口号为0表示由系统自动分配
listener.Start(); listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port; var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop(); listener.Stop();
return port; return port;
} }
@@ -33,9 +33,7 @@ public static class SystemHelper
public static bool IsAdministrator() public static bool IsAdministrator()
{ {
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("IsAdministrator method is only supported on Windows platform."); throw new PlatformNotSupportedException("IsAdministrator method is only supported on Windows platform.");
}
var identity = System.Security.Principal.WindowsIdentity.GetCurrent(); var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
var principal = new System.Security.Principal.WindowsPrincipal(identity); var principal = new System.Security.Principal.WindowsPrincipal(identity);
+16 -13
View File
@@ -158,8 +158,8 @@ public static class ValidationHelper
// 校验码对应值 // 校验码对应值
char[] checkCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' }; char[] checkCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
int sum = 0; var sum = 0;
for (int i = 0; i < 17; i++) for (var i = 0; i < 17; i++)
{ {
if (!char.IsDigit(idCard[i])) if (!char.IsDigit(idCard[i]))
return false; return false;
@@ -167,9 +167,9 @@ public static class ValidationHelper
sum += (idCard[i] - '0') * weights[i]; sum += (idCard[i] - '0') * weights[i];
} }
int mod = sum % 11; var mod = sum % 11;
char expectedCheckCode = checkCodes[mod]; var expectedCheckCode = checkCodes[mod];
char actualCheckCode = char.ToUpperInvariant(idCard[17]); var actualCheckCode = char.ToUpperInvariant(idCard[17]);
return expectedCheckCode == actualCheckCode; return expectedCheckCode == actualCheckCode;
} }
@@ -186,9 +186,9 @@ public static class ValidationHelper
try try
{ {
int year = int.Parse(idCard.Substring(6, 4)); var year = int.Parse(idCard.Substring(6, 4));
int month = int.Parse(idCard.Substring(10, 2)); var month = int.Parse(idCard.Substring(10, 2));
int day = int.Parse(idCard.Substring(12, 2)); var day = int.Parse(idCard.Substring(12, 2));
return new DateTime(year, month, day); return new DateTime(year, month, day);
} }
@@ -210,7 +210,7 @@ public static class ValidationHelper
try try
{ {
int genderCode = int.Parse(idCard.Substring(16, 1)); var genderCode = int.Parse(idCard.Substring(16, 1));
return genderCode % 2 == 0 ? "Female" : "Male"; return genderCode % 2 == 0 ? "Female" : "Male";
} }
catch catch
@@ -281,7 +281,8 @@ public static class ValidationHelper
/// <summary> /// <summary>
/// 验证浮点数在指定范围内(Result 模式) /// 验证浮点数在指定范围内(Result 模式)
/// </summary> /// </summary>
public static Result<double> ValidateRange(double value, double min, double max, string fieldName = "值", long errorCode = 400) public static Result<double> ValidateRange(double value, double min, double max, string fieldName = "值",
long errorCode = 400)
{ {
if (value < min || value > max) if (value < min || value > max)
return Result.Failure<double>(errorCode, $"{fieldName}必须在 {min} 到 {max} 之间"); return Result.Failure<double>(errorCode, $"{fieldName}必须在 {min} 到 {max} 之间");
@@ -302,7 +303,8 @@ public static class ValidationHelper
/// <param name="fieldName">字段名称</param> /// <param name="fieldName">字段名称</param>
/// <param name="errorCode">错误码(默认 400)</param> /// <param name="errorCode">错误码(默认 400)</param>
/// <returns>验证结果</returns> /// <returns>验证结果</returns>
public static Result<string> ValidateLength(string? value, int minLength, int maxLength, string fieldName = "字段", long errorCode = 400) public static Result<string> ValidateLength(string? value, int minLength, int maxLength, string fieldName = "字段",
long errorCode = 400)
{ {
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
return Result.Failure<string>(errorCode, $"{fieldName}不能为空"); return Result.Failure<string>(errorCode, $"{fieldName}不能为空");
@@ -420,7 +422,8 @@ public static class ValidationHelper
/// <param name="errorMessage">错误消息</param> /// <param name="errorMessage">错误消息</param>
/// <param name="errorCode">错误码(默认 400)</param> /// <param name="errorCode">错误码(默认 400)</param>
/// <returns>验证结果</returns> /// <returns>验证结果</returns>
public static Result<string> ValidateRegex(string? value, string pattern, string errorMessage = "格式无效", long errorCode = 400) public static Result<string> ValidateRegex(string? value, string pattern, string errorMessage = "格式无效",
long errorCode = 400)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
return Result.Failure<string>(errorCode, "值不能为空"); return Result.Failure<string>(errorCode, "值不能为空");
@@ -512,4 +515,4 @@ public static class ValidationHelper
} }
#endregion #endregion
} }
+22 -22
View File
@@ -1,30 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- 程序集元数据 --> <!-- 程序集元数据 -->
<Description>Sharp Framework 配置管理库 - 支持 JSON/XML/INI/TOML 多格式配置,提供热重载和验证功能</Description> <Description>Sharp Framework 配置管理库 - 支持 JSON/XML/INI/TOML 多格式配置,提供热重载和验证功能</Description>
<!-- NuGet 包元数据 --> <!-- NuGet 包元数据 -->
<PackageTags>sharp;config;configuration;hotreload;json;xml;ini;toml</PackageTags> <PackageTags>sharp;config;configuration;hotreload;json;xml;ini;toml</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0"/>
<PackageReference Include="Tomlyn" Version="0.19.0" /> <PackageReference Include="Tomlyn" Version="0.19.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+1 -3
View File
@@ -29,11 +29,9 @@ public static class ConfigBuilder
foreach (var f in files) foreach (var f in files)
{ {
if (!ConfigSource.TryGuess(f, out var src)) if (!ConfigSource.TryGuess(f, out var src))
{
throw new NotSupportedException($"Unsupported config extension: {f}"); throw new NotSupportedException($"Unsupported config extension: {f}");
}
opt.Sources.Add(src); opt.Sources.Add(src);
} }
}); });
} }
} }
+10 -13
View File
@@ -22,23 +22,20 @@ public static class ConfigBuilderExtensions
// 根据环境添加特定配置 // 根据环境添加特定配置
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
?? "Production"; ?? "Production";
var envConfigFile = $"appsettings.{environment}.json"; var envConfigFile = $"appsettings.{environment}.json";
var envConfigPath = Path.Combine(basePath, envConfigFile); var envConfigPath = Path.Combine(basePath, envConfigFile);
if (File.Exists(envConfigPath)) if (File.Exists(envConfigPath)) configFiles.Add(envConfigFile);
{
configFiles.Add(envConfigFile);
}
// 构建配置 // 构建配置
return ConfigBuilder.BuildFromFiles( return ConfigBuilder.BuildFromFiles(
configFiles.ToArray(), configFiles.ToArray(),
reloadOnChange: true, true,
includeEnvironment: true, true,
environmentPrefix: environmentPrefix, environmentPrefix,
basePath: basePath basePath
); );
} }
@@ -51,9 +48,9 @@ public static class ConfigBuilderExtensions
return ConfigBuilder.BuildFromFiles( return ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json" }, new[] { "appsettings.json" },
reloadOnChange: false, false,
includeEnvironment: false, false,
basePath: basePath basePath: basePath
); );
} }
} }
+35 -38
View File
@@ -6,16 +6,16 @@ namespace Config;
/// <summary> /// <summary>
/// 描述默认配置构建时的约定。 /// 描述默认配置构建时的约定。
/// </summary> /// </summary>
public sealed class ConfigDefaultsOptions public sealed class ConfigDefaultsOptions
{ {
private readonly IList<ConfigSource> _fluentSources = new List<ConfigSource>(); private readonly IList<ConfigSource> _fluentSources = new List<ConfigSource>();
public ConfigDefaultsOptions() public ConfigDefaultsOptions()
{ {
BasePath = AppContext.BaseDirectory; BasePath = AppContext.BaseDirectory;
EnvironmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") EnvironmentName = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); ?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
} }
/// <summary> /// <summary>
@@ -66,26 +66,34 @@ namespace Config;
/// <summary> /// <summary>
/// 通过流式 API 添加 JSON 配置文件(默认 optional)。 /// 通过流式 API 添加 JSON 配置文件(默认 optional)。
/// </summary> /// </summary>
public ConfigDefaultsOptions WithJson(string path, bool optional = true) => public ConfigDefaultsOptions WithJson(string path, bool optional = true)
AddFluentSource(path, ConfigFormat.Json, optional); {
return AddFluentSource(path, ConfigFormat.Json, optional);
}
/// <summary> /// <summary>
/// 通过流式 API 添加 INI 配置文件(默认 optional)。 /// 通过流式 API 添加 INI 配置文件(默认 optional)。
/// </summary> /// </summary>
public ConfigDefaultsOptions WithIni(string path, bool optional = true) => public ConfigDefaultsOptions WithIni(string path, bool optional = true)
AddFluentSource(path, ConfigFormat.Ini, optional); {
return AddFluentSource(path, ConfigFormat.Ini, optional);
}
/// <summary> /// <summary>
/// 通过流式 API 添加 XML 配置文件(默认 optional)。 /// 通过流式 API 添加 XML 配置文件(默认 optional)。
/// </summary> /// </summary>
public ConfigDefaultsOptions WithXml(string path, bool optional = true) => public ConfigDefaultsOptions WithXml(string path, bool optional = true)
AddFluentSource(path, ConfigFormat.Xml, optional); {
return AddFluentSource(path, ConfigFormat.Xml, optional);
}
/// <summary> /// <summary>
/// 通过流式 API 添加 TOML 配置文件(默认 optional)。 /// 通过流式 API 添加 TOML 配置文件(默认 optional)。
/// </summary> /// </summary>
public ConfigDefaultsOptions WithToml(string path, bool optional = true) => public ConfigDefaultsOptions WithToml(string path, bool optional = true)
AddFluentSource(path, ConfigFormat.Toml, optional); {
return AddFluentSource(path, ConfigFormat.Toml, optional);
}
private ConfigDefaultsOptions AddFluentSource(string path, ConfigFormat format, bool optional) private ConfigDefaultsOptions AddFluentSource(string path, ConfigFormat format, bool optional)
{ {
@@ -98,7 +106,10 @@ namespace Config;
return this; return this;
} }
internal IEnumerable<ConfigSource> GetFluentSources() => _fluentSources; internal IEnumerable<ConfigSource> GetFluentSources()
{
return _fluentSources;
}
} }
/// <summary> /// <summary>
@@ -146,10 +157,7 @@ public static class ConfigDefaults
internal static void ApplyDefaults(ConfigOptions target, ConfigDefaultsOptions defaults) internal static void ApplyDefaults(ConfigOptions target, ConfigDefaultsOptions defaults)
{ {
if (defaults.BasePath is not null) if (defaults.BasePath is not null) target.BasePath = defaults.BasePath;
{
target.BasePath = defaults.BasePath;
}
target.IncludeEnvironmentVariables = defaults.IncludeEnvironmentVariables; target.IncludeEnvironmentVariables = defaults.IncludeEnvironmentVariables;
target.EnvironmentPrefix = defaults.EnvironmentPrefix; target.EnvironmentPrefix = defaults.EnvironmentPrefix;
@@ -159,31 +167,20 @@ public static class ConfigDefaults
if (defaults.IncludeEnvironmentSpecificFile && !string.IsNullOrWhiteSpace(defaults.EnvironmentName)) if (defaults.IncludeEnvironmentSpecificFile && !string.IsNullOrWhiteSpace(defaults.EnvironmentName))
{ {
var envFile = BuildEnvironmentSpecificFile(defaults.MainFile, defaults.EnvironmentName!); var envFile = BuildEnvironmentSpecificFile(defaults.MainFile, defaults.EnvironmentName!);
target.Sources.Add(CreateSource(envFile, optional: true)); target.Sources.Add(CreateSource(envFile, true));
} }
foreach (var file in defaults.AdditionalFiles) foreach (var file in defaults.AdditionalFiles) target.Sources.Add(CreateSource(file, true));
{
target.Sources.Add(CreateSource(file, optional: true));
}
foreach (var source in defaults.GetFluentSources()) foreach (var source in defaults.GetFluentSources()) target.Sources.Add(source);
{
target.Sources.Add(source);
}
foreach (var source in defaults.AdditionalSources) foreach (var source in defaults.AdditionalSources) target.Sources.Add(source);
{
target.Sources.Add(source);
}
} }
private static ConfigSource CreateSource(string path, bool optional) private static ConfigSource CreateSource(string path, bool optional)
{ {
if (!ConfigSource.TryGuess(path, out var guessed)) if (!ConfigSource.TryGuess(path, out var guessed))
{
throw new NotSupportedException($"Unsupported config extension: {path}"); throw new NotSupportedException($"Unsupported config extension: {path}");
}
return new ConfigSource return new ConfigSource
{ {
@@ -202,4 +199,4 @@ public static class ConfigDefaults
var envFile = $"{name}.{environment}{ext}"; var envFile = $"{name}.{environment}{ext}";
return string.IsNullOrEmpty(dir) ? envFile : Path.Combine(dir, envFile); return string.IsNullOrEmpty(dir) ? envFile : Path.Combine(dir, envFile);
} }
} }
+1 -1
View File
@@ -6,4 +6,4 @@ public enum ConfigFormat
Ini, Ini,
Xml, Xml,
Toml Toml
} }
+33 -60
View File
@@ -24,18 +24,11 @@ public sealed class ConfigManager
{ {
var builder = new ConfigurationBuilder(); var builder = new ConfigurationBuilder();
if (!string.IsNullOrWhiteSpace(options.BasePath)) if (!string.IsNullOrWhiteSpace(options.BasePath)) builder.SetBasePath(options.BasePath!);
{
builder.SetBasePath(options.BasePath!);
}
if (options.FileProvider is not null) if (options.FileProvider is not null) builder.SetFileProvider(options.FileProvider);
{
builder.SetFileProvider(options.FileProvider);
}
foreach (var src in options.Sources) foreach (var src in options.Sources)
{
switch (src.Format) switch (src.Format)
{ {
case ConfigFormat.Json: case ConfigFormat.Json:
@@ -46,7 +39,7 @@ public sealed class ConfigManager
// 额外规则:将全局键复制到所有已声明的一级节中(若该节未显式定义该键), // 额外规则:将全局键复制到所有已声明的一级节中(若该节未显式定义该键),
// 以便像 "User=..." 可同时影响 "Database:User" 这类常见节。 // 以便像 "User=..." 可同时影响 "Database:User" 这类常见节。
var iniPath = options.BasePath is { Length: > 0 } var iniPath = options.BasePath is { Length: > 0 }
? System.IO.Path.Combine(options.BasePath!, src.Path) ? Path.Combine(options.BasePath!, src.Path)
: src.Path; : src.Path;
if (!File.Exists(iniPath)) if (!File.Exists(iniPath))
{ {
@@ -64,7 +57,7 @@ public sealed class ConfigManager
case ConfigFormat.Toml: case ConfigFormat.Toml:
// 解析 TOML 并以内存提供器注入(当前测试未要求 TOML 热更新) // 解析 TOML 并以内存提供器注入(当前测试未要求 TOML 热更新)
var tomlPath = options.BasePath is { Length: > 0 } var tomlPath = options.BasePath is { Length: > 0 }
? System.IO.Path.Combine(options.BasePath!, src.Path) ? Path.Combine(options.BasePath!, src.Path)
: src.Path; : src.Path;
if (!File.Exists(tomlPath)) if (!File.Exists(tomlPath))
{ {
@@ -76,9 +69,8 @@ public sealed class ConfigManager
var text = File.ReadAllText(tomlPath, new UTF8Encoding(false)); var text = File.ReadAllText(tomlPath, new UTF8Encoding(false));
var parse = Toml.Parse(text); var parse = Toml.Parse(text);
if (parse.HasErrors) if (parse.HasErrors)
{ throw new InvalidDataException($"Failed to load configuration from file '{tomlPath}'.",
throw new InvalidDataException($"Failed to load configuration from file '{tomlPath}'.", new FormatException("TOML parse failed")); new FormatException("TOML parse failed"));
}
var model = parse.ToModel(); var model = parse.ToModel();
var pairs = FlattenTomlModel(model); var pairs = FlattenTomlModel(model);
builder.AddInMemoryCollection(pairs); builder.AddInMemoryCollection(pairs);
@@ -86,7 +78,6 @@ public sealed class ConfigManager
default: default:
throw new NotSupportedException($"Unsupported format: {src.Format}"); throw new NotSupportedException($"Unsupported format: {src.Format}");
} }
}
if (options.IncludeEnvironmentVariables) if (options.IncludeEnvironmentVariables)
{ {
@@ -97,10 +88,7 @@ public sealed class ConfigManager
// - 同时兼容 "APP__Name" 这类紧跟双下划线的写法(用户容易误用),通过再注册一次 "APP__" 前缀捕获 // - 同时兼容 "APP__Name" 这类紧跟双下划线的写法(用户容易误用),通过再注册一次 "APP__" 前缀捕获
var pfx = options.EnvironmentPrefix; var pfx = options.EnvironmentPrefix;
builder.AddEnvironmentVariables(pfx); builder.AddEnvironmentVariables(pfx);
if (pfx.EndsWith("_")) if (pfx.EndsWith("_")) builder.AddEnvironmentVariables(pfx + "_");
{
builder.AddEnvironmentVariables(pfx + "_");
}
} }
else else
{ {
@@ -109,9 +97,7 @@ public sealed class ConfigManager
} }
if (options.ConfigureBuilderHook is not null) if (options.ConfigureBuilderHook is not null)
{
builder = (ConfigurationBuilder)options.ConfigureBuilderHook(builder); builder = (ConfigurationBuilder)options.ConfigureBuilderHook(builder);
}
var configuration = builder.Build(); var configuration = builder.Build();
return new ConfigManager(configuration, options); return new ConfigManager(configuration, options);
@@ -123,27 +109,20 @@ public sealed class ConfigManager
{ {
var target = new T(); var target = new T();
if (string.IsNullOrWhiteSpace(section)) if (string.IsNullOrWhiteSpace(section))
{
Configuration.Bind(target); Configuration.Bind(target);
}
else else
{
Configuration.GetSection(section).Bind(target); Configuration.GetSection(section).Bind(target);
}
if (validate) if (validate) ValidateObject(target, true);
{
ValidateObject(target, throwOnError: true);
}
return target; return target;
} }
public bool TryGet<T>(out T instance, string? section = null, bool validate = false) where T : new() public bool TryGet<T>(out T instance, string? section = null, bool validate = false) where T : new()
{ {
instance = Get<T>(section, validate: false); instance = Get<T>(section, false);
if (!validate) return true; if (!validate) return true;
var results = ValidateObject(instance, throwOnError: false); var results = ValidateObject(instance, false);
return results.Count == 0; return results.Count == 0;
} }
@@ -153,10 +132,7 @@ public sealed class ConfigManager
var visited = new HashSet<object>(ReferenceEqualityComparer.Instance); var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
RecursiveValidate(instance, results, visited); RecursiveValidate(instance, results, visited);
if (throwOnError && results.Count > 0) if (throwOnError && results.Count > 0) throw new ConfigurationValidationException(results);
{
throw new ConfigurationValidationException(results);
}
return results; return results;
} }
@@ -170,7 +146,7 @@ public sealed class ConfigManager
if (IsSimple(type)) return; if (IsSimple(type)) return;
var context = new ValidationContext(instance); var context = new ValidationContext(instance);
Validator.TryValidateObject(instance, context, results, validateAllProperties: true); Validator.TryValidateObject(instance, context, results, true);
foreach (var prop in type.GetProperties()) foreach (var prop in type.GetProperties())
{ {
@@ -180,30 +156,27 @@ public sealed class ConfigManager
if (IsSimple(pType)) continue; if (IsSimple(pType)) continue;
if (value is System.Collections.IEnumerable enumerable && value is not string) if (value is System.Collections.IEnumerable enumerable && value is not string)
{
foreach (var item in enumerable) foreach (var item in enumerable)
{ {
if (item is null) continue; if (item is null) continue;
RecursiveValidate(item, results, visited); RecursiveValidate(item, results, visited);
} }
}
else else
{
RecursiveValidate(value, results, visited); RecursiveValidate(value, results, visited);
}
} }
} }
private static bool IsSimple(Type t) private static bool IsSimple(Type t)
{ {
t = Nullable.GetUnderlyingType(t) ?? t; t = Nullable.GetUnderlyingType(t) ?? t;
return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(Guid) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan); return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) ||
t == typeof(Guid) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan);
} }
// Save APIs // Save APIs
public void Save<T>(T instance, string path, ConfigFormat format, bool indented = true, Encoding? encoding = null) public void Save<T>(T instance, string path, ConfigFormat format, bool indented = true, Encoding? encoding = null)
{ {
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)) ?? "."); Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)) ?? ".");
var serializer = SerializerFactory.Get(format); var serializer = SerializerFactory.Get(format);
serializer.SerializeToFile(path, instance!, indented, encoding); serializer.SerializeToFile(path, instance!, indented, encoding);
} }
@@ -247,12 +220,12 @@ public sealed class ConfigManager
yield break; yield break;
case TomlArray arr: case TomlArray arr:
for (var i = 0; i < arr.Count; i++) for (var i = 0; i < arr.Count; i++)
{ foreach (var p in WalkValue(key + ":" + i, arr[i]))
foreach (var p in WalkValue(key + ":" + i, arr[i])) yield return p; yield return p;
}
yield break; yield break;
default: default:
yield return new KeyValuePair<string, string?>(key, Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)); yield return new KeyValuePair<string, string?>(key,
Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture));
yield break; yield break;
} }
} }
@@ -275,6 +248,7 @@ public sealed class ConfigManager
current = sections[name] = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); current = sections[name] = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
continue; continue;
} }
var idx = line.IndexOf('='); var idx = line.IndexOf('=');
if (idx <= 0) continue; if (idx <= 0) continue;
var key = line.Substring(0, idx).Trim(); var key = line.Substring(0, idx).Trim();
@@ -293,38 +267,37 @@ public sealed class ConfigManager
foreach (var (sectionName, map) in sections) foreach (var (sectionName, map) in sections)
{ {
foreach (var kv in root) foreach (var kv in root)
{
if (!map.ContainsKey(kv.Key)) if (!map.ContainsKey(kv.Key))
{
yield return new KeyValuePair<string, string?>(sectionName + ":" + kv.Key, kv.Value); yield return new KeyValuePair<string, string?>(sectionName + ":" + kv.Key, kv.Value);
}
}
foreach (var kv in map) foreach (var kv in map)
{
yield return new KeyValuePair<string, string?>(sectionName + ":" + kv.Key, kv.Value); yield return new KeyValuePair<string, string?>(sectionName + ":" + kv.Key, kv.Value);
}
} }
} }
} }
public static class SerializerFactory public static class SerializerFactory
{ {
public static IConfigSerializer Get(ConfigFormat format) => format switch public static IConfigSerializer Get(ConfigFormat format)
{ {
ConfigFormat.Json => new JsonConfigSerializer(), return format switch
ConfigFormat.Xml => new XmlConfigSerializer(), {
ConfigFormat.Ini => new IniConfigSerializer(), ConfigFormat.Json => new JsonConfigSerializer(),
ConfigFormat.Toml => new TomlConfigSerializer(), ConfigFormat.Xml => new XmlConfigSerializer(),
_ => throw new NotSupportedException($"Unsupported format: {format}") ConfigFormat.Ini => new IniConfigSerializer(),
}; ConfigFormat.Toml => new TomlConfigSerializer(),
_ => throw new NotSupportedException($"Unsupported format: {format}")
};
}
} }
public sealed class ConfigurationValidationException : Exception public sealed class ConfigurationValidationException : Exception
{ {
public IReadOnlyList<ValidationResult> Errors { get; } public IReadOnlyList<ValidationResult> Errors { get; }
public ConfigurationValidationException(IReadOnlyList<ValidationResult> errors) public ConfigurationValidationException(IReadOnlyList<ValidationResult> errors)
: base($"Configuration validation failed: {string.Join("; ", errors.Select(e => e.ErrorMessage))}") : base($"Configuration validation failed: {string.Join("; ", errors.Select(e => e.ErrorMessage))}")
{ {
Errors = errors; Errors = errors;
} }
} }
+1 -1
View File
@@ -16,4 +16,4 @@ public sealed class ConfigOptions
// Hook: allow external modules (如 Common) 自定义构建 // Hook: allow external modules (如 Common) 自定义构建
public Func<IConfigurationBuilder, IConfigurationBuilder>? ConfigureBuilderHook { get; set; } public Func<IConfigurationBuilder, IConfigurationBuilder>? ConfigureBuilderHook { get; set; }
} }
+5 -2
View File
@@ -8,7 +8,10 @@ public sealed class ConfigSource
public required ConfigFormat Format { get; init; } public required ConfigFormat Format { get; init; }
public bool Optional { get; init; } = true; public bool Optional { get; init; } = true;
public override string ToString() => $"{Format}:{Path} (Optional={Optional})"; public override string ToString()
{
return $"{Format}:{Path} (Optional={Optional})";
}
public static bool TryGuess(string path, [NotNullWhen(true)] out ConfigSource? source) public static bool TryGuess(string path, [NotNullWhen(true)] out ConfigSource? source)
{ {
@@ -27,4 +30,4 @@ public sealed class ConfigSource
source = new ConfigSource { Path = path, Format = fmt.Value }; source = new ConfigSource { Path = path, Format = fmt.Value };
return true; return true;
} }
} }
@@ -35,7 +35,10 @@ internal sealed class ConfigAccessor<T> : IConfigAccessor<T>, IDisposable where
public T Value => _current; public T Value => _current;
public void Dispose() => _subscription.Dispose(); public void Dispose()
{
_subscription.Dispose();
}
} }
public static class ConfigServiceCollectionExtensions public static class ConfigServiceCollectionExtensions
@@ -101,4 +104,4 @@ public static class ConfigServiceCollectionExtensions
services.AddConfigBinding(section, validate, onReload); services.AddConfigBinding(section, validate, onReload);
return services; return services;
} }
} }
+31 -25
View File
@@ -1,22 +1,24 @@
# Config 模块 # Config 模块
面向 .NET 的通用配置模块,聚合多种配置源(JSON / INI / XML / TOML / 环境变量),支持强类型绑定、数据注解验证、可选热更新、文件读写序列化,并提供对外扩展 Hook 以便与外部公共库(如 Common 模块)集成。 面向 .NET 的通用配置模块,聚合多种配置源(JSON / INI / XML / TOML / 环境变量),支持强类型绑定、数据注解验证、可选热更新、文件读写序列化,并提供对外扩展
Hook 以便与外部公共库(如 Common 模块)集成。
本模块仅包含在 `src/Sharp/Config` 目录内的实现,遵从“公共项目,可插拔”的目标:你可以直接使用,也可通过依赖注入或 Hook 接入整体项目体系。 本模块仅包含在 `src/Sharp/Config` 目录内的实现,遵从“公共项目,可插拔”的目标:你可以直接使用,也可通过依赖注入或 Hook
接入整体项目体系。
## 功能清单 ## 功能清单
- 多格式配置读取: - 多格式配置读取:
- JSON / XML:使用 Microsoft 官方 provider,并支持 `ReloadOnChange` - JSON / XML:使用 Microsoft 官方 provider,并支持 `ReloadOnChange`
- INI / TOML:内置轻量解析,注入为内存配置源(当前默认不启用文件级热更新) - INI / TOML:内置轻量解析,注入为内存配置源(当前默认不启用文件级热更新)
- 多格式配置写入:支持将强类型对象写入为 JSON / XML / INI / TOML 文件 - 多格式配置写入:支持将强类型对象写入为 JSON / XML / INI / TOML 文件
- 强类型绑定:`Get<T>()` / `TryGet<T>()` 直接从 IConfiguration 绑定到 POCO - 强类型绑定:`Get<T>()` / `TryGet<T>()` 直接从 IConfiguration 绑定到 POCO
- 数据注解验证:`[Required]` / `[Range]` 等;支持递归验证嵌套对象与集合 - 数据注解验证:`[Required]` / `[Range]` 等;支持递归验证嵌套对象与集合
- 环境变量合并:可设置前缀,合并到配置树(优先级最高) - 环境变量合并:可设置前缀,合并到配置树(优先级最高)
- 热更新(可选):JSON / XML 支持文件监控自动重载;回调 `OnReload` - 热更新(可选):JSON / XML 支持文件监控自动重载;回调 `OnReload`
- 可扩展: - 可扩展:
- `ConfigOptions.ConfigureBuilderHook`:注入外部对 `IConfigurationBuilder` 的自定义构建(可与 Common 模块对接) - `ConfigOptions.ConfigureBuilderHook`:注入外部对 `IConfigurationBuilder` 的自定义构建(可与 Common 模块对接)
- `AddConfig(IServiceCollection, ...)`:便捷注册到 DI 容器 - `AddConfig(IServiceCollection, ...)`:便捷注册到 DI 容器
## 架构与设计 ## 架构与设计
@@ -27,12 +29,13 @@
- `ConfigSource`:单个配置源描述(路径、格式、是否可选) - `ConfigSource`:单个配置源描述(路径、格式、是否可选)
- `ConfigBuilder`:便捷构建器(从文件列表快速构建) - `ConfigBuilder`:便捷构建器(从文件列表快速构建)
- 序列化器: - 序列化器:
- `Serialization/JsonConfigSerializer` - `Serialization/JsonConfigSerializer`
- `Serialization/XmlConfigSerializer` - `Serialization/XmlConfigSerializer`
- `Serialization/IniConfigSerializer` - `Serialization/IniConfigSerializer`
- `Serialization/TomlConfigSerializer` - `Serialization/TomlConfigSerializer`
注意:当前 INI / TOML 的读取在构建期解析为键值对注入,不启用文件级热更新;JSON / XML 使用官方 provider,支持 `ReloadOnChange`。 注意:当前 INI / TOML 的读取在构建期解析为键值对注入,不启用文件级热更新;JSON / XML 使用官方 provider,支持
`ReloadOnChange`。
## 使用方式 ## 使用方式
@@ -73,7 +76,8 @@ var cm = Config.ConfigBuilder.BuildFromFiles(
); );
``` ```
环境变量键:使用双下划线分隔层级,例如 `APP__Database__Host`。如果设置了 `environmentPrefix: "APP_"`,也兼容 `APP__Name` 这类书写,但推荐前缀不以 `_` 结尾,例如 `"APP"`。 环境变量键:使用双下划线分隔层级,例如 `APP__Database__Host`。如果设置了 `environmentPrefix: "APP_"`,也兼容 `APP__Name`
这类书写,但推荐前缀不以 `_` 结尾,例如 `"APP"`。
### 2) 强类型绑定与验证 ### 2) 强类型绑定与验证
@@ -175,33 +179,35 @@ var cm = provider.GetRequiredService<ConfigManager>();
- 配置源合并策略:后添加覆盖先添加(与官方一致) - 配置源合并策略:后添加覆盖先添加(与官方一致)
- INI 读取: - INI 读取:
- 将“根键(未在任何节中)”与“节内键”一起展平成键路径 - 将“根键(未在任何节中)”与“节内键”一起展平成键路径
- 当前默认行为:将根键复制到所有已声明的一级节(若该节未定义同名键)。此举旨在方便简化配置,但可能与部分 INI 规范理解有差异;如需关闭或自定义,可在后续版本提供开关 - 当前默认行为:将根键复制到所有已声明的一级节(若该节未定义同名键)。此举旨在方便简化配置,但可能与部分 INI
- INI 写入:支持简单类型与一层嵌套对象 规范理解有差异;如需关闭或自定义,可在后续版本提供开关
- INI 写入:支持简单类型与一层嵌套对象
- TOML 读取: - TOML 读取:
- 解析为键值对注入,默认不启用文件级热更新 - 解析为键值对注入,默认不启用文件级热更新
- 直接 `LoadFromFile<T>(..., Toml)` 暂不支持;请通过配置管线绑定获取强类型 - 直接 `LoadFromFile<T>(..., Toml)` 暂不支持;请通过配置管线绑定获取强类型
- 环境变量前缀: - 环境变量前缀:
- 推荐使用不带下划线的短前缀(如 `APP`)+ 双下划线分隔层级(`APP__Section__Key`) - 推荐使用不带下划线的短前缀(如 `APP`)+ 双下划线分隔层级(`APP__Section__Key`)
- 模块对 `"APP_"` 这类带下划线结尾的前缀做了兼容处理,但仍建议遵循推荐写法 - 模块对 `"APP_"` 这类带下划线结尾的前缀做了兼容处理,但仍建议遵循推荐写法
- 验证: - 验证:
- 使用 DataAnnotations,递归验证嵌套对象与集合 - 使用 DataAnnotations,递归验证嵌套对象与集合
- 性能: - 性能:
- INI/TOML 读取为一次性解析(非流式);适合小中等规模配置。大型配置建议 JSON/XML - INI/TOML 读取为一次性解析(非流式);适合小中等规模配置。大型配置建议 JSON/XML
## 常见问题(FAQ) ## 常见问题(FAQ)
1. 为什么 INI/TOML 没有热更新? 1. 为什么 INI/TOML 没有热更新?
- 当前实现为轻量解析注入内存,未挂载文件监控。JSON/XML 已可直接使用热更新。若需要对 INI/TOML 启用热更新,可按 `FileConfigurationProvider` 模式扩展(可在后续版本提供)。 - 当前实现为轻量解析注入内存,未挂载文件监控。JSON/XML 已可直接使用热更新。若需要对 INI/TOML 启用热更新,可按
`FileConfigurationProvider` 模式扩展(可在后续版本提供)。
2. 环境变量用法推荐? 2. 环境变量用法推荐?
- 使用 `environmentPrefix: "APP"`,并设置 `APP__Section__Key`。尽量避免在前缀尾部加 `_`,可减少歧义。 - 使用 `environmentPrefix: "APP"`,并设置 `APP__Section__Key`。尽量避免在前缀尾部加 `_`,可减少歧义。
3. TOML 的写入后如何强类型读取? 3. TOML 的写入后如何强类型读取?
- 通过配置构建(Add... + 绑定)获取强类型对象;`LoadFromFile<T>` 暂不支持 TOML 直接反序列化。 - 通过配置构建(Add... + 绑定)获取强类型对象;`LoadFromFile<T>` 暂不支持 TOML 直接反序列化。
4. 能否与 Common 模块集成? 4. 能否与 Common 模块集成?
- 可以,通过 `ConfigOptions.ConfigureBuilderHook` 把 Common 的扩展注入到构建管线。 - 可以,通过 `ConfigOptions.ConfigureBuilderHook` 把 Common 的扩展注入到构建管线。
## 变更建议与后续计划 ## 变更建议与后续计划
@@ -8,4 +8,4 @@ public interface IConfigSerializer
T? Deserialize<T>(string content); T? Deserialize<T>(string content);
void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null); void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null);
T? DeserializeFromFile<T>(string path, Encoding? encoding = null); T? DeserializeFromFile<T>(string path, Encoding? encoding = null);
} }
@@ -9,18 +9,13 @@ public sealed class IniConfigSerializer : IConfigSerializer
{ {
var (root, sections) = ObjectToIniMaps(instance); var (root, sections) = ObjectToIniMaps(instance);
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var kv in root) foreach (var kv in root) sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
{
sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
}
foreach (var section in sections) foreach (var section in sections)
{ {
sb.Append('\n').Append('[').Append(section.Key).Append(']').Append('\n'); sb.Append('\n').Append('[').Append(section.Key).Append(']').Append('\n');
foreach (var kv in section.Value) foreach (var kv in section.Value) sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
{
sb.Append(kv.Key).Append('=').Append(kv.Value).Append('\n');
}
} }
return sb.ToString(); return sb.ToString();
} }
@@ -48,7 +43,8 @@ public sealed class IniConfigSerializer : IConfigSerializer
return Deserialize<T>(text); return Deserialize<T>(text);
} }
private static (Dictionary<string, string> root, Dictionary<string, Dictionary<string, string>> sections) ObjectToIniMaps<T>(T instance) private static (Dictionary<string, string> root, Dictionary<string, Dictionary<string, string>> sections)
ObjectToIniMaps<T>(T instance)
{ {
var root = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var root = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var sections = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); var sections = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
@@ -71,15 +67,10 @@ public sealed class IniConfigSerializer : IConfigSerializer
{ {
var innerVal = inner.GetValue(value); var innerVal = inner.GetValue(value);
if (innerVal is null) continue; if (innerVal is null) continue;
if (IsSimple(inner.PropertyType)) if (IsSimple(inner.PropertyType)) sec[inner.Name] = Convert.ToString(innerVal) ?? string.Empty;
{
sec[inner.Name] = Convert.ToString(innerVal) ?? string.Empty;
}
}
if (sec.Count > 0)
{
sections[prop.Name] = sec;
} }
if (sec.Count > 0) sections[prop.Name] = sec;
} }
} }
@@ -110,13 +101,9 @@ public sealed class IniConfigSerializer : IConfigSerializer
var value = line.Substring(idx + 1).Trim(); var value = line.Substring(idx + 1).Trim();
if (currentSection is null) if (currentSection is null)
{
dict[key] = value; dict[key] = value;
}
else else
{
currentSection[key] = value; currentSection[key] = value;
}
} }
return dict; return dict;
@@ -125,24 +112,17 @@ public sealed class IniConfigSerializer : IConfigSerializer
private static IEnumerable<KeyValuePair<string, string?>> FlattenToPairs(Dictionary<string, object?> dict) private static IEnumerable<KeyValuePair<string, string?>> FlattenToPairs(Dictionary<string, object?> dict)
{ {
foreach (var (key, value) in dict) foreach (var (key, value) in dict)
{
if (value is Dictionary<string, object?> nested) if (value is Dictionary<string, object?> nested)
{
foreach (var inner in nested) foreach (var inner in nested)
{
yield return new KeyValuePair<string, string?>(key + ":" + inner.Key, inner.Value?.ToString()); yield return new KeyValuePair<string, string?>(key + ":" + inner.Key, inner.Value?.ToString());
}
}
else else
{
yield return new KeyValuePair<string, string?>(key, value?.ToString()); yield return new KeyValuePair<string, string?>(key, value?.ToString());
}
}
} }
private static bool IsSimple(Type t) private static bool IsSimple(Type t)
{ {
t = Nullable.GetUnderlyingType(t) ?? t; t = Nullable.GetUnderlyingType(t) ?? t;
return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(Guid) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan); return t.IsPrimitive || t.IsEnum || t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) ||
t == typeof(Guid) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan);
} }
} }
@@ -25,7 +25,9 @@ public sealed class JsonConfigSerializer : IConfigSerializer
} }
public T? Deserialize<T>(string content) public T? Deserialize<T>(string content)
=> JsonSerializer.Deserialize<T>(content, _options); {
return JsonSerializer.Deserialize<T>(content, _options);
}
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null) public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
{ {
@@ -40,4 +42,4 @@ public sealed class JsonConfigSerializer : IConfigSerializer
var text = File.ReadAllText(path, encoding); var text = File.ReadAllText(path, encoding);
return Deserialize<T>(text); return Deserialize<T>(text);
} }
} }
@@ -31,4 +31,4 @@ public sealed class TomlConfigSerializer : IConfigSerializer
var text = File.ReadAllText(path, encoding); var text = File.ReadAllText(path, encoding);
return Deserialize<T>(text); return Deserialize<T>(text);
} }
} }
@@ -35,4 +35,4 @@ public sealed class XmlConfigSerializer : IConfigSerializer
var text = File.ReadAllText(path, encoding); var text = File.ReadAllText(path, encoding);
return Deserialize<T>(text); return Deserialize<T>(text);
} }
} }
+5 -14
View File
@@ -26,10 +26,7 @@ internal class DataContext : IDataContext
{ {
var type = typeof(TEntity); var type = typeof(TEntity);
if (_repositories.ContainsKey(type)) if (_repositories.ContainsKey(type)) return (IRepository<TEntity>)_repositories[type];
{
return (IRepository<TEntity>)_repositories[type];
}
var repository = new Repository<TEntity>(_context, _options.EnableSoftDelete); var repository = new Repository<TEntity>(_context, _options.EnableSoftDelete);
_repositories[type] = repository; _repositories[type] = repository;
@@ -40,16 +37,10 @@ internal class DataContext : IDataContext
public async Task<int> SaveChangesAsync(CancellationToken ct = default) public async Task<int> SaveChangesAsync(CancellationToken ct = default)
{ {
// 应用审计 // 应用审计
if (_options.EnableAudit) if (_options.EnableAudit) ApplyAuditInfo();
{
ApplyAuditInfo();
}
// 应用软删除 // 应用软删除
if (_options.EnableSoftDelete) if (_options.EnableSoftDelete) ApplySoftDelete();
{
ApplySoftDelete();
}
return await _context.SaveChangesAsync(ct); return await _context.SaveChangesAsync(ct);
} }
@@ -58,7 +49,7 @@ internal class DataContext : IDataContext
{ {
var entries = _context.ChangeTracker.Entries() var entries = _context.ChangeTracker.Entries()
.Where(e => e.Entity is IAuditableEntity && .Where(e => e.Entity is IAuditableEntity &&
(e.State == EntityState.Added || e.State == EntityState.Modified)); (e.State == EntityState.Added || e.State == EntityState.Modified));
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var userId = _options.CurrentUserId; var userId = _options.CurrentUserId;
@@ -101,4 +92,4 @@ internal class DataContext : IDataContext
_context.Dispose(); _context.Dispose();
_repositories.Clear(); _repositories.Clear();
} }
} }
+1 -1
View File
@@ -22,4 +22,4 @@ public interface IDataContext : IDisposable
/// 访问底层 DbContext(高级场景) /// 访问底层 DbContext(高级场景)
/// </summary> /// </summary>
DbContext Context { get; } DbContext Context { get; }
} }
+27 -27
View File
@@ -1,37 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Sharp.Data</RootNamespace> <RootNamespace>Sharp.Data</RootNamespace>
<!-- 程序集元数据 --> <!-- 程序集元数据 -->
<Description>Sharp Framework 数据访问库 - EF Core + Repository 模式,支持审计、软删除等企业级功能</Description> <Description>Sharp Framework 数据访问库 - EF Core + Repository 模式,支持审计、软删除等企业级功能</Description>
<!-- NuGet 包元数据 --> <!-- NuGet 包元数据 -->
<PackageTags>sharp;data;efcore;repository;entity-framework</PackageTags> <PackageTags>sharp;data;efcore;repository;entity-framework</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- EF Core --> <!-- EF Core -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0"/>
<!-- 数据库提供者 --> <!-- 数据库提供者 -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0"/>
<!-- 依赖注入 --> <!-- 依赖注入 -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- 引用 Common 模块,复用基础功能 --> <!-- 引用 Common 模块,复用基础功能 -->
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+8 -15
View File
@@ -34,7 +34,8 @@ public class DataContextBuilder<TContext> where TContext : DbContext
/// <summary> /// <summary>
/// 使用 SQL Server 数据库 /// 使用 SQL Server 数据库
/// </summary> /// </summary>
public DataContextBuilder<TContext> UseSqlServer(string connectionString, Action<SqlServerDbContextOptionsBuilder>? configure = null) public DataContextBuilder<TContext> UseSqlServer(string connectionString,
Action<SqlServerDbContextOptionsBuilder>? configure = null)
{ {
_optionsBuilder.UseSqlServer(connectionString, configure); _optionsBuilder.UseSqlServer(connectionString, configure);
_databaseConfigured = true; _databaseConfigured = true;
@@ -44,7 +45,8 @@ public class DataContextBuilder<TContext> where TContext : DbContext
/// <summary> /// <summary>
/// 使用 MySQL 数据库 /// 使用 MySQL 数据库
/// </summary> /// </summary>
public DataContextBuilder<TContext> UseMySql(string connectionString, string? serverVersion = null, Action<MySqlDbContextOptionsBuilder>? configure = null) public DataContextBuilder<TContext> UseMySql(string connectionString, string? serverVersion = null,
Action<MySqlDbContextOptionsBuilder>? configure = null)
{ {
var version = string.IsNullOrEmpty(serverVersion) var version = string.IsNullOrEmpty(serverVersion)
? ServerVersion.AutoDetect(connectionString) ? ServerVersion.AutoDetect(connectionString)
@@ -58,7 +60,8 @@ public class DataContextBuilder<TContext> where TContext : DbContext
/// <summary> /// <summary>
/// 使用 PostgreSQL 数据库 /// 使用 PostgreSQL 数据库
/// </summary> /// </summary>
public DataContextBuilder<TContext> UseNpgsql(string connectionString, Action<Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.NpgsqlDbContextOptionsBuilder>? configure = null) public DataContextBuilder<TContext> UseNpgsql(string connectionString,
Action<Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.NpgsqlDbContextOptionsBuilder>? configure = null)
{ {
_optionsBuilder.UseNpgsql(connectionString, configure); _optionsBuilder.UseNpgsql(connectionString, configure);
_databaseConfigured = true; _databaseConfigured = true;
@@ -162,32 +165,22 @@ public class DataContextBuilder<TContext> where TContext : DbContext
public IDataContext Build() public IDataContext Build()
{ {
// 如果没有配置数据库,默认使用 SQLite 内存数据库 // 如果没有配置数据库,默认使用 SQLite 内存数据库
if (!_databaseConfigured) if (!_databaseConfigured) _optionsBuilder.UseSqlite("Data Source=:memory:");
{
_optionsBuilder.UseSqlite("Data Source=:memory:");
}
// 创建 DbContext 实例 // 创建 DbContext 实例
var context = (TContext)Activator.CreateInstance(typeof(TContext), _optionsBuilder.Options)!; var context = (TContext)Activator.CreateInstance(typeof(TContext), _optionsBuilder.Options)!;
// 自动迁移 // 自动迁移
if (_dataOptions.EnableAutoMigration) if (_dataOptions.EnableAutoMigration)
{
context.Database.Migrate(); context.Database.Migrate();
}
else else
{
// 确保数据库已创建 // 确保数据库已创建
context.Database.EnsureCreated(); context.Database.EnsureCreated();
}
// 获取当前用户 ID // 获取当前用户 ID
if (_dataOptions.CurrentUserIdProvider != null) if (_dataOptions.CurrentUserIdProvider != null)
{
_dataOptions.CurrentUserId = _dataOptions.CurrentUserIdProvider(); _dataOptions.CurrentUserId = _dataOptions.CurrentUserIdProvider();
}
return new DataContext(context, _dataOptions); return new DataContext(context, _dataOptions);
} }
} }
+1 -1
View File
@@ -34,4 +34,4 @@ public class DataContextOptions
/// 设置当前用户 ID 的委托 /// 设置当前用户 ID 的委托
/// </summary> /// </summary>
public Func<string?>? CurrentUserIdProvider { get; set; } public Func<string?>? CurrentUserIdProvider { get; set; }
} }
@@ -27,8 +27,8 @@ public static class DataServiceExtensions
{ {
var provider = configuration[$"{configSection}:Provider"] ?? "SqlServer"; var provider = configuration[$"{configSection}:Provider"] ?? "SqlServer";
var connectionString = configuration[$"{configSection}:ConnectionString"] var connectionString = configuration[$"{configSection}:ConnectionString"]
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
$"未找到数据库连接字符串配置: {configSection}:ConnectionString"); $"未找到数据库连接字符串配置: {configSection}:ConnectionString");
return AddSharpDatabase<TContext>(services, provider, connectionString); return AddSharpDatabase<TContext>(services, provider, connectionString);
} }
@@ -51,9 +51,9 @@ public static class DataServiceExtensions
options.UseSqlServer(connectionString, sqlOptions => options.UseSqlServer(connectionString, sqlOptions =>
{ {
sqlOptions.EnableRetryOnFailure( sqlOptions.EnableRetryOnFailure(
maxRetryCount: 3, 3,
maxRetryDelay: TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5),
errorNumbersToAdd: null); null);
}); });
break; break;
@@ -63,18 +63,14 @@ public static class DataServiceExtensions
case "postgresql": case "postgresql":
case "postgres": case "postgres":
options.UseNpgsql(connectionString, npgsqlOptions => options.UseNpgsql(connectionString,
{ npgsqlOptions => { npgsqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null); });
npgsqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
});
break; break;
case "mysql": case "mysql":
var serverVersion = ServerVersion.AutoDetect(connectionString); var serverVersion = ServerVersion.AutoDetect(connectionString);
options.UseMySql(connectionString, serverVersion, mysqlOptions => options.UseMySql(connectionString, serverVersion,
{ mysqlOptions => { mysqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null); });
mysqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
});
break; break;
case "inmemory": case "inmemory":
@@ -98,4 +94,4 @@ public static class DataServiceExtensions
return services; return services;
} }
} }
+1 -1
View File
@@ -65,4 +65,4 @@ public abstract class AuditableEntityInt : AuditableEntity<int>
/// </summary> /// </summary>
public abstract class AuditableEntityLong : AuditableEntity<long> public abstract class AuditableEntityLong : AuditableEntity<long>
{ {
} }
+2 -2
View File
@@ -21,7 +21,7 @@ public abstract class Entity<TKey> where TKey : IEquatable<TKey>
if (GetType() != other.GetType()) if (GetType() != other.GetType())
return false; return false;
if (Id.Equals(default(TKey)) || other.Id.Equals(default(TKey))) if (Id.Equals(default) || other.Id.Equals(default))
return false; return false;
return Id.Equals(other.Id); return Id.Equals(other.Id);
@@ -72,4 +72,4 @@ public abstract class EntityInt : Entity<int>
/// </summary> /// </summary>
public abstract class EntityLong : Entity<long> public abstract class EntityLong : Entity<long>
{ {
} }
+1 -1
View File
@@ -24,4 +24,4 @@ public interface IAuditableEntity
/// 最后修改人 /// 最后修改人
/// </summary> /// </summary>
string? UpdatedBy { get; set; } string? UpdatedBy { get; set; }
} }
+1 -1
View File
@@ -19,4 +19,4 @@ public interface ISoftDeletable
/// 删除人 /// 删除人
/// </summary> /// </summary>
string? DeletedBy { get; set; } string? DeletedBy { get; set; }
} }
@@ -70,4 +70,4 @@ public static class QueryableExtensions
return query.Provider.CreateQuery<T>(resultExpression); return query.Provider.CreateQuery<T>(resultExpression);
} }
} }
+3
View File
@@ -177,6 +177,7 @@ public class MyEntity
### AuditableEntity<TKey> - 审计实体(可选) ### AuditableEntity<TKey> - 审计实体(可选)
如果继承此基类,会自动管理以下字段: 如果继承此基类,会自动管理以下字段:
- `Id` - 主键 - `Id` - 主键
- `CreatedAt` - 创建时间 - `CreatedAt` - 创建时间
- `CreatedBy` - 创建人 - `CreatedBy` - 创建人
@@ -361,6 +362,7 @@ builder.Services.AddHttpContextAccessor();
### 与 Sharp.Common 集成(已内置) ### 与 Sharp.Common 集成(已内置)
Data 模块已经引用了 Common 模块,可以直接使用: Data 模块已经引用了 Common 模块,可以直接使用:
- `PagedResult<T>` - 分页结果 - `PagedResult<T>` - 分页结果
- `Result<T>` - 错误处理(可选) - `Result<T>` - 错误处理(可选)
- 各种扩展方法 - 各种扩展方法
@@ -409,6 +411,7 @@ catch
### Q: 我必须使用 Entity/AuditableEntity 基类吗? ### Q: 我必须使用 Entity/AuditableEntity 基类吗?
**不是!** 基类是**完全可选**的。你可以: **不是!** 基类是**完全可选**的。你可以:
- 使用自己的实体定义 - 使用自己的实体定义
- 实现 `IAuditableEntity` 或 `ISoftDeletable` 接口 - 实现 `IAuditableEntity` 或 `ISoftDeletable` 接口
- 完全不使用审计和软删除功能 - 完全不使用审计和软删除功能
+1 -1
View File
@@ -87,4 +87,4 @@ public interface IRepository<TEntity> where TEntity : class
/// 根据主键删除实体(支持软删除) /// 根据主键删除实体(支持软删除)
/// </summary> /// </summary>
Task RemoveAsync(object id, CancellationToken ct = default); Task RemoveAsync(object id, CancellationToken ct = default);
} }
+7 -9
View File
@@ -31,7 +31,8 @@ internal class Repository<TEntity> : IRepository<TEntity> where TEntity : class
return await _dbSet.FindAsync(new[] { id }, ct); return await _dbSet.FindAsync(new[] { id }, ct);
} }
public async Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) public async Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate,
CancellationToken ct = default)
{ {
return await _dbSet.FirstOrDefaultAsync(predicate, ct); return await _dbSet.FirstOrDefaultAsync(predicate, ct);
} }
@@ -41,7 +42,8 @@ internal class Repository<TEntity> : IRepository<TEntity> where TEntity : class
return await _dbSet.ToListAsync(ct); return await _dbSet.ToListAsync(ct);
} }
public async Task<List<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) public async Task<List<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate,
CancellationToken ct = default)
{ {
return await _dbSet.Where(predicate).ToListAsync(ct); return await _dbSet.Where(predicate).ToListAsync(ct);
} }
@@ -134,13 +136,12 @@ internal class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
foreach (var entity in entities) foreach (var entity in entities)
{
if (entity is ISoftDeletable softDeletable) if (entity is ISoftDeletable softDeletable)
{ {
softDeletable.IsDeleted = true; softDeletable.IsDeleted = true;
softDeletable.DeletedAt = now; softDeletable.DeletedAt = now;
} }
}
_dbSet.UpdateRange(entities); _dbSet.UpdateRange(entities);
} }
else else
@@ -152,9 +153,6 @@ internal class Repository<TEntity> : IRepository<TEntity> where TEntity : class
public async Task RemoveAsync(object id, CancellationToken ct = default) public async Task RemoveAsync(object id, CancellationToken ct = default)
{ {
var entity = await GetByIdAsync(id, ct); var entity = await GetByIdAsync(id, ct);
if (entity != null) if (entity != null) Remove(entity);
{
Remove(entity);
}
} }
} }
+1
View File
@@ -3,6 +3,7 @@
本指南展示如何在不同场景下使用 Sharp.Data 模块。 本指南展示如何在不同场景下使用 Sharp.Data 模块。
## 目录 ## 目录
- [快速开始](#快速开始) - [快速开始](#快速开始)
- [控制台应用](#控制台应用) - [控制台应用](#控制台应用)
- [WPF 应用](#wpf-应用) - [WPF 应用](#wpf-应用)
@@ -4,7 +4,8 @@ namespace Sharp.EventBus.DependencyInjection;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddEventBus(this IServiceCollection services, Action<EventBusOptions>? configure = null) public static IServiceCollection AddEventBus(this IServiceCollection services,
Action<EventBusOptions>? configure = null)
{ {
var options = new EventBusOptions(); var options = new EventBusOptions();
configure?.Invoke(options); configure?.Invoke(options);
@@ -12,4 +13,4 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IEventBus, InMemoryEventBus>(); services.AddSingleton<IEventBus, InMemoryEventBus>();
return services; return services;
} }
} }
+14 -14
View File
@@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>Sharp.EventBus</AssemblyName> <AssemblyName>Sharp.EventBus</AssemblyName>
<RootNamespace>Sharp.EventBus</RootNamespace> <RootNamespace>Sharp.EventBus</RootNamespace>
<!-- 程序集元数据 --> <!-- 程序集元数据 -->
<Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅(规划中)</Description> <Description>Sharp Framework 事件总线库 - 进程内事件发布/订阅(规划中)</Description>
<!-- NuGet 包元数据 --> <!-- NuGet 包元数据 -->
<PackageTags>sharp;eventbus;pub-sub;events</PackageTags> <PackageTags>sharp;eventbus;pub-sub;events</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+1 -1
View File
@@ -13,4 +13,4 @@ public class EventBusOptions
/// 全局错误回调。 /// 全局错误回调。
/// </summary> /// </summary>
public Action<Exception, object>? OnError { get; set; } public Action<Exception, object>? OnError { get; set; }
} }
+3 -2
View File
@@ -12,7 +12,8 @@ internal sealed class EventSubscription : IDisposable
private readonly Action<EventSubscription> _onDispose; private readonly Action<EventSubscription> _onDispose;
private bool _disposed; private bool _disposed;
public EventSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority, Action<EventSubscription> onDispose) public EventSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority,
Action<EventSubscription> onDispose)
{ {
EventType = eventType; EventType = eventType;
Handler = handler; Handler = handler;
@@ -33,4 +34,4 @@ internal sealed class EventSubscription : IDisposable
_disposed = true; _disposed = true;
_onDispose(this); _onDispose(this);
} }
} }
+5 -3
View File
@@ -19,10 +19,12 @@ public interface IEventBus : IDisposable
/// <summary> /// <summary>
/// 订阅事件(同步处理器)。 /// 订阅事件(同步处理器)。
/// </summary> /// </summary>
IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class; IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0)
where TEvent : class;
/// <summary> /// <summary>
/// 订阅事件(异步处理器)。 /// 订阅事件(异步处理器)。
/// </summary> /// </summary>
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class; IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0)
} where TEvent : class;
}
+15 -13
View File
@@ -38,7 +38,8 @@ public sealed class InMemoryEventBus : IEventBus
return _channel.Writer.WriteAsync(@event, cancellationToken).AsTask(); return _channel.Writer.WriteAsync(@event, cancellationToken).AsTask();
} }
public IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class public IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0)
where TEvent : class
{ {
ThrowIfDisposed(); ThrowIfDisposed();
if (handler == null) throw new ArgumentNullException(nameof(handler)); if (handler == null) throw new ArgumentNullException(nameof(handler));
@@ -52,17 +53,22 @@ public sealed class InMemoryEventBus : IEventBus
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority); return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
} }
public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null,
int priority = 0) where TEvent : class
{ {
ThrowIfDisposed(); ThrowIfDisposed();
if (handler == null) throw new ArgumentNullException(nameof(handler)); if (handler == null) throw new ArgumentNullException(nameof(handler));
Task Wrapper(object evt) => handler((TEvent)evt); Task Wrapper(object evt)
{
return handler((TEvent)evt);
}
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority); return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
} }
private IDisposable AddSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority) private IDisposable AddSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter,
int priority)
{ {
var sub = new EventSubscription(eventType, handler, filter, priority, RemoveSubscription); var sub = new EventSubscription(eventType, handler, filter, priority, RemoveSubscription);
var list = _subscriptions.GetOrAdd(eventType, _ => new List<EventSubscription>()); var list = _subscriptions.GetOrAdd(eventType, _ => new List<EventSubscription>());
@@ -71,18 +77,17 @@ public sealed class InMemoryEventBus : IEventBus
list.Add(sub); list.Add(sub);
list.Sort((a, b) => a.Priority.CompareTo(b.Priority)); list.Sort((a, b) => a.Priority.CompareTo(b.Priority));
} }
return sub; return sub;
} }
private void RemoveSubscription(EventSubscription subscription) private void RemoveSubscription(EventSubscription subscription)
{ {
if (_subscriptions.TryGetValue(subscription.EventType, out var list)) if (_subscriptions.TryGetValue(subscription.EventType, out var list))
{
lock (list) lock (list)
{ {
list.Remove(subscription); list.Remove(subscription);
} }
}
} }
private async Task ProcessAsync() private async Task ProcessAsync()
@@ -90,12 +95,8 @@ public sealed class InMemoryEventBus : IEventBus
try try
{ {
while (await _channel.Reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false)) while (await _channel.Reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false))
{ while (_channel.Reader.TryRead(out var evt))
while (_channel.Reader.TryRead(out var evt)) await DispatchAsync(evt).ConfigureAwait(false);
{
await DispatchAsync(evt).ConfigureAwait(false);
}
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -145,6 +146,7 @@ public sealed class InMemoryEventBus : IEventBus
{ {
// ignore // ignore
} }
_cts.Dispose(); _cts.Dispose();
} }
} }
+27 -7
View File
@@ -9,6 +9,7 @@
### 场景对比 ### 场景对比
#### ❌ 不使用 EventBus - 紧耦合 #### ❌ 不使用 EventBus - 紧耦合
```csharp ```csharp
public class PlcMonitorService { public class PlcMonitorService {
private readonly MainViewModel _mainViewModel; private readonly MainViewModel _mainViewModel;
@@ -27,6 +28,7 @@ public class PlcMonitorService {
``` ```
#### ✅ 使用 EventBus - 解耦 #### ✅ 使用 EventBus - 解耦
```csharp ```csharp
// 发布者:只管发布,不关心谁处理 // 发布者:只管发布,不关心谁处理
public class PlcMonitorService { public class PlcMonitorService {
@@ -75,6 +77,7 @@ public class PlcDataLogger {
## 🚀 快速开始(未来) ## 🚀 快速开始(未来)
### 1. 注册服务 ### 1. 注册服务
```csharp ```csharp
// App.xaml.cs // App.xaml.cs
protected override void OnStartup(StartupEventArgs e) { protected override void OnStartup(StartupEventArgs e) {
@@ -88,6 +91,7 @@ protected override void OnStartup(StartupEventArgs e) {
``` ```
### 2. 定义事件 ### 2. 定义事件
```csharp ```csharp
// 事件:简单的 POCO 类 // 事件:简单的 POCO 类
public record UserLoginEvent(string Username, DateTime LoginTime); public record UserLoginEvent(string Username, DateTime LoginTime);
@@ -98,6 +102,7 @@ public record OrderCreatedEvent(int OrderId, decimal TotalAmount);
``` ```
### 3. 发布事件 ### 3. 发布事件
```csharp ```csharp
public class LoginService { public class LoginService {
private readonly IEventBus _eventBus; private readonly IEventBus _eventBus;
@@ -114,6 +119,7 @@ public class LoginService {
``` ```
### 4. 订阅事件 ### 4. 订阅事件
```csharp ```csharp
// 方式1:在构造函数中订阅 // 方式1:在构造函数中订阅
public class MainViewModel : ViewModelBase { public class MainViewModel : ViewModelBase {
@@ -158,6 +164,7 @@ public class HighValueOrderMonitor {
## 📚 核心 API 设计 ## 📚 核心 API 设计
### IEventBus 接口 ### IEventBus 接口
```csharp ```csharp
public interface IEventBus { public interface IEventBus {
/// <summary> /// <summary>
@@ -197,6 +204,7 @@ public interface IEventBus {
``` ```
### EventSubscription 类(流式API) ### EventSubscription 类(流式API)
```csharp ```csharp
public class EventSubscription<TEvent> : IDisposable where TEvent : class { public class EventSubscription<TEvent> : IDisposable where TEvent : class {
/// <summary> /// <summary>
@@ -240,6 +248,7 @@ eventBus.CreateSubscription<PlcDataChangedEvent>()
## 🔧 高级功能 ## 🔧 高级功能
### 1. 事件优先级 ### 1. 事件优先级
```csharp ```csharp
// 优先级:数字越小越先执行 // 优先级:数字越小越先执行
eventBus.Subscribe<OrderCreatedEvent>(e => ValidateOrder(e), priority: 1); eventBus.Subscribe<OrderCreatedEvent>(e => ValidateOrder(e), priority: 1);
@@ -248,18 +257,21 @@ eventBus.Subscribe<OrderCreatedEvent>(e => UpdateStatistics(e), priority: 20);
``` ```
### 2. 弱引用订阅(防止内存泄漏) ### 2. 弱引用订阅(防止内存泄漏)
```csharp ```csharp
// 自动模式:订阅者被GC回收时自动取消订阅 // 自动模式:订阅者被GC回收时自动取消订阅
eventBus.SubscribeWeak<UserLoginEvent>(viewModel, (vm, e) => vm.OnLogin(e)); eventBus.SubscribeWeak<UserLoginEvent>(viewModel, (vm, e) => vm.OnLogin(e));
``` ```
### 3. 延迟发布 ### 3. 延迟发布
```csharp ```csharp
// 延迟1秒发布(防抖场景) // 延迟1秒发布(防抖场景)
eventBus.PublishDelayed(new SearchTextChangedEvent(text), TimeSpan.FromSeconds(1)); eventBus.PublishDelayed(new SearchTextChangedEvent(text), TimeSpan.FromSeconds(1));
``` ```
### 4. 错误处理 ### 4. 错误处理
```csharp ```csharp
// 全局错误处理器 // 全局错误处理器
services.AddEventBus(options => { services.AddEventBus(options => {
@@ -280,6 +292,7 @@ eventBus.Subscribe<OrderCreatedEvent>(e => {
``` ```
### 5. 事件日志(可选) ### 5. 事件日志(可选)
```csharp ```csharp
services.AddEventBus(options => { services.AddEventBus(options => {
options.EnableEventLogging = true; // 记录所有事件发布 options.EnableEventLogging = true; // 记录所有事件发布
@@ -292,6 +305,7 @@ services.AddEventBus(options => {
## 💡 典型使用场景 ## 💡 典型使用场景
### 场景1:跨 ViewModel 通信 ### 场景1:跨 ViewModel 通信
```csharp ```csharp
// LoginViewModel 发布登录成功事件 // LoginViewModel 发布登录成功事件
public class LoginViewModel { public class LoginViewModel {
@@ -313,6 +327,7 @@ public class MainViewModel {
``` ```
### 场景2:PLC数据监控 ### 场景2:PLC数据监控
```csharp ```csharp
// PLC监控服务发布数据变化 // PLC监控服务发布数据变化
public class PlcMonitorService { public class PlcMonitorService {
@@ -346,6 +361,7 @@ public class DataLoggerService {
``` ```
### 场景3:报警系统 ### 场景3:报警系统
```csharp ```csharp
// 报警事件 // 报警事件
public record AlarmTriggeredEvent(string AlarmId, AlarmLevel Level, string Message); public record AlarmTriggeredEvent(string AlarmId, AlarmLevel Level, string Message);
@@ -389,11 +405,13 @@ services.AddEventBus(options => {
## 🏗️ 实现方案 ## 🏗️ 实现方案
### 基于 System.Threading.Channels ### 基于 System.Threading.Channels
- **高性能**:异步无锁队列 - **高性能**:异步无锁队列
- **线程安全**:内置并发支持 - **线程安全**:内置并发支持
- **背压支持**:通道容量控制 - **背压支持**:通道容量控制
### 架构设计 ### 架构设计
``` ```
IEventBus(接口) IEventBus(接口)
↓ ↓
@@ -415,12 +433,12 @@ System.Threading.Channels.Channel<EventEnvelope>
## 🔄 与其他模式对比 ## 🔄 与其他模式对比
| 模式 | 优点 | 缺点 | 适用场景 | | 模式 | 优点 | 缺点 | 适用场景 |
|-----|------|------|---------| |--------------|-----------|-----------|------------|
| **EventBus** | 完全解耦、多订阅者 | 略微增加复杂度 | 跨模块通信、插件架构 | | **EventBus** | 完全解耦、多订阅者 | 略微增加复杂度 | 跨模块通信、插件架构 |
| **直接回调** | 简单直接 | 紧耦合、单订阅者 | 简单父子组件通信 | | **直接回调** | 简单直接 | 紧耦合、单订阅者 | 简单父子组件通信 |
| **事件聚合器** | 解耦 | 需要手动管理 | 中等复杂度应用 | | **事件聚合器** | 解耦 | 需要手动管理 | 中等复杂度应用 |
| **消息队列** | 跨进程、持久化 | 重量级、需要中间件 | 分布式系统 | | **消息队列** | 跨进程、持久化 | 重量级、需要中间件 | 分布式系统 |
--- ---
@@ -437,6 +455,7 @@ System.Threading.Channels.Channel<EventEnvelope>
## 🚀 未来扩展 ## 🚀 未来扩展
### 可能的扩展方向 ### 可能的扩展方向
1. **分布式 EventBus**(基于 Redis Pub/Sub) 1. **分布式 EventBus**(基于 Redis Pub/Sub)
2. **事件溯源**(Event Sourcing)支持 2. **事件溯源**(Event Sourcing)支持
3. **事件重放**(Replay)功能 3. **事件重放**(Replay)功能
@@ -448,7 +467,8 @@ System.Threading.Channels.Channel<EventEnvelope>
- [MediatR](https://github.com/jbogard/MediatR) - .NET 消息中介库 - [MediatR](https://github.com/jbogard/MediatR) - .NET 消息中介库
- [Prism EventAggregator](https://prismlibrary.com/docs/event-aggregator.html) - WPF MVVM 事件聚合器 - [Prism EventAggregator](https://prismlibrary.com/docs/event-aggregator.html) - WPF MVVM 事件聚合器
- [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) - .NET 高性能通道 - [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) -
.NET 高性能通道
--- ---
@@ -25,10 +25,8 @@ public static class SharpLogBuilderExtensions
var opt = sp.GetRequiredService<IOptions<SharpLogOptions>>().Value; var opt = sp.GetRequiredService<IOptions<SharpLogOptions>>().Value;
o.MinLevel = opt.MinLevel; o.MinLevel = opt.MinLevel;
foreach (var kv in opt.Overrides) foreach (var kv in opt.Overrides)
{
// category-specific rule // category-specific rule
o.Rules.Add(new LoggerFilterRule(null, kv.Key, kv.Value, null)); o.Rules.Add(new LoggerFilterRule(null, kv.Key, kv.Value, null));
}
})); }));
// Register Serilog provider via DI so it participates in logging pipeline // Register Serilog provider via DI so it participates in logging pipeline
@@ -37,15 +35,13 @@ public static class SharpLogBuilderExtensions
var opt = sp.GetRequiredService<IOptions<SharpLogOptions>>().Value; var opt = sp.GetRequiredService<IOptions<SharpLogOptions>>().Value;
if (!opt.UseSerilog) if (!opt.UseSerilog)
{
// If not using Serilog, don't inject a provider; return a no-op stub // If not using Serilog, don't inject a provider; return a no-op stub
return new NoopLoggerProvider(); return new NoopLoggerProvider();
}
var env = sp.GetService<IHostEnvironment>(); var env = sp.GetService<IHostEnvironment>();
var cfg = sp.GetService<IConfiguration>(); var cfg = sp.GetService<IConfiguration>();
var serilog = SerilogConfigurator.CreateLogger(opt, env, cfg); var serilog = SerilogConfigurator.CreateLogger(opt, env, cfg);
return new SerilogLoggerProvider(serilog, dispose: true); return new SerilogLoggerProvider(serilog, true);
}); });
return builder; return builder;
@@ -55,18 +51,38 @@ public static class SharpLogBuilderExtensions
{ {
private sealed class NoopLogger : ILogger private sealed class NoopLogger : ILogger
{ {
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance; public IDisposable BeginScope<TState>(TState state) where TState : notnull
public bool IsEnabled(LogLevel logLevel) => false; {
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { } return NullScope.Instance;
}
public bool IsEnabled(LogLevel logLevel)
{
return false;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
}
} }
private sealed class NullScope : IDisposable private sealed class NullScope : IDisposable
{ {
public static readonly NullScope Instance = new(); public static readonly NullScope Instance = new();
public void Dispose() { }
public void Dispose()
{
}
} }
public ILogger CreateLogger(string categoryName) => new NoopLogger(); public ILogger CreateLogger(string categoryName)
public void Dispose() { } {
return new NoopLogger();
}
public void Dispose()
{
}
} }
} }
+1 -2
View File
@@ -23,5 +23,4 @@ internal sealed class ActivityEnricher : ILogEventEnricher
logEvent.AddOrUpdateProperty(spanIdProp); logEvent.AddOrUpdateProperty(spanIdProp);
} }
} }
} }
+34 -35
View File
@@ -19,10 +19,7 @@ internal static class SerilogConfigurator
// Minimum levels // Minimum levels
cfg = cfg.MinimumLevel.Is(MapLevel(options.MinLevel)); cfg = cfg.MinimumLevel.Is(MapLevel(options.MinLevel));
foreach (var kv in options.Overrides) foreach (var kv in options.Overrides) cfg = cfg.MinimumLevel.Override(kv.Key, MapLevel(kv.Value));
{
cfg = cfg.MinimumLevel.Override(kv.Key, MapLevel(kv.Value));
}
// Enrichers // Enrichers
cfg = cfg.Enrich.FromLogContext(); cfg = cfg.Enrich.FromLogContext();
@@ -38,19 +35,13 @@ internal static class SerilogConfigurator
cfg = cfg.Enrich.WithEnvironmentName(); cfg = cfg.Enrich.WithEnvironmentName();
var appName = options.Enrich.Application var appName = options.Enrich.Application
?? TryGetAppName() ?? TryGetAppName()
?? AppDomain.CurrentDomain.FriendlyName; ?? AppDomain.CurrentDomain.FriendlyName;
cfg = cfg.Enrich.WithProperty("Application", appName); cfg = cfg.Enrich.WithProperty("Application", appName);
if (env != null) if (env != null) cfg = cfg.Enrich.WithProperty("Environment", env.EnvironmentName);
{
cfg = cfg.Enrich.WithProperty("Environment", env.EnvironmentName);
}
if (options.Enrich.WithActivityTrace) if (options.Enrich.WithActivityTrace) cfg = cfg.Enrich.With(new ActivityEnricher());
{
cfg = cfg.Enrich.With(new ActivityEnricher());
}
} }
// Sinks // Sinks
@@ -62,7 +53,8 @@ internal static class SerilogConfigurator
} }
else else
{ {
const string template = "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"; const string template =
"[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}";
cfg = cfg.WriteTo.Async(a => a.Console(outputTemplate: template)); cfg = cfg.WriteTo.Async(a => a.Console(outputTemplate: template));
} }
} }
@@ -87,7 +79,8 @@ internal static class SerilogConfigurator
} }
else else
{ {
const string template = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"; const string template =
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}";
cfg = cfg.WriteTo.Async(a => a.File(path, cfg = cfg.WriteTo.Async(a => a.File(path,
rollingInterval: rolling, rollingInterval: rolling,
retainedFileCountLimit: retained, retainedFileCountLimit: retained,
@@ -101,27 +94,33 @@ internal static class SerilogConfigurator
return cfg.CreateLogger(); return cfg.CreateLogger();
} }
private static LogEventLevel MapLevel(LogLevel level) => level switch private static LogEventLevel MapLevel(LogLevel level)
{ {
LogLevel.Trace => LogEventLevel.Verbose, return level switch
LogLevel.Debug => LogEventLevel.Debug, {
LogLevel.Information => LogEventLevel.Information, LogLevel.Trace => LogEventLevel.Verbose,
LogLevel.Warning => LogEventLevel.Warning, LogLevel.Debug => LogEventLevel.Debug,
LogLevel.Error => LogEventLevel.Error, LogLevel.Information => LogEventLevel.Information,
LogLevel.Critical => LogEventLevel.Fatal, LogLevel.Warning => LogEventLevel.Warning,
_ => LogEventLevel.Information LogLevel.Error => LogEventLevel.Error,
}; LogLevel.Critical => LogEventLevel.Fatal,
_ => LogEventLevel.Information
};
}
private static Serilog.RollingInterval MapRolling(Sharp.Log.Options.RollingInterval rolling) => rolling switch private static Serilog.RollingInterval MapRolling(Options.RollingInterval rolling)
{ {
Sharp.Log.Options.RollingInterval.Infinite => Serilog.RollingInterval.Infinite, return rolling switch
Sharp.Log.Options.RollingInterval.Year => Serilog.RollingInterval.Year, {
Sharp.Log.Options.RollingInterval.Month => Serilog.RollingInterval.Month, Sharp.Log.Options.RollingInterval.Infinite => Serilog.RollingInterval.Infinite,
Sharp.Log.Options.RollingInterval.Day => Serilog.RollingInterval.Day, Sharp.Log.Options.RollingInterval.Year => Serilog.RollingInterval.Year,
Sharp.Log.Options.RollingInterval.Hour => Serilog.RollingInterval.Hour, Sharp.Log.Options.RollingInterval.Month => Serilog.RollingInterval.Month,
Sharp.Log.Options.RollingInterval.Minute => Serilog.RollingInterval.Minute, Sharp.Log.Options.RollingInterval.Day => Serilog.RollingInterval.Day,
_ => Serilog.RollingInterval.Day Sharp.Log.Options.RollingInterval.Hour => Serilog.RollingInterval.Hour,
}; Sharp.Log.Options.RollingInterval.Minute => Serilog.RollingInterval.Minute,
_ => Serilog.RollingInterval.Day
};
}
private static string? TryGetAppName() private static string? TryGetAppName()
{ {
@@ -134,4 +133,4 @@ internal static class SerilogConfigurator
return null; return null;
} }
} }
} }
+23 -23
View File
@@ -1,30 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- 程序集元数据 --> <!-- 程序集元数据 -->
<Description>Sharp Framework 日志库 - 基于 Serilog 的结构化日志系统,支持文件、控制台等多种输出</Description> <Description>Sharp Framework 日志库 - 基于 Serilog 的结构化日志系统,支持文件、控制台等多种输出</Description>
<!-- NuGet 包元数据 --> <!-- NuGet 包元数据 -->
<PackageTags>sharp;log;logging;serilog;structured-logging</PackageTags> <PackageTags>sharp;log;logging;serilog;structured-logging</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
<PackageReference Include="Serilog" Version="4.0.0" /> <PackageReference Include="Serilog" Version="4.0.0"/>
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0"/>
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" /> <PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0"/>
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" /> <PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -51,9 +51,20 @@ public sealed class SharpRequestLoggingMiddleware
private static readonly double TimestampToMilliseconds = 1000.0 / Stopwatch.Frequency; private static readonly double TimestampToMilliseconds = 1000.0 / Stopwatch.Frequency;
private readonly long _start; private readonly long _start;
private ValueStopwatch(long start) => _start = start; private ValueStopwatch(long start)
public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp()); {
public long GetElapsedMilliseconds() => (long)((Stopwatch.GetTimestamp() - _start) * TimestampToMilliseconds); _start = start;
}
public static ValueStopwatch StartNew()
{
return new ValueStopwatch(Stopwatch.GetTimestamp());
}
public long GetElapsedMilliseconds()
{
return (long)((Stopwatch.GetTimestamp() - _start) * TimestampToMilliseconds);
}
} }
} }
@@ -63,5 +74,4 @@ public static class SharpRequestLoggingExtensions
{ {
return app.UseMiddleware<SharpRequestLoggingMiddleware>(); return app.UseMiddleware<SharpRequestLoggingMiddleware>();
} }
} }
+1 -2
View File
@@ -48,5 +48,4 @@ public sealed class EnrichOptions
public bool WithEnvironmentName { get; set; } = true; public bool WithEnvironmentName { get; set; } = true;
public string? Application { get; set; } = null; // default to entry assembly name public string? Application { get; set; } = null; // default to entry assembly name
public bool WithActivityTrace { get; set; } = true; public bool WithActivityTrace { get; set; } = true;
} }
+15 -4
View File
@@ -1,11 +1,13 @@
# Sharp.Log # Sharp.Log
面向“快速集成”的日志整合层:基于 `Microsoft.Extensions.Logging` 抽象,内置 Serilog 预设,实现一行接入,默认即用(Console + 按日滚动文件,结构化 JSON,丰富上下文)。 面向“快速集成”的日志整合层:基于 `Microsoft.Extensions.Logging` 抽象,内置 Serilog 预设,实现一行接入,默认即用(Console +
按日滚动文件,结构化 JSON,丰富上下文)。
- 目标:不重造日志框架,仅做预设与桥接,减少样板代码 - 目标:不重造日志框架,仅做预设与桥接,减少样板代码
- 适用:控制台应用、Worker、ASP.NET Core(含中间件) - 适用:控制台应用、Worker、ASP.NET Core(含中间件)
## 特性 ## 特性
- 一行集成:`builder.Logging.AddSharpLog()`,无配置也可用 - 一行集成:`builder.Logging.AddSharpLog()`,无配置也可用
- 输出:Console(可彩色)、文件(按日滚动、保留数量、异步写) - 输出:Console(可彩色)、文件(按日滚动、保留数量、异步写)
- 结构化:文本模板/JSON;支持模板+参数与异常详情 - 结构化:文本模板/JSON;支持模板+参数与异常详情
@@ -15,6 +17,7 @@
- 配置驱动:支持 `appsettings.json:SharpLog` 与环境变量绑定 - 配置驱动:支持 `appsettings.json:SharpLog` 与环境变量绑定
## 快速开始 ## 快速开始
1) 引用项目(或 NuGet 包) 1) 引用项目(或 NuGet 包)
2) 在应用入口添加一行: 2) 在应用入口添加一行:
@@ -33,6 +36,7 @@ app.Run();
``` ```
// WPF/WinForms/任意桌面 // WPF/WinForms/任意桌面
```csharp ```csharp
using Sharp.Log; using Sharp.Log;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -48,11 +52,13 @@ logger.LogInformation("桌面程序已启动");
``` ```
无需任何 `appsettings.json`,即可获得: 无需任何 `appsettings.json`,即可获得:
- Console 文本输出 - Console 文本输出
- 文件 JSON 输出:`logs/app-.log`(按天滚动,保留 30 个文件,异步写) - 文件 JSON 输出:`logs/app-.log`(按天滚动,保留 30 个文件,异步写)
- 丰富上下文属性 - 丰富上下文属性
## 可选配置(appsettings.json) ## 可选配置(appsettings.json)
```json ```json
{ {
"SharpLog": { "SharpLog": {
@@ -88,23 +94,28 @@ logger.LogInformation("桌面程序已启动");
``` ```
环境变量示例: 环境变量示例:
- `SHARPLOG__MINLEVEL=Warning` - `SHARPLOG__MINLEVEL=Warning`
- `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7` - `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7`
## API 概览 ## API 概览
- `ILoggingBuilder AddSharpLog(Action<SharpLogOptions>? configure = null)` - `ILoggingBuilder AddSharpLog(Action<SharpLogOptions>? configure = null)`
- `IApplicationBuilder UseSharpRequestLogging()` - `IApplicationBuilder UseSharpRequestLogging()`
- 选项:`SharpLogOptions`、`FileOptions`、`EnrichOptions`、`RollingInterval` - 选项:`SharpLogOptions`、`FileOptions`、`EnrichOptions`、`RollingInterval`
## 常见问题 ## 常见问题
- 需要清空默认 Provider 吗? - 需要清空默认 Provider 吗?
- 如需仅使用 Sharp.Log,请在调用方执行 `builder.Logging.ClearProviders()`,然后 `AddSharpLog()`。 - 如需仅使用 Sharp.Log,请在调用方执行 `builder.Logging.ClearProviders()`,然后 `AddSharpLog()`。
- 一定要 Serilog 吗? - 一定要 Serilog 吗?
- 默认启用(文件/JSON/丰富器等依赖)。如不想使用,可在配置中设置 `UseSerilog=false`,保留框架自带 Console(建议不要 `ClearProviders()`)。 - 默认启用(文件/JSON/丰富器等依赖)。如不想使用,可在配置中设置 `UseSerilog=false`,保留框架自带 Console(建议不要
`ClearProviders()`)。
- 与分布式追踪兼容吗? - 与分布式追踪兼容吗?
- 兼容。自动注入 `Activity.TraceId/SpanId`;若使用 OpenTelemetry,日志会带 Trace 关联信息。 - 兼容。自动注入 `Activity.TraceId/SpanId`;若使用 OpenTelemetry,日志会带 Trace 关联信息。
## 目录 ## 目录
- `Options/` 选项模型 - `Options/` 选项模型
- `Internal/` Serilog 配置与 Enricher - `Internal/` Serilog 配置与 Enricher
- `Extensions/` 集成扩展 - `Extensions/` 集成扩展
+9 -3
View File
@@ -52,9 +52,15 @@ public static class SharpLogDefaults
_inner = provider.GetRequiredService<ILoggerFactory>(); _inner = provider.GetRequiredService<ILoggerFactory>();
} }
public void AddProvider(ILoggerProvider provider) => _inner.AddProvider(provider); public void AddProvider(ILoggerProvider provider)
{
_inner.AddProvider(provider);
}
public ILogger CreateLogger(string categoryName) => _inner.CreateLogger(categoryName); public ILogger CreateLogger(string categoryName)
{
return _inner.CreateLogger(categoryName);
}
public void Dispose() public void Dispose()
{ {
@@ -95,4 +101,4 @@ public static class SharpLogDefaults
} }
} }
} }
} }
+14
View File
@@ -3,6 +3,7 @@
本指南说明如何在不同类型项目中以“最少代码”集成 Sharp.Log,并按需进行配置与扩展。 本指南说明如何在不同类型项目中以“最少代码”集成 Sharp.Log,并按需进行配置与扩展。
## 1. 控制台/Worker 应用 ## 1. 控制台/Worker 应用
```csharp ```csharp
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -19,6 +20,7 @@ await host.RunAsync();
``` ```
## 2. ASP.NET Core 应用 ## 2. ASP.NET Core 应用
```csharp ```csharp
using Sharp.Log.Extensions; using Sharp.Log.Extensions;
using Sharp.Log.Middleware; using Sharp.Log.Middleware;
@@ -33,6 +35,7 @@ app.Run();
``` ```
## 3. 桌面/WPF/WinForms ## 3. 桌面/WPF/WinForms
```csharp ```csharp
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Sharp.Log; using Sharp.Log;
@@ -50,7 +53,9 @@ logger.LogInformation("桌面程序已启动");
无需 HostBuilder,即可获得默认 Console + 滚动文件输出;同样支持 `SharpLog` 配置节/环境变量。 无需 HostBuilder,即可获得默认 Console + 滚动文件输出;同样支持 `SharpLog` 配置节/环境变量。
## 4. 可选配置(appsettings.json) ## 4. 可选配置(appsettings.json)
添加 `SharpLog` 配置节(可省略)。 添加 `SharpLog` 配置节(可省略)。
```json ```json
{ {
"SharpLog": { "SharpLog": {
@@ -79,11 +84,13 @@ logger.LogInformation("桌面程序已启动");
``` ```
环境变量覆盖示例: 环境变量覆盖示例:
- `SHARPLOG__MINLEVEL=Warning` - `SHARPLOG__MINLEVEL=Warning`
- `SHARPLOG__FILE__PATH=/var/logs/app-.log` - `SHARPLOG__FILE__PATH=/var/logs/app-.log`
- `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7` - `SHARPLOG__FILE__RETAINEDFILECOUNTLIMIT=7`
## 5. 常见定制 ## 5. 常见定制
- 仅文件输出(关闭 Console): - 仅文件输出(关闭 Console):
```csharp ```csharp
builder.Logging.AddSharpLog(o => { o.UseConsole = false; }); builder.Logging.AddSharpLog(o => { o.UseConsole = false; });
@@ -102,27 +109,34 @@ logger.LogInformation("桌面程序已启动");
``` ```
## 6. 与现有 Provider 协作 ## 6. 与现有 Provider 协作
- 默认不清空 Provider;若希望仅用 Sharp.Log,可在应用中先执行 `ClearProviders()` 再调用 `AddSharpLog()`。 - 默认不清空 Provider;若希望仅用 Sharp.Log,可在应用中先执行 `ClearProviders()` 再调用 `AddSharpLog()`。
- 如将 `UseSerilog` 设为 `false`,不会注入 Serilog Provider,请保留框架自带 Console Provider(不要 `ClearProviders()`)。 - 如将 `UseSerilog` 设为 `false`,不会注入 Serilog Provider,请保留框架自带 Console Provider(不要 `ClearProviders()`)。
## 7. 请求日志中间件 ## 7. 请求日志中间件
启用后会记录:请求开始、结束(状态码与耗时)、异常。 启用后会记录:请求开始、结束(状态码与耗时)、异常。
```csharp ```csharp
app.UseSharpRequestLogging(); app.UseSharpRequestLogging();
``` ```
## 8. 结构化与上下文 ## 8. 结构化与上下文
日志条目自动包含: 日志条目自动包含:
- `Application`、`Environment`、`MachineName`、`ProcessId`、`ThreadId` - `Application`、`Environment`、`MachineName`、`ProcessId`、`ThreadId`
- `TraceId`、`SpanId`(来自 `Activity.Current`) - `TraceId`、`SpanId`(来自 `Activity.Current`)
你可使用 `BeginScope` 传入业务上下文: 你可使用 `BeginScope` 传入业务上下文:
```csharp ```csharp
using var scope = logger.BeginScope(new Dictionary<string, object>{ ["UserId"] = userId }); using var scope = logger.BeginScope(new Dictionary<string, object>{ ["UserId"] = userId });
logger.LogInformation("User {UserId} logged in", userId); logger.LogInformation("User {UserId} logged in", userId);
``` ```
## 9. 测试建议 ## 9. 测试建议
- 在测试中为文件输出设置临时目录: - 在测试中为文件输出设置临时目录:
```csharp ```csharp
builder.Logging.AddSharpLog(o => builder.Logging.AddSharpLog(o =>
@@ -6,5 +6,4 @@ namespace NetClient.Abstractions;
public interface IAccessTokenProvider public interface IAccessTokenProvider
{ {
Task<string?> GetTokenAsync(CancellationToken cancellationToken = default); Task<string?> GetTokenAsync(CancellationToken cancellationToken = default);
} }
+31 -11
View File
@@ -13,20 +13,40 @@ public interface IHttpClient
Task<string> GetStringAsync(string path, object? query = null, CancellationToken cancellationToken = default); Task<string> GetStringAsync(string path, object? query = null, CancellationToken cancellationToken = default);
Task<byte[]> GetBytesAsync(string path, object? query = null, CancellationToken cancellationToken = default); Task<byte[]> GetBytesAsync(string path, object? query = null, CancellationToken cancellationToken = default);
Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default); Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body,
Task<TResponse?> PutAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<TResponse?> PatchAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default);
Task<TResponse?> PutAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default);
Task<TResponse?> PatchAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default);
Task DeleteAsync(string path, object? query = null, CancellationToken cancellationToken = default); Task DeleteAsync(string path, object? query = null, CancellationToken cancellationToken = default);
Task<TResponse?> PostMultipartAsync<TResponse>(string path, System.Action<System.Net.Http.MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default); Task<TResponse?> PostMultipartAsync<TResponse>(string path, Action<MultipartFormDataContent> buildContent,
CancellationToken cancellationToken = default);
// Result-based convenience methods (non-throwing) // Result-based convenience methods (non-throwing)
Task<Result<T>> GetResultAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default); Task<Result<T>> GetResultAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default);
Task<Result<string>> GetStringResultAsync(string path, object? query = null, CancellationToken cancellationToken = default);
Task<Result<byte[]>> GetBytesResultAsync(string path, object? query = null, CancellationToken cancellationToken = default); Task<Result<string>> GetStringResultAsync(string path, object? query = null,
Task<Result<TResponse>> PostResultAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<Result<TResponse>> PutResultAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default);
Task<Result<TResponse>> PatchResultAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default); Task<Result<byte[]>> GetBytesResultAsync(string path, object? query = null,
CancellationToken cancellationToken = default);
Task<Result<TResponse>> PostResultAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default);
Task<Result<TResponse>> PutResultAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default);
Task<Result<TResponse>> PatchResultAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default);
Task<Result> DeleteResultAsync(string path, object? query = null, CancellationToken cancellationToken = default); Task<Result> DeleteResultAsync(string path, object? query = null, CancellationToken cancellationToken = default);
Task<Result<TResponse>> PostMultipartResultAsync<TResponse>(string path, System.Action<System.Net.Http.MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default);
} Task<Result<TResponse>> PostMultipartResultAsync<TResponse>(string path,
Action<MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default);
}
@@ -9,5 +9,4 @@ public interface IHttpContentSerializer
string ContentType { get; } string ContentType { get; }
Task SerializeAsync<T>(Stream stream, T? value, CancellationToken cancellationToken = default); Task SerializeAsync<T>(Stream stream, T? value, CancellationToken cancellationToken = default);
Task<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default); Task<T?> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default);
} }
@@ -3,5 +3,4 @@ namespace NetClient.Abstractions;
public interface INetClientFactory public interface INetClientFactory
{ {
IHttpClient Create(string name); IHttpClient Create(string name);
} }
@@ -12,10 +12,11 @@ public static class ConfigurationExtensions
/// Bind NetClientOptions from ConfigManager (section "NetClient"). /// Bind NetClientOptions from ConfigManager (section "NetClient").
/// Falls back to SystemConfig for BaseAddress/Timeout when not supplied. /// Falls back to SystemConfig for BaseAddress/Timeout when not supplied.
/// </summary> /// </summary>
public static IServiceCollection AddNetClient(this IServiceCollection services, ConfigManager cm, string section = "NetClient") public static IServiceCollection AddNetClient(this IServiceCollection services, ConfigManager cm,
string section = "NetClient")
{ {
var opt = cm.Get<NetClientOptions>(section, validate: false) ?? new NetClientOptions(); var opt = cm.Get<NetClientOptions>(section, false) ?? new NetClientOptions();
var sys = cm.Get<SystemConfig>("System", validate: false); var sys = cm.Get<SystemConfig>("System", false);
if (opt.BaseAddress is null) if (opt.BaseAddress is null)
{ {
@@ -23,10 +24,7 @@ public static class ConfigurationExtensions
opt.BaseAddress = new Uri(baseUrl); opt.BaseAddress = new Uri(baseUrl);
} }
if (opt.Timeout.TotalSeconds <= 0) if (opt.Timeout.TotalSeconds <= 0) opt.Timeout = TimeSpan.FromSeconds(Math.Max(1, sys.TimeoutSeconds));
{
opt.Timeout = TimeSpan.FromSeconds(Math.Max(1, sys.TimeoutSeconds));
}
// Delegate to existing AddNetClient(Action<NetClientOptions>) // Delegate to existing AddNetClient(Action<NetClientOptions>)
services.AddNetClient(o => services.AddNetClient(o =>
@@ -44,4 +42,4 @@ public static class ConfigurationExtensions
return services; return services;
} }
} }
@@ -21,7 +21,8 @@ public static class ServiceCollectionExtensions
{ {
services.TryAddTransient<LoggingHandler>(); services.TryAddTransient<LoggingHandler>();
services.TryAddTransient<BearerTokenAuthenticationHandler>(); services.TryAddTransient<BearerTokenAuthenticationHandler>();
services.TryAddSingleton<IHttpContentSerializer>(sp => new SystemTextJsonContentSerializer(Common.Utils.JsonHelper.CamelCaseOptions)); services.TryAddSingleton<IHttpContentSerializer>(sp =>
new SystemTextJsonContentSerializer(Common.Utils.JsonHelper.CamelCaseOptions));
services.TryAddSingleton<INetClientFactory, NetClientFactory>(); services.TryAddSingleton<INetClientFactory, NetClientFactory>();
return services; return services;
} }
@@ -32,7 +33,8 @@ public static class ServiceCollectionExtensions
var options = new NetClientOptions(); var options = new NetClientOptions();
configure(options); configure(options);
var builder = services.AddHttpClient<IHttpClient, DefaultNetClient>(client => ApplyClientOptions(client, options)); var builder =
services.AddHttpClient<IHttpClient, DefaultNetClient>(client => ApplyClientOptions(client, options));
if (options.EnableLogging) if (options.EnableLogging)
builder.AddHttpMessageHandler<LoggingHandler>(); builder.AddHttpMessageHandler<LoggingHandler>();
@@ -49,7 +51,8 @@ public static class ServiceCollectionExtensions
return builder; return builder;
} }
public static IHttpClientBuilder AddNamedNetClient(this IServiceCollection services, string name, Action<NetClientOptions> configure) public static IHttpClientBuilder AddNamedNetClient(this IServiceCollection services, string name,
Action<NetClientOptions> configure)
{ {
services.AddNetClientCore(); services.AddNetClientCore();
var options = new NetClientOptions(); var options = new NetClientOptions();
@@ -96,15 +99,11 @@ public static class ServiceCollectionExtensions
.HandleTransientHttpError(); // 5xx + HttpRequestException + 408 .HandleTransientHttpError(); // 5xx + HttpRequestException + 408
if (retry.Handle429) if (retry.Handle429)
{
policyBuilder = policyBuilder.OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests); policyBuilder = policyBuilder.OrResult(msg => msg.StatusCode == HttpStatusCode.TooManyRequests);
}
if (retry.HandleRequestTimeout) if (retry.HandleRequestTimeout)
{
policyBuilder = policyBuilder.OrResult(msg => msg.StatusCode == HttpStatusCode.RequestTimeout); policyBuilder = policyBuilder.OrResult(msg => msg.StatusCode == HttpStatusCode.RequestTimeout);
}
return policyBuilder.WaitAndRetryAsync(delay); return policyBuilder.WaitAndRetryAsync(delay);
} }
} }
@@ -17,5 +17,4 @@ public class ApiException : Exception
StatusCode = response.StatusCode; StatusCode = response.StatusCode;
ResponseContent = content; ResponseContent = content;
} }
} }
@@ -15,18 +15,16 @@ public class BearerTokenAuthenticationHandler : DelegatingHandler
_tokenProvider = tokenProvider; _tokenProvider = tokenProvider;
} }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{ {
if (request.Headers.Authorization is null) if (request.Headers.Authorization is null)
{ {
var token = await _tokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false); var token = await _tokenProvider.GetTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(token)) if (!string.IsNullOrWhiteSpace(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
} }
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
} }
} }
@@ -14,13 +14,10 @@ public class CorrelationIdHandler : DelegatingHandler
_headerName = headerName; _headerName = headerName;
} }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{ {
if (!request.Headers.Contains(_headerName)) if (!request.Headers.Contains(_headerName)) request.Headers.Add(_headerName, Guid.NewGuid().ToString("N"));
{
request.Headers.Add(_headerName, Guid.NewGuid().ToString("N"));
}
return base.SendAsync(request, cancellationToken); return base.SendAsync(request, cancellationToken);
} }
} }
@@ -16,7 +16,8 @@ public class LoggingHandler : DelegatingHandler
_logger = logger; _logger = logger;
} }
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
try try
@@ -24,15 +25,16 @@ public class LoggingHandler : DelegatingHandler
_logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri); _logger.LogDebug("HTTP {Method} {Uri}", request.Method, request.RequestUri);
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
sw.Stop(); sw.Stop();
_logger.LogDebug("HTTP {Method} {Uri} -> {Status} in {Elapsed} ms", request.Method, request.RequestUri, (int)response.StatusCode, sw.ElapsedMilliseconds); _logger.LogDebug("HTTP {Method} {Uri} -> {Status} in {Elapsed} ms", request.Method, request.RequestUri,
(int)response.StatusCode, sw.ElapsedMilliseconds);
return response; return response;
} }
catch (Exception ex) catch (Exception ex)
{ {
sw.Stop(); sw.Stop();
_logger.LogError(ex, "HTTP {Method} {Uri} failed after {Elapsed} ms", request.Method, request.RequestUri, sw.ElapsedMilliseconds); _logger.LogError(ex, "HTTP {Method} {Uri} failed after {Elapsed} ms", request.Method, request.RequestUri,
sw.ElapsedMilliseconds);
throw; throw;
} }
} }
} }
@@ -26,8 +26,11 @@ public class DefaultNetClient : IHttpClient
_logger = logger; _logger = logger;
} }
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
=> _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); CancellationToken cancellationToken = default)
{
return _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
}
public async Task<T?> GetAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<T?> GetAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default)
{ {
@@ -39,7 +42,8 @@ public class DefaultNetClient : IHttpClient
return await _serializer.DeserializeAsync<T>(stream, cancellationToken).ConfigureAwait(false); return await _serializer.DeserializeAsync<T>(stream, cancellationToken).ConfigureAwait(false);
} }
public async Task<string> GetStringAsync(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<string> GetStringAsync(string path, object? query = null,
CancellationToken cancellationToken = default)
{ {
var uri = BuildUri(path, query); var uri = BuildUri(path, query);
using var req = new HttpRequestMessage(HttpMethod.Get, uri); using var req = new HttpRequestMessage(HttpMethod.Get, uri);
@@ -48,7 +52,8 @@ public class DefaultNetClient : IHttpClient
return await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); return await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
} }
public async Task<byte[]> GetBytesAsync(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<byte[]> GetBytesAsync(string path, object? query = null,
CancellationToken cancellationToken = default)
{ {
var uri = BuildUri(path, query); var uri = BuildUri(path, query);
using var req = new HttpRequestMessage(HttpMethod.Get, uri); using var req = new HttpRequestMessage(HttpMethod.Get, uri);
@@ -57,14 +62,23 @@ public class DefaultNetClient : IHttpClient
return await resp.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); return await resp.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
} }
public Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default) public Task<TResponse?> PostAsync<TRequest, TResponse>(string path, TRequest body,
=> SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Post, path, body, cancellationToken); CancellationToken cancellationToken = default)
{
return SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Post, path, body, cancellationToken);
}
public Task<TResponse?> PutAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default) public Task<TResponse?> PutAsync<TRequest, TResponse>(string path, TRequest body,
=> SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Put, path, body, cancellationToken); CancellationToken cancellationToken = default)
{
return SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Put, path, body, cancellationToken);
}
public Task<TResponse?> PatchAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default) public Task<TResponse?> PatchAsync<TRequest, TResponse>(string path, TRequest body,
=> SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Patch, path, body, cancellationToken); CancellationToken cancellationToken = default)
{
return SendWithBodyAsync<TRequest, TResponse>(HttpMethod.Patch, path, body, cancellationToken);
}
public async Task DeleteAsync(string path, object? query = null, CancellationToken cancellationToken = default) public async Task DeleteAsync(string path, object? query = null, CancellationToken cancellationToken = default)
{ {
@@ -74,7 +88,8 @@ public class DefaultNetClient : IHttpClient
await EnsureSuccess(resp).ConfigureAwait(false); await EnsureSuccess(resp).ConfigureAwait(false);
} }
public async Task<TResponse?> PostMultipartAsync<TResponse>(string path, Action<MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default) public async Task<TResponse?> PostMultipartAsync<TResponse>(string path,
Action<MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default)
{ {
var uri = BuildUri(path, null); var uri = BuildUri(path, null);
using var content = new MultipartFormDataContent(); using var content = new MultipartFormDataContent();
@@ -86,7 +101,8 @@ public class DefaultNetClient : IHttpClient
return await _serializer.DeserializeAsync<TResponse>(stream, cancellationToken).ConfigureAwait(false); return await _serializer.DeserializeAsync<TResponse>(stream, cancellationToken).ConfigureAwait(false);
} }
public async Task<Result<T>> GetResultAsync<T>(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<Result<T>> GetResultAsync<T>(string path, object? query = null,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -103,7 +119,8 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result<string>> GetStringResultAsync(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<Result<string>> GetStringResultAsync(string path, object? query = null,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -120,7 +137,8 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result<byte[]>> GetBytesResultAsync(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<Result<byte[]>> GetBytesResultAsync(string path, object? query = null,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -137,7 +155,8 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result<TResponse>> PostResultAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default) public async Task<Result<TResponse>> PostResultAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -154,7 +173,8 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result<TResponse>> PutResultAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default) public async Task<Result<TResponse>> PutResultAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -171,7 +191,8 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result<TResponse>> PatchResultAsync<TRequest, TResponse>(string path, TRequest body, CancellationToken cancellationToken = default) public async Task<Result<TResponse>> PatchResultAsync<TRequest, TResponse>(string path, TRequest body,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -188,7 +209,8 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result> DeleteResultAsync(string path, object? query = null, CancellationToken cancellationToken = default) public async Task<Result> DeleteResultAsync(string path, object? query = null,
CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -205,11 +227,13 @@ public class DefaultNetClient : IHttpClient
} }
} }
public async Task<Result<TResponse>> PostMultipartResultAsync<TResponse>(string path, Action<MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default) public async Task<Result<TResponse>> PostMultipartResultAsync<TResponse>(string path,
Action<MultipartFormDataContent> buildContent, CancellationToken cancellationToken = default)
{ {
try try
{ {
var value = await PostMultipartAsync<TResponse>(path, buildContent, cancellationToken).ConfigureAwait(false); var value = await PostMultipartAsync<TResponse>(path, buildContent, cancellationToken)
.ConfigureAwait(false);
return Result.Success(value!); return Result.Success(value!);
} }
catch (ApiException ex) catch (ApiException ex)
@@ -243,7 +267,9 @@ public class DefaultNetClient : IHttpClient
string? content = null; string? content = null;
try try
{ {
content = response.Content is null ? null : await response.Content.ReadAsStringAsync().ConfigureAwait(false); content = response.Content is null
? null
: await response.Content.ReadAsStringAsync().ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -253,18 +279,20 @@ public class DefaultNetClient : IHttpClient
throw new ApiException($"Request failed with status {(int)response.StatusCode}", response, content); throw new ApiException($"Request failed with status {(int)response.StatusCode}", response, content);
} }
private async Task<TResponse?> SendWithBodyAsync<TRequest, TResponse>(HttpMethod method, string path, TRequest body, CancellationToken cancellationToken) private async Task<TResponse?> SendWithBodyAsync<TRequest, TResponse>(HttpMethod method, string path, TRequest body,
CancellationToken cancellationToken)
{ {
var uri = BuildUri(path, null); var uri = BuildUri(path, null);
using var ms = new MemoryStream(); using var ms = new MemoryStream();
await _serializer.SerializeAsync(ms, body, cancellationToken).ConfigureAwait(false); await _serializer.SerializeAsync(ms, body, cancellationToken).ConfigureAwait(false);
ms.Position = 0; ms.Position = 0;
using var content = new StreamContent(ms); using var content = new StreamContent(ms);
content.Headers.ContentType = new MediaTypeHeaderValue(_serializer.ContentType) { CharSet = Encoding.UTF8.WebName }; content.Headers.ContentType = new MediaTypeHeaderValue(_serializer.ContentType)
{ CharSet = Encoding.UTF8.WebName };
using var req = new HttpRequestMessage(method, uri) { Content = content }; using var req = new HttpRequestMessage(method, uri) { Content = content };
using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false); using var resp = await SendAsync(req, cancellationToken).ConfigureAwait(false);
await EnsureSuccess(resp).ConfigureAwait(false); await EnsureSuccess(resp).ConfigureAwait(false);
await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); await using var stream = await resp.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await _serializer.DeserializeAsync<TResponse>(stream, cancellationToken).ConfigureAwait(false); return await _serializer.DeserializeAsync<TResponse>(stream, cancellationToken).ConfigureAwait(false);
} }
} }
@@ -16,11 +16,10 @@ public class NetClientFactory : INetClientFactory
public IHttpClient Create(string name) public IHttpClient Create(string name)
{ {
var httpFactory = _services.GetRequiredService<System.Net.Http.IHttpClientFactory>(); var httpFactory = _services.GetRequiredService<IHttpClientFactory>();
var client = httpFactory.CreateClient(name); var client = httpFactory.CreateClient(name);
var serializer = _services.GetRequiredService<IHttpContentSerializer>(); var serializer = _services.GetRequiredService<IHttpContentSerializer>();
var logger = _services.GetRequiredService<ILogger<DefaultNetClient>>(); var logger = _services.GetRequiredService<ILogger<DefaultNetClient>>();
return new DefaultNetClient(client, serializer, logger); return new DefaultNetClient(client, serializer, logger);
} }
} }
@@ -27,5 +27,4 @@ internal class SystemTextJsonContentSerializer : IHttpContentSerializer
{ {
return await JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken).ConfigureAwait(false); return await JsonSerializer.DeserializeAsync<T>(stream, _options, cancellationToken).ConfigureAwait(false);
} }
} }
@@ -14,10 +14,7 @@ internal static class QueryStringHelper
{ {
if (query is null) return string.Empty; if (query is null) return string.Empty;
if (query is IEnumerable<KeyValuePair<string, object?>> pairs) if (query is IEnumerable<KeyValuePair<string, object?>> pairs) return BuildFromPairs(pairs);
{
return BuildFromPairs(pairs);
}
var dict = new Dictionary<string, object?>(); var dict = new Dictionary<string, object?>();
foreach (var prop in query.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) foreach (var prop in query.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
@@ -33,6 +30,7 @@ internal static class QueryStringHelper
private static string BuildFromPairs(IEnumerable<KeyValuePair<string, object?>> pairs) private static string BuildFromPairs(IEnumerable<KeyValuePair<string, object?>> pairs)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
void AppendPair(string key, string value) void AppendPair(string key, string value)
{ {
if (sb.Length > 0) sb.Append('&'); if (sb.Length > 0) sb.Append('&');
@@ -48,21 +46,15 @@ internal static class QueryStringHelper
if (val is null) continue; if (val is null) continue;
if (val is string s) if (val is string s)
{
AppendPair(key, s); AppendPair(key, s);
}
else if (val is IEnumerable enumerable && val is not byte[]) else if (val is IEnumerable enumerable && val is not byte[])
{
foreach (var item in enumerable) foreach (var item in enumerable)
{ {
if (item is null) continue; if (item is null) continue;
AppendPair(key, ConvertToString(item)); AppendPair(key, ConvertToString(item));
} }
}
else else
{
AppendPair(key, ConvertToString(val)); AppendPair(key, ConvertToString(val));
}
} }
return sb.ToString(); return sb.ToString();
@@ -79,5 +71,4 @@ internal static class QueryStringHelper
_ => value.ToString() ?? string.Empty _ => value.ToString() ?? string.Empty
}; };
} }
} }
+19 -19
View File
@@ -1,27 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- 程序集元数据 --> <!-- 程序集元数据 -->
<Description>Sharp Framework HTTP 客户端库 - 集成 Polly 弹性策略的 HTTP 客户端</Description> <Description>Sharp Framework HTTP 客户端库 - 集成 Polly 弹性策略的 HTTP 客户端</Description>
<!-- NuGet 包元数据 --> <!-- NuGet 包元数据 -->
<PackageTags>sharp;http;client;polly;resilience</PackageTags> <PackageTags>sharp;http;client;polly;resilience</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0"/>
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" /> <PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj"/>
<ProjectReference Include="..\Config\Config.csproj" /> <ProjectReference Include="..\Config\Config.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -18,4 +18,4 @@ public class NetClientOptions
public RetryOptions Retry { get; set; } = new(); public RetryOptions Retry { get; set; } = new();
public JsonSerializerOptions JsonSerializerOptions { get; set; } = Common.Utils.JsonHelper.CamelCaseOptions; public JsonSerializerOptions JsonSerializerOptions { get; set; } = Common.Utils.JsonHelper.CamelCaseOptions;
} }
+1 -2
View File
@@ -10,5 +10,4 @@ public class RetryOptions
public int MaxJitterMs { get; set; } = 100; public int MaxJitterMs { get; set; } = 100;
public bool Handle429 { get; set; } = true; public bool Handle429 { get; set; } = true;
public bool HandleRequestTimeout { get; set; } = true; // 408 public bool HandleRequestTimeout { get; set; } = true; // 408
} }
+6
View File
@@ -57,12 +57,14 @@ NetServer/
### 1. HTTP 服务器 (Server/) ### 1. HTTP 服务器 (Server/)
**HttpServer.cs** **HttpServer.cs**
- 基于 `HttpListener` 的 HTTP 服务器实现 - 基于 `HttpListener` 的 HTTP 服务器实现
- 管理中间件管道和路由表 - 管理中间件管道和路由表
- 处理请求的并发执行 - 处理请求的并发执行
- 支持依赖注入 - 支持依赖注入
**HttpServerOptions.cs** **HttpServerOptions.cs**
- 服务器配置选项类 - 服务器配置选项类
- 支持 Fluent API 配置 - 支持 Fluent API 配置
- 管理中间件和控制器程序集 - 管理中间件和控制器程序集
@@ -70,18 +72,21 @@ NetServer/
### 2. 路由系统 (Routing/) ### 2. 路由系统 (Routing/)
**RouteTable.cs** **RouteTable.cs**
- 路由注册和管理 - 路由注册和管理
- 基于特性的路由发现 - 基于特性的路由发现
- 路由匹配(支持路径参数) - 路由匹配(支持路径参数)
- 路由元数据收集 - 路由元数据收集
**RouteInfo.cs** **RouteInfo.cs**
- 路由信息数据模型 - 路由信息数据模型
- 包含授权、限流、缓存等配置 - 包含授权、限流、缓存等配置
### 3. 中间件系统 (Middleware/) ### 3. 中间件系统 (Middleware/)
**执行顺序:** **执行顺序:**
1. ExceptionHandling → 全局异常处理 1. ExceptionHandling → 全局异常处理
2. Logging → 请求日志 2. Logging → 请求日志
3. CORS → 跨域处理 3. CORS → 跨域处理
@@ -96,6 +101,7 @@ NetServer/
**BaseController.cs** **BaseController.cs**
提供以下辅助方法: 提供以下辅助方法:
- 请求读取:`GetQueryParam()`, `ReadJsonBodyAsync()` - 请求读取:`GetQueryParam()`, `ReadJsonBodyAsync()`
- 响应写入:`WriteJsonResponseAsync()`, `WriteSuccessResponseAsync()` - 响应写入:`WriteJsonResponseAsync()`, `WriteSuccessResponseAsync()`
- 快速响应:`NotFoundAsync()`, `UnauthorizedAsync()` - 快速响应:`NotFoundAsync()`, `UnauthorizedAsync()`
@@ -27,4 +27,4 @@ public interface ITokenProvider
/// 验证 Token /// 验证 Token
/// </summary> /// </summary>
ClaimsPrincipal? ValidateToken(string token); ClaimsPrincipal? ValidateToken(string token);
} }
@@ -16,4 +16,4 @@ public interface IMiddleware
/// <summary> /// <summary>
/// 中间件委托 /// 中间件委托
/// </summary> /// </summary>
public delegate Task RequestDelegate(HttpContext context); public delegate Task RequestDelegate(HttpContext context);
@@ -28,4 +28,4 @@ public class AuthorizeAttribute : Attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class AllowAnonymousAttribute : Attribute public class AllowAnonymousAttribute : Attribute
{ {
} }
@@ -46,4 +46,4 @@ public class ResponseCacheAttribute : Attribute
{ {
Duration = duration; Duration = duration;
} }
} }
@@ -38,4 +38,4 @@ public class EnableCorsAttribute : Attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class DisableCorsAttribute : Attribute public class DisableCorsAttribute : Attribute
{ {
} }
@@ -21,7 +21,9 @@ public abstract class HttpMethodAttribute : Attribute
/// </summary> /// </summary>
public class HttpGetAttribute : HttpMethodAttribute public class HttpGetAttribute : HttpMethodAttribute
{ {
public HttpGetAttribute(string? template = null) : base("GET", template) { } public HttpGetAttribute(string? template = null) : base("GET", template)
{
}
} }
/// <summary> /// <summary>
@@ -29,7 +31,9 @@ public class HttpGetAttribute : HttpMethodAttribute
/// </summary> /// </summary>
public class HttpPostAttribute : HttpMethodAttribute public class HttpPostAttribute : HttpMethodAttribute
{ {
public HttpPostAttribute(string? template = null) : base("POST", template) { } public HttpPostAttribute(string? template = null) : base("POST", template)
{
}
} }
/// <summary> /// <summary>
@@ -37,7 +41,9 @@ public class HttpPostAttribute : HttpMethodAttribute
/// </summary> /// </summary>
public class HttpPutAttribute : HttpMethodAttribute public class HttpPutAttribute : HttpMethodAttribute
{ {
public HttpPutAttribute(string? template = null) : base("PUT", template) { } public HttpPutAttribute(string? template = null) : base("PUT", template)
{
}
} }
/// <summary> /// <summary>
@@ -45,7 +51,9 @@ public class HttpPutAttribute : HttpMethodAttribute
/// </summary> /// </summary>
public class HttpDeleteAttribute : HttpMethodAttribute public class HttpDeleteAttribute : HttpMethodAttribute
{ {
public HttpDeleteAttribute(string? template = null) : base("DELETE", template) { } public HttpDeleteAttribute(string? template = null) : base("DELETE", template)
{
}
} }
/// <summary> /// <summary>
@@ -53,7 +61,9 @@ public class HttpDeleteAttribute : HttpMethodAttribute
/// </summary> /// </summary>
public class HttpPatchAttribute : HttpMethodAttribute public class HttpPatchAttribute : HttpMethodAttribute
{ {
public HttpPatchAttribute(string? template = null) : base("PATCH", template) { } public HttpPatchAttribute(string? template = null) : base("PATCH", template)
{
}
} }
/// <summary> /// <summary>
@@ -61,7 +71,9 @@ public class HttpPatchAttribute : HttpMethodAttribute
/// </summary> /// </summary>
public class HttpHeadAttribute : HttpMethodAttribute public class HttpHeadAttribute : HttpMethodAttribute
{ {
public HttpHeadAttribute(string? template = null) : base("HEAD", template) { } public HttpHeadAttribute(string? template = null) : base("HEAD", template)
{
}
} }
/// <summary> /// <summary>
@@ -69,5 +81,7 @@ public class HttpHeadAttribute : HttpMethodAttribute
/// </summary> /// </summary>
public class HttpOptionsAttribute : HttpMethodAttribute public class HttpOptionsAttribute : HttpMethodAttribute
{ {
public HttpOptionsAttribute(string? template = null) : base("OPTIONS", template) { } public HttpOptionsAttribute(string? template = null) : base("OPTIONS", template)
} {
}
}
@@ -59,4 +59,4 @@ public class RateLimitAttribute : Attribute
Scope = scope; Scope = scope;
WindowSeconds = windowSeconds; WindowSeconds = windowSeconds;
} }
} }
@@ -20,4 +20,4 @@ public class RouteAttribute : Attribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ApiControllerAttribute : Attribute public class ApiControllerAttribute : Attribute
{ {
} }
@@ -60,7 +60,7 @@ public abstract class BaseController
context.Response.StatusCode = statusCode; context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json; charset=utf-8"; context.Response.ContentType = "application/json; charset=utf-8";
var json = JsonHelper.ToJson(data, useCompact: true); var json = JsonHelper.ToJson(data, true);
var bytes = Encoding.UTF8.GetBytes(json); var bytes = Encoding.UTF8.GetBytes(json);
context.Response.ContentLength = bytes.Length; context.Response.ContentLength = bytes.Length;
@@ -70,7 +70,8 @@ public abstract class BaseController
/// <summary> /// <summary>
/// 写入文本响应 /// 写入文本响应
/// </summary> /// </summary>
protected async Task WriteTextResponseAsync(HttpContext context, string text, int statusCode = 200, string contentType = "text/plain") protected async Task WriteTextResponseAsync(HttpContext context, string text, int statusCode = 200,
string contentType = "text/plain")
{ {
context.Response.StatusCode = statusCode; context.Response.StatusCode = statusCode;
context.Response.ContentType = $"{contentType}; charset=utf-8"; context.Response.ContentType = $"{contentType}; charset=utf-8";
@@ -91,7 +92,8 @@ public abstract class BaseController
/// <summary> /// <summary>
/// 写入错误响应 /// 写入错误响应
/// </summary> /// </summary>
protected async Task WriteErrorResponseAsync(HttpContext context, string message, int statusCode = 500, long? errorCode = null) protected async Task WriteErrorResponseAsync(HttpContext context, string message, int statusCode = 500,
long? errorCode = null)
{ {
var response = ResultExtensions.HttpFailure<object>(message, errorCode); var response = ResultExtensions.HttpFailure<object>(message, errorCode);
await WriteJsonResponseAsync(context, response, statusCode); await WriteJsonResponseAsync(context, response, statusCode);
@@ -153,10 +155,7 @@ public abstract class BaseController
protected void SetCookie(HttpContext context, string name, string value, int? expiresInSeconds = null) protected void SetCookie(HttpContext context, string name, string value, int? expiresInSeconds = null)
{ {
var cookie = new System.Net.Cookie(name, value); var cookie = new System.Net.Cookie(name, value);
if (expiresInSeconds.HasValue) if (expiresInSeconds.HasValue) cookie.Expires = DateTime.UtcNow.AddSeconds(expiresInSeconds.Value);
{
cookie.Expires = DateTime.UtcNow.AddSeconds(expiresInSeconds.Value);
}
context.Response.Cookies.Add(cookie); context.Response.Cookies.Add(cookie);
} }
@@ -180,4 +179,4 @@ public abstract class BaseController
}; };
context.Response.Cookies.Add(cookie); context.Response.Cookies.Add(cookie);
} }
} }
@@ -12,10 +12,11 @@ public static class ServiceCollectionExtensions
/// Adds HttpServerOptions bound from ConfigManager (section "NetServer"). /// Adds HttpServerOptions bound from ConfigManager (section "NetServer").
/// If no ListenUrls specified, uses SystemConfig to build a default base URL. /// If no ListenUrls specified, uses SystemConfig to build a default base URL.
/// </summary> /// </summary>
public static IServiceCollection AddNetServer(this IServiceCollection services, ConfigManager cm, string section = "NetServer") public static IServiceCollection AddNetServer(this IServiceCollection services, ConfigManager cm,
string section = "NetServer")
{ {
var options = cm.Get<HttpServerOptions>(section, validate: false) ?? new HttpServerOptions(); var options = cm.Get<HttpServerOptions>(section, false) ?? new HttpServerOptions();
var sys = cm.Get<SystemConfig>("System", validate: false); var sys = cm.Get<SystemConfig>("System", false);
if (options.ListenUrls == null || options.ListenUrls.Count == 0) if (options.ListenUrls == null || options.ListenUrls.Count == 0)
{ {
@@ -26,4 +27,4 @@ public static class ServiceCollectionExtensions
services.TryAddSingleton(options); services.TryAddSingleton(options);
return services; return services;
} }
} }
@@ -108,4 +108,4 @@ public class DemoController : BaseController
message = "CORS enabled for this endpoint" message = "CORS enabled for this endpoint"
}); });
} }
} }
+1 -1
View File
@@ -95,4 +95,4 @@ public class Program
Console.WriteLine("服务器已停止。"); Console.WriteLine("服务器已停止。");
} }
} }
@@ -90,5 +90,8 @@ public class HttpResult<T>
/// <summary> /// <summary>
/// 获取原始 Result /// 获取原始 Result
/// </summary> /// </summary>
public Result<T> ToResult() => _result; public Result<T> ToResult()
} {
return _result;
}
}
Loaded 100 of 342 files, more files were not shown because too many files have changed in this diff. Show more