環境:
.net core 3.1
MSSSQL , MYSQL
MVC
EFCore
AutoFac
前言:
不同的框架主要解決開發中出現的不同的問題,本框架主要解決多個項目在開發過程中多個模塊的重複使用造成冗餘和不便於管理。
項目適用背景:
1.不同項目之間業務邏輯有所關聯並不是完全獨立的項目
比如 水果商店 和 衣服商店 都是商店的東西有業務上的關聯。但是 水果商店 和 在線教育 就不同了,屬於兩種不同的業務邏輯
2.兩個項目主模塊不能同時存在,只能對模塊進行依賴
一、項目概覽
1.項目目錄結構(其中紅框部分爲主項目1,主項目2)
總體結構:
模塊結構(使用Area來進行模塊化):
2.模塊引用(主項目與主項目之間不能互相引用),下圖在 FtCap.mvc.web 入口項目 處引用了 主項目2
3.調試與發佈
我這裏使用的動態編譯,也就是 .cshtml 不會編譯成 dll。具體怎麼設置可以自行百度
調試:直接啓動項目即可調試,不過這裏的調試有個小問題,被引用項目的 .cshtml,無法做到動態編譯,也就是調試狀態中改了.cshtml頁面之後在瀏覽器刷新後無法更新。入口項目是沒有問題的,知道怎麼解決的朋友歡迎在下面留言
發佈:直接在入口項目右鍵發佈即可,發佈後沒有上述的問題
二、框架大致結構圖:
項目初始化:
每個模塊必須創建 ModuleInitializer
類,並繼承 IModuleInitializer
接口。入口利用接口編譯模塊進行初始化,大致步驟如下:
IModuleInitializer 提供兩個方法用來進行初始化
public interface IModuleInitializer { void ConfigureServices(IServiceCollection serviceCollection, IConfiguration configuration); void Configure(IApplicationBuilder app, IWebHostEnvironment env); }
模塊內部初始化示例:
public class ModuleInitializer : IModuleInitializer { public const string AreaName = "SiteShare"; private static readonly string ModuleName = $"FtCap.Module.{AreaName}"; public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { #region 主要模塊基礎配置 //加載配置文件 AppSettings.InitStaticConfig<Configs>(AreaName); //注入動態路由轉換類 //services.AddScoped<SlugRouteValueTransformer>(); services.ConfigureOptions(typeof(CommonConfigureOptions)); //注入數據庫 context if (Configs.DbConnType?.ToUpper() == "MSSQL") { services.AddDbContextPool<ProjectDbContext>(option => { option.UseSqlServer(Configs.DbConnStr); }); } else { services.AddDbContextPool<ProjectDbContext>(option => { option.UseMySql(Configs.DbConnStr); }); } //注入數據庫服務 services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); services.AddTransient(typeof(IRepositoryWithTypedId<,>), typeof(RepositoryWithTypedId<,>)); //加載mongodb鏈接一個主項目只加載一次 MongoConfig.InitMongoDb(AppSettings.CoreSetting.MonogDbConn); #endregion ConstVar.SrcPath = $"_content/{ModuleName}"; } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseMiddleware<ExceptionMiddleware>(); app.UseEndpoints(endpoints => { //endpoints.MapDynamicControllerRoute<SlugRouteValueTransformer>("/{**slug}"); endpoints.MapAreaControllerRoute( name: "default", areaName: ModuleInitializer.AreaName, pattern: "/{controller=Home}/{action=Index}/{id?}" ); }); }
三、項目初始化流程:
上面說明了項目的大概架構,下面講講如何通過入口項目 FtCap.mvc.web 來初始化模塊的
初始化的工作都在 基礎層(FtCap.Infrastructure) 進行的
1.在入口項目創建 modules.json 文件記錄所有模塊使用情況,並方便後面讀取程序集做準備。文件內容大致如下:
id:程序集完整名稱
isBundledWithHost:是否通過入口項目引用(這裏可以直接將外部dll放進bin目錄引用模塊,而不需要通過入口項目添加引用)
version:版本
2.讀取 modules.json 並加載每個模塊
首先,得有一個 模塊類 來接收這個 JSON 數據,以及對應的程序集
直接上代碼:
public class ModuleInfo { public string Id { get; set; } public string Name { get; set; } /// <summary> /// 是否已經在程序集中引用 /// </summary> public bool IsBundledWithHost { get; set; } /// <summary> /// 版本 /// </summary> public Version Version { get; set; } /// <summary> /// 對應程序集 /// </summary> public Assembly Assembly { get; set; } }
然後,獲取各個程序集到集合中(這裏添加了一個 IServiceCollection 的擴展,方便在 setup.cs 中調用):
public static IServiceCollection AddModules(this IServiceCollection services) { foreach (var module in _modulesConfig.GetModules())//GetModules 是讀取JSON文件,並返回對象 { if(!module.IsBundledWithHost) { TryLoadModuleAssembly(module.Id, module); if (module.Assembly == null) { throw new Exception($"Cannot find main assembly for module {module.Id}"); } } else { module.Assembly = Assembly.Load(new AssemblyName(module.Id)); } GlobalConfiguration.Modules.Add(module); } return services; }
最後、我們需要用這裏的模塊信息處理3個地方(1.初始化,也就是調用各個模塊的ModuleInitializer,2.加載各個模塊MVC的控制器和視圖,3. 註冊EF要用到的實體對象 )
1.初始化,很簡單,遍歷集合調用 接口方法就行了:
foreach (var module in GlobalConfiguration.Modules) { var moduleInitializerType = module.Assembly.GetTypes() .FirstOrDefault(t => typeof(IModuleInitializer).IsAssignableFrom(t)); if ((moduleInitializerType != null) && (moduleInitializerType != typeof(IModuleInitializer))) { var moduleInitializer = (IModuleInitializer)Activator.CreateInstance(moduleInitializerType); services.AddSingleton(typeof(IModuleInitializer), moduleInitializer); moduleInitializer.ConfigureServices(services, _configuration); } }
2.加載各個模塊MVC的控制器和視圖,這段代碼比較固定,在網上也有很多參考:
foreach (var module in modules.Where(x => !x.IsBundledWithHost)) { AddApplicationPart(mvcBuilder, module.Assembly); } --------------------- private static void AddApplicationPart(IMvcBuilder mvcBuilder, Assembly assembly) { var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); foreach (var part in partFactory.GetApplicationParts(assembly)) { mvcBuilder.PartManager.ApplicationParts.Add(part); } var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false); foreach (var relatedAssembly in relatedAssemblies) { partFactory = ApplicationPartFactory.GetApplicationPartFactory(relatedAssembly); foreach (var part in partFactory.GetApplicationParts(relatedAssembly)) { mvcBuilder.PartManager.ApplicationParts.Add(part); } } }
3.加載EF實體對象:
這裏說明一下EF model 在設計的時候增加了一個父類 EntityBase,和 IModuleInitializer 作用一樣 並用於區分哪些類需要註冊到 EF 中,下面代碼主要
在 OnModelCreating 中加載的模塊數據
public class ProjectDbContext : IdentityDbContext { public ProjectDbContext(DbContextOptions options) : base(options) { } public override int SaveChanges(bool acceptAllChangesOnSuccess) { ValidateEntities(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { ValidateEntities(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } protected override void OnModelCreating(ModelBuilder modelBuilder) { List<Type> typeToRegisters = new List<Type>(); foreach (var module in GlobalConfiguration.Modules) { typeToRegisters.AddRange(module.Assembly.DefinedTypes.Select(t => t.AsType())); } RegisterEntities(modelBuilder, typeToRegisters); RegisterConvention(modelBuilder); base.OnModelCreating(modelBuilder); RegisterCustomMappings(modelBuilder, typeToRegisters); if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?)); foreach (var property in properties) { modelBuilder .Entity(entityType.Name) .Property(property.Name) .HasConversion(new DateTimeOffsetToBinaryConverter()); } var decimalProperties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(decimal) || p.PropertyType == typeof(decimal?)); foreach (var property in decimalProperties) { modelBuilder .Entity(entityType.Name) .Property(property.Name) .HasConversion<double>(); } } } } private void ValidateEntities() { var modifiedEntries = ChangeTracker.Entries() .Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified)); foreach (var entity in modifiedEntries) { if (entity.Entity is ValidatableObject validatableObject) { var validationResults = validatableObject.Validate(); if (validationResults.Any()) { throw new ValidationException(entity.Entity.GetType(), validationResults); } } } } private static void RegisterConvention(ModelBuilder modelBuilder) { foreach (var entity in modelBuilder.Model.GetEntityTypes()) { if (entity.ClrType.Namespace != null) { var nameParts = entity.ClrType.Namespace.Split('.'); var tableName = entity.ClrType.Name; //string.Concat(nameParts[2], "_", entity.ClrType.Name); modelBuilder.Entity(entity.Name).ToTable(tableName); } } foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys())) { relationship.DeleteBehavior = DeleteBehavior.Restrict; } } private static void RegisterEntities(ModelBuilder modelBuilder, IEnumerable<Type> typeToRegisters) { var entityTypes = typeToRegisters.Where(x => x.GetTypeInfo().IsSubclassOf(typeof(EntityBase)) && !x.GetTypeInfo().IsAbstract); foreach (var type in entityTypes) { modelBuilder.Entity(type); } } private static void RegisterCustomMappings(ModelBuilder modelBuilder, IEnumerable<Type> typeToRegisters) { var customModelBuilderTypes = typeToRegisters.Where(x => typeof(ICustomModelBuilder).IsAssignableFrom(x)); foreach (var builderType in customModelBuilderTypes) { if (builderType != null && builderType != typeof(ICustomModelBuilder)) { var builder = (ICustomModelBuilder)Activator.CreateInstance(builderType); builder.Build(modelBuilder); } } } }
四、配置文件,靜態資源文件管理:
1.配置文件管理:
每個模塊可以使用不同的配置文件,通過將 json 數據 添加到 靜態model中來保存各個模塊的配置
規定 配置文件格式 {areaName}.Config.json 上面 ModuleInitializer 已經列出來了使用方式
module.core.cs
public static void InitStaticConfig<T>(string areaName) { string path = $"{areaName}.Config.json"; var builder = new ConfigurationBuilder(); builder.AddJsonFile(path); builder.Build().Get<T>(); }
module.模塊.cs
//加載配置文件 AppSettings.InitStaticConfig<Configs>(AreaName);
2.靜態資源文件管理:
上篇文章 已經介紹過,關於靜態資源的管理和壓縮
搭建完成後可以進行一系列的優化,例如我這裏使用了 Directory.Build.props 來統一 模塊的引用和一些基礎配置,還可以添加一些自定義 TagHelpers來管理不同的模塊資源
當然你還可以使用.tagets 來添加一些編譯事件。後續會將源碼放到 github 上
關於.net core 插件化框架基本上就搭建完成了。是不是很簡單