U:调整所有代码格式
This commit is contained in:
1 parent
d133bacf55
commit
155b67e831
342 files changed
+9431
-9868
No files matched your search
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 _);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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. **缓存预加载**(后台任务)
|
||||||
|
|||||||
@@ -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<T> 模式、Global 容器、系统配置等基础功能</Description>
|
<Description>Sharp Framework 核心库 - 提供 Result<T> 模式、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()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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];
|
||||||
|
|||||||
@@ -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
@@ -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 模式(完整的函数式错误处理)
|
||||||
- ✅ 自定义异常(带错误码)
|
- ✅ 自定义异常(带错误码)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,4 @@ public enum ConfigFormat
|
|||||||
Ini,
|
Ini,
|
||||||
Xml,
|
Xml,
|
||||||
Toml
|
Toml
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,4 +22,4 @@ public interface IDataContext : IDisposable
|
|||||||
/// 访问底层 DbContext(高级场景)
|
/// 访问底层 DbContext(高级场景)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
DbContext Context { get; }
|
DbContext Context { get; }
|
||||||
}
|
}
|
||||||
+27
-27
@@ -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>
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,4 +65,4 @@ public abstract class AuditableEntityInt : AuditableEntity<int>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AuditableEntityLong : AuditableEntity<long>
|
public abstract class AuditableEntityLong : AuditableEntity<long>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -24,4 +24,4 @@ public interface IAuditableEntity
|
|||||||
/// 最后修改人
|
/// 最后修改人
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string? UpdatedBy { get; set; }
|
string? UpdatedBy { get; set; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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` 接口
|
||||||
- 完全不使用审计和软删除功能
|
- 完全不使用审计和软删除功能
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -13,4 +13,4 @@ public class EventBusOptions
|
|||||||
/// 全局错误回调。
|
/// 全局错误回调。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<Exception, object>? OnError { get; set; }
|
public Action<Exception, object>? OnError { get; set; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,5 +23,4 @@ internal sealed class ActivityEnricher : ILogEventEnricher
|
|||||||
logEvent.AddOrUpdateProperty(spanIdProp);
|
logEvent.AddOrUpdateProperty(spanIdProp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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/` 集成扩展
|
||||||
|
|||||||
@@ -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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
Reference in new issue
Block a user