diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1693728ca..424bf8708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,14 +15,6 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - - name: Setup dotnet6.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - name: Setup dotnet7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 7.0.x - name: Setup dotnet8.0 uses: actions/setup-dotnet@v1 with: @@ -30,7 +22,11 @@ jobs: - name: Setup dotnet9.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 9.0.x + dotnet-version: 9.0.x + - name: Setup dotnet10.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 10.0.x - name: Linux Build run: | dotnet build ./src/OSharp.Utils/OSharp.Utils.csproj @@ -60,14 +56,6 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 - - name: Setup dotnet6.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - name: Setup dotnet7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 7.0.x - name: Setup dotnet8.0 uses: actions/setup-dotnet@v1 with: @@ -75,7 +63,11 @@ jobs: - name: Setup dotnet9.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 9.0.x + dotnet-version: 9.0.x + - name: Setup dotnet10.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 10.0.x - name: Windows Build run: | dotnet build ./src/OSharp.Wpf/OSharp.Wpf.csproj diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index b0369f63b..6100ea086 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -15,14 +15,6 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v2 - - name: Setup dotnet6.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - name: Setup dotnet7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 7.0.x - name: Setup dotnet8.0 uses: actions/setup-dotnet@v1 with: @@ -31,6 +23,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: 9.0.x + - name: Setup dotnet10.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 10.0.x - name: Restore run: dotnet restore - name: Build and Pack diff --git a/build/OSharpNS.nuspec b/build/OSharpNS.nuspec index 81502397d..f16c23fea 100644 --- a/build/OSharpNS.nuspec +++ b/build/OSharpNS.nuspec @@ -1,37 +1,29 @@ - + + - - OSharpNS - 9.0.0-preview.304 - OSharpFramework(.NET6.0/.NETCoreApp3.1) - 柳柳软件(66soft.net) - LiuliuSoft nnc - false - Apache-2.0 - icon.png - https://github.com/dotnetcore/osharp - OSharp Framework with .NET,此Package包含了OSharp的所有常用组件 - https://github.com/dotnetcore/osharp/releases - Copyright (c) 2014-2022 LIULIUSOFT. All rights reserved. - osharp - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + OSharpNS + 10.0.0 + OSharpFramework + 柳柳软件(66soft.net) + LiuliuSoft nnc + false + Apache-2.0 + icon.png + https://github.com/dotnetcore/osharp + OSharp Framework with .NET,此Package包含了OSharp的所有常用组件 + https://github.com/dotnetcore/osharp/releases + Copyright (c) 2014-2026 LIULIUSOFT. All rights reserved. + osharp + + + + + + + + + + + + diff --git a/build/nuget-push.ps1 b/build/nuget-push.ps1 index 4f5cf293b..e99682e56 100644 --- a/build/nuget-push.ps1 +++ b/build/nuget-push.ps1 @@ -19,7 +19,7 @@ function GetVersion() $server = "https://www.nuget.org" $readkey = Read-Host "默认服务器为nuget.org,确认按回车键`n如要切换为nuget.66soft.net,按 1`n如要切换为ncc.myget.org,按 2`n如要切换为osharp.myget.org,按3" if ($readkey -eq 1) { - $server = "http://nuget.66soft.net/nuget" + $server = "https://nuget.66soft.net/nuget" } elseif ($readkey -eq 2) { $server = "https://www.myget.org/F/ncc/api/v2/package" diff --git a/build/version.props b/build/version.props index 32bc59fc5..168b61acb 100644 --- a/build/version.props +++ b/build/version.props @@ -1,12 +1,12 @@ - 9.0 + 10.0 0 -preview. - 907 - $(VersionMain).$(VersionPrefix)$(VersionSuffix)$(VersionSuffixVersion) - $(VersionMain).$(VersionPrefix).$(VersionSuffixVersion) - + 117 + + $(VersionMain).$(VersionPrefix) + $(VersionMain).$(VersionPrefix) diff --git a/global.json b/global.json index 00b67caef..fe7e453b1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "10.0.100", "rollForward": "latestFeature", "allowPrerelease": false } diff --git a/samples/web/Liuliu.Demo.Core/Liuliu.Demo.Core.csproj b/samples/web/Liuliu.Demo.Core/Liuliu.Demo.Core.csproj index 9953da691..3b8f74969 100644 --- a/samples/web/Liuliu.Demo.Core/Liuliu.Demo.Core.csproj +++ b/samples/web/Liuliu.Demo.Core/Liuliu.Demo.Core.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 Liuliu.Demo false diff --git a/samples/web/Liuliu.Demo.EntityConfiguration/Liuliu.Demo.EntityConfiguration.csproj b/samples/web/Liuliu.Demo.EntityConfiguration/Liuliu.Demo.EntityConfiguration.csproj index df9841777..945684ff5 100644 --- a/samples/web/Liuliu.Demo.EntityConfiguration/Liuliu.Demo.EntityConfiguration.csproj +++ b/samples/web/Liuliu.Demo.EntityConfiguration/Liuliu.Demo.EntityConfiguration.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 false diff --git a/samples/web/Liuliu.Demo.Web/Liuliu.Demo.Web.csproj b/samples/web/Liuliu.Demo.Web/Liuliu.Demo.Web.csproj index a3aa93031..e6178f9be 100644 --- a/samples/web/Liuliu.Demo.Web/Liuliu.Demo.Web.csproj +++ b/samples/web/Liuliu.Demo.Web/Liuliu.Demo.Web.csproj @@ -16,22 +16,6 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - @@ -48,6 +32,14 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/OSharp.AspNetCore/OSharp.AspNetCore.csproj b/src/OSharp.AspNetCore/OSharp.AspNetCore.csproj index b7c412276..c3f41ee18 100644 --- a/src/OSharp.AspNetCore/OSharp.AspNetCore.csproj +++ b/src/OSharp.AspNetCore/OSharp.AspNetCore.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0 OSharp.AspNetCore OSharp AspNetCore组件 OSharp AspNetCore组件,提供AspNetCore的服务端功能的封装 @@ -15,16 +15,6 @@ - - - - - - - - - - @@ -35,6 +25,11 @@ + + + + + diff --git a/src/OSharp.Authorization.Datas/OSharp.Authorization.Datas.csproj b/src/OSharp.Authorization.Datas/OSharp.Authorization.Datas.csproj index 64f155d20..81d1a0f61 100644 --- a/src/OSharp.Authorization.Datas/OSharp.Authorization.Datas.csproj +++ b/src/OSharp.Authorization.Datas/OSharp.Authorization.Datas.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Authorization.Datas OSharp 数据权限组件 OSharp 数据权限组件,对应用中数据权限进行授权的设计实现 diff --git a/src/OSharp.Authorization.Functions/OSharp.Authorization.Functions.csproj b/src/OSharp.Authorization.Functions/OSharp.Authorization.Functions.csproj index 7d4a547bf..7e17aaa26 100644 --- a/src/OSharp.Authorization.Functions/OSharp.Authorization.Functions.csproj +++ b/src/OSharp.Authorization.Functions/OSharp.Authorization.Functions.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Authorization.Functions OSharp 功能权限组件 OSharp 功能权限组件,API功能权限授权的设计实现 diff --git a/src/OSharp.AutoMapper/OSharp.AutoMapper.csproj b/src/OSharp.AutoMapper/OSharp.AutoMapper.csproj index cae6b75e7..439d7ded8 100644 --- a/src/OSharp.AutoMapper/OSharp.AutoMapper.csproj +++ b/src/OSharp.AutoMapper/OSharp.AutoMapper.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.AutoMapper OSharp AutoMapper 对象映射组件 OSharp AutoMapper 对象映射组件,封装基于AutoMapper的对象映射实现 diff --git a/src/OSharp.EntityFrameworkCore.MySql/OSharp.EntityFrameworkCore.MySql.csproj b/src/OSharp.EntityFrameworkCore.MySql/OSharp.EntityFrameworkCore.MySql.csproj index e01ac7a6c..5bf7fa2a9 100644 --- a/src/OSharp.EntityFrameworkCore.MySql/OSharp.EntityFrameworkCore.MySql.csproj +++ b/src/OSharp.EntityFrameworkCore.MySql/OSharp.EntityFrameworkCore.MySql.csproj @@ -4,28 +4,21 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0 OSharp.EntityFrameworkCore.MySql OSharp 数据访问组件,封装EntityFrameworkCore的MySql数据访问功能的实现 OSharp 数据访问组件MySql OSharp.Entity.MySql - - - - - - - + - diff --git a/src/OSharp.EntityFrameworkCore.Oracle/OSharp.EntityFrameworkCore.Oracle.csproj b/src/OSharp.EntityFrameworkCore.Oracle/OSharp.EntityFrameworkCore.Oracle.csproj index 60c165185..2b80f7f4f 100644 --- a/src/OSharp.EntityFrameworkCore.Oracle/OSharp.EntityFrameworkCore.Oracle.csproj +++ b/src/OSharp.EntityFrameworkCore.Oracle/OSharp.EntityFrameworkCore.Oracle.csproj @@ -4,19 +4,13 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.EntityFrameworkCore.Oracle OSharp 数据访问组件,封装EntityFrameworkCore的Oracle数据访问功能的实现 OSharp 数据访问组件Oracle OSharp.Entity.Oracle - - - - - - @@ -25,6 +19,11 @@ + + + + + diff --git a/src/OSharp.EntityFrameworkCore.PostgreSql/OSharp.EntityFrameworkCore.PostgreSql.csproj b/src/OSharp.EntityFrameworkCore.PostgreSql/OSharp.EntityFrameworkCore.PostgreSql.csproj index 6b8d9775e..6e20e4f4b 100644 --- a/src/OSharp.EntityFrameworkCore.PostgreSql/OSharp.EntityFrameworkCore.PostgreSql.csproj +++ b/src/OSharp.EntityFrameworkCore.PostgreSql/OSharp.EntityFrameworkCore.PostgreSql.csproj @@ -4,25 +4,22 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.EntityFrameworkCore.PostgreSql OSharp 数据访问组件,封装EntityFrameworkCore的PostgreSql数据访问功能的实现 OSharp 数据访问组件PostgreSql OSharp.Entity.PostgreSql - - - - - - + + + diff --git a/src/OSharp.EntityFrameworkCore.SqlServer/OSharp.EntityFrameworkCore.SqlServer.csproj b/src/OSharp.EntityFrameworkCore.SqlServer/OSharp.EntityFrameworkCore.SqlServer.csproj index e8ef9975a..a446e62eb 100644 --- a/src/OSharp.EntityFrameworkCore.SqlServer/OSharp.EntityFrameworkCore.SqlServer.csproj +++ b/src/OSharp.EntityFrameworkCore.SqlServer/OSharp.EntityFrameworkCore.SqlServer.csproj @@ -4,25 +4,22 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.EntityFrameworkCore.SqlServer OSharp 数据访问组件,封装EntityFrameworkCore的SqlServer数据访问功能的实现 OSharp 数据访问组件SqlServer OSharp.Entity.SqlServer - - - - - - + + + diff --git a/src/OSharp.EntityFrameworkCore.Sqlite/OSharp.EntityFrameworkCore.Sqlite.csproj b/src/OSharp.EntityFrameworkCore.Sqlite/OSharp.EntityFrameworkCore.Sqlite.csproj index 74fe87d71..dfcb0f87d 100644 --- a/src/OSharp.EntityFrameworkCore.Sqlite/OSharp.EntityFrameworkCore.Sqlite.csproj +++ b/src/OSharp.EntityFrameworkCore.Sqlite/OSharp.EntityFrameworkCore.Sqlite.csproj @@ -4,25 +4,23 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.EntityFrameworkCore.Sqlite OSharp 数据访问组件,封装EntityFrameworkCore的Sqlite数据访问功能的实现 OSharp 数据访问组件Sqlite OSharp.Entity.Sqlite - - - - - - + + + + diff --git a/src/OSharp.EntityFrameworkCore/EntityManager.cs b/src/OSharp.EntityFrameworkCore/EntityManager.cs index 23c1d1010..b4bef0222 100644 --- a/src/OSharp.EntityFrameworkCore/EntityManager.cs +++ b/src/OSharp.EntityFrameworkCore/EntityManager.cs @@ -14,11 +14,12 @@ namespace OSharp.Entity; /// public class EntityManager : IEntityManager { - private readonly ConcurrentDictionary _entityRegistersDict - = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _entityRegistersDict = new(); private readonly ILogger _logger; private bool _initialized; + public static bool IncludeSpecialTable = true; + /// /// 初始化一个类型的新实例 /// @@ -33,7 +34,7 @@ public EntityManager(IServiceProvider provider) public virtual void Initialize() { var dict = _entityRegistersDict; - Type[] types = AssemblyManager.FindTypesByBase(); + Type[] types = AssemblyManager.FindTypesByBase().Where(m => !m.IsNestedPrivate).ToArray(); if (types.Length == 0 || _initialized) { _logger.LogDebug("数据库上下文实体已初始化,跳过"); @@ -48,14 +49,14 @@ public virtual void Initialize() foreach (IGrouping group in groups) { key = group.Key ?? typeof(DefaultDbContext); - List list = dict.ContainsKey(key) ? dict[key].ToList() : new List(); + List list = dict.TryGetValue(key, out var value) ? value.ToList() : new List(); list.AddRange(group); dict[key] = list.ToArray(); } //添加框架的一些默认实体的实体映射信息(如果不存在) key = typeof(DefaultDbContext); - if (dict.ContainsKey(key)) + if (dict.ContainsKey(key) && IncludeSpecialTable) { List list = dict[key].ToList(); list.AddIfNotExist(new EntityInfoConfiguration(), m => m.EntityType.IsBaseOn()); @@ -87,7 +88,7 @@ public virtual IEntityRegister[] GetEntityRegisters(Type dbContextType) { throw new OsharpException("数据访问模块未初始化,请确认数据上下文配置节点 OSharp:DbContexts 与要使用的数据库类型是否匹配"); } - return _entityRegistersDict.ContainsKey(dbContextType) ? _entityRegistersDict[dbContextType] : Array.Empty(); + return _entityRegistersDict.TryGetValue(dbContextType, out var value) ? value : Array.Empty(); } /// diff --git a/src/OSharp.EntityFrameworkCore/EntityTypeConfigurationBase.cs b/src/OSharp.EntityFrameworkCore/EntityTypeConfigurationBase.cs index 2a75d09a9..47c6a2bde 100644 --- a/src/OSharp.EntityFrameworkCore/EntityTypeConfigurationBase.cs +++ b/src/OSharp.EntityFrameworkCore/EntityTypeConfigurationBase.cs @@ -48,4 +48,4 @@ public void RegisterTo(ModelBuilder modelBuilder) /// /// 实体类型创建器 public abstract void Configure(EntityTypeBuilder builder); -} +} diff --git a/src/OSharp.EntityFrameworkCore/OSharp.EntityFrameworkCore.csproj b/src/OSharp.EntityFrameworkCore/OSharp.EntityFrameworkCore.csproj index 0b1bc06d2..51a9f6452 100644 --- a/src/OSharp.EntityFrameworkCore/OSharp.EntityFrameworkCore.csproj +++ b/src/OSharp.EntityFrameworkCore/OSharp.EntityFrameworkCore.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.EntityFrameworkCore OSharp数据访问组件,封装EntityFrameworkCore数据访问功能的实现 OSharp数据访问组件 @@ -12,29 +12,24 @@ 1701;1702;1591 - - - - - - - - - - - + + + - - + + + + + diff --git a/src/OSharp.EntityFrameworkCore/Repository.cs b/src/OSharp.EntityFrameworkCore/Repository.cs index cfba552f3..a92dd0956 100644 --- a/src/OSharp.EntityFrameworkCore/Repository.cs +++ b/src/OSharp.EntityFrameworkCore/Repository.cs @@ -734,8 +734,8 @@ public virtual async Task DeleteBatchAsync(Expression> { // 物理删除 count = await _dbSet.Where(predicate).DeleteAsync(_cancellationTokenProvider.Token); - } - + } + await unitOfWork.CommitAsync(_cancellationTokenProvider.Token); return count; } @@ -828,8 +828,8 @@ public virtual async Task UpdateBatchAsync(Expression> //走EF.Plus的时候,是不调用SaveChanges的,需要手动开启事务 await ((DbContextBase)_dbContext).BeginOrUseTransactionAsync(_cancellationTokenProvider.Token); - int count = await _dbSet.Where(predicate).UpdateAsync(updateExpression, _cancellationTokenProvider.Token); - + int count = await _dbSet.Where(predicate).UpdateAsync(updateExpression, _cancellationTokenProvider.Token); + await unitOfWork.CommitAsync(_cancellationTokenProvider.Token); return count; } @@ -937,16 +937,16 @@ private void CheckDataAuth(DataAuthOperation operation, params TEntity[] entitie if (entities.Length == 0 || _dataAuthService == null) { return; - } - + } + bool flag = _dataAuthService.CheckDataAuth(operation, entities); if (!flag) { throw new OsharpException( $"{operation.ToDescription()}编号为 {entities.ExpandAndToString(m => m.Id.ToString())} 的 {typeof(TEntity).GetDescription()} 时操作权限不足(403)"); } - } - + } + private TEntity[] CheckInsert(params TEntity[] entities) { for (int i = 0; i < entities.Length; i++) diff --git a/src/OSharp.EntityFrameworkCore/UnitOfWork.cs b/src/OSharp.EntityFrameworkCore/UnitOfWork.cs index 369e6144b..c75d3f6e7 100644 --- a/src/OSharp.EntityFrameworkCore/UnitOfWork.cs +++ b/src/OSharp.EntityFrameworkCore/UnitOfWork.cs @@ -264,7 +264,21 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - + + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() + { + foreach (var contexts in _contextDict.Values) + { + foreach (var context in contexts) + { + context.Dispose(); + } + } + } + /// /// 对数据库连接开启事务 /// diff --git a/src/OSharp.Exceptionless/OSharp.Exceptionless.csproj b/src/OSharp.Exceptionless/OSharp.Exceptionless.csproj index 3e6158d0b..cb709e50a 100644 --- a/src/OSharp.Exceptionless/OSharp.Exceptionless.csproj +++ b/src/OSharp.Exceptionless/OSharp.Exceptionless.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Exceptionless OSharp Exceptionless 分布式日志组件 OSharp Exceptionless 分布式日志组件,封装基于Exceptionless 分布式日志记录实现 diff --git a/src/OSharp.Hangfire/OSharp.Hangfire.csproj b/src/OSharp.Hangfire/OSharp.Hangfire.csproj index 2ccf040c8..f62e969ad 100644 --- a/src/OSharp.Hangfire/OSharp.Hangfire.csproj +++ b/src/OSharp.Hangfire/OSharp.Hangfire.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Hangfire OSharp Hangfire 后台任务组件 OSharp Hangfire 后台任务组件,封装基于Hangfire后台任务的服务端实现 diff --git a/src/OSharp.Hosting.Apis/OSharp.Hosting.Apis.csproj b/src/OSharp.Hosting.Apis/OSharp.Hosting.Apis.csproj index f159755d0..e725f4425 100644 --- a/src/OSharp.Hosting.Apis/OSharp.Hosting.Apis.csproj +++ b/src/OSharp.Hosting.Apis/OSharp.Hosting.Apis.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Hosting.Apis Library OSharp框架非业务WebAPI实现 @@ -19,17 +19,15 @@ - - - - - - + + + + diff --git a/src/OSharp.Hosting.Core/Identity/RoleStore.cs b/src/OSharp.Hosting.Core/Identity/RoleStore.cs index 64acffd45..f6dc1e99d 100644 --- a/src/OSharp.Hosting.Core/Identity/RoleStore.cs +++ b/src/OSharp.Hosting.Core/Identity/RoleStore.cs @@ -23,4 +23,5 @@ public class RoleStore : OSharp.Identity.RoleStoreBase roleRepository, IRepository roleClaimRepository) : base(roleRepository, roleClaimRepository) { } + } diff --git a/src/OSharp.Hosting.Core/Identity/UserStore.cs b/src/OSharp.Hosting.Core/Identity/UserStore.cs index de39a5571..0a5a1af0f 100644 --- a/src/OSharp.Hosting.Core/Identity/UserStore.cs +++ b/src/OSharp.Hosting.Core/Identity/UserStore.cs @@ -36,4 +36,5 @@ public UserStore(IRepository userRepository, IEventBus eventBus) : base(userRepository, userLoginRepository, userClaimRepository, userTokenRepository, roleRepository, userRoleRepository, eventBus) { } + } diff --git a/src/OSharp.Hosting.Core/OSharp.Hosting.Core.csproj b/src/OSharp.Hosting.Core/OSharp.Hosting.Core.csproj index 2334d141d..3f57dc8be 100644 --- a/src/OSharp.Hosting.Core/OSharp.Hosting.Core.csproj +++ b/src/OSharp.Hosting.Core/OSharp.Hosting.Core.csproj @@ -4,26 +4,22 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Hosting.Core OSharp框架非业务核心 OSharp框架业务核心,封装框架非业务如认证,权限,系统,消息等模块的接口与业务实现 OSharp.Hosting - - - - - + - + - + - + diff --git a/src/OSharp.Hosting.EntityConfiguration/OSharp.Hosting.EntityConfiguration.csproj b/src/OSharp.Hosting.EntityConfiguration/OSharp.Hosting.EntityConfiguration.csproj index 0858f31bb..0bc5a7baa 100644 --- a/src/OSharp.Hosting.EntityConfiguration/OSharp.Hosting.EntityConfiguration.csproj +++ b/src/OSharp.Hosting.EntityConfiguration/OSharp.Hosting.EntityConfiguration.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Hosting.EntityConfiguration OSharp框架非业务实体映射 OSharp框架非业务实体映射,封装框架非业务如认证,权限,系统,消息等模块的EFCore实体映射 @@ -13,9 +13,20 @@ - + + + + diff --git a/src/OSharp.Identity/Identity/RoleStoreBase.cs b/src/OSharp.Identity/Identity/RoleStoreBase.cs index 7a3a20fd4..120982d04 100644 --- a/src/OSharp.Identity/Identity/RoleStoreBase.cs +++ b/src/OSharp.Identity/Identity/RoleStoreBase.cs @@ -336,4 +336,16 @@ public Task FindByNameAsync(string normalizedRoleName, CancellationToken } #endregion + + #region Overrides of Disposable + + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() + { + + } + + #endregion } diff --git a/src/OSharp.Identity/Identity/UserStoreBase.cs b/src/OSharp.Identity/Identity/UserStoreBase.cs index 6f230d449..d89e45f5b 100644 --- a/src/OSharp.Identity/Identity/UserStoreBase.cs +++ b/src/OSharp.Identity/Identity/UserStoreBase.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2020 OSharp. All rights reserved. // @@ -1298,4 +1298,16 @@ protected void ThrowIfDisposed() } #endregion -} \ No newline at end of file + + #region Overrides of Disposable + + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() + { + + } + + #endregion +} diff --git a/src/OSharp.Identity/OSharp.Identity.csproj b/src/OSharp.Identity/OSharp.Identity.csproj index 7ec33d92f..c2129253e 100644 --- a/src/OSharp.Identity/OSharp.Identity.csproj +++ b/src/OSharp.Identity/OSharp.Identity.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Identity OSharp 身份认证组件 OSharp 身份认证组件,基于AspNetCore.Identity和Osharp仓储系统的身份认证实现 @@ -17,14 +17,6 @@ - - - - - - - - @@ -33,6 +25,10 @@ + + + + diff --git a/src/OSharp.Log4Net/Log4NetLoggerProvider.cs b/src/OSharp.Log4Net/Log4NetLoggerProvider.cs index 2241396d9..75e1fb325 100644 --- a/src/OSharp.Log4Net/Log4NetLoggerProvider.cs +++ b/src/OSharp.Log4Net/Log4NetLoggerProvider.cs @@ -92,13 +92,11 @@ private static Assembly GetCallingAssemblyFromStartup() return null; } - protected override void Dispose(bool disposing) + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() { - if (!Disposed) - { - _loggers.Clear(); - } - - base.Dispose(disposing); + _loggers.Clear(); } } diff --git a/src/OSharp.Log4Net/OSharp.Log4Net.csproj b/src/OSharp.Log4Net/OSharp.Log4Net.csproj index 08173d8f2..f0387f5e5 100644 --- a/src/OSharp.Log4Net/OSharp.Log4Net.csproj +++ b/src/OSharp.Log4Net/OSharp.Log4Net.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Log4Net OSharp Log4Net组件 OSharp Log4Net组件,封装使用log4net组件来实现框架的日志输出功能 diff --git a/src/OSharp.MiniProfiler/OSharp.MiniProfiler.csproj b/src/OSharp.MiniProfiler/OSharp.MiniProfiler.csproj index be700663e..073402b17 100644 --- a/src/OSharp.MiniProfiler/OSharp.MiniProfiler.csproj +++ b/src/OSharp.MiniProfiler/OSharp.MiniProfiler.csproj @@ -4,25 +4,13 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0 OSharp.MiniProfiler OSharp MiniProfiler 性能监测组件 OSharp MiniProfiler 性能监测组件,基于MiniProfiler实现的性能监测组件 - - - - - - - - - - - - - + diff --git a/src/OSharp.NLog/NLogLoggerProvider.cs b/src/OSharp.NLog/NLogLoggerProvider.cs index 51f5f08f0..a20da0e14 100644 --- a/src/OSharp.NLog/NLogLoggerProvider.cs +++ b/src/OSharp.NLog/NLogLoggerProvider.cs @@ -93,14 +93,12 @@ private static Assembly GetCallingAssemblyFromStartup() return null; } - protected override void Dispose(bool disposing) + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() { - if (!Disposed) - { - _loggers.Clear(); - } - - base.Dispose(disposing); + _loggers.Clear(); } } } diff --git a/src/OSharp.NLog/OSharp.NLog.csproj b/src/OSharp.NLog/OSharp.NLog.csproj index 2f0d71625..0e09e3655 100644 --- a/src/OSharp.NLog/OSharp.NLog.csproj +++ b/src/OSharp.NLog/OSharp.NLog.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.NLog OSharp NLog组件,封装使用nlog组件来实现框架的日志输出功能 OSharp NLog组件 diff --git a/src/OSharp.Redis/OSharp.Redis.csproj b/src/OSharp.Redis/OSharp.Redis.csproj index 085433ce4..bb8d604da 100644 --- a/src/OSharp.Redis/OSharp.Redis.csproj +++ b/src/OSharp.Redis/OSharp.Redis.csproj @@ -4,24 +4,21 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Redis OSharp Redis 缓存组件 OSharp Redis 缓存组件,封装基于Redis客户端的缓存实现 - - - - - - + + + diff --git a/src/OSharp.Swagger/OSharp.Swagger.csproj b/src/OSharp.Swagger/OSharp.Swagger.csproj index 5ae573f27..5e451050b 100644 --- a/src/OSharp.Swagger/OSharp.Swagger.csproj +++ b/src/OSharp.Swagger/OSharp.Swagger.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Swagger OSharp Swagger 客户端组件 OSharp Swagger 客户端组件,封装基于Redis客户端实现 diff --git a/src/OSharp.Utils/Data/Disposable.cs b/src/OSharp.Utils/Data/Disposable.cs index f63de8a03..85f12ed66 100644 --- a/src/OSharp.Utils/Data/Disposable.cs +++ b/src/OSharp.Utils/Data/Disposable.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2020 OSharp. All rights reserved. // @@ -21,12 +21,22 @@ public abstract class Disposable : IDisposable protected virtual void Dispose(bool disposing) { + if (Disposed) + { + return; + } if (disposing) { - Disposed = true; + Disposing(); } + Disposed = true; } + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected abstract void Disposing(); + /// 执行与释放或重置非托管资源关联的应用程序定义的任务。 public void Dispose() { @@ -39,4 +49,4 @@ public void Dispose() Dispose(false); } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Data/TreeHelper.README.md b/src/OSharp.Utils/Data/TreeHelper.README.md new file mode 100644 index 000000000..0374430fb --- /dev/null +++ b/src/OSharp.Utils/Data/TreeHelper.README.md @@ -0,0 +1,261 @@ +# TreeHelper 树形数据辅助类 + +`TreeHelper` 是一个功能强大的树形数据操作工具类,提供了平面数据与树形数据之间的相互转换功能,以及树形数据的遍历操作。**无需实现特定接口,完全兼容现有代码**。 + +## 功能特性 + +- ✅ **平面数据转树形数据** - 将扁平的父子关系数据转换为树形结构 +- ✅ **树形数据转平面数据** - 将树形结构数据转换为扁平列表 +- ✅ **树形数据遍历** - 非递归的前序遍历算法 +- ✅ **泛型支持** - 支持 int、string、Guid 等各种类型的节点ID +- ✅ **高性能** - 使用字典查找,时间复杂度 O(n) +- ✅ **非递归实现** - 避免栈溢出,适合深层树结构 +- ✅ **完全兼容** - 无需实现接口,适配任何现有类 +- ✅ **完整测试** - 100% 单元测试覆盖 + +## 核心方法 + +### 1. ToTree - 平面数据转树形数据 + +将具有父子关系的平面数据转换为树形结构。 + +```csharp +public static IList ToTree( + IEnumerable flatData, + Func getId, + Func getParentId, + Func> getChildren, + Action> setChildren, + TKey rootId = default(TKey)) +``` + +**参数说明:** +- `flatData`: 平面数据列表 +- `getId`: 获取节点ID的委托函数 +- `getParentId`: 获取父节点ID的委托函数 +- `getChildren`: 获取子节点集合的委托函数 +- `setChildren`: 设置子节点集合的委托函数 +- `rootId`: 根节点ID,默认为 `default(TKey)` + +**返回值:** +- 树形数据列表(根节点集合) + +### 2. ToFlat - 树形数据转平面数据 + +将树形结构数据转换为扁平列表,支持深度优先遍历。 + +```csharp +public static IList ToFlat( + IEnumerable treeData, + Func> getChildren, + Func createFlatNode) +``` + +**参数说明:** +- `treeData`: 树形数据列表 +- `getChildren`: 获取子节点集合的委托函数 +- `createFlatNode`: 创建平面节点的委托函数 + +**返回值:** +- 平面数据列表 + +### 3. TraverseWithStack - 树形数据遍历 + +使用堆栈实现树的前序遍历(非递归)。 + +```csharp +public static void TraverseWithStack( + T root, + Func> getChildNodes, + Action processNode) +``` + +**参数说明:** +- `root`: 根节点 +- `getChildNodes`: 获取节点子节点的委托函数 +- `processNode`: 处理节点的委托函数 + +## 使用示例 + +### 基本用法 + +```csharp +// 1. 定义树节点类(无需实现任何接口) +public class TreeNode +{ + public int Id { get; set; } + public int ParentId { get; set; } + public string Name { get; set; } + public IList Children { get; set; } = new List(); +} + +// 2. 创建平面数据 +var flatData = new List +{ + new TreeNode { Id = 1, ParentId = 0, Name = "根节点1" }, + new TreeNode { Id = 2, ParentId = 0, Name = "根节点2" }, + new TreeNode { Id = 3, ParentId = 1, Name = "子节点1-1" }, + new TreeNode { Id = 4, ParentId = 1, Name = "子节点1-2" }, + new TreeNode { Id = 5, ParentId = 2, Name = "子节点2-1" } +}; + +// 3. 平面数据转树形数据 +var treeData = TreeHelper.ToTree( + flatData, + x => x.Id, // 获取ID + x => x.ParentId, // 获取父ID + x => x.Children, // 获取子节点集合 + (x, children) => x.Children = children, // 设置子节点集合 + 0 // 根节点ID +); + +// 4. 树形数据转平面数据 +var flatResult = TreeHelper.ToFlat( + treeData, + x => x.Children, // 获取子节点集合 + x => new TreeNode // 创建平面节点 + { + Id = x.Id, + ParentId = x.ParentId, + Name = x.Name, + Children = null + } +); + +// 5. 树形数据遍历 +TreeHelper.TraverseWithStack( + treeData.First(), + x => x.Children, + x => Console.WriteLine($"处理节点: {x.Name}") +); +``` + +### 高级用法 + +#### 支持不同属性名 + +```csharp +public class MenuItem +{ + public string MenuId { get; set; } + public string ParentMenuId { get; set; } + public string MenuName { get; set; } + public List SubMenus { get; set; } = new List(); +} + +// 使用自定义属性名 +var menuTree = TreeHelper.ToTree( + flatMenus, + x => x.MenuId, // 自定义ID属性 + x => x.ParentMenuId, // 自定义父ID属性 + x => x.SubMenus, // 自定义子节点属性 + (x, children) => x.SubMenus = children, + "0" // 字符串类型的根ID +); +``` + +#### 支持不同数据类型 + +```csharp +public class Category +{ + public Guid CategoryId { get; set; } + public Guid? ParentCategoryId { get; set; } + public string CategoryName { get; set; } + public IList SubCategories { get; set; } = new List(); +} + +// 使用Guid类型 +var categoryTree = TreeHelper.ToTree( + flatCategories, + x => x.CategoryId, + x => x.ParentCategoryId ?? Guid.Empty, + x => x.SubCategories, + (x, children) => x.SubCategories = children, + Guid.Empty +); +``` + +## 性能特点 + +- **时间复杂度**: O(n) - 使用字典进行快速查找 +- **空间复杂度**: O(n) - 需要额外的字典存储空间 +- **非递归实现**: 避免栈溢出,适合深层树结构 +- **深度优先遍历**: 保证数据顺序的一致性 + +## 支持的数据类型 + +| 类型 | 示例 | 说明 | +|------|------|------| +| `int` | `1, 2, 3` | 整数ID,最常用 | +| `string` | `"1", "2", "3"` | 字符串ID,支持复杂格式 | +| `Guid` | `Guid.NewGuid()` | GUID ID,全局唯一 | +| `long` | `1L, 2L, 3L` | 长整型ID | +| 其他 | 任何实现了相等比较的类型 | 自定义类型 | + +## 最佳实践 + +### 1. 数据完整性检查 + +```csharp +// 转换前检查数据完整性 +var orphanNodes = flatData.Where(x => + !Equals(x.ParentId, rootId) && + !flatData.Any(p => Equals(p.Id, x.ParentId)) +).ToList(); + +if (orphanNodes.Any()) +{ + Console.WriteLine($"发现孤立节点: {string.Join(", ", orphanNodes.Select(x => x.Id))}"); +} +``` + +### 2. 性能优化 + +```csharp +// 对于大数据集,考虑分批处理 +var batchSize = 1000; +var batches = flatData.Chunk(batchSize); +var allTrees = new List(); + +foreach (var batch in batches) +{ + var batchTree = TreeHelper.ToTree(batch, ...); + allTrees.AddRange(batchTree); +} +``` + +### 3. 错误处理 + +```csharp +try +{ + var treeData = TreeHelper.ToTree(flatData, ...); +} +catch (Exception ex) +{ + // 处理转换异常 + Console.WriteLine($"树形数据转换失败: {ex.Message}"); +} +``` + +## 注意事项 + +1. **无需实现接口**: 完全兼容现有代码,无需修改任何类定义 +2. **数据一致性**: 确保 `ParentId` 与某个节点的 `Id` 对应,或为根节点ID +3. **内存管理**: 转换过程会创建新对象,注意内存使用 +4. **空值处理**: 委托函数必须正确处理 null 值情况 +5. **循环引用**: 避免数据中存在循环引用,可能导致无限递归 + +## 相关文件 + +- `TreeHelper.cs` - 核心实现类 +- `TreeExample.cs` - 详细使用示例 +- `TreeHelperTests.cs` - 完整单元测试 +- `TreeHelper.README.md` - 本文档 + +## 更新日志 + +- **v1.0.0** - 初始版本,支持基本的树形数据转换 +- **v1.1.0** - 移除接口依赖,使用委托函数方式,提高兼容性 +- **v1.2.0** - 添加树形数据遍历功能,完善文档和测试 diff --git a/src/OSharp.Utils/Data/TreeHelper.cs b/src/OSharp.Utils/Data/TreeHelper.cs new file mode 100644 index 000000000..c249a9538 --- /dev/null +++ b/src/OSharp.Utils/Data/TreeHelper.cs @@ -0,0 +1,176 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2025 66SOFT. All rights reserved. +// +// https://ifs.66soft.net +// 郭明锋 +// 2025-10-01 00:10 +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace OSharp.Data +{ + /// + /// 树形数据辅助类 + /// + public static class TreeHelper + { + /// + /// 平面数据转树形数据 + /// + /// 树节点类型 + /// 节点ID类型 + /// 平面数据列表 + /// 获取节点ID的委托 + /// 获取父节点ID的委托 + /// 获取子节点集合的委托 + /// 设置子节点集合的委托 + /// 根节点ID,默认为default(TKey) + /// 树形数据列表 + public static IList ToTree( + IEnumerable flatData, + Func getId, + Func getParentId, + Func> getChildren, + Action> setChildren, + TKey rootId = default(TKey)) + { + if (flatData == null || getId == null || getParentId == null || getChildren == null || setChildren == null) + return new List(); + + var dataList = flatData.ToList(); + if (!dataList.Any()) + return new List(); + + // 初始化所有节点的Children集合 + foreach (var item in dataList) + { + var children = getChildren(item); + if (children == null) + { + setChildren(item, new List()); + } + } + + // 创建节点字典,便于快速查找 + var nodeDict = dataList.ToDictionary(x => getId(x), x => x); + + // 构建树形结构 + var rootNodes = new List(); + foreach (var item in dataList) + { + var parentId = getParentId(item); + if (Equals(parentId, rootId)) + { + // 根节点 + rootNodes.Add(item); + } + else if (nodeDict.ContainsKey(parentId)) + { + // 子节点,添加到父节点的Children集合中 + var parent = nodeDict[parentId]; + var parentChildren = getChildren(parent); + parentChildren.Add(item); + } + } + + return rootNodes; + } + + /// + /// 树形数据转平面数据(深度优先遍历) + /// + /// 树节点类型 + /// 树形数据列表 + /// 获取子节点集合的委托 + /// 创建平面节点的委托 + /// 平面数据列表 + public static IList ToFlat( + IEnumerable treeData, + Func> getChildren, + Func createFlatNode) + { + if (treeData == null || getChildren == null || createFlatNode == null) + return new List(); + + var result = new List(); + var stack = new Stack(); + + // 将所有根节点压入堆栈 + foreach (var root in treeData.Reverse()) + { + stack.Push(root); + } + + // 深度优先遍历 + while (stack.Count > 0) + { + var currentNode = stack.Pop(); + + // 创建当前节点的副本(避免修改原树结构) + var flatNode = createFlatNode(currentNode); + result.Add(flatNode); + + // 将子节点压入堆栈(逆序压入保证遍历顺序) + var children = getChildren(currentNode); + if (children != null) + { + foreach (var child in children.Reverse()) + { + stack.Push(child); + } + } + } + + return result; + } + + + /// + /// 用堆栈实现树的前序遍历(非递归) + /// + /// 树节点类型 + /// 根节点 + /// 获取节点子节点的委托(适配不同树结构) + /// 处理节点的委托(遍历到节点时执行的操作) + public static void TraverseWithStack( + T root, + Func> getChildNodes, + Action processNode) + { + // 边界检查:根节点为空或无处理逻辑时直接返回 + if (root == null || processNode == null || getChildNodes == null) + return; + + // 初始化堆栈并压入根节点 + var stack = new Stack(); + stack.Push(root); + + // 循环处理堆栈中的节点 + while (stack.Count > 0) + { + // 1. 弹出栈顶节点并处理(前序遍历:先处理根节点) + var currentNode = stack.Pop(); + processNode(currentNode); + + // 2. 获取子节点并逆序压入堆栈(保证子节点按原顺序遍历) + // 注意:堆栈是后进先出,所以逆序入栈才能让子节点按正序出栈 + var childNodes = getChildNodes(currentNode); + if (childNodes == null) + { + continue; + } + + foreach (var child in childNodes.Reverse()) + { + stack.Push(child); + } + } + } + } +} + diff --git a/src/OSharp.Utils/Extensions/DictionaryExtensions.cs b/src/OSharp.Utils/Extensions/DictionaryExtensions.cs index 6dd9ef66b..44c388c34 100644 --- a/src/OSharp.Utils/Extensions/DictionaryExtensions.cs +++ b/src/OSharp.Utils/Extensions/DictionaryExtensions.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2017 OSharp. All rights reserved. // @@ -26,7 +26,7 @@ public static class DictionaryExtensions /// 要操作的字典 /// 指定键名 /// 获取到的值 - public static TValue GetOrDefault(this IDictionarydictionary, TKey key) + public static TValue GetOrDefault(this IDictionary dictionary, TKey key) { return dictionary.TryGetValue(key, out TValue value) ? value : default(TValue); } @@ -40,7 +40,7 @@ public static TValue GetOrDefault(this IDictionarydi /// 指定键名 /// 添加值的委托 /// 获取到的值 - public static TValue GetOrAdd(this IDictionarydictionary, TKey key, Func addFunc) + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func addFunc) { if (dictionary.TryGetValue(key, out TValue value)) { @@ -48,5 +48,34 @@ public static TValue GetOrAdd(this IDictionarydictio } return dictionary[key] = addFunc(); } + + /// + /// 获取指定键指定类型的值 + /// + public static T GetValue(this IDictionary dictionary, string key) + { + if (dictionary.TryGetValue(key, out var value)) + { + return value.CastTo(); + } + return default; + } + + /// + /// 设置字典中的值,不存在则添加,存在则更新 + /// + /// 值类型 + /// 字典 + /// 键 + /// 添加值的委托 + /// 更新值的委托 + public static void SetValue(this IDictionary dictionary, string key, Func addFunc, Func updateFunc) + { + object value; + value = dictionary.TryGetValue(key, out value) + ? updateFunc(value.CastTo()) + : addFunc(); + dictionary[key] = value; + } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Extensions/RandomExtensions.cs b/src/OSharp.Utils/Extensions/RandomExtensions.cs index 9cecf8338..354c95346 100644 --- a/src/OSharp.Utils/Extensions/RandomExtensions.cs +++ b/src/OSharp.Utils/Extensions/RandomExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -10,6 +10,7 @@ namespace OSharp.Extensions /// public static class RandomExtensions { + private static readonly Random _random = new Random(); private static readonly string[] Xings = @"赵,钱,孙,李,周,吴,郑,王,冯,陈,褚,卫,蒋,沈,韩,杨,朱,秦,尤,许,何,吕,施,张,孔,曹,严,华,金,魏,陶,姜,戚,谢,邹,喻,柏,水,窦,章,云,苏,潘,葛,奚,范,彭,郎,鲁,韦,昌,马,苗,凤,花,方,俞,任,袁,柳,丰,鲍,史,唐,费,廉,岑,薛,雷,贺,倪,汤,滕,殷,罗,毕,郝,邬,安,常,乐,于,时,傅,皮,卞,齐,康,伍,余,元,卜,顾,孟,平,黄,和,穆,萧,尹,姚,邵,湛,汪,祁,毛,禹,狄,米,贝,明,臧,计,伏,成,戴,谈,宋,茅,庞,熊,纪,舒,屈,项,祝,董,梁,杜,阮,蓝,闵,席,季,麻,强,贾,路,娄,危,江,童,颜,郭,梅,盛,林,刁,钟,徐,丘,骆,高,夏,蔡,田,樊,胡,凌,霍,虞,万,支,柯,昝,管,卢,莫,经,房,裘,缪,干,解,应,宗,丁,宣,贲,邓,郁,单,杭,洪,包,诸,左,石,崔,吉,钮,龚,程,嵇,邢,滑,裴,陆,荣,翁,荀,羊,於,惠,甄,麴,家,封,芮,羿,储,靳,汲,邴,糜,松,井,段,富,巫,乌,焦,巴,弓,牧,隗,山,谷,车,侯,宓,蓬,全,郗,班,仰,秋,仲,伊,宫,宁,仇,栾,暴,甘,钭,厉,戌,祖,武,符,刘,景,詹,束,龙,叶,幸,司,韶,郜,黎,蓟,薄,印,宿,白,怀,蒲,邰,从,鄂,索,咸,籍,赖,卓,蔺,屠,蒙,池,乔,阴,郁,胥,能,苍,双,闻,莘,党,翟,谭,贡,劳,逢,姬,申,扶,堵,冉,宰,郦,雍,郤,璩,桑,桂,濮,牛,寿,通,边,扈,燕,冀,郏,浦,尚,农,温,别,庄,晏,柴,瞿,阎,充,慕,连,茹,习,宦,艾,鱼,容,向,古,易,慎,戈,廖,庾,终,暨,居,衡,步,都,耿,满,弘,匡,国,文,寇,广,禄,阙,东,欧,殳,沃,利,蔚,越,菱,隆,师,巩,厍,聂,晃,勾,敖,融,冷,訾,辛,阚,那,简,饶,空,曾,毋,沙,乜,养,鞠,须,丰,巢,关,蒯,相,查,后,荆,红,游,竺,权,逯,盖,益,桓,公,万俟,司马,上官,欧阳,夏侯,诸葛,闻人,东方,赫连,皇甫,尉迟,公羊,澹台,公冶,宗政,濮阳,淳于,单于,太叔,申屠,公孙,仲孙,轩辕,令狐,钟离,宇文,长孙,慕容,司徒,司空".Replace("\r\n", "").Split(','); private static readonly string[] Mings = @"伟,刚,勇,毅,俊,峰,强,军,平,保,东,文,辉,力,明,永,健,世,广,志,义,兴,良,海,山,仁,波,宁,贵,福,生,龙,元,全,国,胜,学,祥,才,发,武,新,利,清,飞,彬,富,顺,信,子,杰,涛,昌,成,康,星,光,天,达,安,岩,中,茂,进,林,有,坚,和,彪,博,诚,先,敬,震,振,壮,会,思,群,豪,心,邦,承,乐,绍,功,松,善,厚,庆,磊,民,友,裕,河,哲,江,超,浩,亮,政,谦,亨,奇,固,之,轮,翰,朗,伯,宏,言,若,鸣,朋,斌,梁,栋,维,启,克,伦,翔,旭,鹏,泽,晨,辰,士,以,建,家,致,树,炎,德,行,时,泰,盛,雄,琛,钧,冠,策,腾,楠,榕,风,航,弘,秀,娟,英,华,慧,巧,美,娜,静,淑,惠,珠,翠,雅,芝,玉,萍,红,娥,玲,芬,芳,燕,彩,春,菊,兰,凤,洁,梅,琳,素,云,莲,真,环,雪,荣,爱,妹,霞,香,月,莺,媛,艳,瑞,凡,佳,嘉,琼,勤,珍,贞,莉,桂,娣,叶,璧,璐,娅,琦,晶,妍,茜,秋,珊,莎,锦,黛,青,倩,婷,姣,婉,娴,瑾,颖,露,瑶,怡,婵,雁,蓓,纨,仪,荷,丹,蓉,眉,君,琴,蕊,薇,菁,梦,岚,苑,婕,馨,瑗,琰,韵,融,园,艺,咏,卿,聪,澜,纯,毓,悦,昭,冰,爽,琬,茗,羽,希,欣,飘,育,滢,馥,筠,柔,竹,霭,凝,晓,欢,霄,枫,芸,菲,寒,伊,亚,宜,可,姬,舒,影,荔,枝,丽,阳,妮,宝,贝,初,程,梵,罡,恒,鸿,桦,骅,剑,娇,纪,宽,苛,灵,玛,媚,琪,晴,容,睿,烁,堂,唯,威,韦,雯,苇,萱,阅,彦,宇,雨,洋,忠,宗,曼,紫,逸,贤,蝶,菡,绿,蓝,儿,翠,烟".Replace("\r\n", "").Split(','); private static readonly string[] NationNames = @"汉族,壮族,满族,回族,苗族,维吾尔族,土家族,彝族,蒙古族,藏族,布依族,侗族,瑶族,朝鲜族,白族,哈尼族,哈萨克族,黎族,傣族,畲族,傈僳族,仡佬族,东乡族,高山族,拉祜族,水族,佤族,纳西族,羌族,土族,仫佬族,锡伯族,柯尔克孜族,达斡尔族,景颇族,毛南族,撒拉族,布朗族,塔吉克族,阿昌族,普米族,鄂温克族,怒族,京族,基诺族,德昂族,保安族,俄罗斯族,裕固族,乌兹别克族,门巴族,鄂伦春族,独龙族,塔塔尔族,赫哲族,珞巴族".Replace("\r\n", "").Split(','); @@ -76,6 +77,22 @@ public static T NextItem(this Random random, T[] items) return items[random.Next(items.Length)]; } + public static T NextOne(this IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + var list = items.ToList(); + if (list.Count == 0) + { + return default; + } + + var index = _random.Next(0, list.Count); + return list[index]; + } /// /// 返回指定时间段内的随机时间值 /// diff --git a/src/OSharp.Utils/Extensions/StringExtensions.cs b/src/OSharp.Utils/Extensions/StringExtensions.cs index 7182beab2..c587df7e4 100644 --- a/src/OSharp.Utils/Extensions/StringExtensions.cs +++ b/src/OSharp.Utils/Extensions/StringExtensions.cs @@ -384,7 +384,7 @@ public static string ReverseString(this string value) value.CheckNotNull("value"); return new string(value.Reverse().ToArray()); } - + /// /// 单词变成单数形式 /// @@ -476,6 +476,40 @@ public static bool IsImageFile(this string filename) } } + /// + /// 将16进制字符串转为数值 + /// + public static int ToDec(this string hex) + { + if (string.IsNullOrEmpty(hex)) + { + throw new ArgumentException("16进制字符串不能为空", nameof(hex)); + } + + // 移除可能的前缀(如0x, 0X) + hex = hex.Trim(); + if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + hex = hex.Substring(2); + } + + // 验证是否为有效的16进制字符串 + if (!hex.All(c => char.IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) + { + throw new ArgumentException("字符串不是有效的16进制格式", nameof(hex)); + } + + return Convert.ToInt32(hex, 16); + } + + /// + /// 将10进制数值转为16进制字符串 + /// + public static string ToHex(this int dec, int length = 8) + { + return dec.ToString($"X{length}"); + } + /// /// 以指定字符串作为分隔符将指定字符串分隔成数组 /// @@ -652,6 +686,12 @@ public static string ToBase64String(this string source, Encoding encoding = null return Convert.ToBase64String(encoding.GetBytes(source)); } + public static byte[] FromBase64StringToBytes(this string base64String) + { + byte[] bytes = Convert.FromBase64String(base64String); + return bytes; + } + /// /// 将Base64字符串转换为正常字符串,默认编码为 /// @@ -664,7 +704,8 @@ public static string FromBase64String(this string base64String, Encoding encodin { encoding = Encoding.UTF8; } - byte[] bytes = Convert.FromBase64String(base64String); + + byte[] bytes = base64String.FromBase64StringToBytes(); return encoding.GetString(bytes); } diff --git a/src/OSharp.Utils/Http/HttpExtensions.cs b/src/OSharp.Utils/Http/HttpExtensions.cs index d4dcdb812..d67d6edd8 100644 --- a/src/OSharp.Utils/Http/HttpExtensions.cs +++ b/src/OSharp.Utils/Http/HttpExtensions.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2017 OSharp. All rights reserved. // @@ -13,10 +13,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; using System.Threading.Tasks; -using Newtonsoft.Json; - using OSharp.Extensions; @@ -38,7 +37,7 @@ public static async Task GetAsync(this HttpClient client, stri { return json as TResult; } - return JsonConvert.DeserializeObject(json); + return JsonSerializer.Deserialize(json); } /// @@ -66,14 +65,15 @@ public static async Task PostAsync(this HttpClient client, str HttpResponseMessage response = await client.PostAsync(url, data); if (!response.IsSuccessStatusCode) { - return default(TResult); + return null; } string json = await response.Content.ReadAsStringAsync(); if (typeof(TResult) == typeof(string)) { return json as TResult; } - return JsonConvert.DeserializeObject(json); + + return JsonSerializer.Deserialize(json); } /// @@ -213,4 +213,4 @@ public static string GetHostUrl(this Uri uri) return $"{uri.Scheme}://{uri.Host}/"; } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Http/JsonContent.cs b/src/OSharp.Utils/Http/JsonContent.cs index b1d1bfe88..ecaffc638 100644 --- a/src/OSharp.Utils/Http/JsonContent.cs +++ b/src/OSharp.Utils/Http/JsonContent.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2019 OSharp. All rights reserved. // @@ -9,8 +9,7 @@ using System.Net.Http; using System.Text; - -using Newtonsoft.Json; +using System.Text.Json; namespace OSharp.Http @@ -24,7 +23,7 @@ public class JsonContent : StringContent /// 初始化一个类型的新实例 /// public JsonContent(object obj) - : base(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json") + : base(JsonSerializer.Serialize(obj), Encoding.UTF8, "application/json") { } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/IO/DirectoryHelper.cs b/src/OSharp.Utils/IO/DirectoryHelper.cs index eb33c39e3..01f93035a 100644 --- a/src/OSharp.Utils/IO/DirectoryHelper.cs +++ b/src/OSharp.Utils/IO/DirectoryHelper.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014 OSharp. All rights reserved. // @@ -33,6 +33,10 @@ public static string RootPath() /// 要创建的文件夹路径 public static void CreateIfNotExists(string directory) { + if (string.IsNullOrEmpty(directory)) + { + return; + } if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); @@ -154,4 +158,4 @@ public static void SetAttributes(string directory, FileAttributes attribute, boo } } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Logging/RollingFile/FileLoggerFactoryExtensions.cs b/src/OSharp.Utils/Logging/RollingFile/FileLoggerFactoryExtensions.cs index aae54cfd0..636372177 100644 --- a/src/OSharp.Utils/Logging/RollingFile/FileLoggerFactoryExtensions.cs +++ b/src/OSharp.Utils/Logging/RollingFile/FileLoggerFactoryExtensions.cs @@ -1,24 +1,15 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) 2014-2017 OSharp. All rights reserved. -// -// http://www.osharp.org -// -// 2017-09-17 21:17 -// ----------------------------------------------------------------------- - using System; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using OSharp.Logging.RollingFile; +using OSharp.Logging.RollingFile.Formatters; -//power by https://github.com/andrewlock/NetEscapades.Extensions.Logging -namespace Microsoft.Extensions.DependencyInjection +namespace OSharp.Logging.RollingFile { /// - /// Extensions for adding the to the + /// Extensions for adding the to the /// public static class FileLoggerFactoryExtensions { @@ -29,9 +20,13 @@ public static class FileLoggerFactoryExtensions public static ILoggingBuilder AddFile(this ILoggingBuilder builder) { builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +#if NETCOREAPP3_0 + builder.Services.AddSingleton(); +#endif return builder; } - + /// /// Adds a file logger named 'File' to the factory. /// @@ -39,10 +34,7 @@ public static ILoggingBuilder AddFile(this ILoggingBuilder builder) /// Sets the filename prefix to use for log files public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filename) { - builder.AddFile(options => - { - options.FileName = filename; - }); + builder.AddFile(options => options.FileName = filename); return builder; } @@ -63,4 +55,4 @@ public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action /// Options for file logging. /// - ///power by https://github.com/andrewlock/NetEscapades.Extensions.Logging public class FileLoggerOptions : BatchingLoggerOptions { private int? _fileSizeLimit = 10 * 1024 * 1024; private int? _retainedFileCountLimit = 2; - private string _fileName = "log-"; - + private int? _filesPerPeriodicityLimit = 10; + private string _fileName = "logs-"; + private string _extension = "txt"; + /// /// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit. - /// Once the log is full, no more messages will be appended. - /// Defaults to 10MB. + /// Once the log is full, no more messages will be appended, unless + /// is greater than 1. Defaults to 10MB. /// public int? FileSizeLimit { @@ -34,6 +35,24 @@ public int? FileSizeLimit } } + /// + /// Gets or sets a value representing the maximum number of files allowed for a given . + /// Once the specified number of logs per periodicity are created, no more log files will be created. Note that these extra files + /// do not count towards the RetrainedFileCountLimit. Defaults to 1. + /// + public int? FilesPerPeriodicityLimit + { + get { return _filesPerPeriodicityLimit; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FilesPerPeriodicityLimit)} must be greater than 0."); + } + _filesPerPeriodicityLimit = value; + } + } + /// /// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit. /// Defaults to 2. @@ -68,11 +87,27 @@ public string FileName } } + /// + /// Gets or sets the filename extension to use for log files. + /// Defaults to txt. + /// Will strip any prefixed . + /// + public string Extension + { + get { return _extension; } + set { _extension = value?.TrimStart('.'); } + } + + /// + /// Gets or sets the periodicity for rolling over log files. + /// + public PeriodicityOptions Periodicity { get ;set; } = PeriodicityOptions.Daily; + /// /// The directory in which log files will be written, relative to the app process. /// Default to Logs /// /// - public string LogDirectory { get; set; } = "Log"; + public string LogDirectory { get; set; } = "Logs"; } } diff --git a/src/OSharp.Utils/Logging/RollingFile/FileLoggerProvider.cs b/src/OSharp.Utils/Logging/RollingFile/FileLoggerProvider.cs new file mode 100644 index 000000000..ba1ec514c --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/FileLoggerProvider.cs @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in https://github.com/aspnet/Logging for license information. +// https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.AzureAppServices/Internal/FileLoggerProvider.cs + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using OSharp.Logging.RollingFile.Formatters; +using OSharp.Logging.RollingFile.Internal; + + +namespace OSharp.Logging.RollingFile +{ + /// + /// An that writes logs to a file + /// + [ProviderAlias("File")] + public class FileLoggerProvider : BatchingLoggerProvider + { + private readonly string _path; + private readonly string _fileName; + private readonly string _extension; + private readonly int? _maxFileSize; + private readonly int? _maxRetainedFiles; + private readonly int _maxFileCountPerPeriodicity; + private readonly PeriodicityOptions _periodicity; + + /// + /// Creates an instance of the + /// + /// The options object controlling the logger + /// + public FileLoggerProvider(IOptionsMonitor options, IEnumerable formatter) : base(options, formatter) + { + var loggerOptions = options.CurrentValue; + _path = loggerOptions.LogDirectory; + _fileName = loggerOptions.FileName; + _extension = string.IsNullOrEmpty(loggerOptions.Extension) ? null : "." + loggerOptions.Extension; + _maxFileSize = loggerOptions.FileSizeLimit; + _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; + _maxFileCountPerPeriodicity = loggerOptions.FilesPerPeriodicityLimit ?? 1; + _periodicity = loggerOptions.Periodicity; + } + + + /// + protected override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) + { + Directory.CreateDirectory(_path); + + foreach (var group in messages.GroupBy(GetGrouping)) + { + var baseName = GetBaseName(group.Key); + var fullName = GetLogFilePath(baseName, group.Key); + + if (fullName == null) + { + return; + } + + using (var streamWriter = File.AppendText(fullName)) + { + foreach (var item in group) + { + await streamWriter.WriteAsync(item.Message); + } + } + } + + RollFiles(); + } + + private string GetLogFilePath(string baseName, (int Year, int Month, int Day, int Hour, int Minute) fileNameGrouping) + { + if (_maxFileCountPerPeriodicity == 1) + { + var fullPath = Path.Combine(_path, $"{baseName}{_extension}"); + return IsAvailable(fullPath) ? fullPath : null; + } + + var counter = GetCurrentCounter(baseName); + + while (counter < _maxFileCountPerPeriodicity) + { + var fullName = Path.Combine(_path,$"{baseName}.{counter}{_extension}"); + if (!IsAvailable(fullName)) + { + counter++; + continue; + } + + return fullName; + } + + return null; + + bool IsAvailable(string filename) + { + var fileInfo = new FileInfo(filename); + return !(_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize); + } + } + + private int GetCurrentCounter(string baseName) + { + try + { + var files = Directory.GetFiles(_path, $"{baseName}.*{_extension}"); + if (files.Length == 0) + { + // No rolling file currently exists with the base name as pattern + return 0; + } + + // Get file with highest counter + var latestFile = files.OrderByDescending(file => file).First(); + + var baseNameLength = Path.Combine(_path, baseName).Length + 1; + var fileWithoutPrefix = latestFile + .AsSpan() + .Slice(baseNameLength); + var indexOfPeriod = fileWithoutPrefix.IndexOf('.'); + if (indexOfPeriod < 0) + { + // No additional dot could be found + return 0; + } + + var counterSpan = fileWithoutPrefix.Slice(0, indexOfPeriod); + if (int.TryParse(counterSpan.ToString(), out var counter)) + { + return counter; + } + + return 0; + } + catch (Exception) + { + return 0; + } + + } + + private string GetBaseName((int Year, int Month, int Day, int Hour, int Minute) group) + { + switch (_periodicity) + { + case PeriodicityOptions.Minutely: + return $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}{group.Hour:00}{group.Minute:00}"; + case PeriodicityOptions.Hourly: + return $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}{group.Hour:00}"; + case PeriodicityOptions.Daily: + return $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}"; + case PeriodicityOptions.Monthly: + return $"{_fileName}{group.Year:0000}{group.Month:00}"; + } + throw new InvalidDataException("Invalid periodicity"); + } + + private (int Year, int Month, int Day, int Hour, int Minute) GetGrouping(LogMessage message) + { + return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day, message.Timestamp.Hour, message.Timestamp.Minute); + } + + /// + /// Deletes old log files, keeping a number of files defined by + /// + protected void RollFiles() + { + if (_maxRetainedFiles > 0) + { + var groupsToDelete = new DirectoryInfo(_path) + .GetFiles(_fileName + "*") + .GroupBy(file => GetFilenameForGrouping(file.Name)) + .OrderByDescending(f => f.Key) + .Skip(_maxRetainedFiles.Value); + + foreach (var groupToDelete in groupsToDelete) + { + foreach (var fileToDelete in groupToDelete) + { + fileToDelete.Delete(); + } + } + } + + string GetFilenameForGrouping(string filename) + { + var hasExtension = !string.IsNullOrEmpty(_extension); + var isMultiFile = _maxFileCountPerPeriodicity > 1; + if (!hasExtension && !isMultiFile) + return filename; + + if(!isMultiFile || !hasExtension) + return Path.GetFileNameWithoutExtension(filename); + + return Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(filename)); + } + } + } +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Formatters/ILogFormatter.cs b/src/OSharp.Utils/Logging/RollingFile/Formatters/ILogFormatter.cs new file mode 100644 index 000000000..4ce784fc8 --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/Formatters/ILogFormatter.cs @@ -0,0 +1,27 @@ +using System.Text; + +using Microsoft.Extensions.Logging; + + +namespace OSharp.Logging.RollingFile.Formatters +{ + /// + /// Formats log messages that are written to the log file + /// + public interface ILogFormatter + { + /// + /// Gets the name of the formatter + /// + string Name { get; } + + /// + /// Writes the log message to the specified StringBuilder. + /// + /// The log entry. + /// The provider of scope data. + /// The string builder for building the message to write to the log file. + /// The type of the object to be written. + void Write(in LogEntry logEntry, IExternalScopeProvider scopeProvider, StringBuilder stringBuilder); + } +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Formatters/JsonLogFormatter.cs b/src/OSharp.Utils/Logging/RollingFile/Formatters/JsonLogFormatter.cs new file mode 100644 index 000000000..28dda65dc --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/Formatters/JsonLogFormatter.cs @@ -0,0 +1,254 @@ +#if NETCOREAPP3_0 +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace NetEscapades.Extensions.Logging.RollingFile.Formatters +{ + public class JsonLogFormatter: ILogFormatter + { + private const string MessageTemplateKey = "{OriginalFormat}"; + + private static readonly JsonWriterOptions Options = new() + { + Indented = false, + }; + + public string Name => "json"; + + public void Write(in LogEntry logEntry, IExternalScopeProvider scopeProvider, StringBuilder stringBuilder) + { + var exception = logEntry.Exception; + var message = logEntry.Formatter(logEntry.State, exception); + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, Options); + + writer.WriteStartObject(); + writer.WriteString("Timestamp", logEntry.Timestamp); + writer.WriteString("Level", GetLogLevelString(logEntry.LogLevel)); + writer.WriteString("Category", logEntry.Category); + writer.WriteString("Message", message); + if (exception != null) + { + var exceptionMessage = exception.ToString() + .Replace(Environment.NewLine, " "); + writer.WriteString(nameof(Exception), exceptionMessage); + } + + string messageTemplate = null; + if (logEntry.State != null) + { + writer.WriteStartObject(nameof(logEntry.State)); + if (logEntry.State is IEnumerable> stateProperties) + { + foreach (KeyValuePair item in stateProperties) + { + if (item.Key == MessageTemplateKey + && item.Value is string template) + { + messageTemplate = template; + } + else + { + WriteItem(writer, item); + } + } + } + else + { + writer.WriteString("Message", logEntry.State.ToString()); + } + writer.WriteEndObject(); + } + + if (!string.IsNullOrEmpty(messageTemplate)) + { + writer.WriteString("MessageTemplate", messageTemplate); + } + + if (scopeProvider != null) + { + var writerWrapper = new WriterWrapper(writer); + scopeProvider.ForEachScope((scope, state) => + { + // Add dictionary scopes to the "root" object + if (scope is IEnumerable> scopeItems) + { + foreach (KeyValuePair item in scopeItems) + { + WriteItem(state.Writer, item); + } + } + else + { + state.Values.Add(scope); // add to list for inclusion in scope array + } + }, writerWrapper); + + + if (writerWrapper.Values.Any()) + { + writer.WriteStartArray("Scopes"); + foreach (var value in writerWrapper.Values) + { + WriteValue(writer, value); + } + writer.WriteEndArray(); + } + } + + writer.WriteEndObject(); + writer.Flush(); + + stringBuilder.AppendLine(Encoding.UTF8.GetString(stream.ToArray())); + } + + private static string GetLogLevelString(LogLevel logLevel) + { + return logLevel switch + { + LogLevel.Trace => "Trace", + LogLevel.Debug => "Debug", + LogLevel.Information => "Information", + LogLevel.Warning => "Warning", + LogLevel.Error => "Error", + LogLevel.Critical => "Critical", + _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) + }; + } + + private void WriteItem(Utf8JsonWriter writer, KeyValuePair item) + { + var key = item.Key; + switch (item.Value) + { + case bool boolValue: + writer.WriteBoolean(key, boolValue); + break; + case byte byteValue: + writer.WriteNumber(key, byteValue); + break; + case sbyte sbyteValue: + writer.WriteNumber(key, sbyteValue); + break; + case char charValue: +#if NETCOREAPP + writer.WriteString(key, MemoryMarshal.CreateSpan(ref charValue, 1)); +#else + writer.WriteString(key, charValue.ToString()); +#endif + break; + case decimal decimalValue: + writer.WriteNumber(key, decimalValue); + break; + case double doubleValue: + writer.WriteNumber(key, doubleValue); + break; + case float floatValue: + writer.WriteNumber(key, floatValue); + break; + case int intValue: + writer.WriteNumber(key, intValue); + break; + case uint uintValue: + writer.WriteNumber(key, uintValue); + break; + case long longValue: + writer.WriteNumber(key, longValue); + break; + case ulong ulongValue: + writer.WriteNumber(key, ulongValue); + break; + case short shortValue: + writer.WriteNumber(key, shortValue); + break; + case ushort ushortValue: + writer.WriteNumber(key, ushortValue); + break; + case null: + writer.WriteNull(key); + break; + default: + writer.WriteString(key, ToInvariantString(item.Value)); + break; + } + } + + private void WriteValue(Utf8JsonWriter writer, object item) + { + switch (item) + { + case bool boolValue: + writer.WriteBooleanValue(boolValue); + break; + case byte byteValue: + writer.WriteNumberValue(byteValue); + break; + case sbyte sbyteValue: + writer.WriteNumberValue(sbyteValue); + break; + case char charValue: +#if NETCOREAPP + writer.WriteStringValue(MemoryMarshal.CreateSpan(ref charValue, 1)); +#else + writer.WriteStringValue(charValue.ToString()); +#endif + break; + case decimal decimalValue: + writer.WriteNumberValue(decimalValue); + break; + case double doubleValue: + writer.WriteNumberValue(doubleValue); + break; + case float floatValue: + writer.WriteNumberValue(floatValue); + break; + case int intValue: + writer.WriteNumberValue(intValue); + break; + case uint uintValue: + writer.WriteNumberValue(uintValue); + break; + case long longValue: + writer.WriteNumberValue(longValue); + break; + case ulong ulongValue: + writer.WriteNumberValue(ulongValue); + break; + case short shortValue: + writer.WriteNumberValue(shortValue); + break; + case ushort ushortValue: + writer.WriteNumberValue(ushortValue); + break; + case null: + writer.WriteNullValue(); + break; + default: + writer.WriteStringValue(ToInvariantString(item)); + break; + } + } + + private static string ToInvariantString(object obj) => Convert.ToString(obj, CultureInfo.InvariantCulture); + + private readonly struct WriterWrapper + { + public readonly Utf8JsonWriter Writer; + public readonly List Values; + + public WriterWrapper(Utf8JsonWriter writer) + { + Writer = writer; + Values = new List(); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/OSharp.Utils/Logging/RollingFile/Formatters/LogEntry.cs b/src/OSharp.Utils/Logging/RollingFile/Formatters/LogEntry.cs new file mode 100644 index 000000000..a1354aab6 --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/Formatters/LogEntry.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// Based on https://github.com/dotnet/runtime/blob/6cb0e80fe6f4c13a8e7d3a1b43d8fe6ea9ffc344/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntry.cs + +using System; + +using Microsoft.Extensions.Logging; + + +namespace OSharp.Logging.RollingFile.Formatters +{ + /// + /// Holds the information for a single log entry. + /// + public readonly struct LogEntry + { + /// + /// Initializes an instance of the LogEntry struct. + /// + /// The time the event occured + /// The log level. + /// The category name for the log. + /// The log event Id. + /// The state for which log is being written. + /// The log exception. + /// The formatter. + public LogEntry(DateTimeOffset timestamp, LogLevel logLevel, string category, EventId eventId, TState state, Exception exception, Func formatter) + { + Timestamp = timestamp; + LogLevel = logLevel; + Category = category; + EventId = eventId; + State = state; + Exception = exception; + Formatter = formatter; + } + + /// + /// Gets the time the event occured + /// + public DateTimeOffset Timestamp { get; } + + /// + /// Gets the LogLevel + /// + public LogLevel LogLevel { get; } + + /// + /// Gets the log category + /// + public string Category { get; } + + /// + /// Gets the log EventId + /// + public EventId EventId { get; } + + /// + /// Gets the TState + /// + public TState State { get; } + + /// + /// Gets the log exception + /// + public Exception Exception { get; } + + /// + /// Gets the formatter + /// + public Func Formatter { get; } + } +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Formatters/SimpleLogFormatter.cs b/src/OSharp.Utils/Logging/RollingFile/Formatters/SimpleLogFormatter.cs new file mode 100644 index 000000000..f81477bbc --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/Formatters/SimpleLogFormatter.cs @@ -0,0 +1,46 @@ +using System.Text; + +using Microsoft.Extensions.Logging; + + +namespace OSharp.Logging.RollingFile.Formatters +{ + /// + /// A simple formatter for log messages + /// + public class SimpleLogFormatter : ILogFormatter + { + public string Name => "simple"; + + /// + public void Write(in LogEntry logEntry, IExternalScopeProvider scopeProvider, StringBuilder builder) + { + builder.Append(logEntry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); + builder.Append(" ["); + builder.Append(logEntry.LogLevel.ToString()); + builder.Append("] "); + builder.Append(logEntry.Category); + + if (scopeProvider != null) + { + scopeProvider.ForEachScope((scope, stringBuilder) => + { + stringBuilder.Append(" => ").Append(scope); + }, builder); + + builder.Append(':').AppendLine(); + } + else + { + builder.Append(": "); + } + + builder.AppendLine(logEntry.Formatter(logEntry.State, logEntry.Exception)); + + if (logEntry.Exception != null) + { + builder.AppendLine(logEntry.Exception.ToString()); + } + } + } +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLogger.cs b/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLogger.cs index 1357bcdaf..5b9424545 100644 --- a/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLogger.cs +++ b/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLogger.cs @@ -1,78 +1,57 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) 2014-2017 OSharp. All rights reserved. -// -// http://www.osharp.org -// -// 2017-09-17 21:10 -// ----------------------------------------------------------------------- +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in https://github.com/aspnet/Logging for license information. +// https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.AzureAppServices/Internal/BatchingLogger.cs using System; using System.Text; using Microsoft.Extensions.Logging; +using OSharp.Logging.RollingFile.Formatters; + namespace OSharp.Logging.RollingFile.Internal { -//power by https://github.com/andrewlock/NetEscapades.Extensions.Logging - internal class BatchingLogger : ILogger + public class BatchingLogger : ILogger { - private readonly string _category; private readonly BatchingLoggerProvider _provider; + private readonly string _category; + private readonly ILogFormatter _formatter; - public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName) + public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName, ILogFormatter formatter) { _provider = loggerProvider; _category = categoryName; + _formatter = formatter; } public IDisposable BeginScope(TState state) { - return null; + // NOTE: Differs from source + return _provider.ScopeProvider?.Push(state); } public bool IsEnabled(LogLevel logLevel) { - if (logLevel == LogLevel.None) - { - return false; - } - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter); + return _provider.IsEnabled; } - public void Log(DateTimeOffset timestamp, - LogLevel logLevel, - EventId eventId, - TState state, - Exception exception, - Func formatter) + public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (!IsEnabled(logLevel)) { return; } + var logEntry = new LogEntry(timestamp, logLevel, _category, eventId, state, exception, formatter); var builder = new StringBuilder(); - builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); - builder.Append(" ["); - builder.Append(logLevel.ToString()); - builder.Append("] "); - builder.Append(_category); - builder.Append(": "); - builder.AppendLine(formatter(state, exception)); - - if (exception != null) - { - builder.AppendLine(exception.ToString()); - } - + _formatter.Write(in logEntry, _provider.ScopeProvider, builder); _provider.AddMessage(timestamp, builder.ToString()); } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter); + } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerOptions.cs b/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerOptions.cs index ffc3c74c8..449b0dc2c 100644 --- a/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerOptions.cs +++ b/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerOptions.cs @@ -1,25 +1,16 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) 2014-2017 OSharp. All rights reserved. -// -// http://www.osharp.org -// -// 2017-09-17 21:18 -// ----------------------------------------------------------------------- +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in https://github.com/aspnet/Logging for license information. +// https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.AzureAppServices/Internal/BatchingLoggerOptions.cs using System; namespace OSharp.Logging.RollingFile.Internal { - //power by https://github.com/andrewlock/NetEscapades.Extensions.Logging - /// - /// 批量日志记录选项 - /// public class BatchingLoggerOptions { - private int? _backgroundQueueSize; - private int? _batchSize = 32; + private int? _batchSize; + private int? _backgroundQueueSize = 1000; private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1); /// @@ -41,7 +32,7 @@ public TimeSpan FlushPeriod /// /// Gets or sets the maximum size of the background log message queue or null for no limit. /// After maximum queue size is reached log event sink would start blocking. - /// Defaults to null. + /// Defaults to 1000. /// public int? BackgroundQueueSize { @@ -59,6 +50,7 @@ public int? BackgroundQueueSize /// /// Gets or sets a maximum number of events to include in a single batch or null for no limit. /// + /// Defaults to null. public int? BatchSize { get { return _batchSize; } @@ -75,6 +67,18 @@ public int? BatchSize /// /// Gets or sets value indicating if logger accepts and queues writes. /// - public bool IsEnabled { get; set; } + public bool IsEnabled { get; set; } = true; + + /// + /// Gets or sets a value indicating whether scopes should be included in the message. + /// Defaults to false. + /// + public bool IncludeScopes { get; set; } = false; + + /// + /// Gets of sets the name of the log message formatter to use. + /// Defaults to "simple" />. + /// + public string FormatterName { get; set; } = "simple"; } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerProvider.cs b/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerProvider.cs index 6786ee644..5e6e0e699 100644 --- a/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerProvider.cs +++ b/src/OSharp.Utils/Logging/RollingFile/Internal/BatchingLoggerProvider.cs @@ -1,31 +1,45 @@ -using System; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in https://github.com/aspnet/Logging for license information. +// https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.AzureAppServices/Internal/BatchingLoggerProvider.cs + +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OSharp.Logging.RollingFile.Formatters; + namespace OSharp.Logging.RollingFile.Internal { - //power by https://github.com/andrewlock/NetEscapades.Extensions.Logging - public abstract class BatchingLoggerProvider : Disposable, ILoggerProvider + public abstract class BatchingLoggerProvider : ILoggerProvider, ISupportExternalScope { - private readonly List _currentBatch = new List(); + private readonly List _currentBatch = new List(); private readonly TimeSpan _interval; private readonly int? _queueSize; private readonly int? _batchSize; + private readonly IDisposable _optionsChangeToken; - private BlockingCollection _messageQueue; + private BlockingCollection _messageQueue; private Task _outputTask; private CancellationTokenSource _cancellationTokenSource; - protected BatchingLoggerProvider(IOptions options) + private bool _includeScopes; + private IExternalScopeProvider _scopeProvider; + private readonly ILogFormatter _formatter; + + internal IExternalScopeProvider ScopeProvider => _includeScopes ? _scopeProvider : null; + + protected BatchingLoggerProvider(IOptionsMonitor options, IEnumerable formatters) { - // NOTE: Only IsEnabled is monitored + // NOTE: Only IsEnabled and IncludeScopes are monitored - var loggerOptions = options.Value; + var loggerOptions = options.CurrentValue; if (loggerOptions.BatchSize <= 0) { throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number."); @@ -35,16 +49,52 @@ protected BatchingLoggerProvider(IOptions options) throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); } + var formatterName = (string.IsNullOrEmpty(loggerOptions.FormatterName) + ? "simple" + : loggerOptions.FormatterName).ToLowerInvariant(); + var formatter = formatters.FirstOrDefault(x => x.Name == formatterName); + + if (formatter is null) + { + throw new ArgumentException( + $"Unknown formatter name {formatterName} - ensure custom formatters are registered correctly with the DI container", + nameof(loggerOptions.FormatterName)); + } + + _formatter = formatter; _interval = loggerOptions.FlushPeriod; _batchSize = loggerOptions.BatchSize; _queueSize = loggerOptions.BackgroundQueueSize; - Start(); + _optionsChangeToken = options.OnChange(UpdateOptions); + UpdateOptions(options.CurrentValue); + } + + public bool IsEnabled { get; private set; } + + private void UpdateOptions(BatchingLoggerOptions options) + { + var oldIsEnabled = IsEnabled; + IsEnabled = options.IsEnabled; + _includeScopes = options.IncludeScopes; + + if (oldIsEnabled != IsEnabled) + { + if (IsEnabled) + { + Start(); + } + else + { + Stop(); + } + } + } - protected abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); + protected abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); - private async Task ProcessLogQueue(object state) + private async Task ProcessLogQueue() { while (!_cancellationTokenSource.IsCancellationRequested) { @@ -85,7 +135,7 @@ internal void AddMessage(DateTimeOffset timestamp, string message) { try { - _messageQueue.Add(new LogMessageEntry { Message = message, Timestamp = timestamp }, _cancellationTokenSource.Token); + _messageQueue.Add(new LogMessage { Message = message, Timestamp = timestamp }, _cancellationTokenSource.Token); } catch { @@ -97,14 +147,11 @@ internal void AddMessage(DateTimeOffset timestamp, string message) private void Start() { _messageQueue = _queueSize == null ? - new BlockingCollection(new ConcurrentQueue()) : - new BlockingCollection(new ConcurrentQueue(), _queueSize.Value); + new BlockingCollection(new ConcurrentQueue()) : + new BlockingCollection(new ConcurrentQueue(), _queueSize.Value); _cancellationTokenSource = new CancellationTokenSource(); - _outputTask = Task.Factory.StartNew( - ProcessLogQueue, - null, - TaskCreationOptions.LongRunning); + _outputTask = Task.Run(ProcessLogQueue); } private void Stop() @@ -124,18 +171,23 @@ private void Stop() } } - protected override void Dispose(bool disposing) + public void Dispose() { - if (!Disposed) + _optionsChangeToken?.Dispose(); + if (IsEnabled) { Stop(); } - base.Dispose(disposing); } public ILogger CreateLogger(string categoryName) { - return new BatchingLogger(this, categoryName); + return new BatchingLogger(this, categoryName, _formatter); + } + + void ISupportExternalScope.SetScopeProvider(IExternalScopeProvider scopeProvider) + { + _scopeProvider = scopeProvider; } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Logging/RollingFile/Internal/LogMessage.cs b/src/OSharp.Utils/Logging/RollingFile/Internal/LogMessage.cs new file mode 100644 index 000000000..56d5a136f --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/Internal/LogMessage.cs @@ -0,0 +1,11 @@ +using System; + + +namespace OSharp.Logging.RollingFile.Internal +{ + public struct LogMessage + { + public DateTimeOffset Timestamp { get; set; } + public string Message { get; set; } + } +} diff --git a/src/OSharp.Utils/Logging/RollingFile/PeriodicityOptions.cs b/src/OSharp.Utils/Logging/RollingFile/PeriodicityOptions.cs new file mode 100644 index 000000000..bb9b2e18d --- /dev/null +++ b/src/OSharp.Utils/Logging/RollingFile/PeriodicityOptions.cs @@ -0,0 +1,9 @@ +namespace OSharp.Logging.RollingFile +{ + public enum PeriodicityOptions { + Daily, + Hourly, + Minutely, + Monthly + } +} diff --git a/src/OSharp.Utils/Logging/RollingFile/RollingFileLoggerProvider.cs b/src/OSharp.Utils/Logging/RollingFile/RollingFileLoggerProvider.cs deleted file mode 100644 index 110487e1a..000000000 --- a/src/OSharp.Utils/Logging/RollingFile/RollingFileLoggerProvider.cs +++ /dev/null @@ -1,105 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) 2014-2017 OSharp. All rights reserved. -// -// http://www.osharp.org -// -// 2017-09-17 21:19 -// ----------------------------------------------------------------------- - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using OSharp.Logging.RollingFile.Internal; - - -namespace OSharp.Logging.RollingFile -{ - /// - /// An that writes logs to a file - /// - ///power by https://github.com/andrewlock/NetEscapades.Extensions.Logging - [ProviderAlias("File")] - public class FileLoggerProvider : BatchingLoggerProvider - { - private readonly string _fileName; - private readonly int? _maxFileSize; - private readonly int? _maxRetainedFiles; - private readonly string _path; - - /// - /// Creates an instance of the - /// - /// The options object controlling the logger - public FileLoggerProvider(IOptions options) - : base(options) - { - var loggerOptions = options.Value; - _path = loggerOptions.LogDirectory; - _fileName = loggerOptions.FileName; - _maxFileSize = loggerOptions.FileSizeLimit; - _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; - } - - /// - protected override async Task WriteMessagesAsync(IEnumerable messages, CancellationToken cancellationToken) - { - Directory.CreateDirectory(_path); - - foreach (var group in messages.GroupBy(GetGrouping)) - { - var fullName = GetFullName(group.Key); - var fileInfo = new FileInfo(fullName); - if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize) - { - return; - } - - using (var streamWriter = File.AppendText(fullName)) - { - foreach (var item in group) - { - await streamWriter.WriteAsync(item.Message); - } - } - } - - RollFiles(); - } - - private string GetFullName((int Year, int Month, int Day) group) - { - return Path.Combine(_path, $"{_fileName}{group.Year:0000}{group.Month:00}{group.Day:00}.txt"); - } - - private (int Year, int Month, int Day) GetGrouping(LogMessageEntry message) - { - return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day); - } - - /// - /// Deletes old log files, keeping a number of files defined by - /// - protected void RollFiles() - { - if (_maxRetainedFiles > 0) - { - var files = new DirectoryInfo(_path) - .GetFiles(_fileName + "*") - .OrderByDescending(f => f.Name) - .Skip(_maxRetainedFiles.Value); - - foreach (var item in files) - { - item.Delete(); - } - } - } - } -} \ No newline at end of file diff --git a/src/OSharp.Utils/OSharp.Utils.csproj b/src/OSharp.Utils/OSharp.Utils.csproj index cadc4ef9f..4806cc0df 100644 --- a/src/OSharp.Utils/OSharp.Utils.csproj +++ b/src/OSharp.Utils/OSharp.Utils.csproj @@ -4,7 +4,7 @@ - netstandard2.0;net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Utils OSharp工具组件,封装着框架常用的工具类 OSharp工具组件 @@ -13,40 +13,18 @@ true disable - + - - - - - - - - - - - - - - - - - - - - - - + - @@ -63,7 +41,6 @@ - @@ -75,6 +52,22 @@ + + + + + + + + + + + + + + + + True diff --git a/src/OSharp.Utils/Reflection/ComLibraryLoader.cs b/src/OSharp.Utils/Reflection/ComLibraryLoader.cs index a776cd443..1bc3ff743 100644 --- a/src/OSharp.Utils/Reflection/ComLibraryLoader.cs +++ b/src/OSharp.Utils/Reflection/ComLibraryLoader.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2017 OSharp. All rights reserved. // @@ -101,13 +101,12 @@ public object CreateObjectFromPath(string dllPath, Guid clsid, bool setSearchPat return Activator.CreateInstance(type); } - protected override void Dispose(bool disposing) + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() { - if (!Disposed) - { - NativeMethods.FreeLibrary(_lib); - } - base.Dispose(disposing); + NativeMethods.FreeLibrary(_lib); } } -} \ No newline at end of file +} diff --git a/src/OSharp.Utils/Security/Crypto.Readme.md b/src/OSharp.Utils/Security/Crypto.Readme.md new file mode 100644 index 000000000..6d70e403c --- /dev/null +++ b/src/OSharp.Utils/Security/Crypto.Readme.md @@ -0,0 +1,234 @@ +# Crypto 加密解密工具类 + +`Crypto` 是一个功能完整的加密解密工具类,提供了 AES、RSA 以及混合加密等多种加密方式,支持字符串、字节数组和文件的加密解密操作。 + +## 功能特性 + +- **AES 加密解密**:支持 256 位密钥的 AES 加密,使用 CBC 模式和 PKCS7 填充 +- **RSA 加密解密**:支持 RSA 公钥加密和私钥解密,使用 OAEP-SHA256 填充 +- **RSA 数字签名**:支持数据签名和验证,确保数据完整性和身份认证 +- **混合加密**:结合 AES 和 RSA 的优势,提供高性能和高安全性的加密方案 +- **文件加密**:支持直接对文件进行加密和解密操作 +- **JSON 序列化**:加密数据支持 JSON 格式的序列化和反序列化 + +## 使用方法 + +### 1. AES 加密解密 + +#### 生成 AES 密钥 +```csharp +// 生成 32 字节的 AES 密钥 +byte[] aesKey = Crypto.GenerateAesKey(); +``` + +#### 加密字节数组 +```csharp +// 使用指定密钥加密 +byte[] data = Encoding.UTF8.GetBytes("Hello, World!"); +byte[] key = Crypto.GenerateAesKey(); +var (encryptData, returnedKey) = Crypto.AesEncrypt(data, key); + +// 使用随机生成的密钥加密 +var (encryptData2, randomKey) = Crypto.AesEncrypt(data, null); +``` + +#### 解密字节数组 +```csharp +byte[] decryptedData = Crypto.AesDecrypt(encryptData, key); +string result = Encoding.UTF8.GetString(decryptedData); +``` + +#### 加密字符串 +```csharp +string data = "Hello, World!"; +string base64Key = Convert.ToBase64String(Crypto.GenerateAesKey()); +var (encryptData, key) = Crypto.AesEncrypt(data, base64Key); +``` + +#### 解密字符串 +```csharp +string decryptedString = Crypto.AesDecrypt(encryptData, base64Key); +``` + +#### 文件加密解密 +```csharp +// 加密文件 +string sourceFile = "source.txt"; +string targetFile = "encrypted.txt"; +string base64Key = Convert.ToBase64String(Crypto.GenerateAesKey()); +var (encryptData, key) = Crypto.AesEncryptFile(sourceFile, targetFile, base64Key); + +// 解密文件 +string decryptFile = "decrypted.txt"; +Crypto.AesDecryptFile(targetFile, decryptFile, base64Key); +``` + +### 2. RSA 加密解密 + +#### 生成 RSA 密钥对 +```csharp +var (publicKey, privateKey) = Crypto.GenerateRsaKey(); +``` + +#### 加密数据 +```csharp +string data = "Hello, World!"; +string encryptedData = Crypto.RsaEncrypt(data, publicKey); +``` + +#### 解密数据 +```csharp +string decryptedData = Crypto.RsaDecrypt(encryptedData, privateKey); +``` + +#### 字节数组加密解密 +```csharp +byte[] data = Encoding.UTF8.GetBytes("Hello, World!"); +byte[] encryptedBytes = Crypto.RsaEncrypt(data, publicKey); +byte[] decryptedBytes = Crypto.RsaDecrypt(encryptedBytes, privateKey); +``` + +### 3. RSA 数字签名 + +#### 数据签名 +```csharp +string data = "Hello, World!"; +string signature = Crypto.RsaSignData(data, privateKey); +``` + +#### 验证签名 +```csharp +bool isValid = Crypto.RsaVerifyData(data, signature, publicKey); +``` + +#### 字节数组签名验证 +```csharp +byte[] data = Encoding.UTF8.GetBytes("Hello, World!"); +byte[] signature = Crypto.RsaSignData(data, privateKey); +bool isValid = Crypto.RsaVerifyData(data, signature, publicKey); +``` + +### 4. 混合加密(AES + RSA) + +混合加密结合了 AES 的高性能和 RSA 的安全性,适用于需要高性能和高安全性的场景。 + +#### 加密流程 +1. 使用自己的 RSA 私钥对数据进行签名 +2. 使用随机生成的 AES 密钥加密数据 +3. 使用对方的 RSA 公钥加密 AES 密钥 + +```csharp +string data = "Hello, World!"; +var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); +var (facePublicKey, facePrivateKey) = Crypto.GenerateRsaKey(); + +// 加密 +string hybridJson = Crypto.HybridEncrypt(data, ownPrivateKey, facePublicKey); +``` + +#### 解密流程 +1. 使用自己的 RSA 私钥解密 AES 密钥 +2. 使用 AES 密钥解密数据 +3. 使用对方的 RSA 公钥验证数据签名 + +```csharp +// 解密 +string decryptedData = Crypto.HybridDecrypt(hybridJson, facePrivateKey, ownPublicKey); +``` + +#### 字节数组混合加密 +```csharp +byte[] data = Encoding.UTF8.GetBytes("Hello, World!"); +HybridEncryptData hybridData = Crypto.HybridEncrypt(data, ownPrivateKey, facePublicKey); +byte[] decryptedData = Crypto.HybridDecrypt(hybridData, facePrivateKey, ownPublicKey); +``` + +## 数据结构 + +### AesEncryptData +AES 加密后的数据结构,包含加密数据和初始化向量(IV)。 + +```csharp +public class AesEncryptData +{ + public byte[] Iv { get; set; } // 初始化向量(16字节) + public byte[] CipherData { get; set; } // 加密后的数据 + + public string GetIvString() // 获取IV的Base64字符串 + public string GetCipherDataString() // 获取加密数据的Base64字符串 + public string ToJson() // 序列化为JSON + public static AesEncryptData FromJson(string json) // 从JSON反序列化 +} +``` + +### HybridEncryptData +混合加密后的数据结构,包含 AES 加密数据、签名和 RSA 加密的 AES 密钥。 + +```csharp +public class HybridEncryptData +{ + public AesEncryptData AesEncryptData { get; set; } // AES加密数据 + public byte[] Signature { get; set; } // 数据签名 + public byte[] RsaEncryptedAesKey { get; set; } // RSA加密的AES密钥 + + public string ToJson() // 序列化为JSON + public static HybridEncryptData FromJson(string json) // 从JSON反序列化 +} +``` + +## 安全注意事项 + +1. **密钥管理**:请妥善保管加密密钥,建议使用安全的密钥管理系统 +2. **RSA 数据长度限制**:RSA 加密有数据长度限制,对于 2048 位密钥,最大数据长度为 190 字节 +3. **混合加密**:对于大数据量,建议使用混合加密方案 +4. **签名验证**:混合加密会自动验证数据签名,确保数据完整性和身份认证 +5. **异常处理**:加密解密操作可能抛出异常,请做好异常处理 + +## 异常类型 + +- `ArgumentNullException`:参数为 null 时抛出 +- `ArgumentException`:参数格式不正确时抛出 +- `FileNotFoundException`:文件不存在时抛出 +- `CryptographicException`:加密解密失败或签名验证失败时抛出 + +## 示例代码 + +### 完整的加密解密示例 +```csharp +using System; +using System.Text; +using OSharp.Security; + +class Program +{ + static void Main() + { + // AES 加密示例 + string originalText = "这是一个测试数据"; + var (encryptData, aesKey) = Crypto.AesEncrypt(originalText, null); + string decryptedText = Crypto.AesDecrypt(encryptData, Convert.ToBase64String(aesKey)); + Console.WriteLine($"AES 解密结果: {decryptedText}"); + + // RSA 加密示例 + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + string rsaEncrypted = Crypto.RsaEncrypt(originalText, publicKey); + string rsaDecrypted = Crypto.RsaDecrypt(rsaEncrypted, privateKey); + Console.WriteLine($"RSA 解密结果: {rsaDecrypted}"); + + // 混合加密示例 + var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, facePrivateKey) = Crypto.GenerateRsaKey(); + + string hybridEncrypted = Crypto.HybridEncrypt(originalText, ownPrivateKey, facePublicKey); + string hybridDecrypted = Crypto.HybridDecrypt(hybridEncrypted, facePrivateKey, ownPublicKey); + Console.WriteLine($"混合加密解密结果: {hybridDecrypted}"); + } +} +``` + +## 版本信息 + +- **版本**:1.0.0 +- **作者**:郭明锋 +- **最后更新**:2025-10-18 +- **许可证**:Copyright (c) 2025 66SOFT. All rights reserved. diff --git a/src/OSharp.Utils/Security/Crypto.cs b/src/OSharp.Utils/Security/Crypto.cs new file mode 100644 index 000000000..ab7dee5f3 --- /dev/null +++ b/src/OSharp.Utils/Security/Crypto.cs @@ -0,0 +1,557 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2025 66SOFT. All rights reserved. +// +// https://ifs.66soft.net +// 郭明锋 +// 2025-10-18 14:10 +// ----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +using OSharp.Data; + +// ReSharper disable AssignNullToNotNullAttribute +namespace OSharp.Security +{ + /// + /// 加密解密工具类 + /// + public static class Crypto + { + #region AES加密解密 + + /// + /// 生成AES密钥 + /// + /// + public static byte[] GenerateAesKey() + { + using (var aes = CreateAes()) + { + aes.GenerateKey(); + return aes.Key; + } + } + + /// + /// AES加密 byte[] 数据 + /// + /// 要加密的byte[]数据 + /// AES密钥,必须是32位,如果为null,则使用AES随机密钥 + /// 加密后的数据和密钥 + public static (AesEncryptData EncryptData, byte[] Key) AesEncrypt(byte[] data, byte[] key = null) + { + Check.NotNull(data, nameof(data)); + Check.Required(key == null || key.Length == 32, "传入AES密钥必须是32位"); + + using (var aes = CreateAes()) + { + if (key != null && key.Length == 32) + { + aes.Key = key; + } + + using (var encryptor = aes.CreateEncryptor()) + { + using (var ms = new MemoryStream()) + { + using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + { + cs.Write(data, 0, data.Length); + cs.FlushFinalBlock(); + return (new AesEncryptData(ms.ToArray(), aes.IV), aes.Key); + } + } + } + } + } + + /// + /// AES解密 AesEncryptData 数据 + /// + /// 要解密的AesEncryptData数据 + /// AES密钥 + /// 解密后的数据 + public static byte[] AesDecrypt(AesEncryptData encryptData, byte[] key) + { + Check.NotNull(encryptData, nameof(encryptData)); + Check.Required(encryptData.Iv != null && encryptData.Iv.Length == 16, "无效的IV值"); + Check.Required(encryptData.CipherData != null && encryptData.CipherData.Length > 0, + "无效的加密数据"); + Check.Required(key != null && key.Length == 32, "无效的AES密钥"); + + using (var aes = CreateAes()) + { + using (var decryptor = aes.CreateDecryptor(key, encryptData.Iv)) + { + using (var ms = new MemoryStream(encryptData.CipherData)) + { + using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) + { + using (var decMs = new MemoryStream()) + { + cs.CopyTo(decMs); + return decMs.ToArray(); + } + } + } + } + } + } + + /// + /// AES加密 string 数据 + /// + /// 要加密的string数据 + /// AES密钥的Base64字符串,如果为null,则生成一个随机密钥 + /// 加密后的数据和密钥 + public static (AesEncryptData EncryptData, byte[] Key) AesEncrypt(string data, string base64Key = null) + { + Check.NotNull(data, nameof(data)); + var dataBytes = Encoding.UTF8.GetBytes(data); + byte[] keyBytes = null; + if (base64Key != null) + { + keyBytes = SafeFromBase64String(base64Key, nameof(base64Key), "无效的AES密钥Base64格式"); + } + + return AesEncrypt(dataBytes, keyBytes); + } + + /// + /// AES解密 AesEncryptData 数据 + /// + /// 要解密的AesEncryptData数据 + /// AES密钥的Base64字符串 + /// 解密后的数据 + public static string AesDecrypt(AesEncryptData encryptData, string base64Key) + { + Check.NotNull(encryptData, nameof(encryptData)); + Check.NotNullOrEmpty(base64Key, nameof(base64Key)); + + var keyBytes = SafeFromBase64String(base64Key, nameof(base64Key), "无效的AES密钥Base64格式"); + var dataBytes = AesDecrypt(encryptData, keyBytes); + return Encoding.UTF8.GetString(dataBytes); + } + + /// + /// AES加密文件 + /// + /// 要加密的源文件 + /// 写入加密数据的目标文件 + /// AES密钥的Base64字符串,如果为null,则生成一个随机密钥 + /// 加密后的数据和密钥 + public static (AesEncryptData EncryptData, byte[] Key) AesEncryptFile(string sourceFile, string targetFile, + string base64Key = null) + { + Check.NotNullOrEmpty(sourceFile, nameof(sourceFile)); + Check.Required(File.Exists(sourceFile), $"AES加密文件时,源文件 {sourceFile} 不存在。"); + Check.NotNullOrEmpty(targetFile, nameof(targetFile)); + + var dataBytes = File.ReadAllBytes(sourceFile); + byte[] keyBytes = null; + if (base64Key != null) + { + keyBytes = SafeFromBase64String(base64Key, nameof(base64Key), "无效的AES密钥Base64格式"); + } + + var result = AesEncrypt(dataBytes, keyBytes); + File.WriteAllText(targetFile, result.EncryptData.ToJson()); + return result; + } + + /// + /// AES解密文件 + /// + /// 要解密的加密文件 + /// 写入解密数据的目标文件 + /// AES密钥的Base64字符串 + /// 解密后的数据 + public static void AesDecryptFile(string encryptFile, string decryptFile, string base64Key) + { + Check.NotNullOrEmpty(encryptFile, nameof(encryptFile)); + Check.Required(File.Exists(encryptFile), $"AES解密文件时,加密文件 {encryptFile} 不存在"); + Check.NotNullOrEmpty(decryptFile, nameof(decryptFile)); + Check.NotNullOrEmpty(base64Key, nameof(base64Key)); + + var json = File.ReadAllText(encryptFile); + var encryptData = AesEncryptData.FromJson(json); + var keyBytes = SafeFromBase64String(base64Key, nameof(base64Key), "无效的AES密钥Base64格式"); + var dataBytes = AesDecrypt(encryptData, keyBytes); + File.WriteAllBytes(decryptFile, dataBytes); + } + + private static Aes CreateAes() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.KeySize = 256; + aes.BlockSize = 128; + return aes; + } + + #endregion + + #region RSA加密解密 + + /// + /// 生成RSA密钥 + /// + /// RSA密钥 + public static (string PublicKey, string PrivateKey) GenerateRsaKey() + { + using (var rsa = RSA.Create()) + { + return (PublicKey: rsa.ToXmlString(false), PrivateKey: rsa.ToXmlString(true)); + } + } + + /// + /// RSA加密 byte[] 数据 + /// + /// 要加密的byte[]数据 + /// RSA公钥 + /// 加密后的数据 + public static byte[] RsaEncrypt(byte[] data, string publicKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNullOrEmpty(publicKey, nameof(publicKey)); + + using (var rsa = RSA.Create()) + { + rsa.FromXmlString(publicKey); + + // 检查数据长度是否超过RSA密钥的限制 + // 对于OAEP-SHA256填充:最大数据长度 = 密钥长度(字节) - 2 * 哈希长度(字节) - 2 + // 对于2048位密钥:256 - 2 * 32 - 2 = 190字节 + var maxDataLength = rsa.KeySize / 8 - 2 * 32 - 2; // 32是SHA256的字节长度 + if (data.Length > maxDataLength) + { + throw new ArgumentException( + $@"数据长度({data.Length}字节)超过RSA密钥({rsa.KeySize}位)的最大限制({maxDataLength}字节)。请使用混合加密或分块加密。", + nameof(data)); + } + + return rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + } + + /// + /// RSA解密 byte[] 数据 + /// + /// 要解密的byte[]数据 + /// RSA私钥 + /// 解密后的数据 + public static byte[] RsaDecrypt(byte[] data, string privateKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNullOrEmpty(privateKey, nameof(privateKey)); + + using (var rsa = RSA.Create()) + { + rsa.FromXmlString(privateKey); + return rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256); + } + } + + + /// + /// 使用指定私钥对明文进行签名,返回明文签名的字节数组 + /// + /// 要签名的明文字节数组 + /// RSA私钥 + /// 明文数据的签名字节数组 + public static byte[] RsaSignData(byte[] data, string privateKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNullOrEmpty(privateKey, nameof(privateKey)); + + using (var rsa = RSA.Create()) + { + rsa.FromXmlString(privateKey); + return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + } + + /// + /// 使用指定公钥验证解密得到的明文是否符合签名 + /// + /// 解密的明文字节数组 + /// 明文签名字节数组 + /// RSA公钥 + /// 验证是否通过 + public static bool RsaVerifyData(byte[] data, byte[] signature, string publicKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNull(signature, nameof(signature)); + Check.NotNullOrEmpty(publicKey, nameof(publicKey)); + + using (var rsa = RSA.Create()) + { + rsa.FromXmlString(publicKey); + return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + } + + /// + /// RSA加密 string 数据 + /// + /// 要加密的string数据 + /// RSA公钥 + /// 加密后的数据 + public static string RsaEncrypt(string data, string publicKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNull(publicKey, nameof(publicKey)); + + var dataBytes = Encoding.UTF8.GetBytes(data); + var encryptedBytes = RsaEncrypt(dataBytes, publicKey); + return Convert.ToBase64String(encryptedBytes); + } + + /// + /// RSA解密 string 数据 + /// + /// 要解密的string数据 + /// RSA私钥 + /// 解密后的数据 + public static string RsaDecrypt(string data, string privateKey) + { + Check.NotNullOrEmpty(data, nameof(data)); + Check.NotNull(privateKey, nameof(privateKey)); + + var dataBytes = SafeFromBase64String(data, nameof(data), "无效的RSA加密数据Base64格式"); + var decryptedBytes = RsaDecrypt(dataBytes, privateKey); + return Encoding.UTF8.GetString(decryptedBytes); + } + + + /// + /// 使用指定私钥对明文进行签名,返回明文签名的Base64字符串 + /// + /// 要签名的明文字符串 + /// RSA私钥 + /// 明文签名的Base64字符串 + public static string RsaSignData(string data, string privateKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNull(privateKey, nameof(privateKey)); + + var dataBytes = Encoding.UTF8.GetBytes(data); + var signature = RsaSignData(dataBytes, privateKey); + return Convert.ToBase64String(signature); + } + + /// + /// 使用指定公钥验证解密得到的明文是否符合签名 + /// + /// 解密的明文字符串 + /// 明文签名的Base64字符串 + /// RSA公钥 + /// 验证是否通过 + public static bool RsaVerifyData(string data, string signature, string publicKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNullOrEmpty(signature, nameof(signature)); + Check.NotNull(publicKey, nameof(publicKey)); + + var signatureBytes = SafeFromBase64String(signature, nameof(signature), "无效的RSA签名Base64格式"); + var dataBytes = Encoding.UTF8.GetBytes(data); + return RsaVerifyData(dataBytes, signatureBytes, publicKey); + } + + #endregion + + #region AES+RSA组合加密 + + /// + /// AES+RSA组合加密,使用自己的RSA私钥对要加密的数据进行签名,使用AES随机生成的密钥加密数据,使用对方的RSA公钥加密AES密钥 + /// + /// 要加密的明文字节数组 + /// 自己的RSA私钥 + /// 对方的RSA公钥 + /// 组合加密后的数据 + public static HybridEncryptData HybridEncrypt(byte[] data, string ownRsaPrivateKey, string faceRsaPublicKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNullOrEmpty(ownRsaPrivateKey, nameof(ownRsaPrivateKey)); + Check.NotNullOrEmpty(faceRsaPublicKey, nameof(faceRsaPublicKey)); + + //使用自己的RSA私钥对要加密的数据进行签名 + var signature = RsaSignData(data, ownRsaPrivateKey); + + //使用AES随机生成的密钥加密数据 + var aesEncryptResult = AesEncrypt(data); + + //使用对方的RSA公钥加密AES密钥 + var rsaEncryptedAesKey = RsaEncrypt(aesEncryptResult.Key, faceRsaPublicKey); + + var hybridEncryptData = new HybridEncryptData + { + AesEncryptData = aesEncryptResult.EncryptData, + Signature = signature, + RsaEncryptedAesKey = rsaEncryptedAesKey + }; + return hybridEncryptData; + } + + /// + /// AES+RSA组合解密,使用自己的RSA私钥解密AES密钥,使用AES密钥解密数据,使用对方的RSA公钥验证解密数据的签名 + /// + /// 组合加密后的数据 + /// 自己的RSA私钥 + /// 对方的RSA公钥 + /// 解密后的数据 + /// 解密后的数据签名验证失败 + public static byte[] HybridDecrypt(HybridEncryptData hybridEncryptData, string ownRsaPrivateKey, + string faceRsaPublicKey) + { + Check.NotNull(hybridEncryptData, nameof(hybridEncryptData)); + Check.NotNullOrEmpty(ownRsaPrivateKey, nameof(ownRsaPrivateKey)); + Check.NotNullOrEmpty(faceRsaPublicKey, nameof(faceRsaPublicKey)); + + // 使用自己的私钥解密AES密钥 + var aesKey = RsaDecrypt(hybridEncryptData.RsaEncryptedAesKey, ownRsaPrivateKey); + + // 使用AES密钥解密数据 + var decryptedData = AesDecrypt(hybridEncryptData.AesEncryptData, aesKey); + + // 使用对方的RSA公钥验证解密数据的签名 + var isValid = RsaVerifyData(decryptedData, hybridEncryptData.Signature, faceRsaPublicKey); + if (!isValid) + { + throw new CryptographicException("解密后的数据签名验证失败"); + } + + return decryptedData; + } + + /// + /// AES+RSA组合加密,使用自己的RSA私钥对要加密的数据进行签名,使用AES随机生成的密钥加密数据,使用对方的RSA公钥加密AES密钥 + /// + /// 要加密的明文字符串 + /// 自己的RSA私钥 + /// 对方的RSA公钥 + /// 组合加密后的数据 + public static string HybridEncrypt(string data, string ownRsaPrivateKey, string faceRsaPublicKey) + { + Check.NotNull(data, nameof(data)); + Check.NotNull(ownRsaPrivateKey, nameof(ownRsaPrivateKey)); + Check.NotNull(faceRsaPublicKey, nameof(faceRsaPublicKey)); + + var dataBytes = Encoding.UTF8.GetBytes(data); + var hybridEncryptData = HybridEncrypt(dataBytes, ownRsaPrivateKey, faceRsaPublicKey); + return hybridEncryptData.ToJson(); + } + + /// + /// AES+RSA组合解密,使用自己的RSA私钥解密AES密钥,使用AES密钥解密数据,使用对方的RSA公钥验证解密数据的签名 + /// + /// 组合加密后的数据 + /// 自己的RSA私钥 + /// 对方的RSA公钥 + /// 解密后的数据 + /// 解密后的数据签名验证失败 + public static string HybridDecrypt(string json, string ownRsaPrivateKey, string faceRsaPublicKey) + { + Check.NotNullOrEmpty(json, nameof(json)); + Check.NotNull(ownRsaPrivateKey, nameof(ownRsaPrivateKey)); + Check.NotNull(faceRsaPublicKey, nameof(faceRsaPublicKey)); + + var hybridEncryptData = HybridEncryptData.FromJson(json); + var decryptedData = HybridDecrypt(hybridEncryptData, ownRsaPrivateKey, faceRsaPublicKey); + return Encoding.UTF8.GetString(decryptedData); + } + + /// + /// 安全地将Base64字符串转换为字节数组 + /// + /// Base64字符串 + /// 参数名称,用于异常消息 + /// 自定义错误消息,如果为null则使用默认消息 + /// 转换后的字节数组 + /// 当Base64格式无效时 + private static byte[] SafeFromBase64String(string base64String, string paramName, string customMessage = null) + { + try + { + return Convert.FromBase64String(base64String); + } + catch (FormatException) + { + var message = customMessage ?? $"无效的Base64格式: {paramName}"; + throw new ArgumentException(message, paramName); + } + } + + #endregion + } + + /// + /// AES加密后的数据,包含加密后的数据和IV + /// + public class AesEncryptData + { + // 无参构造函数,支持JSON序列化 + public AesEncryptData() + { } + + public AesEncryptData(byte[] cipherData, byte[] iv) + { + CipherData = cipherData ?? throw new ArgumentNullException(nameof(cipherData)); + Iv = iv ?? throw new ArgumentNullException(nameof(iv)); + + if (Iv.Length != 16) + throw new ArgumentException(@"IV长度必须为16字节", nameof(iv)); + } + + public byte[] Iv { get; set; } + public byte[] CipherData { get; set; } + + public string GetIvString() + { + return Convert.ToBase64String(Iv); + } + + public string GetCipherDataString() + { + return Convert.ToBase64String(CipherData); + } + + public string ToJson() + { + return JsonSerializer.Serialize(this); + } + + public static AesEncryptData FromJson(string json) + { + return JsonSerializer.Deserialize(json); + } + } + + /// + /// 组合加密后的数据,AES加密数据、AES向量IV、RSA加密的AES密钥、数据签名 + /// + public class HybridEncryptData + { + public AesEncryptData AesEncryptData { get; set; } + public byte[] Signature { get; set; } + public byte[] RsaEncryptedAesKey { get; set; } + + public string ToJson() + { + return JsonSerializer.Serialize(this); + } + + public static HybridEncryptData FromJson(string json) + { + return JsonSerializer.Deserialize(json); + } + } +} diff --git a/src/OSharp.Utils/Timing/TimeSpanExtensions.cs b/src/OSharp.Utils/Timing/TimeSpanExtensions.cs index 62195f0a8..7c8cd9b63 100644 --- a/src/OSharp.Utils/Timing/TimeSpanExtensions.cs +++ b/src/OSharp.Utils/Timing/TimeSpanExtensions.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------- +// ----------------------------------------------------------------------- // // Copyright (c) 2014-2020 OSharp. All rights reserved. // @@ -53,12 +53,16 @@ public static string ToTimeString(this TimeSpan ts) /// /// 时间片倒计时 /// - public static void CountDown(this TimeSpan ts, Action action, int intervalMilliseconds = 1000) + public static void CountDown(this TimeSpan ts, Action action, int intervalMilliseconds = 1000, Func breakFunc = null) { while (ts > TimeSpan.Zero) { + if (breakFunc != null && breakFunc()) + { + break; + } action(ts); - TimeSpan ts2 = TimeSpan.FromMilliseconds(intervalMilliseconds); + var ts2 = TimeSpan.FromMilliseconds(intervalMilliseconds); Thread.Sleep(ts2); ts = ts.Subtract(ts2); } @@ -67,10 +71,14 @@ public static void CountDown(this TimeSpan ts, Action action, int inte /// /// 时间片倒计时 /// - public static async Task CountDownAsync(this TimeSpan ts, Func action, int intervalMilliseconds = 1000) + public static async Task CountDownAsync(this TimeSpan ts, Func action, int intervalMilliseconds = 1000, Func> breakFunc = null) { while (ts > TimeSpan.Zero) { + if (breakFunc != null && await breakFunc()) + { + break; + } await action(ts); TimeSpan ts2 = TimeSpan.FromMilliseconds(intervalMilliseconds); await Task.Delay(ts2); @@ -78,4 +86,4 @@ public static async Task CountDownAsync(this TimeSpan ts, Func a } } } -} \ No newline at end of file +} diff --git a/src/OSharp.Wpf/Hubs/HubClientBase.cs b/src/OSharp.Wpf/Hubs/HubClientBase.cs index 1679d1e2f..0674ceee2 100644 --- a/src/OSharp.Wpf/Hubs/HubClientBase.cs +++ b/src/OSharp.Wpf/Hubs/HubClientBase.cs @@ -161,8 +161,11 @@ protected virtual async Task OnClosed(Exception error) { int delay = Random.Next(0, 5); await Output.StatusBarCountdown("{0}秒后重试连接通信服务器", delay); - await HubConnection.StartAsync(); - Output.StatusBar($"通信服务器连接{(HubConnection.State == HubConnectionState.Connected ? "成功" : "失败")}"); + if (HubConnection.State == HubConnectionState.Disconnected) + { + await HubConnection.StartAsync(); + Output.StatusBar($"通信服务器连接{(HubConnection.State == HubConnectionState.Connected ? "成功" : "失败")}"); + } } /// diff --git a/src/OSharp.Wpf/OSharp.Wpf.csproj b/src/OSharp.Wpf/OSharp.Wpf.csproj index 0e648082e..9c083d961 100644 --- a/src/OSharp.Wpf/OSharp.Wpf.csproj +++ b/src/OSharp.Wpf/OSharp.Wpf.csproj @@ -4,7 +4,7 @@ - net6.0-windows;net7.0-windows;net8.0-windows;net9.0-windows + net8.0-windows;net9.0-windows;net9.0-windows OSharp.Wpf OSharp Wpf 客户端组件 OSharp Wpf 客户端组件,封装Wpf客户端的辅助操作 @@ -18,20 +18,20 @@ - + - - - - - - + - + + + + + + diff --git a/src/OSharp.Wpf/Stylet/IoC.cs b/src/OSharp.Wpf/Stylet/IoC.cs index 78a2d5431..004e2d21b 100644 --- a/src/OSharp.Wpf/Stylet/IoC.cs +++ b/src/OSharp.Wpf/Stylet/IoC.cs @@ -20,14 +20,14 @@ public static class IoC /// /// 获取指定服务类型的单个实例 /// - public static Func GetInstance = (service, key) => throw new InvalidOperationException("IoC is not initialized"); + public static Func GetInstance = (_, _) => throw new InvalidOperationException("IoC is not initialized"); /// /// 获取指定服务类型的多个实例 /// - public static Func> GetAllInstances = service => throw new InvalidOperationException("IoC is not initialized"); + public static Func> GetAllInstances = _ => throw new InvalidOperationException("IoC is not initialized"); - public static Action BuildUp = instance => throw new InvalidOperationException("IoC is not initialized"); + public static Action BuildUp = _ => throw new InvalidOperationException("IoC is not initialized"); /// /// 获取指定服务类型的单个实例 @@ -62,7 +62,7 @@ public static void Initialize(IContainer container) /// public static void Initialize(IServiceProvider provider) { - GetInstance = (type, key) => provider.GetService(type); + GetInstance = (type, _) => provider.GetService(type); GetAllInstances = provider.GetServices; } } diff --git a/src/OSharp.Wpf/Stylet/ServiceProviderBootstrapper.cs b/src/OSharp.Wpf/Stylet/ServiceProviderBootstrapper.cs index 1d458f984..11ee1db06 100644 --- a/src/OSharp.Wpf/Stylet/ServiceProviderBootstrapper.cs +++ b/src/OSharp.Wpf/Stylet/ServiceProviderBootstrapper.cs @@ -7,31 +7,43 @@ // 2020-05-28 15:00 // ----------------------------------------------------------------------- +using Microsoft.Extensions.Hosting; + + namespace OSharp.Wpf.Stylet; public abstract class ServiceProviderBootstrapper : BootstrapperBase where TRootViewModel : class { - private object _rootViewModel; + private HostApplicationBuilder _hostBuilder; + private IHost _host; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private TRootViewModel _rootViewModel; + protected virtual TRootViewModel RootViewModel => _rootViewModel ??= (TRootViewModel)GetInstance(typeof(TRootViewModel)); - protected virtual object RootViewModel + protected IServiceProvider ServiceProvider { get; private set; } + + /// + /// Called on application startup. This occur after this.Args has been assigned, but before the IoC container has been configured + /// + protected override void OnStart() { - get { return _rootViewModel ??= ServiceProvider.GetService(typeof(TRootViewModel)); } + _hostBuilder = Host.CreateApplicationBuilder(); + _hostBuilder.Environment.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"; } - protected IServiceProvider ServiceProvider { get; private set; } - /// /// Overridden from BootstrapperBase, this sets up the IoC container /// - protected sealed override void ConfigureBootstrapper() + protected override void ConfigureBootstrapper() { - IServiceCollection services = new ServiceCollection(); + _hostBuilder.Services.AddSingleton(_hostBuilder); + DefaultConfigureIoC(_hostBuilder.Services); + ConfigureIoC(_hostBuilder.Services); - // Call DefaultConfigureIoC *after* ConfigureIoIC, so that they can customize builder.Assemblies - this.DefaultConfigureIoC(services); - this.ConfigureIoC(services); - - ServiceProvider = services.BuildServiceProvider(); + _host = _hostBuilder.Build(); + ServiceProvider = _host.Services; + _host.StartAsync(_cancellationTokenSource.Token).GetAwaiter().GetResult(); } protected virtual void ConfigureIoC(IServiceCollection services) @@ -39,22 +51,21 @@ protected virtual void ConfigureIoC(IServiceCollection services) protected virtual void DefaultConfigureIoC(IServiceCollection services) { - ViewManagerConfig viewManagerConfig = new ViewManagerConfig() + var viewManagerConfig = new ViewManagerConfig() { - ViewFactory = this.GetInstance, - ViewAssemblies = new List() { GetType().Assembly } + ViewFactory = GetInstance, + ViewAssemblies = [GetType().Assembly] }; - services.AddSingleton(viewManagerConfig); - services.AddSingleton(); + services.AddSingleton(new ViewManager(viewManagerConfig)); + services.AddTransient(); + services.AddSingleton(this); - services.AddSingleton(p => new WindowManager( - p.GetRequiredService(), - p.GetRequiredService, - p.GetRequiredService())); + services.AddSingleton(); services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); // Not singleton! + // Also need a factory + services.AddSingleton>(() => new MessageBoxViewModel()); } /// @@ -75,12 +86,39 @@ public override object GetInstance(Type type) return ServiceProvider?.GetService(type); } + /// Hook called on application exit + /// The exit event data + protected override void OnExit(ExitEventArgs e) + { + base.OnExit(e); + // 在应用程序退出时停止 host + _cancellationTokenSource.Cancel(); + try + { + _host?.StopAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + // 记录日志但不阻止退出 + Debug.WriteLine($"Error stopping host during exit: {ex.Message}"); + } + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public override void Dispose() { base.Dispose(); + try + { + _host?.StopAsync().GetAwaiter().GetResult(); + } + finally + { + _host?.Dispose(); + _cancellationTokenSource.Dispose(); + } ScreenExtensions.TryDispose(_rootViewModel); } } diff --git a/src/OSharp/Dependency/ServiceLocator.cs b/src/OSharp/Dependency/ServiceLocator.cs index 96a64a8f6..040965038 100644 --- a/src/OSharp/Dependency/ServiceLocator.cs +++ b/src/OSharp/Dependency/ServiceLocator.cs @@ -273,10 +273,12 @@ public async Task ExecuteScopedWorkAsync(Func + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() { _services = null; _provider = null; - base.Dispose(disposing); } -} \ No newline at end of file +} diff --git a/src/OSharp/EventBuses/EventHandlerBase.cs b/src/OSharp/EventBuses/EventHandlerBase.cs index 47806ade1..cf3939259 100644 --- a/src/OSharp/EventBuses/EventHandlerBase.cs +++ b/src/OSharp/EventBuses/EventHandlerBase.cs @@ -43,7 +43,7 @@ public virtual void Handle(IEventData eventData) /// 事件源数据 /// 异步取消标识 /// - public virtual Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default(CancellationToken)) + public virtual Task HandleAsync(IEventData eventData, CancellationToken cancelToken = default) { if (!CanHandle(eventData)) { @@ -56,7 +56,8 @@ public virtual void Handle(IEventData eventData) /// 事件处理 /// /// 事件源数据 - public abstract void Handle(TEventData eventData); + public virtual void Handle(TEventData eventData) + { } /// /// 异步事件处理 @@ -64,8 +65,8 @@ public virtual void Handle(IEventData eventData) /// 事件源数据 /// 异步取消标识 /// 是否成功 - public virtual Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default(CancellationToken)) + public virtual Task HandleAsync(TEventData eventData, CancellationToken cancelToken = default) { return Task.Run(() => Handle(eventData), cancelToken); } -} \ No newline at end of file +} diff --git a/src/OSharp/EventBuses/Internal/EventHandlerDisposeWrapper.cs b/src/OSharp/EventBuses/Internal/EventHandlerDisposeWrapper.cs index 7bf1d9289..8d439c6cc 100644 --- a/src/OSharp/EventBuses/Internal/EventHandlerDisposeWrapper.cs +++ b/src/OSharp/EventBuses/Internal/EventHandlerDisposeWrapper.cs @@ -30,12 +30,11 @@ public EventHandlerDisposeWrapper(IEventHandler eventHandler, Action disposeActi /// public IEventHandler EventHandler { get; set; } - protected override void Dispose(bool disposing) + /// + /// 重写以实现释放派生类资源的逻辑 + /// + protected override void Disposing() { - if (!Disposed) - { - _disposeAction?.Invoke(); - } - base.Dispose(disposing); + _disposeAction?.Invoke(); } -} \ No newline at end of file +} diff --git a/src/OSharp/Filter/FilterHelper.cs b/src/OSharp/Filter/FilterHelper.cs index d236132ce..b748620b5 100644 --- a/src/OSharp/Filter/FilterHelper.cs +++ b/src/OSharp/Filter/FilterHelper.cs @@ -7,6 +7,8 @@ // 2018-07-15 10:22 // ----------------------------------------------------------------------- +using System.Text.Json; + namespace OSharp.Filter; /// @@ -364,39 +366,56 @@ private static bool CheckFilterRule(Type type, FilterRule rule) private static Expression ChangeTypeToExpression(FilterRule rule, Type conversionType) { - //if (item.Method == QueryMethod.StdIn) - //{ - // Array array = (item.Value as Array); - // List expressionList = new List(); - // if (array != null) - // { - // expressionList.AddRange(array.Cast().Select((t, i) => - // ChangeType(array.GetValue(i), conversionType)).Select(newValue => Expression.Constant(newValue, conversionType))); - // } - // return Expression.NewArrayInit(conversionType, expressionList); - //} - if (rule.Value?.ToString() == UserFlagAttribute.Token) + if (rule.Value == null) + { + return Expression.Constant(null, conversionType); + } + + var valueType = rule.Value.GetType(); + var value = rule.Value; + + // 处理 JsonElement 类型 + if (valueType.Name == "JsonElement") { - // todo: 将UserFlag之类的功能提升为接口进行服务注册,好方便实现自定义XXXFlag - if (rule.Operate != FilterOperate.Equal) + var jsonElement = (JsonElement)value; + value = jsonElement.ValueKind switch { - throw new OsharpException($"当前用户“{rule.Value}”只能用在“{FilterOperate.Equal.ToDescription()}”操作中"); - } - ClaimsPrincipal user = ServiceLocator.Instance.GetCurrentUser(); - if (user == null || !user.Identity.IsAuthenticated) + JsonValueKind.String => jsonElement.GetString(), + JsonValueKind.Number => jsonElement.TryGetInt64(out var l) ? l : jsonElement.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => value + }; + if (value != null) { - throw new OsharpException("需要获取当前用户编号,但当前用户为空,可能未登录或已过期"); + valueType = value.GetType(); } - object value = user.Identity.GetClaimValueFirstOrDefault(ClaimTypes.NameIdentifier); - value = value.CastTo(conversionType); - return Expression.Constant(value, conversionType); } - else + + if (value == null) { - object value = rule.Value.CastTo(conversionType); - return Expression.Constant(value, conversionType); + return Expression.Constant(null, conversionType); } + + // 获取目标类型(如果是可空类型,获取其基础类型) + var targetType = conversionType.IsNullableType() + ? Nullable.GetUnderlyingType(conversionType) ?? conversionType + : conversionType; + + // 转换值到目标类型 + if (targetType.IsEnum) + { + value = valueType == typeof(string) ? Enum.Parse(targetType, value.ToString()!) : Enum.ToObject(targetType, value); + } + else if (valueType != targetType) + { + value = targetType == typeof(Guid) ? Guid.Parse(value.ToString()!) : Convert.ChangeType(value, targetType); + } + + // 返回与conversionType匹配的常量表达式 + return Expression.Constant(value, conversionType); } #endregion -} \ No newline at end of file +} diff --git a/src/OSharp/OSharp.csproj b/src/OSharp/OSharp.csproj index 5354f8241..44130e7a1 100644 --- a/src/OSharp/OSharp.csproj +++ b/src/OSharp/OSharp.csproj @@ -4,7 +4,7 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0;net10.0 OSharp.Core OSharp核心组件,封装着框架核心及数据存储,缓存,辅助操作等功能 OSharp核心组件 diff --git a/tests/OSharp.Tests/Security/CryptoTests.cs b/tests/OSharp.Tests/Security/CryptoTests.cs new file mode 100644 index 000000000..480d26472 --- /dev/null +++ b/tests/OSharp.Tests/Security/CryptoTests.cs @@ -0,0 +1,812 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OSharp.Security; + +namespace OSharp.Tests.Security +{ + /// + /// Crypto类的单元测试 + /// + [TestClass] + public class CryptoTests + { + #region AES加密解密测试 + + [TestMethod] + public void GenerateAesKey_ShouldReturn32Bytes() + { + // Act + var key = Crypto.GenerateAesKey(); + + // Assert + Assert.IsNotNull(key); + Assert.AreEqual(32, key.Length); + } + + [TestMethod] + public void AesEncrypt_WithByteArray_ShouldEncryptSuccessfully() + { + // Arrange + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var key = Crypto.GenerateAesKey(); + + // Act + var (encryptData, returnedKey) = Crypto.AesEncrypt(data, key); + + // Assert + Assert.IsNotNull(encryptData); + Assert.IsNotNull(encryptData.CipherData); + Assert.IsNotNull(encryptData.Iv); + Assert.AreEqual(16, encryptData.Iv.Length); + Assert.AreEqual(key, returnedKey); + Assert.AreNotEqual(data, encryptData.CipherData); + } + + [TestMethod] + public void AesEncrypt_WithNullKey_ShouldGenerateRandomKey() + { + // Arrange + var data = Encoding.UTF8.GetBytes("Hello, World!"); + + // Act + var (encryptData, key) = Crypto.AesEncrypt(data, null); + + // Assert + Assert.IsNotNull(encryptData); + Assert.IsNotNull(key); + Assert.AreEqual(32, key.Length); + } + + [TestMethod] + public void AesDecrypt_ShouldDecryptSuccessfully() + { + // Arrange + var originalData = Encoding.UTF8.GetBytes("Hello, World!"); + var (encryptData, key) = Crypto.AesEncrypt(originalData, null); + + // Act + var decryptedData = Crypto.AesDecrypt(encryptData, key); + + // Assert + Assert.AreEqual(originalData, decryptedData); + } + + [TestMethod] + public void AesEncrypt_WithString_ShouldEncryptSuccessfully() + { + // Arrange + var data = "Hello, World!"; + var key = Convert.ToBase64String(Crypto.GenerateAesKey()); + + // Act + var (encryptData, returnedKey) = Crypto.AesEncrypt(data, key); + + // Assert + Assert.IsNotNull(encryptData); + Assert.IsNotNull(returnedKey); + } + + [TestMethod] + public void AesDecrypt_WithString_ShouldDecryptSuccessfully() + { + // Arrange + var originalData = "Hello, World!"; + var (encryptData, key) = Crypto.AesEncrypt(originalData, null); + var base64Key = Convert.ToBase64String(key); + + // Act + var decryptedData = Crypto.AesDecrypt(encryptData, base64Key); + + // Assert + Assert.AreEqual(originalData, decryptedData); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AesEncrypt_WithInvalidKeyLength_ShouldThrowException() + { + // Arrange + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var invalidKey = new byte[16]; // 应该是32字节 + + // Act + Crypto.AesEncrypt(data, invalidKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AesDecrypt_WithInvalidKeyLength_ShouldThrowException() + { + // Arrange + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var (encryptData, _) = Crypto.AesEncrypt(data, null); + var invalidKey = new byte[16]; // 应该是32字节 + + // Act + Crypto.AesDecrypt(encryptData, invalidKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AesDecrypt_WithInvalidBase64Key_ShouldThrowException() + { + // Arrange + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var (encryptData, _) = Crypto.AesEncrypt(data, null); + var invalidBase64Key = "invalid-base64-string"; + + // Act + Crypto.AesDecrypt(encryptData, invalidBase64Key); + } + + #endregion + + #region RSA加密解密测试 + + [TestMethod] + public void GenerateRsaKey_ShouldReturnValidKeys() + { + // Act + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + + // Assert + Assert.IsNotNull(publicKey); + Assert.IsNotNull(privateKey); + Assert.IsTrue(publicKey.Contains("")); + Assert.IsTrue(privateKey.Contains("")); + Assert.IsTrue(privateKey.Length > publicKey.Length); + } + + [TestMethod] + public void RsaEncrypt_WithValidData_ShouldEncryptSuccessfully() + { + // Arrange + var (publicKey, _) = Crypto.GenerateRsaKey(); + var data = Encoding.UTF8.GetBytes("Hello, World!"); + + // Act + var encryptedData = Crypto.RsaEncrypt(data, publicKey); + + // Assert + Assert.IsNotNull(encryptedData); + Assert.AreNotEqual(data, encryptedData); + } + + [TestMethod] + public void RsaDecrypt_ShouldDecryptSuccessfully() + { + // Arrange + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + var originalData = Encoding.UTF8.GetBytes("Hello, World!"); + var encryptedData = Crypto.RsaEncrypt(originalData, publicKey); + + // Act + var decryptedData = Crypto.RsaDecrypt(encryptedData, privateKey); + + // Assert + Assert.AreEqual(originalData, decryptedData); + } + + [TestMethod] + public void RsaEncrypt_WithString_ShouldEncryptSuccessfully() + { + // Arrange + var (publicKey, _) = Crypto.GenerateRsaKey(); + var data = "Hello, World!"; + + // Act + var encryptedData = Crypto.RsaEncrypt(data, publicKey); + + // Assert + Assert.IsNotNull(encryptedData); + Assert.IsTrue(encryptedData.Length > 0); + } + + [TestMethod] + public void RsaDecrypt_WithString_ShouldDecryptSuccessfully() + { + // Arrange + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + var originalData = "Hello, World!"; + var encryptedData = Crypto.RsaEncrypt(originalData, publicKey); + + // Act + var decryptedData = Crypto.RsaDecrypt(encryptedData, privateKey); + + // Assert + Assert.AreEqual(originalData, decryptedData); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RsaEncrypt_WithDataTooLong_ShouldThrowException() + { + // Arrange + var (publicKey, _) = Crypto.GenerateRsaKey(); + var data = new byte[200]; // 超过2048位RSA密钥的190字节限制 + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)(i % 256); + } + + // Act + Crypto.RsaEncrypt(data, publicKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RsaDecrypt_WithInvalidBase64Data_ShouldThrowException() + { + // Arrange + var (_, privateKey) = Crypto.GenerateRsaKey(); + var invalidBase64Data = "invalid-base64-string"; + + // Act + Crypto.RsaDecrypt(invalidBase64Data, privateKey); + } + + #endregion + + #region RSA签名验证测试 + + [TestMethod] + public void RsaSignData_WithByteArray_ShouldSignSuccessfully() + { + // Arrange + var (_, privateKey) = Crypto.GenerateRsaKey(); + var data = Encoding.UTF8.GetBytes("Hello, World!"); + + // Act + var signature = Crypto.RsaSignData(data, privateKey); + + // Assert + Assert.IsNotNull(signature); + Assert.IsTrue(signature.Length > 0); + } + + [TestMethod] + public void RsaVerifyData_WithValidSignature_ShouldReturnTrue() + { + // Arrange + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var signature = Crypto.RsaSignData(data, privateKey); + + // Act + var isValid = Crypto.RsaVerifyData(data, signature, publicKey); + + // Assert + Assert.IsTrue(isValid); + } + + [TestMethod] + public void RsaVerifyData_WithInvalidSignature_ShouldReturnFalse() + { + // Arrange + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var wrongData = Encoding.UTF8.GetBytes("Wrong Data"); + var signature = Crypto.RsaSignData(wrongData, privateKey); + + // Act + var isValid = Crypto.RsaVerifyData(data, signature, publicKey); + + // Assert + Assert.IsFalse(isValid); + } + + [TestMethod] + public void RsaSignData_WithString_ShouldSignSuccessfully() + { + // Arrange + var (_, privateKey) = Crypto.GenerateRsaKey(); + var data = "Hello, World!"; + + // Act + var signature = Crypto.RsaSignData(data, privateKey); + + // Assert + Assert.IsNotNull(signature); + Assert.IsTrue(signature.Length > 0); + } + + [TestMethod] + public void RsaVerifyData_WithString_ShouldVerifySuccessfully() + { + // Arrange + var (publicKey, privateKey) = Crypto.GenerateRsaKey(); + var data = "Hello, World!"; + var signature = Crypto.RsaSignData(data, privateKey); + + // Act + var isValid = Crypto.RsaVerifyData(data, signature, publicKey); + + // Assert + Assert.IsTrue(isValid); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void RsaVerifyData_WithInvalidBase64Signature_ShouldThrowException() + { + // Arrange + var (publicKey, _) = Crypto.GenerateRsaKey(); + var data = "Hello, World!"; + var invalidSignature = "invalid-base64-signature"; + + // Act + Crypto.RsaVerifyData(data, invalidSignature, publicKey); + } + + #endregion + + #region 混合加密测试 + + [TestMethod] + public void HybridEncrypt_WithByteArray_ShouldEncryptSuccessfully() + { + // Arrange + var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, _) = Crypto.GenerateRsaKey(); + var data = Encoding.UTF8.GetBytes("Hello, World!"); + + // Act + var hybridData = Crypto.HybridEncrypt(data, ownPrivateKey, facePublicKey); + + // Assert + Assert.IsNotNull(hybridData); + Assert.IsNotNull(hybridData.AesEncryptData); + Assert.IsNotNull(hybridData.Signature); + Assert.IsNotNull(hybridData.RsaEncryptedAesKey); + } + + [TestMethod] + public void HybridDecrypt_ShouldDecryptSuccessfully() + { + // Arrange + var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, facePrivateKey) = Crypto.GenerateRsaKey(); + var originalData = Encoding.UTF8.GetBytes("Hello, World!"); + var hybridData = Crypto.HybridEncrypt(originalData, ownPrivateKey, facePublicKey); + + // Act + var decryptedData = Crypto.HybridDecrypt(hybridData, facePrivateKey, ownPublicKey); + + // Assert + Assert.AreEqual(originalData, decryptedData); + } + + [TestMethod] + public void HybridEncrypt_WithString_ShouldEncryptSuccessfully() + { + // Arrange + var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, _) = Crypto.GenerateRsaKey(); + var data = "Hello, World!"; + + // Act + var hybridJson = Crypto.HybridEncrypt(data, ownPrivateKey, facePublicKey); + + // Assert + Assert.IsNotNull(hybridJson); + Assert.IsTrue(hybridJson.Length > 0); + } + + [TestMethod] + public void HybridDecrypt_WithString_ShouldDecryptSuccessfully() + { + // Arrange + var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, facePrivateKey) = Crypto.GenerateRsaKey(); + var originalData = "Hello, World!"; + var hybridJson = Crypto.HybridEncrypt(originalData, ownPrivateKey, facePublicKey); + + // Act + var decryptedData = Crypto.HybridDecrypt(hybridJson, facePrivateKey, ownPublicKey); + + // Assert + Assert.AreEqual(originalData, decryptedData); + } + + [TestMethod] + [ExpectedException(typeof(CryptographicException))] + public void HybridDecrypt_WithInvalidSignature_ShouldThrowException() + { + // Arrange + var (ownPublicKey, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, facePrivateKey) = Crypto.GenerateRsaKey(); + var data = Encoding.UTF8.GetBytes("Hello, World!"); + var hybridData = Crypto.HybridEncrypt(data, ownPrivateKey, facePublicKey); + + // 修改签名使其无效 + hybridData.Signature[0] = (byte)(hybridData.Signature[0] ^ 0xFF); + + // Act + Crypto.HybridDecrypt(hybridData, facePrivateKey, ownPublicKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void HybridDecrypt_WithInvalidJson_ShouldThrowException() + { + // Arrange + var (ownPublicKey, _) = Crypto.GenerateRsaKey(); + var (_, facePrivateKey) = Crypto.GenerateRsaKey(); + var invalidJson = "invalid-json-string"; + + // Act + Crypto.HybridDecrypt(invalidJson, facePrivateKey, ownPublicKey); + } + + #endregion + + #region AesEncryptData类测试 + + [TestMethod] + public void AesEncryptData_Constructor_ShouldCreateValidInstance() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + var iv = new byte[16]; + + // Act + var encryptData = new AesEncryptData(cipherData, iv); + + // Assert + Assert.AreEqual(cipherData, encryptData.CipherData); + Assert.AreEqual(iv, encryptData.Iv); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AesEncryptData_Constructor_WithNullCipherData_ShouldThrowException() + { + // Arrange + var iv = new byte[16]; + + // Act + new AesEncryptData(null, iv); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AesEncryptData_Constructor_WithNullIv_ShouldThrowException() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + + // Act + new AesEncryptData(cipherData, null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AesEncryptData_Constructor_WithInvalidIvLength_ShouldThrowException() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + var invalidIv = new byte[8]; // 应该是16字节 + + // Act + new AesEncryptData(cipherData, invalidIv); + } + + [TestMethod] + public void AesEncryptData_GetIvString_ShouldReturnBase64String() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + var iv = new byte[16]; + var encryptData = new AesEncryptData(cipherData, iv); + + // Act + var ivString = encryptData.GetIvString(); + + // Assert + Assert.IsNotNull(ivString); + Assert.AreEqual(Convert.ToBase64String(iv), ivString); + } + + [TestMethod] + public void AesEncryptData_GetCipherDataString_ShouldReturnBase64String() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + var iv = new byte[16]; + var encryptData = new AesEncryptData(cipherData, iv); + + // Act + var cipherString = encryptData.GetCipherDataString(); + + // Assert + Assert.IsNotNull(cipherString); + Assert.AreEqual(Convert.ToBase64String(cipherData), cipherString); + } + + [TestMethod] + public void AesEncryptData_ToJson_ShouldReturnValidJson() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + var iv = new byte[16]; + var encryptData = new AesEncryptData(cipherData, iv); + + // Act + var json = encryptData.ToJson(); + + // Assert + Assert.IsNotNull(json); + Assert.IsTrue(json.Contains("CipherData")); + Assert.IsTrue(json.Contains("Iv")); + } + + [TestMethod] + public void AesEncryptData_FromJson_ShouldDeserializeSuccessfully() + { + // Arrange + var cipherData = new byte[] { 1, 2, 3, 4 }; + var iv = new byte[16]; + var originalData = new AesEncryptData(cipherData, iv); + var json = originalData.ToJson(); + + // Act + var deserializedData = AesEncryptData.FromJson(json); + + // Assert + Assert.IsNotNull(deserializedData); + Assert.AreEqual(originalData.CipherData, deserializedData.CipherData); + Assert.AreEqual(originalData.Iv, deserializedData.Iv); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void AesEncryptData_FromJson_WithInvalidJson_ShouldThrowException() + { + // Arrange + var invalidJson = "invalid-json-string"; + + // Act + AesEncryptData.FromJson(invalidJson); + } + + #endregion + + #region HybridEncryptData类测试 + + [TestMethod] + public void HybridEncryptData_ToJson_ShouldReturnValidJson() + { + // Arrange + var hybridData = new HybridEncryptData + { + AesEncryptData = new AesEncryptData(new byte[] { 1, 2, 3, 4 }, new byte[16]), + Signature = new byte[] { 5, 6, 7, 8 }, + RsaEncryptedAesKey = new byte[] { 9, 10, 11, 12 } + }; + + // Act + var json = hybridData.ToJson(); + + // Assert + Assert.IsNotNull(json); + Assert.IsTrue(json.Contains("AesEncryptData")); + Assert.IsTrue(json.Contains("Signature")); + Assert.IsTrue(json.Contains("RsaEncryptedAesKey")); + } + + [TestMethod] + public void HybridEncryptData_FromJson_ShouldDeserializeSuccessfully() + { + // Arrange + var originalData = new HybridEncryptData + { + AesEncryptData = new AesEncryptData(new byte[] { 1, 2, 3, 4 }, new byte[16]), + Signature = new byte[] { 5, 6, 7, 8 }, + RsaEncryptedAesKey = new byte[] { 9, 10, 11, 12 } + }; + var json = originalData.ToJson(); + + // Act + var deserializedData = HybridEncryptData.FromJson(json); + + // Assert + Assert.IsNotNull(deserializedData); + Assert.IsNotNull(deserializedData.AesEncryptData); + Assert.IsNotNull(deserializedData.Signature); + Assert.IsNotNull(deserializedData.RsaEncryptedAesKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void HybridEncryptData_FromJson_WithInvalidJson_ShouldThrowException() + { + // Arrange + var invalidJson = "invalid-json-string"; + + // Act + HybridEncryptData.FromJson(invalidJson); + } + + #endregion + + #region 文件操作测试 + + [TestMethod] + public void AesEncryptFile_ShouldEncryptFileSuccessfully() + { + // Arrange + var sourceFile = Path.GetTempFileName(); + var targetFile = Path.GetTempFileName(); + var key = Convert.ToBase64String(Crypto.GenerateAesKey()); + var testData = "Hello, World!"; + File.WriteAllText(sourceFile, testData); + + try + { + // Act + var (encryptData, returnedKey) = Crypto.AesEncryptFile(sourceFile, targetFile, key); + + // Assert + Assert.IsNotNull(encryptData); + Assert.IsNotNull(returnedKey); + Assert.IsTrue(File.Exists(targetFile)); + var encryptedContent = File.ReadAllText(targetFile); + Assert.IsNotNull(encryptedContent); + Assert.AreNotEqual(testData, encryptedContent); + } + finally + { + // Cleanup + if (File.Exists(sourceFile)) File.Delete(sourceFile); + if (File.Exists(targetFile)) File.Delete(targetFile); + } + } + + [TestMethod] + public void AesDecryptFile_ShouldDecryptFileSuccessfully() + { + // Arrange + var sourceFile = Path.GetTempFileName(); + var targetFile = Path.GetTempFileName(); + var decryptFile = Path.GetTempFileName(); + var key = Convert.ToBase64String(Crypto.GenerateAesKey()); + var testData = "Hello, World!"; + File.WriteAllText(sourceFile, testData); + + try + { + // 先加密文件 + var (encryptData, _) = Crypto.AesEncryptFile(sourceFile, targetFile, key); + + // Act - 解密文件 + Crypto.AesDecryptFile(targetFile, decryptFile, key); + + // Assert + Assert.IsTrue(File.Exists(decryptFile)); + var decryptedContent = File.ReadAllText(decryptFile); + Assert.AreEqual(testData, decryptedContent); + } + finally + { + // Cleanup + if (File.Exists(sourceFile)) File.Delete(sourceFile); + if (File.Exists(targetFile)) File.Delete(targetFile); + if (File.Exists(decryptFile)) File.Delete(decryptFile); + } + } + + [TestMethod] + [ExpectedException(typeof(FileNotFoundException))] + public void AesEncryptFile_WithNonExistentSource_ShouldThrowException() + { + // Arrange + var nonExistentFile = "non-existent-file.txt"; + var targetFile = Path.GetTempFileName(); + var key = Convert.ToBase64String(Crypto.GenerateAesKey()); + + try + { + // Act + Crypto.AesEncryptFile(nonExistentFile, targetFile, key); + } + finally + { + // Cleanup + if (File.Exists(targetFile)) File.Delete(targetFile); + } + } + + [TestMethod] + [ExpectedException(typeof(FileNotFoundException))] + public void AesDecryptFile_WithNonExistentSource_ShouldThrowException() + { + // Arrange + var nonExistentFile = "non-existent-file.txt"; + var targetFile = Path.GetTempFileName(); + var key = Convert.ToBase64String(Crypto.GenerateAesKey()); + + try + { + // Act + Crypto.AesDecryptFile(nonExistentFile, targetFile, key); + } + finally + { + // Cleanup + if (File.Exists(targetFile)) File.Delete(targetFile); + } + } + + #endregion + + #region 参数验证测试 + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AesEncrypt_WithNullData_ShouldThrowException() + { + // Act + Crypto.AesEncrypt((byte[])null, null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void AesDecrypt_WithNullEncryptData_ShouldThrowException() + { + // Act + Crypto.AesDecrypt(null, new byte[32]); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void RsaEncrypt_WithNullData_ShouldThrowException() + { + // Arrange + var (publicKey, _) = Crypto.GenerateRsaKey(); + + // Act + Crypto.RsaEncrypt((string)null, publicKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void RsaDecrypt_WithNullData_ShouldThrowException() + { + // Arrange + var (_, privateKey) = Crypto.GenerateRsaKey(); + + // Act + Crypto.RsaDecrypt((string)null, privateKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void HybridEncrypt_WithNullData_ShouldThrowException() + { + // Arrange + var (_, ownPrivateKey) = Crypto.GenerateRsaKey(); + var (facePublicKey, _) = Crypto.GenerateRsaKey(); + + // Act + Crypto.HybridEncrypt((string)null, ownPrivateKey, facePublicKey); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void HybridDecrypt_WithNullHybridData_ShouldThrowException() + { + // Arrange + var (ownPublicKey, _) = Crypto.GenerateRsaKey(); + var (_, facePrivateKey) = Crypto.GenerateRsaKey(); + + // Act + Crypto.HybridDecrypt((string)null, facePrivateKey, ownPublicKey); + } + + #endregion + } +}