From 6a7556fb73a70b24343c93b51cdaff90542d94f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Mon, 24 Mar 2025 18:12:35 +0800 Subject: [PATCH 01/17] =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=A4=9A=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E3=80=81=E6=89=A9=E5=B1=95=E5=88=86=E5=BA=93=E5=88=86?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DbAccess/Impl/SimpleEntityScaner.cs | 68 ++++++++++++++ .../DbAccess/Impl/TenantBaseDbContext.cs | 50 +++++++++++ .../DbAccess/Impl/TenantEntityBuilder.cs | 61 +++++++++++++ .../DbAccess/Interface/IEntityScaner.cs | 11 +++ .../DbAccess/Interface/ITenantDbContext.cs | 9 ++ .../Interface/ITenantEntityBuilder.cs | 25 ++++++ .../DbAccess/TenantModelCacheKey.cs | 56 ++++++++++++ .../DbAccess/TenantModelCacheKeyFactory.cs | 32 +++++++ .../Models/ConnectionResolverType.cs | 28 ++++++ .../Models/DbIntegrationType.cs | 10 +++ .../DbMultiTenants/Models/DbsetProperty.cs | 18 ++++ .../DbMultiTenants/Models/TenantInfo.cs | 89 +++++++++++++++++++ .../DbMultiTenants/Models/TenantOption.cs | 43 +++++++++ 13 files changed, 500 insertions(+) create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs new file mode 100644 index 0000000..49e8bf0 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs @@ -0,0 +1,68 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.EntityFrameworkCore; +using System.Reflection; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + /// + /// 简单实体扫描器类,用于扫描数据库上下文中的实体类型 + /// + /// 数据库上下文类型 + public class SimpleEntityScaner : IEntityScaner + where TDbContext : DbContext + { + private static List dbProperties; + private static readonly object locker = new object(); + + /// + /// 扫描数据库上下文中的实体类型 + /// + /// 实体属性列表 + public IList ScanEntityTypes() + { + if (dbProperties == null) + { + lock (locker) + { + if (dbProperties == null) + { + var contextType = typeof(TDbContext); + var properties = contextType.GetProperties(); + var entityProperties = properties.Where(r => IsDbSet(r)).ToList(); + + dbProperties = new List(); + foreach (var property in entityProperties) + { + dbProperties.Add(new DbsetProperty + { + PropertyName = property.Name, + PropertyType = property.PropertyType.GetGenericArguments()[0] + }); + } + } + } + } + return dbProperties; + } + + /// + /// 判断属性是否为 DbSet 类型 + /// + /// 属性信息 + /// 如果是 DbSet 类型,返回 true;否则返回 false + private bool IsDbSet(PropertyInfo property) + { + if (property.CanRead && property.PropertyType.IsGenericType + && typeof(DbSet<>).GetGenericTypeDefinition().Equals(property.PropertyType.GetGenericTypeDefinition())) + { + return true; + } + return false; + } + } +} + + + + + diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs new file mode 100644 index 0000000..fdddc65 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs @@ -0,0 +1,50 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + /// + /// 表示租户基础数据库上下文的抽象类 + /// + public abstract class TenantBaseDbContext : DbContext, ITenantDbContext + { + /// + /// 获取或设置租户信息 + /// + public TenantInfo Tenant { get; protected internal set; } + + private readonly IServiceProvider serviceProvider; + + /// + /// 初始化 TenantBaseDbContext 类的新实例 + /// + /// 数据库上下文选项 + /// 租户信息 + /// 服务提供程序 + public TenantBaseDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) + : base(options) + { + this.serviceProvider = serviceProvider; + this.Tenant = tenant; + } + + /// + /// 配置模型 + /// + /// 模型构建器 + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var builderType = typeof(ITenantEntityBuilder<>).MakeGenericType(this.GetType()); + + ITenantEntityBuilder entityBuilder = (ITenantEntityBuilder)serviceProvider.GetService(builderType); + + entityBuilder.UpdateEntities(modelBuilder); + } + } +} + + + + + + diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs new file mode 100644 index 0000000..309e6aa --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs @@ -0,0 +1,61 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + /// + /// 表示租户实体构建器的类 + /// + /// 数据库上下文类型 + public class TenantEntityBuilder : ITenantEntityBuilder + where TDbContext : DbContext + { + private readonly IEntityScaner entityScaner; + private readonly TenantOption tenantOption; + private readonly TenantInfo tenantInfo; + + /// + /// 初始化 TenantEntityBuilder 类的新实例 + /// + /// 实体扫描器 + /// 租户选项 + /// 租户信息 + public TenantEntityBuilder(IEntityScaner entityScaner, TenantOption tenantOption, TenantInfo tenantInfo) + { + this.tenantInfo = tenantInfo; + this.tenantOption = tenantOption; + this.entityScaner = entityScaner; + } + + /// + /// 更新实体模型 + /// + /// 模型构建器 + public void UpdateEntities(ModelBuilder modelBuilder) + { + var dbsetProperties = entityScaner.ScanEntityTypes(); + + foreach (var property in dbsetProperties) + { + var entity = modelBuilder.Entity(property.PropertyType); + switch (this.tenantOption.ConnectionType) + { + case ConnectionResolverType.BySchema: + var tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) + ?? property.PropertyName; + var schemaName = this.tenantOption.SchemaFunc?.Invoke(this.tenantInfo) + ?? this.tenantInfo.Name; + entity.ToTable(tableName, schemaName); + break; + case ConnectionResolverType.ByTable: + tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) + ?? $"{this.tenantInfo.Name}_{property.PropertyName}"; + entity.ToTable(tableName); + break; + default: + break; + } + } + } + } +} \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs new file mode 100644 index 0000000..c75bda3 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs @@ -0,0 +1,11 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + public interface IEntityScaner + where TDbContext : DbContext + { + IList ScanEntityTypes(); + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs new file mode 100644 index 0000000..be620a3 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs @@ -0,0 +1,9 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + public interface ITenantDbContext + { + TenantInfo Tenant { get;} + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs new file mode 100644 index 0000000..fc7f1a9 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + /// + /// 表示租户实体构建器的接口 + /// + public interface ITenantEntityBuilder + { + /// + /// 更新实体模型 + /// + /// 模型构建器 + void UpdateEntities(ModelBuilder modelBuilder); + } + + /// + /// 表示特定数据库上下文类型的租户实体构建器的接口 + /// + /// 数据库上下文类型 + public interface ITenantEntityBuilder : ITenantEntityBuilder + where TDbContext : DbContext + { + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs new file mode 100644 index 0000000..6a4e2e7 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + /// + /// 表示租户模型缓存键的类 + /// + /// 数据库上下文类型 + internal sealed class TenantModelCacheKey : ModelCacheKey + where TDbContext : DbContext, ITenantDbContext + { + private readonly TDbContext context; + private readonly string identifier; + + /// + /// 初始化 TenantModelCacheKey 类的新实例 + /// + /// 数据库上下文 + /// 租户标识符 + public TenantModelCacheKey(TDbContext context, string identifier) : base(context) + { + this.context = context; + this.identifier = identifier; + } + + /// + /// 确定当前对象是否等于另一个对象 + /// + /// 要与当前对象进行比较的对象 + /// 如果相等,返回 true;否则返回 false + protected override bool Equals(ModelCacheKey other) + { + return base.Equals(other) && (other as TenantModelCacheKey)?.identifier == identifier; + } + + /// + /// 返回当前对象的哈希代码 + /// + /// 当前对象的哈希代码 + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + if (identifier != null) + { + hashCode ^= identifier.GetHashCode(); + } + + return hashCode; + } + } +} + + + + diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs new file mode 100644 index 0000000..cbb23fa --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + /// + /// 表示租户模型缓存键工厂的类 + /// + /// 数据库上下文类型 + public sealed class TenantModelCacheKeyFactory : ModelCacheKeyFactory + where TDbContext : DbContext, ITenantDbContext + { + /// + /// 初始化 TenantModelCacheKeyFactory 类的新实例 + /// + /// 模型缓存键工厂的依赖项 + public TenantModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) + { + } + + /// + /// 创建模型缓存键 + /// + /// 数据库上下文 + /// 模型缓存键对象 + public override object Create(DbContext context) + { + var dbContext = context as TDbContext; + return new TenantModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs new file mode 100644 index 0000000..5811c8c --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs @@ -0,0 +1,28 @@ +namespace LingYanAspCoreFramework.DbMultiTenants.Models +{ + /// + /// 表示连接解析器类型的枚举 + /// + public enum ConnectionResolverType + { + /// + /// 默认连接解析器类型 + /// + Default = 0, + + /// + /// 按数据库解析连接 + /// + ByDatabase = 1, + + /// + /// 按表解析连接 + /// + ByTable = 2, + + /// + /// 按模式解析连接 + /// + BySchema = 3 + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs new file mode 100644 index 0000000..4a24d54 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs @@ -0,0 +1,10 @@ +namespace LingYanAspCoreFramework.DbMultiTenants.Models +{ + public enum DbIntegrationType + { + None = 0, + Mysql = 1, + SqlServer = 2, + Postgre = 3, + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs new file mode 100644 index 0000000..f040560 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs @@ -0,0 +1,18 @@ +namespace LingYanAspCoreFramework.DbMultiTenants.Models +{ + /// + /// 表示数据库集属性的类 + /// + public class DbsetProperty + { + /// + /// 属性的类型 + /// + public Type PropertyType { get; set; } + + /// + /// 属性的名称 + /// + public string PropertyName { get; set; } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs new file mode 100644 index 0000000..e0b475b --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs @@ -0,0 +1,89 @@ +锘縰sing System.Dynamic; + +namespace LingYanAspCoreFramework.DbMultiTenants.Models +{ + /// + /// 琛ㄧず绉熸埛淇℃伅鐨勭被 + /// + public class TenantInfo : DynamicObject + { + /// + /// 绉熸埛鍚嶇О + /// + public string Name { get; set; } + + /// + /// 绉熸埛鏄惁瀛樺湪 + /// + public bool IsPresent { get; set; } + + /// + /// 绉熸埛鐢熸垚鍣 + /// + public object Generator { get; set; } + + /// + /// 鍔ㄦ佸鍣紝鐢ㄤ簬瀛樺偍绉熸埛鐨勫姩鎬佸睘鎬 + /// + public dynamic Container { get; set; } = new ExpandoObject(); + + /// + /// 鍐呴儴瀛楀吀锛岀敤浜庡瓨鍌ㄥ姩鎬佸睘鎬 + /// + private Dictionary dictionary = new Dictionary(); + + /// + /// 灏濊瘯鑾峰彇鍔ㄦ佹垚鍛樼殑鍊 + /// + /// 鎴愬憳缁戝畾鍣 + /// 鎴愬憳鐨勫 + /// 濡傛灉鑾峰彇鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + return dictionary.TryGetValue(binder.Name, out result); + } + + /// + /// 灏濊瘯璁剧疆鍔ㄦ佹垚鍛樼殑鍊 + /// + /// 鎴愬憳缁戝畾鍣 + /// 瑕佽缃殑鍊 + /// 濡傛灉璁剧疆鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false + public override bool TrySetMember(SetMemberBinder binder, object value) + { + dictionary[binder.Name] = value; + return true; + } + + /// + /// 灏濊瘯璁剧疆鍔ㄦ佺储寮曠殑鍊 + /// + /// 绱㈠紩缁戝畾鍣 + /// 绱㈠紩鏁扮粍 + /// 瑕佽缃殑鍊 + /// 濡傛灉璁剧疆鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false + public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) + { + if (!(indexes?.Length > 0)) + return false; + dictionary[indexes[0].ToString()] = value; + return true; + } + + /// + /// 灏濊瘯鑾峰彇鍔ㄦ佺储寮曠殑鍊 + /// + /// 绱㈠紩缁戝畾鍣 + /// 绱㈠紩鏁扮粍 + /// 绱㈠紩鐨勫 + /// 濡傛灉鑾峰彇鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false + public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) + { + result = null; + if (!(indexes?.Length > 0)) + return false; + result = dictionary[indexes[0].ToString()]; + return true; + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs new file mode 100644 index 0000000..78f81a5 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs @@ -0,0 +1,43 @@ +namespace LingYanAspCoreFramework.DbMultiTenants.Models +{ + /// + /// 表示租户选项的类 + /// + public class TenantOption + { + /// + /// 租户选项的键 + /// + public string Key { get; set; } + + /// + /// 连接解析器类型 + /// + public ConnectionResolverType ConnectionType { get; set; } + + /// + /// 数据库集成类型 + /// + public DbIntegrationType DbType { get; set; } + + /// + /// 连接名称 + /// + public string ConnectionName { get; set; } + + /// + /// 连接前缀 + /// + public string ConnectionPrefix { get; set; } + + /// + /// 表名生成函数 + /// + public Func TableNameFunc { get; set; } + + /// + /// 模式生成函数 + /// + public Func SchemaFunc { get; set; } + } +} -- Gitee From d923e52a64b86726d56f9cb2b91606f53d64346d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Tue, 25 Mar 2025 17:50:57 +0800 Subject: [PATCH 02/17] =?UTF-8?q?=E5=88=86=E5=BA=93=E5=88=86=E8=A1=A8?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LingTest/Controllers/ProductController.cs | 51 ++++ LingTest/Controllers/TokenController.cs | 52 ---- LingTest/DbContexts/LingDbContext.cs | 41 --- LingTest/DbContexts/StoreDbContext.cs | 24 ++ LingTest/Entitys/Product.cs | 18 ++ LingTest/Entitys/RoleEntity.cs | 8 - LingTest/Entitys/RoleRouteGroupEntity.cs | 8 - LingTest/Entitys/RouteEntity.cs | 8 - LingTest/Entitys/UserEntity.cs | 8 - LingTest/Entitys/UserRoleGroupEntity.cs | 8 - LingTest/Filters/AuthorFilter.cs | 11 - LingTest/Filters/PermissionHandle.cs | 13 - .../Generator/CombindedConnectionGenerator.cs | 37 +++ .../Migrations/20241223072307_v1.Designer.cs | 196 ------------ LingTest/Migrations/20241223072307_v1.cs | 146 --------- .../Migrations/20241223133026_v2.Designer.cs | 194 ------------ LingTest/Migrations/20241223133026_v2.cs | 74 ----- .../Migrations/20241225083119_v3.Designer.cs | 194 ------------ LingTest/Migrations/20241225083119_v3.cs | 28 -- .../Migrations/20241227064847_v4.Designer.cs | 202 ------------- LingTest/Migrations/20241227064847_v4.cs | 40 --- .../Migrations/LingDbContextModelSnapshot.cs | 199 ------------ LingTest/RestApiModule.cs | 14 +- LingTest/appsettings.json | 5 + .../DbMultiTenants/Core/Constans.cs | 14 + .../Core/Extension/TenantCoreExtension.cs | 282 ++++++++++++++++++ .../Core/Impl/NamedConnectionGenerator.cs | 43 +++ .../Impl/SimpleHeaderTenantInfoGenerator.cs | 32 ++ .../Core/Impl/TenantConnectionResolver.cs | 69 +++++ .../Core/Interface/IConnectionGenerator.cs | 13 + .../Interface/ITenantConnectionResolver.cs | 7 + .../Core/Interface/ITenantInfoGenerator.cs | 11 + .../Core/TenantInfoMiddleware.cs | 22 ++ .../DbMultiTenants/Core/TenantSettings.cs | 63 ++++ .../DbMultiTenants/Core/TenantSettingsT.cs | 65 ++++ .../DbAccess/Impl/SimpleEntityScaner.cs | 10 +- .../DbAccess/Impl/TenantBaseDbContext.cs | 11 +- .../DbAccess/Impl/TenantEntityBuilder.cs | 4 +- .../DbAccess/Interface/IEntityScaner.cs | 4 +- .../DbAccess/Interface/ITenantDbContext.cs | 2 +- .../Interface/ITenantEntityBuilder.cs | 2 +- .../DbAccess/TenantModelCacheKey.cs | 1 + .../DbAccess/TenantModelCacheKeyFactory.cs | 1 + .../DbExtensions/MySqlTenantExtension.cs | 123 ++++++++ .../DbExtensions/PostgreTenantExtension.cs | 90 ++++++ .../DbExtensions/SqlServerTenantExtension.cs | 91 ++++++ .../{ => Enum}/ConnectionResolverType.cs | 2 +- .../Models/{ => Enum}/DbIntegrationType.cs | 2 +- .../DbMultiTenants/Models/TenantOption.cs | 2 + .../Extensions/IocExtension.cs | 116 +++---- .../Helpers/LoggerHelper.cs | 5 +- .../LingYanAspCoreFramework.csproj | 1 + 52 files changed, 1151 insertions(+), 1516 deletions(-) create mode 100644 LingTest/Controllers/ProductController.cs delete mode 100644 LingTest/Controllers/TokenController.cs delete mode 100644 LingTest/DbContexts/LingDbContext.cs create mode 100644 LingTest/DbContexts/StoreDbContext.cs create mode 100644 LingTest/Entitys/Product.cs delete mode 100644 LingTest/Entitys/RoleEntity.cs delete mode 100644 LingTest/Entitys/RoleRouteGroupEntity.cs delete mode 100644 LingTest/Entitys/RouteEntity.cs delete mode 100644 LingTest/Entitys/UserEntity.cs delete mode 100644 LingTest/Entitys/UserRoleGroupEntity.cs delete mode 100644 LingTest/Filters/AuthorFilter.cs delete mode 100644 LingTest/Filters/PermissionHandle.cs create mode 100644 LingTest/Generator/CombindedConnectionGenerator.cs delete mode 100644 LingTest/Migrations/20241223072307_v1.Designer.cs delete mode 100644 LingTest/Migrations/20241223072307_v1.cs delete mode 100644 LingTest/Migrations/20241223133026_v2.Designer.cs delete mode 100644 LingTest/Migrations/20241223133026_v2.cs delete mode 100644 LingTest/Migrations/20241225083119_v3.Designer.cs delete mode 100644 LingTest/Migrations/20241225083119_v3.cs delete mode 100644 LingTest/Migrations/20241227064847_v4.Designer.cs delete mode 100644 LingTest/Migrations/20241227064847_v4.cs delete mode 100644 LingTest/Migrations/LingDbContextModelSnapshot.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs rename LingYanAspCoreFramework/DbMultiTenants/Models/{ => Enum}/ConnectionResolverType.cs (89%) rename LingYanAspCoreFramework/DbMultiTenants/Models/{ => Enum}/DbIntegrationType.cs (68%) diff --git a/LingTest/Controllers/ProductController.cs b/LingTest/Controllers/ProductController.cs new file mode 100644 index 0000000..98b6ad1 --- /dev/null +++ b/LingTest/Controllers/ProductController.cs @@ -0,0 +1,51 @@ +using LingTest.DbContexts; +using LingTest.Entitys; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LingTest.Controllers +{ + [ApiController] + [Route("api/[controller]s")] + [AllowAnonymous] + public class ProductController : ControllerBase + { + private readonly StoreDbContext storeDbContext; + + public ProductController(StoreDbContext storeDbContext) + { + this.storeDbContext = storeDbContext; + //this.storeDbContext.Database.Migrate(); + this.storeDbContext.Database.EnsureCreated(); + } + + [HttpPost("")] + public async Task> Create(Product product) + { + var rct = await this.storeDbContext.Products.AddAsync(product); + + await this.storeDbContext.SaveChangesAsync(); + + return rct?.Entity; + + } + + [HttpGet("{id}")] + public async Task> Get([FromRoute] int id) + { + + var rct = await this.storeDbContext.Products.FindAsync(id); + + return rct; + + } + + [HttpGet("")] + public async Task>> Search() + { + var rct = await this.storeDbContext.Products.ToListAsync(); + return rct; + } + } +} \ No newline at end of file diff --git a/LingTest/Controllers/TokenController.cs b/LingTest/Controllers/TokenController.cs deleted file mode 100644 index 269b4b7..0000000 --- a/LingTest/Controllers/TokenController.cs +++ /dev/null @@ -1,52 +0,0 @@ -using LingYanAspCoreFramework.Extensions; -using LingYanAspCoreFramework.SampleRoots; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Yitter.IdGenerator; - -namespace LingTest.Controllers -{ - /// - /// 天气控制器 - /// - [ApiController] - [Route("api/[controller]/[action]")] - [Authorize(AuthenticationSchemes = SampleHelper.BearerScheme, Policy = SampleHelper.EmpowerPolicy)] - public class TokenController : ControllerBase - { - private readonly ILogger logger; - - public TokenController(ILogger logger) - { - this.logger = logger; - } - /// - /// 获取Token - /// - /// 测试是否引入 - /// 返回默认值 - - [HttpGet] - [AllowAnonymous] - public IActionResult Get(string sss) - { - if (string.IsNullOrEmpty(sss)) - { - throw new Exception("中途测试抛异常"); - } - Dictionary keyValuePairs = new Dictionary(); - keyValuePairs.Add(SampleHelper.ClaimUserId, "77777"); - var token = keyValuePairs.CreateJwtToken(SampleHelper.RSAPublicKey); - return Ok(token); - } - /// - /// 水水水水水 - /// - /// - [HttpGet] - public IActionResult Test() - { - return Ok("完成"); - } - } -} \ No newline at end of file diff --git a/LingTest/DbContexts/LingDbContext.cs b/LingTest/DbContexts/LingDbContext.cs deleted file mode 100644 index 6faff9b..0000000 --- a/LingTest/DbContexts/LingDbContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -锘縰sing LingTest.Entitys; -using LingYanAspCoreFramework.Attributes; -using LingYanAspCoreFramework.SampleRoots; -using Microsoft.EntityFrameworkCore; - -namespace LingTest.DbContexts -{ - [LYDbContext("Default")] - public class LingDbContext : DbContext - { - public LingDbContext() - { - } - - public LingDbContext(DbContextOptions dbContextOptions) : base(dbContextOptions) - { - } - - public DbSet Users { get; set; } - public DbSet Roles { get; set; } - public DbSet Routes { get; set; } - public DbSet UserRoleGroups { get; set; } - public DbSet RoleRouteGroups { get; set; } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - base.OnConfiguring(optionsBuilder); - if (!optionsBuilder.IsConfigured) - { - optionsBuilder.UseMySql(SampleHelper.MysqlConnectionDic["Default"], ServerVersion.AutoDetect(SampleHelper.MysqlConnectionDic["Default"])) - .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) - .EnableSensitiveDataLogging(); - } - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - } - } -} \ No newline at end of file diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs new file mode 100644 index 0000000..610ff81 --- /dev/null +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -0,0 +1,24 @@ +using LingTest.Entitys; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; + +namespace LingTest.DbContexts +{ + public class StoreDbContext : TenantBaseDbContext + { + public DbSet Products => this.Set(); + + public StoreDbContext(IMemoryCache cache,DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) + : base(options, tenant, serviceProvider) + { + string ab="ab"; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/LingTest/Entitys/Product.cs b/LingTest/Entitys/Product.cs new file mode 100644 index 0000000..736a4cc --- /dev/null +++ b/LingTest/Entitys/Product.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace LingTest.Entitys +{ + public class Product + { + [Key] + public int Id { get; set; } + + [StringLength(50), Required] + public string Name { get; set; } + + [StringLength(50)] + public string Category { get; set; } + + public double? Price { get; set; } + } +} diff --git a/LingTest/Entitys/RoleEntity.cs b/LingTest/Entitys/RoleEntity.cs deleted file mode 100644 index ebd66c6..0000000 --- a/LingTest/Entitys/RoleEntity.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縰sing LingYanAspCoreFramework.BaseRoots; - -namespace LingTest.Entitys -{ - public class RoleEntity : BaseRole - { - } -} \ No newline at end of file diff --git a/LingTest/Entitys/RoleRouteGroupEntity.cs b/LingTest/Entitys/RoleRouteGroupEntity.cs deleted file mode 100644 index 87f0fd1..0000000 --- a/LingTest/Entitys/RoleRouteGroupEntity.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縰sing LingYanAspCoreFramework.BaseRoots; - -namespace LingTest.Entitys -{ - public class RoleRouteGroupEntity : BaseRoleRouteGroup - { - } -} \ No newline at end of file diff --git a/LingTest/Entitys/RouteEntity.cs b/LingTest/Entitys/RouteEntity.cs deleted file mode 100644 index 29c3e51..0000000 --- a/LingTest/Entitys/RouteEntity.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縰sing LingYanAspCoreFramework.BaseRoots; - -namespace LingTest.Entitys -{ - public class RouteEntity : BaseRoute - { - } -} \ No newline at end of file diff --git a/LingTest/Entitys/UserEntity.cs b/LingTest/Entitys/UserEntity.cs deleted file mode 100644 index 6b8b52f..0000000 --- a/LingTest/Entitys/UserEntity.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縰sing LingYanAspCoreFramework.BaseRoots; - -namespace LingTest.Entitys -{ - public class UserEntity : BaseUser - { - } -} \ No newline at end of file diff --git a/LingTest/Entitys/UserRoleGroupEntity.cs b/LingTest/Entitys/UserRoleGroupEntity.cs deleted file mode 100644 index 9d4a9a9..0000000 --- a/LingTest/Entitys/UserRoleGroupEntity.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縰sing LingYanAspCoreFramework.BaseRoots; - -namespace LingTest.Entitys -{ - public class UserRoleGroupEntity : BaseUserRoleGroup - { - } -} \ No newline at end of file diff --git a/LingTest/Filters/AuthorFilter.cs b/LingTest/Filters/AuthorFilter.cs deleted file mode 100644 index 0e1d204..0000000 --- a/LingTest/Filters/AuthorFilter.cs +++ /dev/null @@ -1,11 +0,0 @@ -锘縰sing LingTest.Entitys; -using LingYanAspCoreFramework.Attributes; -using LingYanAspCoreFramework.SampleRoots; - -namespace LingTest.Filters -{ - [LYGlobalFilter] - public class AuthorFilter:SampleGlobalAuthorizationFilterAttribute - { - } -} diff --git a/LingTest/Filters/PermissionHandle.cs b/LingTest/Filters/PermissionHandle.cs deleted file mode 100644 index 3fadd71..0000000 --- a/LingTest/Filters/PermissionHandle.cs +++ /dev/null @@ -1,13 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Attributes; -using LingYanAspCoreFramework.SampleRoots; - -namespace LingTest.Filters -{ - [LYAuthorizeHandler] - public class PermissionHandle : SampleGlobalPermissionHandle - { - public PermissionHandle(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) - { - } - } -} diff --git a/LingTest/Generator/CombindedConnectionGenerator.cs b/LingTest/Generator/CombindedConnectionGenerator.cs new file mode 100644 index 0000000..3e76d9c --- /dev/null +++ b/LingTest/Generator/CombindedConnectionGenerator.cs @@ -0,0 +1,37 @@ +锘縰sing LingYanAspCoreFramework.Attributes; +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.Helpers; + +namespace LingTest.Generator +{ + [LYTService(typeof(IConnectionGenerator),ServiceLifetime =ServiceLifetime.Scoped)] + public class CombindedConnectionGenerator : IConnectionGenerator + { + private readonly IConfiguration configuration; + public string TenantKey => ""; + + public CombindedConnectionGenerator(IConfiguration configuration) + { + this.configuration = configuration; + } + + + public string GetConnection(TenantOption option, TenantInfo tenantInfo) + { + var span = tenantInfo.Name.AsSpan(); + if (span.Length > 4 && int.TryParse(span[5].ToString(), out var number)) + { + var connection= configuration.GetConnectionString($"{option.ConnectionPrefix}container{number%2+1}"); + LoggerHelper.DefaultLog(connection); + return connection; + } + throw new NotSupportedException("tenant invalid"); + } + + public bool MatchTenantKey(string tenantKey) + { + return true; + } + } +} diff --git a/LingTest/Migrations/20241223072307_v1.Designer.cs b/LingTest/Migrations/20241223072307_v1.Designer.cs deleted file mode 100644 index 69931f6..0000000 --- a/LingTest/Migrations/20241223072307_v1.Designer.cs +++ /dev/null @@ -1,196 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(LingDbContext))] - [Migration("20241223072307_v1")] - partial class v1 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.RoleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Roles"); - }); - - modelBuilder.Entity("LingTest.Entitys.RoleRouteGroup", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("RoleRouteGroups"); - }); - - modelBuilder.Entity("LingTest.Entitys.RouteEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("ContrillereFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Controller") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerSummary") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Display") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplayFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplaySummary") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("HttpMethod") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("ProjectName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RoutePrefix") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RouteTemplate") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Routes"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("NickName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Password") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Phone") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserRoleGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("UserRoleGroups"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20241223072307_v1.cs b/LingTest/Migrations/20241223072307_v1.cs deleted file mode 100644 index 0f6f5bf..0000000 --- a/LingTest/Migrations/20241223072307_v1.cs +++ /dev/null @@ -1,146 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v1 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateTable( - name: "RoleRouteGroups", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - IsDeleted = table.Column(type: "tinyint(1)", nullable: false), - CreateTimeStamp = table.Column(type: "bigint", nullable: false), - RoleId = table.Column(type: "bigint", nullable: false), - RouteId = table.Column(type: "bigint", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_RoleRouteGroups", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateTable( - name: "Roles", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - IsDeleted = table.Column(type: "tinyint(1)", nullable: false), - CreateTimeStamp = table.Column(type: "bigint", nullable: false), - Name = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Description = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4") - }, - constraints: table => - { - table.PrimaryKey("PK_Roles", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateTable( - name: "Routes", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - IsDeleted = table.Column(type: "tinyint(1)", nullable: false), - CreateTimeStamp = table.Column(type: "bigint", nullable: false), - Display = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - DisplaySummary = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - DisplayFullName = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Controller = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - ControllerSummary = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - ContrillereFullName = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - RouteTemplate = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - HttpMethod = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - RoutePrefix = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - ProjectName = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4") - }, - constraints: table => - { - table.PrimaryKey("PK_Routes", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateTable( - name: "UserRoleGroups", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - IsDeleted = table.Column(type: "tinyint(1)", nullable: false), - CreateTimeStamp = table.Column(type: "bigint", nullable: false), - UserId = table.Column(type: "bigint", nullable: false), - RoleId = table.Column(type: "bigint", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserRoleGroups", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - IsDeleted = table.Column(type: "tinyint(1)", nullable: false), - CreateTimeStamp = table.Column(type: "bigint", nullable: false), - NickName = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Phone = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Password = table.Column(type: "longtext", nullable: false) - .Annotation("MySql:CharSet", "utf8mb4") - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "RoleRouteGroups"); - - migrationBuilder.DropTable( - name: "Roles"); - - migrationBuilder.DropTable( - name: "Routes"); - - migrationBuilder.DropTable( - name: "UserRoleGroups"); - - migrationBuilder.DropTable( - name: "Users"); - } - } -} \ No newline at end of file diff --git a/LingTest/Migrations/20241223133026_v2.Designer.cs b/LingTest/Migrations/20241223133026_v2.Designer.cs deleted file mode 100644 index 6b3ba8d..0000000 --- a/LingTest/Migrations/20241223133026_v2.Designer.cs +++ /dev/null @@ -1,194 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(LingDbContext))] - [Migration("20241223133026_v2")] - partial class v2 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.RoleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Roles"); - }); - - modelBuilder.Entity("LingTest.Entitys.RoleRouteGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("RoleRouteGroups"); - }); - - modelBuilder.Entity("LingTest.Entitys.RouteEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("ContrillereFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Controller") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerSummary") - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Display") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplayFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplaySummary") - .HasColumnType("longtext"); - - b.Property("HttpMethod") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("ProjectName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RoutePrefix") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RouteTemplate") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Routes"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("NickName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Password") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Phone") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserRoleGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("UserRoleGroups"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20241223133026_v2.cs b/LingTest/Migrations/20241223133026_v2.cs deleted file mode 100644 index e1795c8..0000000 --- a/LingTest/Migrations/20241223133026_v2.cs +++ /dev/null @@ -1,74 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v2 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "DisplaySummary", - table: "Routes", - type: "longtext", - nullable: true, - oldClrType: typeof(string), - oldType: "longtext") - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.AlterColumn( - name: "ControllerSummary", - table: "Routes", - type: "longtext", - nullable: true, - oldClrType: typeof(string), - oldType: "longtext") - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.UpdateData( - table: "Routes", - keyColumn: "DisplaySummary", - keyValue: null, - column: "DisplaySummary", - value: ""); - - migrationBuilder.AlterColumn( - name: "DisplaySummary", - table: "Routes", - type: "longtext", - nullable: false, - oldClrType: typeof(string), - oldType: "longtext", - oldNullable: true) - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.UpdateData( - table: "Routes", - keyColumn: "ControllerSummary", - keyValue: null, - column: "ControllerSummary", - value: ""); - - migrationBuilder.AlterColumn( - name: "ControllerSummary", - table: "Routes", - type: "longtext", - nullable: false, - oldClrType: typeof(string), - oldType: "longtext", - oldNullable: true) - .Annotation("MySql:CharSet", "utf8mb4") - .OldAnnotation("MySql:CharSet", "utf8mb4"); - } - } -} \ No newline at end of file diff --git a/LingTest/Migrations/20241225083119_v3.Designer.cs b/LingTest/Migrations/20241225083119_v3.Designer.cs deleted file mode 100644 index 73b8e92..0000000 --- a/LingTest/Migrations/20241225083119_v3.Designer.cs +++ /dev/null @@ -1,194 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(LingDbContext))] - [Migration("20241225083119_v3")] - partial class v3 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.RoleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Roles"); - }); - - modelBuilder.Entity("LingTest.Entitys.RoleRouteGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("RoleRouteGroups"); - }); - - modelBuilder.Entity("LingTest.Entitys.RouteEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Controller") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerSummary") - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Display") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplayFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplaySummary") - .HasColumnType("longtext"); - - b.Property("HttpMethod") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("ProjectName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RoutePrefix") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RouteTemplate") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Routes"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("NickName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Password") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Phone") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserRoleGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("UserRoleGroups"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20241225083119_v3.cs b/LingTest/Migrations/20241225083119_v3.cs deleted file mode 100644 index 1a60aac..0000000 --- a/LingTest/Migrations/20241225083119_v3.cs +++ /dev/null @@ -1,28 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v3 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "ContrillereFullName", - table: "Routes", - newName: "ControllerFullName"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "ControllerFullName", - table: "Routes", - newName: "ContrillereFullName"); - } - } -} diff --git a/LingTest/Migrations/20241227064847_v4.Designer.cs b/LingTest/Migrations/20241227064847_v4.Designer.cs deleted file mode 100644 index d2d5ab4..0000000 --- a/LingTest/Migrations/20241227064847_v4.Designer.cs +++ /dev/null @@ -1,202 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(LingDbContext))] - [Migration("20241227064847_v4")] - partial class v4 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.RoleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Roles"); - }); - - modelBuilder.Entity("LingTest.Entitys.RoleRouteGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("RoleRouteGroups"); - }); - - modelBuilder.Entity("LingTest.Entitys.RouteEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Controller") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerSummary") - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Display") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplayFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplaySummary") - .HasColumnType("longtext"); - - b.Property("HttpMethod") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("ProjectName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RoutePrefix") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RouteTemplate") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Routes"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Avatar") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Email") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("NickName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Password") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Phone") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserRoleGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("UserRoleGroups"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20241227064847_v4.cs b/LingTest/Migrations/20241227064847_v4.cs deleted file mode 100644 index adb5514..0000000 --- a/LingTest/Migrations/20241227064847_v4.cs +++ /dev/null @@ -1,40 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v4 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "Avatar", - table: "Users", - type: "longtext", - nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"); - - migrationBuilder.AddColumn( - name: "Email", - table: "Users", - type: "longtext", - nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Avatar", - table: "Users"); - - migrationBuilder.DropColumn( - name: "Email", - table: "Users"); - } - } -} diff --git a/LingTest/Migrations/LingDbContextModelSnapshot.cs b/LingTest/Migrations/LingDbContextModelSnapshot.cs deleted file mode 100644 index 777fcb0..0000000 --- a/LingTest/Migrations/LingDbContextModelSnapshot.cs +++ /dev/null @@ -1,199 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(LingDbContext))] - partial class LingDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.RoleEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Description") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("Name") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Roles"); - }); - - modelBuilder.Entity("LingTest.Entitys.RoleRouteGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("RouteId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("RoleRouteGroups"); - }); - - modelBuilder.Entity("LingTest.Entitys.RouteEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Controller") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("ControllerSummary") - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Display") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplayFullName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("DisplaySummary") - .HasColumnType("longtext"); - - b.Property("HttpMethod") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("ProjectName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RoutePrefix") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("RouteTemplate") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Routes"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Avatar") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("Email") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("NickName") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Password") - .IsRequired() - .HasColumnType("longtext"); - - b.Property("Phone") - .IsRequired() - .HasColumnType("longtext"); - - b.HasKey("Id"); - - b.ToTable("Users"); - }); - - modelBuilder.Entity("LingTest.Entitys.UserRoleGroupEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.Property("RoleId") - .HasColumnType("bigint"); - - b.Property("UserId") - .HasColumnType("bigint"); - - b.HasKey("Id"); - - b.ToTable("UserRoleGroups"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index c65aed1..cdfa926 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -1,4 +1,7 @@ -锘縰sing LingYanAspCoreFramework.BaseRoots; +锘縰sing LingTest.DbContexts; +using LingYanAspCoreFramework.BaseRoots; +using LingYanAspCoreFramework.DbMultiTenants.Core; +using Microsoft.EntityFrameworkCore; namespace LingTest { @@ -6,14 +9,15 @@ namespace LingTest { public override void ARegisterModule(WebApplicationBuilder services) { + services.Services.AddMySqlPerTable(settings => + { + settings.ConnectionPrefix = "mysql_"; + }); } public override void BInitializationModule(WebApplication provider) { - //Task.Run(async () => - //{ - // await provider.Services.InitSeedData(); - //}).Wait(); + provider.UseMiddleware(); } } } \ No newline at end of file diff --git a/LingTest/appsettings.json b/LingTest/appsettings.json index ec04bc1..8a7dcee 100644 --- a/LingTest/appsettings.json +++ b/LingTest/appsettings.json @@ -5,5 +5,10 @@ "Microsoft.AspNetCore": "Warning" } }, + "ConnectionStrings": { + "mysql_container1": "server=192.168.148.131;port=3306;database=multi_tenant_container1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + "mysql_container2": "server=192.168.148.131;port=3306;database=multi_tenant_container2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + + }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs new file mode 100644 index 0000000..a566ff6 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs @@ -0,0 +1,14 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core +{ + public class Constants + { + internal static List specialConnectionTypes = new List + { + ConnectionResolverType.ByTable, + ConnectionResolverType.BySchema + }; + + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs new file mode 100644 index 0000000..f69ccfa --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs @@ -0,0 +1,282 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core.Impl; +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension +{ + /// + /// 租户核心扩展类,提供多租户数据库的注册方法 + /// + public static class TenantCoreExtension + { + /// + /// 按连接注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 数据库集成类型 + /// 键 + /// 连接前缀 + /// 数据库上下文选项操作 + /// 数据库上下文设置操作 + /// 服务集合 + public static IServiceCollection AddDbPerConnection(this IServiceCollection services, + DbIntegrationType dbType, string key = "default", + string connectionPrefix = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + var settings = new TenantSettings() + { + Key = key, + DbType = dbType, + ConnectionPrefix = connectionPrefix, + ConnectionType = ConnectionResolverType.ByDatabase, + DbContextOptionAction = optionAction, + DbContextSetup = dbContextSetup + }; + + return services.AddTenantedDatabase(settings); + } + + /// + /// 按连接注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 设置操作 + /// 服务集合 + public static IServiceCollection AddDbPerConnection(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + var settings = new TenantSettings() + { + ConnectionType = ConnectionResolverType.ByDatabase + }; + return services.AddTenantedDatabase(settings, setupAction); + } + + /// + /// 按表注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 数据库集成类型 + /// 键 + /// 连接名称 + /// 数据库上下文选项操作 + /// 数据库上下文设置操作 + /// 服务集合 + public static IServiceCollection AddDbPerTable(this IServiceCollection services, + DbIntegrationType dbType, string key = "default", + string connectionName = "tenantConnection", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + var settings = new TenantSettings() + { + Key = key, + DbType = dbType, + ConnectionName = connectionName, + ConnectionType = ConnectionResolverType.ByTable, + DbContextOptionAction = optionAction, + DbContextSetup = dbContextSetup + }; + + return services.AddTenantedDatabase(settings); + } + + /// + /// 按表注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 设置操作 + /// 服务集合 + public static IServiceCollection AddDbPerTable(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + var settings = new TenantSettings() + { + ConnectionType = ConnectionResolverType.ByTable + }; + return services.AddTenantedDatabase(settings, setupAction); + } + + /// + /// 按模式注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 数据库集成类型 + /// 键 + /// 连接名称 + /// 数据库上下文选项操作 + /// 数据库上下文设置操作 + /// 服务集合 + public static IServiceCollection AddDbPerSchema(this IServiceCollection services, + DbIntegrationType dbType, string key = "default", + string connectionName = "tenantConnection", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + var settings = new TenantSettings() + { + Key = key, + DbType = dbType, + ConnectionName = connectionName, + ConnectionType = ConnectionResolverType.BySchema, + DbContextOptionAction = optionAction, + DbContextSetup = dbContextSetup + }; + + return services.AddTenantedDatabase(settings); + } + + /// + /// 按模式注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 设置操作 + /// 服务集合 + public static IServiceCollection AddDbPerSchema(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + var settings = new TenantSettings() + { + ConnectionType = ConnectionResolverType.BySchema + }; + return services.AddTenantedDatabase(settings, setupAction); + } + + /// + /// 注册多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 租户设置 + /// 设置操作 + /// 服务集合 + public static IServiceCollection AddTenantedDatabase(this IServiceCollection services, + TenantSettings settings, Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + services.AddScoped(); + services.AddScoped(); + services.TryAddScoped(); + services.TryAddScoped, TenantConnectionResolver>(); + services.TryAddScoped, TenantEntityBuilder>(); + services.TryAddScoped, SimpleEntityScaner>(); + services.AddScoped(); + services.InitSettings(settings, setupAction); + + services.AddTenantDbContext(); + + + return services; + } + + /// + /// 注册租户数据库上下文 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 服务集合 + internal static IServiceCollection AddTenantDbContext(this IServiceCollection services) + where TDbContext : DbContext, ITenantDbContext + { + services.AddDbContext((serviceProvider, options) => + { + var settings = serviceProvider.GetService>(); + var connectionResolver = serviceProvider.GetService>(); + + var tenant = serviceProvider.GetService(); + settings.DbContextSetup?.Invoke(serviceProvider, connectionResolver.GetConnection(), options); + options.ReplaceServiceTenanted(settings); + settings.DbContextOptionAction?.Invoke(options); + + }); + return services; + } + + /// + /// 初始化租户设置 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 租户设置 + /// 设置操作 + /// 服务集合 + internal static IServiceCollection InitSettings(this IServiceCollection services, + TenantSettings settings, Action> setupAction) + where TDbContext : DbContext, ITenantDbContext + { + services.AddSingleton((sp) => + { + var rct = settings ?? new TenantSettings(); + setupAction?.Invoke(rct); + return rct; + }); + return services; + } + + /// + /// 替换服务为多租户服务 + /// + /// 数据库上下文类型 + /// 数据库上下文选项构建器 + /// 租户设置 + public static void ReplaceServiceTenanted(this DbContextOptionsBuilder dbOptions, + TenantSettings settings) + where TDbContext : DbContext, ITenantDbContext + { + if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + { + dbOptions.ReplaceService>(); + } + } + + /// + /// 配置租户构建器 + /// + /// 数据库上下文类型 + /// 关系型数据库上下文选项构建器类型 + /// 关系型选项扩展类型 + /// 关系型数据库上下文选项构建器 + /// 服务提供程序 + /// 租户设置 + /// 租户信息 + public static void TenantBuilderSetup(this RelationalDbContextOptionsBuilder builder, + IServiceProvider serviceProvider, TenantSettings settings, TenantInfo tenant) + where TDbContext : DbContext, ITenantDbContext + where TBuilder : RelationalDbContextOptionsBuilder + where TExtension : RelationalOptionsExtension, new() + { + if (settings.ConnectionType == ConnectionResolverType.ByTable) + { + builder.MigrationsHistoryTable($"{tenant.Name}__EFMigrationsHistory"); + } + if (settings.ConnectionType == ConnectionResolverType.BySchema) + { + builder.MigrationsHistoryTable("__EFMigrationHistory", $"{(settings.SchemaFunc?.Invoke(tenant) ?? tenant.Name)}"); + } + } + } + + +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs new file mode 100644 index 0000000..f07ffca --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs @@ -0,0 +1,43 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.Helpers; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl +{ + public class NamedConnectionGenerator : IConnectionGenerator + { + private readonly IConfiguration configuration; + public string TenantKey => "unknow"; + + public NamedConnectionGenerator(IConfiguration configuration) + { + this.configuration = configuration; + } + + public string GetConnection(TenantOption option, TenantInfo tenantInfo) + { + string connectionString = null; + switch (option.ConnectionType) + { + case ConnectionResolverType.ByDatabase: + connectionString = configuration.GetConnectionString($"{option.ConnectionPrefix}{tenantInfo.Name}"); + break; + case ConnectionResolverType.ByTable: + case ConnectionResolverType.BySchema: + connectionString = configuration.GetConnectionString(option.ConnectionName); + break; + } + LoggerHelper.DefaultLog(JsonConvert.SerializeObject(option)); + LoggerHelper.DefaultLog(JsonConvert.SerializeObject(tenantInfo)); + return connectionString; + } + + public bool MatchTenantKey(string tenantKey) + { + return false; + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs new file mode 100644 index 0000000..a1499b2 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs @@ -0,0 +1,32 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.AspNetCore.Http; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl +{ + public class SimpleHeaderTenantInfoGenerator : ITenantInfoGenerator + { + private readonly TenantInfo tenantInfo; + public SimpleHeaderTenantInfoGenerator(TenantInfo tenantInfo) + { + this.tenantInfo = tenantInfo; + } + + public TenantInfo GenerateTenant(object sender, HttpContext httpContext) + { + if (!this.tenantInfo.IsPresent) + { + var tenantName = httpContext?.Request?.Headers["TenantName"]; + + if (!string.IsNullOrEmpty(tenantName)) + { + this.tenantInfo.IsPresent = true; + this.tenantInfo.Name = tenantName; + this.tenantInfo.Generator = this; + } + } + + return this.tenantInfo; + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs new file mode 100644 index 0000000..5bed826 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs @@ -0,0 +1,69 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl +{ + public class TenantConnectionResolver : ITenantConnectionResolver + where TDbContext : DbContext + { + private readonly TenantSettings setting; + private readonly TenantInfo tenantInfo; + private readonly IServiceProvider serviceProvider; + private readonly TenantOption tenantOption; + + public TenantConnectionResolver(TenantSettings setting, TenantInfo tenantInfo, + TenantOption tenantOption, IServiceProvider serviceProvider) + { + this.tenantOption = tenantOption; + this.setting = setting; + this.tenantInfo = tenantInfo; + this.serviceProvider = serviceProvider; + } + + public string GetConnection() + { + var connectionGenerator = this.GetConnectionGenerator(); + var option = GenerateOption(); + + return connectionGenerator.GetConnection(option, tenantInfo); + + } + + IConnectionGenerator GetConnectionGenerator() + { + LoggerHelper.DefaultLog("优先使用设置中的 ConnectionGenerator 委托"); + if (this.setting.ConnectionGenerator != null) + { + return this.setting.ConnectionGenerator.Invoke(); + } + LoggerHelper.DefaultLog("从服务提供程序中获取所有 IConnectionGenerator 实现"); + var connectionGenerators = serviceProvider.GetServices(); + LoggerHelper.DefaultLog("查找匹配的 IConnectionGenerator 实现"); + var matchedGenerator = connectionGenerators + .FirstOrDefault(r => r.TenantKey == this.setting.Key || r.MatchTenantKey(this.setting.Key)); + if (matchedGenerator != null) + { + return matchedGenerator; + } + LoggerHelper.DefaultLog("如果没有匹配的实现,使用 NamedConnectionGenerator"); + var defaultConnectionGenerator= serviceProvider.GetService(); + return defaultConnectionGenerator; + } + + TenantOption GenerateOption() + { + tenantOption.Key = setting.Key; + tenantOption.ConnectionType = setting.ConnectionType; + tenantOption.DbType = setting.DbType; + tenantOption.TableNameFunc = setting.TableNameFunc; + tenantOption.SchemaFunc = setting.SchemaFunc; + tenantOption.ConnectionPrefix = setting.ConnectionPrefix; + tenantOption.ConnectionName = setting.ConnectionName; + + return this.tenantOption; + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs new file mode 100644 index 0000000..d1f92bf --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs @@ -0,0 +1,13 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface +{ + public interface IConnectionGenerator + { + string TenantKey { get;} + + bool MatchTenantKey(string tenantKey); + + string GetConnection(TenantOption option, TenantInfo tenantInfo); + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs new file mode 100644 index 0000000..fa93481 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs @@ -0,0 +1,7 @@ +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface +{ + public interface ITenantConnectionResolver + { + string GetConnection(); + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs new file mode 100644 index 0000000..528be52 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs @@ -0,0 +1,11 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; +using Microsoft.AspNetCore.Http; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface +{ + public interface ITenantInfoGenerator + { + TenantInfo GenerateTenant(object sender, HttpContext httpContext); + } + +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs new file mode 100644 index 0000000..08dc8c1 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs @@ -0,0 +1,22 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using Microsoft.AspNetCore.Http; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core +{ + public class TenantInfoMiddleware + { + private readonly RequestDelegate _next; + + public TenantInfoMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context, ITenantInfoGenerator tenantInfoGenerator) + { + tenantInfoGenerator.GenerateTenant(this, context); + + await _next(context); + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs new file mode 100644 index 0000000..7e2874e --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs @@ -0,0 +1,63 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core +{ + /// + /// 表示租户设置的抽象类 + /// + public abstract class TenantSettings + { + /// + /// 租户设置的键 + /// + public string Key { get; set; } + + /// + /// 连接解析器类型 + /// + public ConnectionResolverType ConnectionType { get; set; } + + /// + /// 数据库集成类型 + /// + public DbIntegrationType DbType { get; set; } + + /// + /// 连接生成器函数 + /// + public Func ConnectionGenerator { get; set; } + + /// + /// 连接名称 + /// + public string ConnectionName { get; set; } + + /// + /// 连接前缀 + /// + public string ConnectionPrefix { get; set; } + + /// + /// 数据库上下文选项操作 + /// + public Action DbContextOptionAction { get; set; } + + /// + /// 表名生成函数 + /// + public Func TableNameFunc { get; set; } + + /// + /// 模式生成函数 + /// + public Func SchemaFunc { get; set; } + + /// + /// 数据库上下文设置操作 + /// + public Action DbContextSetup { get; set; } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs new file mode 100644 index 0000000..8163cf5 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs @@ -0,0 +1,65 @@ +锘縰sing LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DbMultiTenants.Core +{ + /// + /// 琛ㄧず鐗瑰畾鏁版嵁搴撲笂涓嬫枃绫诲瀷鐨勭鎴疯缃殑绫 + /// + /// 鏁版嵁搴撲笂涓嬫枃绫诲瀷 + public class TenantSettings + where TDbContext : DbContext + { + /// + /// 绉熸埛璁剧疆鐨勯敭 + /// + public string Key { get; set; } + + /// + /// 杩炴帴瑙f瀽鍣ㄧ被鍨 + /// + public ConnectionResolverType ConnectionType { get; set; } + + /// + /// 鏁版嵁搴撻泦鎴愮被鍨 + /// + public DbIntegrationType DbType { get; set; } + + /// + /// 杩炴帴鐢熸垚鍣ㄥ嚱鏁 + /// + public Func ConnectionGenerator { get; set; } + + /// + /// 杩炴帴鍚嶇О + /// + public string ConnectionName { get; set; } + + /// + /// 杩炴帴鍓嶇紑 + /// + public string ConnectionPrefix { get; set; } + + /// + /// 鏁版嵁搴撲笂涓嬫枃閫夐」鎿嶄綔 + /// + public Action DbContextOptionAction { get; set; } + + /// + /// 琛ㄥ悕鐢熸垚鍑芥暟 + /// + public Func TableNameFunc { get; set; } + + /// + /// 妯″紡鐢熸垚鍑芥暟 + /// + public Func SchemaFunc { get; set; } + + /// + /// 鏁版嵁搴撲笂涓嬫枃璁剧疆鎿嶄綔 + /// + public Action DbContextSetup { get; set; } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs index 49e8bf0..e7a0d64 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs @@ -1,8 +1,9 @@ +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; using System.Reflection; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl { /// /// 简单实体扫描器类,用于扫描数据库上下文中的实体类型 @@ -60,9 +61,4 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess return false; } } -} - - - - - +} \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs index fdddc65..f99667f 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs @@ -1,7 +1,8 @@ +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl { /// /// 表示租户基础数据库上下文的抽象类 @@ -41,10 +42,4 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess entityBuilder.UpdateEntities(modelBuilder); } } -} - - - - - - +} \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs index 309e6aa..382ca2d 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs @@ -1,7 +1,9 @@ +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl { /// /// 表示租户实体构建器的类 diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs index c75bda3..03c5aea 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs @@ -1,11 +1,11 @@ using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface { public interface IEntityScaner where TDbContext : DbContext - { + { IList ScanEntityTypes(); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs index be620a3..18fd30f 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs @@ -1,6 +1,6 @@ using LingYanAspCoreFramework.DbMultiTenants.Models; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface { public interface ITenantDbContext { diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs index fc7f1a9..a0a0b93 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface { /// /// 表示租户实体构建器的接口 diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs index 6a4e2e7..34f48bc 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs @@ -1,3 +1,4 @@ +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs index cbb23fa..108cb7b 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs @@ -1,3 +1,4 @@ +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs new file mode 100644 index 0000000..ed59c00 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs @@ -0,0 +1,123 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core; +using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// MySQL 租户扩展类,提供多租户数据库的注册方法 + /// + public static class MySqlTenantExtension + { + /// + /// 按连接注册 MySQL 多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 键 + /// 连接前缀 + /// 数据库上下文选项操作 + /// 数据库上下文设置操作 + /// 服务集合 + public static IServiceCollection AddMySqlPerConnection(this IServiceCollection services, + string key = "default", + string connectionPrefix = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerConnection(DbIntegrationType.Mysql, key, connectionPrefix, + optionAction, dbContextSetup ?? SetUpMySql); + } + + /// + /// 按连接注册 MySQL 多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 设置操作 + /// 服务集合 + public static IServiceCollection AddMySqlPerConnection(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerConnection(CombineSettings(setupAction)); + } + + /// + /// 按表注册 MySQL 多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 键 + /// 连接名称 + /// 数据库上下文选项操作 + /// 数据库上下文设置操作 + /// 服务集合 + public static IServiceCollection AddMySqlPerTable(this IServiceCollection services, + string key = "default", + string connectionName = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerTable(DbIntegrationType.Mysql, key, connectionName, + optionAction, dbContextSetup ?? SetUpMySql); + } + + /// + /// 按表注册 MySQL 多租户数据库 + /// + /// 数据库上下文类型 + /// 服务集合 + /// 设置操作 + /// 服务集合 + public static IServiceCollection AddMySqlPerTable(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerTable(CombineSettings(setupAction)); + } + + /// + /// 组合设置操作 + /// + /// 数据库上下文类型 + /// 设置操作 + /// 组合后的设置操作 + static Action> CombineSettings( + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return (settings) => + { + settings.DbContextSetup = SetUpMySql; + setupAction?.Invoke(settings); + }; + } + + /// + /// 设置 MySQL 数据库上下文 + /// + /// 数据库上下文类型 + /// 服务提供程序 + /// 连接字符串 + /// 数据库上下文选项构建器 + internal static void SetUpMySql(IServiceProvider serviceProvider, string connectionString, + DbContextOptionsBuilder optionsBuilder) + where TDbContext : DbContext, ITenantDbContext + { + var settings = serviceProvider.GetService>(); + var tenant = serviceProvider.GetService(); + optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => + { + builder.TenantBuilderSetup(serviceProvider, settings, tenant); + }); + } + } + +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs new file mode 100644 index 0000000..8f48692 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs @@ -0,0 +1,90 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core; +using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore +{ + public static class PostgreTenantExtension + { + public static IServiceCollection AddPostgrePerConnection(this IServiceCollection services, + string key = "default", + string connectionPrefix = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerConnection(DbIntegrationType.Postgre, key, connectionPrefix, + optionAction, dbContextSetup ?? SetUpPostgre); + } + + public static IServiceCollection AddPostgrePerConnection(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerConnection(CombineSettings(setupAction)); + } + + public static IServiceCollection AddPostgrePerTable(this IServiceCollection services, + string key = "default", + string connectionName = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerTable(DbIntegrationType.Postgre, key, connectionName, + optionAction, dbContextSetup ?? SetUpPostgre); + } + + public static IServiceCollection AddPostgrePerTable(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerTable(CombineSettings(setupAction)); + } + + public static IServiceCollection AddPostgrePerSchema(this IServiceCollection services, + string key = "default", + string connectionName = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerSchema(DbIntegrationType.Postgre, key, connectionName, + optionAction, dbContextSetup ?? SetUpPostgre); + } + + public static IServiceCollection AddPostgrePerSchema(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerSchema(CombineSettings(setupAction)); + } + + + static Action> CombineSettings( + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return (settings) => + { + settings.DbContextSetup = SetUpPostgre; + setupAction?.Invoke(settings); + }; + } + + internal static void SetUpPostgre(IServiceProvider serviceProvider, string connectionString, + DbContextOptionsBuilder optionsBuilder) + where TDbContext : DbContext, ITenantDbContext + { + var settings = serviceProvider.GetService>(); + var tenant = serviceProvider.GetService(); + optionsBuilder.UseNpgsql(connectionString, builder => + { + builder.TenantBuilderSetup(serviceProvider, settings, tenant); + }); + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs new file mode 100644 index 0000000..76ba342 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs @@ -0,0 +1,91 @@ +using LingYanAspCoreFramework.DbMultiTenants.Core; +using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore +{ + public static class SqlServerTenantExtension + { + public static IServiceCollection AddSqlServerPerConnection(this IServiceCollection services, + string key = "default", + string connectionPrefix = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerConnection(DbIntegrationType.SqlServer, key, connectionPrefix, + optionAction, dbContextSetup ?? SetUpSqlServer); + } + + public static IServiceCollection AddSqlServerPerConnection(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerConnection(CombineSettings(setupAction)); + } + + public static IServiceCollection AddSqlServerPerTable(this IServiceCollection services, + string key = "default", + string connectionName = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerTable(DbIntegrationType.SqlServer, key, connectionName, + optionAction, dbContextSetup ?? SetUpSqlServer); + } + + public static IServiceCollection AddSqlServerPerTable(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerTable(CombineSettings(setupAction)); + } + + public static IServiceCollection AddSqlServerPerSchema(this IServiceCollection services, + string key = "default", + string connectionName = "tenanted", + Action optionAction = null, + Action dbContextSetup = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerSchema(DbIntegrationType.SqlServer, key, connectionName, + optionAction, dbContextSetup ?? SetUpSqlServer); + } + + public static IServiceCollection AddSqlServerPerSchema(this IServiceCollection services, + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return services.AddDbPerSchema(CombineSettings(setupAction)); + } + + static Action> CombineSettings( + Action> setupAction = null) + where TDbContext : DbContext, ITenantDbContext + { + return (settings) => + { + settings.DbContextSetup = SetUpSqlServer; + setupAction?.Invoke(settings); + }; + } + + internal static void SetUpSqlServer(IServiceProvider serviceProvider, string connectionString, + DbContextOptionsBuilder optionsBuilder) + where TDbContext : DbContext, ITenantDbContext + { + var settings = serviceProvider.GetService>(); + var tenant = serviceProvider.GetService(); + optionsBuilder.UseSqlServer(connectionString, builder => + { + builder.TenantBuilderSetup(serviceProvider, settings, tenant); + }); + } + + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs similarity index 89% rename from LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs rename to LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs index 5811c8c..16e63ba 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/ConnectionResolverType.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs @@ -1,4 +1,4 @@ -namespace LingYanAspCoreFramework.DbMultiTenants.Models +namespace LingYanAspCoreFramework.DbMultiTenants.Models.Enum { /// /// 表示连接解析器类型的枚举 diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs similarity index 68% rename from LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs rename to LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs index 4a24d54..7824119 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/DbIntegrationType.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs @@ -1,4 +1,4 @@ -namespace LingYanAspCoreFramework.DbMultiTenants.Models +namespace LingYanAspCoreFramework.DbMultiTenants.Models.Enum { public enum DbIntegrationType { diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs index 78f81a5..fc24a43 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs @@ -1,3 +1,5 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; + namespace LingYanAspCoreFramework.DbMultiTenants.Models { /// diff --git a/LingYanAspCoreFramework/Extensions/IocExtension.cs b/LingYanAspCoreFramework/Extensions/IocExtension.cs index 1d228f0..ec8e4d5 100644 --- a/LingYanAspCoreFramework/Extensions/IocExtension.cs +++ b/LingYanAspCoreFramework/Extensions/IocExtension.cs @@ -41,7 +41,7 @@ namespace LingYanAspCoreFramework.Extensions foreach (var module in ModuleBaseLoadingList) { //妯″潡鏁翠綋娉ㄥ唽 - LoggerHelper.WarnrningLog($"銆恵module.GetType().Name}妯″潡鍒濆鍖栥"); + LoggerHelper.WarnrningLog($"{module.GetType().Name}妯″潡鍒濆鍖"); //鏂规硶 var method = module.GetType().GetMethod("BInitializationModule"); //鍙傛暟 @@ -58,24 +58,24 @@ namespace LingYanAspCoreFramework.Extensions try { - LoggerHelper.DefaultLog("銆愰厤缃垵濮嬪寲銆..."); + LoggerHelper.DefaultLog("閰嶇疆鍒濆鍖"); LingYanRuntimeManager.Init(); - LoggerHelper.DefaultLog("銆愰厤缃郴缁焌ppsettings.json銆..."); + LoggerHelper.DefaultLog("閰嶇疆绯荤粺appsettings.json"); SampleHelper.SysAppsettings = Hostbuilder.GetPropertyValue("Configuration"); - LoggerHelper.DefaultLog("銆愬綊绾抽」鐩ā鍧椼..."); + LoggerHelper.DefaultLog("褰掔撼椤圭洰妯″潡"); LingYanRuntimeManager.RuntimeCacheModel.GetModules(); - LoggerHelper.DefaultLog("銆愯幏鍙栧鍣ㄣ..."); + LoggerHelper.DefaultLog("鑾峰彇瀹瑰櫒"); var BuilderService = Hostbuilder.GetBuilderServiceCollection(); - LoggerHelper.DefaultLog("銆愭坊鍔犺法鍩熺瓥鐣ャ..."); + LoggerHelper.DefaultLog("娣诲姞璺ㄥ煙绛栫暐"); BuilderService.AddCors(c => c.AddPolicy(SampleHelper.CrossPolicy, p => p.WithOrigins(LingYanRuntimeManager.CrossDomains).AllowAnyHeader().AllowAnyMethod().AllowCredentials())); - LoggerHelper.DefaultLog("銆愭坊鍔燗PI绔偣銆..."); + LoggerHelper.DefaultLog("娣诲姞API绔偣"); BuilderService.AddEndpointsApiExplorer(); - LoggerHelper.DefaultLog("銆愭坊鍔燬ignalR銆..."); + LoggerHelper.DefaultLog("娣诲姞SignalR"); BuilderService.AddSignalR(); - LoggerHelper.DefaultLog("銆愭坊鍔犳湰鍦癕emoryCache缂撳瓨銆..."); + LoggerHelper.DefaultLog("娣诲姞鏈湴MemoryCache缂撳瓨"); BuilderService.AddSingleton(); - LoggerHelper.DefaultLog("銆愭帶鍒跺櫒娉ㄥ唽鍏ㄥ眬杩囨护鍣+娉ㄥ唽id-JSON杞崲鍣╨ong<==>string銆..."); + LoggerHelper.DefaultLog("鎺у埗鍣ㄦ敞鍐屽叏灞杩囨护鍣+娉ㄥ唽id-JSON杞崲鍣╨ong<==>string"); BuilderService.AddControllers(mvcOption => { LingYanRuntimeManager.RuntimeCacheModel.ModuleFiler.ForEach(filer => @@ -84,7 +84,7 @@ namespace LingYanAspCoreFramework.Extensions }); }).AddNewtonsoftJson(options => { options.SerializerSettings.Converters.Add(new LongToStringConverter()); }); - LoggerHelper.DefaultLog("銆愭坊鍔犲畨鍏ㄥ畾涔夈..."); + LoggerHelper.DefaultLog("娣诲姞瀹夊叏瀹氫箟"); BuilderService.AddSwaggerGen(swaggerGenOption => { //鎺ュ彛鏂囨。鍒嗙粍 @@ -92,7 +92,7 @@ namespace LingYanAspCoreFramework.Extensions { Title = ProjectHelper.GetProjectName(), Version = "v1", - Description = $"椤圭洰:銆恵ProjectHelper.GetProjectName()}銆戯紝寮鍙戣咃細銆愮伒鐏点", + Description = $"椤圭洰:{ProjectHelper.GetProjectName()}锛屽紑鍙戣咃細鐏电伒", Contact = new OpenApiContact { Name = "ling", @@ -146,51 +146,51 @@ namespace LingYanAspCoreFramework.Extensions } }); }); - LoggerHelper.DefaultLog("銆愭敞鍐屾巿鏉冨鐞嗗櫒銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽鎺堟潈澶勭悊鍣"); BuilderService.RegisterAuthorzeHandler(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐孞WT閴存潈-鎺堟潈瀵瑰簲绛栫暐銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽JWT閴存潈-鎺堟潈瀵瑰簲绛栫暐"); BuilderService.RegisterAuthenticationAndAuthorization(SampleHelper.BearerScheme, SampleHelper.EmpowerPolicy, LingYanRuntimeManager.JwtModel, () => new BaseAuthorizationRequirement(BuilderService.BuildServiceProvider().CreateScope())); - LoggerHelper.DefaultLog("銆怘ttpContext涓婁笅鏂囩殑璁块棶銆..."); + LoggerHelper.DefaultLog("HttpContext涓婁笅鏂囩殑璁块棶"); BuilderService.AddHttpContextAccessor(); - LoggerHelper.DefaultLog("銆愬鍣ㄦ敞鍐屽畼鏂归氱敤鏈嶅姟銆..."); + LoggerHelper.DefaultLog("瀹瑰櫒娉ㄥ唽瀹樻柟閫氱敤鏈嶅姟"); if (Hostbuilder is WebApplicationBuilder web) { - LoggerHelper.DefaultLog("銆愭棩蹇楁竻闄よ嚜甯﹀苟娣诲姞Nlog鏃ュ織閰嶇疆銆..."); + LoggerHelper.DefaultLog("鏃ュ織娓呴櫎鑷甫骞舵坊鍔燦log鏃ュ織閰嶇疆"); web.Logging.ClearProviders(); web.Logging.AddNLog(LingYanRuntimeManager.CommonConfigModel.NlogConfig); } - LoggerHelper.DefaultLog("銆愭敞鍐屼粨鍌ㄤ笌宸ヤ綔鍗曞厓銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽浠撳偍涓庡伐浣滃崟鍏"); BuilderService.RegisterRepository(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐岄鍩烳anager銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽棰嗗煙Manager"); BuilderService.RegisterManager(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐屾湇鍔Service銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽鏈嶅姟TService"); BuilderService.RegisterTService(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐屽疄渚嬨..."); + LoggerHelper.DefaultLog("娉ㄥ唽瀹炰緥"); BuilderService.RegisterTInstance(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐屾暟鎹簱涓婁笅鏂囥..."); + LoggerHelper.DefaultLog("娉ㄥ唽鏁版嵁搴撲笂涓嬫枃"); BuilderService.RegisterDbContext(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐孯edis銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽Redis"); BuilderService.RegisterRedis(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐屽姩鎬佽矾鐢便..."); + LoggerHelper.DefaultLog("娉ㄥ唽鍔ㄦ佽矾鐢"); BuilderService.RegisterDynamicWebApi(); - LoggerHelper.DefaultLog("銆愭敞鍐屽绉熸埛鏈嶅姟銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽澶氱鎴锋湇鍔"); BuilderService.RegisterTenantService(); - LoggerHelper.DefaultLog("銆愭敞鍐屽绉熸埛妯$増鏁版嵁涓婁笅鏂囥..."); + LoggerHelper.DefaultLog("娉ㄥ唽澶氱鎴锋ā鐗堟暟鎹笂涓嬫枃"); BuilderService.RegisterTenantTemplateDbContext(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.DefaultLog("銆愭敞鍐孒ttpClient銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽HttpClient"); BuilderService.AddHttpClient(); - LoggerHelper.DefaultLog("銆愭敞鍐孡ingYanHttpClient銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽LingYanHttpClient"); BuilderService.AddSingleton(); - LoggerHelper.DefaultLog("銆愭敞鍐屾敮浠樺疂鍖呫..."); + LoggerHelper.DefaultLog("娉ㄥ唽鏀粯瀹濆寘"); BuilderService.AddAlipay(); - LoggerHelper.DefaultLog("銆愭敞鍐屽井淇℃敮浠樺寘銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽寰俊鏀粯鍖"); BuilderService.AddWeChatPay(); - LoggerHelper.DefaultLog("銆愰厤缃枃浠舵墿灞曠被鍨嬨..."); + LoggerHelper.DefaultLog("閰嶇疆鏂囦欢鎵╁睍绫诲瀷"); BuilderService.Configure(option => { foreach (var item in LingYanRuntimeManager.FileContentTypeConfig) @@ -198,7 +198,7 @@ namespace LingYanAspCoreFramework.Extensions option.Mappings[item.Key] = item.Value; } }); - LoggerHelper.DefaultLog("銆愰厤缃ā鍨嬪弬鏁伴獙璇併..."); + LoggerHelper.DefaultLog("閰嶇疆妯″瀷鍙傛暟楠岃瘉"); BuilderService.Configure(options => { options.InvalidModelStateResponseFactory = actionContext => @@ -212,31 +212,31 @@ namespace LingYanAspCoreFramework.Extensions throw new CommonException(new ResponceBody(40004, errorMessage)); }; }); - LoggerHelper.DefaultLog("銆愰厤缃〃鍗曘..."); + LoggerHelper.DefaultLog("閰嶇疆琛ㄥ崟"); BuilderService.ConfigureFormSize(); - LoggerHelper.DefaultLog("銆愰厤缃敮浠樺疂銆..."); + LoggerHelper.DefaultLog("閰嶇疆鏀粯瀹"); BuilderService.Configure(SampleHelper.LingAppsettings.GetSection("Alipay")); - LoggerHelper.DefaultLog("銆愰厤缃井淇℃敮浠樸..."); + LoggerHelper.DefaultLog("閰嶇疆寰俊鏀粯"); BuilderService.Configure(SampleHelper.LingAppsettings.GetSection("WeChatPay")); - LoggerHelper.DefaultLog("銆愰厤缃井淇″紑鏀惧钩鍙般..."); + LoggerHelper.DefaultLog("閰嶇疆寰俊寮鏀惧钩鍙"); BuilderService.Configure(SampleHelper.LingAppsettings.GetSection("WeChatDevOption")); - LoggerHelper.DefaultLog("銆愭敞鍐屽垎椤垫暟缁勩..."); + LoggerHelper.DefaultLog("娉ㄥ唽鍒嗛〉鏁扮粍"); BuilderService.AddTransient(typeof(IPagedList<>), typeof(PagedList<>)); - LoggerHelper.DefaultLog("銆愭敞鍐屾湰鍦颁簨浠舵荤嚎銆..."); + LoggerHelper.DefaultLog("娉ㄥ唽鏈湴浜嬩欢鎬荤嚎"); BuilderService.AddSingleton(); - LoggerHelper.DefaultLog("銆愭敞鍐屾敮浠樻柟娉曘..."); + LoggerHelper.DefaultLog("娉ㄥ唽鏀粯鏂规硶"); BuilderService.AddSingleton(); - LoggerHelper.DefaultLog("銆愭敞鍐屽井淇″紑鏀惧钩鍙版湇鍔°..."); + LoggerHelper.DefaultLog("娉ㄥ唽寰俊寮鏀惧钩鍙版湇鍔"); BuilderService.AddSingleton(); - LoggerHelper.DefaultLog("銆愰厤缃叏灞杞崲JsonSerializerSettings銆..."); + LoggerHelper.DefaultLog("閰嶇疆鍏ㄥ眬杞崲JsonSerializerSettings"); JsonSerializerSettings globalSettings = new JsonSerializerSettings { Converters = new List { new LongToStringConverter() }, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; - LoggerHelper.DefaultLog("銆愭敞鍐岄」鐩ā鍧椼..."); + LoggerHelper.DefaultLog("娉ㄥ唽椤圭洰妯″潡"); Hostbuilder.RegisterModule(LingYanRuntimeManager.RuntimeCacheModel); } catch (Exception ex) @@ -254,16 +254,16 @@ namespace LingYanAspCoreFramework.Extensions { var hostService = app.GetBuilderServiceProvider(); var contentTypeProvider = hostService.GetService>()?.Value; - LoggerHelper.WarnrningLog($"銆愬叏灞鍒濆鍖栧紑濮嬨..."); + LoggerHelper.WarnrningLog($"鍏ㄥ眬鍒濆鍖栧紑濮"); if (app is WebApplication web) { - LoggerHelper.WarnrningLog($"銆愬惎鐢⊿wagger涓棿浠跺拰鍙鍖栫晫闈€..."); + LoggerHelper.WarnrningLog($"鍚敤Swagger涓棿浠跺拰鍙鍖栫晫闈"); if (web.Environment.IsDevelopment() || LingYanRuntimeManager.ShowSwaggerUI) { web.UseSwagger(); web.UseSwaggerUI(); } - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄥ紑鍙戠幆澧冨紓甯搁〉闈€..."); + LoggerHelper.WarnrningLog($"鍚敤寮鍙戠幆澧冨紓甯搁〉闈"); if (web.Environment.IsDevelopment()) { web.UseDeveloperExceptionPage(); @@ -273,32 +273,32 @@ namespace LingYanAspCoreFramework.Extensions web.UseExceptionHandler("/Error"); web.UseHsts(); } - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄥ紓甯哥粺涓澶勭悊涓棿浠躲..."); + LoggerHelper.WarnrningLog($"鍚敤寮傚父缁熶竴澶勭悊涓棿浠"); web.UseMiddleware(); - LoggerHelper.WarnrningLog($"銆愬惎鐢℉TTPS閲嶅畾鍚戜腑闂翠欢銆..."); + LoggerHelper.WarnrningLog($"鍚敤HTTPS閲嶅畾鍚戜腑闂翠欢"); web.UseHttpsRedirection(); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄨ矾鐢变腑闂翠欢銆..."); + LoggerHelper.WarnrningLog($"鍚敤璺敱涓棿浠"); web.UseRouting(); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄨ法鍩熺瓥鐣ャ..."); + LoggerHelper.WarnrningLog($"鍚敤璺ㄥ煙绛栫暐"); web.UseCors(SampleHelper.CrossPolicy); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄩ壌鏉冧腑闂翠欢銆..."); + LoggerHelper.WarnrningLog($"鍚敤閴存潈涓棿浠"); web.UseAuthentication(); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄦ巿鏉冧腑闂翠欢銆..."); + LoggerHelper.WarnrningLog($"鍚敤鎺堟潈涓棿浠"); web.UseAuthorization(); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄩ」鐩嚜瀹氫箟鐨勪腑闂翠欢銆..."); + LoggerHelper.WarnrningLog($"鍚敤椤圭洰鑷畾涔夌殑涓棿浠"); foreach (var item in LingYanRuntimeManager.RuntimeCacheModel.ModuleTMiddleware) { web.UseMiddleware(item); } - LoggerHelper.WarnrningLog($"銆愰」鐩悇妯″潡鍒濆鍖栥..."); + LoggerHelper.WarnrningLog($"椤圭洰鍚勬ā鍧楀垵濮嬪寲"); LingYanRuntimeManager.RuntimeCacheModel.ModuleAssemblyBaseLoadingKeyValue.Values.InitModules(app); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄥ绉熸埛涓棿浠躲..."); + LoggerHelper.WarnrningLog($"鍚敤澶氱鎴蜂腑闂翠欢"); web.UseMiddleware(); - LoggerHelper.WarnrningLog($"銆愬绉熸埛閰嶇疆鍒濆鍖栥..."); + LoggerHelper.WarnrningLog($"澶氱鎴烽厤缃垵濮嬪寲"); hostService.InitTenant(LingYanRuntimeManager.RuntimeCacheModel); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄥ叏灞寮鍚韩浠介獙璇併..."); + LoggerHelper.WarnrningLog($"鍚敤鍏ㄥ眬寮鍚韩浠介獙璇"); web.MapControllers().RequireAuthorization(); - LoggerHelper.WarnrningLog($"銆愬惎鐢ㄩ潤鎬佹枃浠跺す-鑷畾涔変腑闂翠欢浠ョ伒娲荤殑瀹炵幇閴存潈鎺堟潈銆..."); + LoggerHelper.WarnrningLog($"鍚敤闈欐佹枃浠跺す-鑷畾涔変腑闂翠欢浠ョ伒娲荤殑瀹炵幇閴存潈鎺堟潈"); web.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = contentTypeProvider, @@ -311,7 +311,7 @@ namespace LingYanAspCoreFramework.Extensions FileProvider = new PhysicalFileProvider(SampleHelper.BaseDir.GetLocalPath(SampleHelper.UnAuthorDir)), RequestPath = $"/{LingYanRuntimeManager.CommonConfigModel.UnAuthorDir}" }); - LoggerHelper.WarnrningLog($"銆愰厤缃湇鍔″惎鍔ㄧ洃鍚鍙c..."); + LoggerHelper.WarnrningLog($"閰嶇疆鏈嶅姟鍚姩鐩戝惉绔彛"); foreach (var port in LingYanRuntimeManager.ListeningPorts) { web.Urls.Add(port); diff --git a/LingYanAspCoreFramework/Helpers/LoggerHelper.cs b/LingYanAspCoreFramework/Helpers/LoggerHelper.cs index 1854c96..1f2e2e9 100644 --- a/LingYanAspCoreFramework/Helpers/LoggerHelper.cs +++ b/LingYanAspCoreFramework/Helpers/LoggerHelper.cs @@ -26,6 +26,7 @@ namespace LingYanAspCoreFramework.Helpers { Console.ForegroundColor = ConsoleColor.DarkRed; Console.WriteLine(str); + Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine("========================================================="); Console.ResetColor(); } @@ -38,6 +39,7 @@ namespace LingYanAspCoreFramework.Helpers { Console.ForegroundColor = ConsoleColor.DarkYellow; Console.WriteLine(str); + Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine("========================================================="); Console.ResetColor(); } @@ -48,8 +50,9 @@ namespace LingYanAspCoreFramework.Helpers /// public static void DefaultLog(string str) { - Console.ForegroundColor = ConsoleColor.DarkBlue; + Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(str); + Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine("========================================================="); Console.ResetColor(); } diff --git a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj index 4518a53..64e48ed 100644 --- a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj +++ b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj @@ -35,6 +35,7 @@ + -- Gitee From 0c841bc3e4214fb01df6c7185a7371949dec24ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Wed, 26 Mar 2025 18:12:37 +0800 Subject: [PATCH 03/17] =?UTF-8?q?=E5=88=86=E5=BA=93=E5=88=86=E8=A1=A8?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LingTest/DbContexts/StoreDbContext.cs | 2 +- .../Generator/CombindedConnectionGenerator.cs | 11 +- LingTest/RestApiModule.cs | 2 +- LingTest/appsettings.json | 6 +- .../Core/Extension/TenantCoreExtension.cs | 10 +- ...ionGenerator.cs => ConnectionGenerator.cs} | 12 +- .../Core/Impl/TenantConnectionResolver.cs | 12 +- ...nfoGenerator.cs => TenantInfoGenerator.cs} | 4 +- .../Core/TenantInfoMiddleware.cs | 4 +- ...{SimpleEntityScaner.cs => EntityScaner.cs} | 2 +- ...antBaseDbContext.cs => TenantDbContext.cs} | 4 +- .../LingYanAspCoreFramework.csproj | 3 + .../SampleRoots/SampleQrCodeHelper.cs | 190 ++++++++++++++++++ 13 files changed, 224 insertions(+), 38 deletions(-) rename LingYanAspCoreFramework/DbMultiTenants/Core/Impl/{NamedConnectionGenerator.cs => ConnectionGenerator.cs} (76%) rename LingYanAspCoreFramework/DbMultiTenants/Core/Impl/{SimpleHeaderTenantInfoGenerator.cs => TenantInfoGenerator.cs} (86%) rename LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/{SimpleEntityScaner.cs => EntityScaner.cs} (96%) rename LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/{TenantBaseDbContext.cs => TenantDbContext.cs} (88%) create mode 100644 LingYanAspCoreFramework/SampleRoots/SampleQrCodeHelper.cs diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index 610ff81..c993290 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Caching.Memory; namespace LingTest.DbContexts { - public class StoreDbContext : TenantBaseDbContext + public class StoreDbContext : TenantDbContext { public DbSet Products => this.Set(); diff --git a/LingTest/Generator/CombindedConnectionGenerator.cs b/LingTest/Generator/CombindedConnectionGenerator.cs index 3e76d9c..6baa399 100644 --- a/LingTest/Generator/CombindedConnectionGenerator.cs +++ b/LingTest/Generator/CombindedConnectionGenerator.cs @@ -1,11 +1,9 @@ -锘縰sing LingYanAspCoreFramework.Attributes; -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +锘縰sing LingYanAspCoreFramework.DbMultiTenants.Core.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.Helpers; namespace LingTest.Generator { - [LYTService(typeof(IConnectionGenerator),ServiceLifetime =ServiceLifetime.Scoped)] + //[LYTService(typeof(IConnectionGenerator),ServiceLifetime =ServiceLifetime.Scoped)] public class CombindedConnectionGenerator : IConnectionGenerator { private readonly IConfiguration configuration; @@ -18,12 +16,11 @@ namespace LingTest.Generator public string GetConnection(TenantOption option, TenantInfo tenantInfo) - { + { var span = tenantInfo.Name.AsSpan(); if (span.Length > 4 && int.TryParse(span[5].ToString(), out var number)) { - var connection= configuration.GetConnectionString($"{option.ConnectionPrefix}container{number%2+1}"); - LoggerHelper.DefaultLog(connection); + var connection = configuration.GetConnectionString($"{option.ConnectionPrefix}container{number % 2 + 1}"); return connection; } throw new NotSupportedException("tenant invalid"); diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index cdfa926..ce3d331 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -9,7 +9,7 @@ namespace LingTest { public override void ARegisterModule(WebApplicationBuilder services) { - services.Services.AddMySqlPerTable(settings => + services.Services.AddMySqlPerConnection(settings => { settings.ConnectionPrefix = "mysql_"; }); diff --git a/LingTest/appsettings.json b/LingTest/appsettings.json index 8a7dcee..c298c69 100644 --- a/LingTest/appsettings.json +++ b/LingTest/appsettings.json @@ -6,9 +6,9 @@ } }, "ConnectionStrings": { - "mysql_container1": "server=192.168.148.131;port=3306;database=multi_tenant_container1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - "mysql_container2": "server=192.168.148.131;port=3306;database=multi_tenant_container2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" - + "mysql_default": "server=192.168.148.131;port=3306;database=multi_tenant_default;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + "mysql_store1": "server=192.168.148.131;port=3306;database=multi_tenant_store1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + "mysql_store2": "server=192.168.148.131;port=3306;database=multi_tenant_store2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs index f69ccfa..22a5abc 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs @@ -178,11 +178,11 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension { services.AddScoped(); services.AddScoped(); - services.TryAddScoped(); + services.TryAddScoped(); services.TryAddScoped, TenantConnectionResolver>(); services.TryAddScoped, TenantEntityBuilder>(); - services.TryAddScoped, SimpleEntityScaner>(); - services.AddScoped(); + services.TryAddScoped, EntityScaner>(); + services.AddScoped(); services.InitSettings(settings, setupAction); services.AddTenantDbContext(); @@ -204,9 +204,9 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension { var settings = serviceProvider.GetService>(); var connectionResolver = serviceProvider.GetService>(); - var tenant = serviceProvider.GetService(); - settings.DbContextSetup?.Invoke(serviceProvider, connectionResolver.GetConnection(), options); + var connectionStr = connectionResolver.GetConnection(); + settings.DbContextSetup?.Invoke(serviceProvider, connectionStr, options); options.ReplaceServiceTenanted(settings); settings.DbContextOptionAction?.Invoke(options); diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs similarity index 76% rename from LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs rename to LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs index f07ffca..f301ae6 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/NamedConnectionGenerator.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs @@ -1,24 +1,22 @@ using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Helpers; using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl { - public class NamedConnectionGenerator : IConnectionGenerator + public class ConnectionGenerator : IConnectionGenerator { private readonly IConfiguration configuration; public string TenantKey => "unknow"; - public NamedConnectionGenerator(IConfiguration configuration) + public ConnectionGenerator(IConfiguration configuration) { this.configuration = configuration; } public string GetConnection(TenantOption option, TenantInfo tenantInfo) - { + { string connectionString = null; switch (option.ConnectionType) { @@ -29,9 +27,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl case ConnectionResolverType.BySchema: connectionString = configuration.GetConnectionString(option.ConnectionName); break; - } - LoggerHelper.DefaultLog(JsonConvert.SerializeObject(option)); - LoggerHelper.DefaultLog(JsonConvert.SerializeObject(tenantInfo)); + } return connectionString; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs index 5bed826..9b56d5f 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs @@ -1,6 +1,5 @@ using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -27,29 +26,28 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl { var connectionGenerator = this.GetConnectionGenerator(); var option = GenerateOption(); - return connectionGenerator.GetConnection(option, tenantInfo); } IConnectionGenerator GetConnectionGenerator() { - LoggerHelper.DefaultLog("优先使用设置中的 ConnectionGenerator 委托"); + //优先使用设置中的 ConnectionGenerator 委托 if (this.setting.ConnectionGenerator != null) { return this.setting.ConnectionGenerator.Invoke(); } - LoggerHelper.DefaultLog("从服务提供程序中获取所有 IConnectionGenerator 实现"); + //从服务提供程序中获取所有 IConnectionGenerator 实现 var connectionGenerators = serviceProvider.GetServices(); - LoggerHelper.DefaultLog("查找匹配的 IConnectionGenerator 实现"); + //查找匹配的 IConnectionGenerator 实现 var matchedGenerator = connectionGenerators .FirstOrDefault(r => r.TenantKey == this.setting.Key || r.MatchTenantKey(this.setting.Key)); if (matchedGenerator != null) { return matchedGenerator; } - LoggerHelper.DefaultLog("如果没有匹配的实现,使用 NamedConnectionGenerator"); - var defaultConnectionGenerator= serviceProvider.GetService(); + //如果没有匹配的实现,使用 ConnectionGenerator + var defaultConnectionGenerator = serviceProvider.GetService(); return defaultConnectionGenerator; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs similarity index 86% rename from LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs rename to LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs index a1499b2..5f86e1c 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/SimpleHeaderTenantInfoGenerator.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs @@ -4,10 +4,10 @@ using Microsoft.AspNetCore.Http; namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl { - public class SimpleHeaderTenantInfoGenerator : ITenantInfoGenerator + public class TenantInfoGenerator : ITenantInfoGenerator { private readonly TenantInfo tenantInfo; - public SimpleHeaderTenantInfoGenerator(TenantInfo tenantInfo) + public TenantInfoGenerator(TenantInfo tenantInfo) { this.tenantInfo = tenantInfo; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs index 08dc8c1..64a2fd9 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs @@ -1,4 +1,5 @@ using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; +using LingYanAspCoreFramework.Helpers; using Microsoft.AspNetCore.Http; namespace LingYanAspCoreFramework.DbMultiTenants.Core @@ -14,8 +15,9 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core public async Task InvokeAsync(HttpContext context, ITenantInfoGenerator tenantInfoGenerator) { + LoggerHelper.DefaultLog("进入中间件"); tenantInfoGenerator.GenerateTenant(this, context); - + await _next(context); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/EntityScaner.cs similarity index 96% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs rename to LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/EntityScaner.cs index e7a0d64..bdd2369 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/SimpleEntityScaner.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/EntityScaner.cs @@ -9,7 +9,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl /// 简单实体扫描器类,用于扫描数据库上下文中的实体类型 /// /// 数据库上下文类型 - public class SimpleEntityScaner : IEntityScaner + public class EntityScaner : IEntityScaner where TDbContext : DbContext { private static List dbProperties; diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs similarity index 88% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs rename to LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs index f99667f..71d69b2 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantBaseDbContext.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs @@ -7,7 +7,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl /// /// 表示租户基础数据库上下文的抽象类 /// - public abstract class TenantBaseDbContext : DbContext, ITenantDbContext + public abstract class TenantDbContext : DbContext, ITenantDbContext { /// /// 获取或设置租户信息 @@ -22,7 +22,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl /// 数据库上下文选项 /// 租户信息 /// 服务提供程序 - public TenantBaseDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) + public TenantDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) : base(options) { this.serviceProvider = serviceProvider; diff --git a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj index 64e48ed..893a334 100644 --- a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj +++ b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj @@ -38,6 +38,8 @@ + + @@ -48,6 +50,7 @@ + diff --git a/LingYanAspCoreFramework/SampleRoots/SampleQrCodeHelper.cs b/LingYanAspCoreFramework/SampleRoots/SampleQrCodeHelper.cs new file mode 100644 index 0000000..f37bcf2 --- /dev/null +++ b/LingYanAspCoreFramework/SampleRoots/SampleQrCodeHelper.cs @@ -0,0 +1,190 @@ +锘縰sing SkiaSharp; +using SkiaSharp.QrCode; +using System.Text; + +namespace LingYanAspCoreFramework.SampleRoots +{ + class SampleQrCodeHelper + { + public class QRCodeConfig + { + public string Text { get; set; } = default!; + public int Width { get; set; } + public int Height { get; set; } + public byte[]? LogoImgae { get; set; } + public int KeepWhiteBorderPixelVal { get; set; } = 0; + public string BackgroundColor { get; set; } = "ffffff"; + public string CodeColor { get; set; } = "000000"; + } + public static byte[] CreateQRCode(string content, int width, int hight) + { + using var generator = new QRCodeGenerator(); + var qr = generator.CreateQrCode(content, ECCLevel.H); + var info = new SKImageInfo(width, hight); + using var surface = SKSurface.Create(info); + var canvas = surface.Canvas; + canvas.Render(qr, info.Width, info.Height); + using var image = surface.Snapshot(); + using var data = image.Encode(SKEncodedImageFormat.Png, 100); + return data.ToArray(); + } + public static byte[] CreateLogoQRCode(QRCodeConfig qRCodeConfig) + { + qRCodeConfig.Text = qRCodeConfig.Text ?? throw new ArgumentNullException(nameof(qRCodeConfig)); + var keepWhiteBorderPixelVal = qRCodeConfig.KeepWhiteBorderPixelVal is >= -1 and < 10 ? qRCodeConfig.KeepWhiteBorderPixelVal : -1; + var width = qRCodeConfig.Width is > 100 and < 1200 ? qRCodeConfig.Width : 500; + var height = qRCodeConfig.Height is > 100 and < 1200 ? qRCodeConfig.Height : 500; ; + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + var qRCodeWriter = new ZXing.QrCode.QRCodeWriter(); + var hints = new Dictionary + { + { ZXing.EncodeHintType.CHARACTER_SET, "UTF-8" }, + { ZXing.EncodeHintType.QR_VERSION, 8 }, + { ZXing.EncodeHintType.ERROR_CORRECTION, ZXing.QrCode.Internal.ErrorCorrectionLevel.H } + }; + var bitMatrix = qRCodeWriter.encode(qRCodeConfig.Text, ZXing.BarcodeFormat.QR_CODE, width, height, hints); + var w = bitMatrix.Width; + var h = bitMatrix.Height; + var sKBitmap = new SKBitmap(w, h); + var blackStartPointX = 0; + var blackStartPointY = 0; + var blackEndPointX = w; + var blackEndPointY = h; + using var sKCanvas = new SKCanvas(sKBitmap); + var sKColorBlack = SKColor.Parse(qRCodeConfig.CodeColor); + var sKColorWihte = SKColor.Parse(qRCodeConfig.BackgroundColor); + sKCanvas.Clear(sKColorWihte); + var blackStartPointIsNotWriteDown = true; + for (var y = 0; y < h; y++) + { + for (var x = 0; x < w; x++) + { + var flag = bitMatrix[x, y]; + if (flag) + { + if (blackStartPointIsNotWriteDown) + { + blackStartPointX = x; + blackStartPointY = y; + blackStartPointIsNotWriteDown = false; + } + blackEndPointX = x; + blackEndPointY = y; + sKCanvas.DrawPoint(x, y, sKColorBlack); + } + else + { + sKCanvas.DrawPoint(x, y, sKColorWihte); + } + } + } + var qrcodeRealWidth = blackEndPointX - blackStartPointX; + var qrcodeRealHeight = blackEndPointY - blackStartPointY; + if (keepWhiteBorderPixelVal > -1) + { + var borderMaxWidth = (int)Math.Floor((double)qrcodeRealWidth / 10); + if (keepWhiteBorderPixelVal > borderMaxWidth) + { + keepWhiteBorderPixelVal = borderMaxWidth; + } + var nQrcodeRealWidth = width - keepWhiteBorderPixelVal - keepWhiteBorderPixelVal; + var nQrcodeRealHeight = height - keepWhiteBorderPixelVal - keepWhiteBorderPixelVal; + + var sKBitmap2 = new SKBitmap(width, height); + var sKCanvas2 = new SKCanvas(sKBitmap2); + sKCanvas2.Clear(sKColorWihte); + sKCanvas2.DrawBitmap( + sKBitmap, + new SKRect + { + Location = new SKPoint { X = blackStartPointX, Y = blackStartPointY }, + Size = new SKSize { Height = qrcodeRealHeight, Width = qrcodeRealWidth } + }, + new SKRect + { + Location = new SKPoint { X = keepWhiteBorderPixelVal, Y = keepWhiteBorderPixelVal }, + Size = new SKSize { Width = nQrcodeRealWidth, Height = nQrcodeRealHeight } + }); + + blackStartPointX = keepWhiteBorderPixelVal; + blackStartPointY = keepWhiteBorderPixelVal; + qrcodeRealWidth = nQrcodeRealWidth; + qrcodeRealHeight = nQrcodeRealHeight; + + sKCanvas2.Dispose(); + sKBitmap.Dispose(); + sKBitmap = sKBitmap2; + } + if (qRCodeConfig is { LogoImgae.Length: > 0 }) + { + using SKBitmap sKBitmapLogo = SKBitmap.Decode(qRCodeConfig.LogoImgae); + if (!sKBitmapLogo.IsEmpty) + { + using var sKPaint2 = new SKPaint + { + FilterQuality = SKFilterQuality.None, + IsAntialias = true + }; + var logoTargetMaxWidth = (int)Math.Floor((double)qrcodeRealWidth / 6); + var logoTargetMaxHeight = (int)Math.Floor((double)qrcodeRealHeight / 6); + var qrcodeCenterX = (int)Math.Floor((double)qrcodeRealWidth / 2); + var qrcodeCenterY = (int)Math.Floor((double)qrcodeRealHeight / 2); + var logoResultWidth = sKBitmapLogo.Width; + var logoResultHeight = sKBitmapLogo.Height; + if (logoResultWidth > logoTargetMaxWidth) + { + var r = (double)logoTargetMaxWidth / logoResultWidth; + logoResultWidth = logoTargetMaxWidth; + logoResultHeight = (int)Math.Floor(logoResultHeight * r); + } + if (logoResultHeight > logoTargetMaxHeight) + { + var r = (double)logoTargetMaxHeight / logoResultHeight; + logoResultHeight = logoTargetMaxHeight; + logoResultWidth = (int)Math.Floor(logoResultWidth * r); + } + var pointX = qrcodeCenterX - (int)Math.Floor((double)logoResultWidth / 2) + blackStartPointX; + var pointY = qrcodeCenterY - (int)Math.Floor((double)logoResultHeight / 2) + blackStartPointY; + + using var sKCanvas3 = new SKCanvas(sKBitmap); + using var sKPaint = new SKPaint + { + FilterQuality = SKFilterQuality.Medium, + IsAntialias = true + }; + sKCanvas3.DrawBitmap( + sKBitmapLogo, + new SKRect + { + Location = new SKPoint { X = 0, Y = 0 }, + Size = new SKSize { Height = sKBitmapLogo.Height, Width = sKBitmapLogo.Width } + }, + new SKRect + { + Location = new SKPoint { X = pointX, Y = pointY }, + Size = new SKSize { Height = logoResultHeight, Width = logoResultWidth } + }, sKPaint); + } + } + using SKImage sKImage = SKImage.FromBitmap(sKBitmap); + sKBitmap.Dispose(); + using var data = sKImage.Encode(SKEncodedImageFormat.Png, 100); + return data.ToArray(); + } + public static byte[] ConvertImageToByteArray(string imagePath, SKEncodedImageFormat format = SKEncodedImageFormat.Png, int quality = 100) + { + // 鍔犺浇鍥剧墖 + using (SKBitmap skBitmap = SKBitmap.Decode(imagePath)) + { + // 鍒涘缓涓涓唴瀛樻祦鏉ヤ繚瀛樼紪鐮佸悗鐨勫浘鍍 + using (var memoryStream = new MemoryStream()) + { + // 灏嗗浘鐗囩紪鐮佷负鎸囧畾鏍煎紡 + skBitmap.Encode(memoryStream, format, quality); + // 灏嗗唴瀛樻祦杞崲涓哄瓧鑺傛暟缁 + return memoryStream.ToArray(); + } + } + } + } +} -- Gitee From 826d59673616fa9a741c501738e3e61760d48ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Wed, 26 Mar 2025 18:27:50 +0800 Subject: [PATCH 04/17] =?UTF-8?q?=E5=9C=A8=E8=BF=9E=E6=8E=A5=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E4=B8=BA=E7=A9=BA=E6=97=B6=E6=8A=A5=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DbMultiTenants/DbExtensions/MySqlTenantExtension.cs | 5 +++++ LingYanAspCoreFramework/Envs/Configs/StatusCode.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs index ed59c00..c0e4095 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs @@ -3,6 +3,7 @@ using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; @@ -111,6 +112,10 @@ namespace Microsoft.EntityFrameworkCore DbContextOptionsBuilder optionsBuilder) where TDbContext : DbContext, ITenantDbContext { + if (string.IsNullOrEmpty(connectionString)) + { + throw new CommonException(new ResponceBody(63000,"连接字符串不能为空")); + } var settings = serviceProvider.GetService>(); var tenant = serviceProvider.GetService(); optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => diff --git a/LingYanAspCoreFramework/Envs/Configs/StatusCode.json b/LingYanAspCoreFramework/Envs/Configs/StatusCode.json index 2e068e1..c6595e7 100644 --- a/LingYanAspCoreFramework/Envs/Configs/StatusCode.json +++ b/LingYanAspCoreFramework/Envs/Configs/StatusCode.json @@ -39,5 +39,6 @@ "60022": "妗嗘灦閿欒:DNS-TXT鍒犻櫎澶辫触", "60023": "妗嗘灦閿欒:HTTP涓嬭浇涓婁紶绫", "61000": "妗嗘灦閿欒:HTTP灏佽鍖呭紓甯", - "62000": "妗嗘灦閿欒:寰俊寮鏀惧钩鍙板紓甯" + "62000": "妗嗘灦閿欒:寰俊寮鏀惧钩鍙板紓甯", + "63000": "妗嗘灦閿欒:鍒嗗簱鍒嗚〃璁捐閿欒" } \ No newline at end of file -- Gitee From 52ab9559fb18c14ac16221c8386342868da32a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Thu, 27 Mar 2025 18:30:17 +0800 Subject: [PATCH 05/17] =?UTF-8?q?mysql=EF=BC=8Cpgsql.sqlserver=E4=B8=89?= =?UTF-8?q?=E7=A7=8D=E6=95=B0=E6=8D=AE=E6=89=A9=E5=B1=95=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LingTest/Controllers/ProductController.cs | 2 +- LingTest/DbContexts/StoreDbContext.cs | 4 +- LingTest/LingTest.csproj | 4 + .../Migrations/20250327025935_v1.Designer.cs | 56 ++++++ LingTest/Migrations/20250327025935_v1.cs | 57 ++++++ .../Migrations/StoreDbContextModelSnapshot.cs | 50 +++++ LingTest/RestApiModule.cs | 6 +- .../Core/Extension/TenantCoreExtension.cs | 178 ++---------------- .../Core/Impl/ConnectionGenerator.cs | 7 +- .../Core/TenantInfoMiddleware.cs | 2 - .../DbMultiTenants/Core/TenantSettingsT.cs | 8 +- .../DbAccess/Impl/TenantEntityBuilder.cs | 3 + .../DbAccess/MigrationByTenantAssembly.cs | 39 ++++ .../DbAccess/TenantModelCacheKey.cs | 4 +- .../DbAccess/TenantModelCacheKeyFactory.cs | 11 +- .../DbExtensions/MySqlTenantExtension.cs | 101 +++------- .../DbExtensions/PostgreTenantExtension.cs | 104 +++++----- .../DbExtensions/SqlServerTenantExtension.cs | 76 +++----- .../DbMultiTenants/Models/TenantInfo.cs | 2 +- .../DbMultiTenants/Models/TenantOption.cs | 6 +- 20 files changed, 362 insertions(+), 358 deletions(-) create mode 100644 LingTest/Migrations/20250327025935_v1.Designer.cs create mode 100644 LingTest/Migrations/20250327025935_v1.cs create mode 100644 LingTest/Migrations/StoreDbContextModelSnapshot.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs diff --git a/LingTest/Controllers/ProductController.cs b/LingTest/Controllers/ProductController.cs index 98b6ad1..e712a85 100644 --- a/LingTest/Controllers/ProductController.cs +++ b/LingTest/Controllers/ProductController.cs @@ -16,7 +16,7 @@ namespace LingTest.Controllers public ProductController(StoreDbContext storeDbContext) { this.storeDbContext = storeDbContext; - //this.storeDbContext.Database.Migrate(); + this.storeDbContext.Database.Migrate(); this.storeDbContext.Database.EnsureCreated(); } diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index c993290..706230b 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -2,7 +2,6 @@ using LingTest.Entitys; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; namespace LingTest.DbContexts { @@ -10,10 +9,9 @@ namespace LingTest.DbContexts { public DbSet Products => this.Set(); - public StoreDbContext(IMemoryCache cache,DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) + public StoreDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) : base(options, tenant, serviceProvider) { - string ab="ab"; } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/LingTest/LingTest.csproj b/LingTest/LingTest.csproj index f55199d..9b0dd51 100644 --- a/LingTest/LingTest.csproj +++ b/LingTest/LingTest.csproj @@ -23,4 +23,8 @@ + + + + diff --git a/LingTest/Migrations/20250327025935_v1.Designer.cs b/LingTest/Migrations/20250327025935_v1.Designer.cs new file mode 100644 index 0000000..c339aba --- /dev/null +++ b/LingTest/Migrations/20250327025935_v1.Designer.cs @@ -0,0 +1,56 @@ +锘// +using System; +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20250327025935_v1")] + partial class v1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Price") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("_Products", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/Migrations/20250327025935_v1.cs b/LingTest/Migrations/20250327025935_v1.cs new file mode 100644 index 0000000..1ad41b8 --- /dev/null +++ b/LingTest/Migrations/20250327025935_v1.cs @@ -0,0 +1,57 @@ +锘縰sing LingYanAspCoreFramework.Helpers; +using LingYanAspCoreFramework.Models; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LingTest.Migrations +{ + /// + public partial class v1 : Migration + { + //姝ラ1:娉ㄥ叆琛ㄥ悕鍓嶇紑 + private readonly string prefix; + public v1(string prefix) + { + LoggerHelper.DefaultLog("杩佺Щ鍓嶇紑"+ prefix); + if (string.IsNullOrEmpty(prefix)) + { + throw new CommonException(new ResponceBody(63000, "杩佺Щ鍓嶇紑涓虹┖")); + } + this.prefix = prefix; + } + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + //姝ラ2:琛ㄥ悕+鍓嶇紑 + migrationBuilder.CreateTable( + name: prefix + "_Products", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Price = table.Column(type: "double", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK__Products", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + //姝ラ3:琛ㄥ悕+鍓嶇紑 + migrationBuilder.DropTable( + name: prefix + "_Products"); + } + } +} diff --git a/LingTest/Migrations/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/StoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..12cc3a9 --- /dev/null +++ b/LingTest/Migrations/StoreDbContextModelSnapshot.cs @@ -0,0 +1,50 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +#nullable disable + +namespace LingTest.Migrations +{ + [DbContext(typeof(StoreDbContext))] + partial class StoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Price") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("_Products", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index ce3d331..b1a828e 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -9,9 +9,13 @@ namespace LingTest { public override void ARegisterModule(WebApplicationBuilder services) { - services.Services.AddMySqlPerConnection(settings => + services.Services.AddMySqlPerTable(settings => { settings.ConnectionPrefix = "mysql_"; + settings.TableNameFunc = (tenantInfo, tableName) => + { + return $"{tenantInfo.Name}_{tableName}"; + }; }); } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs index 22a5abc..1566073 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs @@ -7,6 +7,7 @@ using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -17,37 +18,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension /// public static class TenantCoreExtension { - /// - /// 按连接注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 数据库集成类型 - /// 键 - /// 连接前缀 - /// 数据库上下文选项操作 - /// 数据库上下文设置操作 - /// 服务集合 - public static IServiceCollection AddDbPerConnection(this IServiceCollection services, - DbIntegrationType dbType, string key = "default", - string connectionPrefix = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - var settings = new TenantSettings() - { - Key = key, - DbType = dbType, - ConnectionPrefix = connectionPrefix, - ConnectionType = ConnectionResolverType.ByDatabase, - DbContextOptionAction = optionAction, - DbContextSetup = dbContextSetup - }; - - return services.AddTenantedDatabase(settings); - } - /// /// 按连接注册多租户数据库 /// @@ -65,38 +35,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension }; return services.AddTenantedDatabase(settings, setupAction); } - - /// - /// 按表注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 数据库集成类型 - /// 键 - /// 连接名称 - /// 数据库上下文选项操作 - /// 数据库上下文设置操作 - /// 服务集合 - public static IServiceCollection AddDbPerTable(this IServiceCollection services, - DbIntegrationType dbType, string key = "default", - string connectionName = "tenantConnection", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - var settings = new TenantSettings() - { - Key = key, - DbType = dbType, - ConnectionName = connectionName, - ConnectionType = ConnectionResolverType.ByTable, - DbContextOptionAction = optionAction, - DbContextSetup = dbContextSetup - }; - - return services.AddTenantedDatabase(settings); - } - /// /// 按表注册多租户数据库 /// @@ -104,8 +42,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension /// 服务集合 /// 设置操作 /// 服务集合 - public static IServiceCollection AddDbPerTable(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDbPerTable(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext { var settings = new TenantSettings() @@ -114,38 +51,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension }; return services.AddTenantedDatabase(settings, setupAction); } - - /// - /// 按模式注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 数据库集成类型 - /// 键 - /// 连接名称 - /// 数据库上下文选项操作 - /// 数据库上下文设置操作 - /// 服务集合 - public static IServiceCollection AddDbPerSchema(this IServiceCollection services, - DbIntegrationType dbType, string key = "default", - string connectionName = "tenantConnection", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - var settings = new TenantSettings() - { - Key = key, - DbType = dbType, - ConnectionName = connectionName, - ConnectionType = ConnectionResolverType.BySchema, - DbContextOptionAction = optionAction, - DbContextSetup = dbContextSetup - }; - - return services.AddTenantedDatabase(settings); - } - /// /// 按模式注册多租户数据库 /// @@ -173,7 +78,8 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension /// 设置操作 /// 服务集合 public static IServiceCollection AddTenantedDatabase(this IServiceCollection services, - TenantSettings settings, Action> setupAction = null) + TenantSettings settings, + Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext { services.AddScoped(); @@ -183,74 +89,25 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension services.TryAddScoped, TenantEntityBuilder>(); services.TryAddScoped, EntityScaner>(); services.AddScoped(); - services.InitSettings(settings, setupAction); - - services.AddTenantDbContext(); - - - return services; - } - - /// - /// 注册租户数据库上下文 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 服务集合 - internal static IServiceCollection AddTenantDbContext(this IServiceCollection services) - where TDbContext : DbContext, ITenantDbContext - { - services.AddDbContext((serviceProvider, options) => - { - var settings = serviceProvider.GetService>(); - var connectionResolver = serviceProvider.GetService>(); - var tenant = serviceProvider.GetService(); - var connectionStr = connectionResolver.GetConnection(); - settings.DbContextSetup?.Invoke(serviceProvider, connectionStr, options); - options.ReplaceServiceTenanted(settings); - settings.DbContextOptionAction?.Invoke(options); - - }); - return services; - } - - /// - /// 初始化租户设置 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 租户设置 - /// 设置操作 - /// 服务集合 - internal static IServiceCollection InitSettings(this IServiceCollection services, - TenantSettings settings, Action> setupAction) - where TDbContext : DbContext, ITenantDbContext - { services.AddSingleton((sp) => { var rct = settings ?? new TenantSettings(); setupAction?.Invoke(rct); return rct; }); - return services; - } - - /// - /// 替换服务为多租户服务 - /// - /// 数据库上下文类型 - /// 数据库上下文选项构建器 - /// 租户设置 - public static void ReplaceServiceTenanted(this DbContextOptionsBuilder dbOptions, - TenantSettings settings) - where TDbContext : DbContext, ITenantDbContext - { - if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + services.AddDbContext((serviceProvider, options) => { - dbOptions.ReplaceService>(); - } - } + var tenantSettingT = serviceProvider.GetService>(); + var connectionResolver = serviceProvider.GetService>(); + var tenantInfo = serviceProvider.GetService(); + var connectionStr = connectionResolver.GetConnection(); + tenantSettingT.DbContextSetup?.Invoke(tenantSettingT, tenantInfo, connectionStr, options); + tenantSettingT.DbContextOptionAction?.Invoke(options); + }); + + return services; + } /// /// 配置租户构建器 /// @@ -258,11 +115,9 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension /// 关系型数据库上下文选项构建器类型 /// 关系型选项扩展类型 /// 关系型数据库上下文选项构建器 - /// 服务提供程序 /// 租户设置 /// 租户信息 - public static void TenantBuilderSetup(this RelationalDbContextOptionsBuilder builder, - IServiceProvider serviceProvider, TenantSettings settings, TenantInfo tenant) + public static void TenantBuilderMigration(this RelationalDbContextOptionsBuilder builder, TenantSettings settings, TenantInfo tenant) where TDbContext : DbContext, ITenantDbContext where TBuilder : RelationalDbContextOptionsBuilder where TExtension : RelationalOptionsExtension, new() @@ -278,5 +133,4 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension } } - } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs index f301ae6..71fa03c 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs @@ -16,18 +16,19 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl } public string GetConnection(TenantOption option, TenantInfo tenantInfo) - { + { string connectionString = null; switch (option.ConnectionType) { + case ConnectionResolverType.Default: case ConnectionResolverType.ByDatabase: connectionString = configuration.GetConnectionString($"{option.ConnectionPrefix}{tenantInfo.Name}"); break; case ConnectionResolverType.ByTable: case ConnectionResolverType.BySchema: - connectionString = configuration.GetConnectionString(option.ConnectionName); + connectionString = configuration.GetConnectionString($"{option.ConnectionPrefix}{tenantInfo.Name}"); break; - } + } return connectionString; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs index 64a2fd9..d35f879 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs @@ -15,9 +15,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core public async Task InvokeAsync(HttpContext context, ITenantInfoGenerator tenantInfoGenerator) { - LoggerHelper.DefaultLog("进入中间件"); tenantInfoGenerator.GenerateTenant(this, context); - await _next(context); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs index 8163cf5..a29029f 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore; namespace LingYanAspCoreFramework.DbMultiTenants.Core { /// - /// 琛ㄧず鐗瑰畾鏁版嵁搴撲笂涓嬫枃绫诲瀷鐨勭鎴疯缃殑绫 + /// 澶氱鎴锋暟鎹簱涓婁笅鏂囩殑鍔ㄦ侀厤缃 /// /// 鏁版嵁搴撲笂涓嬫枃绫诲瀷 public class TenantSettings @@ -20,12 +20,12 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core /// /// 杩炴帴瑙f瀽鍣ㄧ被鍨 /// - public ConnectionResolverType ConnectionType { get; set; } + internal ConnectionResolverType ConnectionType { get; set; } /// /// 鏁版嵁搴撻泦鎴愮被鍨 /// - public DbIntegrationType DbType { get; set; } + internal DbIntegrationType DbType { get; set; } /// /// 杩炴帴鐢熸垚鍣ㄥ嚱鏁 @@ -60,6 +60,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core /// /// 鏁版嵁搴撲笂涓嬫枃璁剧疆鎿嶄綔 /// - public Action DbContextSetup { get; set; } + public Action, TenantInfo, string, DbContextOptionsBuilder> DbContextSetup { get; set; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs index 382ca2d..e32c417 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs @@ -1,6 +1,7 @@ using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.Helpers; using Microsoft.EntityFrameworkCore; namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl @@ -48,11 +49,13 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl var schemaName = this.tenantOption.SchemaFunc?.Invoke(this.tenantInfo) ?? this.tenantInfo.Name; entity.ToTable(tableName, schemaName); + LoggerHelper.DefaultLog($"实体构建表:{tableName},模式:{schemaName}"); break; case ConnectionResolverType.ByTable: tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) ?? $"{this.tenantInfo.Name}_{property.PropertyName}"; entity.ToTable(tableName); + LoggerHelper.DefaultLog($"实体构建表:{tableName}"); break; default: break; diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs new file mode 100644 index 0000000..e5f360c --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs @@ -0,0 +1,39 @@ +锘縰sing LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Internal; +using System.Reflection; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + public class MigrationByTenantAssembly : MigrationsAssembly + { + private readonly DbContext context; + public MigrationByTenantAssembly(ICurrentDbContext currentContext, + IDbContextOptions options, IMigrationsIdGenerator idGenerator, + IDiagnosticsLogger logger) + : base(currentContext, options, idGenerator, logger) + { + context = currentContext.Context; + } + public override Migration CreateMigration(TypeInfo migrationClass, string activeProvider) + { + if (activeProvider == null) + throw new ArgumentNullException($"{nameof(activeProvider)} argument is null"); + + var hasCtorWithSchema = migrationClass + .GetConstructor(new[] { typeof(string) }) != null; + + if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext) + { + var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.Tenant?.Name); + instance.ActiveProvider = activeProvider; + return instance; + } + + return base.CreateMigration(migrationClass, activeProvider); + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs index 34f48bc..8397602 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs @@ -32,7 +32,8 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess /// 如果相等,返回 true;否则返回 false protected override bool Equals(ModelCacheKey other) { - return base.Equals(other) && (other as TenantModelCacheKey)?.identifier == identifier; + var isEqual= base.Equals(other) && (other as TenantModelCacheKey)?.identifier == identifier; + return isEqual; } /// @@ -46,7 +47,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess { hashCode ^= identifier.GetHashCode(); } - return hashCode; } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs index 108cb7b..a3870c9 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs @@ -17,17 +17,22 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess /// 模型缓存键工厂的依赖项 public TenantModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) { - } + } + public override object Create(DbContext context) + { + return Create(context, false); + } /// /// 创建模型缓存键 /// /// 数据库上下文 /// 模型缓存键对象 - public override object Create(DbContext context) + public override object Create(DbContext context, bool designTime) { var dbContext = context as TDbContext; - return new TenantModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); + var tenantModelCacheKey = new TenantModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); + return tenantModelCacheKey; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs index c0e4095..b6daa91 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs @@ -1,10 +1,12 @@ using LingYanAspCoreFramework.DbMultiTenants.Core; using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore @@ -14,27 +16,6 @@ namespace Microsoft.EntityFrameworkCore /// public static class MySqlTenantExtension { - /// - /// 按连接注册 MySQL 多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 键 - /// 连接前缀 - /// 数据库上下文选项操作 - /// 数据库上下文设置操作 - /// 服务集合 - public static IServiceCollection AddMySqlPerConnection(this IServiceCollection services, - string key = "default", - string connectionPrefix = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerConnection(DbIntegrationType.Mysql, key, connectionPrefix, - optionAction, dbContextSetup ?? SetUpMySql); - } - /// /// 按连接注册 MySQL 多租户数据库 /// @@ -42,34 +23,11 @@ namespace Microsoft.EntityFrameworkCore /// 服务集合 /// 设置操作 /// 服务集合 - public static IServiceCollection AddMySqlPerConnection(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddMySqlPerConnection(this IServiceCollection services, Action> setupAction) where TDbContext : DbContext, ITenantDbContext { return services.AddDbPerConnection(CombineSettings(setupAction)); } - - /// - /// 按表注册 MySQL 多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 键 - /// 连接名称 - /// 数据库上下文选项操作 - /// 数据库上下文设置操作 - /// 服务集合 - public static IServiceCollection AddMySqlPerTable(this IServiceCollection services, - string key = "default", - string connectionName = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerTable(DbIntegrationType.Mysql, key, connectionName, - optionAction, dbContextSetup ?? SetUpMySql); - } - /// /// 按表注册 MySQL 多租户数据库 /// @@ -77,8 +35,7 @@ namespace Microsoft.EntityFrameworkCore /// 服务集合 /// 设置操作 /// 服务集合 - public static IServiceCollection AddMySqlPerTable(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddMySqlPerTable(this IServiceCollection services, Action> setupAction) where TDbContext : DbContext, ITenantDbContext { return services.AddDbPerTable(CombineSettings(setupAction)); @@ -90,38 +47,34 @@ namespace Microsoft.EntityFrameworkCore /// 数据库上下文类型 /// 设置操作 /// 组合后的设置操作 - static Action> CombineSettings( - Action> setupAction = null) + static Action> CombineSettings(Action> setupAction) where TDbContext : DbContext, ITenantDbContext { - return (settings) => + Action> settingAction = (settings) => { - settings.DbContextSetup = SetUpMySql; + settings.DbContextSetup = (tenantSettings, tenantInfo, connectionString, optionsBuilder) => + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); + } + optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => + { + builder.TenantBuilderMigration(tenantSettings, tenantInfo); + }); + }; + settings.DbContextOptionAction = (dbContextOptionsBuilder) => + { + if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + { + dbContextOptionsBuilder.ReplaceService>(); + dbContextOptionsBuilder.ReplaceService(); + } + }; + settings.DbType = DbIntegrationType.Mysql; setupAction?.Invoke(settings); }; - } - - /// - /// 设置 MySQL 数据库上下文 - /// - /// 数据库上下文类型 - /// 服务提供程序 - /// 连接字符串 - /// 数据库上下文选项构建器 - internal static void SetUpMySql(IServiceProvider serviceProvider, string connectionString, - DbContextOptionsBuilder optionsBuilder) - where TDbContext : DbContext, ITenantDbContext - { - if (string.IsNullOrEmpty(connectionString)) - { - throw new CommonException(new ResponceBody(63000,"连接字符串不能为空")); - } - var settings = serviceProvider.GetService>(); - var tenant = serviceProvider.GetService(); - optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => - { - builder.TenantBuilderSetup(serviceProvider, settings, tenant); - }); + return settingAction; } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs index 8f48692..d272770 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs @@ -1,25 +1,25 @@ using LingYanAspCoreFramework.DbMultiTenants.Core; using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.Models; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore { public static class PostgreTenantExtension { - public static IServiceCollection AddPostgrePerConnection(this IServiceCollection services, - string key = "default", - string connectionPrefix = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerConnection(DbIntegrationType.Postgre, key, connectionPrefix, - optionAction, dbContextSetup ?? SetUpPostgre); - } - + /// + /// 按连接注册多租户 + /// + /// + /// + /// + /// public static IServiceCollection AddPostgrePerConnection(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext @@ -27,35 +27,26 @@ namespace Microsoft.EntityFrameworkCore return services.AddDbPerConnection(CombineSettings(setupAction)); } - public static IServiceCollection AddPostgrePerTable(this IServiceCollection services, - string key = "default", - string connectionName = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerTable(DbIntegrationType.Postgre, key, connectionName, - optionAction, dbContextSetup ?? SetUpPostgre); - } - + /// + /// 按表注册多租户 + /// + /// + /// + /// + /// public static IServiceCollection AddPostgrePerTable(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext { return services.AddDbPerTable(CombineSettings(setupAction)); } - - public static IServiceCollection AddPostgrePerSchema(this IServiceCollection services, - string key = "default", - string connectionName = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerSchema(DbIntegrationType.Postgre, key, connectionName, - optionAction, dbContextSetup ?? SetUpPostgre); - } - + /// + /// 按模式 + /// + /// + /// + /// + /// public static IServiceCollection AddPostgrePerSchema(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext @@ -63,28 +54,43 @@ namespace Microsoft.EntityFrameworkCore return services.AddDbPerSchema(CombineSettings(setupAction)); } - + /// + /// 组合租户信息设置 + /// + /// + /// + /// + /// static Action> CombineSettings( Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext { - return (settings) => + Action> settingAction = (settings) => { - settings.DbContextSetup = SetUpPostgre; + settings.DbContextSetup = (tenantSettings, tenantInfo, connectionString, optionsBuilder) => + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); + } + optionsBuilder.UseNpgsql(connectionString, builder => + { + builder.TenantBuilderMigration(tenantSettings, tenantInfo); + }); + }; + settings.DbContextOptionAction = (dbContextOptionsBuilder) => + { + if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + { + dbContextOptionsBuilder.ReplaceService>(); + dbContextOptionsBuilder.ReplaceService(); + } + }; + settings.DbType = DbIntegrationType.Postgre; setupAction?.Invoke(settings); }; - } - - internal static void SetUpPostgre(IServiceProvider serviceProvider, string connectionString, - DbContextOptionsBuilder optionsBuilder) - where TDbContext : DbContext, ITenantDbContext - { - var settings = serviceProvider.GetService>(); - var tenant = serviceProvider.GetService(); - optionsBuilder.UseNpgsql(connectionString, builder => - { - builder.TenantBuilderSetup(serviceProvider, settings, tenant); - }); + return settingAction; + } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs index 76ba342..4f8b209 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs @@ -1,26 +1,18 @@ using LingYanAspCoreFramework.DbMultiTenants.Core; using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore { public static class SqlServerTenantExtension { - public static IServiceCollection AddSqlServerPerConnection(this IServiceCollection services, - string key = "default", - string connectionPrefix = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerConnection(DbIntegrationType.SqlServer, key, connectionPrefix, - optionAction, dbContextSetup ?? SetUpSqlServer); - } - public static IServiceCollection AddSqlServerPerConnection(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext @@ -28,17 +20,6 @@ namespace Microsoft.EntityFrameworkCore return services.AddDbPerConnection(CombineSettings(setupAction)); } - public static IServiceCollection AddSqlServerPerTable(this IServiceCollection services, - string key = "default", - string connectionName = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerTable(DbIntegrationType.SqlServer, key, connectionName, - optionAction, dbContextSetup ?? SetUpSqlServer); - } - public static IServiceCollection AddSqlServerPerTable(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext @@ -46,17 +27,6 @@ namespace Microsoft.EntityFrameworkCore return services.AddDbPerTable(CombineSettings(setupAction)); } - public static IServiceCollection AddSqlServerPerSchema(this IServiceCollection services, - string key = "default", - string connectionName = "tenanted", - Action optionAction = null, - Action dbContextSetup = null) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDbPerSchema(DbIntegrationType.SqlServer, key, connectionName, - optionAction, dbContextSetup ?? SetUpSqlServer); - } - public static IServiceCollection AddSqlServerPerSchema(this IServiceCollection services, Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext @@ -64,28 +34,34 @@ namespace Microsoft.EntityFrameworkCore return services.AddDbPerSchema(CombineSettings(setupAction)); } - static Action> CombineSettings( - Action> setupAction = null) + static Action> CombineSettings(Action> setupAction = null) where TDbContext : DbContext, ITenantDbContext { - return (settings) => + Action> settingAction = (settings) => { - settings.DbContextSetup = SetUpSqlServer; + settings.DbContextSetup = (tenantSettings, tenantInfo, connectionString, optionsBuilder) => + { + if (string.IsNullOrEmpty(connectionString)) + { + throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); + } + optionsBuilder.UseSqlServer(connectionString, builder => + { + builder.TenantBuilderMigration(tenantSettings, tenantInfo); + }); + }; + settings.DbContextOptionAction = (dbContextOptionsBuilder) => + { + if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + { + dbContextOptionsBuilder.ReplaceService>(); + dbContextOptionsBuilder.ReplaceService(); + } + }; + settings.DbType = DbIntegrationType.SqlServer; setupAction?.Invoke(settings); }; + return settingAction; } - - internal static void SetUpSqlServer(IServiceProvider serviceProvider, string connectionString, - DbContextOptionsBuilder optionsBuilder) - where TDbContext : DbContext, ITenantDbContext - { - var settings = serviceProvider.GetService>(); - var tenant = serviceProvider.GetService(); - optionsBuilder.UseSqlServer(connectionString, builder => - { - builder.TenantBuilderSetup(serviceProvider, settings, tenant); - }); - } - } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs index e0b475b..38defe7 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs @@ -3,7 +3,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models { /// - /// 琛ㄧず绉熸埛淇℃伅鐨勭被 + /// 绉熸埛杩愯鏃朵俊鎭 /// public class TenantInfo : DynamicObject { diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs index fc24a43..1feb4a9 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs @@ -3,7 +3,7 @@ using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; namespace LingYanAspCoreFramework.DbMultiTenants.Models { /// - /// 表示租户选项的类 + /// 租户的数据库连接策略和表结构规则 /// public class TenantOption { @@ -33,12 +33,12 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models public string ConnectionPrefix { get; set; } /// - /// 表名生成函数 + /// 表名生成委托 /// public Func TableNameFunc { get; set; } /// - /// 模式生成函数 + /// 模式生成委托 /// public Func SchemaFunc { get; set; } } -- Gitee From adcc4adcff0e6387d10dbfdf4fc71fe826017cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Fri, 28 Mar 2025 18:21:12 +0800 Subject: [PATCH 06/17] =?UTF-8?q?=E7=A3=81=E7=9B=98=E4=B8=8D=E8=B6=B3?= =?UTF-8?q?=E5=85=88=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LingTest/Controllers/ProductController.cs | 13 +- LingTest/DbContexts/StoreDbContext.cs | 10 +- LingTest/Entitys/Order.cs | 19 +++ LingTest/Entitys/Product.cs | 1 + LingTest/LingTest.csproj | 9 +- .../Migrations/20250327025935_v1.Designer.cs | 56 -------- LingTest/Migrations/20250327025935_v1.cs | 57 -------- .../Migrations/StoreDbContextModelSnapshot.cs | 50 ------- LingTest/RestApiModule.cs | 22 +++- LingTest/appsettings.json | 11 +- .../Core/Extension/TenantCoreExtension.cs | 124 ++++++------------ .../Core/Impl/ConnectionGenerator.cs | 4 +- .../DbMultiTenants/Core/TenantSettingsT.cs | 16 +-- .../DbAccess/Impl/TenantDbContext.cs | 8 +- .../DbAccess/Impl/TenantEntityBuilder.cs | 19 ++- .../DbAccess/MigrationByTenantAssembly.cs | 4 +- .../MigrationsByTenantCodeGenerator.cs | 20 +++ .../DbAccess/TenantModelCacheKeyFactory.cs | 16 ++- .../DbExtensions/MySqlTenantExtension.cs | 64 +++------ .../DbExtensions/PostgreTenantExtension.cs | 72 +++------- .../DbExtensions/SqlServerTenantExtension.cs | 49 +++---- .../Models/Enum/DbIntegrationType.cs | 2 +- .../LingYanAspCoreFramework.csproj | 1 + 23 files changed, 224 insertions(+), 423 deletions(-) create mode 100644 LingTest/Entitys/Order.cs delete mode 100644 LingTest/Migrations/20250327025935_v1.Designer.cs delete mode 100644 LingTest/Migrations/20250327025935_v1.cs delete mode 100644 LingTest/Migrations/StoreDbContextModelSnapshot.cs create mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs diff --git a/LingTest/Controllers/ProductController.cs b/LingTest/Controllers/ProductController.cs index e712a85..0e048d1 100644 --- a/LingTest/Controllers/ProductController.cs +++ b/LingTest/Controllers/ProductController.cs @@ -1,5 +1,6 @@ using LingTest.DbContexts; using LingTest.Entitys; +using LingYanAspCoreFramework.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -16,8 +17,16 @@ namespace LingTest.Controllers public ProductController(StoreDbContext storeDbContext) { this.storeDbContext = storeDbContext; - this.storeDbContext.Database.Migrate(); - this.storeDbContext.Database.EnsureCreated(); + try + { + this.storeDbContext.Database.Migrate(); + this.storeDbContext.Database.EnsureCreated(); + } + catch (Exception ex) + { + LoggerHelper.ErrorLog(ex.Message); + } + } [HttpPost("")] diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index 706230b..c0429a5 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -1,4 +1,5 @@ using LingTest.Entitys; +using LingYanAspCoreFramework.DbMultiTenants.DbAccess; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; @@ -8,8 +9,8 @@ namespace LingTest.DbContexts public class StoreDbContext : TenantDbContext { public DbSet Products => this.Set(); - - public StoreDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) + public DbSet Orders => this.Set(); + public StoreDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) : base(options, tenant, serviceProvider) { } @@ -18,5 +19,10 @@ namespace LingTest.DbContexts { base.OnModelCreating(modelBuilder); } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.ReplaceService(); + } } } diff --git a/LingTest/Entitys/Order.cs b/LingTest/Entitys/Order.cs new file mode 100644 index 0000000..1475a51 --- /dev/null +++ b/LingTest/Entitys/Order.cs @@ -0,0 +1,19 @@ +锘縰sing System.ComponentModel.DataAnnotations; + +namespace LingTest.Entitys +{ + public class Order + { + [Key] + public int Id { get; set; } + + [StringLength(50), Required] + public string Name { get; set; } + + [StringLength(50)] + public string Category { get; set; } + + public double? Price { get; set; } + public string? TestName { get; set; } + } +} diff --git a/LingTest/Entitys/Product.cs b/LingTest/Entitys/Product.cs index 736a4cc..f63337d 100644 --- a/LingTest/Entitys/Product.cs +++ b/LingTest/Entitys/Product.cs @@ -14,5 +14,6 @@ namespace LingTest.Entitys public string Category { get; set; } public double? Price { get; set; } + public string? TestName { get; set; } } } diff --git a/LingTest/LingTest.csproj b/LingTest/LingTest.csproj index 9b0dd51..0464b01 100644 --- a/LingTest/LingTest.csproj +++ b/LingTest/LingTest.csproj @@ -8,10 +8,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,8 +20,4 @@ - - - - diff --git a/LingTest/Migrations/20250327025935_v1.Designer.cs b/LingTest/Migrations/20250327025935_v1.Designer.cs deleted file mode 100644 index c339aba..0000000 --- a/LingTest/Migrations/20250327025935_v1.Designer.cs +++ /dev/null @@ -1,56 +0,0 @@ -锘// -using System; -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(StoreDbContext))] - [Migration("20250327025935_v1")] - partial class v1 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Category") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("varchar(50)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("varchar(50)"); - - b.Property("Price") - .HasColumnType("double"); - - b.HasKey("Id"); - - b.ToTable("_Products", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20250327025935_v1.cs b/LingTest/Migrations/20250327025935_v1.cs deleted file mode 100644 index 1ad41b8..0000000 --- a/LingTest/Migrations/20250327025935_v1.cs +++ /dev/null @@ -1,57 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Helpers; -using LingYanAspCoreFramework.Models; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v1 : Migration - { - //姝ラ1:娉ㄥ叆琛ㄥ悕鍓嶇紑 - private readonly string prefix; - public v1(string prefix) - { - LoggerHelper.DefaultLog("杩佺Щ鍓嶇紑"+ prefix); - if (string.IsNullOrEmpty(prefix)) - { - throw new CommonException(new ResponceBody(63000, "杩佺Щ鍓嶇紑涓虹┖")); - } - this.prefix = prefix; - } - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase() - .Annotation("MySql:CharSet", "utf8mb4"); - //姝ラ2:琛ㄥ悕+鍓嶇紑 - migrationBuilder.CreateTable( - name: prefix + "_Products", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Category = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Price = table.Column(type: "double", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK__Products", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - //姝ラ3:琛ㄥ悕+鍓嶇紑 - migrationBuilder.DropTable( - name: prefix + "_Products"); - } - } -} diff --git a/LingTest/Migrations/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/StoreDbContextModelSnapshot.cs deleted file mode 100644 index 12cc3a9..0000000 --- a/LingTest/Migrations/StoreDbContextModelSnapshot.cs +++ /dev/null @@ -1,50 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(StoreDbContext))] - partial class StoreDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("Category") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("varchar(50)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("varchar(50)"); - - b.Property("Price") - .HasColumnType("double"); - - b.HasKey("Id"); - - b.ToTable("_Products", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index b1a828e..4495e0b 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -9,12 +9,26 @@ namespace LingTest { public override void ARegisterModule(WebApplicationBuilder services) { - services.Services.AddMySqlPerTable(settings => + //鍒嗗簱鏂规($DynamicDataBaseName)-(杩佺Щ琛)-锛$DynamicDataBaseName_杩炴帴锛 + //鍒嗚〃鏂规($DynamicTableName)-($DynamicTableName_杩佺Щ琛)-锛堣繛鎺ワ級 + //鍒嗗簱鍒嗚〃($DynamicDataBaseName-$DynamicTableName)-锛$DynamicTableName_杩佺Щ琛級-锛$DynamicDataBaseNam_杩炴帴锛 + services.Services.AddDynamicShardingSchemaWithSqlServer(settings => { - settings.ConnectionPrefix = "mysql_"; - settings.TableNameFunc = (tenantInfo, tableName) => + settings.ConnectionName = "sqlserver_default"; + //settings.TableNameFunc = (tenantInfo, tableName) => + //{ + // return $"{tenantInfo?.Name ?? "default"}_{tableName}"; + //}; + settings.SchemaFunc = (tenantInfo) => { - return $"{tenantInfo.Name}_{tableName}"; + if (string.IsNullOrEmpty(tenantInfo?.Name)) + { + return $"dbo"; + } + else + { + return $"dbo.{tenantInfo?.Name}"; + } }; }); } diff --git a/LingTest/appsettings.json b/LingTest/appsettings.json index c298c69..685f43b 100644 --- a/LingTest/appsettings.json +++ b/LingTest/appsettings.json @@ -6,9 +6,18 @@ } }, "ConnectionStrings": { + //mysql杩炴帴妗堜緥 "mysql_default": "server=192.168.148.131;port=3306;database=multi_tenant_default;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", "mysql_store1": "server=192.168.148.131;port=3306;database=multi_tenant_store1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - "mysql_store2": "server=192.168.148.131;port=3306;database=multi_tenant_store2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + "mysql_store2": "server=192.168.148.131;port=3306;database=multi_tenant_store2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + //sqlserver杩炴帴妗堜緥 + "sqlserver_default": "Server=192.168.148.131;Database=multi_tenant_default;User Id=sa;Password=12345678;Encrypt=False;", + "sqlserver_store1": "Server=192.168.148.131;Database=multi_tenant_store1;User Id=sa;Password=12345678;Encrypt=False;", + "sqlserver_store2": "Server=192.168.148.131;Database=multi_tenant_store2;User Id=sa;Password=12345678;Encrypt=False;", + //pgsql 杩炴帴妗堜緥 + "postgre_default": "Server=192.168.148.131;Port=5432;Database=multi_tenant_default;User Id=postgres;Password=12345678;", + "postgre_store1": "Server=192.168.148.131;Port=5432;Database=multi_tenant_store1;User Id=postgres;Password=12345678;", + "postgre_store2": "Server=192.168.148.131;Port=5432;Database=multi_tenant_store2;User Id=postgres;Password=12345678;" }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs index 1566073..abfea63 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs @@ -5,6 +5,7 @@ using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.Helpers; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -13,75 +14,29 @@ using Microsoft.Extensions.DependencyInjection.Extensions; namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension { - /// - /// 租户核心扩展类,提供多租户数据库的注册方法 - /// public static class TenantCoreExtension { - /// - /// 按连接注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 设置操作 - /// 服务集合 - public static IServiceCollection AddDbPerConnection(this IServiceCollection services, - Action> setupAction = null) + internal static IServiceCollection AddDynamicShardingDataBase(this IServiceCollection services, Action> makeWithDbTypeAndOption) where TDbContext : DbContext, ITenantDbContext { - var settings = new TenantSettings() - { - ConnectionType = ConnectionResolverType.ByDatabase - }; - return services.AddTenantedDatabase(settings, setupAction); + return services.AddDynamicSharding(MakeWithShardingTypeAndConfig(makeWithDbTypeAndOption)); } - /// - /// 按表注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 设置操作 - /// 服务集合 - public static IServiceCollection AddDbPerTable(this IServiceCollection services, Action> setupAction = null) + + internal static IServiceCollection AddDynamicShardingTable(this IServiceCollection services, Action> makeWithDbTypeAndOption) where TDbContext : DbContext, ITenantDbContext { - var settings = new TenantSettings() - { - ConnectionType = ConnectionResolverType.ByTable - }; - return services.AddTenantedDatabase(settings, setupAction); + return services.AddDynamicSharding(MakeWithShardingTypeAndConfig(makeWithDbTypeAndOption)); } - /// - /// 按模式注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 设置操作 - /// 服务集合 - public static IServiceCollection AddDbPerSchema(this IServiceCollection services, - Action> setupAction = null) + + internal static IServiceCollection AddDynamicShardingSchema(this IServiceCollection services, Action> makeWithDbTypeAndOption) where TDbContext : DbContext, ITenantDbContext { - var settings = new TenantSettings() - { - ConnectionType = ConnectionResolverType.BySchema - }; - return services.AddTenantedDatabase(settings, setupAction); + return services.AddDynamicSharding(MakeWithShardingTypeAndConfig(makeWithDbTypeAndOption)); } - - /// - /// 注册多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 租户设置 - /// 设置操作 - /// 服务集合 - public static IServiceCollection AddTenantedDatabase(this IServiceCollection services, - TenantSettings settings, - Action> setupAction = null) + private static IServiceCollection AddDynamicSharding(this IServiceCollection services, Func> makeTenantSettingsFunc) where TDbContext : DbContext, ITenantDbContext { + //注册基础服务 services.AddScoped(); services.AddScoped(); services.TryAddScoped(); @@ -89,48 +44,55 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension services.TryAddScoped, TenantEntityBuilder>(); services.TryAddScoped, EntityScaner>(); services.AddScoped(); - services.AddSingleton((sp) => - { - var rct = settings ?? new TenantSettings(); - setupAction?.Invoke(rct); - return rct; - }); + services.AddSingleton(makeTenantSettingsFunc); + //注册数据库上下文 services.AddDbContext((serviceProvider, options) => { - var tenantSettingT = serviceProvider.GetService>(); - var connectionResolver = serviceProvider.GetService>(); - var tenantInfo = serviceProvider.GetService(); + var tenantSettingT = serviceProvider.GetRequiredService>(); + var connectionResolver = serviceProvider.GetRequiredService>(); + var tenantInfo = serviceProvider.GetRequiredService(); var connectionStr = connectionResolver.GetConnection(); - tenantSettingT.DbContextSetup?.Invoke(tenantSettingT, tenantInfo, connectionStr, options); - tenantSettingT.DbContextOptionAction?.Invoke(options); + tenantSettingT?.DbContextOptionDelegate?.Invoke(tenantInfo, connectionStr, options); + tenantSettingT?.DbContextConfigDelegate?.Invoke(options); }); - - return services; } - /// - /// 配置租户构建器 - /// - /// 数据库上下文类型 - /// 关系型数据库上下文选项构建器类型 - /// 关系型选项扩展类型 - /// 关系型数据库上下文选项构建器 - /// 租户设置 - /// 租户信息 - public static void TenantBuilderMigration(this RelationalDbContextOptionsBuilder builder, TenantSettings settings, TenantInfo tenant) + private static Func> MakeWithShardingTypeAndConfig(Action> configure) + where TDbContext : DbContext, ITenantDbContext + { + Func> makeWithShardingTypeAndConfig = (ServiceProvider) => + { + var settings = new TenantSettings(); + settings.DbContextConfigDelegate = options => + { + if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + { + options.ReplaceService>(); + options.ReplaceService(); + } + }; + configure?.Invoke(settings); + return settings; + }; + return makeWithShardingTypeAndConfig; + } + internal static void DbContextBuilderMigration(this RelationalDbContextOptionsBuilder builder, + TenantSettings settings, TenantInfo tenantInfo) where TDbContext : DbContext, ITenantDbContext where TBuilder : RelationalDbContextOptionsBuilder where TExtension : RelationalOptionsExtension, new() { if (settings.ConnectionType == ConnectionResolverType.ByTable) { - builder.MigrationsHistoryTable($"{tenant.Name}__EFMigrationsHistory"); + builder.MigrationsHistoryTable($"{tenantInfo.Name}__EFMigrationsHistory"); } if (settings.ConnectionType == ConnectionResolverType.BySchema) { - builder.MigrationsHistoryTable("__EFMigrationHistory", $"{(settings.SchemaFunc?.Invoke(tenant) ?? tenant.Name)}"); + var schemaName = (settings.SchemaFunc?.Invoke(tenantInfo) ?? tenantInfo.Name); + builder.MigrationsHistoryTable("__EFMigrationHistory", $"{schemaName}"); } } + } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs index 71fa03c..79093c1 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs @@ -22,11 +22,11 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl { case ConnectionResolverType.Default: case ConnectionResolverType.ByDatabase: - connectionString = configuration.GetConnectionString($"{option.ConnectionPrefix}{tenantInfo.Name}"); + connectionString = configuration.GetConnectionString($"{option.ConnectionName}"); break; case ConnectionResolverType.ByTable: case ConnectionResolverType.BySchema: - connectionString = configuration.GetConnectionString($"{option.ConnectionPrefix}{tenantInfo.Name}"); + connectionString = configuration.GetConnectionString($"{option.ConnectionName}"); break; } return connectionString; diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs index a29029f..57ca892 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs @@ -41,12 +41,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core /// 杩炴帴鍓嶇紑 /// public string ConnectionPrefix { get; set; } - - /// - /// 鏁版嵁搴撲笂涓嬫枃閫夐」鎿嶄綔 - /// - public Action DbContextOptionAction { get; set; } - /// /// 琛ㄥ悕鐢熸垚鍑芥暟 /// @@ -58,8 +52,14 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core public Func SchemaFunc { get; set; } /// - /// 鏁版嵁搴撲笂涓嬫枃璁剧疆鎿嶄綔 + /// 鏁版嵁搴撲笂涓嬫枃閫夐」 /// - public Action, TenantInfo, string, DbContextOptionsBuilder> DbContextSetup { get; set; } + public Action DbContextOptionDelegate; + + /// + /// 鏁版嵁搴撲笂涓嬫枃閰嶇疆 + /// + public Action DbContextConfigDelegate; + } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs index 71d69b2..057bdb9 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs @@ -1,6 +1,8 @@ using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Migrations.Design; +using Microsoft.Extensions.DependencyInjection; namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl { @@ -17,7 +19,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl private readonly IServiceProvider serviceProvider; /// - /// 初始化 TenantBaseDbContext 类的新实例 + /// 初始化 TenantDbContext 类的新实例 /// /// 数据库上下文选项 /// 租户信息 @@ -36,9 +38,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl protected override void OnModelCreating(ModelBuilder modelBuilder) { var builderType = typeof(ITenantEntityBuilder<>).MakeGenericType(this.GetType()); - - ITenantEntityBuilder entityBuilder = (ITenantEntityBuilder)serviceProvider.GetService(builderType); - + ITenantEntityBuilder entityBuilder = (ITenantEntityBuilder)serviceProvider.GetRequiredService(builderType); entityBuilder.UpdateEntities(modelBuilder); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs index e32c417..81c72e6 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs @@ -2,6 +2,7 @@ using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using LingYanAspCoreFramework.Helpers; +using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl @@ -37,23 +38,27 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl public void UpdateEntities(ModelBuilder modelBuilder) { var dbsetProperties = entityScaner.ScanEntityTypes(); - foreach (var property in dbsetProperties) { var entity = modelBuilder.Entity(property.PropertyType); switch (this.tenantOption.ConnectionType) { case ConnectionResolverType.BySchema: - var tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) - ?? property.PropertyName; - var schemaName = this.tenantOption.SchemaFunc?.Invoke(this.tenantInfo) - ?? this.tenantInfo.Name; + if (this.tenantOption.SchemaFunc == null) + { + throw new CommonException(new ResponceBody(63000, "分模式方案不得为空")); + } + var tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) ?? property.PropertyName; + var schemaName = this.tenantOption.SchemaFunc?.Invoke(this.tenantInfo); entity.ToTable(tableName, schemaName); LoggerHelper.DefaultLog($"实体构建表:{tableName},模式:{schemaName}"); break; case ConnectionResolverType.ByTable: - tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) - ?? $"{this.tenantInfo.Name}_{property.PropertyName}"; + if (this.tenantOption.TableNameFunc == null) + { + throw new CommonException(new ResponceBody(63000, "分表方案不得为空")); + } + tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName); entity.ToTable(tableName); LoggerHelper.DefaultLog($"实体构建表:{tableName}"); break; diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs index e5f360c..7cfa4f7 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs @@ -1,4 +1,5 @@ 锘縰sing LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -21,7 +22,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess public override Migration CreateMigration(TypeInfo migrationClass, string activeProvider) { if (activeProvider == null) - throw new ArgumentNullException($"{nameof(activeProvider)} argument is null"); + throw new CommonException(new ResponceBody(63000, $"杩佺Щ鏃,{nameof(activeProvider)}鍙傛暟涓虹┖")); var hasCtorWithSchema = migrationClass .GetConstructor(new[] { typeof(string) }) != null; @@ -32,7 +33,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess instance.ActiveProvider = activeProvider; return instance; } - return base.CreateMigration(migrationClass, activeProvider); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs new file mode 100644 index 0000000..09b6641 --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs @@ -0,0 +1,20 @@ +锘縰sing LingYanAspCoreFramework.Helpers; +using Microsoft.EntityFrameworkCore.Migrations.Design; +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +{ + public class MigrationsByTenantCodeGenerator : CSharpMigrationsGenerator + { + public MigrationsByTenantCodeGenerator(MigrationsCodeGeneratorDependencies dependencies, CSharpMigrationsGeneratorDependencies csharpDependencies) : base(dependencies, csharpDependencies) + { + LoggerHelper.DefaultLog("杩涘叆鍘熺敓浠g爜鐢熸垚鍣"); + } + public override string GenerateMigration(string? migrationNamespace, string migrationName, IReadOnlyList upOperations, IReadOnlyList downOperations) + { + var orgionCode = base.GenerateMigration(migrationNamespace, migrationName, upOperations, downOperations); + LoggerHelper.DefaultLog("杩囩▼鎵ц"+orgionCode); + return orgionCode; + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs index a3870c9..44ec60c 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs @@ -1,6 +1,7 @@ using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess { @@ -30,9 +31,18 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess /// 模型缓存键对象 public override object Create(DbContext context, bool designTime) { - var dbContext = context as TDbContext; - var tenantModelCacheKey = new TenantModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); - return tenantModelCacheKey; + // 如果是设计时环境,返回默认缓存键 + if (designTime) + { + return new ModelCacheKey(context, designTime); + } + else + { + var dbContext = context as TDbContext; + var tenantModelCacheKey = new TenantModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); + return tenantModelCacheKey; + + } } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs index b6daa91..6855de6 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs @@ -1,80 +1,46 @@ using LingYanAspCoreFramework.DbMultiTenants.Core; using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using LingYanAspCoreFramework.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore { - /// - /// MySQL 租户扩展类,提供多租户数据库的注册方法 - /// public static class MySqlTenantExtension { - /// - /// 按连接注册 MySQL 多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 设置操作 - /// 服务集合 - public static IServiceCollection AddMySqlPerConnection(this IServiceCollection services, Action> setupAction) + public static IServiceCollection AddDynamicShardingDataBaseWithMysql(this IServiceCollection services, Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerConnection(CombineSettings(setupAction)); + return services.AddDynamicShardingDataBase(MakeWithDbTypeAndOption(tenantSettingShardingFunc,ConnectionResolverType.ByDatabase)); } - /// - /// 按表注册 MySQL 多租户数据库 - /// - /// 数据库上下文类型 - /// 服务集合 - /// 设置操作 - /// 服务集合 - public static IServiceCollection AddMySqlPerTable(this IServiceCollection services, Action> setupAction) + public static IServiceCollection AddDynamicShardingTableWithMysql(this IServiceCollection services, Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerTable(CombineSettings(setupAction)); + return services.AddDynamicShardingTable(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByTable)); } - - /// - /// 组合设置操作 - /// - /// 数据库上下文类型 - /// 设置操作 - /// 组合后的设置操作 - static Action> CombineSettings(Action> setupAction) - where TDbContext : DbContext, ITenantDbContext + private static Action> MakeWithDbTypeAndOption(Action> tenantSettingShardingFunc, + ConnectionResolverType connectionResolverType) + where TDbContext : DbContext, ITenantDbContext { - Action> settingAction = (settings) => + Action> tenantSettingDbTypeAndOption = (tenantSettings) => { - settings.DbContextSetup = (tenantSettings, tenantInfo, connectionString, optionsBuilder) => + tenantSettings.ConnectionType= connectionResolverType; + tenantSettings.DbType = DbIntegrationType.Mysql; + tenantSettings.DbContextOptionDelegate = (tenantInfo, connectionString, optionsBuilder) => { if (string.IsNullOrEmpty(connectionString)) { throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); - } + } optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => { - builder.TenantBuilderMigration(tenantSettings, tenantInfo); + builder.DbContextBuilderMigration(tenantSettings, tenantInfo); }); }; - settings.DbContextOptionAction = (dbContextOptionsBuilder) => - { - if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) - { - dbContextOptionsBuilder.ReplaceService>(); - dbContextOptionsBuilder.ReplaceService(); - } - }; - settings.DbType = DbIntegrationType.Mysql; - setupAction?.Invoke(settings); + tenantSettingShardingFunc?.Invoke(tenantSettings); }; - return settingAction; + return tenantSettingDbTypeAndOption; } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs index d272770..839af16 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs @@ -13,61 +13,31 @@ namespace Microsoft.EntityFrameworkCore { public static class PostgreTenantExtension { - /// - /// 按连接注册多租户 - /// - /// - /// - /// - /// - public static IServiceCollection AddPostgrePerConnection(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDynamicShardingDataBaseWithPostgreSql(this IServiceCollection services, Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerConnection(CombineSettings(setupAction)); + return services.AddDynamicShardingDataBase(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByDatabase)); } - - /// - /// 按表注册多租户 - /// - /// - /// - /// - /// - public static IServiceCollection AddPostgrePerTable(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDynamicShardingTableWithPostgreSql(this IServiceCollection services, Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerTable(CombineSettings(setupAction)); + return services.AddDynamicShardingTable(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByTable)); } - /// - /// 按模式 - /// - /// - /// - /// - /// - public static IServiceCollection AddPostgrePerSchema(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDynamicShardingSchemaWithPostgreSql(this IServiceCollection services, Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerSchema(CombineSettings(setupAction)); + return services.AddDynamicShardingSchema(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.BySchema)); } - /// - /// 组合租户信息设置 - /// - /// - /// - /// - /// - static Action> CombineSettings( - Action> setupAction = null) - where TDbContext : DbContext, ITenantDbContext + private static Action> MakeWithDbTypeAndOption(Action> tenantSettingShardingFunc, + ConnectionResolverType connectionResolverType) + where TDbContext : DbContext, ITenantDbContext { - Action> settingAction = (settings) => + Action> tenantSettingDbTypeAndOption = (tenantSettings) => { - settings.DbContextSetup = (tenantSettings, tenantInfo, connectionString, optionsBuilder) => + tenantSettings.ConnectionType = connectionResolverType; + tenantSettings.DbType = DbIntegrationType.PostgreSql; + tenantSettings.DbContextOptionDelegate = (tenantInfo, connectionString, optionsBuilder) => { if (string.IsNullOrEmpty(connectionString)) { @@ -75,22 +45,12 @@ namespace Microsoft.EntityFrameworkCore } optionsBuilder.UseNpgsql(connectionString, builder => { - builder.TenantBuilderMigration(tenantSettings, tenantInfo); + builder.DbContextBuilderMigration(tenantSettings, tenantInfo); }); }; - settings.DbContextOptionAction = (dbContextOptionsBuilder) => - { - if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) - { - dbContextOptionsBuilder.ReplaceService>(); - dbContextOptionsBuilder.ReplaceService(); - } - }; - settings.DbType = DbIntegrationType.Postgre; - setupAction?.Invoke(settings); + tenantSettingShardingFunc?.Invoke(tenantSettings); }; - return settingAction; - + return tenantSettingDbTypeAndOption; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs index 4f8b209..83f87e6 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs @@ -1,67 +1,56 @@ using LingYanAspCoreFramework.DbMultiTenants.Core; using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess; using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using LingYanAspCoreFramework.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore { public static class SqlServerTenantExtension { - public static IServiceCollection AddSqlServerPerConnection(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDynamicShardingDataBaseWithSqlServer(this IServiceCollection services, + Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerConnection(CombineSettings(setupAction)); + return services.AddDynamicShardingDataBase(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByDatabase)); } - public static IServiceCollection AddSqlServerPerTable(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDynamicShardingTableWithSqlServer(this IServiceCollection services, + Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerTable(CombineSettings(setupAction)); + return services.AddDynamicShardingTable(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByTable)); } - public static IServiceCollection AddSqlServerPerSchema(this IServiceCollection services, - Action> setupAction = null) + public static IServiceCollection AddDynamicShardingSchemaWithSqlServer(this IServiceCollection services, + Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - return services.AddDbPerSchema(CombineSettings(setupAction)); + return services.AddDynamicShardingSchema(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.BySchema)); } - - static Action> CombineSettings(Action> setupAction = null) + private static Action> MakeWithDbTypeAndOption(Action> tenantSettingShardingFunc, + ConnectionResolverType connectionResolverType) where TDbContext : DbContext, ITenantDbContext { - Action> settingAction = (settings) => + Action> tenantSettingDbTypeAndOption = (tenantSettings) => { - settings.DbContextSetup = (tenantSettings, tenantInfo, connectionString, optionsBuilder) => + tenantSettings.ConnectionType = connectionResolverType; + tenantSettings.DbType = DbIntegrationType.SqlServer; + tenantSettings.DbContextOptionDelegate = (tenantInfo, connectionString, optionsBuilder) => { if (string.IsNullOrEmpty(connectionString)) { throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); - } + } optionsBuilder.UseSqlServer(connectionString, builder => { - builder.TenantBuilderMigration(tenantSettings, tenantInfo); + builder.DbContextBuilderMigration(tenantSettings, tenantInfo); }); }; - settings.DbContextOptionAction = (dbContextOptionsBuilder) => - { - if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) - { - dbContextOptionsBuilder.ReplaceService>(); - dbContextOptionsBuilder.ReplaceService(); - } - }; - settings.DbType = DbIntegrationType.SqlServer; - setupAction?.Invoke(settings); + tenantSettingShardingFunc?.Invoke(tenantSettings); }; - return settingAction; + return tenantSettingDbTypeAndOption; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs index 7824119..0654c91 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs @@ -5,6 +5,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models.Enum None = 0, Mysql = 1, SqlServer = 2, - Postgre = 3, + PostgreSql = 3, } } diff --git a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj index 893a334..2bf7857 100644 --- a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj +++ b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj @@ -33,6 +33,7 @@ + -- Gitee From 5006635d29678eccb0c6f34d29c00fc93a781c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=20=E7=8E=8B?= <1321180895@qq.com> Date: Sun, 30 Mar 2025 19:42:49 +0800 Subject: [PATCH 07/17] =?UTF-8?q?=E5=88=86=E5=BA=93=E5=88=86=E8=A1=A8?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E5=A7=94=E6=89=98=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...oller.cs => DynamicShardingController .cs} | 20 +- LingTest/DbContexts/StoreDbContext.cs | 14 +- LingTest/Entitys/Order.cs | 15 +- LingTest/Entitys/Product.cs | 3 - .../Generator/CombindedConnectionGenerator.cs | 34 - .../Migrations/20250329115203_v1.Designer.cs | 52 ++ LingTest/Migrations/20250329115203_v1.cs | 49 ++ .../Migrations/20250329120406_v2.Designer.cs | 71 ++ LingTest/Migrations/20250329120406_v2.cs | 44 ++ .../Migrations/StoreDbContextModelSnapshot.cs | 68 ++ LingTest/MyDesignTimeServices.cs | 14 + LingTest/RestApiModule.cs | 30 +- LingTest/appsettings.json | 18 +- .../DbMultiTenants/Core/Constans.cs | 14 - .../Core/Extension/TenantCoreExtension.cs | 98 +-- .../Core/Impl/ConnectionGenerator.cs | 40 -- .../Core/Impl/TenantConnectionResolver.cs | 54 +- .../Core/Interface/IConnectionGenerator.cs | 13 - .../Interface/ITenantConnectionResolver.cs | 5 +- .../DbMultiTenants/Core/TenantSettings.cs | 63 -- .../MigrationsByTenantCodeGenerator.cs | 20 - .../DbExtensions/MySqlTenantExtension.cs | 47 -- .../DbExtensions/PostgreTenantExtension.cs | 56 -- .../DbExtensions/SqlServerTenantExtension.cs | 56 -- .../DynamicShardingMigrationAssembly.cs} | 11 +- .../DynamicShardingMigrationGenerator.cs | 121 ++++ .../DynamicShardingModelCacheKey.cs} | 10 +- .../DynamicShardingModelCacheKeyFactory.cs} | 10 +- .../{DbAccess => EFCore}/Impl/EntityScaner.cs | 4 +- .../Impl/TenantDbContext.cs | 9 +- .../Impl/TenantEntityBuilder.cs | 40 +- .../Interface/IEntityScaner.cs | 4 +- .../Interface/ITenantDbContext.cs | 4 +- .../Interface/ITenantEntityBuilder.cs | 2 +- .../Models/Enum/ConnectionResolverType.cs | 28 - ...ationType.cs => ShardingConnectionType.cs} | 2 +- .../DbMultiTenants/Models/TenantOption.cs | 25 +- .../{Core => Models}/TenantSettingsT.cs | 42 +- .../Envs/Configs/AppSetting.json | 2 +- .../Extensions/GeneralExtension.cs | 61 +- .../Extensions/IocExtension.cs | 13 +- .../Extensions/ProjectRegisterExtension.cs | 48 -- .../LingYanAspCoreFramework.csproj | 3 +- .../MultiTenants/IMigrationNamespace.cs | 7 - .../MultiTenants/IShardingBuilder.cs | 9 - .../MultiTenants/ITenantContextAccessor.cs | 7 - .../MultiTenants/ITenantManager.cs | 15 - .../MultiTenants/MigrationExtension.cs | 20 - .../MigrationNamespaceExtension.cs | 48 -- .../MultiDatabaseMigrationsAssembly.cs | 136 ---- .../MultiTenants/MySqlMigrationNamespace.cs | 10 - .../MultiTenants/ShardingBuilder.cs | 123 ---- .../ShardingMySqlMigrationsSqlGenerator.cs | 34 - .../MultiTenants/ShardingTenantOptions.cs | 37 - .../MultiTenants/SharedTypeExtensions.cs | 661 ------------------ .../MultiTenants/TenantContext.cs | 19 - .../MultiTenants/TenantContextAccessor.cs | 13 - .../MultiTenants/TenantExtension.cs | 62 -- .../MultiTenants/TenantManager.cs | 41 -- .../MultiTenants/TenantScope.cs | 17 - .../MultiTenants/TenantSelectMiddleware.cs | 59 -- .../SampleCommonExceptionMiddleware.cs | 2 +- 62 files changed, 608 insertions(+), 2049 deletions(-) rename LingTest/Controllers/{ProductController.cs => DynamicShardingController .cs} (73%) delete mode 100644 LingTest/Generator/CombindedConnectionGenerator.cs create mode 100644 LingTest/Migrations/20250329115203_v1.Designer.cs create mode 100644 LingTest/Migrations/20250329115203_v1.cs create mode 100644 LingTest/Migrations/20250329120406_v2.Designer.cs create mode 100644 LingTest/Migrations/20250329120406_v2.cs create mode 100644 LingTest/Migrations/StoreDbContextModelSnapshot.cs create mode 100644 LingTest/MyDesignTimeServices.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess/MigrationByTenantAssembly.cs => EFCore/DynamicShardingMigrationAssembly.cs} (76%) create mode 100644 LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess/TenantModelCacheKey.cs => EFCore/DynamicShardingModelCacheKey.cs} (76%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess/TenantModelCacheKeyFactory.cs => EFCore/DynamicShardingModelCacheKeyFactory.cs} (71%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess => EFCore}/Impl/EntityScaner.cs (94%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess => EFCore}/Impl/TenantDbContext.cs (88%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess => EFCore}/Impl/TenantEntityBuilder.cs (51%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess => EFCore}/Interface/IEntityScaner.cs (75%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess => EFCore}/Interface/ITenantDbContext.cs (51%) rename LingYanAspCoreFramework/DbMultiTenants/{DbAccess => EFCore}/Interface/ITenantEntityBuilder.cs (90%) delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs rename LingYanAspCoreFramework/DbMultiTenants/Models/Enum/{DbIntegrationType.cs => ShardingConnectionType.cs} (80%) rename LingYanAspCoreFramework/DbMultiTenants/{Core => Models}/TenantSettingsT.cs (38%) delete mode 100644 LingYanAspCoreFramework/MultiTenants/IMigrationNamespace.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/IShardingBuilder.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/ITenantContextAccessor.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/ITenantManager.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/MigrationExtension.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/MigrationNamespaceExtension.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/MultiDatabaseMigrationsAssembly.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/MySqlMigrationNamespace.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/ShardingBuilder.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/ShardingMySqlMigrationsSqlGenerator.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/ShardingTenantOptions.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/SharedTypeExtensions.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/TenantContext.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/TenantContextAccessor.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/TenantExtension.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/TenantManager.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/TenantScope.cs delete mode 100644 LingYanAspCoreFramework/MultiTenants/TenantSelectMiddleware.cs diff --git a/LingTest/Controllers/ProductController.cs b/LingTest/Controllers/DynamicShardingController .cs similarity index 73% rename from LingTest/Controllers/ProductController.cs rename to LingTest/Controllers/DynamicShardingController .cs index 0e048d1..51c38a0 100644 --- a/LingTest/Controllers/ProductController.cs +++ b/LingTest/Controllers/DynamicShardingController .cs @@ -8,13 +8,13 @@ using Microsoft.EntityFrameworkCore; namespace LingTest.Controllers { [ApiController] - [Route("api/[controller]s")] + [Route("api/[controller]/[action]")] [AllowAnonymous] - public class ProductController : ControllerBase + public class DynamicShardingController : ControllerBase { private readonly StoreDbContext storeDbContext; - public ProductController(StoreDbContext storeDbContext) + public DynamicShardingController(StoreDbContext storeDbContext) { this.storeDbContext = storeDbContext; try @@ -29,7 +29,7 @@ namespace LingTest.Controllers } - [HttpPost("")] + [HttpPost] public async Task> Create(Product product) { var rct = await this.storeDbContext.Products.AddAsync(product); @@ -40,17 +40,7 @@ namespace LingTest.Controllers } - [HttpGet("{id}")] - public async Task> Get([FromRoute] int id) - { - - var rct = await this.storeDbContext.Products.FindAsync(id); - - return rct; - - } - - [HttpGet("")] + [HttpGet] public async Task>> Search() { var rct = await this.storeDbContext.Products.ToListAsync(); diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index c0429a5..c70aa6a 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -1,6 +1,5 @@ using LingTest.Entitys; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; @@ -14,15 +13,14 @@ namespace LingTest.DbContexts : base(options, tenant, serviceProvider) { } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); - optionsBuilder.ReplaceService(); } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + } + } } diff --git a/LingTest/Entitys/Order.cs b/LingTest/Entitys/Order.cs index 1475a51..b4a2c97 100644 --- a/LingTest/Entitys/Order.cs +++ b/LingTest/Entitys/Order.cs @@ -1,19 +1,8 @@ -锘縰sing System.ComponentModel.DataAnnotations; +锘縰sing LingYanAspCoreFramework.BaseRoots; namespace LingTest.Entitys { - public class Order + public class Order:BaseEntity { - [Key] - public int Id { get; set; } - - [StringLength(50), Required] - public string Name { get; set; } - - [StringLength(50)] - public string Category { get; set; } - - public double? Price { get; set; } - public string? TestName { get; set; } } } diff --git a/LingTest/Entitys/Product.cs b/LingTest/Entitys/Product.cs index f63337d..e0072a0 100644 --- a/LingTest/Entitys/Product.cs +++ b/LingTest/Entitys/Product.cs @@ -12,8 +12,5 @@ namespace LingTest.Entitys [StringLength(50)] public string Category { get; set; } - - public double? Price { get; set; } - public string? TestName { get; set; } } } diff --git a/LingTest/Generator/CombindedConnectionGenerator.cs b/LingTest/Generator/CombindedConnectionGenerator.cs deleted file mode 100644 index 6baa399..0000000 --- a/LingTest/Generator/CombindedConnectionGenerator.cs +++ /dev/null @@ -1,34 +0,0 @@ -锘縰sing LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; - -namespace LingTest.Generator -{ - //[LYTService(typeof(IConnectionGenerator),ServiceLifetime =ServiceLifetime.Scoped)] - public class CombindedConnectionGenerator : IConnectionGenerator - { - private readonly IConfiguration configuration; - public string TenantKey => ""; - - public CombindedConnectionGenerator(IConfiguration configuration) - { - this.configuration = configuration; - } - - - public string GetConnection(TenantOption option, TenantInfo tenantInfo) - { - var span = tenantInfo.Name.AsSpan(); - if (span.Length > 4 && int.TryParse(span[5].ToString(), out var number)) - { - var connection = configuration.GetConnectionString($"{option.ConnectionPrefix}container{number % 2 + 1}"); - return connection; - } - throw new NotSupportedException("tenant invalid"); - } - - public bool MatchTenantKey(string tenantKey) - { - return true; - } - } -} diff --git a/LingTest/Migrations/20250329115203_v1.Designer.cs b/LingTest/Migrations/20250329115203_v1.Designer.cs new file mode 100644 index 0000000..e05d26d --- /dev/null +++ b/LingTest/Migrations/20250329115203_v1.Designer.cs @@ -0,0 +1,52 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20250329115203_v1")] + partial class v1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/Migrations/20250329115203_v1.cs b/LingTest/Migrations/20250329115203_v1.cs new file mode 100644 index 0000000..8b8fc2e --- /dev/null +++ b/LingTest/Migrations/20250329115203_v1.cs @@ -0,0 +1,49 @@ +锘縰sing Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LingTest.Migrations +{ + /// + public partial class v1 : Migration + { + //鐏电嚂妗嗘灦娉ㄥ叆鍔ㄦ佸弬鏁 + private readonly string shardingKey; + public v1(string _shardingKey) + { + this.shardingKey = _shardingKey; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: $"{this.shardingKey}_Products", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: $"{this.shardingKey}_Products"); + } + } +} diff --git a/LingTest/Migrations/20250329120406_v2.Designer.cs b/LingTest/Migrations/20250329120406_v2.Designer.cs new file mode 100644 index 0000000..6bb2d7b --- /dev/null +++ b/LingTest/Migrations/20250329120406_v2.Designer.cs @@ -0,0 +1,71 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20250329120406_v2")] + partial class v2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/Migrations/20250329120406_v2.cs b/LingTest/Migrations/20250329120406_v2.cs new file mode 100644 index 0000000..9527cf0 --- /dev/null +++ b/LingTest/Migrations/20250329120406_v2.cs @@ -0,0 +1,44 @@ +锘縰sing Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LingTest.Migrations +{ + /// + public partial class v2 : Migration + { + //鐏电嚂妗嗘灦娉ㄥ叆鍔ㄦ佸弬鏁 + private readonly string shardingKey; + public v2(string _shardingKey) + { + this.shardingKey = _shardingKey; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: $"{this.shardingKey}_Orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), + CreateTimeStamp = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: $"{this.shardingKey}_Orders"); + } + } +} diff --git a/LingTest/Migrations/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/StoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..dfbc196 --- /dev/null +++ b/LingTest/Migrations/StoreDbContextModelSnapshot.cs @@ -0,0 +1,68 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations +{ + [DbContext(typeof(StoreDbContext))] + partial class StoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Orders", (string)null); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Products", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/MyDesignTimeServices.cs b/LingTest/MyDesignTimeServices.cs new file mode 100644 index 0000000..f05b380 --- /dev/null +++ b/LingTest/MyDesignTimeServices.cs @@ -0,0 +1,14 @@ +锘縰sing LingYanAspCoreFramework.DbMultiTenants.EFCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Migrations.Design; + +namespace LingTest +{ + public class MyDesignTimeServices : IDesignTimeServices + { + public void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + } + } +} diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index 4495e0b..08c502a 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -1,7 +1,8 @@ 锘縰sing LingTest.DbContexts; using LingYanAspCoreFramework.BaseRoots; using LingYanAspCoreFramework.DbMultiTenants.Core; -using Microsoft.EntityFrameworkCore; +using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; namespace LingTest { @@ -12,24 +13,23 @@ namespace LingTest //鍒嗗簱鏂规($DynamicDataBaseName)-(杩佺Щ琛)-锛$DynamicDataBaseName_杩炴帴锛 //鍒嗚〃鏂规($DynamicTableName)-($DynamicTableName_杩佺Щ琛)-锛堣繛鎺ワ級 //鍒嗗簱鍒嗚〃($DynamicDataBaseName-$DynamicTableName)-锛$DynamicTableName_杩佺Щ琛級-锛$DynamicDataBaseNam_杩炴帴锛 - services.Services.AddDynamicShardingSchemaWithSqlServer(settings => + services.Services.AddDynamicSharding(settings => { - settings.ConnectionName = "sqlserver_default"; - //settings.TableNameFunc = (tenantInfo, tableName) => + + settings.ConnectionNameFunc = (serviceProvider, tenantInfo) => + { + return new Tuple(ShardingConnectionType.Mysql, $"server=192.168.188.128;port=3306;database=test777;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;"); + }; + //mysql-mariadb鏄病鏈夋ā寮忥紝sqlserver\pgsql + //settings.SchemaFunc = (tenantinfo) => //{ - // return $"{tenantInfo?.Name ?? "default"}_{tableName}"; + // return ""; //}; - settings.SchemaFunc = (tenantInfo) => - { - if (string.IsNullOrEmpty(tenantInfo?.Name)) - { - return $"dbo"; - } - else - { - return $"dbo.{tenantInfo?.Name}"; - } + settings.TableNameFunc = (tenantinfo, tablename) => + { + return !string.IsNullOrEmpty(tenantinfo?.Name) ? $"{tenantinfo.Name}_{tablename}" : tablename; }; + }); } diff --git a/LingTest/appsettings.json b/LingTest/appsettings.json index 685f43b..f38d2f4 100644 --- a/LingTest/appsettings.json +++ b/LingTest/appsettings.json @@ -7,17 +7,17 @@ }, "ConnectionStrings": { //mysql杩炴帴妗堜緥 - "mysql_default": "server=192.168.148.131;port=3306;database=multi_tenant_default;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - "mysql_store1": "server=192.168.148.131;port=3306;database=multi_tenant_store1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - "mysql_store2": "server=192.168.148.131;port=3306;database=multi_tenant_store2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + "mysql_default": "server=192.168.188.128;port=3306;database=multi_tenant_default;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + "mysql_store1": "server=192.168.188.128;port=3306;database=multi_tenant_store1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", + "mysql_store2": "server=192.168.188.128;port=3306;database=multi_tenant_store2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", //sqlserver杩炴帴妗堜緥 - "sqlserver_default": "Server=192.168.148.131;Database=multi_tenant_default;User Id=sa;Password=12345678;Encrypt=False;", - "sqlserver_store1": "Server=192.168.148.131;Database=multi_tenant_store1;User Id=sa;Password=12345678;Encrypt=False;", - "sqlserver_store2": "Server=192.168.148.131;Database=multi_tenant_store2;User Id=sa;Password=12345678;Encrypt=False;", + "sqlserver_default": "Server=192.168.188.128;Database=multi_tenant_default;User Id=sa;Password=12345678;Encrypt=False;", + "sqlserver_store1": "Server=192.168.188.128;Database=multi_tenant_store1;User Id=sa;Password=12345678;Encrypt=False;", + "sqlserver_store2": "Server=192.168.188.128;Database=multi_tenant_store2;User Id=sa;Password=12345678;Encrypt=False;", //pgsql 杩炴帴妗堜緥 - "postgre_default": "Server=192.168.148.131;Port=5432;Database=multi_tenant_default;User Id=postgres;Password=12345678;", - "postgre_store1": "Server=192.168.148.131;Port=5432;Database=multi_tenant_store1;User Id=postgres;Password=12345678;", - "postgre_store2": "Server=192.168.148.131;Port=5432;Database=multi_tenant_store2;User Id=postgres;Password=12345678;" + "postgre_default": "Server=192.168.188.128;Port=5432;Database=multi_tenant_default;User Id=postgres;Password=12345678;", + "postgre_store1": "Server=192.168.188.128;Port=5432;Database=multi_tenant_store1;User Id=postgres;Password=12345678;", + "postgre_store2": "Server=192.168.188.128;Port=5432;Database=multi_tenant_store2;User Id=postgres;Password=12345678;" }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs deleted file mode 100644 index a566ff6..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Constans.cs +++ /dev/null @@ -1,14 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core -{ - public class Constants - { - internal static List specialConnectionTypes = new List - { - ConnectionResolverType.ByTable, - ConnectionResolverType.BySchema - }; - - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs index abfea63..7ea0a76 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs @@ -1,11 +1,11 @@ using LingYanAspCoreFramework.DbMultiTenants.Core.Impl; using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.EFCore; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Helpers; +using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -16,24 +16,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension { public static class TenantCoreExtension { - internal static IServiceCollection AddDynamicShardingDataBase(this IServiceCollection services, Action> makeWithDbTypeAndOption) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicSharding(MakeWithShardingTypeAndConfig(makeWithDbTypeAndOption)); - } - - internal static IServiceCollection AddDynamicShardingTable(this IServiceCollection services, Action> makeWithDbTypeAndOption) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicSharding(MakeWithShardingTypeAndConfig(makeWithDbTypeAndOption)); - } - - internal static IServiceCollection AddDynamicShardingSchema(this IServiceCollection services, Action> makeWithDbTypeAndOption) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicSharding(MakeWithShardingTypeAndConfig(makeWithDbTypeAndOption)); - } - private static IServiceCollection AddDynamicSharding(this IServiceCollection services, Func> makeTenantSettingsFunc) + public static IServiceCollection AddDynamicSharding(this IServiceCollection services, Action> makeTenantSettingsFunc) where TDbContext : DbContext, ITenantDbContext { //注册基础服务 @@ -43,54 +26,73 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension services.TryAddScoped, TenantConnectionResolver>(); services.TryAddScoped, TenantEntityBuilder>(); services.TryAddScoped, EntityScaner>(); - services.AddScoped(); - services.AddSingleton(makeTenantSettingsFunc); + services.AddSingleton(ConfigShardingConnectionType(makeTenantSettingsFunc)); //注册数据库上下文 services.AddDbContext((serviceProvider, options) => { + //初始配置 var tenantSettingT = serviceProvider.GetRequiredService>(); - var connectionResolver = serviceProvider.GetRequiredService>(); - var tenantInfo = serviceProvider.GetRequiredService(); - var connectionStr = connectionResolver.GetConnection(); - tenantSettingT?.DbContextOptionDelegate?.Invoke(tenantInfo, connectionStr, options); - tenantSettingT?.DbContextConfigDelegate?.Invoke(options); + var connectionResolver = serviceProvider.GetRequiredService>(); + var tuple = connectionResolver.GetConnection(); + tenantSettingT?.DbContextOptionDelegate?.Invoke(tuple.Item1, tuple.Item2, options); }); return services; } - private static Func> MakeWithShardingTypeAndConfig(Action> configure) + private static Func> ConfigShardingConnectionType(Action> tenantSettingShardingFunc) where TDbContext : DbContext, ITenantDbContext { - Func> makeWithShardingTypeAndConfig = (ServiceProvider) => + Func> tenantSettingDbTypeAndOption = (serviceProvider) => { - var settings = new TenantSettings(); - settings.DbContextConfigDelegate = options => + var tenantSettings = new TenantSettings(); + //按照数据库类型进行配置 + tenantSettings.DbContextOptionDelegate = (tenantInfo, tenantOption, optionsBuilder) => { - if (Constants.specialConnectionTypes.Contains(settings.ConnectionType)) + if (string.IsNullOrEmpty(tenantOption.ConnectionName)) { - options.ReplaceService>(); - options.ReplaceService(); + throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); } + switch (tenantOption.ShardingConnectionType) + { + case ShardingConnectionType.Mysql: + optionsBuilder.UseMySql(tenantOption.ConnectionName, ServerVersion.AutoDetect(tenantOption.ConnectionName), builder => + { + builder.DbContextBuilderMigration(tenantInfo, tenantOption); + }); + break; + case ShardingConnectionType.PostgreSql: + optionsBuilder.UseNpgsql(tenantOption.ConnectionName, builder => + { + builder.DbContextBuilderMigration(tenantInfo, tenantOption); + }); + break; + case ShardingConnectionType.SqlServer: + optionsBuilder.UseSqlServer(tenantOption.ConnectionName, builder => + { + builder.DbContextBuilderMigration(tenantInfo, tenantOption); + }); + break; + } + optionsBuilder.ReplaceService>(); + optionsBuilder.ReplaceService(); }; - configure?.Invoke(settings); - return settings; + //配置动态分片方案 + tenantSettingShardingFunc?.Invoke(tenantSettings); + return tenantSettings; }; - return makeWithShardingTypeAndConfig; + return tenantSettingDbTypeAndOption; } - internal static void DbContextBuilderMigration(this RelationalDbContextOptionsBuilder builder, - TenantSettings settings, TenantInfo tenantInfo) - where TDbContext : DbContext, ITenantDbContext + internal static void DbContextBuilderMigration(this RelationalDbContextOptionsBuilder builder, TenantInfo tenantInfo,TenantOption tenantOption) where TBuilder : RelationalDbContextOptionsBuilder where TExtension : RelationalOptionsExtension, new() { - if (settings.ConnectionType == ConnectionResolverType.ByTable) + if (!string.IsNullOrEmpty(tenantOption.SchemaName)) { - builder.MigrationsHistoryTable($"{tenantInfo.Name}__EFMigrationsHistory"); + builder.MigrationsHistoryTable("_EFMigrationHistory", tenantOption.SchemaName); } - if (settings.ConnectionType == ConnectionResolverType.BySchema) + else { - var schemaName = (settings.SchemaFunc?.Invoke(tenantInfo) ?? tenantInfo.Name); - builder.MigrationsHistoryTable("__EFMigrationHistory", $"{schemaName}"); - } + builder.MigrationsHistoryTable($"{tenantInfo.Name}_EFMigrationHistory"); + } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs deleted file mode 100644 index 79093c1..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/ConnectionGenerator.cs +++ /dev/null @@ -1,40 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using Microsoft.Extensions.Configuration; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl -{ - public class ConnectionGenerator : IConnectionGenerator - { - private readonly IConfiguration configuration; - public string TenantKey => "unknow"; - - public ConnectionGenerator(IConfiguration configuration) - { - this.configuration = configuration; - } - - public string GetConnection(TenantOption option, TenantInfo tenantInfo) - { - string connectionString = null; - switch (option.ConnectionType) - { - case ConnectionResolverType.Default: - case ConnectionResolverType.ByDatabase: - connectionString = configuration.GetConnectionString($"{option.ConnectionName}"); - break; - case ConnectionResolverType.ByTable: - case ConnectionResolverType.BySchema: - connectionString = configuration.GetConnectionString($"{option.ConnectionName}"); - break; - } - return connectionString; - } - - public bool MatchTenantKey(string tenantKey) - { - return false; - } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs index 9b56d5f..2c537cd 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs @@ -1,7 +1,6 @@ using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl { @@ -13,8 +12,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl private readonly IServiceProvider serviceProvider; private readonly TenantOption tenantOption; - public TenantConnectionResolver(TenantSettings setting, TenantInfo tenantInfo, - TenantOption tenantOption, IServiceProvider serviceProvider) + public TenantConnectionResolver(TenantSettings setting, TenantInfo tenantInfo, TenantOption tenantOption, IServiceProvider serviceProvider) { this.tenantOption = tenantOption; this.setting = setting; @@ -22,46 +20,22 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl this.serviceProvider = serviceProvider; } - public string GetConnection() - { - var connectionGenerator = this.GetConnectionGenerator(); - var option = GenerateOption(); - return connectionGenerator.GetConnection(option, tenantInfo); - - } - - IConnectionGenerator GetConnectionGenerator() - { - //优先使用设置中的 ConnectionGenerator 委托 - if (this.setting.ConnectionGenerator != null) - { - return this.setting.ConnectionGenerator.Invoke(); - } - //从服务提供程序中获取所有 IConnectionGenerator 实现 - var connectionGenerators = serviceProvider.GetServices(); - //查找匹配的 IConnectionGenerator 实现 - var matchedGenerator = connectionGenerators - .FirstOrDefault(r => r.TenantKey == this.setting.Key || r.MatchTenantKey(this.setting.Key)); - if (matchedGenerator != null) - { - return matchedGenerator; - } - //如果没有匹配的实现,使用 ConnectionGenerator - var defaultConnectionGenerator = serviceProvider.GetService(); - return defaultConnectionGenerator; - } - - TenantOption GenerateOption() + public Tuple GetConnection() { + var connectionTupple = this.setting.ConnectionNameFunc.Invoke(this.serviceProvider, this.tenantInfo); tenantOption.Key = setting.Key; - tenantOption.ConnectionType = setting.ConnectionType; - tenantOption.DbType = setting.DbType; + tenantOption.ShardingConnectionType = connectionTupple.Item1; + tenantOption.ConnectionName = connectionTupple.Item2; tenantOption.TableNameFunc = setting.TableNameFunc; - tenantOption.SchemaFunc = setting.SchemaFunc; - tenantOption.ConnectionPrefix = setting.ConnectionPrefix; - tenantOption.ConnectionName = setting.ConnectionName; - - return this.tenantOption; + if (this.setting.SchemaFunc != null) + { + var schemaName = this.setting.SchemaFunc.Invoke(this.tenantInfo); + if (!string.IsNullOrEmpty(schemaName)) + { + tenantOption.SchemaName = schemaName; + } + } + return new Tuple(this.tenantInfo,this.tenantOption); } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs deleted file mode 100644 index d1f92bf..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/IConnectionGenerator.cs +++ /dev/null @@ -1,13 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface -{ - public interface IConnectionGenerator - { - string TenantKey { get;} - - bool MatchTenantKey(string tenantKey); - - string GetConnection(TenantOption option, TenantInfo tenantInfo); - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs index fa93481..602956a 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs @@ -1,7 +1,10 @@ +using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; + namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface { public interface ITenantConnectionResolver { - string GetConnection(); + Tuple GetConnection(); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs deleted file mode 100644 index 7e2874e..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettings.cs +++ /dev/null @@ -1,63 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using Microsoft.EntityFrameworkCore; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core -{ - /// - /// 表示租户设置的抽象类 - /// - public abstract class TenantSettings - { - /// - /// 租户设置的键 - /// - public string Key { get; set; } - - /// - /// 连接解析器类型 - /// - public ConnectionResolverType ConnectionType { get; set; } - - /// - /// 数据库集成类型 - /// - public DbIntegrationType DbType { get; set; } - - /// - /// 连接生成器函数 - /// - public Func ConnectionGenerator { get; set; } - - /// - /// 连接名称 - /// - public string ConnectionName { get; set; } - - /// - /// 连接前缀 - /// - public string ConnectionPrefix { get; set; } - - /// - /// 数据库上下文选项操作 - /// - public Action DbContextOptionAction { get; set; } - - /// - /// 表名生成函数 - /// - public Func TableNameFunc { get; set; } - - /// - /// 模式生成函数 - /// - public Func SchemaFunc { get; set; } - - /// - /// 数据库上下文设置操作 - /// - public Action DbContextSetup { get; set; } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs deleted file mode 100644 index 09b6641..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationsByTenantCodeGenerator.cs +++ /dev/null @@ -1,20 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Helpers; -using Microsoft.EntityFrameworkCore.Migrations.Design; -using Microsoft.EntityFrameworkCore.Migrations.Operations; - -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess -{ - public class MigrationsByTenantCodeGenerator : CSharpMigrationsGenerator - { - public MigrationsByTenantCodeGenerator(MigrationsCodeGeneratorDependencies dependencies, CSharpMigrationsGeneratorDependencies csharpDependencies) : base(dependencies, csharpDependencies) - { - LoggerHelper.DefaultLog("杩涘叆鍘熺敓浠g爜鐢熸垚鍣"); - } - public override string GenerateMigration(string? migrationNamespace, string migrationName, IReadOnlyList upOperations, IReadOnlyList downOperations) - { - var orgionCode = base.GenerateMigration(migrationNamespace, migrationName, upOperations, downOperations); - LoggerHelper.DefaultLog("杩囩▼鎵ц"+orgionCode); - return orgionCode; - } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs deleted file mode 100644 index 6855de6..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/MySqlTenantExtension.cs +++ /dev/null @@ -1,47 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core; -using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore -{ - public static class MySqlTenantExtension - { - public static IServiceCollection AddDynamicShardingDataBaseWithMysql(this IServiceCollection services, Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingDataBase(MakeWithDbTypeAndOption(tenantSettingShardingFunc,ConnectionResolverType.ByDatabase)); - } - public static IServiceCollection AddDynamicShardingTableWithMysql(this IServiceCollection services, Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingTable(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByTable)); - } - private static Action> MakeWithDbTypeAndOption(Action> tenantSettingShardingFunc, - ConnectionResolverType connectionResolverType) - where TDbContext : DbContext, ITenantDbContext - { - Action> tenantSettingDbTypeAndOption = (tenantSettings) => - { - tenantSettings.ConnectionType= connectionResolverType; - tenantSettings.DbType = DbIntegrationType.Mysql; - tenantSettings.DbContextOptionDelegate = (tenantInfo, connectionString, optionsBuilder) => - { - if (string.IsNullOrEmpty(connectionString)) - { - throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); - } - optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder => - { - builder.DbContextBuilderMigration(tenantSettings, tenantInfo); - }); - }; - tenantSettingShardingFunc?.Invoke(tenantSettings); - }; - return tenantSettingDbTypeAndOption; - } - } - -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs deleted file mode 100644 index 839af16..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/PostgreTenantExtension.cs +++ /dev/null @@ -1,56 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core; -using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore -{ - public static class PostgreTenantExtension - { - public static IServiceCollection AddDynamicShardingDataBaseWithPostgreSql(this IServiceCollection services, Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingDataBase(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByDatabase)); - } - public static IServiceCollection AddDynamicShardingTableWithPostgreSql(this IServiceCollection services, Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingTable(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByTable)); - } - public static IServiceCollection AddDynamicShardingSchemaWithPostgreSql(this IServiceCollection services, Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingSchema(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.BySchema)); - } - - private static Action> MakeWithDbTypeAndOption(Action> tenantSettingShardingFunc, - ConnectionResolverType connectionResolverType) - where TDbContext : DbContext, ITenantDbContext - { - Action> tenantSettingDbTypeAndOption = (tenantSettings) => - { - tenantSettings.ConnectionType = connectionResolverType; - tenantSettings.DbType = DbIntegrationType.PostgreSql; - tenantSettings.DbContextOptionDelegate = (tenantInfo, connectionString, optionsBuilder) => - { - if (string.IsNullOrEmpty(connectionString)) - { - throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); - } - optionsBuilder.UseNpgsql(connectionString, builder => - { - builder.DbContextBuilderMigration(tenantSettings, tenantInfo); - }); - }; - tenantSettingShardingFunc?.Invoke(tenantSettings); - }; - return tenantSettingDbTypeAndOption; - } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs b/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs deleted file mode 100644 index 83f87e6..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/DbExtensions/SqlServerTenantExtension.cs +++ /dev/null @@ -1,56 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core; -using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Models; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore -{ - public static class SqlServerTenantExtension - { - public static IServiceCollection AddDynamicShardingDataBaseWithSqlServer(this IServiceCollection services, - Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingDataBase(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByDatabase)); - } - - public static IServiceCollection AddDynamicShardingTableWithSqlServer(this IServiceCollection services, - Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingTable(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.ByTable)); - } - - public static IServiceCollection AddDynamicShardingSchemaWithSqlServer(this IServiceCollection services, - Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext - { - return services.AddDynamicShardingSchema(MakeWithDbTypeAndOption(tenantSettingShardingFunc, ConnectionResolverType.BySchema)); - } - private static Action> MakeWithDbTypeAndOption(Action> tenantSettingShardingFunc, - ConnectionResolverType connectionResolverType) - where TDbContext : DbContext, ITenantDbContext - { - Action> tenantSettingDbTypeAndOption = (tenantSettings) => - { - tenantSettings.ConnectionType = connectionResolverType; - tenantSettings.DbType = DbIntegrationType.SqlServer; - tenantSettings.DbContextOptionDelegate = (tenantInfo, connectionString, optionsBuilder) => - { - if (string.IsNullOrEmpty(connectionString)) - { - throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); - } - optionsBuilder.UseSqlServer(connectionString, builder => - { - builder.DbContextBuilderMigration(tenantSettings, tenantInfo); - }); - }; - tenantSettingShardingFunc?.Invoke(tenantSettings); - }; - return tenantSettingDbTypeAndOption; - } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs similarity index 76% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs index 7cfa4f7..af22293 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/MigrationByTenantAssembly.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs @@ -1,18 +1,20 @@ -锘縰sing LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +锘縰sing LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; +using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; +using Newtonsoft.Json; using System.Reflection; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore { - public class MigrationByTenantAssembly : MigrationsAssembly + public class DynamicShardingMigrationAssembly : MigrationsAssembly { private readonly DbContext context; - public MigrationByTenantAssembly(ICurrentDbContext currentContext, + public DynamicShardingMigrationAssembly(ICurrentDbContext currentContext, IDbContextOptions options, IMigrationsIdGenerator idGenerator, IDiagnosticsLogger logger) : base(currentContext, options, idGenerator, logger) @@ -29,6 +31,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext) { + LoggerHelper.DefaultLog($"杩愯鏃惰縼绉讳紶鍙倇tenantDbContext?.Tenant?.Name}"); var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.Tenant?.Name); instance.ActiveProvider = activeProvider; return instance; diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs new file mode 100644 index 0000000..628ed8b --- /dev/null +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs @@ -0,0 +1,121 @@ +锘縰sing LingYanAspCoreFramework.Extensions; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations.Design; +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore +{ + public class DynamicShardingMigrationGenerator : CSharpMigrationsGenerator + { + + public DynamicShardingMigrationGenerator(MigrationsCodeGeneratorDependencies dependencies, + CSharpMigrationsGeneratorDependencies csharpDependencies) : base(dependencies, csharpDependencies) + { + } + public override string GenerateMigration(string migrationNamespace, string migrationName, IReadOnlyList upOperations, + IReadOnlyList downOperations) + { + // 鏋勯犲櫒鍙傛暟 + string ctorStr = "shardingKey"; + //杩佺Щ鐢熸垚鍣ㄨ繍琛屽墠缃坊鍔犺〃鍚嶅崰浣嶇 + GernateBeforeUpDownTableName(ctorStr, upOperations, downOperations); + //杩佺Щ鐢熸垚鍣ㄨ繍琛 + var generatedCode = base.GenerateMigration(migrationNamespace, migrationName, upOperations, downOperations); + //鏋勯犲櫒浠g爜鎻掑叆杩佺Щ浠g爜 + generatedCode = GernateAfterCtor(ctorStr, migrationName, generatedCode); + //鏇挎崲琛ㄥ悕涓殑鍗犱綅绗 + generatedCode = generatedCode.Replace($"\"${ctorStr}", $"$\"{{this.{ctorStr}}}"); + return generatedCode; + } + private void GernateBeforeUpDownTableName(string ctorStr, + IReadOnlyList upOperations, + IReadOnlyList downOperations) + { + // 涓 upOperations 涓厤缃姩鎬佽〃鍚 + foreach (var operation in upOperations) + { + if (operation is CreateTableOperation createTableOperation) + { + createTableOperation.Name = GernateBeforeTableName(ctorStr, createTableOperation.Name); + } + else if (operation is RenameTableOperation renameTableOperation) + { + renameTableOperation.NewName = GernateBeforeTableName(ctorStr, renameTableOperation.NewName); + } + else if (operation is DropTableOperation dropTableOperation) + { + dropTableOperation.Name = GernateBeforeTableName(ctorStr, dropTableOperation.Name); + } + else if (operation is AlterTableOperation alterTableOperation) + { + alterTableOperation.Name = GernateBeforeTableName(ctorStr, alterTableOperation.Name); + } + else + { + var columnTableName = operation.GetPropertyValue("Table"); + if (!string.IsNullOrEmpty(columnTableName)) + { + operation.SetPropertyValue("Table", GernateBeforeTableName(ctorStr, columnTableName)); + } + } + } + + // 涓 downOperations 涓厤缃姩鎬佽〃鍚 + foreach (var operation in downOperations) + { + if (operation is CreateTableOperation createTableOperation) + { + createTableOperation.Name = GernateBeforeTableName(ctorStr, createTableOperation.Name); + } + else if (operation is RenameTableOperation renameTableOperation) + { + renameTableOperation.NewName = GernateBeforeTableName(ctorStr, renameTableOperation.NewName); + } + else if (operation is DropTableOperation dropTableOperation) + { + dropTableOperation.Name = GernateBeforeTableName(ctorStr, dropTableOperation.Name); + } + else if (operation is AlterTableOperation alterTableOperation) + { + alterTableOperation.Name = GernateBeforeTableName(ctorStr, alterTableOperation.Name); + } + else + { + var columnTableName = operation.GetPropertyValue("Table"); + if (!string.IsNullOrEmpty(columnTableName)) + { + operation.SetPropertyValue("Table", GernateBeforeTableName(ctorStr, columnTableName)); + } + } + } + // 鍏朵粬绫诲瀷鐨勬搷浣滃彲浠ュ湪 + //鐢熸垚琛ㄦ槑鍗犱綅绗 + string GernateBeforeTableName(string ctorKey, string tableName) + { + return $"${ctorKey}_{tableName}"; + } + } + private string GernateAfterCtor(string ctorStr, string migrationName, string generatedCode) + { + + IndentedStringBuilder dynamicShardingCodeBuilder = new IndentedStringBuilder(); + dynamicShardingCodeBuilder.IncrementIndent().IncrementIndent().AppendLine(); + dynamicShardingCodeBuilder.AppendLine("//鐏电嚂妗嗘灦娉ㄥ叆鍔ㄦ佸弬鏁"); + dynamicShardingCodeBuilder.AppendLine($"private readonly string {ctorStr};"); + dynamicShardingCodeBuilder.AppendLine($"public {migrationName}(string _{ctorStr})"); + dynamicShardingCodeBuilder.AppendLine("{"); + dynamicShardingCodeBuilder.IncrementIndent().AppendLine($"this.{ctorStr} = _{ctorStr};").DecrementIndent(); + dynamicShardingCodeBuilder.AppendLine("}"); + var classIndex = generatedCode.IndexOf($"public partial class {migrationName}"); + if (classIndex > 0) + { + var braceIndex = generatedCode.IndexOf("{", classIndex); + if (braceIndex > 0) + { + generatedCode = generatedCode.Insert(braceIndex + 1, dynamicShardingCodeBuilder.ToString()); + } + } + return generatedCode; + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs similarity index 76% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs index 8397602..1fef712 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKey.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs @@ -1,14 +1,14 @@ -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore { /// /// 表示租户模型缓存键的类 /// /// 数据库上下文类型 - internal sealed class TenantModelCacheKey : ModelCacheKey + internal sealed class DynamicShardingModelCacheKey : ModelCacheKey where TDbContext : DbContext, ITenantDbContext { private readonly TDbContext context; @@ -19,7 +19,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess /// /// 数据库上下文 /// 租户标识符 - public TenantModelCacheKey(TDbContext context, string identifier) : base(context) + public DynamicShardingModelCacheKey(TDbContext context, string identifier) : base(context) { this.context = context; this.identifier = identifier; @@ -32,7 +32,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess /// 如果相等,返回 true;否则返回 false protected override bool Equals(ModelCacheKey other) { - var isEqual= base.Equals(other) && (other as TenantModelCacheKey)?.identifier == identifier; + var isEqual = base.Equals(other) && (other as DynamicShardingModelCacheKey)?.identifier == identifier; return isEqual; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs similarity index 71% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs index 44ec60c..c88e8c4 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/TenantModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs @@ -1,22 +1,22 @@ -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore { /// /// 表示租户模型缓存键工厂的类 /// /// 数据库上下文类型 - public sealed class TenantModelCacheKeyFactory : ModelCacheKeyFactory + public sealed class DynamicShardingModelCacheKeyFactory : ModelCacheKeyFactory where TDbContext : DbContext, ITenantDbContext { /// /// 初始化 TenantModelCacheKeyFactory 类的新实例 /// /// 模型缓存键工厂的依赖项 - public TenantModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) + public DynamicShardingModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) { } @@ -39,7 +39,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess else { var dbContext = context as TDbContext; - var tenantModelCacheKey = new TenantModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); + var tenantModelCacheKey = new DynamicShardingModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); return tenantModelCacheKey; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/EntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/EntityScaner.cs similarity index 94% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/EntityScaner.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/EntityScaner.cs index bdd2369..046595c 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/EntityScaner.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/EntityScaner.cs @@ -1,9 +1,9 @@ -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; using System.Reflection; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl { /// /// 简单实体扫描器类,用于扫描数据库上下文中的实体类型 diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantDbContext.cs similarity index 88% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantDbContext.cs index 057bdb9..2d510da 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantDbContext.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantDbContext.cs @@ -1,10 +1,10 @@ -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations.Design; using Microsoft.Extensions.DependencyInjection; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl { /// /// 表示租户基础数据库上下文的抽象类 @@ -28,16 +28,15 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl : base(options) { this.serviceProvider = serviceProvider; - this.Tenant = tenant; + Tenant = tenant; } - /// /// 配置模型 /// /// 模型构建器 protected override void OnModelCreating(ModelBuilder modelBuilder) { - var builderType = typeof(ITenantEntityBuilder<>).MakeGenericType(this.GetType()); + var builderType = typeof(ITenantEntityBuilder<>).MakeGenericType(GetType()); ITenantEntityBuilder entityBuilder = (ITenantEntityBuilder)serviceProvider.GetRequiredService(builderType); entityBuilder.UpdateEntities(modelBuilder); } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs similarity index 51% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs index 81c72e6..93bd5cc 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Impl/TenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs @@ -1,11 +1,11 @@ -using LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface; +using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; using LingYanAspCoreFramework.DbMultiTenants.Models; using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl { /// /// 表示租户实体构建器的类 @@ -41,30 +41,20 @@ namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Impl foreach (var property in dbsetProperties) { var entity = modelBuilder.Entity(property.PropertyType); - switch (this.tenantOption.ConnectionType) + if (tenantOption.TableNameFunc == null) { - case ConnectionResolverType.BySchema: - if (this.tenantOption.SchemaFunc == null) - { - throw new CommonException(new ResponceBody(63000, "分模式方案不得为空")); - } - var tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName) ?? property.PropertyName; - var schemaName = this.tenantOption.SchemaFunc?.Invoke(this.tenantInfo); - entity.ToTable(tableName, schemaName); - LoggerHelper.DefaultLog($"实体构建表:{tableName},模式:{schemaName}"); - break; - case ConnectionResolverType.ByTable: - if (this.tenantOption.TableNameFunc == null) - { - throw new CommonException(new ResponceBody(63000, "分表方案不得为空")); - } - tableName = this.tenantOption.TableNameFunc?.Invoke(this.tenantInfo, property.PropertyName); - entity.ToTable(tableName); - LoggerHelper.DefaultLog($"实体构建表:{tableName}"); - break; - default: - break; + throw new CommonException(new ResponceBody(63000, "表名不得为空")); } + var tableName = tenantOption.TableNameFunc?.Invoke(tenantInfo, property.PropertyName) ?? property.PropertyName; + if (!string.IsNullOrEmpty(tenantOption.SchemaName)) + { + entity.ToTable(tableName, tenantOption.SchemaName); + } + else + { + entity.ToTable(tableName); + } + } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/IEntityScaner.cs similarity index 75% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/IEntityScaner.cs index 03c5aea..de6be64 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/IEntityScaner.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/IEntityScaner.cs @@ -1,11 +1,11 @@ using LingYanAspCoreFramework.DbMultiTenants.Models; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface { public interface IEntityScaner where TDbContext : DbContext - { + { IList ScanEntityTypes(); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs similarity index 51% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs index 18fd30f..ac1d99a 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantDbContext.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs @@ -1,9 +1,9 @@ using LingYanAspCoreFramework.DbMultiTenants.Models; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface { public interface ITenantDbContext { - TenantInfo Tenant { get;} + TenantInfo Tenant { get; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs similarity index 90% rename from LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs rename to LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs index a0a0b93..90ccc06 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/DbAccess/Interface/ITenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.DbAccess.Interface +namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface { /// /// 表示租户实体构建器的接口 diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs deleted file mode 100644 index 16e63ba..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ConnectionResolverType.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LingYanAspCoreFramework.DbMultiTenants.Models.Enum -{ - /// - /// 表示连接解析器类型的枚举 - /// - public enum ConnectionResolverType - { - /// - /// 默认连接解析器类型 - /// - Default = 0, - - /// - /// 按数据库解析连接 - /// - ByDatabase = 1, - - /// - /// 按表解析连接 - /// - ByTable = 2, - - /// - /// 按模式解析连接 - /// - BySchema = 3 - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ShardingConnectionType.cs similarity index 80% rename from LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs rename to LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ShardingConnectionType.cs index 0654c91..4cc4c13 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/DbIntegrationType.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ShardingConnectionType.cs @@ -1,6 +1,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models.Enum { - public enum DbIntegrationType + public enum ShardingConnectionType { None = 0, Mysql = 1, diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs index 1feb4a9..1e023e4 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs @@ -11,35 +11,22 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models /// 租户选项的键 /// public string Key { get; set; } - /// - /// 连接解析器类型 + /// 数据库类型 /// - public ConnectionResolverType ConnectionType { get; set; } - + internal ShardingConnectionType ShardingConnectionType { get; set; } /// - /// 数据库集成类型 + /// 数据库连接字符串 /// - public DbIntegrationType DbType { get; set; } + internal string ConnectionName { get; set; } /// - /// 连接名称 + /// 新模式名称 /// - public string ConnectionName { get; set; } - - /// - /// 连接前缀 - /// - public string ConnectionPrefix { get; set; } - + internal string SchemaName { get; set; } /// /// 表名生成委托 /// public Func TableNameFunc { get; set; } - - /// - /// 模式生成委托 - /// - public Func SchemaFunc { get; set; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantSettingsT.cs similarity index 38% rename from LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs rename to LingYanAspCoreFramework/DbMultiTenants/Models/TenantSettingsT.cs index 57ca892..343528b 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantSettingsT.cs +++ b/LingYanAspCoreFramework/DbMultiTenants/Models/TenantSettingsT.cs @@ -1,9 +1,7 @@ -锘縰sing LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +锘縰sing LingYanAspCoreFramework.DbMultiTenants.Models.Enum; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.Core +namespace LingYanAspCoreFramework.DbMultiTenants.Models { /// /// 澶氱鎴锋暟鎹簱涓婁笅鏂囩殑鍔ㄦ侀厤缃 @@ -15,32 +13,12 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core /// /// 绉熸埛璁剧疆鐨勯敭 /// - public string Key { get; set; } - - /// - /// 杩炴帴瑙f瀽鍣ㄧ被鍨 - /// - internal ConnectionResolverType ConnectionType { get; set; } - - /// - /// 鏁版嵁搴撻泦鎴愮被鍨 - /// - internal DbIntegrationType DbType { get; set; } - - /// - /// 杩炴帴鐢熸垚鍣ㄥ嚱鏁 - /// - public Func ConnectionGenerator { get; set; } - + public string Key { get; set; } /// - /// 杩炴帴鍚嶇О + /// 鏁版嵁搴撹繛鎺ュ瓧绗︿覆鐢熸垚鍑芥暟 /// - public string ConnectionName { get; set; } + public Func> ConnectionNameFunc { get; set; } - /// - /// 杩炴帴鍓嶇紑 - /// - public string ConnectionPrefix { get; set; } /// /// 琛ㄥ悕鐢熸垚鍑芥暟 /// @@ -52,14 +30,8 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Core public Func SchemaFunc { get; set; } /// - /// 鏁版嵁搴撲笂涓嬫枃閫夐」 + /// 鏁版嵁搴撲笂涓嬫枃閫夐」閰嶇疆 /// - public Action DbContextOptionDelegate; - - /// - /// 鏁版嵁搴撲笂涓嬫枃閰嶇疆 - /// - public Action DbContextConfigDelegate; - + public Action DbContextOptionDelegate; } } diff --git a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json index a37e638..852722d 100644 --- a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json +++ b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json @@ -19,7 +19,7 @@ "RedisCofigModel": { "Pattern": "Single", // 鍗曟満妯″紡 - "Single": "192.168.148.131:6379,defaultDatabase=0,password=12345678,prefix=ling", + "Single": "192.168.188.128:6379,defaultDatabase=0,password=,prefix=ling", //鍝ㄥ叺妯″紡 "SentinelModel": { "Master": "longyumaster,password=,prefix=", diff --git a/LingYanAspCoreFramework/Extensions/GeneralExtension.cs b/LingYanAspCoreFramework/Extensions/GeneralExtension.cs index 2885b49..7b9d350 100644 --- a/LingYanAspCoreFramework/Extensions/GeneralExtension.cs +++ b/LingYanAspCoreFramework/Extensions/GeneralExtension.cs @@ -1,18 +1,14 @@ 锘縰sing LingYanAspCoreFramework.BaseRoots; using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; -using LingYanAspCoreFramework.MultiTenants; using LingYanAspCoreFramework.SampleRoots; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; -using ShardingCore.Extensions; using System.Reflection; using System.Text; using System.Xml; @@ -30,11 +26,29 @@ namespace LingYanAspCoreFramework.Extensions /// public static T GetPropertyValue(this object obj, string propertyName) { - PropertyInfo propertyType = obj.GetType().GetProperty(propertyName); - var peropertyValue = propertyType.GetValue(obj); - return (T)peropertyValue; + PropertyInfo propertyInfo = obj.GetType().GetProperty(propertyName); + if (propertyInfo != null) + { + var propertyValue = propertyInfo.GetValue(obj); + if (propertyValue != null) + { + return (T)propertyValue; + } + } + return default(T); + } + /// + ///鍙嶅皠璁剧疆灞炴х殑鍊 + /// + /// + /// + /// + /// + public static void SetPropertyValue(this object obj, string propertyName, T value) + { + PropertyInfo propertyInfo = obj.GetType().GetProperty(propertyName); + propertyInfo.SetValue(obj, value); } - /// /// 鍒ゆ柇娉涘瀷鏂规硶 /// @@ -281,37 +295,6 @@ namespace LingYanAspCoreFramework.Extensions ProjectHelper.GenerateControllerXml(actionDescs); return actionDescs; } - - /// - ///1銆侀渶瑕佹湁Migration绯荤粺鐨勭鎴风鐞嗘暟鎹簱杩佺Щ - ///2銆侀渶瑕佹湁Migration绯荤粺鐨勭鎴锋ā鐗堟暟鎹簱杩佺Щ - ///3銆佺郴缁熺殑绉熸埛绠$悊鏁版嵁搴撲笂涓嬫枃 - ///4銆佺郴缁熺鎴锋ā鐗堟暟鎹簱涓婁笅鏂 - ///5銆佸鏋滅郴缁熺鎴锋ā鐗堟暟鎹簱鏈夎櫄鎷熻矾鐢憋紝闇瑕佽櫄鎷熷寲 - /// - /// - /// - /// - public static void AddSysTenant(this IServiceProvider provider, object tid, ShardingTenantOptions shardingTenantOptions) - { - var _shardingBuilder = provider.GetService(); - var _tenantManager = provider.GetService(); - //鍒涘缓杩愯鏃 - var shardingRuntimeContext = _shardingBuilder.Build(shardingTenantOptions); - //娣诲姞绉熸埛淇℃伅 - _tenantManager.AddTenantSharding(tid, shardingRuntimeContext); - //鍒涘缓绉熸埛鐜 - using (_tenantManager.CreateScope(tid)) - //寮鍚垎鐗囧畾鏃朵换鍔 - using (var scope = provider.CreateScope()) - { - var runtimeContext = _tenantManager.GetCurrentTenantContext().GetShardingRuntimeContext(); - var tenantDbContext = (DbContext)scope.ServiceProvider.GetService(LingYanRuntimeManager.RuntimeCacheModel.TenantTemplateDbContexts.FirstOrDefault()); - tenantDbContext.Database.Migrate(); - runtimeContext.UseAutoTryCompensateTable(); - } - } - /// /// 浠庢寚瀹氱殑 URI 涓嬭浇鏂囦欢骞舵姤鍛婁笅杞借繘搴 /// diff --git a/LingYanAspCoreFramework/Extensions/IocExtension.cs b/LingYanAspCoreFramework/Extensions/IocExtension.cs index ec8e4d5..d6831dd 100644 --- a/LingYanAspCoreFramework/Extensions/IocExtension.cs +++ b/LingYanAspCoreFramework/Extensions/IocExtension.cs @@ -7,7 +7,6 @@ using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Https; using LingYanAspCoreFramework.Models; using LingYanAspCoreFramework.Models.WeChat; -using LingYanAspCoreFramework.MultiTenants; using LingYanAspCoreFramework.OnlinePays; using LingYanAspCoreFramework.SampleRoots; using LingYanAspCoreFramework.UnitOfWork; @@ -174,11 +173,7 @@ namespace LingYanAspCoreFramework.Extensions LoggerHelper.DefaultLog("娉ㄥ唽Redis"); BuilderService.RegisterRedis(LingYanRuntimeManager.RuntimeCacheModel); LoggerHelper.DefaultLog("娉ㄥ唽鍔ㄦ佽矾鐢"); - BuilderService.RegisterDynamicWebApi(); - LoggerHelper.DefaultLog("娉ㄥ唽澶氱鎴锋湇鍔"); - BuilderService.RegisterTenantService(); - LoggerHelper.DefaultLog("娉ㄥ唽澶氱鎴锋ā鐗堟暟鎹笂涓嬫枃"); - BuilderService.RegisterTenantTemplateDbContext(LingYanRuntimeManager.RuntimeCacheModel); + BuilderService.RegisterDynamicWebApi(); LoggerHelper.DefaultLog("娉ㄥ唽HttpClient"); BuilderService.AddHttpClient(); @@ -291,11 +286,7 @@ namespace LingYanAspCoreFramework.Extensions web.UseMiddleware(item); } LoggerHelper.WarnrningLog($"椤圭洰鍚勬ā鍧楀垵濮嬪寲"); - LingYanRuntimeManager.RuntimeCacheModel.ModuleAssemblyBaseLoadingKeyValue.Values.InitModules(app); - LoggerHelper.WarnrningLog($"鍚敤澶氱鎴蜂腑闂翠欢"); - web.UseMiddleware(); - LoggerHelper.WarnrningLog($"澶氱鎴烽厤缃垵濮嬪寲"); - hostService.InitTenant(LingYanRuntimeManager.RuntimeCacheModel); + LingYanRuntimeManager.RuntimeCacheModel.ModuleAssemblyBaseLoadingKeyValue.Values.InitModules(app); LoggerHelper.WarnrningLog($"鍚敤鍏ㄥ眬寮鍚韩浠介獙璇"); web.MapControllers().RequireAuthorization(); LoggerHelper.WarnrningLog($"鍚敤闈欐佹枃浠跺す-鑷畾涔変腑闂翠欢浠ョ伒娲荤殑瀹炵幇閴存潈鎺堟潈"); diff --git a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs index ffec1a0..ffcca6e 100644 --- a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs +++ b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs @@ -3,7 +3,6 @@ using LingYanAspCoreFramework.Attributes; using LingYanAspCoreFramework.DynamicApis; using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; -using LingYanAspCoreFramework.MultiTenants; using LingYanAspCoreFramework.SampleRoots; using LingYanAspCoreFramework.UnitOfWork; using Microsoft.AspNetCore.Authorization; @@ -12,10 +11,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using ShardingCore; -using ShardingCore.Core.RuntimeContexts; using System.Reflection; -using Yitter.IdGenerator; namespace LingYanAspCoreFramework.Extensions { @@ -231,49 +227,5 @@ namespace LingYanAspCoreFramework.Extensions }); } - /// - /// 娉ㄥ唽澶氱鎴锋ā鐗堟暟鎹笂涓嬫枃 - /// - /// - internal static void RegisterTenantTemplateDbContext(this IServiceCollection BuilderService, RuntimeCacheModel lYBuilderRuntimeModel) - { - //Add-Migration InitialCreate -Context TenantDbContext -OutputDir Migrations\SqlServer -Args "--provider SqlServer" - //Add-Migration InitialCreate -Context TenantDbContext -OutputDir Migrations\MySql -Args "--provider MySql" - //鍙嶅皠鑾峰彇娉ㄥ唽鏁版嵁搴撲笂涓嬫枃鎵╁睍 - var addDbContextMethod = typeof(EntityFrameworkServiceCollectionExtensions).GetMethods() - .Where(m => m.Name == "AddDbContext" && m.GetParameters()[1].ParameterType == typeof(Action)) - .FirstOrDefault(); - //鍙嶅皠鑾峰彇鍒濆鍖栧绉熸埛鎵╁睍 - var useDefaultShardingMethod = typeof(ShardingCoreExtension).GetMethods() - .Where(m => m.Name == "UseDefaultSharding" && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType == typeof(DbContextOptionsBuilder) - && m.GetParameters()[1].ParameterType == typeof(IShardingRuntimeContext)).FirstOrDefault(); - //娉ㄥ唽鏁版嵁搴撲笂涓嬫枃鐨勫鎵樸佸鏋滄湁涓婁笅鏂囬偅涔堝垱寤虹鎴穌bcontext鍚﹀垯灏辨槸鍚姩鍛戒护Add-Migration - var optionsAction = new Action((provider, optionBuilder) => - { - var tenantManager = provider.GetService(); - var currentTenantContext = tenantManager.GetCurrentTenantContext(); - if (currentTenantContext != null) - { - var shardingRuntimeContext = currentTenantContext.GetShardingRuntimeContext(); - useDefaultShardingMethod.MakeGenericMethod(lYBuilderRuntimeModel.TenantTemplateDbContexts.FirstOrDefault()).Invoke(null, new object[] { optionBuilder, shardingRuntimeContext }); - } - }); - //娉ㄥ唽澶氱鎴锋暟鎹笂涓嬫枃妯$増 - if (lYBuilderRuntimeModel.TenantTemplateDbContexts.FirstOrDefault() != null) - { - addDbContextMethod.MakeGenericMethod(lYBuilderRuntimeModel.TenantTemplateDbContexts.FirstOrDefault()).Invoke(null, new object[] { BuilderService, optionsAction, ServiceLifetime.Scoped, ServiceLifetime.Scoped }); - } - } - - /// - /// 娉ㄥ唽澶氱鎴风鐞 - /// - /// - internal static void RegisterTenantService(this IServiceCollection BuilderService) - { - BuilderService.AddSingleton(); - BuilderService.AddSingleton(); - BuilderService.AddSingleton(); - } } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj index 2bf7857..3ffc75f 100644 --- a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj +++ b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj @@ -33,12 +33,11 @@ - + - diff --git a/LingYanAspCoreFramework/MultiTenants/IMigrationNamespace.cs b/LingYanAspCoreFramework/MultiTenants/IMigrationNamespace.cs deleted file mode 100644 index eab78d0..0000000 --- a/LingYanAspCoreFramework/MultiTenants/IMigrationNamespace.cs +++ /dev/null @@ -1,7 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.MultiTenants -{ - public interface IMigrationNamespace - { - string GetNamespace(); - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/IShardingBuilder.cs b/LingYanAspCoreFramework/MultiTenants/IShardingBuilder.cs deleted file mode 100644 index 9115798..0000000 --- a/LingYanAspCoreFramework/MultiTenants/IShardingBuilder.cs +++ /dev/null @@ -1,9 +0,0 @@ -锘縰sing ShardingCore.Core.RuntimeContexts; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public interface IShardingBuilder - { - IShardingRuntimeContext Build(ShardingTenantOptions tenantOptions); - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/ITenantContextAccessor.cs b/LingYanAspCoreFramework/MultiTenants/ITenantContextAccessor.cs deleted file mode 100644 index 0df975b..0000000 --- a/LingYanAspCoreFramework/MultiTenants/ITenantContextAccessor.cs +++ /dev/null @@ -1,7 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.MultiTenants -{ - public interface ITenantContextAccessor - { - TenantContext? TenantContext { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/ITenantManager.cs b/LingYanAspCoreFramework/MultiTenants/ITenantManager.cs deleted file mode 100644 index 7978220..0000000 --- a/LingYanAspCoreFramework/MultiTenants/ITenantManager.cs +++ /dev/null @@ -1,15 +0,0 @@ -锘縰sing ShardingCore.Core.RuntimeContexts; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public interface ITenantManager - { - bool AddTenantSharding(object tenantId, IShardingRuntimeContext shardingRuntimeContext); - - TenantScope CreateScope(object tenantId); - - List GetAll(); - - TenantContext GetCurrentTenantContext(); - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/MigrationExtension.cs b/LingYanAspCoreFramework/MultiTenants/MigrationExtension.cs deleted file mode 100644 index db7f8c2..0000000 --- a/LingYanAspCoreFramework/MultiTenants/MigrationExtension.cs +++ /dev/null @@ -1,20 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public static class MigrationExtension - { - public static DbContextOptionsBuilder UseMigrationNamespace(this DbContextOptionsBuilder optionsBuilder, IMigrationNamespace migrationNamespace) - { - var shardingWrapExtension = optionsBuilder.CreateOrGetExtension(migrationNamespace); - ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(shardingWrapExtension); - return optionsBuilder; - } - - private static MigrationNamespaceExtension CreateOrGetExtension( - this DbContextOptionsBuilder optionsBuilder, IMigrationNamespace migrationNamespace) - => optionsBuilder.Options.FindExtension() ?? - new MigrationNamespaceExtension(migrationNamespace); - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/MigrationNamespaceExtension.cs b/LingYanAspCoreFramework/MultiTenants/MigrationNamespaceExtension.cs deleted file mode 100644 index 1a556f9..0000000 --- a/LingYanAspCoreFramework/MultiTenants/MigrationNamespaceExtension.cs +++ /dev/null @@ -1,48 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.DependencyInjection; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class MigrationNamespaceExtension : IDbContextOptionsExtension - { - public IMigrationNamespace MigrationNamespace { get; } - - public MigrationNamespaceExtension(IMigrationNamespace migrationNamespace) - { - MigrationNamespace = migrationNamespace; - } - - public void ApplyServices(IServiceCollection services) - { - services.AddSingleton(sp => MigrationNamespace); - } - - public void Validate(IDbContextOptions options) - { - } - - public DbContextOptionsExtensionInfo Info => new MigrationNamespaceExtensionInfo(this); - - private class MigrationNamespaceExtensionInfo : DbContextOptionsExtensionInfo - { - private readonly MigrationNamespaceExtension _migrationNamespaceExtension; - - public MigrationNamespaceExtensionInfo(IDbContextOptionsExtension extension) : base(extension) - { - _migrationNamespaceExtension = (MigrationNamespaceExtension)extension; - } - - public override int GetServiceProviderHashCode() => - _migrationNamespaceExtension.MigrationNamespace.GetNamespace().GetHashCode(); - - public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true; - - public override void PopulateDebugInfo(IDictionary debugInfo) - { - } - - public override bool IsDatabaseProvider => false; - public override string LogFragment => "MigrationNamespaceExtension"; - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/MultiDatabaseMigrationsAssembly.cs b/LingYanAspCoreFramework/MultiTenants/MultiDatabaseMigrationsAssembly.cs deleted file mode 100644 index fcfb046..0000000 --- a/LingYanAspCoreFramework/MultiTenants/MultiDatabaseMigrationsAssembly.cs +++ /dev/null @@ -1,136 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using System.Reflection; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class MultiDatabaseMigrationsAssembly : IMigrationsAssembly - { - public string MigrationNamespace { get; } - private readonly IMigrationsIdGenerator _idGenerator; - private readonly IDiagnosticsLogger _logger; - private IReadOnlyDictionary? _migrations; - private ModelSnapshot? _modelSnapshot; - private readonly Type _contextType; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public MultiDatabaseMigrationsAssembly( - IMigrationNamespace migrationNamespace, - ICurrentDbContext currentContext, - IDbContextOptions options, - IMigrationsIdGenerator idGenerator, - IDiagnosticsLogger logger) - { - _contextType = currentContext.Context.GetType(); - - var assemblyName = RelationalOptionsExtension.Extract(options)?.MigrationsAssembly; - Assembly = assemblyName == null - ? _contextType.Assembly - : Assembly.Load(new AssemblyName(assemblyName)); - - MigrationNamespace = migrationNamespace.GetNamespace(); - _idGenerator = idGenerator; - _logger = logger; - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IReadOnlyDictionary Migrations - { - get - { - IReadOnlyDictionary Create() - { - var result = new SortedList(); - var items - = from t in Assembly.GetConstructibleTypes() - where t.IsSubclassOf(typeof(Migration)) - && t.Namespace.Equals(MigrationNamespace) - && t.GetCustomAttribute()?.ContextType == _contextType - let id = t.GetCustomAttribute()?.Id - orderby id - select (id, t); - foreach (var (id, t) in items) - { - if (id == null) - { - _logger.MigrationAttributeMissingWarning(t); - - continue; - } - - result.Add(id, t); - } - - return result; - } - - return _migrations ??= Create(); - } - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual ModelSnapshot? ModelSnapshot - => _modelSnapshot ??= (from t in Assembly.GetConstructibleTypes() - where t.IsSubclassOf(typeof(ModelSnapshot)) - && MigrationNamespace.Equals(t?.Namespace) - && t.GetCustomAttribute()?.ContextType == _contextType - select (ModelSnapshot)Activator.CreateInstance(t.AsType())!) - .FirstOrDefault(); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual Assembly Assembly { get; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual string? FindMigrationId(string nameOrId) - => Migrations.Keys - .Where( - _idGenerator.IsValidId(nameOrId) - // ReSharper disable once ImplicitlyCapturedClosure - ? id => string.Equals(id, nameOrId, StringComparison.OrdinalIgnoreCase) - : id => string.Equals(_idGenerator.GetName(id), nameOrId, StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual Migration CreateMigration(TypeInfo migrationClass, string activeProvider) - { - Console.WriteLine(migrationClass.FullName); - - var migration = (Migration)Activator.CreateInstance(migrationClass.AsType())!; - migration.ActiveProvider = activeProvider; - - return migration; - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/MySqlMigrationNamespace.cs b/LingYanAspCoreFramework/MultiTenants/MySqlMigrationNamespace.cs deleted file mode 100644 index eaed21a..0000000 --- a/LingYanAspCoreFramework/MultiTenants/MySqlMigrationNamespace.cs +++ /dev/null @@ -1,10 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.MultiTenants -{ - public class MySqlMigrationNamespace : IMigrationNamespace - { - public string GetNamespace() - { - return "ShardingCoreMultiTenantSys.Migrations.MySql"; - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/ShardingBuilder.cs b/LingYanAspCoreFramework/MultiTenants/ShardingBuilder.cs deleted file mode 100644 index 0725282..0000000 --- a/LingYanAspCoreFramework/MultiTenants/ShardingBuilder.cs +++ /dev/null @@ -1,123 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Attributes; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using ShardingCore; -using ShardingCore.Core.RuntimeContexts; -using ShardingCore.Core.ServiceProviders; -using ShardingCore.Core.ShardingConfigurations; -using ShardingCore.Core.ShardingConfigurations.Abstractions; -using ShardingCore.TableExists; -using ShardingCore.TableExists.Abstractions; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class ShardingBuilder : IShardingBuilder - { - private readonly IServiceProvider _serviceProvider; - - public ShardingBuilder(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public IShardingRuntimeContext Build(ShardingTenantOptions tenantOptions) - { - Type shardingRuntimeBuilderType = typeof(ShardingRuntimeBuilder<>); - Type dbContextType = LingYanRuntimeManager.RuntimeCacheModel.TenantTemplateDbContexts.FirstOrDefault(); - Type constructedType = shardingRuntimeBuilderType.MakeGenericType(dbContextType); - var shardingRuntimeBuilderInstance = Activator.CreateInstance(constructedType); - // 鍏蜂綋鍖栧悗鐨勭被鍨嬬‘瀹氬悗锛岃幏鍙栧叿浣撳寲鍚庣殑绫诲瀷鐨凪ethodInfo - Type specificType = shardingRuntimeBuilderInstance.GetType(); - //鑾峰彇閰嶇疆璺敱鏂规硶淇℃伅 - var UseRouteConfigMethodInfo = specificType.GetMethods().FirstOrDefault(f => f.Name == "UseRouteConfig" && f.GetParameters()[0].ParameterType == typeof(Action)); - //閰嶇疆璺敱濮旀墭 - var UseRouteConfigDelegate = new Action((option) => - { - if (LingYanRuntimeManager.RuntimeCacheModel.VirtualTableList.Keys.Count > 0) - { - if (tenantOptions.ShardingKeyType == ShardingKeyType.Mod) - { - LingYanRuntimeManager.RuntimeCacheModel.VirtualTableList[ShardingKeyType.Mod].ForEach(modShardingTable => - { - option.AddShardingTableRoute(modShardingTable); - }); - } - if (tenantOptions.ShardingKeyType == ShardingKeyType.Time) - { - LingYanRuntimeManager.RuntimeCacheModel.VirtualTableList[ShardingKeyType.Time].ForEach(modShardingTable => - { - option.AddShardingTableRoute(modShardingTable); - }); - } - } - }); - //鎵ц閰嶇疆 - UseRouteConfigMethodInfo.Invoke(shardingRuntimeBuilderInstance, new object[] { UseRouteConfigDelegate }); - //鍙嶅皠閰嶇疆鏁版嵁搴撴柟娉曚俊鎭 - var UseConfigMethodInfo = specificType.GetMethods().FirstOrDefault(f => f.Name == "UseConfig" && f.GetParameters()[0].ParameterType == typeof(Action)); - //閰嶇疆鏁版嵁搴撳鎵 - var UseConfigDelegate = new Action((provider, option) => - { - option.ThrowIfQueryRouteNotMatch = false; - option.UseShardingQuery((conStr, builder) => - { - if (tenantOptions.DataBaseType == DataBaseType.MYSQL) - { - builder.UseMySql(conStr, MySqlServerVersion.AutoDetect(conStr)) - .UseMigrationNamespace(new MySqlMigrationNamespace()); - } - builder.UseLoggerFactory(provider.GetService()) - .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) - .ReplaceService(); - }); - option.UseShardingTransaction((connection, builder) => - { - if (tenantOptions.DataBaseType == DataBaseType.MYSQL) - { - builder.UseMySql(connection, MySqlServerVersion.AutoDetect(connection.ConnectionString)) - .UseMigrationNamespace(new MySqlMigrationNamespace());//杩佺Щ鍙細鐢╟onnection string鍒涘缓鎵浠ュ彲浠ヤ笉鍔 - } - builder.UseLoggerFactory(provider.GetService()) - .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); - }); - option.AddDefaultDataSource(tenantOptions.DefaultDataSourceName, tenantOptions.DefaultConnectionString); - //杩佺Щ閰嶇疆 - option.UseShardingMigrationConfigure(b => - { - if (tenantOptions.DataBaseType == DataBaseType.MYSQL) - { - b.ReplaceService(); - } - }); - }); - //鎵ц - UseConfigMethodInfo.Invoke(shardingRuntimeBuilderInstance, new object[] { UseConfigDelegate }); - //鍙嶅皠娣诲姞鏈嶅姟閰嶇疆鏂规硶淇℃伅 - var AddServiceConfigureMethodInfo = specificType.GetMethods().FirstOrDefault(f => f.Name == "AddServiceConfigure" && f.GetParameters()[0].ParameterType == typeof(Action)); - //娣诲姞鏈嶅姟閰嶇疆濮旀墭 - var AddServiceConfigureDelegate = new Action((service) => - { - service.AddSingleton(tenantOptions); - }); - //鎵ц - AddServiceConfigureMethodInfo.Invoke(shardingRuntimeBuilderInstance, new object[] { AddServiceConfigureDelegate }); - if (tenantOptions.DataBaseType == DataBaseType.MYSQL) - { - //鍙嶅皠鏇挎崲鏈嶅姟鏂规硶淇℃伅 - var ReplaceServiceMethodInfo = specificType.GetMethod("ReplaceService").MakeGenericMethod(typeof(ITableEnsureManager), typeof(MySqlTableEnsureManager)); - ReplaceServiceMethodInfo.Invoke(shardingRuntimeBuilderInstance, new object[] { ServiceLifetime.Singleton }); - } - if (tenantOptions.DataBaseType == DataBaseType.MSSQL) - { - //鍙嶅皠鏇挎崲鏈嶅姟鏂规硶淇℃伅 - var ReplaceServiceMethodInfo = specificType.GetMethod("ReplaceService").MakeGenericMethod(typeof(ITableEnsureManager), typeof(SqlServerTableEnsureManager)); - ReplaceServiceMethodInfo.Invoke(shardingRuntimeBuilderInstance, new object[] { ServiceLifetime.Singleton }); - } - //鍙嶅皠鏋勫缓鍒嗙墖杩愯鏃舵柟娉曚俊鎭 - var BuildMethodInfo = specificType.GetMethods().FirstOrDefault(w => w.Name == "Build" && w.GetParameters() != null && w.GetParameters().Count() == 1 && w.GetParameters()[0].ParameterType == typeof(IServiceProvider)); - return (IShardingRuntimeContext)BuildMethodInfo.Invoke(shardingRuntimeBuilderInstance, new object[] { _serviceProvider }); - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/ShardingMySqlMigrationsSqlGenerator.cs b/LingYanAspCoreFramework/MultiTenants/ShardingMySqlMigrationsSqlGenerator.cs deleted file mode 100644 index 7623413..0000000 --- a/LingYanAspCoreFramework/MultiTenants/ShardingMySqlMigrationsSqlGenerator.cs +++ /dev/null @@ -1,34 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Migrations.Operations; -using Microsoft.EntityFrameworkCore.Update; -using Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal; -using Pomelo.EntityFrameworkCore.MySql.Migrations; -using ShardingCore.Core.RuntimeContexts; -using ShardingCore.Helpers; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class ShardingMySqlMigrationsSqlGenerator : MySqlMigrationsSqlGenerator - { - private readonly IShardingRuntimeContext _shardingRuntimeContext; - - public ShardingMySqlMigrationsSqlGenerator(IShardingRuntimeContext shardingRuntimeContext, MigrationsSqlGeneratorDependencies dependencies, ICommandBatchPreparer commandBatchPreparer, IMySqlOptions options) : base(dependencies, commandBatchPreparer, options) - { - _shardingRuntimeContext = shardingRuntimeContext; - } - - protected override void Generate( - MigrationOperation operation, - IModel model, - MigrationCommandListBuilder builder) - { - var oldCmds = builder.GetCommandList().ToList(); - base.Generate(operation, model, builder); - var newCmds = builder.GetCommandList().ToList(); - var addCmds = newCmds.Where(x => !oldCmds.Contains(x)).ToList(); - - MigrationHelper.Generate(_shardingRuntimeContext, operation, builder, Dependencies.SqlGenerationHelper, addCmds); - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/ShardingTenantOptions.cs b/LingYanAspCoreFramework/MultiTenants/ShardingTenantOptions.cs deleted file mode 100644 index b13a244..0000000 --- a/LingYanAspCoreFramework/MultiTenants/ShardingTenantOptions.cs +++ /dev/null @@ -1,37 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Attributes; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class ShardingTenantOptions - { - /// - /// 榛樿鏁版嵁婧愬悕绉 - /// - public string DefaultDataSourceName { get; set; } - - /// - /// 榛樿鏁版嵁搴撳湴鍧 - /// - public string DefaultConnectionString { get; set; } - - /// - /// 鏁版嵁搴撶被鍨 - /// - public DataBaseType DataBaseType { get; set; } - - /// - /// 鍒嗙墖妯″紡 鍙栨ā杩樻槸鎸夋湀 - /// - public ShardingKeyType ShardingKeyType { get; set; } - - /// - /// 鏃堕棿鍒嗙墖寮濮嬫椂闂 - /// - public DateTime BeginTimeForSharding { get; set; } - - /// - /// 鍒嗙墖杩佺Щ鐨勫懡鍚嶇┖闂 - /// - public string MigrationNamespace { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/SharedTypeExtensions.cs b/LingYanAspCoreFramework/MultiTenants/SharedTypeExtensions.cs deleted file mode 100644 index dadeccd..0000000 --- a/LingYanAspCoreFramework/MultiTenants/SharedTypeExtensions.cs +++ /dev/null @@ -1,661 +0,0 @@ -锘縰sing System.Diagnostics; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; - -#nullable enable - -// ReSharper disable once CheckNamespace -namespace System -{ - [DebuggerStepThrough] - internal static class SharedTypeExtensions - { - private static readonly Dictionary _builtInTypeNames = new() - { - { typeof(bool), "bool" }, - { typeof(byte), "byte" }, - { typeof(char), "char" }, - { typeof(decimal), "decimal" }, - { typeof(double), "double" }, - { typeof(float), "float" }, - { typeof(int), "int" }, - { typeof(long), "long" }, - { typeof(object), "object" }, - { typeof(sbyte), "sbyte" }, - { typeof(short), "short" }, - { typeof(string), "string" }, - { typeof(uint), "uint" }, - { typeof(ulong), "ulong" }, - { typeof(ushort), "ushort" }, - { typeof(void), "void" } - }; - - public static Type UnwrapNullableType(this Type type) - => Nullable.GetUnderlyingType(type) ?? type; - - public static bool IsNullableValueType(this Type type) - => type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - - public static bool IsNullableType(this Type type) - => !type.IsValueType || type.IsNullableValueType(); - - public static bool IsValidEntityType(this Type type) - => type.IsClass - && !type.IsArray; - - public static bool IsPropertyBagType(this Type type) - { - if (type.IsGenericTypeDefinition) - { - return false; - } - - var types = GetGenericTypeImplementations(type, typeof(IDictionary<,>)); - return types.Any( - t => t.GetGenericArguments()[0] == typeof(string) - && t.GetGenericArguments()[1] == typeof(object)); - } - - public static Type MakeNullable(this Type type, bool nullable = true) - => type.IsNullableType() == nullable - ? type - : nullable - ? typeof(Nullable<>).MakeGenericType(type) - : type.UnwrapNullableType(); - - public static bool IsNumeric(this Type type) - { - type = type.UnwrapNullableType(); - - return type.IsInteger() - || type == typeof(decimal) - || type == typeof(float) - || type == typeof(double); - } - - public static bool IsInteger(this Type type) - { - type = type.UnwrapNullableType(); - - return type == typeof(int) - || type == typeof(long) - || type == typeof(short) - || type == typeof(byte) - || type == typeof(uint) - || type == typeof(ulong) - || type == typeof(ushort) - || type == typeof(sbyte) - || type == typeof(char); - } - - public static bool IsSignedInteger(this Type type) - => type == typeof(int) - || type == typeof(long) - || type == typeof(short) - || type == typeof(sbyte); - - public static bool IsAnonymousType(this Type type) - => type.Name.StartsWith("<>", StringComparison.Ordinal) - && type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), inherit: false).Length > 0 - && type.Name.Contains("AnonymousType"); - - public static bool IsTupleType(this Type type) - { - if (type == typeof(Tuple)) - { - return true; - } - - if (type.IsGenericType) - { - var genericDefinition = type.GetGenericTypeDefinition(); - if (genericDefinition == typeof(Tuple<>) - || genericDefinition == typeof(Tuple<,>) - || genericDefinition == typeof(Tuple<,,>) - || genericDefinition == typeof(Tuple<,,,>) - || genericDefinition == typeof(Tuple<,,,,>) - || genericDefinition == typeof(Tuple<,,,,,>) - || genericDefinition == typeof(Tuple<,,,,,,>) - || genericDefinition == typeof(Tuple<,,,,,,,>) - || genericDefinition == typeof(Tuple<,,,,,,,>)) - { - return true; - } - } - - return false; - } - - public static PropertyInfo? GetAnyProperty(this Type type, string name) - { - var props = type.GetRuntimeProperties().Where(p => p.Name == name).ToList(); - if (props.Count > 1) - { - throw new AmbiguousMatchException(); - } - - return props.SingleOrDefault(); - } - - public static MethodInfo GetRequiredMethod(this Type type, string name, params Type[] parameters) - { - var method = type.GetTypeInfo().GetMethod(name, parameters); - - if (method == null - && parameters.Length == 0) - { - method = type.GetMethod(name); - } - - if (method == null) - { - throw new InvalidOperationException(); - } - - return method; - } - - public static PropertyInfo GetRequiredProperty(this Type type, string name) - => type.GetTypeInfo().GetProperty(name) - ?? throw new InvalidOperationException($"Could not find property '{name}' on type '{type}'"); - - public static FieldInfo GetRequiredDeclaredField(this Type type, string name) - => type.GetTypeInfo().GetDeclaredField(name) - ?? throw new InvalidOperationException($"Could not find field '{name}' on type '{type}'"); - - public static MethodInfo GetRequiredDeclaredMethod(this Type type, string name) - => type.GetTypeInfo().GetDeclaredMethod(name) - ?? throw new InvalidOperationException($"Could not find method '{name}' on type '{type}'"); - - public static MethodInfo GetRequiredDeclaredMethod(this Type type, string name, Func methodSelector) - => type.GetTypeInfo().GetDeclaredMethods(name).Single(methodSelector); - - public static PropertyInfo GetRequiredDeclaredProperty(this Type type, string name) - => type.GetTypeInfo().GetDeclaredProperty(name) - ?? throw new InvalidOperationException($"Could not find property '{name}' on type '{type}'"); - - public static MethodInfo GetRequiredRuntimeMethod(this Type type, string name, params Type[] parameters) - => type.GetTypeInfo().GetRuntimeMethod(name, parameters) - ?? throw new InvalidOperationException($"Could not find method '{name}' on type '{type}'"); - - public static PropertyInfo GetRequiredRuntimeProperty(this Type type, string name) - => type.GetTypeInfo().GetRuntimeProperty(name) - ?? throw new InvalidOperationException($"Could not find property '{name}' on type '{type}'"); - - public static bool IsInstantiable(this Type type) - => !type.IsAbstract - && !type.IsInterface - && (!type.IsGenericType || !type.IsGenericTypeDefinition); - - public static Type UnwrapEnumType(this Type type) - { - var isNullable = type.IsNullableType(); - var underlyingNonNullableType = isNullable ? type.UnwrapNullableType() : type; - if (!underlyingNonNullableType.IsEnum) - { - return type; - } - - var underlyingEnumType = Enum.GetUnderlyingType(underlyingNonNullableType); - return isNullable ? MakeNullable(underlyingEnumType) : underlyingEnumType; - } - - public static Type GetSequenceType(this Type type) - { - var sequenceType = TryGetSequenceType(type); - if (sequenceType == null) - { - throw new ArgumentException($"The type {type.Name} does not represent a sequence"); - } - - return sequenceType; - } - - public static Type? TryGetSequenceType(this Type type) - => type.TryGetElementType(typeof(IEnumerable<>)) - ?? type.TryGetElementType(typeof(IAsyncEnumerable<>)); - - public static Type? TryGetElementType(this Type type, Type interfaceOrBaseType) - { - if (type.IsGenericTypeDefinition) - { - return null; - } - - var types = GetGenericTypeImplementations(type, interfaceOrBaseType); - - Type? singleImplementation = null; - foreach (var implementation in types) - { - if (singleImplementation == null) - { - singleImplementation = implementation; - } - else - { - singleImplementation = null; - break; - } - } - - return singleImplementation?.GenericTypeArguments.FirstOrDefault(); - } - - public static bool IsCompatibleWith(this Type propertyType, Type fieldType) - { - if (propertyType.IsAssignableFrom(fieldType) - || fieldType.IsAssignableFrom(propertyType)) - { - return true; - } - - var propertyElementType = propertyType.TryGetSequenceType(); - var fieldElementType = fieldType.TryGetSequenceType(); - - return propertyElementType != null - && fieldElementType != null - && IsCompatibleWith(propertyElementType, fieldElementType); - } - - public static IEnumerable GetGenericTypeImplementations(this Type type, Type interfaceOrBaseType) - { - var typeInfo = type.GetTypeInfo(); - if (!typeInfo.IsGenericTypeDefinition) - { - var baseTypes = interfaceOrBaseType.GetTypeInfo().IsInterface - ? typeInfo.ImplementedInterfaces - : type.GetBaseTypes(); - foreach (var baseType in baseTypes) - { - if (baseType.IsGenericType - && baseType.GetGenericTypeDefinition() == interfaceOrBaseType) - { - yield return baseType; - } - } - - if (type.IsGenericType - && type.GetGenericTypeDefinition() == interfaceOrBaseType) - { - yield return type; - } - } - } - - public static IEnumerable GetBaseTypes(this Type type) - { - var currentType = type.BaseType; - - while (currentType != null) - { - yield return currentType; - - currentType = currentType.BaseType; - } - } - - public static List GetBaseTypesAndInterfacesInclusive(this Type type) - { - var baseTypes = new List(); - var typesToProcess = new Queue(); - typesToProcess.Enqueue(type); - - while (typesToProcess.Count > 0) - { - type = typesToProcess.Dequeue(); - baseTypes.Add(type); - - if (type.IsNullableValueType()) - { - typesToProcess.Enqueue(Nullable.GetUnderlyingType(type)!); - } - - if (type.IsConstructedGenericType) - { - typesToProcess.Enqueue(type.GetGenericTypeDefinition()); - } - - if (!type.IsGenericTypeDefinition - && !type.IsInterface) - { - if (type.BaseType != null) - { - typesToProcess.Enqueue(type.BaseType); - } - - foreach (var @interface in GetDeclaredInterfaces(type)) - { - typesToProcess.Enqueue(@interface); - } - } - } - - return baseTypes; - } - - public static IEnumerable GetTypesInHierarchy(this Type type) - { - var currentType = type; - - while (currentType != null) - { - yield return currentType; - - currentType = currentType.BaseType; - } - } - - public static IEnumerable GetDeclaredInterfaces(this Type type) - { - var interfaces = type.GetInterfaces(); - if (type.BaseType == typeof(object) - || type.BaseType == null) - { - return interfaces; - } - - return interfaces.Except(type.BaseType.GetInterfaces()); - } - - public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[]? types) - { - types ??= Array.Empty(); - - return type.GetTypeInfo().DeclaredConstructors - .SingleOrDefault( - c => !c.IsStatic - && c.GetParameters().Select(p => p.ParameterType).SequenceEqual(types))!; - } - - public static IEnumerable GetPropertiesInHierarchy(this Type type, string name) - { - var currentType = type; - do - { - var typeInfo = currentType.GetTypeInfo(); - foreach (var propertyInfo in typeInfo.DeclaredProperties) - { - if (propertyInfo.Name.Equals(name, StringComparison.Ordinal) - && !(propertyInfo.GetMethod ?? propertyInfo.SetMethod)!.IsStatic) - { - yield return propertyInfo; - } - } - - currentType = typeInfo.BaseType; - } - while (currentType != null); - } - - // Looking up the members through the whole hierarchy allows to find inherited private members. - public static IEnumerable GetMembersInHierarchy(this Type type) - { - var currentType = type; - - do - { - // Do the whole hierarchy for properties first since looking for fields is slower. - foreach (var propertyInfo in currentType.GetRuntimeProperties().Where(pi => !(pi.GetMethod ?? pi.SetMethod)!.IsStatic)) - { - yield return propertyInfo; - } - - foreach (var fieldInfo in currentType.GetRuntimeFields().Where(f => !f.IsStatic)) - { - yield return fieldInfo; - } - - currentType = currentType.BaseType; - } - while (currentType != null); - } - - public static IEnumerable GetMembersInHierarchy(this Type type, string name) - => type.GetMembersInHierarchy().Where(m => m.Name == name); - - private static readonly Dictionary _commonTypeDictionary = new() - { -#pragma warning disable IDE0034 // Simplify 'default' expression - default causes default(object) - { typeof(int), default(int) }, - { typeof(Guid), default(Guid) }, - { typeof(DateOnly), default(DateOnly) }, - { typeof(DateTime), default(DateTime) }, - { typeof(DateTimeOffset), default(DateTimeOffset) }, - { typeof(TimeOnly), default(TimeOnly) }, - { typeof(long), default(long) }, - { typeof(bool), default(bool) }, - { typeof(double), default(double) }, - { typeof(short), default(short) }, - { typeof(float), default(float) }, - { typeof(byte), default(byte) }, - { typeof(char), default(char) }, - { typeof(uint), default(uint) }, - { typeof(ushort), default(ushort) }, - { typeof(ulong), default(ulong) }, - { typeof(sbyte), default(sbyte) } -#pragma warning restore IDE0034 // Simplify 'default' expression - }; - - public static object? GetDefaultValue(this Type type) - { - if (!type.IsValueType) - { - return null; - } - - // A bit of perf code to avoid calling Activator.CreateInstance for common types and - // to avoid boxing on every call. This is about 50% faster than just calling CreateInstance - // for all value types. - return _commonTypeDictionary.TryGetValue(type, out var value) - ? value - : Activator.CreateInstance(type); - } - - public static IEnumerable GetConstructibleTypes(this Assembly assembly) - => assembly.GetLoadableDefinedTypes().Where( - t => !t.IsAbstract - && !t.IsGenericTypeDefinition); - - public static IEnumerable GetLoadableDefinedTypes(this Assembly assembly) - { - try - { - return assembly.DefinedTypes; - } - catch (ReflectionTypeLoadException ex) - { - return ex.Types.Where(t => t != null).Select(IntrospectionExtensions.GetTypeInfo!); - } - } - - public static bool IsQueryableType(this Type type) - { - if (type.IsGenericType - && type.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - return true; - } - - return type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryable<>)); - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public static string DisplayName(this Type type, bool fullName = true, bool compilable = false) - { - var stringBuilder = new StringBuilder(); - ProcessType(stringBuilder, type, fullName, compilable); - return stringBuilder.ToString(); - } - - private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable) - { - if (type.IsGenericType) - { - var genericArguments = type.GetGenericArguments(); - ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable); - } - else if (type.IsArray) - { - ProcessArrayType(builder, type, fullName, compilable); - } - else if (_builtInTypeNames.TryGetValue(type, out var builtInName)) - { - builder.Append(builtInName); - } - else if (!type.IsGenericParameter) - { - if (compilable) - { - if (type.IsNested) - { - ProcessType(builder, type.DeclaringType!, fullName, compilable); - builder.Append('.'); - } - else if (fullName) - { - builder.Append(type.Namespace).Append('.'); - } - - builder.Append(type.Name); - } - else - { - builder.Append(fullName ? type.FullName : type.Name); - } - } - } - - private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable) - { - var innerType = type; - while (innerType.IsArray) - { - innerType = innerType.GetElementType()!; - } - - ProcessType(builder, innerType, fullName, compilable); - - while (type.IsArray) - { - builder.Append('['); - builder.Append(',', type.GetArrayRank() - 1); - builder.Append(']'); - type = type.GetElementType()!; - } - } - - private static void ProcessGenericType( - StringBuilder builder, - Type type, - Type[] genericArguments, - int length, - bool fullName, - bool compilable) - { - if (type.IsConstructedGenericType - && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - ProcessType(builder, type.UnwrapNullableType(), fullName, compilable); - builder.Append('?'); - return; - } - - var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0; - - if (compilable) - { - if (type.IsNested) - { - ProcessType(builder, type.DeclaringType!, fullName, compilable); - builder.Append('.'); - } - else if (fullName) - { - builder.Append(type.Namespace); - builder.Append('.'); - } - } - else - { - if (fullName) - { - if (type.IsNested) - { - ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable); - builder.Append('+'); - } - else - { - builder.Append(type.Namespace); - builder.Append('.'); - } - } - } - - var genericPartIndex = type.Name.IndexOf('`'); - if (genericPartIndex <= 0) - { - builder.Append(type.Name); - return; - } - - builder.Append(type.Name, 0, genericPartIndex); - builder.Append('<'); - - for (var i = offset; i < length; i++) - { - ProcessType(builder, genericArguments[i], fullName, compilable); - if (i + 1 == length) - { - continue; - } - - builder.Append(','); - if (!genericArguments[i + 1].IsGenericParameter) - { - builder.Append(' '); - } - } - - builder.Append('>'); - } - - public static IEnumerable GetNamespaces(this Type type) - { - if (_builtInTypeNames.ContainsKey(type)) - { - yield break; - } - - yield return type.Namespace!; - - if (type.IsGenericType) - { - foreach (var typeArgument in type.GenericTypeArguments) - { - foreach (var ns in typeArgument.GetNamespaces()) - { - yield return ns; - } - } - } - } - - public static ConstantExpression GetDefaultValueConstant(this Type type) - => (ConstantExpression)_generateDefaultValueConstantMethod - .MakeGenericMethod(type).Invoke(null, Array.Empty())!; - - private static readonly MethodInfo _generateDefaultValueConstantMethod = - typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant))!; - - private static ConstantExpression GenerateDefaultValueConstant() - => Expression.Constant(default(TDefault), typeof(TDefault)); - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/TenantContext.cs b/LingYanAspCoreFramework/MultiTenants/TenantContext.cs deleted file mode 100644 index 6509360..0000000 --- a/LingYanAspCoreFramework/MultiTenants/TenantContext.cs +++ /dev/null @@ -1,19 +0,0 @@ -锘縰sing ShardingCore.Core.RuntimeContexts; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class TenantContext - { - private readonly IShardingRuntimeContext _shardingRuntimeContext; - - public TenantContext(IShardingRuntimeContext shardingRuntimeContext) - { - _shardingRuntimeContext = shardingRuntimeContext; - } - - public IShardingRuntimeContext GetShardingRuntimeContext() - { - return _shardingRuntimeContext; - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/TenantContextAccessor.cs b/LingYanAspCoreFramework/MultiTenants/TenantContextAccessor.cs deleted file mode 100644 index 580da02..0000000 --- a/LingYanAspCoreFramework/MultiTenants/TenantContextAccessor.cs +++ /dev/null @@ -1,13 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.MultiTenants -{ - public class TenantContextAccessor : ITenantContextAccessor - { - private static readonly AsyncLocal _tenantContext = new AsyncLocal(); - - public TenantContext? TenantContext - { - get => _tenantContext.Value; - set => _tenantContext.Value = value; - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/TenantExtension.cs b/LingYanAspCoreFramework/MultiTenants/TenantExtension.cs deleted file mode 100644 index feb6ee1..0000000 --- a/LingYanAspCoreFramework/MultiTenants/TenantExtension.cs +++ /dev/null @@ -1,62 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Attributes; -using LingYanAspCoreFramework.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using ShardingCore.Extensions; -using System.Collections; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public static class TenantExtension - { - public static void InitTenant(this IServiceProvider serviceProvider, RuntimeCacheModel lYBuilderRuntimeModel) - { - var tenantManager = serviceProvider.GetService(); - var shardingBuilder = serviceProvider.GetService(); - using (var scope = serviceProvider.CreateScope()) - { - if (lYBuilderRuntimeModel.ModuleDbContextList.Count > 0) - { - //澶氱鎴烽厤缃〃杩愯杩佺Щ - var configDbContextType = lYBuilderRuntimeModel.ModuleDbContextList.Where(pair => pair.Value == DbContextType.TenantConfigDbContext).Select(pair => pair.Key).FirstOrDefault(); - if (configDbContextType != null) - { - var configDbContext = (DbContext)scope.ServiceProvider.GetService(configDbContextType); - configDbContext.Database.Migrate(); - var SetMethod = configDbContext.GetType().GetMethods().FirstOrDefault(f => f.Name == "Set").MakeGenericMethod(lYBuilderRuntimeModel.ModuleTenantBaseEntitys["BaseSysOwnerTenantConfig"]); - var dbSet = SetMethod.Invoke(configDbContext, null); - var toListMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(lYBuilderRuntimeModel.ModuleTenantBaseEntitys["BaseSysOwnerTenantConfig"]); - var sysUserTenantConfigs = toListMethod.Invoke(null, new object[] { dbSet }) as IEnumerable; - if (sysUserTenantConfigs != null) - { - foreach (dynamic sysUserTenantConfig in sysUserTenantConfigs) - { - var shardingTenantOptions = JsonConvert.DeserializeObject(sysUserTenantConfig.ConfigJson); - - var shardingRuntimeContext = shardingBuilder.Build(shardingTenantOptions); - - tenantManager.AddTenantSharding(sysUserTenantConfig.CompanyId, shardingRuntimeContext); - } - } - } - } - } - var tenantIds = tenantManager.GetAll(); - foreach (var tenantId in tenantIds) - { - using (tenantManager.CreateScope(tenantId)) - using (var scope = serviceProvider.CreateScope()) - { - var shardingRuntimeContext = tenantManager.GetCurrentTenantContext().GetShardingRuntimeContext(); - //寮鍚畾鏃朵换鍔 - //shardingRuntimeContext.UseAutoShardingCreate(); - var tenantDbContext = (DbContext)scope.ServiceProvider.GetService(lYBuilderRuntimeModel.TenantTemplateDbContexts.FirstOrDefault()); - tenantDbContext.Database.Migrate(); - //琛ュ伩琛 - shardingRuntimeContext.UseAutoTryCompensateTable(); - } - } - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/TenantManager.cs b/LingYanAspCoreFramework/MultiTenants/TenantManager.cs deleted file mode 100644 index 8986d40..0000000 --- a/LingYanAspCoreFramework/MultiTenants/TenantManager.cs +++ /dev/null @@ -1,41 +0,0 @@ -锘縰sing ShardingCore.Core.RuntimeContexts; -using System.Collections.Concurrent; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class TenantManager : ITenantManager - { - private readonly ITenantContextAccessor _tenantContextAccessor; - private readonly ConcurrentDictionary _cache = new(); - - public TenantManager(ITenantContextAccessor tenantContextAccessor) - { - _tenantContextAccessor = tenantContextAccessor; - } - - public List GetAll() - { - return _cache.Keys.ToList(); - } - - public TenantContext GetCurrentTenantContext() - { - return _tenantContextAccessor.TenantContext; - } - - public bool AddTenantSharding(object tenantId, IShardingRuntimeContext shardingRuntimeContext) - { - return _cache.TryAdd(tenantId, shardingRuntimeContext); - } - - public TenantScope CreateScope(object tenantId) - { - if (!_cache.TryGetValue(tenantId, out var shardingRuntimeContext)) - { - throw new InvalidOperationException("鏈壘鍒板搴旂鎴风殑閰嶇疆"); - } - _tenantContextAccessor.TenantContext = new TenantContext(shardingRuntimeContext); - return new TenantScope(_tenantContextAccessor); - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/TenantScope.cs b/LingYanAspCoreFramework/MultiTenants/TenantScope.cs deleted file mode 100644 index a1149fe..0000000 --- a/LingYanAspCoreFramework/MultiTenants/TenantScope.cs +++ /dev/null @@ -1,17 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.MultiTenants -{ - public class TenantScope : IDisposable - { - public TenantScope(ITenantContextAccessor tenantContextAccessor) - { - TenantContextAccessor = tenantContextAccessor; - } - - public ITenantContextAccessor TenantContextAccessor { get; } - - public void Dispose() - { - TenantContextAccessor.TenantContext = null; - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/MultiTenants/TenantSelectMiddleware.cs b/LingYanAspCoreFramework/MultiTenants/TenantSelectMiddleware.cs deleted file mode 100644 index fe208bf..0000000 --- a/LingYanAspCoreFramework/MultiTenants/TenantSelectMiddleware.cs +++ /dev/null @@ -1,59 +0,0 @@ -锘縰sing Microsoft.AspNetCore.Http; - -namespace LingYanAspCoreFramework.MultiTenants -{ - public class TenantSelectMiddleware - { - private readonly RequestDelegate _next; - private readonly ITenantManager _tenantManager; - - public TenantSelectMiddleware(RequestDelegate next, ITenantManager tenantManager) - { - _next = next; - _tenantManager = tenantManager; - } - - /// - /// 1.涓棿浠剁殑鏂规硶蹇呴』鍙獻nvoke锛屼笖涓簆ublic锛岄潪static銆 - /// 2.Invoke鏂规硶绗竴涓弬鏁板繀椤绘槸HttpContext绫诲瀷銆 - /// 3.Invoke鏂规硶蹇呴』杩斿洖Task銆 - /// 4.Invoke鏂规硶鍙互鏈夊涓弬鏁帮紝闄ttpContext澶栧叾瀹冨弬鏁颁細灏濊瘯浠庝緷璧栨敞鍏ュ鍣ㄤ腑鑾峰彇銆 - /// 5.Invoke鏂规硶涓嶈兘鏈夐噸杞姐 - /// - /// Author : Napoleon - /// Created : 2020/1/30 21:30 - public async Task Invoke(HttpContext context) - { - if (context.Request.Path.ToString().StartsWith(LingYanRuntimeManager.CommonConfigModel.TenantRoutePrefix.ToLower(), StringComparison.CurrentCultureIgnoreCase)) - { - if (!context.User.Identity.IsAuthenticated) - { - await _next(context); - return; - } - - var tenantId = context.User.Claims.FirstOrDefault((o) => o.Type == "uid")?.Value; - if (string.IsNullOrWhiteSpace(tenantId)) - { - await DoUnAuthorized(context, "not found tenant id"); - return; - } - - using (_tenantManager.CreateScope(tenantId)) - { - await _next(context); - } - } - else - { - await _next(context); - } - } - - private async Task DoUnAuthorized(HttpContext context, string msg) - { - context.Response.StatusCode = 403; - await context.Response.WriteAsync(msg); - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/SampleRoots/SampleCommonExceptionMiddleware.cs b/LingYanAspCoreFramework/SampleRoots/SampleCommonExceptionMiddleware.cs index 885991c..a8e6682 100644 --- a/LingYanAspCoreFramework/SampleRoots/SampleCommonExceptionMiddleware.cs +++ b/LingYanAspCoreFramework/SampleRoots/SampleCommonExceptionMiddleware.cs @@ -31,7 +31,7 @@ namespace LingYanAspCoreFramework.SampleRoots } else { - logger.LogError(LoggerHelper.ExcetionEvent,ex.StackTrace); + logger.LogError(LoggerHelper.ExcetionEvent,ex.Message); context.Response.StatusCode = StatusCodes.Status403Forbidden; context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(new ResponceBody(40000, ex.Message)); -- Gitee From a69811e87b2d5746f5cc4cc3d856914710084e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Mon, 31 Mar 2025 18:41:39 +0800 Subject: [PATCH 08/17] =?UTF-8?q?=E9=80=82=E9=85=8Dsqlserver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DynamicShardingController .cs | 10 +- LingTest/DbContexts/StoreDbContext.cs | 12 +-- LingTest/Entitys/Product.cs | 9 +- ...igner.cs => 20250331103401_v1.Designer.cs} | 18 ++-- ...50329115203_v1.cs => 20250331103401_v1.cs} | 46 +++++---- ...igner.cs => 20250331103436_v2.Designer.cs} | 14 +-- ...50329120406_v2.cs => 20250331103436_v2.cs} | 38 ++++---- .../Migrations/20250331104009_v3.Designer.cs | 60 ++++++++++++ LingTest/Migrations/20250331104009_v3.cs | 78 +++++++++++++++ .../Migrations/StoreDbContextModelSnapshot.cs | 43 ++++----- LingTest/MyDesignTimeServices.cs | 4 +- LingTest/RestApiModule.cs | 35 +++---- .../Core/Impl/TenantConnectionResolver.cs | 41 -------- .../Core/Impl/TenantInfoGenerator.cs | 32 ------- .../Interface/ITenantConnectionResolver.cs | 10 -- .../Core/Interface/ITenantInfoGenerator.cs | 11 --- .../EFCore/Interface/ITenantDbContext.cs | 9 -- .../DynamicControllerConvention.cs | 2 +- .../Core/DynamicShardInfoMiddleware.cs} | 13 ++- .../Extension/DynamicShardCoreExtension.cs} | 96 +++++++++---------- .../Impl/DynamicShardConnectionResolver.cs | 50 ++++++++++ .../Core/Impl/DynamicShardInfoGenerator.cs | 41 ++++++++ .../IDynamicShardConnectionResolver.cs | 9 ++ .../Interface/IDynamicShardInfoGenerator.cs | 11 +++ .../EFCore/DynamicShardMigrationAssembly.cs} | 15 ++- .../EFCore/DynamicShardMigrationGenerator.cs} | 64 +++++++++++-- .../EFCore/DynamicShardModelCacheKey.cs} | 12 +-- .../DynamicShardModelCacheKeyFactory.cs} | 15 ++- .../EFCore/Impl/DynamicShardDbContext.cs} | 43 ++++++--- .../EFCore/Impl/DynamicShardEntityBuilder.cs} | 46 ++++----- .../EFCore/Impl/DynamicShardEntityScaner.cs} | 8 +- .../Interface/IDynamicShardEntityBuilder.cs} | 13 +-- .../Interface/IDynamicShardEntityBuilderT.cs | 13 +++ .../Interface/IDynamicShardEntityScaner.cs} | 6 +- .../Interface/IDynamicShardtDbContext.cs | 9 ++ .../Models/DbsetProperty.cs | 2 +- .../Models/DynamicGetMemberBinder.cs | 29 ++++++ .../Models/DynamicSetMemberBinder.cs | 24 +++++ .../Models/DynamicShardInfo.cs} | 9 +- .../Models/DynamicShardOption.cs} | 16 ++-- .../Models/DynamicShardSettingsT.cs} | 21 ++-- .../Models/Enum/ConnectionType.cs} | 4 +- .../Envs/Configs/AppSetting.json | 2 +- .../Envs/Configs/StatusCode.json | 52 +++------- .../Extensions/DeEncryptExtension.cs | 8 +- .../Extensions/FFMpegExtension.cs | 14 +-- .../Extensions/GeneralExtension.cs | 12 +-- .../Extensions/IocExtension.cs | 8 +- .../Helpers/AlibabaCloudHelper.cs | 4 +- .../Helpers/TencentCloudHelper.cs | 2 +- .../SampleRoots/SampleGeneralHelper.cs | 18 ++-- ...ampleGlobalAuthorizationFilterAttribute.cs | 8 +- .../SampleGlobalPermissionHandle.cs | 4 +- 53 files changed, 707 insertions(+), 466 deletions(-) rename LingTest/Migrations/{20250329120406_v2.Designer.cs => 20250331103401_v1.Designer.cs} (78%) rename LingTest/Migrations/{20250329115203_v1.cs => 20250331103401_v1.cs} (30%) rename LingTest/Migrations/{20250329115203_v1.Designer.cs => 20250331103436_v2.Designer.cs} (79%) rename LingTest/Migrations/{20250329120406_v2.cs => 20250331103436_v2.cs} (50%) create mode 100644 LingTest/Migrations/20250331104009_v3.Designer.cs create mode 100644 LingTest/Migrations/20250331104009_v3.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs delete mode 100644 LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs rename LingYanAspCoreFramework/{DbMultiTenants/Core/TenantInfoMiddleware.cs => DynamicShard/Core/DynamicShardInfoMiddleware.cs} (32%) rename LingYanAspCoreFramework/{DbMultiTenants/Core/Extension/TenantCoreExtension.cs => DynamicShard/Core/Extension/DynamicShardCoreExtension.cs} (36%) create mode 100644 LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs create mode 100644 LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs create mode 100644 LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardConnectionResolver.cs create mode 100644 LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardInfoGenerator.cs rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs => DynamicShard/EFCore/DynamicShardMigrationAssembly.cs} (69%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs => DynamicShard/EFCore/DynamicShardMigrationGenerator.cs} (60%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs => DynamicShard/EFCore/DynamicShardModelCacheKey.cs} (77%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs => DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs} (63%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/Impl/TenantDbContext.cs => DynamicShard/EFCore/Impl/DynamicShardDbContext.cs} (32%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs => DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs} (33%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/Impl/EntityScaner.cs => DynamicShard/EFCore/Impl/DynamicShardEntityScaner.cs} (88%) rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs => DynamicShard/EFCore/Interface/IDynamicShardEntityBuilder.cs} (43%) create mode 100644 LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilderT.cs rename LingYanAspCoreFramework/{DbMultiTenants/EFCore/Interface/IEntityScaner.cs => DynamicShard/EFCore/Interface/IDynamicShardEntityScaner.cs} (43%) create mode 100644 LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardtDbContext.cs rename LingYanAspCoreFramework/{DbMultiTenants => DynamicShard}/Models/DbsetProperty.cs (86%) create mode 100644 LingYanAspCoreFramework/DynamicShard/Models/DynamicGetMemberBinder.cs create mode 100644 LingYanAspCoreFramework/DynamicShard/Models/DynamicSetMemberBinder.cs rename LingYanAspCoreFramework/{DbMultiTenants/Models/TenantInfo.cs => DynamicShard/Models/DynamicShardInfo.cs} (91%) rename LingYanAspCoreFramework/{DbMultiTenants/Models/TenantOption.cs => DynamicShard/Models/DynamicShardOption.cs} (52%) rename LingYanAspCoreFramework/{DbMultiTenants/Models/TenantSettingsT.cs => DynamicShard/Models/DynamicShardSettingsT.cs} (54%) rename LingYanAspCoreFramework/{DbMultiTenants/Models/Enum/ShardingConnectionType.cs => DynamicShard/Models/Enum/ConnectionType.cs} (50%) diff --git a/LingTest/Controllers/DynamicShardingController .cs b/LingTest/Controllers/DynamicShardingController .cs index 51c38a0..22e2d91 100644 --- a/LingTest/Controllers/DynamicShardingController .cs +++ b/LingTest/Controllers/DynamicShardingController .cs @@ -1,6 +1,7 @@ using LingTest.DbContexts; using LingTest.Entitys; using LingYanAspCoreFramework.Helpers; +using LingYanAspCoreFramework.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -19,12 +20,15 @@ namespace LingTest.Controllers this.storeDbContext = storeDbContext; try { - this.storeDbContext.Database.Migrate(); - this.storeDbContext.Database.EnsureCreated(); + if (storeDbContext.Database.GetPendingMigrations().Any()) + { + storeDbContext.Database.Migrate(); + storeDbContext.Database.EnsureCreated(); + } } catch (Exception ex) { - LoggerHelper.ErrorLog(ex.Message); + throw new CommonException(new ResponceBody(63000, ex.Message)); } } diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index c70aa6a..f1d6539 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -1,16 +1,16 @@ using LingTest.Entitys; -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl; -using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DynamicShard.EFCore.Impl; +using LingYanAspCoreFramework.DynamicShard.Models; using Microsoft.EntityFrameworkCore; namespace LingTest.DbContexts { - public class StoreDbContext : TenantDbContext + public class StoreDbContext : DynamicShardDbContext { public DbSet Products => this.Set(); - public DbSet Orders => this.Set(); - public StoreDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) - : base(options, tenant, serviceProvider) + //public DbSet Orders => this.Set(); + public StoreDbContext(DbContextOptions options, DynamicShardOption dynamicShardOption, IServiceProvider serviceProvider) + : base(options, dynamicShardOption, serviceProvider) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/LingTest/Entitys/Product.cs b/LingTest/Entitys/Product.cs index e0072a0..182c8bd 100644 --- a/LingTest/Entitys/Product.cs +++ b/LingTest/Entitys/Product.cs @@ -1,7 +1,10 @@ +using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; namespace LingTest.Entitys { + [Index(nameof(TestIndex))] + [Index(nameof(TYes),IsUnique =true)] public class Product { [Key] @@ -10,7 +13,9 @@ namespace LingTest.Entitys [StringLength(50), Required] public string Name { get; set; } - [StringLength(50)] - public string Category { get; set; } + public string TYes { get; set; } + + + public string TestIndex { get; set; } } } diff --git a/LingTest/Migrations/20250329120406_v2.Designer.cs b/LingTest/Migrations/20250331103401_v1.Designer.cs similarity index 78% rename from LingTest/Migrations/20250329120406_v2.Designer.cs rename to LingTest/Migrations/20250331103401_v1.Designer.cs index 6bb2d7b..ee14488 100644 --- a/LingTest/Migrations/20250329120406_v2.Designer.cs +++ b/LingTest/Migrations/20250331103401_v1.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace LingTest.Migrations { [DbContext(typeof(StoreDbContext))] - [Migration("20250329120406_v2")] - partial class v2 + [Migration("20250331103401_v1")] + partial class v1 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -20,9 +20,9 @@ namespace LingTest.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 128); - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("LingTest.Entitys.Order", b => { @@ -30,13 +30,13 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("CreateTimeStamp") .HasColumnType("bigint"); b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); + .HasColumnType("bit"); b.HasKey("Id"); @@ -49,17 +49,17 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("Category") .IsRequired() .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasColumnType("nvarchar(50)"); b.Property("Name") .IsRequired() .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasColumnType("nvarchar(50)"); b.HasKey("Id"); diff --git a/LingTest/Migrations/20250329115203_v1.cs b/LingTest/Migrations/20250331103401_v1.cs similarity index 30% rename from LingTest/Migrations/20250329115203_v1.cs rename to LingTest/Migrations/20250331103401_v1.cs index 8b8fc2e..8c7a0dc 100644 --- a/LingTest/Migrations/20250329115203_v1.cs +++ b/LingTest/Migrations/20250331103401_v1.cs @@ -1,5 +1,4 @@ -锘縰sing Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; +锘縰sing Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -8,42 +7,53 @@ namespace LingTest.Migrations /// public partial class v1 : Migration { - //鐏电嚂妗嗘灦娉ㄥ叆鍔ㄦ佸弬鏁 - private readonly string shardingKey; - public v1(string _shardingKey) + //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 + private readonly string dynamicShardPrefix; + public v1(string _dynamicShardPrefix) { - this.shardingKey = _shardingKey; + this.dynamicShardPrefix = _dynamicShardPrefix; } /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterDatabase() - .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: $"{this.dynamicShardPrefix}Orders", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + IsDeleted = table.Column(type: "bit", nullable: false), + CreateTimeStamp = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey($"{this.dynamicShardPrefix}PK_Orders", x => x.Id); + }); migrationBuilder.CreateTable( - name: $"{this.shardingKey}_Products", + name: $"{this.dynamicShardPrefix}Products", columns: table => new { Id = table.Column(type: "int", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - Category = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) - .Annotation("MySql:CharSet", "utf8mb4") + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Category = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) }, constraints: table => { - table.PrimaryKey("PK_Products", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); + table.PrimaryKey($"{this.dynamicShardPrefix}PK_Products", x => x.Id); + }); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: $"{this.shardingKey}_Products"); + name: $"{this.dynamicShardPrefix}Orders"); + + migrationBuilder.DropTable( + name: $"{this.dynamicShardPrefix}Products"); } } } diff --git a/LingTest/Migrations/20250329115203_v1.Designer.cs b/LingTest/Migrations/20250331103436_v2.Designer.cs similarity index 79% rename from LingTest/Migrations/20250329115203_v1.Designer.cs rename to LingTest/Migrations/20250331103436_v2.Designer.cs index e05d26d..ed6f168 100644 --- a/LingTest/Migrations/20250329115203_v1.Designer.cs +++ b/LingTest/Migrations/20250331103436_v2.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace LingTest.Migrations { [DbContext(typeof(StoreDbContext))] - [Migration("20250329115203_v1")] - partial class v1 + [Migration("20250331103436_v2")] + partial class v2 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -20,9 +20,9 @@ namespace LingTest.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 128); - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("LingTest.Entitys.Product", b => { @@ -30,17 +30,17 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("Category") .IsRequired() .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasColumnType("nvarchar(50)"); b.Property("Name") .IsRequired() .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasColumnType("nvarchar(50)"); b.HasKey("Id"); diff --git a/LingTest/Migrations/20250329120406_v2.cs b/LingTest/Migrations/20250331103436_v2.cs similarity index 50% rename from LingTest/Migrations/20250329120406_v2.cs rename to LingTest/Migrations/20250331103436_v2.cs index 9527cf0..daa63fb 100644 --- a/LingTest/Migrations/20250329120406_v2.cs +++ b/LingTest/Migrations/20250331103436_v2.cs @@ -1,5 +1,4 @@ -锘縰sing Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; +锘縰sing Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -8,37 +7,36 @@ namespace LingTest.Migrations /// public partial class v2 : Migration { - //鐏电嚂妗嗘灦娉ㄥ叆鍔ㄦ佸弬鏁 - private readonly string shardingKey; - public v2(string _shardingKey) + //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 + private readonly string dynamicShardPrefix; + public v2(string _dynamicShardPrefix) { - this.shardingKey = _shardingKey; + this.dynamicShardPrefix = _dynamicShardPrefix; } /// protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: $"{this.dynamicShardPrefix}Orders"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: $"{this.shardingKey}_Orders", + name: $"{this.dynamicShardPrefix}Orders", columns: table => new { Id = table.Column(type: "bigint", nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - IsDeleted = table.Column(type: "tinyint(1)", nullable: false), - CreateTimeStamp = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CreateTimeStamp = table.Column(type: "bigint", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) }, constraints: table => { - table.PrimaryKey("PK_Orders", x => x.Id); - }) - .Annotation("MySql:CharSet", "utf8mb4"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: $"{this.shardingKey}_Orders"); + table.PrimaryKey($"{this.dynamicShardPrefix}PK_Orders", x => x.Id); + }); } } } diff --git a/LingTest/Migrations/20250331104009_v3.Designer.cs b/LingTest/Migrations/20250331104009_v3.Designer.cs new file mode 100644 index 0000000..c81a8f1 --- /dev/null +++ b/LingTest/Migrations/20250331104009_v3.Designer.cs @@ -0,0 +1,60 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20250331104009_v3")] + partial class v3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("TYes") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("TYes") + .IsUnique(); + + b.HasIndex("TestIndex"); + + b.ToTable("Products", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/Migrations/20250331104009_v3.cs b/LingTest/Migrations/20250331104009_v3.cs new file mode 100644 index 0000000..f68842d --- /dev/null +++ b/LingTest/Migrations/20250331104009_v3.cs @@ -0,0 +1,78 @@ +锘縰sing Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LingTest.Migrations +{ + /// + public partial class v3 : Migration + { + //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 + private readonly string dynamicShardPrefix; + public v3(string _dynamicShardPrefix) + { + this.dynamicShardPrefix = _dynamicShardPrefix; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Category", + table: $"{this.dynamicShardPrefix}Products"); + + migrationBuilder.AddColumn( + name: "TYes", + table: $"{this.dynamicShardPrefix}Products", + type: "nvarchar(450)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "TestIndex", + table: $"{this.dynamicShardPrefix}Products", + type: "nvarchar(450)", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: $"{this.dynamicShardPrefix}IX_Products_TestIndex", + table: "Products", + column: "TestIndex"); + + migrationBuilder.CreateIndex( + name: $"{this.dynamicShardPrefix}IX_Products_TYes", + table: "Products", + column: "TYes", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Products_TestIndex", + table: $"{this.dynamicShardPrefix}Products"); + + migrationBuilder.DropIndex( + name: "IX_Products_TYes", + table: $"{this.dynamicShardPrefix}Products"); + + migrationBuilder.DropColumn( + name: "TYes", + table: $"{this.dynamicShardPrefix}Products"); + + migrationBuilder.DropColumn( + name: "TestIndex", + table: $"{this.dynamicShardPrefix}Products"); + + migrationBuilder.AddColumn( + name: "Category", + table: $"{this.dynamicShardPrefix}Products", + type: "nvarchar(50)", + maxLength: 50, + nullable: false, + defaultValue: ""); + } + } +} diff --git a/LingTest/Migrations/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/StoreDbContextModelSnapshot.cs index dfbc196..5cf37ef 100644 --- a/LingTest/Migrations/StoreDbContextModelSnapshot.cs +++ b/LingTest/Migrations/StoreDbContextModelSnapshot.cs @@ -17,28 +17,9 @@ namespace LingTest.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 64); + .HasAnnotation("Relational:MaxIdentifierLength", 128); - MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.Order", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); - - b.Property("CreateTimeStamp") - .HasColumnType("bigint"); - - b.Property("IsDeleted") - .HasColumnType("tinyint(1)"); - - b.HasKey("Id"); - - b.ToTable("Orders", (string)null); - }); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("LingTest.Entitys.Product", b => { @@ -46,20 +27,28 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); - MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("Category") + b.Property("Name") .IsRequired() .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasColumnType("nvarchar(50)"); - b.Property("Name") + b.Property("TYes") .IsRequired() - .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("nvarchar(450)"); b.HasKey("Id"); + b.HasIndex("TYes") + .IsUnique(); + + b.HasIndex("TestIndex"); + b.ToTable("Products", (string)null); }); #pragma warning restore 612, 618 diff --git a/LingTest/MyDesignTimeServices.cs b/LingTest/MyDesignTimeServices.cs index f05b380..a0be35a 100644 --- a/LingTest/MyDesignTimeServices.cs +++ b/LingTest/MyDesignTimeServices.cs @@ -1,4 +1,4 @@ -锘縰sing LingYanAspCoreFramework.DbMultiTenants.EFCore; +锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Migrations.Design; @@ -8,7 +8,7 @@ namespace LingTest { public void ConfigureDesignTimeServices(IServiceCollection serviceCollection) { - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } } } diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index 08c502a..91900cb 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -1,8 +1,9 @@ 锘縰sing LingTest.DbContexts; using LingYanAspCoreFramework.BaseRoots; -using LingYanAspCoreFramework.DbMultiTenants.Core; -using LingYanAspCoreFramework.DbMultiTenants.Core.Extension; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.DynamicShard.Core; +using LingYanAspCoreFramework.DynamicShard.Core.Extension; +using LingYanAspCoreFramework.DynamicShard.Models.Enum; +using LingYanAspCoreFramework.Helpers; namespace LingTest { @@ -10,24 +11,26 @@ namespace LingTest { public override void ARegisterModule(WebApplicationBuilder services) { - //鍒嗗簱鏂规($DynamicDataBaseName)-(杩佺Щ琛)-锛$DynamicDataBaseName_杩炴帴锛 - //鍒嗚〃鏂规($DynamicTableName)-($DynamicTableName_杩佺Щ琛)-锛堣繛鎺ワ級 - //鍒嗗簱鍒嗚〃($DynamicDataBaseName-$DynamicTableName)-锛$DynamicTableName_杩佺Щ琛級-锛$DynamicDataBaseNam_杩炴帴锛 - services.Services.AddDynamicSharding(settings => + //鍒嗗簱 + //鍒嗘ā寮 + //鍒嗚〃 + services.Services.AddDynamicSharding(dynamicSettingsFunc => { - settings.ConnectionNameFunc = (serviceProvider, tenantInfo) => + dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => { - return new Tuple(ShardingConnectionType.Mysql, $"server=192.168.188.128;port=3306;database=test777;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;"); + var connectionTupple = new Tuple(ConnectionType.SqlServer, + $"Server=192.168.148.131;Database=testmssql;User Id=sa;Password=12345678;Encrypt=False;"); + LoggerHelper.DefaultLog(connectionTupple.Item1.ToString() + connectionTupple.Item2); + return connectionTupple; }; - //mysql-mariadb鏄病鏈夋ā寮忥紝sqlserver\pgsql - //settings.SchemaFunc = (tenantinfo) => + //dynamicSettingsFunc.SchemaNameFunc = (dynamicShardInfo) => //{ - // return ""; + // return dynamicShardInfo?.Name??"test"; //}; - settings.TableNameFunc = (tenantinfo, tablename) => - { - return !string.IsNullOrEmpty(tenantinfo?.Name) ? $"{tenantinfo.Name}_{tablename}" : tablename; + dynamicSettingsFunc.TablePrefixFunc = (tenantinfo) => + { + return tenantinfo?.Name; }; }); @@ -35,7 +38,7 @@ namespace LingTest public override void BInitializationModule(WebApplication provider) { - provider.UseMiddleware(); + provider.UseMiddleware(); } } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs deleted file mode 100644 index 2c537cd..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantConnectionResolver.cs +++ /dev/null @@ -1,41 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using Microsoft.EntityFrameworkCore; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl -{ - public class TenantConnectionResolver : ITenantConnectionResolver - where TDbContext : DbContext - { - private readonly TenantSettings setting; - private readonly TenantInfo tenantInfo; - private readonly IServiceProvider serviceProvider; - private readonly TenantOption tenantOption; - - public TenantConnectionResolver(TenantSettings setting, TenantInfo tenantInfo, TenantOption tenantOption, IServiceProvider serviceProvider) - { - this.tenantOption = tenantOption; - this.setting = setting; - this.tenantInfo = tenantInfo; - this.serviceProvider = serviceProvider; - } - - public Tuple GetConnection() - { - var connectionTupple = this.setting.ConnectionNameFunc.Invoke(this.serviceProvider, this.tenantInfo); - tenantOption.Key = setting.Key; - tenantOption.ShardingConnectionType = connectionTupple.Item1; - tenantOption.ConnectionName = connectionTupple.Item2; - tenantOption.TableNameFunc = setting.TableNameFunc; - if (this.setting.SchemaFunc != null) - { - var schemaName = this.setting.SchemaFunc.Invoke(this.tenantInfo); - if (!string.IsNullOrEmpty(schemaName)) - { - tenantOption.SchemaName = schemaName; - } - } - return new Tuple(this.tenantInfo,this.tenantOption); - } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs deleted file mode 100644 index 5f86e1c..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Impl/TenantInfoGenerator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using Microsoft.AspNetCore.Http; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Impl -{ - public class TenantInfoGenerator : ITenantInfoGenerator - { - private readonly TenantInfo tenantInfo; - public TenantInfoGenerator(TenantInfo tenantInfo) - { - this.tenantInfo = tenantInfo; - } - - public TenantInfo GenerateTenant(object sender, HttpContext httpContext) - { - if (!this.tenantInfo.IsPresent) - { - var tenantName = httpContext?.Request?.Headers["TenantName"]; - - if (!string.IsNullOrEmpty(tenantName)) - { - this.tenantInfo.IsPresent = true; - this.tenantInfo.Name = tenantName; - this.tenantInfo.Generator = this; - } - } - - return this.tenantInfo; - } - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs deleted file mode 100644 index 602956a..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantConnectionResolver.cs +++ /dev/null @@ -1,10 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface -{ - public interface ITenantConnectionResolver - { - Tuple GetConnection(); - } -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs b/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs deleted file mode 100644 index 528be52..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Interface/ITenantInfoGenerator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models; -using Microsoft.AspNetCore.Http; - -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Interface -{ - public interface ITenantInfoGenerator - { - TenantInfo GenerateTenant(object sender, HttpContext httpContext); - } - -} diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs b/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs deleted file mode 100644 index ac1d99a..0000000 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantDbContext.cs +++ /dev/null @@ -1,9 +0,0 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models; - -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface -{ - public interface ITenantDbContext - { - TenantInfo Tenant { get; } - } -} diff --git a/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs b/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs index 4bf7c56..fd47dce 100644 --- a/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs +++ b/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs @@ -127,7 +127,7 @@ namespace LingYanAspCoreFramework.DynamicApis } else { - throw new CommonException(new ResponceBody(60010)); + throw new CommonException(new ResponceBody(60000, "閰嶇疆鏂囦欢涓嶅寘鍚獶ynamicHttpRoutePrefix閿")); } //鍒涘缓璺敱璺緞 var routePath = string.Concat(routePrefix + "/", controllerName + "/", actionName).Replace("//", "/"); diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs b/LingYanAspCoreFramework/DynamicShard/Core/DynamicShardInfoMiddleware.cs similarity index 32% rename from LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs rename to LingYanAspCoreFramework/DynamicShard/Core/DynamicShardInfoMiddleware.cs index d35f879..184876e 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/TenantInfoMiddleware.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/DynamicShardInfoMiddleware.cs @@ -1,21 +1,20 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.Helpers; +using LingYanAspCoreFramework.DynamicShard.Core.Interface; using Microsoft.AspNetCore.Http; -namespace LingYanAspCoreFramework.DbMultiTenants.Core +namespace LingYanAspCoreFramework.DynamicShard.Core { - public class TenantInfoMiddleware + public class DynamicShardInfoMiddleware { private readonly RequestDelegate _next; - public TenantInfoMiddleware(RequestDelegate next) + public DynamicShardInfoMiddleware(RequestDelegate next) { _next = next; } - public async Task InvokeAsync(HttpContext context, ITenantInfoGenerator tenantInfoGenerator) + public async Task InvokeAsync(HttpContext context, IDynamicShardInfoGenerator dynamicShardInfoGenerator) { - tenantInfoGenerator.GenerateTenant(this, context); + dynamicShardInfoGenerator.GenerateDynamicShardInfo(this, context); await _next(context); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs similarity index 36% rename from LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs rename to LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs index 7ea0a76..167e407 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Core/Extension/TenantCoreExtension.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs @@ -1,10 +1,10 @@ -using LingYanAspCoreFramework.DbMultiTenants.Core.Impl; -using LingYanAspCoreFramework.DbMultiTenants.Core.Interface; -using LingYanAspCoreFramework.DbMultiTenants.EFCore; -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl; -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.DynamicShard.Core.Impl; +using LingYanAspCoreFramework.DynamicShard.Core.Interface; +using LingYanAspCoreFramework.DynamicShard.EFCore; +using LingYanAspCoreFramework.DynamicShard.EFCore.Impl; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +using LingYanAspCoreFramework.DynamicShard.Models; +using LingYanAspCoreFramework.DynamicShard.Models.Enum; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -12,87 +12,87 @@ using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace LingYanAspCoreFramework.DbMultiTenants.Core.Extension +namespace LingYanAspCoreFramework.DynamicShard.Core.Extension { - public static class TenantCoreExtension + public static class DynamicShardCoreExtension { - public static IServiceCollection AddDynamicSharding(this IServiceCollection services, Action> makeTenantSettingsFunc) - where TDbContext : DbContext, ITenantDbContext + public static IServiceCollection AddDynamicSharding(this IServiceCollection services, Action> dynamicSettingsFunc) + where TDbContext : DbContext, IDynamicShardtDbContext { //注册基础服务 - services.AddScoped(); - services.AddScoped(); - services.TryAddScoped(); - services.TryAddScoped, TenantConnectionResolver>(); - services.TryAddScoped, TenantEntityBuilder>(); - services.TryAddScoped, EntityScaner>(); - services.AddSingleton(ConfigShardingConnectionType(makeTenantSettingsFunc)); + services.AddScoped(); + services.AddScoped(); + services.TryAddScoped(); + services.TryAddScoped, DynamicShardConnectionResolver>(); + services.TryAddScoped, DynamicShardEntityBuilder>(); + services.TryAddScoped, DynamicShardEntityScaner>(); + services.AddSingleton(ConfigShardingConnectionType(dynamicSettingsFunc)); //注册数据库上下文 services.AddDbContext((serviceProvider, options) => { //初始配置 - var tenantSettingT = serviceProvider.GetRequiredService>(); - var connectionResolver = serviceProvider.GetRequiredService>(); - var tuple = connectionResolver.GetConnection(); - tenantSettingT?.DbContextOptionDelegate?.Invoke(tuple.Item1, tuple.Item2, options); + var dynamicShardSettings = serviceProvider.GetRequiredService>(); + var connectionResolver = serviceProvider.GetRequiredService>(); + var dynamicShardOption = connectionResolver.GetConnection(); + dynamicShardSettings?.DbContextOptionDelegate?.Invoke(dynamicShardOption, options); }); return services; } - private static Func> ConfigShardingConnectionType(Action> tenantSettingShardingFunc) - where TDbContext : DbContext, ITenantDbContext + private static Func> ConfigShardingConnectionType(Action> dynamicShardSettingsAction) + where TDbContext : DbContext, IDynamicShardtDbContext { - Func> tenantSettingDbTypeAndOption = (serviceProvider) => + Func> dynamicShardOptionConfigFunc = (serviceProvider) => { - var tenantSettings = new TenantSettings(); + var dynamicShardSettings = new DynamicShardSettings(); //按照数据库类型进行配置 - tenantSettings.DbContextOptionDelegate = (tenantInfo, tenantOption, optionsBuilder) => + dynamicShardSettings.DbContextOptionDelegate = (dynamicShardOption, optionsBuilder) => { - if (string.IsNullOrEmpty(tenantOption.ConnectionName)) + if (string.IsNullOrEmpty(dynamicShardOption.ConnectionName)) { throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); } - switch (tenantOption.ShardingConnectionType) + switch (dynamicShardOption.ConnectionType) { - case ShardingConnectionType.Mysql: - optionsBuilder.UseMySql(tenantOption.ConnectionName, ServerVersion.AutoDetect(tenantOption.ConnectionName), builder => + case ConnectionType.Mysql: + optionsBuilder.UseMySql(dynamicShardOption.ConnectionName, ServerVersion.AutoDetect(dynamicShardOption.ConnectionName), builder => { - builder.DbContextBuilderMigration(tenantInfo, tenantOption); + builder.DbContextBuilderMigration(dynamicShardOption); }); break; - case ShardingConnectionType.PostgreSql: - optionsBuilder.UseNpgsql(tenantOption.ConnectionName, builder => + case ConnectionType.PostgreSql: + optionsBuilder.UseNpgsql(dynamicShardOption.ConnectionName, builder => { - builder.DbContextBuilderMigration(tenantInfo, tenantOption); + builder.DbContextBuilderMigration(dynamicShardOption); }); break; - case ShardingConnectionType.SqlServer: - optionsBuilder.UseSqlServer(tenantOption.ConnectionName, builder => + case ConnectionType.SqlServer: + optionsBuilder.UseSqlServer(dynamicShardOption.ConnectionName, builder => { - builder.DbContextBuilderMigration(tenantInfo, tenantOption); + builder.DbContextBuilderMigration(dynamicShardOption); }); break; } - optionsBuilder.ReplaceService>(); - optionsBuilder.ReplaceService(); + optionsBuilder.ReplaceService>(); + optionsBuilder.ReplaceService(); }; //配置动态分片方案 - tenantSettingShardingFunc?.Invoke(tenantSettings); - return tenantSettings; + dynamicShardSettingsAction?.Invoke(dynamicShardSettings); + return dynamicShardSettings; }; - return tenantSettingDbTypeAndOption; + return dynamicShardOptionConfigFunc; } - internal static void DbContextBuilderMigration(this RelationalDbContextOptionsBuilder builder, TenantInfo tenantInfo,TenantOption tenantOption) + internal static void DbContextBuilderMigration(this RelationalDbContextOptionsBuilder builder, DynamicShardOption dynamicShardOption) where TBuilder : RelationalDbContextOptionsBuilder where TExtension : RelationalOptionsExtension, new() { - if (!string.IsNullOrEmpty(tenantOption.SchemaName)) + if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { - builder.MigrationsHistoryTable("_EFMigrationHistory", tenantOption.SchemaName); + builder.MigrationsHistoryTable("_EFMigrationHistory", dynamicShardOption.SchemaName); } else { - builder.MigrationsHistoryTable($"{tenantInfo.Name}_EFMigrationHistory"); - } + builder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory"); + } } } diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs new file mode 100644 index 0000000..28c3cb0 --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs @@ -0,0 +1,50 @@ +using LingYanAspCoreFramework.DynamicShard.Core.Interface; +using LingYanAspCoreFramework.DynamicShard.Models; +using LingYanAspCoreFramework.DynamicShard.Models.Enum; +using LingYanAspCoreFramework.Models; +using Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DynamicShard.Core.Impl +{ + public class DynamicShardConnectionResolver : IDynamicShardConnectionResolver + where TDbContext : DbContext + { + private readonly DynamicShardSettings dynamicShardSettings; + private readonly DynamicShardInfo dynamicShardInfo; + private readonly IServiceProvider serviceProvider; + private readonly DynamicShardOption dynamicShardOption; + + public DynamicShardConnectionResolver(DynamicShardSettings dynamicShardSettings, + DynamicShardInfo dynamicShardInfo, + DynamicShardOption dynamicShardOption, + IServiceProvider serviceProvider) + { + this.dynamicShardOption = dynamicShardOption; + this.dynamicShardSettings = dynamicShardSettings; + this.dynamicShardInfo = dynamicShardInfo; + this.serviceProvider = serviceProvider; + } + + public DynamicShardOption GetConnection() + { + var connectionTupple = this.dynamicShardSettings.ConnectionNameFunc?.Invoke(this.serviceProvider, this.dynamicShardInfo); + + dynamicShardOption.Key = this.dynamicShardSettings.Key; + dynamicShardOption.ConnectionType = connectionTupple.Item1; + dynamicShardOption.ConnectionName = connectionTupple.Item2; + var SchemaName = this.dynamicShardSettings.SchemaNameFunc?.Invoke(this.dynamicShardInfo); + dynamicShardOption.SchemaName = SchemaName; + if (dynamicShardOption.ConnectionType == ConnectionType.Mysql && !string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + { + throw new CommonException(new ResponceBody(63000, $"{dynamicShardOption.ConnectionType.ToString()}不支持按模式分片")); + } + var tablePreFix = this.dynamicShardSettings.TablePrefixFunc?.Invoke(this.dynamicShardInfo); + if (!string.IsNullOrEmpty(tablePreFix)) + { + tablePreFix += "_"; + } + dynamicShardOption.TablePrefix = tablePreFix; + return this.dynamicShardOption; + } + } +} diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs new file mode 100644 index 0000000..71febae --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs @@ -0,0 +1,41 @@ +using LingYanAspCoreFramework.DynamicShard.Core.Interface; +using LingYanAspCoreFramework.DynamicShard.Models; +using LingYanAspCoreFramework.Helpers; +using Microsoft.AspNetCore.Http; + +namespace LingYanAspCoreFramework.DynamicShard.Core.Impl +{ + public class DynamicShardInfoGenerator : IDynamicShardInfoGenerator + { + private readonly DynamicShardInfo dynamicShardInfo; + public DynamicShardInfoGenerator(DynamicShardInfo dynamicShardInfo) + { + this.dynamicShardInfo = dynamicShardInfo; + } + + public DynamicShardInfo GenerateDynamicShardInfo(object sender, HttpContext httpContext) + { + if (!this.dynamicShardInfo.IsPresent) + { + foreach (var item in httpContext.Request.Headers) + { + if (item.Key.ToLower().StartsWith("DynamicShard".ToLower())) + { + this.dynamicShardInfo.TrySetMember(new DynamicSetMemberBinder(item.Key), item.Value); + this.dynamicShardInfo.TryGetMember(new DynamicGetMemberBinder(item.Key), out object value); + LoggerHelper.DefaultLog(value.ToString()); + } + } + var dynamicShardKey = httpContext?.Request?.Headers["DynamicShardKey"]; + if (!string.IsNullOrEmpty(dynamicShardKey)) + { + this.dynamicShardInfo.IsPresent = true; + this.dynamicShardInfo.Name = dynamicShardKey; + this.dynamicShardInfo.Generator = this; + } + } + + return this.dynamicShardInfo; + } + } +} diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardConnectionResolver.cs b/LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardConnectionResolver.cs new file mode 100644 index 0000000..c459c87 --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardConnectionResolver.cs @@ -0,0 +1,9 @@ +using LingYanAspCoreFramework.DynamicShard.Models; + +namespace LingYanAspCoreFramework.DynamicShard.Core.Interface +{ + public interface IDynamicShardConnectionResolver + { + DynamicShardOption GetConnection(); + } +} diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardInfoGenerator.cs b/LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardInfoGenerator.cs new file mode 100644 index 0000000..dafd089 --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/Core/Interface/IDynamicShardInfoGenerator.cs @@ -0,0 +1,11 @@ +using LingYanAspCoreFramework.DynamicShard.Models; +using Microsoft.AspNetCore.Http; + +namespace LingYanAspCoreFramework.DynamicShard.Core.Interface +{ + public interface IDynamicShardInfoGenerator + { + DynamicShardInfo GenerateDynamicShardInfo(object sender, HttpContext httpContext); + } + +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs similarity index 69% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs index af22293..670e339 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationAssembly.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs @@ -1,20 +1,18 @@ -锘縰sing LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; -using LingYanAspCoreFramework.Helpers; +锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; -using Newtonsoft.Json; using System.Reflection; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore +namespace LingYanAspCoreFramework.DynamicShard.EFCore { - public class DynamicShardingMigrationAssembly : MigrationsAssembly + public class DynamicShardMigrationAssembly : MigrationsAssembly { private readonly DbContext context; - public DynamicShardingMigrationAssembly(ICurrentDbContext currentContext, + public DynamicShardMigrationAssembly(ICurrentDbContext currentContext, IDbContextOptions options, IMigrationsIdGenerator idGenerator, IDiagnosticsLogger logger) : base(currentContext, options, idGenerator, logger) @@ -29,10 +27,9 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore var hasCtorWithSchema = migrationClass .GetConstructor(new[] { typeof(string) }) != null; - if (hasCtorWithSchema && context is ITenantDbContext tenantDbContext) + if (hasCtorWithSchema && context is IDynamicShardtDbContext dynamicShardtDbContext) { - LoggerHelper.DefaultLog($"杩愯鏃惰縼绉讳紶鍙倇tenantDbContext?.Tenant?.Name}"); - var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), tenantDbContext?.Tenant?.Name); + var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), dynamicShardtDbContext?.DynamicShardOptionImpl?.TablePrefix); instance.ActiveProvider = activeProvider; return instance; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs similarity index 60% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs index 628ed8b..0cd1a1a 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingMigrationGenerator.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs @@ -1,14 +1,15 @@ 锘縰sing LingYanAspCoreFramework.Extensions; +using LingYanAspCoreFramework.Helpers; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations.Design; using Microsoft.EntityFrameworkCore.Migrations.Operations; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore +namespace LingYanAspCoreFramework.DynamicShard.EFCore { - public class DynamicShardingMigrationGenerator : CSharpMigrationsGenerator + public class DynamicShardMigrationGenerator : CSharpMigrationsGenerator { - public DynamicShardingMigrationGenerator(MigrationsCodeGeneratorDependencies dependencies, + public DynamicShardMigrationGenerator(MigrationsCodeGeneratorDependencies dependencies, CSharpMigrationsGeneratorDependencies csharpDependencies) : base(dependencies, csharpDependencies) { } @@ -16,7 +17,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore IReadOnlyList downOperations) { // 鏋勯犲櫒鍙傛暟 - string ctorStr = "shardingKey"; + string ctorStr = "dynamicShardPrefix"; //杩佺Щ鐢熸垚鍣ㄨ繍琛屽墠缃坊鍔犺〃鍚嶅崰浣嶇 GernateBeforeUpDownTableName(ctorStr, upOperations, downOperations); //杩佺Щ鐢熸垚鍣ㄨ繍琛 @@ -37,6 +38,15 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore if (operation is CreateTableOperation createTableOperation) { createTableOperation.Name = GernateBeforeTableName(ctorStr, createTableOperation.Name); + createTableOperation.PrimaryKey.Name = GernateBeforeTableName(ctorStr, createTableOperation.PrimaryKey.Name); + foreach (var foreignKey in createTableOperation.ForeignKeys) + { + foreignKey.Name = GernateBeforeTableName(ctorStr, foreignKey.Name); + } + foreach (var uniqueConstraint in createTableOperation.UniqueConstraints) + { + uniqueConstraint.Name = GernateBeforeTableName(ctorStr, uniqueConstraint.Name); + } } else if (operation is RenameTableOperation renameTableOperation) { @@ -50,6 +60,22 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore { alterTableOperation.Name = GernateBeforeTableName(ctorStr, alterTableOperation.Name); } + else if (operation is AddForeignKeyOperation addForeignKeyOperation) + { + addForeignKeyOperation.Name = GernateBeforeTableName(ctorStr, addForeignKeyOperation.Name); + } + else if (operation is AddPrimaryKeyOperation addPrimaryKeyOperation) + { + addPrimaryKeyOperation.Name = GernateBeforeTableName(ctorStr, addPrimaryKeyOperation.Name); + } + else if (operation is AddUniqueConstraintOperation addUniqueConstraintOperation) + { + addUniqueConstraintOperation.Name = GernateBeforeTableName(ctorStr, addUniqueConstraintOperation.Name); + } + else if (operation is CreateIndexOperation createIndexOperation) + { + createIndexOperation.Name = GernateBeforeTableName(ctorStr, createIndexOperation.Name); + } else { var columnTableName = operation.GetPropertyValue("Table"); @@ -66,6 +92,15 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore if (operation is CreateTableOperation createTableOperation) { createTableOperation.Name = GernateBeforeTableName(ctorStr, createTableOperation.Name); + createTableOperation.PrimaryKey.Name = GernateBeforeTableName(ctorStr, createTableOperation.PrimaryKey.Name); + foreach (var foreignKey in createTableOperation.ForeignKeys) + { + foreignKey.Name = GernateBeforeTableName(ctorStr, foreignKey.Name); + } + foreach (var uniqueConstraint in createTableOperation.UniqueConstraints) + { + uniqueConstraint.Name = GernateBeforeTableName(ctorStr, uniqueConstraint.Name); + } } else if (operation is RenameTableOperation renameTableOperation) { @@ -79,6 +114,22 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore { alterTableOperation.Name = GernateBeforeTableName(ctorStr, alterTableOperation.Name); } + else if (operation is AddForeignKeyOperation addForeignKeyOperation) + { + addForeignKeyOperation.Name = GernateBeforeTableName(ctorStr, addForeignKeyOperation.Name); + } + else if (operation is AddPrimaryKeyOperation addPrimaryKeyOperation) + { + addPrimaryKeyOperation.Name = GernateBeforeTableName(ctorStr, addPrimaryKeyOperation.Name); + } + else if (operation is AddUniqueConstraintOperation addUniqueConstraintOperation) + { + addUniqueConstraintOperation.Name = GernateBeforeTableName(ctorStr, addUniqueConstraintOperation.Name); + } + else if (operation is CreateIndexOperation createIndexOperation) + { + createIndexOperation.Name = GernateBeforeTableName(ctorStr, createIndexOperation.Name); + } else { var columnTableName = operation.GetPropertyValue("Table"); @@ -92,7 +143,8 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore //鐢熸垚琛ㄦ槑鍗犱綅绗 string GernateBeforeTableName(string ctorKey, string tableName) { - return $"${ctorKey}_{tableName}"; + LoggerHelper.DefaultLog(ctorKey+ tableName); + return $"${ctorKey}{tableName}"; } } private string GernateAfterCtor(string ctorStr, string migrationName, string generatedCode) @@ -100,7 +152,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore IndentedStringBuilder dynamicShardingCodeBuilder = new IndentedStringBuilder(); dynamicShardingCodeBuilder.IncrementIndent().IncrementIndent().AppendLine(); - dynamicShardingCodeBuilder.AppendLine("//鐏电嚂妗嗘灦娉ㄥ叆鍔ㄦ佸弬鏁"); + dynamicShardingCodeBuilder.AppendLine("//DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁"); dynamicShardingCodeBuilder.AppendLine($"private readonly string {ctorStr};"); dynamicShardingCodeBuilder.AppendLine($"public {migrationName}(string _{ctorStr})"); dynamicShardingCodeBuilder.AppendLine("{"); diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKey.cs similarity index 77% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKey.cs index 1fef712..6baea10 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKey.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKey.cs @@ -1,15 +1,15 @@ -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore +namespace LingYanAspCoreFramework.DynamicShard.EFCore { /// /// 表示租户模型缓存键的类 /// /// 数据库上下文类型 - internal sealed class DynamicShardingModelCacheKey : ModelCacheKey - where TDbContext : DbContext, ITenantDbContext + internal sealed class DynamicShardModelCacheKey : ModelCacheKey + where TDbContext : DbContext, IDynamicShardtDbContext { private readonly TDbContext context; private readonly string identifier; @@ -19,7 +19,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore /// /// 数据库上下文 /// 租户标识符 - public DynamicShardingModelCacheKey(TDbContext context, string identifier) : base(context) + public DynamicShardModelCacheKey(TDbContext context, string identifier) : base(context) { this.context = context; this.identifier = identifier; @@ -32,7 +32,7 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore /// 如果相等,返回 true;否则返回 false protected override bool Equals(ModelCacheKey other) { - var isEqual = base.Equals(other) && (other as DynamicShardingModelCacheKey)?.identifier == identifier; + var isEqual = base.Equals(other) && (other as DynamicShardModelCacheKey)?.identifier == identifier; return isEqual; } diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs similarity index 63% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs index c88e8c4..c1ad2f7 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/DynamicShardingModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs @@ -1,22 +1,21 @@ -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore +namespace LingYanAspCoreFramework.DynamicShard.EFCore { /// /// 表示租户模型缓存键工厂的类 /// /// 数据库上下文类型 - public sealed class DynamicShardingModelCacheKeyFactory : ModelCacheKeyFactory - where TDbContext : DbContext, ITenantDbContext + public sealed class DynamicShardModelCacheKeyFactory : ModelCacheKeyFactory + where TDbContext : DbContext, IDynamicShardtDbContext { /// /// 初始化 TenantModelCacheKeyFactory 类的新实例 /// /// 模型缓存键工厂的依赖项 - public DynamicShardingModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) + public DynamicShardModelCacheKeyFactory(ModelCacheKeyFactoryDependencies dependencies) : base(dependencies) { } @@ -39,8 +38,8 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore else { var dbContext = context as TDbContext; - var tenantModelCacheKey = new DynamicShardingModelCacheKey(dbContext, dbContext?.Tenant?.Name ?? "no_tenant_identifier"); - return tenantModelCacheKey; + var dynamicShardModelCacheKey = new DynamicShardModelCacheKey(dbContext, dbContext?.DynamicShardOptionImpl?.TablePrefix ?? "no_dynamicshard_identifier"); + return dynamicShardModelCacheKey; } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantDbContext.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs similarity index 32% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantDbContext.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs index 2d510da..640079d 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantDbContext.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs @@ -1,34 +1,47 @@ -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +using LingYanAspCoreFramework.DynamicShard.Models; +using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Migrations.Design; using Microsoft.Extensions.DependencyInjection; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl { /// /// 表示租户基础数据库上下文的抽象类 /// - public abstract class TenantDbContext : DbContext, ITenantDbContext + public abstract class DynamicShardDbContext : DbContext, IDynamicShardtDbContext { /// /// 获取或设置租户信息 /// - public TenantInfo Tenant { get; protected internal set; } + public DynamicShardOption DynamicShardOptionImpl { get; protected internal set; } private readonly IServiceProvider serviceProvider; /// - /// 初始化 TenantDbContext 类的新实例 + /// 初始化 /// - /// 数据库上下文选项 - /// 租户信息 - /// 服务提供程序 - public TenantDbContext(DbContextOptions options, TenantInfo tenant, IServiceProvider serviceProvider) - : base(options) + /// + /// + /// + public DynamicShardDbContext(DbContextOptions dbContextOptions, + DynamicShardOption dynamicShardOption, + IServiceProvider serviceProvider) : base(dbContextOptions) { this.serviceProvider = serviceProvider; - Tenant = tenant; + this.DynamicShardOptionImpl = dynamicShardOption; + //try + //{ + // if (this.Database.GetPendingMigrations().Any()) + // { + // this.Database.Migrate(); + // this.Database.EnsureCreated(); + // } + //} + //catch (Exception ex) + //{ + // throw new CommonException(new ResponceBody(63000, ex.Message)); + //} } /// /// 配置模型 @@ -36,8 +49,8 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl /// 模型构建器 protected override void OnModelCreating(ModelBuilder modelBuilder) { - var builderType = typeof(ITenantEntityBuilder<>).MakeGenericType(GetType()); - ITenantEntityBuilder entityBuilder = (ITenantEntityBuilder)serviceProvider.GetRequiredService(builderType); + var builderType = typeof(IDynamicShardEntityBuilder<>).MakeGenericType(GetType()); + IDynamicShardEntityBuilder entityBuilder = (IDynamicShardEntityBuilder)serviceProvider.GetRequiredService(builderType); entityBuilder.UpdateEntities(modelBuilder); } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs similarity index 33% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs index 93bd5cc..c51797a 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/TenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs @@ -1,34 +1,30 @@ -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; -using LingYanAspCoreFramework.Models; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +using LingYanAspCoreFramework.DynamicShard.Models; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl { /// /// 表示租户实体构建器的类 /// /// 数据库上下文类型 - public class TenantEntityBuilder : ITenantEntityBuilder + public class DynamicShardEntityBuilder : IDynamicShardEntityBuilder where TDbContext : DbContext { - private readonly IEntityScaner entityScaner; - private readonly TenantOption tenantOption; - private readonly TenantInfo tenantInfo; + private readonly IDynamicShardEntityScaner dynamicShardEntityScaner; + private readonly DynamicShardOption dynamicShardOption; /// - /// 初始化 TenantEntityBuilder 类的新实例 + /// 初始化 /// - /// 实体扫描器 - /// 租户选项 - /// 租户信息 - public TenantEntityBuilder(IEntityScaner entityScaner, TenantOption tenantOption, TenantInfo tenantInfo) + /// + /// + /// + public DynamicShardEntityBuilder(IDynamicShardEntityScaner dynamicShardEntityScaner, + DynamicShardOption dynamicShardOption) { - this.tenantInfo = tenantInfo; - this.tenantOption = tenantOption; - this.entityScaner = entityScaner; + this.dynamicShardOption = dynamicShardOption; + this.dynamicShardEntityScaner = dynamicShardEntityScaner; } /// @@ -37,24 +33,20 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl /// 模型构建器 public void UpdateEntities(ModelBuilder modelBuilder) { - var dbsetProperties = entityScaner.ScanEntityTypes(); + var dbsetProperties = dynamicShardEntityScaner.ScanEntityTypes(); foreach (var property in dbsetProperties) { var entity = modelBuilder.Entity(property.PropertyType); - if (tenantOption.TableNameFunc == null) + var tableName = dynamicShardOption.TablePrefix + property.PropertyName; + if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { - throw new CommonException(new ResponceBody(63000, "表名不得为空")); - } - var tableName = tenantOption.TableNameFunc?.Invoke(tenantInfo, property.PropertyName) ?? property.PropertyName; - if (!string.IsNullOrEmpty(tenantOption.SchemaName)) - { - entity.ToTable(tableName, tenantOption.SchemaName); + entity.ToTable(tableName, dynamicShardOption.SchemaName); + } else { entity.ToTable(tableName); } - } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/EntityScaner.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityScaner.cs similarity index 88% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/EntityScaner.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityScaner.cs index 046595c..6963313 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Impl/EntityScaner.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityScaner.cs @@ -1,15 +1,15 @@ -using LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface; -using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +using LingYanAspCoreFramework.DynamicShard.Models; using Microsoft.EntityFrameworkCore; using System.Reflection; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Impl +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl { /// /// 简单实体扫描器类,用于扫描数据库上下文中的实体类型 /// /// 数据库上下文类型 - public class EntityScaner : IEntityScaner + public class DynamicShardEntityScaner : IDynamicShardEntityScaner where TDbContext : DbContext { private static List dbProperties; diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilder.cs similarity index 43% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilder.cs index 90ccc06..73406e8 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/ITenantEntityBuilder.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilder.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Interface { /// /// 表示租户实体构建器的接口 /// - public interface ITenantEntityBuilder + public interface IDynamicShardEntityBuilder { /// /// 更新实体模型 @@ -13,13 +13,4 @@ namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface /// 模型构建器 void UpdateEntities(ModelBuilder modelBuilder); } - - /// - /// 表示特定数据库上下文类型的租户实体构建器的接口 - /// - /// 数据库上下文类型 - public interface ITenantEntityBuilder : ITenantEntityBuilder - where TDbContext : DbContext - { - } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilderT.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilderT.cs new file mode 100644 index 0000000..97bb718 --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityBuilderT.cs @@ -0,0 +1,13 @@ +锘縰sing Microsoft.EntityFrameworkCore; + +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Interface +{ + /// + /// 琛ㄧず鐗瑰畾鏁版嵁搴撲笂涓嬫枃绫诲瀷鐨勭鎴峰疄浣撴瀯寤哄櫒鐨勬帴鍙 + /// + /// 鏁版嵁搴撲笂涓嬫枃绫诲瀷 + public interface IDynamicShardEntityBuilder : IDynamicShardEntityBuilder + where TDbContext : DbContext + { + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/IEntityScaner.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityScaner.cs similarity index 43% rename from LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/IEntityScaner.cs rename to LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityScaner.cs index de6be64..cf0f1d6 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/EFCore/Interface/IEntityScaner.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardEntityScaner.cs @@ -1,9 +1,9 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models; +using LingYanAspCoreFramework.DynamicShard.Models; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.EFCore.Interface +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Interface { - public interface IEntityScaner + public interface IDynamicShardEntityScaner where TDbContext : DbContext { IList ScanEntityTypes(); diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardtDbContext.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardtDbContext.cs new file mode 100644 index 0000000..52d3c27 --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Interface/IDynamicShardtDbContext.cs @@ -0,0 +1,9 @@ +using LingYanAspCoreFramework.DynamicShard.Models; + +namespace LingYanAspCoreFramework.DynamicShard.EFCore.Interface +{ + public interface IDynamicShardtDbContext + { + DynamicShardOption DynamicShardOptionImpl { get; } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs b/LingYanAspCoreFramework/DynamicShard/Models/DbsetProperty.cs similarity index 86% rename from LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs rename to LingYanAspCoreFramework/DynamicShard/Models/DbsetProperty.cs index f040560..03c5761 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/DbsetProperty.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DbsetProperty.cs @@ -1,4 +1,4 @@ -namespace LingYanAspCoreFramework.DbMultiTenants.Models +namespace LingYanAspCoreFramework.DynamicShard.Models { /// /// 表示数据库集属性的类 diff --git a/LingYanAspCoreFramework/DynamicShard/Models/DynamicGetMemberBinder.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicGetMemberBinder.cs new file mode 100644 index 0000000..1985c21 --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicGetMemberBinder.cs @@ -0,0 +1,29 @@ +锘縰sing System.Dynamic; +using System.Linq.Expressions; + +namespace LingYanAspCoreFramework.DynamicShard.Models +{ + public class DynamicGetMemberBinder : GetMemberBinder + { + public DynamicGetMemberBinder(string name) : base(name, false) + { + } + + public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion) + { + var expando = (ExpandoObject)target.Value; + var dict = (IDictionary)expando; + if (dict.TryGetValue(Name, out var result)) + { + return new DynamicMetaObject( + Expression.Constant(result), + BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType) + ); + } + return new DynamicMetaObject( + Expression.Constant(null), + BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType) + ); + } + } +} diff --git a/LingYanAspCoreFramework/DynamicShard/Models/DynamicSetMemberBinder.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicSetMemberBinder.cs new file mode 100644 index 0000000..d82fd9b --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicSetMemberBinder.cs @@ -0,0 +1,24 @@ +锘縰sing System.Dynamic; +using System.Linq.Expressions; + +namespace LingYanAspCoreFramework.DynamicShard.Models +{ + public class DynamicSetMemberBinder : SetMemberBinder + { + public DynamicSetMemberBinder(string name) : base(name, false) + { + + } + + public override DynamicMetaObject FallbackSetMember(DynamicMetaObject target, DynamicMetaObject value, DynamicMetaObject errorSuggestion) + { + var expando = (ExpandoObject)target.Value; + var dict = (IDictionary)expando; + dict[Name] = value.Value; + return new DynamicMetaObject( + Expression.Constant(value.Value), + BindingRestrictions.GetTypeRestriction(target.Expression, target.LimitType) + ); + } + } +} diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs similarity index 91% rename from LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs rename to LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs index 38defe7..71efd5e 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantInfo.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs @@ -1,11 +1,11 @@ 锘縰sing System.Dynamic; -namespace LingYanAspCoreFramework.DbMultiTenants.Models +namespace LingYanAspCoreFramework.DynamicShard.Models { /// /// 绉熸埛杩愯鏃朵俊鎭 /// - public class TenantInfo : DynamicObject + public class DynamicShardInfo : DynamicObject { /// /// 绉熸埛鍚嶇О @@ -22,11 +22,6 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models /// public object Generator { get; set; } - /// - /// 鍔ㄦ佸鍣紝鐢ㄤ簬瀛樺偍绉熸埛鐨勫姩鎬佸睘鎬 - /// - public dynamic Container { get; set; } = new ExpandoObject(); - /// /// 鍐呴儴瀛楀吀锛岀敤浜庡瓨鍌ㄥ姩鎬佸睘鎬 /// diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs similarity index 52% rename from LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs rename to LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs index 1e023e4..34513cf 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantOption.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs @@ -1,11 +1,11 @@ -using LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +using LingYanAspCoreFramework.DynamicShard.Models.Enum; -namespace LingYanAspCoreFramework.DbMultiTenants.Models +namespace LingYanAspCoreFramework.DynamicShard.Models { /// /// 租户的数据库连接策略和表结构规则 /// - public class TenantOption + public class DynamicShardOption { /// /// 租户选项的键 @@ -14,19 +14,19 @@ namespace LingYanAspCoreFramework.DbMultiTenants.Models /// /// 数据库类型 /// - internal ShardingConnectionType ShardingConnectionType { get; set; } + public ConnectionType ConnectionType { get; set; } /// /// 数据库连接字符串 /// - internal string ConnectionName { get; set; } + public string ConnectionName { get; set; } /// - /// 新模式名称 + /// 模式名称 /// - internal string SchemaName { get; set; } + public string SchemaName { get; set; } /// /// 表名生成委托 /// - public Func TableNameFunc { get; set; } + public string TablePrefix { get; set; } } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantSettingsT.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs similarity index 54% rename from LingYanAspCoreFramework/DbMultiTenants/Models/TenantSettingsT.cs rename to LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs index 343528b..0079080 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/TenantSettingsT.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs @@ -1,37 +1,36 @@ -锘縰sing LingYanAspCoreFramework.DbMultiTenants.Models.Enum; +锘縰sing LingYanAspCoreFramework.DynamicShard.Models.Enum; using Microsoft.EntityFrameworkCore; -namespace LingYanAspCoreFramework.DbMultiTenants.Models +namespace LingYanAspCoreFramework.DynamicShard.Models { /// /// 澶氱鎴锋暟鎹簱涓婁笅鏂囩殑鍔ㄦ侀厤缃 /// /// 鏁版嵁搴撲笂涓嬫枃绫诲瀷 - public class TenantSettings + public class DynamicShardSettings where TDbContext : DbContext { /// /// 绉熸埛璁剧疆鐨勯敭 /// - public string Key { get; set; } + public string Key { get; set; } /// /// 鏁版嵁搴撹繛鎺ュ瓧绗︿覆鐢熸垚鍑芥暟 /// - public Func> ConnectionNameFunc { get; set; } + public Func> ConnectionNameFunc { get; set; } /// - /// 琛ㄥ悕鐢熸垚鍑芥暟 + /// 妯″紡鐢熸垚鍑芥暟 /// - public Func TableNameFunc { get; set; } + public Func SchemaNameFunc { get; set; } /// - /// 妯″紡鐢熸垚鍑芥暟 + /// 琛ㄥ悕鐢熸垚鍑芥暟 /// - public Func SchemaFunc { get; set; } - + public Func TablePrefixFunc { get; set; } /// /// 鏁版嵁搴撲笂涓嬫枃閫夐」閰嶇疆 /// - public Action DbContextOptionDelegate; + public Action DbContextOptionDelegate; } } diff --git a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ShardingConnectionType.cs b/LingYanAspCoreFramework/DynamicShard/Models/Enum/ConnectionType.cs similarity index 50% rename from LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ShardingConnectionType.cs rename to LingYanAspCoreFramework/DynamicShard/Models/Enum/ConnectionType.cs index 4cc4c13..8cae903 100644 --- a/LingYanAspCoreFramework/DbMultiTenants/Models/Enum/ShardingConnectionType.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/Enum/ConnectionType.cs @@ -1,6 +1,6 @@ -namespace LingYanAspCoreFramework.DbMultiTenants.Models.Enum +namespace LingYanAspCoreFramework.DynamicShard.Models.Enum { - public enum ShardingConnectionType + public enum ConnectionType { None = 0, Mysql = 1, diff --git a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json index 852722d..7f039e6 100644 --- a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json +++ b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json @@ -19,7 +19,7 @@ "RedisCofigModel": { "Pattern": "Single", // 鍗曟満妯″紡 - "Single": "192.168.188.128:6379,defaultDatabase=0,password=,prefix=ling", + "Single": "192.168.148.130:6379,defaultDatabase=0,password=12345678,prefix=ling", //鍝ㄥ叺妯″紡 "SentinelModel": { "Master": "longyumaster,password=,prefix=", diff --git a/LingYanAspCoreFramework/Envs/Configs/StatusCode.json b/LingYanAspCoreFramework/Envs/Configs/StatusCode.json index c6595e7..31ccab2 100644 --- a/LingYanAspCoreFramework/Envs/Configs/StatusCode.json +++ b/LingYanAspCoreFramework/Envs/Configs/StatusCode.json @@ -1,44 +1,18 @@ { - "20000": "璇锋眰鎻愮ず:鎿嶄綔鎴愬姛", + "20000": "璇锋眰鎴愬姛:瀹岀編閫氳繃", - "30000": "璧勬簮璋冩暣:閲嶅畾鍚戣祫婧", + "40000": "璇锋眰寮傚父:鏅氶敊璇", + "41000": "璇锋眰寮傚父:閴存潈閿欒", + "42000": "璇锋眰寮傚父:鎺堟潈閿欒", + "43000": "璇锋眰寮傚父:鍙傛暟閿欒", - "40000": "瀹㈡埛绔敊璇:鎿嶄綔澶辫触", - "40001": "瀹㈡埛绔敊璇:Token閴存潈璀﹀憡", - "40002": "瀹㈡埛绔敊璇:瑙掕壊缁勮鍛", - "40003": "瀹㈡埛绔敊璇:璺敱缁勮鍛", - "40004": "瀹㈡埛绔敊璇:妯″瀷鍙傛暟楠岃瘉閿欒", - - "50000": "鏈嶅姟绔敊璇:杩囩▼閿欒,鍐呭淇濆瘑", - "50001": "涓氬姟灞傝鍛:鏃犵鍚堟暟鎹", - "50002": "鏁版嵁搴撳眰閿欒:淇濆瓨澶辫触锛岄泦浣撳洖閫", - "50003": "涓氬姟灞傞敊璇:瑕佹眰鏁版嵁鍞竴鎬у師鍒", - - "60000": "妗嗘灦閿欒:妗嗘灦瑙f瀽鏈嶅姟鏃堕敊璇", - "60001": "妗嗘灦閿欒:妗嗘灦娉ㄥ叆鏈嶅姟鏃堕敊璇", - "60002": "妗嗘灦閿欒:妗嗘灦鍒濆鍖栨湇鍔℃椂閿欒", - "60003": "妗嗘灦閿欒:ffmpeg瑙f瀽鍏冩暟鎹け璐,鏈緭鍑哄疄浣撲俊鎭", - "60004": "妗嗘灦閿欒:ffmpeg瑙f瀽鍏冩暟鎹繃绋嬮敊璇", - "60005": "妗嗘灦閿欒:ffmpeg瑙f瀽鍏冩暟鎹棰戜笉瀛樺湪", - "60006": "妗嗘灦閿欒:ffmpeg瑙f瀽鍏冩暟鎹湭鐢熸垚缂╃暐鍥", - "60007": "妗嗘灦閿欒:ffmpeg鏈惎鍔", - "60008": "妗嗘灦閿欒:ffmpeg鏈粨鏉", - "60009": "妗嗘灦閿欒:鍚敤椤圭洰鏈嬀閫夌敓鎴恱ml鏂囦欢", - "60010": "妗嗘灦閿欒:璇烽厤缃姩鎬佽矾鐢卞墠缂", - "60011": "妗嗘灦閿欒:鍔犲瘑鏂囦欢澶辫触", - "60012": "妗嗘灦閿欒:瑙e瘑鏂囦欢澶辫触", - "60013": "妗嗘灦閿欒:鎺у埗鍣ㄦ湭娣诲姞娉ㄩ噴", - "60014": "妗嗘灦閿欒:璺敱鏈坊鍔犳敞閲", - "60015": "妗嗘灦閿欒:娉ㄩ噴鏂囦欢瑙f瀽澶辫触", - "60016": "妗嗘灦閿欒:鑾峰彇瀹瑰櫒閿欒", - "60017": "妗嗘灦閿欒:閰嶇疆涓叕閽ユ棤鏁", - "60018": "妗嗘灦閿欒:閰嶇疆涓閽ユ棤鏁", - "60020": "妗嗘灦閿欒:鎵嬫満鍙风煭淇¢獙璇佺爜鍙戦佸け璐", - "60021": "妗嗘灦閿欒:DNS-TXT娣诲姞澶辫触", - "60022": "妗嗘灦閿欒:DNS-TXT鍒犻櫎澶辫触", - "60023": "妗嗘灦閿欒:HTTP涓嬭浇涓婁紶绫", - "61000": "妗嗘灦閿欒:HTTP灏佽鍖呭紓甯", - "62000": "妗嗘灦閿欒:寰俊寮鏀惧钩鍙板紓甯", - "63000": "妗嗘灦閿欒:鍒嗗簱鍒嗚〃璁捐閿欒" + "60000": "妗嗘灦寮傚父:瀹瑰櫒妯″潡閿欒", + "61000": "妗嗘灦寮傚父:HTTP妯″潡閿欒", + "62000": "妗嗘灦寮傚父:寰俊骞冲彴妯″潡閿欒", + "63000": "妗嗘灦寮傚父:鍒嗗簱鍒嗚〃妯″潡閿欒", + "64000": "妗嗘灦寮傚父:ffmpeg妯″潡閿欒", + "65000": "妗嗘灦寮傚父:闃块噷浜戝钩鍙版ā鍧楅敊璇", + "66000": "妗嗘灦寮傚父:鑵捐浜戝钩鍙版ā鍧楅敊璇", + "67000": "妗嗘灦寮傚父:鍔犲瘑瑙e瘑妯″潡閿欒" } \ No newline at end of file diff --git a/LingYanAspCoreFramework/Extensions/DeEncryptExtension.cs b/LingYanAspCoreFramework/Extensions/DeEncryptExtension.cs index 0859505..ea4a823 100644 --- a/LingYanAspCoreFramework/Extensions/DeEncryptExtension.cs +++ b/LingYanAspCoreFramework/Extensions/DeEncryptExtension.cs @@ -116,10 +116,10 @@ namespace LingYanAspCoreFramework.Extensions } } } - catch + catch(Exception ex) { File.Delete(path + ".temp"); - throw new CommonException(new ResponceBody(60011)); + throw new CommonException(new ResponceBody(67000, ex.Message)); } } @@ -167,10 +167,10 @@ namespace LingYanAspCoreFramework.Extensions } } } - catch + catch(Exception ex) { File.Delete(path + ".temp"); - throw new CommonException(new ResponceBody(60012)); + throw new CommonException(new ResponceBody(60000,ex.Message)); } } diff --git a/LingYanAspCoreFramework/Extensions/FFMpegExtension.cs b/LingYanAspCoreFramework/Extensions/FFMpegExtension.cs index 11bd847..163f866 100644 --- a/LingYanAspCoreFramework/Extensions/FFMpegExtension.cs +++ b/LingYanAspCoreFramework/Extensions/FFMpegExtension.cs @@ -38,13 +38,13 @@ namespace LingYanAspCoreFramework.Extensions Thread.Sleep(100); if (!started) { - throw new CommonException(new ResponceBody(60007)); + throw new CommonException(new ResponceBody(64000,"ffmpeg鍚姩澶辫触")); } // 绛夊緟杩涚▼閫鍑 bool exited = ffprobeProcess.WaitForExit(1000); // 澧炲姞绛夊緟鏃堕棿鍒10000姣锛10绉掞級 if (!exited) { - throw new CommonException(new ResponceBody(60008)); + throw new CommonException(new ResponceBody(64000,"ffmpeg閫鍑哄け璐")); } // 璇诲彇ffprobe鐨勬爣鍑嗚緭鍑烘祦 StreamReader outputReader = new StreamReader(ffprobeProcess.StandardOutput.BaseStream, Encoding.UTF8); @@ -59,7 +59,7 @@ namespace LingYanAspCoreFramework.Extensions var videoInfo = Newtonsoft.Json.JsonConvert.DeserializeObject(successOutput); if (videoInfo == null) { - throw new CommonException(new ResponceBody(60003)); + throw new CommonException(new ResponceBody(64000,"ffmpeg杈撳嚭json涓虹┖")); } return new ResponceBody(code: 20000, data: videoInfo); } @@ -104,7 +104,7 @@ namespace LingYanAspCoreFramework.Extensions // 妫鏌ヨ棰戞枃浠舵槸鍚﹀瓨鍦 if (!File.Exists(videoFilePath)) { - throw new CommonException(new ResponceBody(60005)); + throw new CommonException(new ResponceBody(64000,"瑙嗛涓嶅瓨鍦")); } using (var process = new Process()) { @@ -127,13 +127,13 @@ namespace LingYanAspCoreFramework.Extensions Thread.Sleep(100); if (!started) { - throw new CommonException(new ResponceBody(60007)); + throw new CommonException(new ResponceBody(64000,"鍚姩澶辫触")); } // 绛夊緟杩涚▼閫鍑 bool exited = process.WaitForExit(1000); // 澧炲姞绛夊緟鏃堕棿鍒10000姣锛10绉掞級 if (!exited) { - throw new CommonException(new ResponceBody(60008)); + throw new CommonException(new ResponceBody(64000,"閫鍑哄け璐")); } //璇诲彇ffprobe鐨勬爣鍑嗚緭鍑烘祦 StreamReader outputReader = new StreamReader(process.StandardOutput.BaseStream, Encoding.UTF8); @@ -147,7 +147,7 @@ namespace LingYanAspCoreFramework.Extensions // 妫鏌ヨ棰戞枃浠舵槸鍚﹀瓨鍦 if (!File.Exists(thumbPath)) { - throw new CommonException(new ResponceBody(60006)); + throw new CommonException(new ResponceBody(64000,"瑙嗛涓嶅瓨鍦")); } return new ResponceBody(data: thumbPath); } diff --git a/LingYanAspCoreFramework/Extensions/GeneralExtension.cs b/LingYanAspCoreFramework/Extensions/GeneralExtension.cs index 7b9d350..26e6425 100644 --- a/LingYanAspCoreFramework/Extensions/GeneralExtension.cs +++ b/LingYanAspCoreFramework/Extensions/GeneralExtension.cs @@ -143,7 +143,7 @@ namespace LingYanAspCoreFramework.Extensions { return (IServiceCollection)property.GetValue(builder); } - throw new CommonException(new ResponceBody(60016, attach: "builder鏈壘鍒癐ServiceCollection绫诲瀷")); + throw new CommonException(new ResponceBody(60000, attach: "builder鏈壘鍒癐ServiceCollection绫诲瀷")); } /// @@ -209,7 +209,7 @@ namespace LingYanAspCoreFramework.Extensions { OnChallenge = context => { - throw new CommonException(new ResponceBody(40001, "閴存潈涓嶉氳繃")); + throw new CommonException(new ResponceBody(41000, "閴存潈澶辫触")); } }; }); @@ -256,7 +256,7 @@ namespace LingYanAspCoreFramework.Extensions XmlNodeList xmlItems = ProjectHelper.GetRelevantXmlNodeList(projectXmlDocuments); if (xmlItems == null) { - throw new CommonException(new ResponceBody(60015)); + throw new CommonException(new ResponceBody(60000,"xml鏂囦欢娉ㄨВ淇℃伅鑾峰彇閿欒")); } // 寰幆鎵鏈夋帴鍙 foreach (var node in actionDescs) @@ -276,7 +276,7 @@ namespace LingYanAspCoreFramework.Extensions } else { - throw new CommonException(new ResponceBody(60013, node.ControllerFullName)); + throw new CommonException(new ResponceBody(60000, node.ControllerFullName+"鎺у埗鍣ㄦ湭娣诲姞娉ㄨВ")); } XmlNode apiNode = ProjectHelper.FindMatchingXmlNode(xmlItems, node.DisplayFullName); if (apiNode == null) @@ -289,7 +289,7 @@ namespace LingYanAspCoreFramework.Extensions } else { - throw new CommonException(new ResponceBody(60014, node.DisplayFullName)); + throw new CommonException(new ResponceBody(60000,node.DisplayFullName+"鎺ュ彛鏈坊鍔犳敞閲")); } } ProjectHelper.GenerateControllerXml(actionDescs); @@ -313,7 +313,7 @@ namespace LingYanAspCoreFramework.Extensions long? totalBytes = response.Content.Headers.ContentLength; if (!totalBytes.HasValue) { - throw new CommonException(new ResponceBody(60023, "鏃犳硶鑾峰彇鍐呭闀垮害")); + throw new CommonException(new ResponceBody(61000, "鏃犳硶鑾峰彇鍐呭闀垮害")); } byte[] buffer = new byte[4096]; diff --git a/LingYanAspCoreFramework/Extensions/IocExtension.cs b/LingYanAspCoreFramework/Extensions/IocExtension.cs index d6831dd..6a1adec 100644 --- a/LingYanAspCoreFramework/Extensions/IocExtension.cs +++ b/LingYanAspCoreFramework/Extensions/IocExtension.cs @@ -107,7 +107,7 @@ namespace LingYanAspCoreFramework.Extensions //娉ㄨВ閰嶇疆 if (!File.Exists(SampleHelper.BaseDir.GetLocalUrl(ProjectHelper.GetProjectName() + ".xml"))) { - throw new CommonException(new ResponceBody(60009)); + throw new CommonException(new ResponceBody(60000,"椤圭洰鏈嬀閫夌敓鎴愭椂xml娉ㄨВ鏂囦欢杈撳嚭")); } else { @@ -204,7 +204,7 @@ namespace LingYanAspCoreFramework.Extensions .Select(e => e.ErrorMessage) .ToList(); var errorMessage = string.Join("|", errors); - throw new CommonException(new ResponceBody(40004, errorMessage)); + throw new CommonException(new ResponceBody(43000, errorMessage)); }; }); LoggerHelper.DefaultLog("閰嶇疆琛ㄥ崟"); @@ -236,7 +236,7 @@ namespace LingYanAspCoreFramework.Extensions } catch (Exception ex) { - throw new CommonException(new ResponceBody(60001), ex); + throw new CommonException(new ResponceBody(60000,ex.Message), ex); } } @@ -311,7 +311,7 @@ namespace LingYanAspCoreFramework.Extensions } catch (Exception ex) { - throw new CommonException(new ResponceBody(60002), ex); + throw new CommonException(new ResponceBody(60000,ex.Message), ex); } } } diff --git a/LingYanAspCoreFramework/Helpers/AlibabaCloudHelper.cs b/LingYanAspCoreFramework/Helpers/AlibabaCloudHelper.cs index 91fefef..567315c 100644 --- a/LingYanAspCoreFramework/Helpers/AlibabaCloudHelper.cs +++ b/LingYanAspCoreFramework/Helpers/AlibabaCloudHelper.cs @@ -22,7 +22,7 @@ namespace LingYanAspCoreFramework.Helpers { if (!ValidateHelper.IsMobile(userPhone)) { - throw new CommonException(new ResponceBody(60020,"鎵嬫満鍙风爜楠岃瘉涓嶉氳繃")); + throw new CommonException(new ResponceBody(65000, "鎵嬫満鍙风爜楠岃瘉涓嶉氳繃")); } AlibabaCloud.SDK.Dysmsapi20170525.Client client = CreateCilentExtension(); AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest sendSmsRequest = new AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest @@ -40,7 +40,7 @@ namespace LingYanAspCoreFramework.Helpers } else { - throw new CommonException(new ResponceBody(60020, "鐭俊鍙戦佸け璐")); + throw new CommonException(new ResponceBody(65000, "鐭俊鍙戦佸け璐")); } } } diff --git a/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs b/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs index 0fb21b9..eb378d9 100644 --- a/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs +++ b/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs @@ -51,7 +51,7 @@ namespace LingYanAspCoreFramework.Helpers } else { - throw new CommonException(new ResponceBody(60020, "鐭俊鍙戦佸け璐")); + throw new CommonException(new ResponceBody(66000, "鐭俊鍙戦佸け璐")); } } public static async Task DomainAddTxTRecord(string domain, string recordValue) diff --git a/LingYanAspCoreFramework/SampleRoots/SampleGeneralHelper.cs b/LingYanAspCoreFramework/SampleRoots/SampleGeneralHelper.cs index d47991a..84c7984 100644 --- a/LingYanAspCoreFramework/SampleRoots/SampleGeneralHelper.cs +++ b/LingYanAspCoreFramework/SampleRoots/SampleGeneralHelper.cs @@ -57,7 +57,7 @@ namespace LingYanAspCoreFramework.SampleRoots int footerIndex = pem.IndexOf(footer); if (headerIndex == -1 || footerIndex == -1 || headerIndex >= footerIndex) { - throw new CommonException(new ResponceBody(60017)); + throw new CommonException(new ResponceBody(67000,"鎻愬彇鍏挜鍐呭澶辫触")); } // 鎻愬彇涓棿鐨勫瘑閽ラ儴鍒 string publicKey = pem.Substring(headerIndex, footerIndex - headerIndex).Trim(); @@ -78,7 +78,7 @@ namespace LingYanAspCoreFramework.SampleRoots int footerIndex = pem.IndexOf(footer); if (headerIndex == -1 || footerIndex == -1 || headerIndex >= footerIndex) { - throw new CommonException(new ResponceBody(60018)); + throw new CommonException(new ResponceBody(67000,"鎻愬彇绉侀挜鍐呭澶辫触")); } // 鎻愬彇涓棿鐨勫瘑閽ラ儴鍒 string publicKey = pem.Substring(headerIndex, footerIndex - headerIndex).Trim(); @@ -121,15 +121,15 @@ namespace LingYanAspCoreFramework.SampleRoots } catch (HttpRequestException ex) { - throw new CommonException(new ResponceBody(60023, ex.Message)); + throw new CommonException(new ResponceBody(61000, ex.Message)); } catch (IOException ex) { - throw new CommonException(new ResponceBody(60023, ex.Message)); + throw new CommonException(new ResponceBody(61000, ex.Message)); } catch (Exception ex) { - throw new CommonException(new ResponceBody(60023, ex.Message)); + throw new CommonException(new ResponceBody(61000, ex.Message)); } } } @@ -145,7 +145,7 @@ namespace LingYanAspCoreFramework.SampleRoots { if (httpDonwloadTasks == null || httpDonwloadTasks.Count == 0) { - throw new CommonException(new ResponceBody(60023, "鏂囦欢涓嬭浇浠诲姟鍒楄〃涓嶈兘涓虹┖鎴栨暟閲忎负0")); + throw new CommonException(new ResponceBody(61000, "鏂囦欢涓嬭浇浠诲姟鍒楄〃涓嶈兘涓虹┖鎴栨暟閲忎负0")); } long totalBytesReceived = 0; long totalBytesToReceive = 0; @@ -166,11 +166,11 @@ namespace LingYanAspCoreFramework.SampleRoots } catch (HttpRequestException ex) { - throw new CommonException(new ResponceBody(60023, $"鏃犳硶鑾峰彇鏂囦欢 {task.NetworkUrl} 鐨勫ぇ灏: {ex.Message}")); + throw new CommonException(new ResponceBody(61000, $"鏃犳硶鑾峰彇鏂囦欢 {task.NetworkUrl} 鐨勫ぇ灏: {ex.Message}")); } catch (Exception ex) { - throw new CommonException(new ResponceBody(60023, ex.Message)); + throw new CommonException(new ResponceBody(61000, ex.Message)); } } foreach (var task in httpDonwloadTasks) @@ -186,7 +186,7 @@ namespace LingYanAspCoreFramework.SampleRoots } catch (Exception ex) { - throw new CommonException(new ResponceBody(60023, $"涓嬭浇鏂囦欢 {task.NetworkUrl} 澶辫触: {ex.Message}")); + throw new CommonException(new ResponceBody(61000, $"涓嬭浇鏂囦欢 {task.NetworkUrl} 澶辫触: {ex.Message}")); } } } diff --git a/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs b/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs index a0ad1d0..80ca3b9 100644 --- a/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs +++ b/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs @@ -34,7 +34,7 @@ namespace LingYanAspCoreFramework.SampleRoots { if (requestHeaderToke != redisToken) { - context.Result = new OkObjectResult(new ResponceBody(40001, "Token琚姠鍗犲凡鍒锋柊", null)); + context.Result = new OkObjectResult(new ResponceBody(41000, "Token琚姠鍗犲凡鍒锋柊", null)); return; } } @@ -49,7 +49,7 @@ namespace LingYanAspCoreFramework.SampleRoots var userRolesJson =await RedisHelper.HGetAsync>(SampleHelper.RedisUserRole, userId); if (userRolesJson == null || userRolesJson.Count == 0) { - context.Result = new OkObjectResult(new ResponceBody(40002, "瑙掕壊鏈垎閰", null)); + context.Result = new OkObjectResult(new ResponceBody(42000, "瑙掕壊鏈垎閰", null)); return; } // 鑾峰彇姣忎釜瑙掕壊鐨勮矾鐢变俊鎭苟鍚堝苟 @@ -65,14 +65,14 @@ namespace LingYanAspCoreFramework.SampleRoots // 妫鏌ュ悎骞跺悗鐨勮矾鐢变俊鎭 if (allRequestRoutes == null || allRequestRoutes.Count == 0) { - context.Result = new OkObjectResult(new ResponceBody(40003, "鏉冮檺鏈垎閰", null)); + context.Result = new OkObjectResult(new ResponceBody(42000, "璺敱鏈垎閰", null)); return; } //鍒ゆ柇璺敱鏄惁鍦ㄨ鑹叉潈闄愬唴 var isInRole = allRequestRoutes.Any(a => a.RouteTemplate.ToLower().Equals(requestUrl)); if (!isInRole) { - context.Result = new OkObjectResult(new ResponceBody(40003, "鏉冮檺绾у埆涓嶈冻", null)); + context.Result = new OkObjectResult(new ResponceBody(42000, "鏉冮檺涓嶈冻", null)); return; } } diff --git a/LingYanAspCoreFramework/SampleRoots/SampleGlobalPermissionHandle.cs b/LingYanAspCoreFramework/SampleRoots/SampleGlobalPermissionHandle.cs index c6ee96f..11feab6 100644 --- a/LingYanAspCoreFramework/SampleRoots/SampleGlobalPermissionHandle.cs +++ b/LingYanAspCoreFramework/SampleRoots/SampleGlobalPermissionHandle.cs @@ -20,12 +20,12 @@ namespace LingYanAspCoreFramework.SampleRoots var userIdClaim = context.User.Claims.FirstOrDefault(f => f.Type == SampleHelper.ClaimUserId); if (userIdClaim == null) { - throw new CommonException(new ResponceBody(40001,"Token涓嶅睘浜庢湰绯荤粺")); + throw new CommonException(new ResponceBody(41000, "Token涓嶅睘浜庢湰绯荤粺")); } var userId = userIdClaim.Value.DecryptWithRSA(SampleHelper.RSAPrivateKey); if (string.IsNullOrEmpty(userId)) { - throw new CommonException(new ResponceBody(40001,"Token濂藉儚琚鏀")); + throw new CommonException(new ResponceBody(41000, "Token濂藉儚琚鏀")); } else { -- Gitee From e0e7a1326c5127d514752edf5b9688dea47c9263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Tue, 1 Apr 2025 15:06:00 +0800 Subject: [PATCH 09/17] =?UTF-8?q?=E5=B7=B2=E9=80=82=E9=85=8Dsqlserver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LingTest/DbContexts/StoreDbContext.cs | 2 +- LingTest/Entitys/Order.cs | 11 ++ LingTest/Entitys/Product.cs | 8 +- .../Migrations/20250331103436_v2.Designer.cs | 52 ------ LingTest/Migrations/20250331103436_v2.cs | 42 ----- .../Migrations/20250331104009_v3.Designer.cs | 60 ------- LingTest/Migrations/20250331104009_v3.cs | 78 --------- ...igner.cs => 20250401063233_v1.Designer.cs} | 42 ++++- ...50331103401_v1.cs => 20250401063233_v1.cs} | 61 +++++-- .../Migrations/StoreDbContextModelSnapshot.cs | 67 ++++++-- LingTest/RestApiModule.cs | 14 +- .../Extension/DynamicShardCoreExtension.cs | 4 +- .../Impl/DynamicShardConnectionResolver.cs | 42 +++-- .../Core/Impl/DynamicShardInfoGenerator.cs | 28 +--- .../EFCore/DynamicShardMigrationAssembly.cs | 9 +- .../EFCore/DynamicShardMigrationGenerator.cs | 158 ++++++++---------- .../DynamicShardModelCacheKeyFactory.cs | 3 +- .../EFCore/Impl/DynamicShardEntityBuilder.cs | 16 +- .../DynamicShard/Models/DynamicShardInfo.cs | 79 +-------- 19 files changed, 296 insertions(+), 480 deletions(-) delete mode 100644 LingTest/Migrations/20250331103436_v2.Designer.cs delete mode 100644 LingTest/Migrations/20250331103436_v2.cs delete mode 100644 LingTest/Migrations/20250331104009_v3.Designer.cs delete mode 100644 LingTest/Migrations/20250331104009_v3.cs rename LingTest/Migrations/{20250331103401_v1.Designer.cs => 20250401063233_v1.Designer.cs} (61%) rename LingTest/Migrations/{20250331103401_v1.cs => 20250401063233_v1.cs} (35%) diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index f1d6539..a581ef6 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -8,7 +8,7 @@ namespace LingTest.DbContexts public class StoreDbContext : DynamicShardDbContext { public DbSet Products => this.Set(); - //public DbSet Orders => this.Set(); + public DbSet Orders => this.Set(); public StoreDbContext(DbContextOptions options, DynamicShardOption dynamicShardOption, IServiceProvider serviceProvider) : base(options, dynamicShardOption, serviceProvider) { diff --git a/LingTest/Entitys/Order.cs b/LingTest/Entitys/Order.cs index b4a2c97..92d3e7a 100644 --- a/LingTest/Entitys/Order.cs +++ b/LingTest/Entitys/Order.cs @@ -1,8 +1,19 @@ 锘縰sing LingYanAspCoreFramework.BaseRoots; +using Microsoft.EntityFrameworkCore; namespace LingTest.Entitys { + [Index(nameof(TestIndex))] + [Index(nameof(TestIndex1),IsUnique =true)] + [Index(nameof(TestIndex2),nameof(TestIndex3))] + [Index(nameof(TestIndex1), nameof(TestIndex2),IsUnique =true)] public class Order:BaseEntity { + public string TestIndex { get; set; } + + public string TestIndex1 { get; set; } + public string TestIndex2 { get; set; } + public string TestIndex3 { get; set; } + public string TeaColm { get; set; } } } diff --git a/LingTest/Entitys/Product.cs b/LingTest/Entitys/Product.cs index 182c8bd..18c5e0b 100644 --- a/LingTest/Entitys/Product.cs +++ b/LingTest/Entitys/Product.cs @@ -3,8 +3,6 @@ using System.ComponentModel.DataAnnotations; namespace LingTest.Entitys { - [Index(nameof(TestIndex))] - [Index(nameof(TYes),IsUnique =true)] public class Product { [Key] @@ -13,9 +11,7 @@ namespace LingTest.Entitys [StringLength(50), Required] public string Name { get; set; } - public string TYes { get; set; } - - - public string TestIndex { get; set; } + public string Category { get; set; } + public int Price { get; set; } } } diff --git a/LingTest/Migrations/20250331103436_v2.Designer.cs b/LingTest/Migrations/20250331103436_v2.Designer.cs deleted file mode 100644 index ed6f168..0000000 --- a/LingTest/Migrations/20250331103436_v2.Designer.cs +++ /dev/null @@ -1,52 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(StoreDbContext))] - [Migration("20250331103436_v2")] - partial class v2 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("Category") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); - - b.HasKey("Id"); - - b.ToTable("Products", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20250331103436_v2.cs b/LingTest/Migrations/20250331103436_v2.cs deleted file mode 100644 index daa63fb..0000000 --- a/LingTest/Migrations/20250331103436_v2.cs +++ /dev/null @@ -1,42 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v2 : Migration - { - //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 - private readonly string dynamicShardPrefix; - public v2(string _dynamicShardPrefix) - { - this.dynamicShardPrefix = _dynamicShardPrefix; - } - - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: $"{this.dynamicShardPrefix}Orders"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: $"{this.dynamicShardPrefix}Orders", - columns: table => new - { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - CreateTimeStamp = table.Column(type: "bigint", nullable: false), - IsDeleted = table.Column(type: "bit", nullable: false) - }, - constraints: table => - { - table.PrimaryKey($"{this.dynamicShardPrefix}PK_Orders", x => x.Id); - }); - } - } -} diff --git a/LingTest/Migrations/20250331104009_v3.Designer.cs b/LingTest/Migrations/20250331104009_v3.Designer.cs deleted file mode 100644 index c81a8f1..0000000 --- a/LingTest/Migrations/20250331104009_v3.Designer.cs +++ /dev/null @@ -1,60 +0,0 @@ -锘// -using LingTest.DbContexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace LingTest.Migrations -{ - [DbContext(typeof(StoreDbContext))] - [Migration("20250331104009_v3")] - partial class v3 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("LingTest.Entitys.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); - - b.Property("TYes") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("TestIndex") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.HasKey("Id"); - - b.HasIndex("TYes") - .IsUnique(); - - b.HasIndex("TestIndex"); - - b.ToTable("Products", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/LingTest/Migrations/20250331104009_v3.cs b/LingTest/Migrations/20250331104009_v3.cs deleted file mode 100644 index f68842d..0000000 --- a/LingTest/Migrations/20250331104009_v3.cs +++ /dev/null @@ -1,78 +0,0 @@ -锘縰sing Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace LingTest.Migrations -{ - /// - public partial class v3 : Migration - { - //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 - private readonly string dynamicShardPrefix; - public v3(string _dynamicShardPrefix) - { - this.dynamicShardPrefix = _dynamicShardPrefix; - } - - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Category", - table: $"{this.dynamicShardPrefix}Products"); - - migrationBuilder.AddColumn( - name: "TYes", - table: $"{this.dynamicShardPrefix}Products", - type: "nvarchar(450)", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn( - name: "TestIndex", - table: $"{this.dynamicShardPrefix}Products", - type: "nvarchar(450)", - nullable: false, - defaultValue: ""); - - migrationBuilder.CreateIndex( - name: $"{this.dynamicShardPrefix}IX_Products_TestIndex", - table: "Products", - column: "TestIndex"); - - migrationBuilder.CreateIndex( - name: $"{this.dynamicShardPrefix}IX_Products_TYes", - table: "Products", - column: "TYes", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_Products_TestIndex", - table: $"{this.dynamicShardPrefix}Products"); - - migrationBuilder.DropIndex( - name: "IX_Products_TYes", - table: $"{this.dynamicShardPrefix}Products"); - - migrationBuilder.DropColumn( - name: "TYes", - table: $"{this.dynamicShardPrefix}Products"); - - migrationBuilder.DropColumn( - name: "TestIndex", - table: $"{this.dynamicShardPrefix}Products"); - - migrationBuilder.AddColumn( - name: "Category", - table: $"{this.dynamicShardPrefix}Products", - type: "nvarchar(50)", - maxLength: 50, - nullable: false, - defaultValue: ""); - } - } -} diff --git a/LingTest/Migrations/20250331103401_v1.Designer.cs b/LingTest/Migrations/20250401063233_v1.Designer.cs similarity index 61% rename from LingTest/Migrations/20250331103401_v1.Designer.cs rename to LingTest/Migrations/20250401063233_v1.Designer.cs index ee14488..a0c040a 100644 --- a/LingTest/Migrations/20250331103401_v1.Designer.cs +++ b/LingTest/Migrations/20250401063233_v1.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace LingTest.Migrations { [DbContext(typeof(StoreDbContext))] - [Migration("20250331103401_v1")] + [Migration("20250401063233_v1")] partial class v1 { /// @@ -38,9 +38,39 @@ namespace LingTest.Migrations b.Property("IsDeleted") .HasColumnType("bit"); + b.Property("TeaColm") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex1") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex2") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex3") + .IsRequired() + .HasColumnType("nvarchar(450)"); + b.HasKey("Id"); - b.ToTable("Orders", (string)null); + b.HasIndex("TestIndex"); + + b.HasIndex("TestIndex1") + .IsUnique(); + + b.HasIndex("TestIndex1", "TestIndex2") + .IsUnique(); + + b.HasIndex("TestIndex2", "TestIndex3"); + + b.ToTable("Orders", "dbo"); }); modelBuilder.Entity("LingTest.Entitys.Product", b => @@ -53,17 +83,19 @@ namespace LingTest.Migrations b.Property("Category") .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasColumnType("nvarchar(max)"); b.Property("Name") .IsRequired() .HasMaxLength(50) .HasColumnType("nvarchar(50)"); + b.Property("Price") + .HasColumnType("int"); + b.HasKey("Id"); - b.ToTable("Products", (string)null); + b.ToTable("Products", "dbo"); }); #pragma warning restore 612, 618 } diff --git a/LingTest/Migrations/20250331103401_v1.cs b/LingTest/Migrations/20250401063233_v1.cs similarity index 35% rename from LingTest/Migrations/20250331103401_v1.cs rename to LingTest/Migrations/20250401063233_v1.cs index 8c7a0dc..dbd06d7 100644 --- a/LingTest/Migrations/20250331103401_v1.cs +++ b/LingTest/Migrations/20250401063233_v1.cs @@ -8,52 +8,93 @@ namespace LingTest.Migrations public partial class v1 : Migration { //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 - private readonly string dynamicShardPrefix; - public v1(string _dynamicShardPrefix) + private readonly string shardingTablePrefix; + private readonly string shardingSchemaPostfix; + public v1(string _shardingTablePrefix,string _shardingSchemaPostfix) { - this.dynamicShardPrefix = _dynamicShardPrefix; + this.shardingTablePrefix = _shardingTablePrefix; + this.shardingSchemaPostfix = _shardingSchemaPostfix; } /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.EnsureSchema( + name: "dbo"); + migrationBuilder.CreateTable( - name: $"{this.dynamicShardPrefix}Orders", + name: $"{this.shardingTablePrefix}Orders", + schema: "dbo"+this.shardingSchemaPostfix, columns: table => new { Id = table.Column(type: "bigint", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), + TestIndex = table.Column(type: "nvarchar(450)", nullable: false), + TestIndex1 = table.Column(type: "nvarchar(450)", nullable: false), + TestIndex2 = table.Column(type: "nvarchar(450)", nullable: false), + TestIndex3 = table.Column(type: "nvarchar(450)", nullable: false), + TeaColm = table.Column(type: "nvarchar(max)", nullable: false), IsDeleted = table.Column(type: "bit", nullable: false), CreateTimeStamp = table.Column(type: "bigint", nullable: false) }, constraints: table => { - table.PrimaryKey($"{this.dynamicShardPrefix}PK_Orders", x => x.Id); + table.PrimaryKey($"{this.shardingTablePrefix}PK_Orders", x => x.Id); }); migrationBuilder.CreateTable( - name: $"{this.dynamicShardPrefix}Products", + name: $"{this.shardingTablePrefix}Products", + schema: "dbo"+this.shardingSchemaPostfix, columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), - Category = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) + Category = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "int", nullable: false) }, constraints: table => { - table.PrimaryKey($"{this.dynamicShardPrefix}PK_Products", x => x.Id); + table.PrimaryKey($"{this.shardingTablePrefix}PK_Products", x => x.Id); }); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + column: "TestIndex"); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + column: "TestIndex1", + unique: true); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1_TestIndex2", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + columns: new[] { "TestIndex1", "TestIndex2" }, + unique: true); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex2_TestIndex3", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + columns: new[] { "TestIndex2", "TestIndex3" }); } /// protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: $"{this.dynamicShardPrefix}Orders"); + name: $"{this.shardingTablePrefix}Orders", + schema: "dbo"+this.shardingSchemaPostfix); migrationBuilder.DropTable( - name: $"{this.dynamicShardPrefix}Products"); + name: $"{this.shardingTablePrefix}Products", + schema: "dbo"+this.shardingSchemaPostfix); } } } diff --git a/LingTest/Migrations/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/StoreDbContextModelSnapshot.cs index 5cf37ef..4a12fb9 100644 --- a/LingTest/Migrations/StoreDbContextModelSnapshot.cs +++ b/LingTest/Migrations/StoreDbContextModelSnapshot.cs @@ -21,35 +21,78 @@ namespace LingTest.Migrations SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("LingTest.Entitys.Product", b => + modelBuilder.Entity("LingTest.Entitys.Order", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int"); + .HasColumnType("bigint"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("Name") + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TeaColm") .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasColumnType("nvarchar(max)"); - b.Property("TYes") + b.Property("TestIndex") .IsRequired() .HasColumnType("nvarchar(450)"); - b.Property("TestIndex") + b.Property("TestIndex1") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex2") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex3") .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("Id"); - b.HasIndex("TYes") + b.HasIndex("TestIndex"); + + b.HasIndex("TestIndex1") .IsUnique(); - b.HasIndex("TestIndex"); + b.HasIndex("TestIndex1", "TestIndex2") + .IsUnique(); + + b.HasIndex("TestIndex2", "TestIndex3"); + + b.ToTable("Orders", "dbo"); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Price") + .HasColumnType("int"); + + b.HasKey("Id"); - b.ToTable("Products", (string)null); + b.ToTable("Products", "dbo"); }); #pragma warning restore 612, 618 } diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index 91900cb..39f9582 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -20,17 +20,17 @@ namespace LingTest dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => { var connectionTupple = new Tuple(ConnectionType.SqlServer, - $"Server=192.168.148.131;Database=testmssql;User Id=sa;Password=12345678;Encrypt=False;"); + $"Server=192.168.148.131;Database={dynamicShardInfo.ShardingDataBase};User Id=sa;Password=12345678;Encrypt=False;"); LoggerHelper.DefaultLog(connectionTupple.Item1.ToString() + connectionTupple.Item2); return connectionTupple; }; - //dynamicSettingsFunc.SchemaNameFunc = (dynamicShardInfo) => - //{ - // return dynamicShardInfo?.Name??"test"; - //}; - dynamicSettingsFunc.TablePrefixFunc = (tenantinfo) => + dynamicSettingsFunc.SchemaNameFunc = (dynamicShardInfo) => { - return tenantinfo?.Name; + return dynamicShardInfo?.ShardingSchema; + }; + dynamicSettingsFunc.TablePrefixFunc = (dynamicShardInfo) => + { + return dynamicShardInfo?.ShardingTable; }; }); diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs index 167e407..665bfab 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs @@ -87,9 +87,9 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension { if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { - builder.MigrationsHistoryTable("_EFMigrationHistory", dynamicShardOption.SchemaName); + builder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", dynamicShardOption.SchemaName); } - else + else if (!string.IsNullOrEmpty(dynamicShardOption.TablePrefix)) { builder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory"); } diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs index 28c3cb0..a080724 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs @@ -28,22 +28,44 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Impl public DynamicShardOption GetConnection() { var connectionTupple = this.dynamicShardSettings.ConnectionNameFunc?.Invoke(this.serviceProvider, this.dynamicShardInfo); - dynamicShardOption.Key = this.dynamicShardSettings.Key; dynamicShardOption.ConnectionType = connectionTupple.Item1; - dynamicShardOption.ConnectionName = connectionTupple.Item2; - var SchemaName = this.dynamicShardSettings.SchemaNameFunc?.Invoke(this.dynamicShardInfo); - dynamicShardOption.SchemaName = SchemaName; - if (dynamicShardOption.ConnectionType == ConnectionType.Mysql && !string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + dynamicShardOption.ConnectionName = connectionTupple.Item2; + dynamicShardOption.SchemaName = this.dynamicShardSettings.SchemaNameFunc?.Invoke(this.dynamicShardInfo); + switch (dynamicShardOption.ConnectionType) { - throw new CommonException(new ResponceBody(63000, $"{dynamicShardOption.ConnectionType.ToString()}不支持按模式分片")); + case ConnectionType.Mysql: + if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + { + throw new CommonException(new ResponceBody(63000, $"{dynamicShardOption.ConnectionType.ToString()}数据库不支持模式分片")); + } + break; + case ConnectionType.SqlServer: + if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + { + dynamicShardOption.SchemaName = "dbo." + dynamicShardOption.SchemaName; + } + else + { + dynamicShardOption.SchemaName = "dbo"; + } + break; + case ConnectionType.PostgreSql: + if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + { + dynamicShardOption.SchemaName = "public." + dynamicShardOption.SchemaName; + } + else + { + dynamicShardOption.SchemaName = "public"; + } + break; } - var tablePreFix = this.dynamicShardSettings.TablePrefixFunc?.Invoke(this.dynamicShardInfo); - if (!string.IsNullOrEmpty(tablePreFix)) + dynamicShardOption.TablePrefix = this.dynamicShardSettings.TablePrefixFunc?.Invoke(this.dynamicShardInfo); + if (!string.IsNullOrEmpty(dynamicShardOption.TablePrefix)) { - tablePreFix += "_"; + dynamicShardOption.TablePrefix += "_"; } - dynamicShardOption.TablePrefix = tablePreFix; return this.dynamicShardOption; } } diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs index 71febae..b1dd45d 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardInfoGenerator.cs @@ -1,6 +1,5 @@ using LingYanAspCoreFramework.DynamicShard.Core.Interface; using LingYanAspCoreFramework.DynamicShard.Models; -using LingYanAspCoreFramework.Helpers; using Microsoft.AspNetCore.Http; namespace LingYanAspCoreFramework.DynamicShard.Core.Impl @@ -15,26 +14,17 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Impl public DynamicShardInfo GenerateDynamicShardInfo(object sender, HttpContext httpContext) { - if (!this.dynamicShardInfo.IsPresent) + if (this.dynamicShardInfo.RequestOtherHeader==null) { - foreach (var item in httpContext.Request.Headers) - { - if (item.Key.ToLower().StartsWith("DynamicShard".ToLower())) - { - this.dynamicShardInfo.TrySetMember(new DynamicSetMemberBinder(item.Key), item.Value); - this.dynamicShardInfo.TryGetMember(new DynamicGetMemberBinder(item.Key), out object value); - LoggerHelper.DefaultLog(value.ToString()); - } - } - var dynamicShardKey = httpContext?.Request?.Headers["DynamicShardKey"]; - if (!string.IsNullOrEmpty(dynamicShardKey)) - { - this.dynamicShardInfo.IsPresent = true; - this.dynamicShardInfo.Name = dynamicShardKey; - this.dynamicShardInfo.Generator = this; - } + this.dynamicShardInfo.RequestOtherHeader = new Dictionary(); } - + foreach (var item in httpContext.Request.Headers) + { + this.dynamicShardInfo.RequestOtherHeader.TryAdd(item.Key, item.Value); + } + this.dynamicShardInfo.ShardingDataBase = httpContext?.Request?.Headers["ShardingDataBase"]; + this.dynamicShardInfo.ShardingSchema = httpContext?.Request?.Headers["ShardingSchema"]; + this.dynamicShardInfo.ShardingTable = httpContext?.Request?.Headers["ShardingTable"]; return this.dynamicShardInfo; } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs index 670e339..0a06ffd 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs @@ -25,11 +25,16 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore throw new CommonException(new ResponceBody(63000, $"杩佺Щ鏃,{nameof(activeProvider)}鍙傛暟涓虹┖")); var hasCtorWithSchema = migrationClass - .GetConstructor(new[] { typeof(string) }) != null; + .GetConstructor(new[] { typeof(string),typeof(string) }) != null; if (hasCtorWithSchema && context is IDynamicShardtDbContext dynamicShardtDbContext) { - var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), dynamicShardtDbContext?.DynamicShardOptionImpl?.TablePrefix); + string schemaPostFix = ""; + if (!string.IsNullOrEmpty(dynamicShardtDbContext.DynamicShardOptionImpl.SchemaName) && dynamicShardtDbContext.DynamicShardOptionImpl.SchemaName.StartsWith("dbo.")) + { + schemaPostFix = dynamicShardtDbContext.DynamicShardOptionImpl.SchemaName.Replace("dbo", ""); + } + var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), dynamicShardtDbContext?.DynamicShardOptionImpl?.TablePrefix, schemaPostFix); instance.ActiveProvider = activeProvider; return instance; } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs index 0cd1a1a..76df082 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationGenerator.cs @@ -17,146 +17,120 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore IReadOnlyList downOperations) { // 鏋勯犲櫒鍙傛暟 - string ctorStr = "dynamicShardPrefix"; + string ctorTableStr = "shardingTablePrefix"; + string ctorSchemaStr = "shardingSchemaPostfix"; //杩佺Щ鐢熸垚鍣ㄨ繍琛屽墠缃坊鍔犺〃鍚嶅崰浣嶇 - GernateBeforeUpDownTableName(ctorStr, upOperations, downOperations); + GernateBeforeUpDownTableName(ctorTableStr, ctorSchemaStr, upOperations, downOperations); //杩佺Щ鐢熸垚鍣ㄨ繍琛 var generatedCode = base.GenerateMigration(migrationNamespace, migrationName, upOperations, downOperations); //鏋勯犲櫒浠g爜鎻掑叆杩佺Щ浠g爜 - generatedCode = GernateAfterCtor(ctorStr, migrationName, generatedCode); + generatedCode = GernateAfterCtor(ctorTableStr, ctorSchemaStr, migrationName, generatedCode); //鏇挎崲琛ㄥ悕涓殑鍗犱綅绗 - generatedCode = generatedCode.Replace($"\"${ctorStr}", $"$\"{{this.{ctorStr}}}"); + generatedCode = generatedCode.Replace($"\"${ctorTableStr}", $"$\"{{this.{ctorTableStr}}}"); + //鏇挎崲妯″紡涓殑鍗犱綅绗 + generatedCode = generatedCode.Replace($"${ctorSchemaStr}\"", $"\"+this.{ctorSchemaStr}"); return generatedCode; } - private void GernateBeforeUpDownTableName(string ctorStr, + private void GernateBeforeUpDownTableName(string ctorTableStr, string ctorSchemaStr, IReadOnlyList upOperations, IReadOnlyList downOperations) { - // 涓 upOperations 涓厤缃姩鎬佽〃鍚 + // 淇敼涓厤缃姩鎬佽〃鍚 foreach (var operation in upOperations) { - if (operation is CreateTableOperation createTableOperation) + TableOperationModify(operation); + } + + // 鍥炴粴涓厤缃姩鎬佽〃鍚 + foreach (var operation in downOperations) + { + TableOperationModify(operation); + } + //琛ㄣ佺储寮曘侀敭銆佺害鏉熴佸垪瀵硅薄淇敼 + void TableOperationModify(MigrationOperation migrationOperation) + { + if (migrationOperation is CreateTableOperation createTableOperation) { - createTableOperation.Name = GernateBeforeTableName(ctorStr, createTableOperation.Name); - createTableOperation.PrimaryKey.Name = GernateBeforeTableName(ctorStr, createTableOperation.PrimaryKey.Name); + createTableOperation.Name = GernateBeforeTableName(createTableOperation.Name); + createTableOperation.PrimaryKey.Name = GernateBeforeTableName(createTableOperation.PrimaryKey.Name); foreach (var foreignKey in createTableOperation.ForeignKeys) { - foreignKey.Name = GernateBeforeTableName(ctorStr, foreignKey.Name); + foreignKey.Name = GernateBeforeTableName(foreignKey.Name); } foreach (var uniqueConstraint in createTableOperation.UniqueConstraints) { - uniqueConstraint.Name = GernateBeforeTableName(ctorStr, uniqueConstraint.Name); + uniqueConstraint.Name = GernateBeforeTableName(uniqueConstraint.Name); } } - else if (operation is RenameTableOperation renameTableOperation) - { - renameTableOperation.NewName = GernateBeforeTableName(ctorStr, renameTableOperation.NewName); - } - else if (operation is DropTableOperation dropTableOperation) - { - dropTableOperation.Name = GernateBeforeTableName(ctorStr, dropTableOperation.Name); - } - else if (operation is AlterTableOperation alterTableOperation) - { - alterTableOperation.Name = GernateBeforeTableName(ctorStr, alterTableOperation.Name); - } - else if (operation is AddForeignKeyOperation addForeignKeyOperation) + else if (migrationOperation is RenameTableOperation renameTableOperation) { - addForeignKeyOperation.Name = GernateBeforeTableName(ctorStr, addForeignKeyOperation.Name); + renameTableOperation.Name = GernateBeforeTableName(renameTableOperation.Name); + renameTableOperation.NewName = GernateBeforeTableName(renameTableOperation.NewName); } - else if (operation is AddPrimaryKeyOperation addPrimaryKeyOperation) + else if (migrationOperation is DropTableOperation dropTableOperation) { - addPrimaryKeyOperation.Name = GernateBeforeTableName(ctorStr, addPrimaryKeyOperation.Name); + dropTableOperation.Name = GernateBeforeTableName(dropTableOperation.Name); } - else if (operation is AddUniqueConstraintOperation addUniqueConstraintOperation) + else if (migrationOperation is AlterTableOperation alterTableOperation) { - addUniqueConstraintOperation.Name = GernateBeforeTableName(ctorStr, addUniqueConstraintOperation.Name); - } - else if (operation is CreateIndexOperation createIndexOperation) - { - createIndexOperation.Name = GernateBeforeTableName(ctorStr, createIndexOperation.Name); + alterTableOperation.Name = GernateBeforeTableName(alterTableOperation.Name); } else { - var columnTableName = operation.GetPropertyValue("Table"); - if (!string.IsNullOrEmpty(columnTableName)) + //鍒楄〃鍚 + var tableName = migrationOperation.GetPropertyValue("Table"); + if (!string.IsNullOrEmpty(tableName)) { - operation.SetPropertyValue("Table", GernateBeforeTableName(ctorStr, columnTableName)); + migrationOperation.SetPropertyValue("Table", GernateBeforeTableName(tableName)); } - } - } - // 涓 downOperations 涓厤缃姩鎬佽〃鍚 - foreach (var operation in downOperations) - { - if (operation is CreateTableOperation createTableOperation) - { - createTableOperation.Name = GernateBeforeTableName(ctorStr, createTableOperation.Name); - createTableOperation.PrimaryKey.Name = GernateBeforeTableName(ctorStr, createTableOperation.PrimaryKey.Name); - foreach (var foreignKey in createTableOperation.ForeignKeys) + if (migrationOperation is AddForeignKeyOperation addForeignKeyOperation) { - foreignKey.Name = GernateBeforeTableName(ctorStr, foreignKey.Name); + addForeignKeyOperation.Name = GernateBeforeTableName(addForeignKeyOperation.Name); } - foreach (var uniqueConstraint in createTableOperation.UniqueConstraints) + else if (migrationOperation is AddPrimaryKeyOperation addPrimaryKeyOperation) { - uniqueConstraint.Name = GernateBeforeTableName(ctorStr, uniqueConstraint.Name); + addPrimaryKeyOperation.Name = GernateBeforeTableName(addPrimaryKeyOperation.Name); } - } - else if (operation is RenameTableOperation renameTableOperation) - { - renameTableOperation.NewName = GernateBeforeTableName(ctorStr, renameTableOperation.NewName); - } - else if (operation is DropTableOperation dropTableOperation) - { - dropTableOperation.Name = GernateBeforeTableName(ctorStr, dropTableOperation.Name); - } - else if (operation is AlterTableOperation alterTableOperation) - { - alterTableOperation.Name = GernateBeforeTableName(ctorStr, alterTableOperation.Name); - } - else if (operation is AddForeignKeyOperation addForeignKeyOperation) - { - addForeignKeyOperation.Name = GernateBeforeTableName(ctorStr, addForeignKeyOperation.Name); - } - else if (operation is AddPrimaryKeyOperation addPrimaryKeyOperation) - { - addPrimaryKeyOperation.Name = GernateBeforeTableName(ctorStr, addPrimaryKeyOperation.Name); - } - else if (operation is AddUniqueConstraintOperation addUniqueConstraintOperation) - { - addUniqueConstraintOperation.Name = GernateBeforeTableName(ctorStr, addUniqueConstraintOperation.Name); - } - else if (operation is CreateIndexOperation createIndexOperation) - { - createIndexOperation.Name = GernateBeforeTableName(ctorStr, createIndexOperation.Name); - } - else - { - var columnTableName = operation.GetPropertyValue("Table"); - if (!string.IsNullOrEmpty(columnTableName)) + else if (migrationOperation is AddUniqueConstraintOperation addUniqueConstraintOperation) { - operation.SetPropertyValue("Table", GernateBeforeTableName(ctorStr, columnTableName)); + addUniqueConstraintOperation.Name = GernateBeforeTableName(addUniqueConstraintOperation.Name); } + else if (migrationOperation is CreateIndexOperation createIndexOperation) + { + createIndexOperation.Name = GernateBeforeTableName(createIndexOperation.Name); + + } + } + //妯″紡鍚 + var schemaName = migrationOperation.GetPropertyValue("Schema"); + if (!string.IsNullOrEmpty(schemaName)) + { + migrationOperation.SetPropertyValue("Schema", GernateAftoreSchemaName(schemaName)); } } - // 鍏朵粬绫诲瀷鐨勬搷浣滃彲浠ュ湪 - //鐢熸垚琛ㄦ槑鍗犱綅绗 - string GernateBeforeTableName(string ctorKey, string tableName) + //鐢熸垚琛ㄥ悕鍗犱綅绗 + string GernateBeforeTableName(string tableName) + { + return $"${ctorTableStr}{tableName}"; + } + string GernateAftoreSchemaName(string schemaName) { - LoggerHelper.DefaultLog(ctorKey+ tableName); - return $"${ctorKey}{tableName}"; + return $"{schemaName}${ctorSchemaStr}"; } } - private string GernateAfterCtor(string ctorStr, string migrationName, string generatedCode) + private string GernateAfterCtor(string ctorTableStr, string ctorSchemaStr, string migrationName, string generatedCode) { IndentedStringBuilder dynamicShardingCodeBuilder = new IndentedStringBuilder(); dynamicShardingCodeBuilder.IncrementIndent().IncrementIndent().AppendLine(); dynamicShardingCodeBuilder.AppendLine("//DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁"); - dynamicShardingCodeBuilder.AppendLine($"private readonly string {ctorStr};"); - dynamicShardingCodeBuilder.AppendLine($"public {migrationName}(string _{ctorStr})"); + dynamicShardingCodeBuilder.AppendLine($"private readonly string {ctorTableStr};"); + dynamicShardingCodeBuilder.AppendLine($"private readonly string {ctorSchemaStr};"); + dynamicShardingCodeBuilder.AppendLine($"public {migrationName}(string _{ctorTableStr},string _{ctorSchemaStr})"); dynamicShardingCodeBuilder.AppendLine("{"); - dynamicShardingCodeBuilder.IncrementIndent().AppendLine($"this.{ctorStr} = _{ctorStr};").DecrementIndent(); + dynamicShardingCodeBuilder.IncrementIndent().AppendLine($"this.{ctorTableStr} = _{ctorTableStr};").DecrementIndent(); + dynamicShardingCodeBuilder.IncrementIndent().AppendLine($"this.{ctorSchemaStr} = _{ctorSchemaStr};").DecrementIndent(); dynamicShardingCodeBuilder.AppendLine("}"); var classIndex = generatedCode.IndexOf($"public partial class {migrationName}"); if (classIndex > 0) diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs index c1ad2f7..603450f 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs @@ -38,7 +38,8 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore else { var dbContext = context as TDbContext; - var dynamicShardModelCacheKey = new DynamicShardModelCacheKey(dbContext, dbContext?.DynamicShardOptionImpl?.TablePrefix ?? "no_dynamicshard_identifier"); + var dynamicShardModelCacheKey = new DynamicShardModelCacheKey(dbContext, + dbContext?.DynamicShardOptionImpl?.SchemaName + dbContext?.DynamicShardOptionImpl?.TablePrefix ?? "no_dynamicshard_identifier"); return dynamicShardModelCacheKey; } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs index c51797a..fb7ef92 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs @@ -1,5 +1,6 @@ using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using LingYanAspCoreFramework.DynamicShard.Models; +using LingYanAspCoreFramework.DynamicShard.Models.Enum; using Microsoft.EntityFrameworkCore; namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl @@ -38,14 +39,15 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl { var entity = modelBuilder.Entity(property.PropertyType); var tableName = dynamicShardOption.TablePrefix + property.PropertyName; - if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + switch (dynamicShardOption.ConnectionType) { - entity.ToTable(tableName, dynamicShardOption.SchemaName); - - } - else - { - entity.ToTable(tableName); + case ConnectionType.Mysql: + entity.ToTable(tableName); + break; + case ConnectionType.SqlServer: + case ConnectionType.PostgreSql: + entity.ToTable(tableName, this.dynamicShardOption.SchemaName); + break; } } } diff --git a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs index 71efd5e..ebe567f 100644 --- a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardInfo.cs @@ -3,82 +3,13 @@ namespace LingYanAspCoreFramework.DynamicShard.Models { /// - /// 绉熸埛杩愯鏃朵俊鎭 + /// 鍒嗛厤淇℃伅 /// public class DynamicShardInfo : DynamicObject { - /// - /// 绉熸埛鍚嶇О - /// - public string Name { get; set; } - - /// - /// 绉熸埛鏄惁瀛樺湪 - /// - public bool IsPresent { get; set; } - - /// - /// 绉熸埛鐢熸垚鍣 - /// - public object Generator { get; set; } - - /// - /// 鍐呴儴瀛楀吀锛岀敤浜庡瓨鍌ㄥ姩鎬佸睘鎬 - /// - private Dictionary dictionary = new Dictionary(); - - /// - /// 灏濊瘯鑾峰彇鍔ㄦ佹垚鍛樼殑鍊 - /// - /// 鎴愬憳缁戝畾鍣 - /// 鎴愬憳鐨勫 - /// 濡傛灉鑾峰彇鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - return dictionary.TryGetValue(binder.Name, out result); - } - - /// - /// 灏濊瘯璁剧疆鍔ㄦ佹垚鍛樼殑鍊 - /// - /// 鎴愬憳缁戝畾鍣 - /// 瑕佽缃殑鍊 - /// 濡傛灉璁剧疆鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false - public override bool TrySetMember(SetMemberBinder binder, object value) - { - dictionary[binder.Name] = value; - return true; - } - - /// - /// 灏濊瘯璁剧疆鍔ㄦ佺储寮曠殑鍊 - /// - /// 绱㈠紩缁戝畾鍣 - /// 绱㈠紩鏁扮粍 - /// 瑕佽缃殑鍊 - /// 濡傛灉璁剧疆鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false - public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value) - { - if (!(indexes?.Length > 0)) - return false; - dictionary[indexes[0].ToString()] = value; - return true; - } - - /// - /// 灏濊瘯鑾峰彇鍔ㄦ佺储寮曠殑鍊 - /// - /// 绱㈠紩缁戝畾鍣 - /// 绱㈠紩鏁扮粍 - /// 绱㈠紩鐨勫 - /// 濡傛灉鑾峰彇鎴愬姛锛岃繑鍥 true锛涘惁鍒欒繑鍥 false - public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result) - { - result = null; - if (!(indexes?.Length > 0)) - return false; - result = dictionary[indexes[0].ToString()]; - return true; - } + public string ShardingDataBase { get; set; } + public string ShardingSchema { get; set; } + public string ShardingTable { get; set; } + public Dictionary RequestOtherHeader { get; set; } } } -- Gitee From af02356fb2a93a6b0506a89de400448b0cd26d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Tue, 1 Apr 2025 15:51:35 +0800 Subject: [PATCH 10/17] =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/slnx.sqlite | Bin 1228800 -> 1228800 bytes README.en.md | 36 ------------------------------------ 2 files changed, 36 deletions(-) delete mode 100644 README.en.md diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index 042597b3e3c26e68ee449aa2e35f3fc335e18d7a..f0cd908ad958f9726c81f3ac4030f4b438b9b0f7 100644 GIT binary patch literal 1228800 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%0Drv)bQSNe9??PdR`e4ktK>kvC-${e^cL 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 delta 164960 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 jCJJ0VWx6SyYbczxow^t~(cM$3H(W2*lb~S3qZ Date: Tue, 1 Apr 2025 16:08:51 +0800 Subject: [PATCH 11/17] =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b7d115..ef45762 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,26 @@ -# LingYan_ASPFramework +# LingYanASPFramework -#### 浠嬬粛 -閫傜敤浜嶢sp.Net Core鐨勫悗绔鏋 +#### 妗嗘灦绠浠 + +##### 浜ゆ祦瀛︿範 + +QQ缇わ細571927325锛屽井淇★細13028792031 + +##### 鍏充簬LingYanAspCoreFramework + +璇ユ鏋剁敱.NET棰嗗煙寮鍙戜汉鍛-鍙告櫒鑰佸笀-寮鍙戯紝閫傚簲浜嶤#绯诲垪鍚勭鏂瑰悜锛岄噰鐢―DD棰嗗煙椹卞姩鎬濇兂锛岀壒鎬ц嚜鍔ㄦ敞鍏ユ湇鍔℃浛浠f墜鍐欐敞鍏ユ湇鍔★紝妯″潡鍖栧熀绫讳繚璇佹瘡涓灞傞兘鍙互杩涜鑷畾涔夋墿灞曠殑鏈嶅姟娉ㄥ叆鍜屾湇鍔″垵濮嬪寲锛岄拡瀵笶FCore灏佽甯歌浣跨敤鐨勫伐浣滃崟鍏冧笌浠撳偍鍙婂垎椤靛姛鑳斤紝鑷爺鍩轰簬EFCore鐨勫姩鎬佸垎搴撳垎妯″紡鍒嗚〃妗嗘灦浠ラ傚簲澶ф暟鎹満鏅佸绉熸埛鍦烘櫙銆 + +瀹樼綉锛歔鐏电嚂绌洪棿瀹樼綉](https://www.lingyanspace.com/) + +B绔欙細[鐏电嚂绌洪棿B绔橾(https://space.bilibili.com/237905456) + +鍗氬锛歔鐏电嚂绌洪棿鍗氬](https://book.lingyanspace.com/) #### 浣跨敤璇存槑 + 1銆佹ā鍧楀寲娉ㄥ叆锛屾ā鍧楀唴绫诲簲褰撶户鎵緽aseModule锛屾鎶借薄绫绘湁涓夌褰㈠紡 BaseModule绫诲瀷 + ``` public class RESTApiModule : BaseModule { -- Gitee From 9d4c14e2012e684b3b3fe119f162a9c7b840df05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Tue, 1 Apr 2025 17:59:43 +0800 Subject: [PATCH 12/17] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E6=97=A7=E7=89=88?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E6=96=B9=E6=A1=88=E5=AE=9E=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/VSWorkspaceState.json | 5 +- .vs/slnx.sqlite | Bin 1228800 -> 1228800 bytes .../Controllers/DynamicShardingController .cs | 2 - LingTest/DynamicRoute/TestDynamicRoute.cs | 23 +++++++++ LingTest/Helper/TestHelper.cs | 14 ++++++ LingTest/LingTest.csproj | 2 +- LingTest/RestApiModule.cs | 16 +++++-- LingTest/TestProvider/ITestService.cs | 9 ++++ LingTest/TestProvider/TestService.cs | 15 ++++++ .../Attributes/LYDbContextAttribute.cs | 26 +--------- .../BaseRoots/BaseSysOwner.cs | 11 ----- .../BaseRoots/BaseSysOwnerTenantConfig.cs | 16 ------- .../Extensions/ProjectGetExtension.cs | 45 ++---------------- .../Extensions/ProjectRegisterExtension.cs | 4 +- .../Helpers/TencentCloudHelper.cs | 2 - .../Models/RuntimeCacheModel.cs | 11 ++--- 16 files changed, 85 insertions(+), 116 deletions(-) create mode 100644 LingTest/DynamicRoute/TestDynamicRoute.cs create mode 100644 LingTest/Helper/TestHelper.cs create mode 100644 LingTest/TestProvider/ITestService.cs create mode 100644 LingTest/TestProvider/TestService.cs delete mode 100644 LingYanAspCoreFramework/BaseRoots/BaseSysOwner.cs delete mode 100644 LingYanAspCoreFramework/BaseRoots/BaseSysOwnerTenantConfig.cs diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json index ed270fe..d736dd5 100644 --- a/.vs/VSWorkspaceState.json +++ b/.vs/VSWorkspaceState.json @@ -1,7 +1,8 @@ { "ExpandedNodes": [ - "" + "", + "\\LingYanAspCoreFramework" ], - "SelectedNode": "\\LingYanAspCoreFramework.sln", + "SelectedNode": "\\.gitattributes", "PreviewInSolutionExplorer": false } \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index f0cd908ad958f9726c81f3ac4030f4b438b9b0f7..8a2a6ce8336e65eee11c06c4ba3a958697b6073e 100644 GIT binary patch delta 159 zcmZo@@NQ`Eo*>P*bfSzi#l+EK z%*M*#++n=gP9}$O@;hk-zL3=7l9(8u%)E3EYjS~Kf n2E^<@%mKulK+FZi+(66&#JoVv2gLk9EC9rU+gHg7g)IUAuyQxw delta 149 zcmZo@@NQ`Eo*>P*WTK2S - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index 39f9582..0b454ff 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -11,12 +11,10 @@ namespace LingTest { public override void ARegisterModule(WebApplicationBuilder services) { - //鍒嗗簱 - //鍒嗘ā寮 - //鍒嗚〃 + //娉ㄥ唽鍔ㄦ佸垎搴撳垎妯″紡鍒嗚〃璁捐 services.Services.AddDynamicSharding(dynamicSettingsFunc => { - + //鍒嗗簱濮旀墭-杩斿洖杩炴帴瀛楃涓诧紙string锛 dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => { var connectionTupple = new Tuple(ConnectionType.SqlServer, @@ -24,21 +22,29 @@ namespace LingTest LoggerHelper.DefaultLog(connectionTupple.Item1.ToString() + connectionTupple.Item2); return connectionTupple; }; + //鍒嗘ā寮忓鎵-杩斿洖妯″紡鍚嶏紙string锛 dynamicSettingsFunc.SchemaNameFunc = (dynamicShardInfo) => { return dynamicShardInfo?.ShardingSchema; }; + //鍒嗚〃濮旀墭-杩斿洖鍒嗚〃鍓嶇紑锛坰tring锛 dynamicSettingsFunc.TablePrefixFunc = (dynamicShardInfo) => { return dynamicShardInfo?.ShardingTable; }; - }); } public override void BInitializationModule(WebApplication provider) { + + //浣跨敤鍔ㄦ佸垎搴撳垎妯″紡鍒嗚〃璁捐涓棿浠讹紝姝や腑闂翠欢璋冪敤DynamicShardInfoGenerator鍒嗙墖淇℃伅鐢熸垚鍣紝濉厖鍒嗙墖淇℃伅 provider.UseMiddleware(); + + //鍒嗙墖淇℃伅鐢熸垚鍣ㄦ帴鍙DynamicShardInfoGenerator锛屽湪鍒嗙墖淇℃伅鐢熸垚鍣ㄤ腑璇诲彇HTTPS璇锋眰澶磋繘琛屽~鍏咃紝鍒嗗簱鍊笺佸垎妯″紡鍊笺佸垎琛ㄥ硷紝鍙墿灞 + //dynamicShardInfo.ShardingDataBase = httpContext?.Request?.Headers["ShardingDataBase"]; + //dynamicShardInfo.ShardingSchema = httpContext?.Request?.Headers["ShardingSchema"]; + //dynamicShardInfo.ShardingTable = httpContext?.Request?.Headers["ShardingTable"]; } } } \ No newline at end of file diff --git a/LingTest/TestProvider/ITestService.cs b/LingTest/TestProvider/ITestService.cs new file mode 100644 index 0000000..92f51b2 --- /dev/null +++ b/LingTest/TestProvider/ITestService.cs @@ -0,0 +1,9 @@ +锘縰sing LingYanAspCoreFramework.Models; + +namespace LingTest.TestProvider +{ + public interface ITestService + { + ResponceBody TestBack(); + } +} \ No newline at end of file diff --git a/LingTest/TestProvider/TestService.cs b/LingTest/TestProvider/TestService.cs new file mode 100644 index 0000000..613e217 --- /dev/null +++ b/LingTest/TestProvider/TestService.cs @@ -0,0 +1,15 @@ +锘縰sing LingYanAspCoreFramework.Attributes; +using LingYanAspCoreFramework.Models; + +namespace LingTest.TestProvider +{ + //璇ョ壒鎬у彧闇瑕佹墦鍦ㄦ湇鍔$被涓婏紝浼犲叆鎺ュ彛绫诲瀷鍗冲彲锛屾鏋朵細鍦ㄥ惎鍔ㄦ椂鑷姩娉ㄥ叆 + [LYTService(typeof(ITestService))] + public class TestService : ITestService + { + public ResponceBody TestBack() + { + return new ResponceBody(20000, "娴嬭瘯杩斿洖"); + } + } +} diff --git a/LingYanAspCoreFramework/Attributes/LYDbContextAttribute.cs b/LingYanAspCoreFramework/Attributes/LYDbContextAttribute.cs index 1329294..9b26e7e 100644 --- a/LingYanAspCoreFramework/Attributes/LYDbContextAttribute.cs +++ b/LingYanAspCoreFramework/Attributes/LYDbContextAttribute.cs @@ -3,35 +3,11 @@ [AttributeUsage(AttributeTargets.Class, Inherited = true)] public class LYDbContextAttribute : Attribute { - public DbContextType DbContextType { get; set; } public string ConnectionString { get; set; } - public Type[] ShardingTable { get; set; } - public LYDbContextAttribute(string connectionString, DbContextType dbContextType = DbContextType.DbContext, params Type[] shardingTable) + public LYDbContextAttribute(string connectionString) { this.ConnectionString = connectionString; - DbContextType = dbContextType; - ShardingTable = shardingTable; } } - - public enum DataBaseType - { - MYSQL = 1, - MSSQL = 2 - } - - public enum DbContextType - { - DbContext = 1, - TenantConfigDbContext = 2, - TenantTemplateDbContext = 3, - ShardingDbContext = 4 - } - - public enum ShardingKeyType - { - Mod = 1, - Time = 2 - } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/BaseRoots/BaseSysOwner.cs b/LingYanAspCoreFramework/BaseRoots/BaseSysOwner.cs deleted file mode 100644 index e281f82..0000000 --- a/LingYanAspCoreFramework/BaseRoots/BaseSysOwner.cs +++ /dev/null @@ -1,11 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.BaseRoots -{ - public class BaseSysOwner - { - public virtual Tid Id { get; set; } - public virtual string Name { get; set; } - public virtual string Password { get; set; } - public virtual DateTime CreationTime { get; set; } - public virtual bool IsDeleted { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/BaseRoots/BaseSysOwnerTenantConfig.cs b/LingYanAspCoreFramework/BaseRoots/BaseSysOwnerTenantConfig.cs deleted file mode 100644 index 5205fbb..0000000 --- a/LingYanAspCoreFramework/BaseRoots/BaseSysOwnerTenantConfig.cs +++ /dev/null @@ -1,16 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.BaseRoots -{ - public class BaseSysOwnerTenantConfig - { - public virtual Tid Id { get; set; } - - //浼佷笟鍏宠仈瀛楁 - public virtual long CompanyId { get; set; } - - //鏁版嵁搴撹繛鎺ュ瓧绗︿覆 - public virtual string ConfigJson { get; set; } - - public virtual DateTime CreationTime { get; set; } - public virtual bool IsDeleted { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs b/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs index d4eb176..439f41c 100644 --- a/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs +++ b/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs @@ -33,9 +33,7 @@ namespace LingYanAspCoreFramework.Extensions //鑾峰彇涓棿浠 assembly.GetMiddleware(lYBuilderRuntimeModel); //鑾峰彇鏁版嵁搴撳疄浣 - assembly.GetBaseEntitys(lYBuilderRuntimeModel); - //鑾峰緱澶氱鎴峰熀绫诲疄鐜 - assembly.GetTenantBaseEntitys(lYBuilderRuntimeModel); + assembly.GetBaseEntitys(lYBuilderRuntimeModel); //鑾峰緱DbContext assembly.GetDbContexts(lYBuilderRuntimeModel); //鑾峰緱Manager @@ -93,27 +91,7 @@ namespace LingYanAspCoreFramework.Extensions { lYBuilderRuntimeModel.ModuleTMiddleware.AddRange(moduleEntity); } - } - ///

- /// 鑾峰彇澶氱鎴峰疄浣 - /// - /// - internal static void GetTenantBaseEntitys(this Assembly assembly, RuntimeCacheModel lYBuilderRuntimeModel) - { - var moduleEntity = assembly.GetTypes() - .Where(x => x.BaseType != null && x.BaseType.IsGenericType && - ((x.BaseType.GetGenericTypeDefinition() == typeof(BaseSysOwner<>) || - x.BaseType.GetGenericTypeDefinition() == typeof(BaseSysOwnerTenantConfig<>)) && - x.BaseType.GetGenericArguments().Length > 0 && - !assembly.GetTypes().Any(z => z.BaseType == x))).ToList(); - if (moduleEntity != null && moduleEntity.Count > 0) - { - moduleEntity.ForEach(f => - { - lYBuilderRuntimeModel.ModuleTenantBaseEntitys.TryAdd(f.BaseType.Name.Replace("`1", ""), f); - }); - } - } + } /// /// 鑾峰彇鍩虹瀹炰綋 /// @@ -147,24 +125,7 @@ namespace LingYanAspCoreFramework.Extensions { moduleDbContext.ForEach(dbcontext => { - var longyudb = dbcontext.GetCustomAttribute(); - var value = longyudb.GetPropertyValue("DbContextType"); - if (value != DbContextType.TenantTemplateDbContext) - { - lYBuilderRuntimeModel.ModuleDbContextList.TryAdd(dbcontext, dbcontext.GetCustomAttribute().DbContextType); - } - else if (value == DbContextType.TenantTemplateDbContext) - { - lYBuilderRuntimeModel.TenantTemplateDbContexts.Add(dbcontext); - lYBuilderRuntimeModel.VirtualTableList.TryAdd(ShardingKeyType.Mod, longyudb.GetPropertyValue("ShardingTable").Where(w => - w.BaseType != null && w.BaseType.IsGenericType && - w.BaseType.GetGenericTypeDefinition().Name == "AbstractSimpleShardingModKeyIntVirtualTableRoute`1" && - w.BaseType.GetGenericArguments().Length > 0).ToList()); - lYBuilderRuntimeModel.VirtualTableList.TryAdd(ShardingKeyType.Time, longyudb.GetPropertyValue("ShardingTable").Where(w => - w.BaseType != null && w.BaseType.IsGenericType && - w.BaseType.GetGenericTypeDefinition().Name != "AbstractSimpleShardingModKeyIntVirtualTableRoute`1" && - w.BaseType.GetGenericArguments().Length > 0).ToList()); - } + lYBuilderRuntimeModel.ModuleDbContextList.Add(dbcontext); }); } } diff --git a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs index ffcca6e..a69cf72 100644 --- a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs +++ b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs @@ -44,7 +44,7 @@ namespace LingYanAspCoreFramework.Extensions /// internal static void RegisterRepository(this IServiceCollection BuilderService, RuntimeCacheModel lYBuilderRuntimeModel) { - foreach (var dbcontext in lYBuilderRuntimeModel.ModuleDbContextList.Keys) + foreach (var dbcontext in lYBuilderRuntimeModel.ModuleDbContextList) { var dbsetGener = typeof(DbSet<>); var entitys = dbcontext.GetProperties().Where(w => w.PropertyType.IsGenericType && w.PropertyType.GetGenericTypeDefinition() == dbsetGener) @@ -123,7 +123,7 @@ namespace LingYanAspCoreFramework.Extensions ///
internal static void RegisterDbContext(this IServiceCollection BuilderService, RuntimeCacheModel lYBuilderRuntimeModel) { - foreach (var dbContextType in lYBuilderRuntimeModel.ModuleDbContextList.Keys) + foreach (var dbContextType in lYBuilderRuntimeModel.ModuleDbContextList) { var attribute = dbContextType.GetCustomAttribute(); if (attribute != null && !string.IsNullOrEmpty(attribute.ConnectionString)) diff --git a/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs b/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs index eb378d9..5016e6f 100644 --- a/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs +++ b/LingYanAspCoreFramework/Helpers/TencentCloudHelper.cs @@ -1,12 +1,10 @@ 锘縰sing LingYanAspCoreFramework.Models; -using Microsoft.Extensions.Configuration; using TencentCloud.Common; using TencentCloud.Common.Profile; using TencentCloud.Dnspod.V20210323; using TencentCloud.Dnspod.V20210323.Models; using TencentCloud.Sms.V20210111; using TencentCloud.Sms.V20210111.Models; -using TencentCloud.Tse.V20201207; namespace LingYanAspCoreFramework.Helpers { diff --git a/LingYanAspCoreFramework/Models/RuntimeCacheModel.cs b/LingYanAspCoreFramework/Models/RuntimeCacheModel.cs index 0633c81..8ee93c0 100644 --- a/LingYanAspCoreFramework/Models/RuntimeCacheModel.cs +++ b/LingYanAspCoreFramework/Models/RuntimeCacheModel.cs @@ -1,5 +1,4 @@ 锘縰sing LingYanAspCoreFramework.Attributes; -using Microsoft.Extensions.Configuration; using System.Reflection; namespace LingYanAspCoreFramework.Models @@ -8,7 +7,7 @@ namespace LingYanAspCoreFramework.Models { //閿负缂栬瘧搴擄紝鍊间负鍔犺浇鍩虹被 internal Dictionary ModuleAssemblyBaseLoadingKeyValue { get; set; } - internal Dictionary ModuleDbContextList { get; set; } + internal List ModuleDbContextList { get; set; } internal Dictionary ModuleTenantBaseEntitys { get; set; } internal Dictionary ModuleTBaseEntitys { get; set; } internal List ModuleAuthorizeHandler { get; set; } @@ -16,20 +15,16 @@ namespace LingYanAspCoreFramework.Models internal List ModuleManagerList { get; set; } internal List ModuleTService { get; set; } internal List ModuleTInstance { get; set; } - internal Dictionary> VirtualTableList { get; set; } - internal List TenantTemplateDbContexts { get; set; } internal List ModuleFiler { get; set; } public RuntimeCacheModel() { this.ModuleAssemblyBaseLoadingKeyValue = new Dictionary(); - this.ModuleDbContextList = new Dictionary(); + this.ModuleDbContextList = new List(); this.ModuleTenantBaseEntitys = new Dictionary(); this.ModuleManagerList = new List(); this.ModuleTService = new List(); - this.ModuleTInstance = new List(); - this.VirtualTableList = new Dictionary>(); - this.TenantTemplateDbContexts = new List(); + this.ModuleTInstance = new List(); this.ModuleFiler = new List(); this.ModuleTBaseEntitys = new Dictionary(); this.ModuleTMiddleware = new List(); -- Gitee From e555d885169f4b06ab5aaf2346fda7522cc8f7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=20=E7=8E=8B?= <1321180895@qq.com> Date: Wed, 2 Apr 2025 02:59:09 +0800 Subject: [PATCH 13/17] =?UTF-8?q?=E5=A4=9A=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=A4=9A=E8=BF=81=E7=A7=BB=E5=BC=95=E5=AF=BC=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DynamicShardingController .cs | 18 +++++- .../20250401183955_init.Designer.cs} | 34 +++++------ .../20250401183955_init.cs} | 58 ++++++++++--------- .../StoreDbContextModelSnapshot.cs | 30 +++++----- LingTest/MyDesignTimeServices.cs | 2 + LingTest/RestApiModule.cs | 21 +++++-- LingTest/appsettings.json | 16 +---- .../DynamicControllerConvention.cs | 10 +--- .../DynamicApis/MvcOptionsExtensions.cs | 4 +- .../Extension/DynamicShardCoreExtension.cs | 7 ++- .../Impl/DynamicShardConnectionResolver.cs | 5 +- .../EFCore/DynamicShardMigrationAssembly.cs | 45 +++++++++++++- .../DynamicShardMigrationsScaffolder.cs | 38 ++++++++++++ .../DynamicShard/Models/DynamicShardOption.cs | 4 ++ .../Envs/Configs/AppSetting.json | 2 +- .../Extensions/GeneralExtension.cs | 19 ++++++ .../Extensions/ProjectRegisterExtension.cs | 2 +- 17 files changed, 216 insertions(+), 99 deletions(-) rename LingTest/Migrations/{20250401063233_v1.Designer.cs => Mysql/20250401183955_init.Designer.cs} (72%) rename LingTest/Migrations/{20250401063233_v1.cs => Mysql/20250401183955_init.cs} (56%) rename LingTest/Migrations/{ => Mysql}/StoreDbContextModelSnapshot.cs (74%) create mode 100644 LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs diff --git a/LingTest/Controllers/DynamicShardingController .cs b/LingTest/Controllers/DynamicShardingController .cs index 0a91c12..38d256f 100644 --- a/LingTest/Controllers/DynamicShardingController .cs +++ b/LingTest/Controllers/DynamicShardingController .cs @@ -1,10 +1,12 @@ using LingTest.DbContexts; using LingTest.Entitys; -using LingYanAspCoreFramework.Helpers; +using LingYanAspCoreFramework.DynamicShard.Models.Enum; using LingYanAspCoreFramework.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; namespace LingTest.Controllers { @@ -19,9 +21,18 @@ namespace LingTest.Controllers this.storeDbContext = storeDbContext; try { - if (storeDbContext.Database.GetPendingMigrations().Any()) + var pendingMigrations = storeDbContext.Database.GetPendingMigrations(); + if (pendingMigrations.Any()) { - storeDbContext.Database.Migrate(); + var migrator = storeDbContext.GetService(); + foreach (var migration in pendingMigrations) + { + if (migration.ToLower().Contains(storeDbContext.DynamicShardOptionImpl.ConnectionType.ToString().ToLower())) + { + migrator.Migrate(migration); + break; + } + } storeDbContext.Database.EnsureCreated(); } } @@ -34,6 +45,7 @@ namespace LingTest.Controllers [HttpPost] public async Task> Create(Product product) { + //数据库操作还是采用原生的EFCORE不会影响 var rct = await this.storeDbContext.Products.AddAsync(product); await this.storeDbContext.SaveChangesAsync(); diff --git a/LingTest/Migrations/20250401063233_v1.Designer.cs b/LingTest/Migrations/Mysql/20250401183955_init.Designer.cs similarity index 72% rename from LingTest/Migrations/20250401063233_v1.Designer.cs rename to LingTest/Migrations/Mysql/20250401183955_init.Designer.cs index a0c040a..d746c9f 100644 --- a/LingTest/Migrations/20250401063233_v1.Designer.cs +++ b/LingTest/Migrations/Mysql/20250401183955_init.Designer.cs @@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace LingTest.Migrations +namespace LingTest.Migrations.Mysql { [DbContext(typeof(StoreDbContext))] - [Migration("20250401063233_v1")] - partial class v1 + [Migration("20250401183955_init")] + partial class init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -20,9 +20,9 @@ namespace LingTest.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 128); + .HasAnnotation("Relational:MaxIdentifierLength", 64); - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); modelBuilder.Entity("LingTest.Entitys.Order", b => { @@ -30,33 +30,33 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); b.Property("CreateTimeStamp") .HasColumnType("bigint"); b.Property("IsDeleted") - .HasColumnType("bit"); + .HasColumnType("tinyint(1)"); b.Property("TeaColm") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("longtext"); b.Property("TestIndex") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.Property("TestIndex1") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.Property("TestIndex2") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.Property("TestIndex3") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.HasKey("Id"); @@ -70,7 +70,7 @@ namespace LingTest.Migrations b.HasIndex("TestIndex2", "TestIndex3"); - b.ToTable("Orders", "dbo"); + b.ToTable("Orders", (string)null); }); modelBuilder.Entity("LingTest.Entitys.Product", b => @@ -79,23 +79,23 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); b.Property("Category") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("longtext"); b.Property("Name") .IsRequired() .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasColumnType("varchar(50)"); b.Property("Price") .HasColumnType("int"); b.HasKey("Id"); - b.ToTable("Products", "dbo"); + b.ToTable("Products", (string)null); }); #pragma warning restore 612, 618 } diff --git a/LingTest/Migrations/20250401063233_v1.cs b/LingTest/Migrations/Mysql/20250401183955_init.cs similarity index 56% rename from LingTest/Migrations/20250401063233_v1.cs rename to LingTest/Migrations/Mysql/20250401183955_init.cs index dbd06d7..7f2f1fd 100644 --- a/LingTest/Migrations/20250401063233_v1.cs +++ b/LingTest/Migrations/Mysql/20250401183955_init.cs @@ -1,16 +1,17 @@ -锘縰sing Microsoft.EntityFrameworkCore.Migrations; +锘縰sing Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LingTest.Migrations +namespace LingTest.Migrations.Mysql { /// - public partial class v1 : Migration + public partial class init : Migration { //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 private readonly string shardingTablePrefix; private readonly string shardingSchemaPostfix; - public v1(string _shardingTablePrefix,string _shardingSchemaPostfix) + public init(string _shardingTablePrefix,string _shardingSchemaPostfix) { this.shardingTablePrefix = _shardingTablePrefix; this.shardingSchemaPostfix = _shardingSchemaPostfix; @@ -19,68 +20,71 @@ namespace LingTest.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.EnsureSchema( - name: "dbo"); + migrationBuilder.AlterDatabase() + .Annotation("MySql:CharSet", "utf8mb4"); migrationBuilder.CreateTable( name: $"{this.shardingTablePrefix}Orders", - schema: "dbo"+this.shardingSchemaPostfix, columns: table => new { Id = table.Column(type: "bigint", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - TestIndex = table.Column(type: "nvarchar(450)", nullable: false), - TestIndex1 = table.Column(type: "nvarchar(450)", nullable: false), - TestIndex2 = table.Column(type: "nvarchar(450)", nullable: false), - TestIndex3 = table.Column(type: "nvarchar(450)", nullable: false), - TeaColm = table.Column(type: "nvarchar(max)", nullable: false), - IsDeleted = table.Column(type: "bit", nullable: false), + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TestIndex = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TestIndex1 = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TestIndex2 = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TestIndex3 = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + TeaColm = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + IsDeleted = table.Column(type: "tinyint(1)", nullable: false), CreateTimeStamp = table.Column(type: "bigint", nullable: false) }, constraints: table => { table.PrimaryKey($"{this.shardingTablePrefix}PK_Orders", x => x.Id); - }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); migrationBuilder.CreateTable( name: $"{this.shardingTablePrefix}Products", - schema: "dbo"+this.shardingSchemaPostfix, columns: table => new { Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), - Category = table.Column(type: "nvarchar(max)", nullable: false), + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Category = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), Price = table.Column(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey($"{this.shardingTablePrefix}PK_Products", x => x.Id); - }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); migrationBuilder.CreateIndex( name: $"{this.shardingTablePrefix}IX_Orders_TestIndex", - schema: "dbo"+this.shardingSchemaPostfix, table: $"{this.shardingTablePrefix}Orders", column: "TestIndex"); migrationBuilder.CreateIndex( name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1", - schema: "dbo"+this.shardingSchemaPostfix, table: $"{this.shardingTablePrefix}Orders", column: "TestIndex1", unique: true); migrationBuilder.CreateIndex( name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1_TestIndex2", - schema: "dbo"+this.shardingSchemaPostfix, table: $"{this.shardingTablePrefix}Orders", columns: new[] { "TestIndex1", "TestIndex2" }, unique: true); migrationBuilder.CreateIndex( name: $"{this.shardingTablePrefix}IX_Orders_TestIndex2_TestIndex3", - schema: "dbo"+this.shardingSchemaPostfix, table: $"{this.shardingTablePrefix}Orders", columns: new[] { "TestIndex2", "TestIndex3" }); } @@ -89,12 +93,10 @@ namespace LingTest.Migrations protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: $"{this.shardingTablePrefix}Orders", - schema: "dbo"+this.shardingSchemaPostfix); + name: $"{this.shardingTablePrefix}Orders"); migrationBuilder.DropTable( - name: $"{this.shardingTablePrefix}Products", - schema: "dbo"+this.shardingSchemaPostfix); + name: $"{this.shardingTablePrefix}Products"); } } } diff --git a/LingTest/Migrations/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs similarity index 74% rename from LingTest/Migrations/StoreDbContextModelSnapshot.cs rename to LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs index 4a12fb9..4fb2cb6 100644 --- a/LingTest/Migrations/StoreDbContextModelSnapshot.cs +++ b/LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs @@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace LingTest.Migrations +namespace LingTest.Migrations.Mysql { [DbContext(typeof(StoreDbContext))] partial class StoreDbContextModelSnapshot : ModelSnapshot @@ -17,9 +17,9 @@ namespace LingTest.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("ProductVersion", "8.0.14") - .HasAnnotation("Relational:MaxIdentifierLength", 128); + .HasAnnotation("Relational:MaxIdentifierLength", 64); - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); modelBuilder.Entity("LingTest.Entitys.Order", b => { @@ -27,33 +27,33 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); b.Property("CreateTimeStamp") .HasColumnType("bigint"); b.Property("IsDeleted") - .HasColumnType("bit"); + .HasColumnType("tinyint(1)"); b.Property("TeaColm") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("longtext"); b.Property("TestIndex") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.Property("TestIndex1") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.Property("TestIndex2") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.Property("TestIndex3") .IsRequired() - .HasColumnType("nvarchar(450)"); + .HasColumnType("varchar(255)"); b.HasKey("Id"); @@ -67,7 +67,7 @@ namespace LingTest.Migrations b.HasIndex("TestIndex2", "TestIndex3"); - b.ToTable("Orders", "dbo"); + b.ToTable("Orders", (string)null); }); modelBuilder.Entity("LingTest.Entitys.Product", b => @@ -76,23 +76,23 @@ namespace LingTest.Migrations .ValueGeneratedOnAdd() .HasColumnType("int"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); b.Property("Category") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasColumnType("longtext"); b.Property("Name") .IsRequired() .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasColumnType("varchar(50)"); b.Property("Price") .HasColumnType("int"); b.HasKey("Id"); - b.ToTable("Products", "dbo"); + b.ToTable("Products", (string)null); }); #pragma warning restore 612, 618 } diff --git a/LingTest/MyDesignTimeServices.cs b/LingTest/MyDesignTimeServices.cs index a0be35a..570f9d6 100644 --- a/LingTest/MyDesignTimeServices.cs +++ b/LingTest/MyDesignTimeServices.cs @@ -1,6 +1,7 @@ 锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Migrations.Design; +using Microsoft.Extensions.Options; namespace LingTest { @@ -9,6 +10,7 @@ namespace LingTest public void ConfigureDesignTimeServices(IServiceCollection serviceCollection) { serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } } } diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index 0b454ff..f23f85c 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -3,7 +3,6 @@ using LingYanAspCoreFramework.BaseRoots; using LingYanAspCoreFramework.DynamicShard.Core; using LingYanAspCoreFramework.DynamicShard.Core.Extension; using LingYanAspCoreFramework.DynamicShard.Models.Enum; -using LingYanAspCoreFramework.Helpers; namespace LingTest { @@ -17,10 +16,22 @@ namespace LingTest //鍒嗗簱濮旀墭-杩斿洖杩炴帴瀛楃涓诧紙string锛 dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => { - var connectionTupple = new Tuple(ConnectionType.SqlServer, - $"Server=192.168.148.131;Database={dynamicShardInfo.ShardingDataBase};User Id=sa;Password=12345678;Encrypt=False;"); - LoggerHelper.DefaultLog(connectionTupple.Item1.ToString() + connectionTupple.Item2); - return connectionTupple; + dynamicShardInfo.RequestOtherHeader = new Dictionary(); + dynamicShardInfo.RequestOtherHeader.Add("ShardingDbName", "ShardingDbName"); + Dictionary> connectionDic = new Dictionary>(); + connectionDic["mysql_128"] = new Tuple( + ConnectionType.Mysql, + $"server=192.168.188.128;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + ); + connectionDic["mysql_129"] = new Tuple( + ConnectionType.Mysql, + $"server=192.168.188.129;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + ); + connectionDic["sqlserver"] = new Tuple( + ConnectionType.SqlServer, + $"Server=192.168.188.129;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" + ); + return connectionDic["sqlserver"]; }; //鍒嗘ā寮忓鎵-杩斿洖妯″紡鍚嶏紙string锛 dynamicSettingsFunc.SchemaNameFunc = (dynamicShardInfo) => diff --git a/LingTest/appsettings.json b/LingTest/appsettings.json index f38d2f4..7153dab 100644 --- a/LingTest/appsettings.json +++ b/LingTest/appsettings.json @@ -4,20 +4,6 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } - }, - "ConnectionStrings": { - //mysql杩炴帴妗堜緥 - "mysql_default": "server=192.168.188.128;port=3306;database=multi_tenant_default;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - "mysql_store1": "server=192.168.188.128;port=3306;database=multi_tenant_store1;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - "mysql_store2": "server=192.168.188.128;port=3306;database=multi_tenant_store2;user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;", - //sqlserver杩炴帴妗堜緥 - "sqlserver_default": "Server=192.168.188.128;Database=multi_tenant_default;User Id=sa;Password=12345678;Encrypt=False;", - "sqlserver_store1": "Server=192.168.188.128;Database=multi_tenant_store1;User Id=sa;Password=12345678;Encrypt=False;", - "sqlserver_store2": "Server=192.168.188.128;Database=multi_tenant_store2;User Id=sa;Password=12345678;Encrypt=False;", - //pgsql 杩炴帴妗堜緥 - "postgre_default": "Server=192.168.188.128;Port=5432;Database=multi_tenant_default;User Id=postgres;Password=12345678;", - "postgre_store1": "Server=192.168.188.128;Port=5432;Database=multi_tenant_store1;User Id=postgres;Password=12345678;", - "postgre_store2": "Server=192.168.188.128;Port=5432;Database=multi_tenant_store2;User Id=postgres;Password=12345678;" - }, + }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs b/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs index fd47dce..206cc1d 100644 --- a/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs +++ b/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs @@ -1,6 +1,7 @@ 锘縰sing LingYanAspCoreFramework.Attributes; using LingYanAspCoreFramework.BaseRoots; using LingYanAspCoreFramework.Models; +using LingYanAspCoreFramework.SampleRoots; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -10,11 +11,6 @@ namespace LingYanAspCoreFramework.DynamicApis { internal class DynamicControllerConvention : IApplicationModelConvention { - private IConfiguration _configuration; - public DynamicControllerConvention(IConfiguration configuration) - { - _configuration = configuration; - } public void Apply(ApplicationModel application) { //寰幆姣忎竴涓帶鍒跺櫒淇℃伅 @@ -121,9 +117,9 @@ namespace LingYanAspCoreFramework.DynamicApis private AttributeRouteModel CreateActionRoutePath(string controllerName, string actionName) { string routePrefix=""; - if (!string.IsNullOrEmpty(_configuration.GetSection("DynamicHttpRoutePrefix").Get())) + if (!string.IsNullOrEmpty(LingYanRuntimeManager.CommonConfigModel.DynamicHttpRoutePrefix)) { - routePrefix = _configuration.GetSection("DynamicHttpRoutePrefix").Get(); + routePrefix = LingYanRuntimeManager.CommonConfigModel.DynamicHttpRoutePrefix; } else { diff --git a/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs b/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs index 5009082..8233845 100644 --- a/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs +++ b/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs @@ -12,9 +12,9 @@ namespace LingYanAspCoreFramework.DynamicApis ///
/// /// - public static void UseCentralRouteJsonConfig(this MvcOptions opts, IConfiguration configuration) + public static void UseCentralRouteJsonConfig(this MvcOptions opts) { - opts.Conventions.Insert(0, new DynamicControllerConvention(configuration)); + opts.Conventions.Insert(0, new DynamicControllerConvention()); } } } diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs index 665bfab..e9e1316 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs @@ -5,10 +5,13 @@ using LingYanAspCoreFramework.DynamicShard.EFCore.Impl; using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using LingYanAspCoreFramework.DynamicShard.Models; using LingYanAspCoreFramework.DynamicShard.Models.Enum; +using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Design; +using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -56,7 +59,7 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension case ConnectionType.Mysql: optionsBuilder.UseMySql(dynamicShardOption.ConnectionName, ServerVersion.AutoDetect(dynamicShardOption.ConnectionName), builder => { - builder.DbContextBuilderMigration(dynamicShardOption); + builder.DbContextBuilderMigration(dynamicShardOption); }); break; case ConnectionType.PostgreSql: @@ -74,6 +77,7 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension } optionsBuilder.ReplaceService>(); optionsBuilder.ReplaceService(); + Console.WriteLine($"Migrations Assembly: {optionsBuilder.Options.Extensions.OfType().FirstOrDefault()?.Assembly.GetName().Name}"); }; //配置动态分片方案 dynamicShardSettingsAction?.Invoke(dynamicShardSettings); @@ -85,6 +89,7 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension where TBuilder : RelationalDbContextOptionsBuilder where TExtension : RelationalOptionsExtension, new() { + //builder.MigrationsAssembly(dynamicShardOption.MigrationNameSpace); if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { builder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", dynamicShardOption.SchemaName); diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs index a080724..65b8b8f 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs @@ -34,13 +34,15 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Impl dynamicShardOption.SchemaName = this.dynamicShardSettings.SchemaNameFunc?.Invoke(this.dynamicShardInfo); switch (dynamicShardOption.ConnectionType) { - case ConnectionType.Mysql: + case ConnectionType.Mysql: + dynamicShardOption.MigrationNameSpace = "LingTest.Migrations.MySql"; if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { throw new CommonException(new ResponceBody(63000, $"{dynamicShardOption.ConnectionType.ToString()}数据库不支持模式分片")); } break; case ConnectionType.SqlServer: + dynamicShardOption.MigrationNameSpace = "LingTest.Migrations.SqlServer"; if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { dynamicShardOption.SchemaName = "dbo." + dynamicShardOption.SchemaName; @@ -51,6 +53,7 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Impl } break; case ConnectionType.PostgreSql: + dynamicShardOption.MigrationNameSpace = "DynamicShard.Migrations.PostgreSql"; if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) { dynamicShardOption.SchemaName = "public." + dynamicShardOption.SchemaName; diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs index 0a06ffd..5fac0fc 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs @@ -1,4 +1,7 @@ -锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore.Impl; +using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +using LingYanAspCoreFramework.Extensions; +using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -6,26 +9,34 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; using System.Reflection; - namespace LingYanAspCoreFramework.DynamicShard.EFCore { public class DynamicShardMigrationAssembly : MigrationsAssembly { private readonly DbContext context; + private readonly IMigrationsIdGenerator idGenerator; + public DynamicShardMigrationAssembly(ICurrentDbContext currentContext, IDbContextOptions options, IMigrationsIdGenerator idGenerator, IDiagnosticsLogger logger) : base(currentContext, options, idGenerator, logger) { context = currentContext.Context; + LoggerHelper.DefaultLog("鏌ユ壘绋嬪簭闆"); + var assemblyName = RelationalOptionsExtension.Extract(options)?.MigrationsAssembly; + Assembly = assemblyName == null ? context.GetType().Assembly : Assembly.Load(new AssemblyName(assemblyName)); + LoggerHelper.DefaultLog("鏌ユ壘绋嬪簭闆" + Assembly.FullName); + this.idGenerator = idGenerator; } + public override Assembly Assembly { get; } + public override Migration CreateMigration(TypeInfo migrationClass, string activeProvider) { if (activeProvider == null) throw new CommonException(new ResponceBody(63000, $"杩佺Щ鏃,{nameof(activeProvider)}鍙傛暟涓虹┖")); var hasCtorWithSchema = migrationClass - .GetConstructor(new[] { typeof(string),typeof(string) }) != null; + .GetConstructor(new[] { typeof(string), typeof(string) }) != null; if (hasCtorWithSchema && context is IDynamicShardtDbContext dynamicShardtDbContext) { @@ -40,5 +51,33 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore } return base.CreateMigration(migrationClass, activeProvider); } + /// + /// 鍔ㄦ佸姞杞 ModelSnapshot锛屾敮鎸佹牴鎹暟鎹簱绫诲瀷鍖哄垎閫昏緫銆 + /// + public override ModelSnapshot? ModelSnapshot + { + get + { + var sss =(from t in Assembly.GetConstructibleTypes() + where t.IsSubclassOf(typeof(ModelSnapshot)) && t is IDynamicShardtDbContext dynamicShardtDbContext + && dynamicShardtDbContext.DynamicShardOptionImpl.MigrationNameSpace.Equals(t?.Namespace) + && t.GetCustomAttribute()?.ContextType == this.context.GetType() + select (ModelSnapshot)Activator.CreateInstance(t.AsType())!) + .FirstOrDefault(); + LoggerHelper.DefaultLog("777"+sss.ToString()); + return sss; + } + } + + public override string? FindMigrationId(string nameOrId) + { + var migrationId = Migrations.Keys + .Where(this.idGenerator.IsValidId(nameOrId) + ? id => string.Equals(id, nameOrId, StringComparison.OrdinalIgnoreCase) + : id => string.Equals(this.idGenerator.GetName(id), nameOrId, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(); + LoggerHelper.DefaultLog("鏌ユ壘杩佺Щ" + migrationId); + return migrationId; + } } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs new file mode 100644 index 0000000..f10c32a --- /dev/null +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs @@ -0,0 +1,38 @@ +锘縰sing Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Migrations.Design; + +namespace LingYanAspCoreFramework.DynamicShard.EFCore +{ + public class DynamicShardMigrationsScaffolder : MigrationsScaffolder + { + private readonly Type _contextType; + public DynamicShardMigrationsScaffolder(MigrationsScaffolderDependencies dependencies) : base(dependencies) + { + _contextType = dependencies.CurrentContext.Context.GetType(); + } + protected override string GetDirectory(string projectDir, string? siblingFileName, string subnamespace) + { + var defaultDirectory = Path.Combine(projectDir, Path.Combine(subnamespace.Split('.'))); + + if (siblingFileName != null) + { + if (!siblingFileName.StartsWith(_contextType.Name + "ModelSnapshot.")) + { + var siblingPath = TryGetProjectFile(projectDir, siblingFileName); + if (siblingPath != null) + { + var lastDirectory = Path.GetDirectoryName(siblingPath)!; + if (!defaultDirectory.Equals(lastDirectory, StringComparison.OrdinalIgnoreCase)) + { + Dependencies.OperationReporter.WriteVerbose(DesignStrings.ReusingNamespace(siblingFileName)); + + return lastDirectory; + } + } + } + } + + return defaultDirectory; + } + } +} diff --git a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs index 34513cf..c061ab5 100644 --- a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs @@ -28,5 +28,9 @@ namespace LingYanAspCoreFramework.DynamicShard.Models /// 表名生成委托 ///
public string TablePrefix { get; set; } + /// + /// 迁移模板命名空间 + /// + public string MigrationNameSpace { get; set; } } } diff --git a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json index 7f039e6..852722d 100644 --- a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json +++ b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json @@ -19,7 +19,7 @@ "RedisCofigModel": { "Pattern": "Single", // 鍗曟満妯″紡 - "Single": "192.168.148.130:6379,defaultDatabase=0,password=12345678,prefix=ling", + "Single": "192.168.188.128:6379,defaultDatabase=0,password=,prefix=ling", //鍝ㄥ叺妯″紡 "SentinelModel": { "Master": "longyumaster,password=,prefix=", diff --git a/LingYanAspCoreFramework/Extensions/GeneralExtension.cs b/LingYanAspCoreFramework/Extensions/GeneralExtension.cs index 26e6425..cd6b4a5 100644 --- a/LingYanAspCoreFramework/Extensions/GeneralExtension.cs +++ b/LingYanAspCoreFramework/Extensions/GeneralExtension.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; using System.Xml; @@ -17,6 +18,24 @@ namespace LingYanAspCoreFramework.Extensions { public static class GeneralExtension { + public static IEnumerable GetConstructibleTypes(this Assembly assembly) + { + return from t in assembly.GetLoadableDefinedTypes() + where (object)t != null && !t.IsAbstract && !t.IsGenericTypeDefinition + select t; + } + + public static IEnumerable GetLoadableDefinedTypes(this Assembly assembly) + { + try + { + return assembly.DefinedTypes; + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where((Type t) => t != null).Select(IntrospectionExtensions.GetTypeInfo); + } + } /// /// 鍙嶅皠鑾峰彇灞炴х殑鍊 /// diff --git a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs index a69cf72..0fa6d47 100644 --- a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs +++ b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs @@ -195,7 +195,7 @@ namespace LingYanAspCoreFramework.Extensions BuilderService.AddControllers(options => { //璺敱瑙勫垯 - options.UseCentralRouteJsonConfig(SampleHelper.LingAppsettings); + options.UseCentralRouteJsonConfig(); }).ConfigureApplicationPartManager(t => { //璺敱鎵皠 -- Gitee From 8632b5eaf8ac02db6d45c5a176c8f890060a7e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Wed, 2 Apr 2025 17:47:34 +0800 Subject: [PATCH 14/17] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=88=86=E7=89=87=E8=BF=81=E7=A7=BB=E9=80=82=E5=BA=94=E5=A4=9A?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DynamicShardingController .cs | 73 ++++++++----- LingTest/DbContexts/StoreDbContext.cs | 4 +- .../20250402060703_initCreate.Designer.cs} | 6 +- .../20250402060703_initCreate.cs} | 6 +- .../Mysql/StoreDbContextModelSnapshot.cs | 2 +- .../20250402060722_initCreate.Designer.cs | 103 ++++++++++++++++++ .../SqlServer/20250402060722_initCreate.cs | 100 +++++++++++++++++ .../SqlServer/StoreDbContextModelSnapshot.cs | 100 +++++++++++++++++ LingTest/MyDesignTimeServices.cs | 1 - LingTest/RestApiModule.cs | 14 +-- .../Extension/DynamicShardCoreExtension.cs | 79 +++++++------- .../Impl/DynamicShardConnectionResolver.cs | 46 +------- .../EFCore/DynamicShardMigrationAssembly.cs | 91 +++++++++------- .../DynamicShardMigrationsScaffolder.cs | 30 ++--- .../DynamicShardModelCacheKeyFactory.cs | 2 +- .../EFCore/Impl/DynamicShardDbContext.cs | 56 +++++----- .../EFCore/Impl/DynamicShardEntityBuilder.cs | 4 +- .../DynamicShard/Models/DynamicShardOption.cs | 35 +++++- .../Models/DynamicShardSettingsT.cs | 10 +- .../Envs/Configs/AppSetting.json | 2 +- 20 files changed, 540 insertions(+), 224 deletions(-) rename LingTest/Migrations/{Mysql/20250401183955_init.Designer.cs => MySql/20250402060703_initCreate.Designer.cs} (96%) rename LingTest/Migrations/{Mysql/20250401183955_init.cs => MySql/20250402060703_initCreate.cs} (96%) create mode 100644 LingTest/Migrations/SqlServer/20250402060722_initCreate.Designer.cs create mode 100644 LingTest/Migrations/SqlServer/20250402060722_initCreate.cs create mode 100644 LingTest/Migrations/SqlServer/StoreDbContextModelSnapshot.cs diff --git a/LingTest/Controllers/DynamicShardingController .cs b/LingTest/Controllers/DynamicShardingController .cs index 38d256f..456fe2b 100644 --- a/LingTest/Controllers/DynamicShardingController .cs +++ b/LingTest/Controllers/DynamicShardingController .cs @@ -1,12 +1,12 @@ using LingTest.DbContexts; using LingTest.Entitys; using LingYanAspCoreFramework.DynamicShard.Models.Enum; -using LingYanAspCoreFramework.Models; +using LingYanAspCoreFramework.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Newtonsoft.Json; +using TencentCloud.Ess.V20201111.Models; namespace LingTest.Controllers { @@ -19,27 +19,6 @@ namespace LingTest.Controllers public DynamicShardingController(StoreDbContext storeDbContext) { this.storeDbContext = storeDbContext; - try - { - var pendingMigrations = storeDbContext.Database.GetPendingMigrations(); - if (pendingMigrations.Any()) - { - var migrator = storeDbContext.GetService(); - foreach (var migration in pendingMigrations) - { - if (migration.ToLower().Contains(storeDbContext.DynamicShardOptionImpl.ConnectionType.ToString().ToLower())) - { - migrator.Migrate(migration); - break; - } - } - storeDbContext.Database.EnsureCreated(); - } - } - catch (Exception ex) - { - throw new CommonException(new ResponceBody(63000, ex.Message)); - } } [HttpPost] @@ -47,18 +26,54 @@ namespace LingTest.Controllers { //数据库操作还是采用原生的EFCORE不会影响 var rct = await this.storeDbContext.Products.AddAsync(product); - await this.storeDbContext.SaveChangesAsync(); - return rct?.Entity; } [HttpGet] - public async Task>> Search() + public async Task Search() { - var rct = await this.storeDbContext.Products.ToListAsync(); - return rct; + //List results = new List(); + //await storeDbContext.FindDbContext((dynamicSettingsFunc) => + //{ + // //分库委托-返回连接字符串(string) + // dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => + // { + // Dictionary> connectionDic = new Dictionary>(); + // connectionDic["mysql_128"] = new Tuple( + // ConnectionType.Mysql, + // $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + // ); + // connectionDic["mysql_129"] = new Tuple( + // ConnectionType.Mysql, + // $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + // ); + // connectionDic["sqlserver"] = new Tuple( + // ConnectionType.SqlServer, + // $"Server=192.168.148.131;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" + // ); + // return new Tuple(ConnectionType.SqlServer, + // $"Server=192.168.148.131;Database=111;User Id=sa;Password=12345678;Encrypt=False;"); + // }; + // //分模式委托-返回模式名(string) + // dynamicSettingsFunc.SchemaNameFunc = (serviceProvider, dynamicShardInfo) => + // { + // return null; + // }; + // //分表委托-返回分表前缀(string) + // dynamicSettingsFunc.TablePrefixFunc = (serviceProvider, dynamicShardInfo) => + // { + // return "111"; + // }; + //}, + // async (dbContext) => + // { + // results = await dbContext.Products.ToListAsync(); + // LoggerHelper.SuccessLog("指定数据库111,模式空,表111查询结构:"+JsonConvert.SerializeObject(results)); + // }); + var ssss = await this.storeDbContext.Products.ToListAsync(); + return Ok(ssss); } } } \ No newline at end of file diff --git a/LingTest/DbContexts/StoreDbContext.cs b/LingTest/DbContexts/StoreDbContext.cs index a581ef6..1cf1e65 100644 --- a/LingTest/DbContexts/StoreDbContext.cs +++ b/LingTest/DbContexts/StoreDbContext.cs @@ -7,8 +7,8 @@ namespace LingTest.DbContexts { public class StoreDbContext : DynamicShardDbContext { - public DbSet Products => this.Set(); - public DbSet Orders => this.Set(); + public DbSet Products { get; set; } + public DbSet Orders { get; set; } public StoreDbContext(DbContextOptions options, DynamicShardOption dynamicShardOption, IServiceProvider serviceProvider) : base(options, dynamicShardOption, serviceProvider) { diff --git a/LingTest/Migrations/Mysql/20250401183955_init.Designer.cs b/LingTest/Migrations/MySql/20250402060703_initCreate.Designer.cs similarity index 96% rename from LingTest/Migrations/Mysql/20250401183955_init.Designer.cs rename to LingTest/Migrations/MySql/20250402060703_initCreate.Designer.cs index d746c9f..589c659 100644 --- a/LingTest/Migrations/Mysql/20250401183955_init.Designer.cs +++ b/LingTest/Migrations/MySql/20250402060703_initCreate.Designer.cs @@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace LingTest.Migrations.Mysql +namespace LingTest.Migrations.MySql { [DbContext(typeof(StoreDbContext))] - [Migration("20250401183955_init")] - partial class init + [Migration("20250402060703_initCreate")] + partial class initCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/LingTest/Migrations/Mysql/20250401183955_init.cs b/LingTest/Migrations/MySql/20250402060703_initCreate.cs similarity index 96% rename from LingTest/Migrations/Mysql/20250401183955_init.cs rename to LingTest/Migrations/MySql/20250402060703_initCreate.cs index 7f2f1fd..802f6aa 100644 --- a/LingTest/Migrations/Mysql/20250401183955_init.cs +++ b/LingTest/Migrations/MySql/20250402060703_initCreate.cs @@ -3,15 +3,15 @@ using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace LingTest.Migrations.Mysql +namespace LingTest.Migrations.MySql { /// - public partial class init : Migration + public partial class initCreate : Migration { //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 private readonly string shardingTablePrefix; private readonly string shardingSchemaPostfix; - public init(string _shardingTablePrefix,string _shardingSchemaPostfix) + public initCreate(string _shardingTablePrefix,string _shardingSchemaPostfix) { this.shardingTablePrefix = _shardingTablePrefix; this.shardingSchemaPostfix = _shardingSchemaPostfix; diff --git a/LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs index 4fb2cb6..8b2d1a6 100644 --- a/LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs +++ b/LingTest/Migrations/Mysql/StoreDbContextModelSnapshot.cs @@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; #nullable disable -namespace LingTest.Migrations.Mysql +namespace LingTest.Migrations.MySql { [DbContext(typeof(StoreDbContext))] partial class StoreDbContextModelSnapshot : ModelSnapshot diff --git a/LingTest/Migrations/SqlServer/20250402060722_initCreate.Designer.cs b/LingTest/Migrations/SqlServer/20250402060722_initCreate.Designer.cs new file mode 100644 index 0000000..bf7b03c --- /dev/null +++ b/LingTest/Migrations/SqlServer/20250402060722_initCreate.Designer.cs @@ -0,0 +1,103 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations.SqlServer +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20250402060722_initCreate")] + partial class initCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TeaColm") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex1") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex2") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex3") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("TestIndex"); + + b.HasIndex("TestIndex1") + .IsUnique(); + + b.HasIndex("TestIndex1", "TestIndex2") + .IsUnique(); + + b.HasIndex("TestIndex2", "TestIndex3"); + + b.ToTable("Orders", "dbo"); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Price") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Products", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/Migrations/SqlServer/20250402060722_initCreate.cs b/LingTest/Migrations/SqlServer/20250402060722_initCreate.cs new file mode 100644 index 0000000..f71e774 --- /dev/null +++ b/LingTest/Migrations/SqlServer/20250402060722_initCreate.cs @@ -0,0 +1,100 @@ +锘縰sing Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LingTest.Migrations.SqlServer +{ + /// + public partial class initCreate : Migration + { + //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 + private readonly string shardingTablePrefix; + private readonly string shardingSchemaPostfix; + public initCreate(string _shardingTablePrefix,string _shardingSchemaPostfix) + { + this.shardingTablePrefix = _shardingTablePrefix; + this.shardingSchemaPostfix = _shardingSchemaPostfix; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.CreateTable( + name: $"{this.shardingTablePrefix}Orders", + schema: "dbo"+this.shardingSchemaPostfix, + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + TestIndex = table.Column(type: "nvarchar(450)", nullable: false), + TestIndex1 = table.Column(type: "nvarchar(450)", nullable: false), + TestIndex2 = table.Column(type: "nvarchar(450)", nullable: false), + TestIndex3 = table.Column(type: "nvarchar(450)", nullable: false), + TeaColm = table.Column(type: "nvarchar(max)", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false), + CreateTimeStamp = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey($"{this.shardingTablePrefix}PK_Orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: $"{this.shardingTablePrefix}Products", + schema: "dbo"+this.shardingSchemaPostfix, + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + Category = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey($"{this.shardingTablePrefix}PK_Products", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + column: "TestIndex"); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + column: "TestIndex1", + unique: true); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1_TestIndex2", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + columns: new[] { "TestIndex1", "TestIndex2" }, + unique: true); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex2_TestIndex3", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + columns: new[] { "TestIndex2", "TestIndex3" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: $"{this.shardingTablePrefix}Orders", + schema: "dbo"+this.shardingSchemaPostfix); + + migrationBuilder.DropTable( + name: $"{this.shardingTablePrefix}Products", + schema: "dbo"+this.shardingSchemaPostfix); + } + } +} diff --git a/LingTest/Migrations/SqlServer/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/SqlServer/StoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..36dd1a2 --- /dev/null +++ b/LingTest/Migrations/SqlServer/StoreDbContextModelSnapshot.cs @@ -0,0 +1,100 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LingTest.Migrations.SqlServer +{ + [DbContext(typeof(StoreDbContext))] + partial class StoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("TeaColm") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex1") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex2") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TestIndex3") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("TestIndex"); + + b.HasIndex("TestIndex1") + .IsUnique(); + + b.HasIndex("TestIndex1", "TestIndex2") + .IsUnique(); + + b.HasIndex("TestIndex2", "TestIndex3"); + + b.ToTable("Orders", "dbo"); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Price") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Products", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/MyDesignTimeServices.cs b/LingTest/MyDesignTimeServices.cs index 570f9d6..5969c86 100644 --- a/LingTest/MyDesignTimeServices.cs +++ b/LingTest/MyDesignTimeServices.cs @@ -1,7 +1,6 @@ 锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Migrations.Design; -using Microsoft.Extensions.Options; namespace LingTest { diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index f23f85c..04bbb43 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -16,30 +16,28 @@ namespace LingTest //鍒嗗簱濮旀墭-杩斿洖杩炴帴瀛楃涓诧紙string锛 dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => { - dynamicShardInfo.RequestOtherHeader = new Dictionary(); - dynamicShardInfo.RequestOtherHeader.Add("ShardingDbName", "ShardingDbName"); Dictionary> connectionDic = new Dictionary>(); connectionDic["mysql_128"] = new Tuple( ConnectionType.Mysql, - $"server=192.168.188.128;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" ); connectionDic["mysql_129"] = new Tuple( ConnectionType.Mysql, - $"server=192.168.188.129;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" ); connectionDic["sqlserver"] = new Tuple( ConnectionType.SqlServer, - $"Server=192.168.188.129;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" + $"Server=192.168.148.131;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" ); - return connectionDic["sqlserver"]; + return connectionDic[dynamicShardInfo.ShardingDataBase]; }; //鍒嗘ā寮忓鎵-杩斿洖妯″紡鍚嶏紙string锛 - dynamicSettingsFunc.SchemaNameFunc = (dynamicShardInfo) => + dynamicSettingsFunc.SchemaNameFunc = (serviceProvider,dynamicShardInfo) => { return dynamicShardInfo?.ShardingSchema; }; //鍒嗚〃濮旀墭-杩斿洖鍒嗚〃鍓嶇紑锛坰tring锛 - dynamicSettingsFunc.TablePrefixFunc = (dynamicShardInfo) => + dynamicSettingsFunc.TablePrefixFunc = (serviceProvider,dynamicShardInfo) => { return dynamicShardInfo?.ShardingTable; }; diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs index e9e1316..5efbf81 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs @@ -5,13 +5,10 @@ using LingYanAspCoreFramework.DynamicShard.EFCore.Impl; using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using LingYanAspCoreFramework.DynamicShard.Models; using LingYanAspCoreFramework.DynamicShard.Models.Enum; -using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Migrations.Design; -using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -22,18 +19,15 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension public static IServiceCollection AddDynamicSharding(this IServiceCollection services, Action> dynamicSettingsFunc) where TDbContext : DbContext, IDynamicShardtDbContext { - //注册基础服务 services.AddScoped(); services.AddScoped(); services.TryAddScoped(); services.TryAddScoped, DynamicShardConnectionResolver>(); services.TryAddScoped, DynamicShardEntityBuilder>(); services.TryAddScoped, DynamicShardEntityScaner>(); - services.AddSingleton(ConfigShardingConnectionType(dynamicSettingsFunc)); - //注册数据库上下文 + services.AddSingleton(dynamicSettingsFunc.ConfigShardingConnectionType()); services.AddDbContext((serviceProvider, options) => { - //初始配置 var dynamicShardSettings = serviceProvider.GetRequiredService>(); var connectionResolver = serviceProvider.GetRequiredService>(); var dynamicShardOption = connectionResolver.GetConnection(); @@ -41,62 +35,67 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension }); return services; } - private static Func> ConfigShardingConnectionType(Action> dynamicShardSettingsAction) + internal static Func> ConfigShardingConnectionType(this Action> dynamicShardSettingsAction) where TDbContext : DbContext, IDynamicShardtDbContext { Func> dynamicShardOptionConfigFunc = (serviceProvider) => { var dynamicShardSettings = new DynamicShardSettings(); - //按照数据库类型进行配置 dynamicShardSettings.DbContextOptionDelegate = (dynamicShardOption, optionsBuilder) => { if (string.IsNullOrEmpty(dynamicShardOption.ConnectionName)) { throw new CommonException(new ResponceBody(63000, "连接字符串不能为空")); } - switch (dynamicShardOption.ConnectionType) - { - case ConnectionType.Mysql: - optionsBuilder.UseMySql(dynamicShardOption.ConnectionName, ServerVersion.AutoDetect(dynamicShardOption.ConnectionName), builder => - { - builder.DbContextBuilderMigration(dynamicShardOption); - }); - break; - case ConnectionType.PostgreSql: - optionsBuilder.UseNpgsql(dynamicShardOption.ConnectionName, builder => - { - builder.DbContextBuilderMigration(dynamicShardOption); - }); - break; - case ConnectionType.SqlServer: - optionsBuilder.UseSqlServer(dynamicShardOption.ConnectionName, builder => - { - builder.DbContextBuilderMigration(dynamicShardOption); - }); - break; - } + optionsBuilder.DbContextOptionsBuilderSetting(dynamicShardOption); optionsBuilder.ReplaceService>(); optionsBuilder.ReplaceService(); - Console.WriteLine($"Migrations Assembly: {optionsBuilder.Options.Extensions.OfType().FirstOrDefault()?.Assembly.GetName().Name}"); + }; - //配置动态分片方案 dynamicShardSettingsAction?.Invoke(dynamicShardSettings); return dynamicShardSettings; }; return dynamicShardOptionConfigFunc; } - internal static void DbContextBuilderMigration(this RelationalDbContextOptionsBuilder builder, DynamicShardOption dynamicShardOption) - where TBuilder : RelationalDbContextOptionsBuilder - where TExtension : RelationalOptionsExtension, new() + internal static void DbContextOptionsBuilderSetting(this DbContextOptionsBuilder dbContextOptionsBuilder, DynamicShardOption dynamicShardOption) { - //builder.MigrationsAssembly(dynamicShardOption.MigrationNameSpace); - if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) + switch (dynamicShardOption.ConnectionType) { - builder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", dynamicShardOption.SchemaName); + case ConnectionType.Mysql: + dbContextOptionsBuilder.UseMySql(dynamicShardOption.ConnectionName, ServerVersion.AutoDetect(dynamicShardOption.ConnectionName), builder => + { + builder.DbContextOptionsBuilderMigrationHistory(dynamicShardOption); + }); + break; + case ConnectionType.PostgreSql: + dbContextOptionsBuilder.UseNpgsql(dynamicShardOption.ConnectionName, builder => + { + builder.DbContextOptionsBuilderMigrationHistory(dynamicShardOption); + }); + break; + case ConnectionType.SqlServer: + dbContextOptionsBuilder.UseSqlServer(dynamicShardOption.ConnectionName, builder => + { + builder.DbContextOptionsBuilderMigrationHistory(dynamicShardOption); + }); + break; } - else if (!string.IsNullOrEmpty(dynamicShardOption.TablePrefix)) + } + internal static void DbContextOptionsBuilderMigrationHistory(this RelationalDbContextOptionsBuilder relationalDbContextOptionsBuilder, DynamicShardOption dynamicShardOption) + where TBuilder : RelationalDbContextOptionsBuilder + where TExtension : RelationalOptionsExtension, new() + { + switch (dynamicShardOption.ConnectionType) { - builder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory"); + case ConnectionType.Mysql: + relationalDbContextOptionsBuilder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory"); + break; + case ConnectionType.SqlServer: + relationalDbContextOptionsBuilder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", $"dbo{dynamicShardOption.SchemaPostfix}"); + break; + case ConnectionType.PostgreSql: + relationalDbContextOptionsBuilder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", $"public{dynamicShardOption.SchemaPostfix}"); + break; } } diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs index 65b8b8f..c22ca73 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Impl/DynamicShardConnectionResolver.cs @@ -1,7 +1,5 @@ using LingYanAspCoreFramework.DynamicShard.Core.Interface; using LingYanAspCoreFramework.DynamicShard.Models; -using LingYanAspCoreFramework.DynamicShard.Models.Enum; -using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; namespace LingYanAspCoreFramework.DynamicShard.Core.Impl @@ -28,47 +26,11 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Impl public DynamicShardOption GetConnection() { var connectionTupple = this.dynamicShardSettings.ConnectionNameFunc?.Invoke(this.serviceProvider, this.dynamicShardInfo); - dynamicShardOption.Key = this.dynamicShardSettings.Key; - dynamicShardOption.ConnectionType = connectionTupple.Item1; + dynamicShardOption.ConnectionType = connectionTupple!.Item1; dynamicShardOption.ConnectionName = connectionTupple.Item2; - dynamicShardOption.SchemaName = this.dynamicShardSettings.SchemaNameFunc?.Invoke(this.dynamicShardInfo); - switch (dynamicShardOption.ConnectionType) - { - case ConnectionType.Mysql: - dynamicShardOption.MigrationNameSpace = "LingTest.Migrations.MySql"; - if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) - { - throw new CommonException(new ResponceBody(63000, $"{dynamicShardOption.ConnectionType.ToString()}数据库不支持模式分片")); - } - break; - case ConnectionType.SqlServer: - dynamicShardOption.MigrationNameSpace = "LingTest.Migrations.SqlServer"; - if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) - { - dynamicShardOption.SchemaName = "dbo." + dynamicShardOption.SchemaName; - } - else - { - dynamicShardOption.SchemaName = "dbo"; - } - break; - case ConnectionType.PostgreSql: - dynamicShardOption.MigrationNameSpace = "DynamicShard.Migrations.PostgreSql"; - if (!string.IsNullOrEmpty(dynamicShardOption.SchemaName)) - { - dynamicShardOption.SchemaName = "public." + dynamicShardOption.SchemaName; - } - else - { - dynamicShardOption.SchemaName = "public"; - } - break; - } - dynamicShardOption.TablePrefix = this.dynamicShardSettings.TablePrefixFunc?.Invoke(this.dynamicShardInfo); - if (!string.IsNullOrEmpty(dynamicShardOption.TablePrefix)) - { - dynamicShardOption.TablePrefix += "_"; - } + dynamicShardOption.SchemaPostfix = this.dynamicShardSettings.SchemaNameFunc?.Invoke(this.serviceProvider, this.dynamicShardInfo)!; + dynamicShardOption.TablePrefix = this.dynamicShardSettings.TablePrefixFunc?.Invoke(this.serviceProvider, this.dynamicShardInfo)!; + dynamicShardOption.PostSetting(); return this.dynamicShardOption; } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs index 5fac0fc..1ddd12e 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationAssembly.cs @@ -1,5 +1,4 @@ -锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore.Impl; -using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using LingYanAspCoreFramework.Extensions; using LingYanAspCoreFramework.Helpers; using LingYanAspCoreFramework.Models; @@ -14,70 +13,80 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore public class DynamicShardMigrationAssembly : MigrationsAssembly { private readonly DbContext context; - private readonly IMigrationsIdGenerator idGenerator; - public DynamicShardMigrationAssembly(ICurrentDbContext currentContext, IDbContextOptions options, IMigrationsIdGenerator idGenerator, IDiagnosticsLogger logger) : base(currentContext, options, idGenerator, logger) { context = currentContext.Context; - LoggerHelper.DefaultLog("鏌ユ壘绋嬪簭闆"); - var assemblyName = RelationalOptionsExtension.Extract(options)?.MigrationsAssembly; - Assembly = assemblyName == null ? context.GetType().Assembly : Assembly.Load(new AssemblyName(assemblyName)); - LoggerHelper.DefaultLog("鏌ユ壘绋嬪簭闆" + Assembly.FullName); - this.idGenerator = idGenerator; } - public override Assembly Assembly { get; } public override Migration CreateMigration(TypeInfo migrationClass, string activeProvider) { if (activeProvider == null) throw new CommonException(new ResponceBody(63000, $"杩佺Щ鏃,{nameof(activeProvider)}鍙傛暟涓虹┖")); - - var hasCtorWithSchema = migrationClass - .GetConstructor(new[] { typeof(string), typeof(string) }) != null; - - if (hasCtorWithSchema && context is IDynamicShardtDbContext dynamicShardtDbContext) + if (context is IDynamicShardtDbContext dynamicShardtDbContext) { - string schemaPostFix = ""; - if (!string.IsNullOrEmpty(dynamicShardtDbContext.DynamicShardOptionImpl.SchemaName) && dynamicShardtDbContext.DynamicShardOptionImpl.SchemaName.StartsWith("dbo.")) + var dynamicShardMigration = (Migration)Activator.CreateInstance(migrationClass.AsType(), + dynamicShardtDbContext?.DynamicShardOptionImpl?.TablePrefix, dynamicShardtDbContext!.DynamicShardOptionImpl.SchemaPostfix)!; + dynamicShardMigration.ActiveProvider = activeProvider; + return dynamicShardMigration; + } + return base.CreateMigration(migrationClass, activeProvider); + } + public override IReadOnlyDictionary Migrations + { + get + { + if (context is IDynamicShardtDbContext dynamicShardtDbContext) { - schemaPostFix = dynamicShardtDbContext.DynamicShardOptionImpl.SchemaName.Replace("dbo", ""); + var dyanamicShardMigrations = new SortedList(); + var items = Assembly.GetConstructibleTypes() + .Where(t => t.IsSubclassOf(typeof(Migration)) + && t.Namespace!.Equals(ProductMigrationNameSpaceStr(dynamicShardtDbContext)) + && t.GetCustomAttribute()?.ContextType == this.context.GetType()) + .Select(t => new + { + Id = t.GetCustomAttribute()?.Id, + Type = t + }) + .Where(x => x.Id != null) + .OrderBy(x => x.Id); + + foreach (var item in items) + { + LoggerHelper.DefaultLog($"杩佺Щ璁板綍:{item.Id},杩佺Щ绫诲瀷:{item.Type}"); + dyanamicShardMigrations!.TryAdd(item.Id, item.Type); + } + return dyanamicShardMigrations; } - var instance = (Migration)Activator.CreateInstance(migrationClass.AsType(), dynamicShardtDbContext?.DynamicShardOptionImpl?.TablePrefix, schemaPostFix); - instance.ActiveProvider = activeProvider; - return instance; + return base.Migrations; + } - return base.CreateMigration(migrationClass, activeProvider); } - /// - /// 鍔ㄦ佸姞杞 ModelSnapshot锛屾敮鎸佹牴鎹暟鎹簱绫诲瀷鍖哄垎閫昏緫銆 - /// + public override ModelSnapshot? ModelSnapshot { get { - var sss =(from t in Assembly.GetConstructibleTypes() - where t.IsSubclassOf(typeof(ModelSnapshot)) && t is IDynamicShardtDbContext dynamicShardtDbContext - && dynamicShardtDbContext.DynamicShardOptionImpl.MigrationNameSpace.Equals(t?.Namespace) - && t.GetCustomAttribute()?.ContextType == this.context.GetType() - select (ModelSnapshot)Activator.CreateInstance(t.AsType())!) - .FirstOrDefault(); - LoggerHelper.DefaultLog("777"+sss.ToString()); - return sss; + if (context is IDynamicShardtDbContext dynamicShardtDbContext) + { + var snapshotType = this.Assembly.GetConstructibleTypes() + .FirstOrDefault(t => t.IsSubclassOf(typeof(ModelSnapshot)) + && t.Namespace!.Equals(ProductMigrationNameSpaceStr(dynamicShardtDbContext)) + && t.GetCustomAttribute()?.ContextType == this.context.GetType()); + if (snapshotType == null) + return null; + var dyanamicShardModelSnapshot = (ModelSnapshot)Activator.CreateInstance(snapshotType.AsType())!; + return dyanamicShardModelSnapshot; + } + return base.ModelSnapshot; } } - public override string? FindMigrationId(string nameOrId) + private string ProductMigrationNameSpaceStr(IDynamicShardtDbContext dynamicShardtDbContext) { - var migrationId = Migrations.Keys - .Where(this.idGenerator.IsValidId(nameOrId) - ? id => string.Equals(id, nameOrId, StringComparison.OrdinalIgnoreCase) - : id => string.Equals(this.idGenerator.GetName(id), nameOrId, StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(); - LoggerHelper.DefaultLog("鏌ユ壘杩佺Щ" + migrationId); - return migrationId; + return dynamicShardtDbContext.DynamicShardOptionImpl.MigrationNameSpace.Replace("$DynamicShard", this.Assembly.GetName().Name); } } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs index f10c32a..7d9e191 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardMigrationsScaffolder.cs @@ -1,4 +1,6 @@ -锘縰sing Microsoft.EntityFrameworkCore.Internal; +锘縰sing LingYanAspCoreFramework.DynamicShard.EFCore.Interface; +using LingYanAspCoreFramework.Helpers; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Migrations.Design; namespace LingYanAspCoreFramework.DynamicShard.EFCore @@ -12,27 +14,29 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore } protected override string GetDirectory(string projectDir, string? siblingFileName, string subnamespace) { - var defaultDirectory = Path.Combine(projectDir, Path.Combine(subnamespace.Split('.'))); - - if (siblingFileName != null) + if (typeof(IDynamicShardtDbContext).IsAssignableFrom(_contextType)) { - if (!siblingFileName.StartsWith(_contextType.Name + "ModelSnapshot.")) + var defaultDirectory = Path.Combine(projectDir, Path.Combine(subnamespace.Split('.'))); + if (siblingFileName != null) { - var siblingPath = TryGetProjectFile(projectDir, siblingFileName); - if (siblingPath != null) + if (!siblingFileName.StartsWith(_contextType.Name + "ModelSnapshot.")) { - var lastDirectory = Path.GetDirectoryName(siblingPath)!; - if (!defaultDirectory.Equals(lastDirectory, StringComparison.OrdinalIgnoreCase)) + var siblingPath = TryGetProjectFile(projectDir, siblingFileName); + if (siblingPath != null) { - Dependencies.OperationReporter.WriteVerbose(DesignStrings.ReusingNamespace(siblingFileName)); + var lastDirectory = Path.GetDirectoryName(siblingPath)!; + if (!defaultDirectory.Equals(lastDirectory, StringComparison.OrdinalIgnoreCase)) + { + Dependencies.OperationReporter.WriteVerbose(DesignStrings.ReusingNamespace(siblingFileName)); - return lastDirectory; + return lastDirectory; + } } } } + return defaultDirectory; } - - return defaultDirectory; + return base.GetDirectory(projectDir, siblingFileName, subnamespace); } } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs index 603450f..b34768c 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/DynamicShardModelCacheKeyFactory.cs @@ -39,7 +39,7 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore { var dbContext = context as TDbContext; var dynamicShardModelCacheKey = new DynamicShardModelCacheKey(dbContext, - dbContext?.DynamicShardOptionImpl?.SchemaName + dbContext?.DynamicShardOptionImpl?.TablePrefix ?? "no_dynamicshard_identifier"); + dbContext?.DynamicShardOptionImpl?.SchemaPostfix + dbContext?.DynamicShardOptionImpl?.TablePrefix ?? "no_dynamicshard_identifier"); return dynamicShardModelCacheKey; } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs index 640079d..c3a6535 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardDbContext.cs @@ -1,8 +1,11 @@ +using LingYanAspCoreFramework.DynamicShard.Core.Extension; +using LingYanAspCoreFramework.DynamicShard.Core.Interface; using LingYanAspCoreFramework.DynamicShard.EFCore.Interface; using LingYanAspCoreFramework.DynamicShard.Models; using LingYanAspCoreFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl { @@ -17,41 +20,42 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl public DynamicShardOption DynamicShardOptionImpl { get; protected internal set; } private readonly IServiceProvider serviceProvider; - - /// - /// 初始化 - /// - /// - /// - /// - public DynamicShardDbContext(DbContextOptions dbContextOptions, - DynamicShardOption dynamicShardOption, - IServiceProvider serviceProvider) : base(dbContextOptions) + public DynamicShardDbContext(DbContextOptions dbContextOptions, DynamicShardOption dynamicShardOption, IServiceProvider serviceProvider) : base(dbContextOptions) { this.serviceProvider = serviceProvider; this.DynamicShardOptionImpl = dynamicShardOption; - //try - //{ - // if (this.Database.GetPendingMigrations().Any()) - // { - // this.Database.Migrate(); - // this.Database.EnsureCreated(); - // } - //} - //catch (Exception ex) - //{ - // throw new CommonException(new ResponceBody(63000, ex.Message)); - //} + try + { + if (this.Database.GetPendingMigrations().Any()) + { + this.Database.Migrate(); + this.Database.EnsureCreated(); + } + } + catch (Exception ex) + { + throw new CommonException(new ResponceBody(63000, ex.Message)); + } } - /// - /// 配置模型 - /// - /// 模型构建器 protected override void OnModelCreating(ModelBuilder modelBuilder) { var builderType = typeof(IDynamicShardEntityBuilder<>).MakeGenericType(GetType()); IDynamicShardEntityBuilder entityBuilder = (IDynamicShardEntityBuilder)serviceProvider.GetRequiredService(builderType); entityBuilder.UpdateEntities(modelBuilder); } + //public async Task FindDbContext(Action> dynamicShardSetting, Func func) + // where TDbContext : DbContext, IDynamicShardtDbContext + //{ + // using (var scope = this.serviceProvider.CreateScope()) + // { + // var dynamicShardSettings = dynamicShardSetting.ConfigShardingConnectionType().Invoke(scope.ServiceProvider); + // var connectionResolver = this.serviceProvider.GetRequiredService>(); + // var dynamicShardOption = connectionResolver.GetConnection(); + // var dbContextOptionsBuilder = new DbContextOptionsBuilder(); + // dynamicShardSettings.DbContextOptionDelegate.Invoke(dynamicShardOption, dbContextOptionsBuilder); + // var dbContext = (TDbContext)Activator.CreateInstance(typeof(TDbContext), dbContextOptionsBuilder.Options, dynamicShardOption, scope.ServiceProvider); + // await func.Invoke(dbContext); + // } + //} } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs index fb7ef92..86fa42d 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs @@ -45,8 +45,10 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl entity.ToTable(tableName); break; case ConnectionType.SqlServer: + entity.ToTable(tableName, "dbo" + this.dynamicShardOption.SchemaPostfix); + break; case ConnectionType.PostgreSql: - entity.ToTable(tableName, this.dynamicShardOption.SchemaName); + entity.ToTable(tableName, "public" + this.dynamicShardOption.SchemaPostfix); break; } } diff --git a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs index c061ab5..30e332d 100644 --- a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardOption.cs @@ -1,4 +1,5 @@ using LingYanAspCoreFramework.DynamicShard.Models.Enum; +using LingYanAspCoreFramework.Models; namespace LingYanAspCoreFramework.DynamicShard.Models { @@ -7,10 +8,6 @@ namespace LingYanAspCoreFramework.DynamicShard.Models ///
public class DynamicShardOption { - /// - /// 租户选项的键 - /// - public string Key { get; set; } /// /// 数据库类型 /// @@ -23,7 +20,7 @@ namespace LingYanAspCoreFramework.DynamicShard.Models /// /// 模式名称 /// - public string SchemaName { get; set; } + public string SchemaPostfix { get; set; } /// /// 表名生成委托 /// @@ -32,5 +29,33 @@ namespace LingYanAspCoreFramework.DynamicShard.Models /// 迁移模板命名空间 ///
public string MigrationNameSpace { get; set; } + + public void PostSetting() + { + switch (ConnectionType) + { + case ConnectionType.Mysql: + MigrationNameSpace = "$DynamicShard.Migrations.MySql"; + if (!string.IsNullOrEmpty(SchemaPostfix)) + { + throw new CommonException(new ResponceBody(63000, $"{ConnectionType.ToString()}数据库不支持模式分片")); + } + break; + case ConnectionType.SqlServer: + MigrationNameSpace = "$DynamicShard.Migrations.SqlServer"; + break; + case ConnectionType.PostgreSql: + MigrationNameSpace = "$DynamicShard.Migrations.PostgreSql"; + break; + } + if (!string.IsNullOrEmpty(TablePrefix)) + { + TablePrefix += "_"; + } + if (!string.IsNullOrEmpty(SchemaPostfix)) + { + SchemaPostfix = "." + SchemaPostfix; + } + } } } diff --git a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs index 0079080..940209d 100644 --- a/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs +++ b/LingYanAspCoreFramework/DynamicShard/Models/DynamicShardSettingsT.cs @@ -10,10 +10,6 @@ namespace LingYanAspCoreFramework.DynamicShard.Models public class DynamicShardSettings where TDbContext : DbContext { - /// - /// 绉熸埛璁剧疆鐨勯敭 - /// - public string Key { get; set; } /// /// 鏁版嵁搴撹繛鎺ュ瓧绗︿覆鐢熸垚鍑芥暟 /// @@ -22,15 +18,15 @@ namespace LingYanAspCoreFramework.DynamicShard.Models /// /// 妯″紡鐢熸垚鍑芥暟 /// - public Func SchemaNameFunc { get; set; } + public Func SchemaNameFunc { get; set; } /// /// 琛ㄥ悕鐢熸垚鍑芥暟 /// - public Func TablePrefixFunc { get; set; } + public Func TablePrefixFunc { get; set; } /// /// 鏁版嵁搴撲笂涓嬫枃閫夐」閰嶇疆 /// - public Action DbContextOptionDelegate; + internal Action DbContextOptionDelegate; } } diff --git a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json index 852722d..a37e638 100644 --- a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json +++ b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json @@ -19,7 +19,7 @@ "RedisCofigModel": { "Pattern": "Single", // 鍗曟満妯″紡 - "Single": "192.168.188.128:6379,defaultDatabase=0,password=,prefix=ling", + "Single": "192.168.148.131:6379,defaultDatabase=0,password=12345678,prefix=ling", //鍝ㄥ叺妯″紡 "SentinelModel": { "Master": "longyumaster,password=,prefix=", -- Gitee From 1ac0ab530783434e1c3ff4acabcf5353fcd50a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=20=E7=8E=8B?= <1321180895@qq.com> Date: Wed, 2 Apr 2025 20:34:02 +0800 Subject: [PATCH 15/17] =?UTF-8?q?=E9=80=82=E9=85=8DPostgreSql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/DynamicShardingController .cs | 38 ------- .../20250402121617_initCreate.Designer.cs | 103 ++++++++++++++++++ .../PostgreSql/20250402121617_initCreate.cs | 101 +++++++++++++++++ .../PostgreSql/StoreDbContextModelSnapshot.cs | 100 +++++++++++++++++ LingTest/RestApiModule.cs | 14 ++- .../Extension/DynamicShardCoreExtension.cs | 2 +- .../EFCore/Impl/DynamicShardEntityBuilder.cs | 2 +- .../Envs/Configs/AppSetting.json | 2 +- 8 files changed, 315 insertions(+), 47 deletions(-) create mode 100644 LingTest/Migrations/PostgreSql/20250402121617_initCreate.Designer.cs create mode 100644 LingTest/Migrations/PostgreSql/20250402121617_initCreate.cs create mode 100644 LingTest/Migrations/PostgreSql/StoreDbContextModelSnapshot.cs diff --git a/LingTest/Controllers/DynamicShardingController .cs b/LingTest/Controllers/DynamicShardingController .cs index 456fe2b..7a8422c 100644 --- a/LingTest/Controllers/DynamicShardingController .cs +++ b/LingTest/Controllers/DynamicShardingController .cs @@ -34,44 +34,6 @@ namespace LingTest.Controllers [HttpGet] public async Task Search() { - //List results = new List(); - //await storeDbContext.FindDbContext((dynamicSettingsFunc) => - //{ - // //分库委托-返回连接字符串(string) - // dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => - // { - // Dictionary> connectionDic = new Dictionary>(); - // connectionDic["mysql_128"] = new Tuple( - // ConnectionType.Mysql, - // $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" - // ); - // connectionDic["mysql_129"] = new Tuple( - // ConnectionType.Mysql, - // $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" - // ); - // connectionDic["sqlserver"] = new Tuple( - // ConnectionType.SqlServer, - // $"Server=192.168.148.131;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" - // ); - // return new Tuple(ConnectionType.SqlServer, - // $"Server=192.168.148.131;Database=111;User Id=sa;Password=12345678;Encrypt=False;"); - // }; - // //分模式委托-返回模式名(string) - // dynamicSettingsFunc.SchemaNameFunc = (serviceProvider, dynamicShardInfo) => - // { - // return null; - // }; - // //分表委托-返回分表前缀(string) - // dynamicSettingsFunc.TablePrefixFunc = (serviceProvider, dynamicShardInfo) => - // { - // return "111"; - // }; - //}, - // async (dbContext) => - // { - // results = await dbContext.Products.ToListAsync(); - // LoggerHelper.SuccessLog("指定数据库111,模式空,表111查询结构:"+JsonConvert.SerializeObject(results)); - // }); var ssss = await this.storeDbContext.Products.ToListAsync(); return Ok(ssss); } diff --git a/LingTest/Migrations/PostgreSql/20250402121617_initCreate.Designer.cs b/LingTest/Migrations/PostgreSql/20250402121617_initCreate.Designer.cs new file mode 100644 index 0000000..cda13f0 --- /dev/null +++ b/LingTest/Migrations/PostgreSql/20250402121617_initCreate.Designer.cs @@ -0,0 +1,103 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LingTest.Migrations.PostgreSql +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20250402121617_initCreate")] + partial class initCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("TeaColm") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex1") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex2") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex3") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TestIndex"); + + b.HasIndex("TestIndex1") + .IsUnique(); + + b.HasIndex("TestIndex1", "TestIndex2") + .IsUnique(); + + b.HasIndex("TestIndex2", "TestIndex3"); + + b.ToTable("Orders", "dbo"); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Products", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/Migrations/PostgreSql/20250402121617_initCreate.cs b/LingTest/Migrations/PostgreSql/20250402121617_initCreate.cs new file mode 100644 index 0000000..0174dde --- /dev/null +++ b/LingTest/Migrations/PostgreSql/20250402121617_initCreate.cs @@ -0,0 +1,101 @@ +锘縰sing Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LingTest.Migrations.PostgreSql +{ + /// + public partial class initCreate : Migration + { + //DynamicShard妗嗘灦娉ㄥ叆鍔ㄦ佸垎鐗囧弬鏁 + private readonly string shardingTablePrefix; + private readonly string shardingSchemaPostfix; + public initCreate(string _shardingTablePrefix,string _shardingSchemaPostfix) + { + this.shardingTablePrefix = _shardingTablePrefix; + this.shardingSchemaPostfix = _shardingSchemaPostfix; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dbo"); + + migrationBuilder.CreateTable( + name: $"{this.shardingTablePrefix}Orders", + schema: "dbo"+this.shardingSchemaPostfix, + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + TestIndex = table.Column(type: "text", nullable: false), + TestIndex1 = table.Column(type: "text", nullable: false), + TestIndex2 = table.Column(type: "text", nullable: false), + TestIndex3 = table.Column(type: "text", nullable: false), + TeaColm = table.Column(type: "text", nullable: false), + IsDeleted = table.Column(type: "boolean", nullable: false), + CreateTimeStamp = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey($"{this.shardingTablePrefix}PK_Orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: $"{this.shardingTablePrefix}Products", + schema: "dbo"+this.shardingSchemaPostfix, + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + Category = table.Column(type: "text", nullable: false), + Price = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey($"{this.shardingTablePrefix}PK_Products", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + column: "TestIndex"); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + column: "TestIndex1", + unique: true); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex1_TestIndex2", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + columns: new[] { "TestIndex1", "TestIndex2" }, + unique: true); + + migrationBuilder.CreateIndex( + name: $"{this.shardingTablePrefix}IX_Orders_TestIndex2_TestIndex3", + schema: "dbo"+this.shardingSchemaPostfix, + table: $"{this.shardingTablePrefix}Orders", + columns: new[] { "TestIndex2", "TestIndex3" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: $"{this.shardingTablePrefix}Orders", + schema: "dbo"+this.shardingSchemaPostfix); + + migrationBuilder.DropTable( + name: $"{this.shardingTablePrefix}Products", + schema: "dbo"+this.shardingSchemaPostfix); + } + } +} diff --git a/LingTest/Migrations/PostgreSql/StoreDbContextModelSnapshot.cs b/LingTest/Migrations/PostgreSql/StoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..1e980f4 --- /dev/null +++ b/LingTest/Migrations/PostgreSql/StoreDbContextModelSnapshot.cs @@ -0,0 +1,100 @@ +锘// +using LingTest.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LingTest.Migrations.PostgreSql +{ + [DbContext(typeof(StoreDbContext))] + partial class StoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.14") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("LingTest.Entitys.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreateTimeStamp") + .HasColumnType("bigint"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("TeaColm") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex1") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex2") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestIndex3") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TestIndex"); + + b.HasIndex("TestIndex1") + .IsUnique(); + + b.HasIndex("TestIndex1", "TestIndex2") + .IsUnique(); + + b.HasIndex("TestIndex2", "TestIndex3"); + + b.ToTable("Orders", "dbo"); + }); + + modelBuilder.Entity("LingTest.Entitys.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Price") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Products", "dbo"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LingTest/RestApiModule.cs b/LingTest/RestApiModule.cs index 04bbb43..85f3b50 100644 --- a/LingTest/RestApiModule.cs +++ b/LingTest/RestApiModule.cs @@ -3,6 +3,7 @@ using LingYanAspCoreFramework.BaseRoots; using LingYanAspCoreFramework.DynamicShard.Core; using LingYanAspCoreFramework.DynamicShard.Core.Extension; using LingYanAspCoreFramework.DynamicShard.Models.Enum; +using LingYanAspCoreFramework.Helpers; namespace LingTest { @@ -17,18 +18,19 @@ namespace LingTest dynamicSettingsFunc.ConnectionNameFunc = (serviceProvider, dynamicShardInfo) => { Dictionary> connectionDic = new Dictionary>(); - connectionDic["mysql_128"] = new Tuple( + connectionDic["mysql"] = new Tuple( ConnectionType.Mysql, - $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + $"server=192.168.188.128;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" ); - connectionDic["mysql_129"] = new Tuple( - ConnectionType.Mysql, - $"server=192.168.148.131;port=3306;database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};user=laoda;password=12345678;charset=utf8mb4;AllowLoadLocalInfile=true;SslMode=none;" + connectionDic["pgsql"] = new Tuple( + ConnectionType.PostgreSql, + $"Host=192.168.188.128;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};Username=postgres;Password=12345678;" ); connectionDic["sqlserver"] = new Tuple( ConnectionType.SqlServer, - $"Server=192.168.148.131;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" + $"Server=192.168.188.129;Database={dynamicShardInfo?.RequestOtherHeader["ShardingDbName"]};User Id=sa;Password=12345678;Encrypt=False;" ); + LoggerHelper.DefaultLog($"鏁版嵁搴撶被鍨:{dynamicShardInfo.ShardingDataBase},杩炴帴瀛楃涓瞷connectionDic[dynamicShardInfo.ShardingDataBase]}"); return connectionDic[dynamicShardInfo.ShardingDataBase]; }; //鍒嗘ā寮忓鎵-杩斿洖妯″紡鍚嶏紙string锛 diff --git a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs index 5efbf81..19befcb 100644 --- a/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs +++ b/LingYanAspCoreFramework/DynamicShard/Core/Extension/DynamicShardCoreExtension.cs @@ -94,7 +94,7 @@ namespace LingYanAspCoreFramework.DynamicShard.Core.Extension relationalDbContextOptionsBuilder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", $"dbo{dynamicShardOption.SchemaPostfix}"); break; case ConnectionType.PostgreSql: - relationalDbContextOptionsBuilder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", $"public{dynamicShardOption.SchemaPostfix}"); + relationalDbContextOptionsBuilder.MigrationsHistoryTable($"{dynamicShardOption.TablePrefix}EFMigrationHistory", $"dbo{dynamicShardOption.SchemaPostfix}"); break; } } diff --git a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs index 86fa42d..f583323 100644 --- a/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs +++ b/LingYanAspCoreFramework/DynamicShard/EFCore/Impl/DynamicShardEntityBuilder.cs @@ -48,7 +48,7 @@ namespace LingYanAspCoreFramework.DynamicShard.EFCore.Impl entity.ToTable(tableName, "dbo" + this.dynamicShardOption.SchemaPostfix); break; case ConnectionType.PostgreSql: - entity.ToTable(tableName, "public" + this.dynamicShardOption.SchemaPostfix); + entity.ToTable(tableName, "dbo" + this.dynamicShardOption.SchemaPostfix); break; } } diff --git a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json index a37e638..852722d 100644 --- a/LingYanAspCoreFramework/Envs/Configs/AppSetting.json +++ b/LingYanAspCoreFramework/Envs/Configs/AppSetting.json @@ -19,7 +19,7 @@ "RedisCofigModel": { "Pattern": "Single", // 鍗曟満妯″紡 - "Single": "192.168.148.131:6379,defaultDatabase=0,password=12345678,prefix=ling", + "Single": "192.168.188.128:6379,defaultDatabase=0,password=,prefix=ling", //鍝ㄥ叺妯″紡 "SentinelModel": { "Master": "longyumaster,password=,prefix=", -- Gitee From 2cd8c186713c69256e7cc684fbe0d4e8c0b3906f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E7=87=95=E7=A9=BA=E9=97=B4?= <1321180895@qq.com> Date: Mon, 7 Apr 2025 10:38:59 +0800 Subject: [PATCH 16/17] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E5=9F=BA=E7=B1=BB=EF=BC=8C=E6=B7=BB=E5=8A=A0=E9=87=8D?= =?UTF-8?q?=E8=AF=95http=E5=AE=A2=E6=88=B7=E7=AB=AF=E4=BB=A5=E9=80=82?= =?UTF-8?q?=E5=BA=94=E5=BE=AE=E6=9C=8D=E5=8A=A1=E5=92=8C=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E6=8E=A5=E5=85=A5=E7=9A=84=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BaseRoots/BaseEntity.cs | 2 +- .../BaseRoots/BaseModule.cs | 8 +- LingYanAspCoreFramework/BaseRoots/BaseRole.cs | 6 +- .../BaseRoots/BaseRoleRouteGroup.cs | 8 - .../BaseRoots/BaseRoute.cs | 16 +- LingYanAspCoreFramework/BaseRoots/BaseUser.cs | 11 - .../BaseRoots/BaseUserRoleGroup.cs | 8 - ...s => DynamicApplicationModelConvention.cs} | 4 +- .../DynamicApis/MvcOptionsExtensions.cs | 7 +- .../Events/DispatcherEventSubscription.cs | 40 +- .../Events/IDelegateReference.cs | 7 - .../Events/IEventAggregator.cs | 11 - .../Extensions/IocExtension.cs | 53 ++- .../Extensions/ProjectGetExtension.cs | 3 +- .../Extensions/ProjectRegisterExtension.cs | 8 +- .../Https/LingYanHttpClient.cs | 445 ------------------ .../Https/PollyHttpClient.cs | 69 +++ .../Https/PollyHttpClientOption.cs | 8 + .../Https/PollyHttpClientPart.cs | 138 ++++++ .../LingYanAspCoreFramework.csproj | 17 +- .../Models/WeChat/WeChatDevOption.cs | 8 +- ...ampleGlobalAuthorizationFilterAttribute.cs | 20 +- .../WeChatDevs/WeChatDevService.cs | 4 +- 23 files changed, 301 insertions(+), 600 deletions(-) delete mode 100644 LingYanAspCoreFramework/BaseRoots/BaseRoleRouteGroup.cs delete mode 100644 LingYanAspCoreFramework/BaseRoots/BaseUser.cs delete mode 100644 LingYanAspCoreFramework/BaseRoots/BaseUserRoleGroup.cs rename LingYanAspCoreFramework/DynamicApis/{DynamicControllerConvention.cs => DynamicApplicationModelConvention.cs} (97%) delete mode 100644 LingYanAspCoreFramework/Https/LingYanHttpClient.cs create mode 100644 LingYanAspCoreFramework/Https/PollyHttpClient.cs create mode 100644 LingYanAspCoreFramework/Https/PollyHttpClientOption.cs create mode 100644 LingYanAspCoreFramework/Https/PollyHttpClientPart.cs diff --git a/LingYanAspCoreFramework/BaseRoots/BaseEntity.cs b/LingYanAspCoreFramework/BaseRoots/BaseEntity.cs index 2a442a4..b61ea3e 100644 --- a/LingYanAspCoreFramework/BaseRoots/BaseEntity.cs +++ b/LingYanAspCoreFramework/BaseRoots/BaseEntity.cs @@ -6,7 +6,7 @@ namespace LingYanAspCoreFramework.BaseRoots { public long Id { get; set; } - //10鏂板缓銆20鍒犻櫎 + //鐪熷垹闄わ紝鍋囦笉鍒犻櫎 public bool IsDeleted { get; set; } public long CreateTimeStamp { get; set; } diff --git a/LingYanAspCoreFramework/BaseRoots/BaseModule.cs b/LingYanAspCoreFramework/BaseRoots/BaseModule.cs index 2137c1a..1732efe 100644 --- a/LingYanAspCoreFramework/BaseRoots/BaseModule.cs +++ b/LingYanAspCoreFramework/BaseRoots/BaseModule.cs @@ -6,9 +6,9 @@ { public virtual int PageIndex { get; set; } - public abstract void ARegisterModule(TCollection services); + public abstract void ARegisterModule(TCollection AppBuilder); - public abstract void BInitializationModule(TProvider provider); + public abstract void BInitializationModule(TProvider App); } public abstract class BaseModule @@ -18,8 +18,8 @@ { public virtual int PageIndex { get; set; } - public abstract void ARegisterModule(TCollection services, TConfiguration configuration); + public abstract void ARegisterModule(TCollection AppBuilder, TConfiguration configuration); - public abstract void BInitializationModule(TProvider provider); + public abstract void BInitializationModule(TProvider App); } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/BaseRoots/BaseRole.cs b/LingYanAspCoreFramework/BaseRoots/BaseRole.cs index 7c74c33..10e2370 100644 --- a/LingYanAspCoreFramework/BaseRoots/BaseRole.cs +++ b/LingYanAspCoreFramework/BaseRoots/BaseRole.cs @@ -1,8 +1,6 @@ 锘縩amespace LingYanAspCoreFramework.BaseRoots { - public class BaseRole : BaseEntity + public class BaseRole: BaseEntity { - public string RoleName { get; set; } - public string Description { get; set; } } -} \ No newline at end of file +} diff --git a/LingYanAspCoreFramework/BaseRoots/BaseRoleRouteGroup.cs b/LingYanAspCoreFramework/BaseRoots/BaseRoleRouteGroup.cs deleted file mode 100644 index 1509ec0..0000000 --- a/LingYanAspCoreFramework/BaseRoots/BaseRoleRouteGroup.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.BaseRoots -{ - public class BaseRoleRouteGroup : BaseEntity - { - public long RoleId { get; set; } - public long RouteId { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/BaseRoots/BaseRoute.cs b/LingYanAspCoreFramework/BaseRoots/BaseRoute.cs index 88b9b2b..af0638c 100644 --- a/LingYanAspCoreFramework/BaseRoots/BaseRoute.cs +++ b/LingYanAspCoreFramework/BaseRoots/BaseRoute.cs @@ -1,5 +1,4 @@ 锘縰sing LingYanAspCoreFramework.Helpers; -using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; namespace LingYanAspCoreFramework.BaseRoots @@ -11,26 +10,15 @@ namespace LingYanAspCoreFramework.BaseRoots public virtual string DisplayFullName { get; set; } public virtual string Controller { get; set; } public virtual string? ControllerSummary { get; set; } - public virtual string ControllerFullName { get; set; } + public virtual string ControllerFullName { get; set; } public virtual string RouteTemplate { get; set; } public virtual string HttpMethod { get; set; } public virtual string RoutePrefix { get; set; } public virtual string ProjectName { get; set; } - - [NotMapped] internal MethodInfo RefMethodInfo { get; set; } - - public BaseRoute() : base() + public BaseRoute() { this.ProjectName = ProjectHelper.GetProjectName(); } - - public void UpdateEvent(BaseRoute baseRoute) - { - if (baseRoute != null) - { - this.Id = baseRoute.Id; - } - } } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/BaseRoots/BaseUser.cs b/LingYanAspCoreFramework/BaseRoots/BaseUser.cs deleted file mode 100644 index ec372f1..0000000 --- a/LingYanAspCoreFramework/BaseRoots/BaseUser.cs +++ /dev/null @@ -1,11 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.BaseRoots -{ - public class BaseUser : BaseEntity - { - public string NickName { get; set; } - public string Phone { get; set; } - public string Password { get; set; } - public string Email { get; set; } - public string Avatar { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/BaseRoots/BaseUserRoleGroup.cs b/LingYanAspCoreFramework/BaseRoots/BaseUserRoleGroup.cs deleted file mode 100644 index 1f05b62..0000000 --- a/LingYanAspCoreFramework/BaseRoots/BaseUserRoleGroup.cs +++ /dev/null @@ -1,8 +0,0 @@ -锘縩amespace LingYanAspCoreFramework.BaseRoots -{ - public class BaseUserRoleGroup : BaseEntity - { - public long UserId { get; set; } - public long RoleId { get; set; } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs b/LingYanAspCoreFramework/DynamicApis/DynamicApplicationModelConvention.cs similarity index 97% rename from LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs rename to LingYanAspCoreFramework/DynamicApis/DynamicApplicationModelConvention.cs index 206cc1d..a02507d 100644 --- a/LingYanAspCoreFramework/DynamicApis/DynamicControllerConvention.cs +++ b/LingYanAspCoreFramework/DynamicApis/DynamicApplicationModelConvention.cs @@ -7,9 +7,9 @@ using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Configuration; -namespace LingYanAspCoreFramework.DynamicApis +namespace LingYanAspCoreFramework.DynamicApis { - internal class DynamicControllerConvention : IApplicationModelConvention + internal class DynamicApplicationModelConvention : IApplicationModelConvention { public void Apply(ApplicationModel application) { diff --git a/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs b/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs index 8233845..5e00118 100644 --- a/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs +++ b/LingYanAspCoreFramework/DynamicApis/MvcOptionsExtensions.cs @@ -1,7 +1,4 @@ -锘縰sing LingYanAspCoreFramework.DynamicApis; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.Extensions.Configuration; +锘縰sing Microsoft.AspNetCore.Mvc; namespace LingYanAspCoreFramework.DynamicApis { @@ -14,7 +11,7 @@ namespace LingYanAspCoreFramework.DynamicApis /// public static void UseCentralRouteJsonConfig(this MvcOptions opts) { - opts.Conventions.Insert(0, new DynamicControllerConvention()); + opts.Conventions.Insert(0, new DynamicApplicationModelConvention()); } } } diff --git a/LingYanAspCoreFramework/Events/DispatcherEventSubscription.cs b/LingYanAspCoreFramework/Events/DispatcherEventSubscription.cs index b6d84d7..310ce43 100644 --- a/LingYanAspCoreFramework/Events/DispatcherEventSubscription.cs +++ b/LingYanAspCoreFramework/Events/DispatcherEventSubscription.cs @@ -1,19 +1,19 @@ namespace LingYanAspCoreFramework.Events { /// - /// 扩展 以在特定的 中调用 委托。 + /// 锟斤拷展 锟斤拷锟斤拷锟截讹拷锟斤拷 锟叫碉拷锟斤拷 委锟叫★拷 /// public class DispatcherEventSubscription : EventSubscription { private readonly SynchronizationContext syncContext; /// - /// 创建一个新的 实例。 + /// 锟斤拷锟斤拷一锟斤拷锟铰碉拷 实锟斤拷锟斤拷 /// - ///指向 类型的委托的引用。 - ///用于UI线程调度的同步上下文。 - /// 时抛出。 - /// 的目标不是 类型时抛出。 + ///指锟斤拷 锟斤拷锟酵碉拷委锟叫碉拷锟斤拷锟矫★拷 + ///锟斤拷锟斤拷UI锟竭程碉拷锟饺碉拷同锟斤拷锟斤拷锟斤拷锟侥★拷 + ///锟斤拷 锟斤拷 时锟阶筹拷锟斤拷 + ///锟斤拷 锟斤拷目锟疥不锟斤拷 锟斤拷锟斤拷时锟阶筹拷锟斤拷 public DispatcherEventSubscription(IDelegateReference actionReference, SynchronizationContext context) : base(actionReference) { @@ -21,9 +21,9 @@ namespace LingYanAspCoreFramework.Events } /// - /// 在指定的 中异步调用指定的 。 + /// 锟斤拷指锟斤拷锟斤拷 锟斤拷锟届步锟斤拷锟斤拷指锟斤拷锟斤拷 锟斤拷 /// - /// 要执行的动作。 + /// 要执锟叫的讹拷锟斤拷锟斤拷 public override void InvokeAction(Action action) { syncContext.Post((o) => action(), null); @@ -31,22 +31,22 @@ namespace LingYanAspCoreFramework.Events } /// - /// 扩展 以在特定的 中调用 委托。 + /// 锟斤拷展 锟斤拷锟斤拷锟截讹拷锟斤拷 锟叫碉拷锟斤拷 委锟叫★拷 /// - /// 用于泛型 类型的类型。 + /// 锟斤拷锟节凤拷锟斤拷 锟斤拷 锟斤拷锟酵碉拷锟斤拷锟酵★拷 public class DispatcherEventSubscription : EventSubscription { private readonly SynchronizationContext syncContext; /// - /// 创建一个新的 实例。 + /// 锟斤拷锟斤拷一锟斤拷锟铰碉拷 实锟斤拷锟斤拷 /// - ///指向 类型的委托的引用。 - ///指向 类型的委托的引用。 - ///用于UI线程调度的同步上下文。 - /// 时抛出。 - /// 的目标不是 类型, - ///或 的目标不是 类型时抛出。 + ///指锟斤拷 锟斤拷锟酵碉拷委锟叫碉拷锟斤拷锟矫★拷 + ///指锟斤拷 锟斤拷锟酵碉拷委锟叫碉拷锟斤拷锟矫★拷 + ///锟斤拷锟斤拷UI锟竭程碉拷锟饺碉拷同锟斤拷锟斤拷锟斤拷锟侥★拷 + ///锟斤拷 锟斤拷 时锟阶筹拷锟斤拷 + ///锟斤拷 锟斤拷目锟疥不锟斤拷 锟斤拷锟酵o拷 + ///锟斤拷 锟斤拷目锟疥不锟斤拷 锟斤拷锟斤拷时锟阶筹拷锟斤拷 public DispatcherEventSubscription(IDelegateReference actionReference, IDelegateReference filterReference, SynchronizationContext context) : base(actionReference, filterReference) { @@ -54,10 +54,10 @@ namespace LingYanAspCoreFramework.Events } /// - /// 在指定的 中异步调用指定的 。 + /// 锟斤拷指锟斤拷锟斤拷 锟斤拷锟届步锟斤拷锟斤拷指锟斤拷锟斤拷 锟斤拷 /// - /// 要执行的动作。 - /// 调用时传递给 的负载。 + /// 要执锟叫的讹拷锟斤拷锟斤拷 + /// 锟斤拷锟斤拷时锟斤拷锟捷革拷 锟侥革拷锟截★拷 public override void InvokeAction(Action action, TPayload argument) { syncContext.Post((o) => action((TPayload)o), argument); diff --git a/LingYanAspCoreFramework/Events/IDelegateReference.cs b/LingYanAspCoreFramework/Events/IDelegateReference.cs index 78c3a55..9872813 100644 --- a/LingYanAspCoreFramework/Events/IDelegateReference.cs +++ b/LingYanAspCoreFramework/Events/IDelegateReference.cs @@ -1,14 +1,7 @@ namespace LingYanAspCoreFramework.Events { - /// - /// 表示对 的引用。 - /// public interface IDelegateReference { - /// - /// 获取引用的 对象。 - /// - /// 如果目标有效,则返回一个 实例;否则返回 Delegate Target { get; } } } \ No newline at end of file diff --git a/LingYanAspCoreFramework/Events/IEventAggregator.cs b/LingYanAspCoreFramework/Events/IEventAggregator.cs index 19a373e..b870700 100644 --- a/LingYanAspCoreFramework/Events/IEventAggregator.cs +++ b/LingYanAspCoreFramework/Events/IEventAggregator.cs @@ -1,18 +1,7 @@ namespace LingYanAspCoreFramework.Events { - /// - /// 定义了一个接口,用于获取事件类型的实例。 - /// public interface IEventAggregator { - /// - /// 获取指定类型的事件实例。 - /// - /// 要获取的事件类型,必须继承自 并提供无参构造函数。 - /// 返回一个类型为 的事件对象实例。 - /// - /// 此方法使用了泛型约束 where TEventType : EventBase, new(),确保传入的类型是 的子类,并且该类有一个无参构造函数。 - /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] TEventType GetEvent() where TEventType : EventBase, new(); } diff --git a/LingYanAspCoreFramework/Extensions/IocExtension.cs b/LingYanAspCoreFramework/Extensions/IocExtension.cs index 6a1adec..e7b0986 100644 --- a/LingYanAspCoreFramework/Extensions/IocExtension.cs +++ b/LingYanAspCoreFramework/Extensions/IocExtension.cs @@ -64,10 +64,11 @@ namespace LingYanAspCoreFramework.Extensions LoggerHelper.DefaultLog("褰掔撼椤圭洰妯″潡"); LingYanRuntimeManager.RuntimeCacheModel.GetModules(); LoggerHelper.DefaultLog("鑾峰彇瀹瑰櫒"); - var BuilderService = Hostbuilder.GetBuilderServiceCollection(); + var BuilderService = Hostbuilder.GetBuilderServiceCollection(); LoggerHelper.DefaultLog("娣诲姞璺ㄥ煙绛栫暐"); - BuilderService.AddCors(c => c.AddPolicy(SampleHelper.CrossPolicy, p => p.WithOrigins(LingYanRuntimeManager.CrossDomains).AllowAnyHeader().AllowAnyMethod().AllowCredentials())); + BuilderService.AddCors(c => + c.AddPolicy(SampleHelper.CrossPolicy, p => p.WithOrigins(LingYanRuntimeManager.CrossDomains).AllowAnyHeader().AllowAnyMethod().AllowCredentials())); LoggerHelper.DefaultLog("娣诲姞API绔偣"); BuilderService.AddEndpointsApiExplorer(); LoggerHelper.DefaultLog("娣诲姞SignalR"); @@ -76,13 +77,22 @@ namespace LingYanAspCoreFramework.Extensions BuilderService.AddSingleton(); LoggerHelper.DefaultLog("鎺у埗鍣ㄦ敞鍐屽叏灞杩囨护鍣+娉ㄥ唽id-JSON杞崲鍣╨ong<==>string"); BuilderService.AddControllers(mvcOption => - { + { LingYanRuntimeManager.RuntimeCacheModel.ModuleFiler.ForEach(filer => { mvcOption.Filters.Add(filer); }); - }).AddNewtonsoftJson(options => { options.SerializerSettings.Converters.Add(new LongToStringConverter()); }); - + }).AddNewtonsoftJson(options => + { + options.SerializerSettings.Converters.Add(new LongToStringConverter()); + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }); + //LoggerHelper.DefaultLog("閰嶇疆鍏ㄥ眬杞崲JsonSerializerSettings"); + //JsonSerializerSettings globalSettings = new JsonSerializerSettings + //{ + // Converters = new List { new LongToStringConverter() }, + // ReferenceLoopHandling = ReferenceLoopHandling.Ignore + //}; LoggerHelper.DefaultLog("娣诲姞瀹夊叏瀹氫箟"); BuilderService.AddSwaggerGen(swaggerGenOption => { @@ -107,7 +117,7 @@ namespace LingYanAspCoreFramework.Extensions //娉ㄨВ閰嶇疆 if (!File.Exists(SampleHelper.BaseDir.GetLocalUrl(ProjectHelper.GetProjectName() + ".xml"))) { - throw new CommonException(new ResponceBody(60000,"椤圭洰鏈嬀閫夌敓鎴愭椂xml娉ㄨВ鏂囦欢杈撳嚭")); + throw new CommonException(new ResponceBody(60000, "椤圭洰鏈嬀閫夌敓鎴愭椂xml娉ㄨВ鏂囦欢杈撳嚭")); } else { @@ -151,7 +161,7 @@ namespace LingYanAspCoreFramework.Extensions BuilderService.RegisterAuthenticationAndAuthorization(SampleHelper.BearerScheme, SampleHelper.EmpowerPolicy, LingYanRuntimeManager.JwtModel, () => new BaseAuthorizationRequirement(BuilderService.BuildServiceProvider().CreateScope())); LoggerHelper.DefaultLog("HttpContext涓婁笅鏂囩殑璁块棶"); - BuilderService.AddHttpContextAccessor(); + BuilderService.AddHttpContextAccessor(); LoggerHelper.DefaultLog("瀹瑰櫒娉ㄥ唽瀹樻柟閫氱敤鏈嶅姟"); if (Hostbuilder is WebApplicationBuilder web) { @@ -164,26 +174,26 @@ namespace LingYanAspCoreFramework.Extensions LoggerHelper.DefaultLog("娉ㄥ唽棰嗗煙Manager"); BuilderService.RegisterManager(LingYanRuntimeManager.RuntimeCacheModel); LoggerHelper.DefaultLog("娉ㄥ唽鏈嶅姟TService"); - BuilderService.RegisterTService(LingYanRuntimeManager.RuntimeCacheModel); + BuilderService.RegisterTService(LingYanRuntimeManager.RuntimeCacheModel); LoggerHelper.DefaultLog("娉ㄥ唽瀹炰緥"); BuilderService.RegisterTInstance(LingYanRuntimeManager.RuntimeCacheModel); LoggerHelper.DefaultLog("娉ㄥ唽鏁版嵁搴撲笂涓嬫枃"); BuilderService.RegisterDbContext(LingYanRuntimeManager.RuntimeCacheModel); - + LoggerHelper.DefaultLog("娉ㄥ唽Redis"); BuilderService.RegisterRedis(LingYanRuntimeManager.RuntimeCacheModel); LoggerHelper.DefaultLog("娉ㄥ唽鍔ㄦ佽矾鐢"); - BuilderService.RegisterDynamicWebApi(); - + BuilderService.RegisterDynamicWebApi(); + LoggerHelper.DefaultLog("娉ㄥ唽HttpClient"); BuilderService.AddHttpClient(); LoggerHelper.DefaultLog("娉ㄥ唽LingYanHttpClient"); - BuilderService.AddSingleton(); + BuilderService.AddSingleton(); LoggerHelper.DefaultLog("娉ㄥ唽鏀粯瀹濆寘"); BuilderService.AddAlipay(); LoggerHelper.DefaultLog("娉ㄥ唽寰俊鏀粯鍖"); - BuilderService.AddWeChatPay(); + BuilderService.AddWeChatPay(); LoggerHelper.DefaultLog("閰嶇疆鏂囦欢鎵╁睍绫诲瀷"); BuilderService.Configure(option => @@ -209,9 +219,9 @@ namespace LingYanAspCoreFramework.Extensions }); LoggerHelper.DefaultLog("閰嶇疆琛ㄥ崟"); BuilderService.ConfigureFormSize(); - LoggerHelper.DefaultLog("閰嶇疆鏀粯瀹"); + LoggerHelper.DefaultLog("閰嶇疆鏀粯瀹"); BuilderService.Configure(SampleHelper.LingAppsettings.GetSection("Alipay")); - LoggerHelper.DefaultLog("閰嶇疆寰俊鏀粯"); + LoggerHelper.DefaultLog("閰嶇疆寰俊鏀粯"); BuilderService.Configure(SampleHelper.LingAppsettings.GetSection("WeChatPay")); LoggerHelper.DefaultLog("閰嶇疆寰俊寮鏀惧钩鍙"); BuilderService.Configure(SampleHelper.LingAppsettings.GetSection("WeChatDevOption")); @@ -224,19 +234,12 @@ namespace LingYanAspCoreFramework.Extensions BuilderService.AddSingleton(); LoggerHelper.DefaultLog("娉ㄥ唽寰俊寮鏀惧钩鍙版湇鍔"); BuilderService.AddSingleton(); - - LoggerHelper.DefaultLog("閰嶇疆鍏ㄥ眬杞崲JsonSerializerSettings"); - JsonSerializerSettings globalSettings = new JsonSerializerSettings - { - Converters = new List { new LongToStringConverter() }, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }; LoggerHelper.DefaultLog("娉ㄥ唽椤圭洰妯″潡"); Hostbuilder.RegisterModule(LingYanRuntimeManager.RuntimeCacheModel); } catch (Exception ex) { - throw new CommonException(new ResponceBody(60000,ex.Message), ex); + throw new CommonException(new ResponceBody(60000, ex.Message), ex); } } @@ -286,7 +289,7 @@ namespace LingYanAspCoreFramework.Extensions web.UseMiddleware(item); } LoggerHelper.WarnrningLog($"椤圭洰鍚勬ā鍧楀垵濮嬪寲"); - LingYanRuntimeManager.RuntimeCacheModel.ModuleAssemblyBaseLoadingKeyValue.Values.InitModules(app); + LingYanRuntimeManager.RuntimeCacheModel.ModuleAssemblyBaseLoadingKeyValue.Values.InitModules(app); LoggerHelper.WarnrningLog($"鍚敤鍏ㄥ眬寮鍚韩浠介獙璇"); web.MapControllers().RequireAuthorization(); LoggerHelper.WarnrningLog($"鍚敤闈欐佹枃浠跺す-鑷畾涔変腑闂翠欢浠ョ伒娲荤殑瀹炵幇閴存潈鎺堟潈"); @@ -311,7 +314,7 @@ namespace LingYanAspCoreFramework.Extensions } catch (Exception ex) { - throw new CommonException(new ResponceBody(60000,ex.Message), ex); + throw new CommonException(new ResponceBody(60000, ex.Message), ex); } } } diff --git a/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs b/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs index 439f41c..9c5e68e 100644 --- a/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs +++ b/LingYanAspCoreFramework/Extensions/ProjectGetExtension.cs @@ -101,8 +101,7 @@ namespace LingYanAspCoreFramework.Extensions { var moduleEntitys = assembly.GetTypes() .Where(x => x.BaseType != null && !x.BaseType.IsGenericType && - (x.BaseType == typeof(BaseUser) || x.BaseType == typeof(BaseRole) || x.BaseType == typeof(BaseRoute) || - x.BaseType == typeof(BaseUserRoleGroup) || x.BaseType == typeof(BaseRoleRouteGroup)) + (x.BaseType == typeof(BaseEntity)) && !assembly.GetTypes().Any(z => z.BaseType == x)).ToList(); foreach (var entity in moduleEntitys) { diff --git a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs index 0fa6d47..be361ae 100644 --- a/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs +++ b/LingYanAspCoreFramework/Extensions/ProjectRegisterExtension.cs @@ -174,16 +174,12 @@ namespace LingYanAspCoreFramework.Extensions backBody = null; break; } - if (backBody != null && backBody.Nodes.Count > 0 && backBody.Nodes.Any(a => a.Value.IsAvailable)) + if (backBody?.Nodes.Count > 0 && backBody.Nodes.Any(a => a.Value.IsAvailable)) { LoggerHelper.DefaultLog("銆怰edis娉ㄥ唽鎴愬姛銆..."); RedisHelper.Initialization(backBody); } - else - { - backBody.Dispose(); - LoggerHelper.ErrorLog("銆怰edis鍙栨秷娉ㄥ唽銆..."); - } + LoggerHelper.DefaultLog("銆怰edis鍙栨秷娉ㄥ唽銆..."); } /// diff --git a/LingYanAspCoreFramework/Https/LingYanHttpClient.cs b/LingYanAspCoreFramework/Https/LingYanHttpClient.cs deleted file mode 100644 index 5634479..0000000 --- a/LingYanAspCoreFramework/Https/LingYanHttpClient.cs +++ /dev/null @@ -1,445 +0,0 @@ -锘縰sing LingYanAspCoreFramework.Models; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System.Net.Http.Headers; -using System.Web; - -namespace LingYanAspCoreFramework.Https -{ - public class LingYanHttpClient - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger _logger; - - public LingYanHttpClient(IHttpClientFactory httpClientFactory, ILogger logger) - { - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - /// - /// 鍙戦 GET 璇锋眰 - /// - /// 杩斿洖鏁版嵁鐨勭被鍨 - /// 璇锋眰鐨 URL - /// 鏌ヨ鍙傛暟 - /// 璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// 杩斿洖鏁版嵁 - public async Task GetAsync(string url, Dictionary queryParams = null, Dictionary dicHeaders = null, int timeoutSecond = 180, string clientName = null, HttpMessageHandler handler = null) - { - try - { - var client = BuildHttpClient(dicHeaders, timeoutSecond, clientName, handler); - if (queryParams != null) - { - url = AppendQueryParams(url, queryParams); - } - var response = await client.GetAsync(url); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(responseContent); - } - else - { - throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); - } - } - catch (HttpRequestException ex) - { - _logger.LogError($"HttpGet:{url} HttpRequestException:{ex}"); - throw new Exception($"HttpGet:{url} HttpRequestException", ex); - } - catch (TaskCanceledException ex) - { - _logger.LogError($"HttpGet:{url} TaskCanceledException:{ex}"); - throw new Exception($"HttpGet:{url} TaskCanceledException", ex); - } - catch (OperationCanceledException ex) - { - _logger.LogError($"HttpGet:{url} OperationCanceledException:{ex}"); - throw new Exception($"HttpGet:{url} OperationCanceledException", ex); - } - catch (Exception ex) - { - _logger.LogError($"HttpGet:{url} Error:{ex}"); - throw new Exception($"HttpGet:{url} Error", ex); - } - } - - /// - /// 鍙戦 POST 璇锋眰 - /// - /// 杩斿洖鏁版嵁鐨勭被鍨 - /// 璇锋眰鐨 URL - /// 璇锋眰浣 - /// 璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// 杩斿洖鏁版嵁 - public async Task PostAsync(string url, object requestBody, Dictionary dicHeaders = null, int timeoutSecond = 180, string clientName = null, HttpMessageHandler handler = null) - { - try - { - var client = BuildHttpClient(dicHeaders, timeoutSecond, clientName, handler); - var requestContent = GenerateHttpContent(requestBody, dicHeaders); - var response = await client.PostAsync(url, requestContent); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(responseContent); - } - else - { - throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); - } - } - catch (HttpRequestException ex) - { - _logger.LogError($"HttpPost:{url} HttpRequestException:{ex}"); - throw new Exception($"HttpPost:{url} HttpRequestException", ex); - } - catch (TaskCanceledException ex) - { - _logger.LogError($"HttpPost:{url} TaskCanceledException:{ex}"); - throw new Exception($"HttpPost:{url} TaskCanceledException", ex); - } - catch (OperationCanceledException ex) - { - _logger.LogError($"HttpPost:{url} OperationCanceledException:{ex}"); - throw new Exception($"HttpPost:{url} OperationCanceledException", ex); - } - catch (Exception ex) - { - _logger.LogError($"HttpPost:{url},body:{requestBody} Error:{ex}"); - throw new Exception($"HttpPost:{url} Error", ex); - } - } - - /// - /// 鍙戦 PUT 璇锋眰 - /// - /// 杩斿洖鏁版嵁鐨勭被鍨 - /// 璇锋眰鐨 URL - /// 璇锋眰浣 - /// 璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// 杩斿洖鏁版嵁 - public async Task PutAsync(string url, object requestBody, Dictionary dicHeaders = null, int timeoutSecond = 180, string clientName = null, HttpMessageHandler handler = null) - { - try - { - var client = BuildHttpClient(dicHeaders, timeoutSecond, clientName, handler); - var requestContent = GenerateHttpContent(requestBody, dicHeaders); - var response = await client.PutAsync(url, requestContent); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(responseContent); - } - else - { - throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); - } - } - catch (HttpRequestException ex) - { - _logger.LogError($"HttpPut:{url} HttpRequestException:{ex}"); - throw new Exception($"HttpPut:{url} HttpRequestException", ex); - } - catch (TaskCanceledException ex) - { - _logger.LogError($"HttpPut:{url} TaskCanceledException:{ex}"); - throw new Exception($"HttpPut:{url} TaskCanceledException", ex); - } - catch (OperationCanceledException ex) - { - _logger.LogError($"HttpPut:{url} OperationCanceledException:{ex}"); - throw new Exception($"HttpPut:{url} OperationCanceledException", ex); - } - catch (Exception ex) - { - _logger.LogError($"HttpPut:{url},Body:{requestBody}, Error:{ex}"); - throw new Exception($"HttpPut:{url} Error", ex); - } - } - - /// - /// 鍙戦 PATCH 璇锋眰 - /// - /// 杩斿洖鏁版嵁鐨勭被鍨 - /// 璇锋眰鐨 URL - /// 璇锋眰浣 - /// 璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// 杩斿洖鏁版嵁 - public async Task PatchAsync(string url, object requestBody, Dictionary dicHeaders = null, int timeoutSecond = 180, string clientName = null, HttpMessageHandler handler = null) - { - try - { - var client = BuildHttpClient(dicHeaders, timeoutSecond, clientName, handler); - var requestContent = GenerateHttpContent(requestBody, dicHeaders); - var response = await client.PatchAsync(url, requestContent); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(responseContent); - } - else - { - throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); - } - } - catch (HttpRequestException ex) - { - _logger.LogError($"HttpPatch:{url} HttpRequestException:{ex}"); - throw new Exception($"HttpPatch:{url} HttpRequestException", ex); - } - catch (TaskCanceledException ex) - { - _logger.LogError($"HttpPatch:{url} TaskCanceledException:{ex}"); - throw new Exception($"HttpPatch:{url} TaskCanceledException", ex); - } - catch (OperationCanceledException ex) - { - _logger.LogError($"HttpPatch:{url} OperationCanceledException:{ex}"); - throw new Exception($"HttpPatch:{url} OperationCanceledException", ex); - } - catch (Exception ex) - { - _logger.LogError($"HttpPatch:{url},body:{requestBody}, Error:{ex}"); - throw new Exception($"HttpPatch:{url} Error", ex); - } - } - - /// - /// 鍙戦 DELETE 璇锋眰 - /// - /// 杩斿洖鏁版嵁鐨勭被鍨 - /// 璇锋眰鐨 URL - /// 璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// 杩斿洖鏁版嵁 - public async Task DeleteAsync(string url, Dictionary dicHeaders = null, int timeoutSecond = 180,string clientName = null, HttpMessageHandler handler = null) - { - try - { - var client = BuildHttpClient(dicHeaders, timeoutSecond, clientName, handler); - var response = await client.DeleteAsync(url); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - return JsonConvert.DeserializeObject(responseContent); - } - else - { - throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); - } - } - catch (HttpRequestException ex) - { - _logger.LogError($"HttpDelete:{url} HttpRequestException:{ex}"); - throw new Exception($"HttpDelete:{url} HttpRequestException", ex); - } - catch (TaskCanceledException ex) - { - _logger.LogError($"HttpDelete:{url} TaskCanceledException:{ex}"); - throw new Exception($"HttpDelete:{url} TaskCanceledException", ex); - } - catch (OperationCanceledException ex) - { - _logger.LogError($"HttpDelete:{url} OperationCanceledException:{ex}"); - throw new Exception($"HttpDelete:{url} OperationCanceledException", ex); - } - catch (Exception ex) - { - _logger.LogError($"HttpDelete:{url}, Error:{ex}"); - throw new Exception($"HttpDelete:{url} Error", ex); - } - } - - /// - /// 鍙戦侀氱敤璇锋眰 - /// - /// 杩斿洖鏁版嵁鐨勭被鍨 - /// 璇锋眰鐨 URL - /// HTTP 鏂规硶 - /// 璇锋眰浣 - /// 鏌ヨ鍙傛暟 - /// 璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// 杩斿洖鏁版嵁 - public async Task ExecuteAsync(string url, HttpMethod method,object requestBody = null, Dictionary queryParams = null, Dictionary dicHeaders = null, int timeoutSecond = 180, string clientName = null, HttpMessageHandler handler = null) - { - try - { - var client = BuildHttpClient(dicHeaders, timeoutSecond, clientName, handler); - if (queryParams != null) - { - url = AppendQueryParams(url, queryParams); - } - var request = GenerateHttpRequestMessage(url, requestBody, method, dicHeaders); - var response = await client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - if (response.IsSuccessStatusCode) - { - var type = typeof(T); - if (type.IsPrimitive || type == typeof(string)) - { - return (T)Convert.ChangeType(responseContent, typeof(T)); - } - else - { - return JsonConvert.DeserializeObject(responseContent); - } - } - else - { - throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); - } - } - catch (HttpRequestException ex) - { - _logger.LogError($"{method}:{url} HttpRequestException:{ex}"); - throw new Exception($"{method}:{url} HttpRequestException", ex); - } - catch (TaskCanceledException ex) - { - _logger.LogError($"{method}:{url} TaskCanceledException:{ex}"); - throw new Exception($"{method}:{url} TaskCanceledException", ex); - } - catch (OperationCanceledException ex) - { - _logger.LogError($"{method}:{url} OperationCanceledException:{ex}"); - throw new Exception($"{method}:{url} OperationCanceledException", ex); - } - catch (Exception ex) - { - _logger.LogError($"{method}:{url},body:{requestBody}, Error:{ex}"); - throw new Exception($"{method}:{url} Error", ex); - } - } - - /// - /// 鏋勫缓 HttpClient - /// - /// 榛樿璇锋眰澶 - /// 瓒呮椂鏃堕棿锛堢锛 - /// HttpClient 鍚嶇О - /// 鑷畾涔 HttpMessageHandler - /// HttpClient 瀹炰緥 - private HttpClient BuildHttpClient(Dictionary dicDefaultHeaders, int? timeoutSecond, string clientName = null, HttpMessageHandler handler = null) - { - HttpClient httpClient; - if (handler != null) - { - httpClient = new HttpClient(handler); - } - else if (!string.IsNullOrEmpty(clientName)) - { - httpClient = _httpClientFactory.CreateClient(clientName); - } - else - { - httpClient = _httpClientFactory.CreateClient(); - } - - httpClient.DefaultRequestHeaders.Clear(); - httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - if (dicDefaultHeaders != null) - { - foreach (var headerItem in dicDefaultHeaders) - { - httpClient.DefaultRequestHeaders.Add(headerItem.Key, headerItem.Value); - } - } - if (timeoutSecond != null) - { - httpClient.Timeout = TimeSpan.FromSeconds(timeoutSecond.Value); - } - return httpClient; - } - - /// - /// 鐢熸垚 HttpRequestMessage - /// - /// 璇锋眰鐨 URL - /// 璇锋眰浣 - /// HTTP 鏂规硶 - /// 璇锋眰澶 - /// HttpRequestMessage 瀹炰緥 - private HttpRequestMessage GenerateHttpRequestMessage(string url, object requestBody, HttpMethod method, Dictionary dicHeaders) - { - var request = new HttpRequestMessage(method, url); - if (requestBody != null) - { - request.Content = GenerateHttpContent(requestBody, dicHeaders); - } - if (dicHeaders != null) - { - foreach (var header in dicHeaders) - { - request.Headers.Add(header.Key, header.Value); - } - } - return request; - } - - /// - /// 鐢熸垚 HttpContent - /// - /// 璇锋眰浣 - /// 璇锋眰澶 - /// HttpContent 瀹炰緥 - private HttpContent GenerateHttpContent(object requestBody, Dictionary dicHeaders) - { - HttpContent content; - switch (requestBody) - { - case string str: - content = new StringContent(str, System.Text.Encoding.UTF8, "application/json"); - break; - case byte[] bytes: - content = new ByteArrayContent(bytes); - content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - break; - case Dictionary formData: - content = new FormUrlEncodedContent(formData); - break; - case MultipartFormDataContent multipartFormData: - content = multipartFormData; - break; - default: - var json = JsonConvert.SerializeObject(requestBody); - content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); - break; - } - if (dicHeaders != null) - { - foreach (var headerItem in dicHeaders) - { - content.Headers.Add(headerItem.Key, headerItem.Value); - } - } - return content; - } - - /// - /// 娣诲姞鏌ヨ鍙傛暟鍒 URL - /// - /// 璇锋眰鐨 URL - /// 鏌ヨ鍙傛暟 - /// 甯︽煡璇㈠弬鏁扮殑 URL - private string AppendQueryParams(string url, Dictionary queryParams) - { - var uriBuilder = new UriBuilder(url); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - foreach (var param in queryParams) - { - query[param.Key] = param.Value; - } - uriBuilder.Query = query.ToString(); - return uriBuilder.ToString(); - } - } -} \ No newline at end of file diff --git a/LingYanAspCoreFramework/Https/PollyHttpClient.cs b/LingYanAspCoreFramework/Https/PollyHttpClient.cs new file mode 100644 index 0000000..f2aeb85 --- /dev/null +++ b/LingYanAspCoreFramework/Https/PollyHttpClient.cs @@ -0,0 +1,69 @@ +锘縰sing LingYanAspCoreFramework.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Polly; +using Polly.Retry; + +namespace LingYanAspCoreFramework.Https +{ + public partial class PollyHttpClient + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly AsyncRetryPolicy _retryPolicy; + private readonly PollyHttpClientOption _options; + + public PollyHttpClient(IHttpClientFactory httpClientFactory, ILogger logger, IOptions options) + { + _httpClientFactory = httpClientFactory; + _logger = logger; + _options = options.Value; + _retryPolicy = Policy.HandleResult(r => !r.IsSuccessStatusCode) + .WaitAndRetryAsync(_options.RetryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + (response, timespan, retryCount, context) => + { + _logger.LogWarning($"璇锋眰澶辫触,绛夊緟{timespan}鍚庨噸璇,閲嶈瘯娆℃暟{retryCount}"); + }); + } + + public async Task ExecuteAsync(string url, HttpMethod method, object requestBody = null, Dictionary queryParams = null, Dictionary dicHeaders = null, int? timeoutSecond = null, string clientName = null, HttpMessageHandler handler = null) + { + try + { + var client = BuildHttpClient(dicHeaders, timeoutSecond ?? _options.DefaultTimeout, clientName, handler); + if (queryParams != null) + { + url = AppendQueryParams(url, queryParams); + } + var request = GenerateHttpRequestMessage(url, requestBody, method, dicHeaders); + var response = await _retryPolicy.ExecuteAsync(() => client.SendAsync(request)); + var responseContent = await response.Content.ReadAsStringAsync(); + LogRequestAndResponse(url, requestBody, responseContent, response.IsSuccessStatusCode); + if (response.IsSuccessStatusCode) + { + var type = typeof(T); + if (type.IsPrimitive || type == typeof(string)) + { + return (T)Convert.ChangeType(responseContent, typeof(T)); + } + else + { + return JsonConvert.DeserializeObject(responseContent); + } + } + else + { + throw new CommonException(new ResponceBody(61000, response.StatusCode.ToString() + responseContent)); + } + } + catch (Exception ex) + { + _logger.LogError($"{method}:{url} 閿欒:{ex}"); + throw; + } + } + + + } +} \ No newline at end of file diff --git a/LingYanAspCoreFramework/Https/PollyHttpClientOption.cs b/LingYanAspCoreFramework/Https/PollyHttpClientOption.cs new file mode 100644 index 0000000..c18464e --- /dev/null +++ b/LingYanAspCoreFramework/Https/PollyHttpClientOption.cs @@ -0,0 +1,8 @@ +锘縩amespace LingYanAspCoreFramework.Https +{ + public class PollyHttpClientOption + { + public int RetryCount { get; set; } = 3; + public int DefaultTimeout { get; set; } = 180; + } +} diff --git a/LingYanAspCoreFramework/Https/PollyHttpClientPart.cs b/LingYanAspCoreFramework/Https/PollyHttpClientPart.cs new file mode 100644 index 0000000..ae1ec8a --- /dev/null +++ b/LingYanAspCoreFramework/Https/PollyHttpClientPart.cs @@ -0,0 +1,138 @@ +锘縰sing Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System.Net.Http.Headers; +using System.Web; + +namespace LingYanAspCoreFramework.Https +{ + public partial class PollyHttpClient + { + public Task GetAsync(string url, Dictionary queryParams = null, Dictionary dicHeaders = null, int? timeoutSecond = null, string clientName = null, HttpMessageHandler handler = null) + { + return ExecuteAsync(url, HttpMethod.Get, null, queryParams, dicHeaders, timeoutSecond, clientName, handler); + } + + public Task PostAsync(string url, object requestBody, Dictionary dicHeaders = null, int? timeoutSecond = null, string clientName = null, HttpMessageHandler handler = null) + { + return ExecuteAsync(url, HttpMethod.Post, requestBody, null, dicHeaders, timeoutSecond, clientName, handler); + } + + public Task PutAsync(string url, object requestBody, Dictionary dicHeaders = null, int? timeoutSecond = null, string clientName = null, HttpMessageHandler handler = null) + { + return ExecuteAsync(url, HttpMethod.Put, requestBody, null, dicHeaders, timeoutSecond, clientName, handler); + } + + public Task PatchAsync(string url, object requestBody, Dictionary dicHeaders = null, int? timeoutSecond = null, string clientName = null, HttpMessageHandler handler = null) + { + return ExecuteAsync(url, HttpMethod.Patch, requestBody, null, dicHeaders, timeoutSecond, clientName, handler); + } + + public Task DeleteAsync(string url, Dictionary dicHeaders = null, int? timeoutSecond = null, string clientName = null, HttpMessageHandler handler = null) + { + return ExecuteAsync(url, HttpMethod.Delete, null, null, dicHeaders, timeoutSecond, clientName, handler); + } + + + private HttpClient BuildHttpClient(Dictionary dicDefaultHeaders, int timeoutSecond, string clientName = null, HttpMessageHandler handler = null) + { + HttpClient httpClient; + if (handler != null) + { + httpClient = new HttpClient(handler); + } + else if (!string.IsNullOrEmpty(clientName)) + { + httpClient = _httpClientFactory.CreateClient(clientName); + } + else + { + httpClient = _httpClientFactory.CreateClient(); + } + + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + if (dicDefaultHeaders != null) + { + foreach (var headerItem in dicDefaultHeaders) + { + httpClient.DefaultRequestHeaders.Add(headerItem.Key, headerItem.Value); + } + } + httpClient.Timeout = TimeSpan.FromSeconds(timeoutSecond); + return httpClient; + } + + private HttpRequestMessage GenerateHttpRequestMessage(string url, object requestBody, HttpMethod method, Dictionary dicHeaders) + { + var request = new HttpRequestMessage(method, url); + if (requestBody != null) + { + request.Content = GenerateHttpContent(requestBody, dicHeaders); + } + if (dicHeaders != null) + { + foreach (var header in dicHeaders) + { + request.Headers.Add(header.Key, header.Value); + } + } + return request; + } + + private HttpContent GenerateHttpContent(object requestBody, Dictionary dicHeaders) + { + HttpContent content; + switch (requestBody) + { + case string str: + content = new StringContent(str, System.Text.Encoding.UTF8, "application/json"); + break; + case byte[] bytes: + content = new ByteArrayContent(bytes); + content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + break; + case Dictionary formData: + content = new FormUrlEncodedContent(formData); + break; + case MultipartFormDataContent multipartFormData: + content = multipartFormData; + break; + default: + var json = JsonConvert.SerializeObject(requestBody); + content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + break; + } + if (dicHeaders != null) + { + foreach (var headerItem in dicHeaders) + { + content.Headers.Add(headerItem.Key, headerItem.Value); + } + } + return content; + } + + private string AppendQueryParams(string url, Dictionary queryParams) + { + var uriBuilder = new UriBuilder(url); + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + foreach (var param in queryParams) + { + query[param.Key] = param.Value; + } + uriBuilder.Query = query.ToString(); + return uriBuilder.ToString(); + } + + private void LogRequestAndResponse(string url, object requestBody, string responseContent, bool isSuccessStatusCode) + { + _logger.LogInformation($"璺敱: {url}"); + if (requestBody != null) + { + _logger.LogInformation($"璇锋眰浣: {JsonConvert.SerializeObject(requestBody)}"); + } + _logger.LogInformation($"杩斿洖浣: {responseContent}"); + _logger.LogInformation($"鐘舵: {isSuccessStatusCode}"); + } + } +} diff --git a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj index 3ffc75f..c56e75b 100644 --- a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj +++ b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj @@ -27,30 +27,31 @@ - - - - + + + + + - - + + - + - + diff --git a/LingYanAspCoreFramework/Models/WeChat/WeChatDevOption.cs b/LingYanAspCoreFramework/Models/WeChat/WeChatDevOption.cs index 9013122..c7806e2 100644 --- a/LingYanAspCoreFramework/Models/WeChat/WeChatDevOption.cs +++ b/LingYanAspCoreFramework/Models/WeChat/WeChatDevOption.cs @@ -1,10 +1,4 @@ -锘縰sing System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LingYanAspCoreFramework.Models.WeChat +锘縩amespace LingYanAspCoreFramework.Models.WeChat { public class WeChatDevOption { diff --git a/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs b/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs index 80ca3b9..b93f97d 100644 --- a/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs +++ b/LingYanAspCoreFramework/SampleRoots/SampleGlobalAuthorizationFilterAttribute.cs @@ -9,18 +9,17 @@ using Microsoft.Extensions.Logging; namespace LingYanAspCoreFramework.SampleRoots { - public class SampleGlobalAuthorizationFilterAttribute : Attribute, IAuthorizationFilter - where TBaseRole : BaseRole, new() - where TBaseRoute : BaseRoute, new() + public class SampleGlobalAuthorizationFilterAttribute : Attribute, IAsyncAuthorizationFilter + where TBaseRole : BaseRole, new() + where TBaseRoute : BaseRoute, new() { - public virtual async void OnAuthorization(AuthorizationFilterContext context) + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { LoggerHelper.ResolveFilterLogger(context).LogDebug(LoggerHelper.AuthorEvent, "娓稿璁板綍"); context.HttpContext.Items[SampleHelper.ClaimLogStart] = DateTime.Now; - //閴村畾鏄惁鍏朵粬鍦版柟鐧诲綍 - var methodInfo = (context.ActionDescriptor as ControllerActionDescriptor).MethodInfo; // 棣栧厛妫鏌ユ柟娉曚笂鏄惁鏈塠AllowAnonymous]鐗规 - bool hasAllowAnonymousOnMethod = methodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any(); + bool hasAllowAnonymousOnMethod = (context.ActionDescriptor as ControllerActionDescriptor). + MethodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any(); // 濡傛灉鏂规硶涓婃病鏈夋壘鍒帮紝妫鏌ユ帶鍒跺櫒绾у埆 bool hasAllowAnonymousOnController = (context.ActionDescriptor as ControllerActionDescriptor). ControllerTypeInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any(); @@ -29,7 +28,7 @@ namespace LingYanAspCoreFramework.SampleRoots var userId = context.HttpContext.Items?.FirstOrDefault(f => f.Key == SampleHelper.ClaimUserId).Value?.ToString(); //鎶㈠崰鐧诲綍鐘舵 var requestHeaderToke = context.HttpContext.Request.Headers.FirstOrDefault(f => f.Key == "Authorization").Value.ToString(); - var redisToken =await RedisHelper.HGetAsync(SampleHelper.RedisUserToken, userId); + var redisToken = await RedisHelper.HGetAsync(SampleHelper.RedisUserToken, userId); if (!string.IsNullOrEmpty(requestHeaderToke) && !string.IsNullOrEmpty(redisToken)) { if (requestHeaderToke != redisToken) @@ -43,10 +42,11 @@ namespace LingYanAspCoreFramework.SampleRoots await ValidateAuthor(context, requestUrl, userId); } } + public virtual async Task ValidateAuthor(AuthorizationFilterContext context, string requestUrl, string userId) { // 鑾峰彇鐢ㄦ埛鐨勬墍鏈夎鑹睮D鍒楄〃 - var userRolesJson =await RedisHelper.HGetAsync>(SampleHelper.RedisUserRole, userId); + var userRolesJson = await RedisHelper.HGetAsync>(SampleHelper.RedisUserRole, userId); if (userRolesJson == null || userRolesJson.Count == 0) { context.Result = new OkObjectResult(new ResponceBody(42000, "瑙掕壊鏈垎閰", null)); @@ -56,7 +56,7 @@ namespace LingYanAspCoreFramework.SampleRoots var allRequestRoutes = new List(); foreach (var roleEntity in userRolesJson) { - var routeEntitys =await RedisHelper.HGetAsync>(SampleHelper.RedisRoleRoute, roleEntity.Id.ToString()); + var routeEntitys = await RedisHelper.HGetAsync>(SampleHelper.RedisRoleRoute, roleEntity.Id.ToString()); if (routeEntitys != null && routeEntitys.Count > 0) { allRequestRoutes.AddRange(routeEntitys); diff --git a/LingYanAspCoreFramework/WeChatDevs/WeChatDevService.cs b/LingYanAspCoreFramework/WeChatDevs/WeChatDevService.cs index 01e0e7e..325b4a2 100644 --- a/LingYanAspCoreFramework/WeChatDevs/WeChatDevService.cs +++ b/LingYanAspCoreFramework/WeChatDevs/WeChatDevService.cs @@ -10,10 +10,10 @@ namespace LingYanAspCoreFramework.WeChatDevs { public class WeChatDevService : IWeChatDevService { - private readonly LingYanHttpClient lingYanHttpClient; + private readonly PollyHttpClient lingYanHttpClient; private readonly IOptions options; - public WeChatDevService(LingYanHttpClient lingYanHttpClient, IOptions options) + public WeChatDevService(PollyHttpClient lingYanHttpClient, IOptions options) { this.lingYanHttpClient = lingYanHttpClient; this.options = options; -- Gitee From 7759466dd8f348ee54e62c39fa8613ee4df5480e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=20=E7=8E=8B?= <1321180895@qq.com> Date: Sat, 19 Apr 2025 18:49:34 +0800 Subject: [PATCH 17/17] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=89=93=E5=8C=85?= =?UTF-8?q?=E5=BF=85=E5=A4=87=E9=85=8D=E7=BD=AE=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LingYanAspCoreFramework.csproj | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj index c56e75b..1beed3f 100644 --- a/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj +++ b/LingYanAspCoreFramework/LingYanAspCoreFramework.csproj @@ -5,7 +5,7 @@ enable True LingYanAspCoreFramework - 2.0.7 + 8.0.0 閬靛畧DDD棰嗗煙璁捐鎬濇兂锛岄拡瀵笰SP.NET CORE鐨勫悗绔垎灞傛鏋讹紝灏佽濂界伒娲荤殑浠撳偍銆佸伐浣滃崟鍏冿紝鍚勭鏈嶅姟鏈夌壒鎬ф爣璁板彲浠ヨ嚜鍔ㄦ敞鍏 LingYanSpace @@ -53,18 +53,24 @@ - + + + true + contentFiles\any\any\Envs\ + + + True -- Gitee