1. 程式人生 > 實用技巧 >.netcore MVC模組化開發框架搭建基礎

.netcore MVC模組化開發框架搭建基礎

環境:

  .net core 3.1

  MSSSQL , MYSQL

  MVC

  EFCore

  AutoFac

前言:

  不同的框架主要解決開發中出現的不同的問題,本框架主要解決多個專案在開發過程中多個模組的重複使用造成冗餘和不便於管理。

專案適用背景:

  1.不同專案之間業務邏輯有所關聯並不是完全獨立的專案

    比如 水果商店 和 衣服商店 都是商店的東西有業務上的關聯。但是 水果商店 和 線上教育 就不同了,屬於兩種不同的業務邏輯

   2.兩個專案主模組不能同時存在,只能對模組進行依賴

 一、專案概覽

  1.專案目錄結構(其中紅框部分為主專案1,主專案2)

    

  2.模組引用(主專案與主專案之間不能互相引用),下圖在FtCap.mvc.web 入口專案 處引用了 主專案2

  

  3.除錯與釋出

  我這裡使用的動態編譯,也就是 .cshtml 不會編譯成 dll。具體怎麼設定可以自行百度

  除錯:直接啟動專案即可除錯,不過這裡的除錯有個小問題,被引用專案的 .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 來統一 模組的引用和一些基礎配置,

當然你還可以使用.tagets 來新增一些編譯事件。後續會將原始碼放到 github 上

關於.net core 外掛化框架基本上就搭建完成了。