diff --git a/.idea/.idea.NPin/.idea/.gitignore b/.idea/.idea.NPin/.idea/.gitignore new file mode 100644 index 0000000..6ea7a0a --- /dev/null +++ b/.idea/.idea.NPin/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.NPin.iml +/modules.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.NPin/.idea/encodings.xml b/.idea/.idea.NPin/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.NPin/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.NPin/.idea/indexLayout.xml b/.idea/.idea.NPin/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.NPin/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.NPin/.idea/vcs.xml b/.idea/.idea.NPin/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.NPin/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Framework/NPin.Framework.Core/Enums/FileTypeEnum.cs b/Framework/NPin.Framework.Core/Enums/FileTypeEnum.cs new file mode 100644 index 0000000..dac82cb --- /dev/null +++ b/Framework/NPin.Framework.Core/Enums/FileTypeEnum.cs @@ -0,0 +1,32 @@ +namespace NPin.Framework.Core.Enums; + +/// +/// 文件类型 +/// +public enum FileTypeEnum +{ + /// + /// 普通文件 + /// + File, + + /// + /// 图片 + /// + Image, + + /// + /// 缩略图 + /// + Thumbnail, + + /// + /// Excel + /// + Excel, + + /// + /// 临时文件 + /// + Temp +} \ No newline at end of file diff --git a/Framework/NPin.Framework.Core/Enums/OrderByEnum.cs b/Framework/NPin.Framework.Core/Enums/OrderByEnum.cs new file mode 100644 index 0000000..681701e --- /dev/null +++ b/Framework/NPin.Framework.Core/Enums/OrderByEnum.cs @@ -0,0 +1,14 @@ +namespace NPin.Framework.Core.Enums; + +public enum OrderByEnum +{ + /// + /// 升序 + /// + Asc, + + /// + /// 降序 + /// + Desc, +} \ No newline at end of file diff --git a/Framework/NPin.Framework.Core/Extensions/HttpContextExtensions.cs b/Framework/NPin.Framework.Core/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000..97329f1 --- /dev/null +++ b/Framework/NPin.Framework.Core/Extensions/HttpContextExtensions.cs @@ -0,0 +1,6 @@ +namespace NPin.Framework.Core.Extensions; + +public static class HttpContextExtensions +{ + // public static void FileInlineHandle(this HttpContext) +} \ No newline at end of file diff --git a/Framework/NPin.Framework.Core/Helper/ReflectHelper.cs b/Framework/NPin.Framework.Core/Helper/ReflectHelper.cs new file mode 100644 index 0000000..0f47b84 --- /dev/null +++ b/Framework/NPin.Framework.Core/Helper/ReflectHelper.cs @@ -0,0 +1,57 @@ +namespace NPin.Framework.Core.Helper; + +public static class ReflectHelper +{ + /// + /// 反射获取对象属性值(string) + /// + /// + /// + /// + /// + public static string? GetModelValue(string fieldName, object obj) + { + try + { + var typ = obj.GetType(); + object o = typ.GetProperty(fieldName).GetValue(obj, null); + if (o == null) + { + return null; + } + + var value = Convert.ToString(o); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + return value; + } + catch (Exception e) + { + throw e; + } + } + + /// + /// 反射设置对象属性值 + /// + /// + /// + /// + /// + public static bool SetModelValue(string fieldName, object value, object obj) + { + try + { + var typ = obj.GetType(); + typ.GetProperty(fieldName)?.SetValue(obj, value, null); + return true; + } + catch (Exception e) + { + throw e; + } + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.Core/NPin.Framework.Core.csproj b/Framework/NPin.Framework.Core/NPin.Framework.Core.csproj new file mode 100644 index 0000000..3cf2969 --- /dev/null +++ b/Framework/NPin.Framework.Core/NPin.Framework.Core.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + Lan.Framework.Core + + + + + + + + + + + + diff --git a/Framework/NPin.Framework.Core/NPinFrameworkCoreModule.cs b/Framework/NPin.Framework.Core/NPinFrameworkCoreModule.cs new file mode 100644 index 0000000..c66e187 --- /dev/null +++ b/Framework/NPin.Framework.Core/NPinFrameworkCoreModule.cs @@ -0,0 +1,7 @@ +using Volo.Abp.Modularity; + +namespace NPin.Framework.Core; + +public class NPinFrameworkCoreModule : AbpModule +{ +} diff --git a/Framework/NPin.Framework.Mapster/MapsterAutoObjectMappingProvider.cs b/Framework/NPin.Framework.Mapster/MapsterAutoObjectMappingProvider.cs new file mode 100644 index 0000000..adc1265 --- /dev/null +++ b/Framework/NPin.Framework.Mapster/MapsterAutoObjectMappingProvider.cs @@ -0,0 +1,18 @@ +using Mapster; +using Volo.Abp.ObjectMapping; + +namespace NPin.Framework.Mapster; + +public class MapsterAutoObjectMappingProvider: IAutoObjectMappingProvider +{ + public TDestination Map(object source) + { + // var sss = typeof(TDestination).Name; + return source.Adapt(); + } + + public TDestination Map(TSource source, TDestination destination) + { + return source.Adapt(destination); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.Mapster/MapsterObjectMapper.cs b/Framework/NPin.Framework.Mapster/MapsterObjectMapper.cs new file mode 100644 index 0000000..31a064d --- /dev/null +++ b/Framework/NPin.Framework.Mapster/MapsterObjectMapper.cs @@ -0,0 +1,18 @@ +using Volo.Abp.ObjectMapping; + +namespace NPin.Framework.Mapster; + +public class MapsterObjectMapper : IObjectMapper +{ + public IAutoObjectMappingProvider AutoObjectMappingProvider => throw new NotImplementedException(); + + public TDestination Map(TSource source) + { + throw new NotImplementedException(); + } + + public TDestination Map(TSource source, TDestination destination) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.Mapster/NPin.Framework.Mapster.csproj b/Framework/NPin.Framework.Mapster/NPin.Framework.Mapster.csproj new file mode 100644 index 0000000..489f7d3 --- /dev/null +++ b/Framework/NPin.Framework.Mapster/NPin.Framework.Mapster.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Framework/NPin.Framework.Mapster/NPinFrameworkMapsterModule.cs b/Framework/NPin.Framework.Mapster/NPinFrameworkMapsterModule.cs new file mode 100644 index 0000000..3e83c80 --- /dev/null +++ b/Framework/NPin.Framework.Mapster/NPinFrameworkMapsterModule.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using NPin.Framework.Core; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectMapping; + +namespace NPin.Framework.Mapster; + +[DependsOn(typeof(NPinFrameworkCoreModule), typeof(AbpObjectMappingModule))] +public class NPinFrameworkMapsterModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // 注入 MapsterAutoObjectMappingProvider + context.Services.AddTransient(); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs new file mode 100644 index 0000000..9e17cd0 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs @@ -0,0 +1,105 @@ +using SqlSugar; + +namespace NPin.Framework.SqlSugarCore.Abstractions; + +public class DbConnOptions +{ + /// + /// 连接字符串 必填 + /// + public string? Url { get; set; } + + /// + /// 数据库类型 + /// + public DbType? DbType { get; set; } + + /// + /// 开启种子数据 + /// + public bool EnabledDbSeed { get; set; } = false; + + /// + /// 开启 CodeFirst 自动化表结构 + /// + public bool EnabledCodeFirst { get; set; } = false; + + /// + /// 开启sql日志 + /// + public bool EnabledSqlLog { get; set; } = true; + + /// + /// 实体程序集 + /// + public List? EntityAssembly { get; set; } + + /// + /// 开启读写分离 + /// + public bool EnabledReadWrite { get; set; } = false; + + /// + /// 读写分离 + /// + public List? ReadUrl { get; set; } + + /// + /// 开启Saas多租户 + /// + public bool EnabledSaasMultiTenancy { get; set; } = false; + + + /// + /// 默认租户库连接,如果不填,那就是默认库的地址 + /// + public string? MasterSaasMultiTenancyUrl { get; set; } + + + /// + /// Saas租户连接 + /// + public List? SaasMultiTenancy { get; set; } + + public static string MasterTenantDbDefaultName = "Master"; + public static string TenantDbDefaultName = "Default"; + + public SaasMultiTenancyOptions GetDefaultSaasMultiTenancy() + { + return new SaasMultiTenancyOptions { Name = TenantDbDefaultName, Url = Url }; + } + + public SaasMultiTenancyOptions? GetDefaultMasterSaasMultiTenancy() + { + if (EnabledSaasMultiTenancy == false) + { + return null; + } + + if (string.IsNullOrEmpty(MasterSaasMultiTenancyUrl)) + { + return new SaasMultiTenancyOptions { Name = MasterTenantDbDefaultName, Url = Url }; + } + else + { + return new SaasMultiTenancyOptions() + { + Name = MasterTenantDbDefaultName, + Url = MasterSaasMultiTenancyUrl + }; + } + } +} + +public class SaasMultiTenancyOptions +{ + /// + /// 租户名称标识 + /// + public string Name { get; set; } + + /// + /// 连接Url + /// + public string Url { get; set; } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarDbConnectionCreator.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarDbConnectionCreator.cs new file mode 100644 index 0000000..3e35021 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarDbConnectionCreator.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using SqlSugar; + +namespace NPin.Framework.SqlSugarCore.Abstractions; + +public interface ISqlSugarDbConnectionCreator +{ + DbConnOptions Options { get; } + + Action OnSqlSugarClientConfig { get; set; } + Action DataExecuted { get; set; } + Action DataExecuting { get; set; } + Action OnLogExecuting { get; set; } + Action OnLogExecuted { get; set; } + Action EntityService { get; set; } + + ConnectionConfig Build(Action? action = null); + void SetDbAop(ISqlSugarClient currentDb); +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarDbContext.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarDbContext.cs new file mode 100644 index 0000000..96f57eb --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarDbContext.cs @@ -0,0 +1,27 @@ +using SqlSugar; + +namespace NPin.Framework.SqlSugarCore.Abstractions; + +public interface ISqlSugarDbContext +{ + /// + /// Db 客户端 + /// + ISqlSugarClient SqlSugarClient { get; } + + /// + /// 连接配置 + /// + DbConnOptions Options { get; } + + /// + /// 备份数据库 + /// + void Backup(); + + /// + /// 设置客户端 + /// + /// + void SetSqlSugarClient(ISqlSugarClient sqlSugarClient); +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarRepository.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarRepository.cs new file mode 100644 index 0000000..a22772f --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISqlSugarRepository.cs @@ -0,0 +1,87 @@ +using System.Linq.Expressions; +using SqlSugar; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; + +namespace NPin.Framework.SqlSugarCore.Abstractions; + +public interface ISqlSugarRepository : IRepository where TEntity : class, IEntity, new() +{ + ISqlSugarClient Db { get; } + + ISugarQueryable DbQueryable { get; } + + Task GetDbContextAsync(); + Task> AsDeleteable(); + Task> AsInsertable(List insertObjs); + Task> AsInsertable(TEntity insertObj); + Task> AsInsertable(TEntity[] insertObjs); + Task> AsQueryable(); + Task AsSugarClient(); + Task AsTenant(); + Task> AsUpdateable(List updateObjs); + Task> AsUpdateable(TEntity updateObj); + Task> AsUpdateable(); + Task> AsUpdateable(TEntity[] updateObjs); + + #region 单查 + + Task GetByIdAsync(dynamic id); + Task GetSingleAsync(Expression> whereExpression); + Task GetFirstAsync(Expression> whereExpression); + Task IsAnyAsync(Expression> whereExpression); + Task CountAsync(Expression> whereExpression); + + #endregion + + #region 多查 + + Task> GetListAsync(); + Task> GetListAsync(Expression> whereExpression); + + #endregion + + #region 分页查 + + Task> GetPageListAsync(Expression> whereExpression, int pageNum, int pageSize); + + Task> GetPageListAsync(Expression> whereExpression, int pageNum, int pageSize, + Expression>? orderByExpression = null, OrderByType orderByType = OrderByType.Asc); + + #endregion + + #region 插入 + + Task InsertAsync(TEntity insertObj); + Task InsertOrUpdateAsync(TEntity data); + Task InsertOrUpdateAsync(List datas); + Task InsertReturnIdentityAsync(TEntity insertObj); + Task InsertReturnBigIdentityAsync(TEntity insertObj); + Task InsertReturnSnowflakeIdAsync(TEntity insertObj); + Task InsertReturnEntityAsync(TEntity insertObj); + Task InsertRangeAsync(List insertObjs); + + #endregion + + #region 更新 + + Task UpdateAsync(TEntity updateObj); + Task UpdateRangeAsync(List updateObjs); + Task UpdateAsync(Expression> columns, Expression> whereExpression); + + #endregion + + #region 删除 + + Task DeleteAsync(TEntity deleteObj); + Task DeleteAsync(List deleteObjs); + Task DeleteAsync(Expression> whereExpression); + Task DeleteByIdAsync(dynamic id); + Task DeleteByIdsAsync(dynamic[] ids); + + #endregion +} + +public interface ISqlSugarRepository : ISqlSugarRepository,IRepository where TEntity : class, IEntity, new() +{ +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISugarDbContextProvider.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISugarDbContextProvider.cs new file mode 100644 index 0000000..bc0d33e --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/ISugarDbContextProvider.cs @@ -0,0 +1,6 @@ +namespace NPin.Framework.SqlSugarCore.Abstractions; + +public interface ISugarDbContextProvider where TDbContext : ISqlSugarDbContext +{ + Task GetDbContextAsync(); +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/IgnoreCodeFirstAttribute.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/IgnoreCodeFirstAttribute.cs new file mode 100644 index 0000000..d5209ab --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/IgnoreCodeFirstAttribute.cs @@ -0,0 +1,6 @@ +namespace NPin.Framework.SqlSugarCore.Abstractions; + +[AttributeUsage(AttributeTargets.Class)] +public class IgnoreCodeFirstAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/MasterTenantAttribute.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/MasterTenantAttribute.cs new file mode 100644 index 0000000..c61ad57 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/MasterTenantAttribute.cs @@ -0,0 +1,6 @@ +namespace NPin.Framework.SqlSugarCore.Abstractions; + +[AttributeUsage(AttributeTargets.Class)] +public class MasterTenantAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/NPin.Framework.SqlSugarCore.Abstractions.csproj b/Framework/NPin.Framework.SqlSugarCore.Abstractions/NPin.Framework.SqlSugarCore.Abstractions.csproj new file mode 100644 index 0000000..6e4a7d0 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/NPin.Framework.SqlSugarCore.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Framework/NPin.Framework.SqlSugarCore.Abstractions/NPinFrameworkSqlSugarCoreAbstractionsModule.cs b/Framework/NPin.Framework.SqlSugarCore.Abstractions/NPinFrameworkSqlSugarCoreAbstractionsModule.cs new file mode 100644 index 0000000..54b31a4 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore.Abstractions/NPinFrameworkSqlSugarCoreAbstractionsModule.cs @@ -0,0 +1,9 @@ +using NPin.Framework.Core; +using Volo.Abp.Modularity; + +namespace NPin.Framework.SqlSugarCore.Abstractions; + +[DependsOn(typeof(NPinFrameworkCoreModule))] +public class NPinFrameworkSqlSugarCoreAbstractionsModule : AbpModule +{ +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/AsyncLocalDbContextAccessor.cs b/Framework/NPin.Framework.SqlSugarCore/AsyncLocalDbContextAccessor.cs new file mode 100644 index 0000000..e975145 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/AsyncLocalDbContextAccessor.cs @@ -0,0 +1,21 @@ +using NPin.Framework.SqlSugarCore.Abstractions; + +namespace NPin.Framework.SqlSugarCore; + +public class AsyncLocalDbContextAccessor +{ + public static AsyncLocalDbContextAccessor Instance { get; } = new(); + + private readonly AsyncLocal _currentScope; + + public ISqlSugarDbContext? Current + { + get => _currentScope.Value; + set => _currentScope.Value = value; + } + + public AsyncLocalDbContextAccessor() + { + _currentScope = new(); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/NPin.Framework.SqlSugarCore.csproj b/Framework/NPin.Framework.SqlSugarCore/NPin.Framework.SqlSugarCore.csproj new file mode 100644 index 0000000..0dc0783 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/NPin.Framework.SqlSugarCore.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Framework/NPin.Framework.SqlSugarCore/NPinFrameworkSqlSugarCoreModule.cs b/Framework/NPin.Framework.SqlSugarCore/NPinFrameworkSqlSugarCoreModule.cs new file mode 100644 index 0000000..3a8f899 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/NPinFrameworkSqlSugarCoreModule.cs @@ -0,0 +1,95 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using NPin.Framework.SqlSugarCore.Abstractions; +using NPin.Framework.SqlSugarCore.Repositories; +using NPin.Framework.SqlSugarCore.Uow; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Data; +using Volo.Abp.Domain; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Modularity; + +namespace NPin.Framework.SqlSugarCore; + +[DependsOn(typeof(AbpDddDomainModule))] +public class NPinFrameworkSqlSugarCoreModule : AbpModule +{ + public override Task ConfigureServicesAsync(ServiceConfigurationContext context) + { + var service = context.Services; + var configuration = service.GetConfiguration(); + Configure(configuration.GetSection("DbConnOptions")); + + // 开放 sqlSugarClient + service.TryAddScoped(); + // 不开放 sqlSugarClient + //service.AddTransient(x => x.GetRequiredService().SqlSugarClient); + + service.AddTransient(typeof(IRepository<>), typeof(SqlSugarRepository<>)); + service.AddTransient(typeof(IRepository<,>), typeof(SqlSugarRepository<,>)); + service.AddTransient(typeof(ISqlSugarRepository<>), typeof(SqlSugarRepository<>)); + service.AddTransient(typeof(ISqlSugarRepository<,>), typeof(SqlSugarRepository<,>)); + + service.AddTransient(typeof(ISugarDbContextProvider<>), typeof(UnitOfWorkSqlSugarDbContextProvider<>)); + + return Task.CompletedTask; + } + + public override async Task OnPreApplicationInitializationAsync(ApplicationInitializationContext context) + { + // CodeFirst & DataSeed + var service = context.ServiceProvider; + var options = service.GetRequiredService>().Value; + + // TODO 多租户 种子数据 和 CodeFirst + if (options.EnabledCodeFirst) + { + CodeFirst(service); + } + + if (options.EnabledDbSeed) + { + await DataSeedAsync(service); + } + } + + /// + /// CodeFirst 建库建表 + /// + /// + private void CodeFirst(IServiceProvider service) + { + var moduleContainer = service.GetRequiredService(); + var db = service.GetRequiredService().SqlSugarClient; + + // 尝试创建数据库 + db.DbMaintenance.CreateDatabase(); + + var types = new List(); + foreach (var module in moduleContainer.Modules) + { + types.AddRange(module.Assembly.GetTypes() + .Where(x => x.GetCustomAttribute() == null) + .Where(x => x.GetCustomAttribute() != null) + .Where(x => x.GetCustomAttribute() is null)); + } + + if (types.Count > 0) + { + db.CopyNew().CodeFirst.InitTables(types.ToArray()); + } + } + + /// + /// 同步种子数据 + /// + /// + private async Task DataSeedAsync(IServiceProvider service) + { + var dataSeeder = service.GetRequiredService(); + await dataSeeder.SeedAsync(); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs b/Framework/NPin.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs new file mode 100644 index 0000000..8255279 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/Repositories/SqlSugarRepository.cs @@ -0,0 +1,430 @@ +using System.Linq.Expressions; +using NPin.Framework.Core.Helper; +using NPin.Framework.SqlSugarCore.Abstractions; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Linq; + +namespace NPin.Framework.SqlSugarCore.Repositories; + +public class SqlSugarRepository : ISqlSugarRepository, IRepository + where TEntity : class, IEntity, new() +{ + public ISqlSugarClient Db => GetDbContextAsync().Result; + + public ISugarQueryable DbQueryable => GetDbContextAsync().Result.Queryable(); + + private ISugarDbContextProvider _sugarDbContextProvider; + public IAsyncQueryableExecuter AsyncExecuter { get; } + + public bool? IsChangeTrackingEnabled => false; + + public SqlSugarRepository(ISugarDbContextProvider sugarDbContextProvider) + { + _sugarDbContextProvider = sugarDbContextProvider; + } + + public virtual async Task GetDbContextAsync() + { + var db = (await _sugarDbContextProvider.GetDbContextAsync()).SqlSugarClient; + return db; + } + + public virtual async Task> GetDbSimpleClientAsync() + { + var db = await GetDbContextAsync(); + return new SimpleClient(db); + } + + #region Abp模块 + + public virtual async Task FindAsync(Expression> predicate, bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetFirstAsync(predicate); + } + + public virtual async Task GetAsync(Expression> predicate, bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetFirstAsync(predicate); + } + + public virtual async Task DeleteAsync(Expression> predicate, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await this.DeleteAsync(predicate); + } + + public virtual async Task DeleteDirectAsync(Expression> predicate, + CancellationToken cancellationToken = default) + { + await this.DeleteAsync(predicate); + } + + public IQueryable WithDetails() + { + throw new NotImplementedException(); + } + + public IQueryable WithDetails(params Expression>[] propertySelectors) + { + throw new NotImplementedException(); + } + + public Task> WithDetailsAsync() + { + throw new NotImplementedException(); + } + + public Task> WithDetailsAsync(params Expression>[] propertySelectors) + { + throw new NotImplementedException(); + } + + public Task> GetQueryableAsync() + { + throw new NotImplementedException(); + } + + public virtual async Task> GetListAsync(Expression> predicate, + bool includeDetails = false, CancellationToken cancellationToken = default) + { + return await GetListAsync(predicate); + } + + public virtual async Task InsertAsync(TEntity entity, bool autoSave = false, + CancellationToken cancellationToken = default) + { + return await InsertReturnEntityAsync(entity); + } + + public virtual async Task InsertManyAsync(IEnumerable entities, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await InsertRangeAsync(entities.ToList()); + } + + public virtual async Task UpdateAsync(TEntity entity, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await UpdateAsync(entity); + return entity; + } + + public virtual async Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await UpdateRangeAsync(entities.ToList()); + } + + public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await DeleteAsync(entity); + } + + public virtual async Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await DeleteAsync(entities.ToList()); + } + + public virtual async Task> GetListAsync(bool includeDetails = false, + CancellationToken cancellationToken = default) + { + return await GetListAsync(); + } + + public virtual async Task GetCountAsync(CancellationToken cancellationToken = default) + { + return await this.CountAsync(); + } + + public virtual async Task> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, + bool includeDetails = false, CancellationToken cancellationToken = default) + { + return await GetPageListAsync(_ => true, skipCount, maxResultCount); + } + + #endregion + + #region 内置DB快捷操作 + + public virtual async Task> AsDeleteable() + { + return (await GetDbSimpleClientAsync()).AsDeleteable(); + } + + public virtual async Task> AsInsertable(List insertObjs) + { + return (await GetDbSimpleClientAsync()).AsInsertable(insertObjs); + } + + public virtual async Task> AsInsertable(TEntity insertObj) + { + return (await GetDbSimpleClientAsync()).AsInsertable(insertObj); + } + + public virtual async Task> AsInsertable(TEntity[] insertObjs) + { + return (await GetDbSimpleClientAsync()).AsInsertable(insertObjs); + } + + public virtual async Task> AsQueryable() + { + return (await GetDbSimpleClientAsync()).AsQueryable(); + } + + public virtual async Task AsSugarClient() + { + return (await GetDbSimpleClientAsync()).AsSugarClient(); + } + + public virtual async Task AsTenant() + { + return (await GetDbSimpleClientAsync()).AsTenant(); + } + + public virtual async Task> AsUpdateable(List updateObjs) + { + return (await GetDbSimpleClientAsync()).AsUpdateable(updateObjs); + } + + public virtual async Task> AsUpdateable(TEntity updateObj) + { + return (await GetDbSimpleClientAsync()).AsUpdateable(updateObj); + } + + public virtual async Task> AsUpdateable() + { + return (await GetDbSimpleClientAsync()).AsUpdateable(); + } + + public virtual async Task> AsUpdateable(TEntity[] updateObjs) + { + return (await GetDbSimpleClientAsync()).AsUpdateable(updateObjs); + } + + #endregion + + #region SimpleClient模块 + + public virtual async Task CountAsync(Expression> whereExpression) + { + return await (await GetDbSimpleClientAsync()).CountAsync(whereExpression); + } + + public virtual async Task DeleteAsync(TEntity deleteObj) + { + if (deleteObj is ISoftDelete) + { + ReflectHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, deleteObj); + return await (await GetDbSimpleClientAsync()).UpdateAsync(deleteObj); + } + else + { + return await (await GetDbSimpleClientAsync()).DeleteAsync(deleteObj); + } + } + + public virtual async Task DeleteAsync(List deleteObjs) + { + if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) + { + deleteObjs.ForEach(e => ReflectHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, e)); + return await (await GetDbSimpleClientAsync()).UpdateRangeAsync(deleteObjs); + } + else + { + return await (await GetDbSimpleClientAsync()).DeleteAsync(deleteObjs); + } + } + + public virtual async Task DeleteAsync(Expression> whereExpression) + { + if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) + { + return await (await GetDbSimpleClientAsync()).AsUpdateable().SetColumns(nameof(ISoftDelete), true) + .Where(whereExpression).ExecuteCommandAsync() > 0; + } + else + { + return await (await GetDbSimpleClientAsync()).DeleteAsync(whereExpression); + } + } + + public virtual async Task DeleteByIdAsync(dynamic id) + { + if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) + { + var entity = await GetByIdAsync(id); + //反射赋值 + ReflectHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, entity); + return await UpdateAsync(entity); + } + else + { + return await (await GetDbSimpleClientAsync()).DeleteByIdAsync(id); + } + } + + public virtual async Task DeleteByIdsAsync(dynamic[] ids) + { + if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) + { + var simpleClient = (await GetDbSimpleClientAsync()); + var entities = await simpleClient.AsQueryable().In(ids).ToListAsync(); + if (entities.Count == 0) + { + return false; + } + + // 反射赋值 + entities.ForEach(e => ReflectHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, e)); + return await UpdateRangeAsync(entities); + } + else + { + return await (await GetDbSimpleClientAsync()).DeleteByIdAsync(ids); + } + } + + public virtual async Task GetByIdAsync(dynamic id) + { + return await (await GetDbSimpleClientAsync()).GetByIdAsync(id); + } + + + public virtual async Task GetFirstAsync(Expression> whereExpression) + { + return await (await GetDbSimpleClientAsync()).GetFirstAsync(whereExpression); + } + + public virtual async Task> GetListAsync() + { + return await (await GetDbSimpleClientAsync()).GetListAsync(); + } + + public virtual async Task> GetListAsync(Expression> whereExpression) + { + return await (await GetDbSimpleClientAsync()).GetListAsync(whereExpression); + } + + public virtual async Task> GetPageListAsync(Expression> whereExpression, + int pageNum, int pageSize) + { + return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, + new PageModel() { PageIndex = pageNum, PageSize = pageSize }); + } + + public virtual async Task> GetPageListAsync(Expression> whereExpression, + int pageNum, int pageSize, + Expression>? orderByExpression = null, OrderByType orderByType = OrderByType.Asc) + { + return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, + new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType); + } + + public virtual async Task GetSingleAsync(Expression> whereExpression) + { + return await (await GetDbSimpleClientAsync()).GetSingleAsync(whereExpression); + } + + public virtual async Task InsertAsync(TEntity insertObj) + { + return await (await GetDbSimpleClientAsync()).InsertAsync(insertObj); + } + + public virtual async Task InsertOrUpdateAsync(TEntity data) + { + return await (await GetDbSimpleClientAsync()).InsertOrUpdateAsync(data); + } + + public virtual async Task InsertOrUpdateAsync(List datas) + { + return await (await GetDbSimpleClientAsync()).InsertOrUpdateAsync(datas); + } + + public virtual async Task InsertRangeAsync(List insertObjs) + { + return await (await GetDbSimpleClientAsync()).InsertRangeAsync(insertObjs); + } + + public virtual async Task InsertReturnBigIdentityAsync(TEntity insertObj) + { + return await (await GetDbSimpleClientAsync()).InsertReturnBigIdentityAsync(insertObj); + } + + public virtual async Task InsertReturnEntityAsync(TEntity insertObj) + { + return await (await GetDbSimpleClientAsync()).InsertReturnEntityAsync(insertObj); + } + + public virtual async Task InsertReturnIdentityAsync(TEntity insertObj) + { + return await (await GetDbSimpleClientAsync()).InsertReturnIdentityAsync(insertObj); + } + + public virtual async Task InsertReturnSnowflakeIdAsync(TEntity insertObj) + { + return await (await GetDbSimpleClientAsync()).InsertReturnSnowflakeIdAsync(insertObj); + } + + public virtual async Task IsAnyAsync(Expression> whereExpression) + { + return await (await GetDbSimpleClientAsync()).IsAnyAsync(whereExpression); + } + + public virtual async Task UpdateAsync(TEntity updateObj) + { + return await (await GetDbSimpleClientAsync()).UpdateAsync(updateObj); + } + + public virtual async Task UpdateAsync(Expression> columns, + Expression> whereExpression) + { + return await (await GetDbSimpleClientAsync()).UpdateAsync(columns, whereExpression); + } + + public virtual async Task UpdateRangeAsync(List updateObjs) + { + return await (await GetDbSimpleClientAsync()).UpdateRangeAsync(updateObjs); + } + + #endregion +} + +public class SqlSugarRepository : SqlSugarRepository, ISqlSugarRepository, + IRepository where TEntity : class, IEntity, new() +{ + public SqlSugarRepository(ISugarDbContextProvider sugarDbContextProvider) : base( + sugarDbContextProvider) + { + } + + public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) + { + await DeleteByIdAsync(id); + } + + public virtual async Task DeleteManyAsync(IEnumerable ids, bool autoSave = false, + CancellationToken cancellationToken = default) + { + await DeleteByIdsAsync(ids.Select(x => (object)x).ToArray()); + } + + public virtual async Task FindAsync(TKey id, bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetByIdAsync(id); + } + + public virtual async Task GetAsync(TKey id, bool includeDetails = true, + CancellationToken cancellationToken = default) + { + return await GetByIdAsync(id); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/SqlSugarCoreExtensions.cs b/Framework/NPin.Framework.SqlSugarCore/SqlSugarCoreExtensions.cs new file mode 100644 index 0000000..a037f03 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/SqlSugarCoreExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NPin.Framework.SqlSugarCore.Abstractions; + +namespace NPin.Framework.SqlSugarCore; + +public static class SqlSugarCoreExtensions +{ + public static IServiceCollection AddNPinDbContext(this IServiceCollection service) + where TDbContext : class, ISqlSugarDbContext + { + service.Replace(new ServiceDescriptor(typeof(ISqlSugarDbContext), typeof(TDbContext), + ServiceLifetime.Transient)); + return service; + } + + public static IServiceCollection TryAddNPinDbContext(this IServiceCollection service) + where TDbContext : class, ISqlSugarDbContext + { + service.TryAdd(new ServiceDescriptor(typeof(ISqlSugarDbContext), typeof(TDbContext), + ServiceLifetime.Transient)); + return service; + } + + public static IServiceCollection AddNPinDbContext(this IServiceCollection service, + Action options) + where TDbContext : class, ISqlSugarDbContext + { + service.Configure(options.Invoke); + service.AddNPinDbContext(); + return service; + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbConnectionCreator.cs b/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbConnectionCreator.cs new file mode 100644 index 0000000..8bd7056 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbConnectionCreator.cs @@ -0,0 +1,111 @@ +using System.Reflection; +using Microsoft.Extensions.Options; +using NPin.Framework.SqlSugarCore.Abstractions; +using SqlSugar; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace NPin.Framework.SqlSugarCore; + +public class SqlSugarDbConnectionCreator : ISqlSugarDbConnectionCreator, ITransientDependency +{ + [DisablePropertyInjection] public Action OnSqlSugarClientConfig { get; set; } + + [DisablePropertyInjection] public Action DataExecuted { get; set; } + + [DisablePropertyInjection] public Action DataExecuting { get; set; } + + [DisablePropertyInjection] public Action OnLogExecuting { get; set; } + + [DisablePropertyInjection] public Action OnLogExecuted { get; set; } + + [DisablePropertyInjection] public Action EntityService { get; set; } + + public DbConnOptions Options { get; } + + public SqlSugarDbConnectionCreator(IOptions options) + { + Options = options.Value; + } + + public void SetDbAop(ISqlSugarClient currentDb) + { + currentDb.Aop.OnLogExecuting = this.OnLogExecuting; + currentDb.Aop.OnLogExecuted = this.OnLogExecuted; + currentDb.Aop.DataExecuting = this.DataExecuting; + currentDb.Aop.DataExecuted = this.DataExecuted; + OnSqlSugarClientConfig(currentDb); + } + + public ConnectionConfig Build(Action? action = null) + { + var dbConnOptions = Options; + + #region 组装Options + + if (dbConnOptions.DbType is null) + { + throw new ArgumentException("DbType配置为空"); + } + + // 读写分离 slave模式 + var slaveConfigs = new List(); + if (dbConnOptions.EnabledReadWrite) + { + if (dbConnOptions.ReadUrl is null) + { + throw new ArgumentException("读写分离开启,但读库配置为空"); + } + + var readConn = dbConnOptions.ReadUrl; + readConn.ForEach(s => + { + // 如果是动态分库,连接串不能写死,需要动态添加,所以此处只配置共享库的连接 + slaveConfigs.Add(new SlaveConnectionConfig { ConnectionString = s }); + }); + } + + #endregion + + #region 组装连接配置 + + var connectionConfig = new ConnectionConfig + { + ConfigId = ConnectionStrings.DefaultConnectionStringName, + DbType = dbConnOptions.DbType ?? DbType.Sqlite, + ConnectionString = dbConnOptions.Url, + IsAutoCloseConnection = true, + SlaveConnectionConfigs = slaveConfigs, + // CodeFirst 非空值判断 + ConfigureExternalServices = new ConfigureExternalServices + { + EntityService = (c, p) => + { + if (new NullabilityInfoContext().Create(c).WriteState is NullabilityState.Nullable) + { + p.IsNullable = true; + } + + EntityService(c, p); + } + }, + // Aop + AopEvents = new AopEvents + { + DataExecuted = DataExecuted, + DataExecuting = DataExecuting, + OnLogExecuted = OnLogExecuted, + OnLogExecuting = OnLogExecuting + } + }; + + if (action is not null) + { + action.Invoke(connectionConfig); + } + + #endregion + + return connectionConfig; + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs b/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs new file mode 100644 index 0000000..81280df --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs @@ -0,0 +1,238 @@ +using System.Reflection; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NPin.Framework.SqlSugarCore.Abstractions; +using SqlSugar; +using Volo.Abp; +using Volo.Abp.Auditing; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Users; + +namespace NPin.Framework.SqlSugarCore; + +public class SqlSugarDbContext : ISqlSugarDbContext +{ + private IAbpLazyServiceProvider LazyServiceProvider { get; } + + public ISqlSugarClient SqlSugarClient { get; private set; } + public DbConnOptions Options => LazyServiceProvider.LazyGetRequiredService>().Value; + public ICurrentUser CurrentUser => LazyServiceProvider.GetRequiredService(); + public ICurrentTenant? CurrentTenant => LazyServiceProvider.LazyGetRequiredService(); + public IDataFilter DataFilter => LazyServiceProvider.LazyGetRequiredService(); + + private IGuidGenerator GuidGenerator => LazyServiceProvider.LazyGetRequiredService(); + + protected ILoggerFactory Logger => LazyServiceProvider.LazyGetRequiredService(); + protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled() ?? false; + protected virtual bool IsSoftDeleteFilterEnabled => DataFilter?.IsEnabled() ?? false; + + public IEntityChangeEventHelper EntityChangeEventHelper => + LazyServiceProvider.LazyGetService(NullEntityChangeEventHelper.Instance); + + public void SetSqlSugarClient(ISqlSugarClient sqlSugarClient) + { + SqlSugarClient = sqlSugarClient; + } + + public SqlSugarDbContext(IAbpLazyServiceProvider lazyServiceProvider) + { + LazyServiceProvider = lazyServiceProvider; + var connectionCreator = LazyServiceProvider.LazyGetRequiredService(); + connectionCreator.OnSqlSugarClientConfig = OnSqlSugarClientConfig; + connectionCreator.EntityService = EntityService; + connectionCreator.DataExecuting = DataExecuting; + connectionCreator.DataExecuted = DataExecuted; + connectionCreator.OnLogExecuting = OnLogExecuting; + connectionCreator.OnLogExecuted = OnLogExecuted; + SqlSugarClient = new SqlSugarClient(connectionCreator.Build()); + connectionCreator.SetDbAop(SqlSugarClient); + } + + /// + /// 上下文对象扩展 + /// + /// + protected virtual void OnSqlSugarClientConfig(ISqlSugarClient sqlSugarClient) + { + //需自定义扩展 + if (IsSoftDeleteFilterEnabled) + { + sqlSugarClient.QueryFilter.AddTableFilter(u => u.IsDeleted == false); + } + + if (IsMultiTenantFilterEnabled) + { + sqlSugarClient.QueryFilter.AddTableFilter(u => u.TenantId == GuidGenerator.Create()); + } + + // 自定义其它Filter + CustomDataFilter(sqlSugarClient); + } + + protected virtual void CustomDataFilter(ISqlSugarClient sqlSugarClient) + { + } + + protected virtual void DataExecuted(object oldValue, DataAfterModel entityInfo) + { + } + + protected virtual void DataExecuting(object? oldValue, DataFilterModel entityInfo) + { + // 审计日志 + switch (entityInfo.OperationType) + { + case DataFilterType.UpdateByObject: + if (entityInfo.PropertyName.Equals(nameof(IAuditedObject.LastModificationTime))) + { + if (!DateTime.MinValue.Equals(oldValue)) + { + entityInfo.SetValue(DateTime.Now); + } + } + + if (entityInfo.PropertyName.Equals(nameof(IAuditedObject.LastModifierId))) + { + if (CurrentUser.Id != null) + { + entityInfo.SetValue(CurrentUser.Id); + } + } + + break; + case DataFilterType.InsertByObject: + if (entityInfo.PropertyName.Equals(nameof(IEntity.Id))) + { + //主键为空或者为默认最小值 + if (Guid.Empty.Equals(oldValue)) + { + entityInfo.SetValue(GuidGenerator.Create()); + } + } + + if (entityInfo.PropertyName.Equals(nameof(IAuditedObject.CreationTime))) + { + //为空或者为默认最小值 + if (oldValue is null || DateTime.MinValue.Equals(oldValue)) + { + entityInfo.SetValue(DateTime.Now); + } + } + + if (entityInfo.PropertyName.Equals(nameof(IAuditedObject.CreatorId))) + { + if (CurrentUser.Id != null) + { + entityInfo.SetValue(CurrentUser.Id); + } + } + + //插入时,需要租户id,先预留 + if (entityInfo.PropertyName.Equals(nameof(IMultiTenant.TenantId))) + { + if (CurrentTenant is not null) + { + entityInfo.SetValue(CurrentTenant.Id); + } + } + + break; + } + + + //领域事件 + switch (entityInfo.OperationType) + { + case DataFilterType.InsertByObject: + if (entityInfo.PropertyName == nameof(IEntity.Id)) + { + EntityChangeEventHelper.PublishEntityCreatedEvent(entityInfo.EntityValue); + } + + break; + case DataFilterType.UpdateByObject: + if (entityInfo.PropertyName == nameof(IEntity.Id)) + { + //软删除,发布的是删除事件 + if (entityInfo.EntityValue is ISoftDelete softDelete) + { + if (softDelete.IsDeleted) + { + EntityChangeEventHelper.PublishEntityDeletedEvent(entityInfo.EntityValue); + } + } + else + { + EntityChangeEventHelper.PublishEntityUpdatedEvent(entityInfo.EntityValue); + } + } + + break; + case DataFilterType.DeleteByObject: + if (entityInfo.PropertyName == nameof(IEntity.Id)) + { + EntityChangeEventHelper.PublishEntityDeletedEvent(entityInfo.EntityValue); + } + + break; + } + } + + protected virtual void OnLogExecuting(string sql, SugarParameter[] pars) + { + if (Options.EnabledSqlLog) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendLine("==========NPin-SQL=========="); + sb.AppendLine(UtilMethods.GetSqlString(DbType.SqlServer, sql, pars)); + sb.AppendLine("============================"); + Logger.CreateLogger().LogDebug(sb.ToString()); + } + } + + protected virtual void OnLogExecuted(string sql, SugarParameter[] pars) + { + } + + protected virtual void EntityService(PropertyInfo property, EntityColumnInfo column) + { + } + + public void Backup() + { + string directoryName = "database_backup"; + string fileName = $"{SqlSugarClient.Ado.Connection.Database}_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); + if (!Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + + switch (Options.DbType) + { + case DbType.MySql: + // mysql 只支持.net core + SqlSugarClient.DbMaintenance.BackupDataBase(SqlSugarClient.Ado.Connection.Database, + $"{Path.Combine(directoryName, fileName)}.sql"); + break; + case DbType.Sqlite: + // sqlite 只支持.net core + SqlSugarClient.DbMaintenance.BackupDataBase(null, $"{fileName}.db"); + break; + case DbType.SqlServer: + // 第一个参数库名 + SqlSugarClient.DbMaintenance.BackupDataBase(SqlSugarClient.Ado.Connection.Database, + $"{Path.Combine(directoryName, fileName)}.bak"); + break; + default: + throw new NotImplementedException("其他数据库备份未实现"); + } + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbContextCreationContext.cs b/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbContextCreationContext.cs new file mode 100644 index 0000000..f256296 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/SqlSugarDbContextCreationContext.cs @@ -0,0 +1,28 @@ +using System.Data.Common; +using SqlSugar.DistributedSystem.Snowflake; + +namespace NPin.Framework.SqlSugarCore; + +public class SqlSugarDbContextCreationContext +{ + public static readonly AsyncLocal _current = new(); + public static SqlSugarDbContextCreationContext Current => _current.Value; + public string ConnectionStringName { get; } + public string ConnectionString { get; } + + // public DbConnection ExistsConnection { get; internal set; } + + public SqlSugarDbContextCreationContext(string connectionStringName, string connectionString) + { + ConnectionStringName = connectionStringName; + ConnectionString = connectionString; + } + + public static IDisposable Use(SqlSugarDbContextCreationContext context) + { + var previousValue = Current; + _current.Value = context; + + return new DisposableAction(() => _current.Value = previousValue); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/Uow/SqlSugarDatabaseApi.cs b/Framework/NPin.Framework.SqlSugarCore/Uow/SqlSugarDatabaseApi.cs new file mode 100644 index 0000000..889d787 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/Uow/SqlSugarDatabaseApi.cs @@ -0,0 +1,14 @@ +using NPin.Framework.SqlSugarCore.Abstractions; +using Volo.Abp.Uow; + +namespace NPin.Framework.SqlSugarCore.Uow; + +public class SqlSugarDatabaseApi : IDatabaseApi +{ + public ISqlSugarDbContext DbContext { get; } + + public SqlSugarDatabaseApi(ISqlSugarDbContext dbContext) + { + DbContext = dbContext; + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/Uow/SqlSugarTransactionApi.cs b/Framework/NPin.Framework.SqlSugarCore/Uow/SqlSugarTransactionApi.cs new file mode 100644 index 0000000..29b10f9 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/Uow/SqlSugarTransactionApi.cs @@ -0,0 +1,33 @@ +using NPin.Framework.SqlSugarCore.Abstractions; +using Volo.Abp.Uow; + +namespace NPin.Framework.SqlSugarCore.Uow; + +public class SqlSugarTransactionApi : ITransactionApi, ISupportsRollback +{ + private ISqlSugarDbContext _sqlSugarDbContext; + + public SqlSugarTransactionApi(ISqlSugarDbContext sqlSugarDbContext) + { + _sqlSugarDbContext = sqlSugarDbContext; + } + + public ISqlSugarDbContext GetDbContext() + { + return _sqlSugarDbContext; + } + + public void Dispose() + { + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + await _sqlSugarDbContext.SqlSugarClient.Ado.CommitTranAsync(); + } + + public async Task RollbackAsync(CancellationToken cancellationToken = default) + { + await _sqlSugarDbContext.SqlSugarClient.Ado.RollbackTranAsync(); + } +} \ No newline at end of file diff --git a/Framework/NPin.Framework.SqlSugarCore/Uow/UnitOfWorkSqlSugarDbContextProvider.cs b/Framework/NPin.Framework.SqlSugarCore/Uow/UnitOfWorkSqlSugarDbContextProvider.cs new file mode 100644 index 0000000..80331a0 --- /dev/null +++ b/Framework/NPin.Framework.SqlSugarCore/Uow/UnitOfWorkSqlSugarDbContextProvider.cs @@ -0,0 +1,195 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NPin.Framework.SqlSugarCore.Abstractions; +using SqlSugar; +using Volo.Abp.Data; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Threading; +using Volo.Abp.Uow; +using Check = Volo.Abp.Check; + +namespace NPin.Framework.SqlSugarCore.Uow; + +public class UnitOfWorkSqlSugarDbContextProvider : ISugarDbContextProvider + where TDbContext : ISqlSugarDbContext +{ + private readonly ISqlSugarDbConnectionCreator _dbConnectionCreator; + private readonly string MasterTenantDbDefaultName = DbConnOptions.MasterTenantDbDefaultName; + private ILogger> Logger { get; set; } + private IServiceProvider ServiceProvider { get; set; } + + private static AsyncLocalDbContextAccessor ContextInstance => AsyncLocalDbContextAccessor.Instance; + protected readonly IUnitOfWorkManager UnitOfWorkManager; + protected readonly IConnectionStringResolver ConnectionStringResolver; + protected readonly ICancellationTokenProvider CancellationTokenProvider; + protected readonly ICurrentTenant CurrentTenant; + + public UnitOfWorkSqlSugarDbContextProvider( + IUnitOfWorkManager unitOfWorkManager, + IConnectionStringResolver connectionStringResolver, + ICancellationTokenProvider cancellationTokenProvider, + ICurrentTenant currentTenant, + ISqlSugarDbConnectionCreator dbConnectionCreator + ) + { + UnitOfWorkManager = unitOfWorkManager; + ConnectionStringResolver = connectionStringResolver; + CancellationTokenProvider = cancellationTokenProvider; + CurrentTenant = currentTenant; + Logger = NullLogger>.Instance; + _dbConnectionCreator = dbConnectionCreator; + } + + public virtual async Task GetDbContextAsync() + { + var connectionStringName = ConnectionStrings.DefaultConnectionStringName; + + // 获取当前的连接字符串,未开启多租户时为空 + var connectionString = await ResolveConnectionStringAsync(connectionStringName); + var dbContextKey = $"{GetType().FullName}_{connectionString}"; + + var unitOfWork = UnitOfWorkManager.Current; + if (unitOfWork == null || !unitOfWork.Options.IsTransactional) + { + // set if is null + ContextInstance.Current ??= (TDbContext)ServiceProvider.GetRequiredService(); + + var dbContext = (TDbContext)ContextInstance.Current; + var output = DatabaseChange(dbContext, connectionStringName, connectionString); + // 提高体验,取消uow的强制性 + // 若不启用uow,创建新db,不开启事务 + return output; + } + + // 尝试当前工作单元获取db + var databaseApi = unitOfWork.FindDatabaseApi(dbContextKey); + if (databaseApi == null) + { + databaseApi = + new SqlSugarDatabaseApi(CreateDbContextAsync(unitOfWork, connectionStringName, connectionString) + .Result); + unitOfWork.AddDatabaseApi(dbContextKey, databaseApi); + } + + return (TDbContext)((SqlSugarDatabaseApi)databaseApi).DbContext; + } + + /// + /// 解析连接字符串 + /// + /// + /// + protected virtual async Task ResolveConnectionStringAsync(string connectionStringName) + { + if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false)) + { + using (CurrentTenant.Change(null)) + { + return await ConnectionStringResolver.ResolveAsync(connectionStringName); + } + } + + return await ConnectionStringResolver.ResolveAsync(connectionStringName); + } + + protected virtual TDbContext DatabaseChange(TDbContext dbContext, string configId, string connectionString) + { + // 没有检测到使用多租户功能,默认使用默认库即可 + if (string.IsNullOrWhiteSpace(connectionString)) + { + connectionString = dbContext.Options.Url; + configId = DbConnOptions.TenantDbDefaultName; + } + + var dbOption = dbContext.Options; + var db = dbContext.SqlSugarClient.AsTenant(); + // 主库的Db切换,当操作的是租户表的时候 + if (CurrentTenant.Name == MasterTenantDbDefaultName) + { + //直接切换 + configId = MasterTenantDbDefaultName; + var conStrOrNull = dbOption.GetDefaultMasterSaasMultiTenancy(); + Check.NotNull(conStrOrNull, "租户主库未找到"); + connectionString = conStrOrNull.Url; + } + + // 租户Db的动态切换 + // 二级缓存 + var changed = false; + if (!db.IsAnyConnection(configId)) + { + var config = _dbConnectionCreator.Build(options => + { + options.DbType = dbOption.DbType!.Value; + options.ConfigId = configId; //设置库的唯一标识 + options.IsAutoCloseConnection = true; + options.ConnectionString = connectionString; + }); + //添加一个db到当前上下文 (Add部分不写上下文不会共享) + db.AddConnection(config); + changed = true; + } + + var currentDb = db.GetConnection(configId) as ISqlSugarClient; + + //设置Aop + if (changed) + { + _dbConnectionCreator.SetDbAop(currentDb); + } + + + dbContext.SetSqlSugarClient(currentDb); + return dbContext; + } + + protected virtual async Task CreateDbContextAsync(IUnitOfWork unitOfWork, string connectionStringName, + string connectionString) + { + var creationContext = new SqlSugarDbContextCreationContext(connectionStringName, connectionString); + //将连接key进行传值 + using (SqlSugarDbContextCreationContext.Use(creationContext)) + { + var dbContext = await CreateDbContextAsync(unitOfWork); + + //获取到DB之后,对多租户多库进行处理 + var changedDbContext = DatabaseChange(dbContext, connectionStringName, connectionString); + return changedDbContext; + } + } + + protected virtual async Task CreateDbContextAsync(IUnitOfWork unitOfWork) + { + return unitOfWork.Options.IsTransactional + ? await CreateDbContextWithTransactionAsync(unitOfWork) + : unitOfWork.ServiceProvider.GetRequiredService(); + } + + protected virtual async Task CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork) + { + //事务key + var transactionApiKey = $"SqlsugarCore_{SqlSugarDbContextCreationContext.Current.ConnectionString}"; + + //尝试查找事务 + var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as SqlSugarTransactionApi; + + //该db还没有进行开启事务 + if (activeTransaction == null) + { + //获取到db添加事务即可 + var dbContext = unitOfWork.ServiceProvider.GetRequiredService(); + var transaction = new SqlSugarTransactionApi( + dbContext + ); + unitOfWork.AddTransactionApi(transactionApiKey, transaction); + + await dbContext.SqlSugarClient.Ado.BeginTranAsync(); + return dbContext; + } + else + { + return (TDbContext)activeTransaction.GetDbContext(); + } + } +} \ No newline at end of file diff --git a/NPin.sln b/NPin.sln new file mode 100644 index 0000000..5b39685 --- /dev/null +++ b/NPin.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.Core", "Framework\NPin.Framework.Core\NPin.Framework.Core.csproj", "{088B4948-AE5B-45B3-A9FA-B853671CFA05}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Framework", "Framework", "{F2A0A89E-A2F9-48CF-AD38-0318B5ACD11C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{86F61EBB-4ACC-459C-AB3C-C8D486C3017D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Web", "src\NPin.Web\NPin.Web.csproj", "{91A9536C-FD3D-4099-8459-6C1C5238C037}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.Mapster", "Framework\NPin.Framework.Mapster\NPin.Framework.Mapster.csproj", "{E6BCF6D7-A0B0-445D-AFE0-8F8C779FAB6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.SqlSugarCore.Abstractions", "Framework\NPin.Framework.SqlSugarCore.Abstractions\NPin.Framework.SqlSugarCore.Abstractions.csproj", "{CBC83167-9806-41ED-98FA-105D38825A88}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NPin.Framework.SqlSugarCore", "Framework\NPin.Framework.SqlSugarCore\NPin.Framework.SqlSugarCore.csproj", "{CC3D8269-F965-4C87-93F2-351AFEF9E3D2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {088B4948-AE5B-45B3-A9FA-B853671CFA05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {088B4948-AE5B-45B3-A9FA-B853671CFA05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {088B4948-AE5B-45B3-A9FA-B853671CFA05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {088B4948-AE5B-45B3-A9FA-B853671CFA05}.Release|Any CPU.Build.0 = Release|Any CPU + {91A9536C-FD3D-4099-8459-6C1C5238C037}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91A9536C-FD3D-4099-8459-6C1C5238C037}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91A9536C-FD3D-4099-8459-6C1C5238C037}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91A9536C-FD3D-4099-8459-6C1C5238C037}.Release|Any CPU.Build.0 = Release|Any CPU + {E6BCF6D7-A0B0-445D-AFE0-8F8C779FAB6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6BCF6D7-A0B0-445D-AFE0-8F8C779FAB6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6BCF6D7-A0B0-445D-AFE0-8F8C779FAB6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6BCF6D7-A0B0-445D-AFE0-8F8C779FAB6B}.Release|Any CPU.Build.0 = Release|Any CPU + {CBC83167-9806-41ED-98FA-105D38825A88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBC83167-9806-41ED-98FA-105D38825A88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBC83167-9806-41ED-98FA-105D38825A88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBC83167-9806-41ED-98FA-105D38825A88}.Release|Any CPU.Build.0 = Release|Any CPU + {CC3D8269-F965-4C87-93F2-351AFEF9E3D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC3D8269-F965-4C87-93F2-351AFEF9E3D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC3D8269-F965-4C87-93F2-351AFEF9E3D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC3D8269-F965-4C87-93F2-351AFEF9E3D2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {088B4948-AE5B-45B3-A9FA-B853671CFA05} = {F2A0A89E-A2F9-48CF-AD38-0318B5ACD11C} + {91A9536C-FD3D-4099-8459-6C1C5238C037} = {86F61EBB-4ACC-459C-AB3C-C8D486C3017D} + {E6BCF6D7-A0B0-445D-AFE0-8F8C779FAB6B} = {F2A0A89E-A2F9-48CF-AD38-0318B5ACD11C} + {CBC83167-9806-41ED-98FA-105D38825A88} = {F2A0A89E-A2F9-48CF-AD38-0318B5ACD11C} + {CC3D8269-F965-4C87-93F2-351AFEF9E3D2} = {F2A0A89E-A2F9-48CF-AD38-0318B5ACD11C} + EndGlobalSection +EndGlobal diff --git a/src/NPin.Web/.dockerignore b/src/NPin.Web/.dockerignore new file mode 100644 index 0000000..cd967fc --- /dev/null +++ b/src/NPin.Web/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/src/NPin.Web/Dockerfile b/src/NPin.Web/Dockerfile new file mode 100644 index 0000000..3fe8b87 --- /dev/null +++ b/src/NPin.Web/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["NPin/NPin.csproj", "NPin/"] +RUN dotnet restore "NPin/NPin.csproj" +COPY . . +WORKDIR "/src/NPin" +RUN dotnet build "NPin.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "NPin.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "NPin.dll"] diff --git a/src/NPin.Web/NPin.Web.csproj b/src/NPin.Web/NPin.Web.csproj new file mode 100644 index 0000000..d4cb4c6 --- /dev/null +++ b/src/NPin.Web/NPin.Web.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + true + Linux + NPin + + + + + + + + diff --git a/src/NPin.Web/NPin.http b/src/NPin.Web/NPin.http new file mode 100644 index 0000000..2ea2787 --- /dev/null +++ b/src/NPin.Web/NPin.http @@ -0,0 +1,6 @@ +@NPin_HostAddress = http://localhost:5122 + +GET {{NPin_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/NPin.Web/Program.cs b/src/NPin.Web/Program.cs new file mode 100644 index 0000000..161f695 --- /dev/null +++ b/src/NPin.Web/Program.cs @@ -0,0 +1,44 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast") + .WithOpenApi(); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} \ No newline at end of file diff --git a/src/NPin.Web/Properties/launchSettings.json b/src/NPin.Web/Properties/launchSettings.json new file mode 100644 index 0000000..bc50b86 --- /dev/null +++ b/src/NPin.Web/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:27344", + "sslPort": 44340 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5122", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7122;http://localhost:5122", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/NPin.Web/appsettings.Development.json b/src/NPin.Web/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/NPin.Web/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/NPin.Web/appsettings.json b/src/NPin.Web/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/NPin.Web/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}