GitHub:https://github.com/iamoldli/NetModularhtml
地址:https://nm.iamoldli.com
帳戶:admin
密碼:admin前端
地址:http://nm.demo.iamoldli.com/index.html
帳戶:admin
密碼:admingit
一、開篇
二、快速建立一個業務模塊
三、數據訪問模塊介紹
四、模塊化實現思路github
爲了方便查看源碼,咱們先獲取下官方的源碼數據庫
下載 AspNetCore
源碼json
git clone --recursive https://github.com/aspnet/AspNetCore
下載 Extensions
源碼後端
git clone https://github.com/aspnet/Extensions.git
參考文檔:ASP.NET Core 中的應用程序部件前端框架
在ASP.NET Core
中經過應用程序部件ApplicationPart
來發現控制器、視圖組件或標記幫助程序等 MVC 功能,應用程序部件是由ApplicationPartManager
類來管理。當調用AddMvc
或者AddMvcCore
方法添加MVC相關功能時,ASP.NET Core
內部會建立ApplicationPartManager
的實例,而後以入口程序集爲起點,查找其依賴項樹中的全部非官方包的程序集,並添加到它的ApplicationParts
屬性中,最後將ApplicationPartManager
的實例以單例模式注入到容器中。下面是相關的源碼:mvc
源碼路徑:AspNetCore\src\Mvc.Core\src\DependencyInjection\MvcCoreServiceCollectionExtensions.csapp
public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } var partManager = GetApplicationPartManager(services); //單例模式注入ApplicationPartManager services.TryAddSingleton(partManager); ConfigureDefaultFeatureProviders(partManager); ConfigureDefaultServices(services); AddMvcCoreServices(services); var builder = new MvcCoreBuilder(services, partManager); return builder; } //獲取ApplicationPartManager實例 private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services) { var manager = GetServiceFromCollection<ApplicationPartManager>(services); if (manager == null) { manager = new ApplicationPartManager(); var environment = GetServiceFromCollection<IHostingEnvironment>(services); var entryAssemblyName = environment?.ApplicationName; if (string.IsNullOrEmpty(entryAssemblyName)) { return manager; } manager.PopulateDefaultParts(entryAssemblyName); } return manager; }
源碼路徑:AspNetCore\src\Mvc.Core\src\ApplicationParts\ApplicationPartManager.cs
internal void PopulateDefaultParts(string entryAssemblyName) { var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName)); var assembliesProvider = new ApplicationAssembliesProvider(); //加載入口程序集的依賴項樹中的全部非官方包的依賴程序集 var applicationAssemblies = assembliesProvider.ResolveAssemblies(entryAssembly); foreach (var assembly in applicationAssemblies) { var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); foreach (var part in partFactory.GetApplicationParts(assembly)) { ApplicationParts.Add(part); } } }
由於咱們的全部模塊都是經過nuget包安裝的,因此在編譯時會自動引入到依賴項樹中,也就是說,咱們不須要手動加載模塊中的程序集。
對於在編譯時未引用的程序集,咱們能夠經過應用程序部件來手動加載
// create an assembly part from a class's assembly var assembly = typeof(Startup).GetTypeInfo().Assembly; services.AddMvc() .AddApplicationPart(assembly); // OR var assembly = typeof(Startup).GetTypeInfo().Assembly; var part = new AssemblyPart(assembly); services.AddMvc() .ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
NetModular
的規則是在項目啓動時,查找程序根目錄下的modules
目錄,該目錄專門用於保存全部模塊的信息,它的結構以下:
modules
目錄下的每一個子目錄表示一個模塊,每一個子目錄裏面都有一個module.json
文件,該文件用於描述模塊信息,其結構以下:
{"Id": "Admin","Name":"權限管理","Version":"1.0.0"}
module.json
文件是在模塊編譯的時候自動生成並打包進Nuget包,當安裝模塊時會自動包含在項目中。這裏用到了MSBuild,有興趣的能夠看看。 *如下是生成module.json
文件對應的配置信息
<Project> <PropertyGroup> <ModulesDir>modules\$(Id)</ModulesDir> <ModuleInfo>{"Id": "$(Id)","Name":"$(Name)","Version":"$(Version)"}</ModuleInfo> </PropertyGroup> <ItemGroup> <Content Include="$(ModulesDir)\**"> <Pack>true</Pack> <PackagePath>contentFiles\any\any\$(ModulesDir)</PackagePath> <PackageCopyToOutput>true</PackageCopyToOutput> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <TargetPath>Modules\$(Id)\%(RecursiveDir)%(FileName)%(Extension)</TargetPath> </Content> </ItemGroup> <Target Name="ModulesBuildBefore" AfterTargets="Build"> <!--建立modules目錄--> <MakeDir Directories="$(ModulesDir)"/> <!--生成module.json文件,Note:項目須要生成兩次,不然Nuget包中的文件不是最新的--> <WriteLinesToFile File="$(ModulesDir)\module.json" Overwrite="true" Lines="$(ModuleInfo)" /> </Target> </Project>
NetModular
定義了一個描述模塊信息的ModuleInfo.cs
類和一個保存模塊信息的IModuleCollection.cs
接口
/// <summary> /// 模塊信息 /// </summary> public class ModuleInfo { /// <summary> /// 編號 /// </summary> public string Id { get; set; } /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// 版本 /// </summary> public string Version { get; set; } /// <summary> /// 模塊初始化器 /// </summary> public IModuleInitializer Initializer { get; set; } /// <summary> /// 程序集信息 /// </summary> public ModuleAssembliesInfo AssembliesInfo { get; set; } } /// <summary> /// 模塊集合 /// </summary> public interface IModuleCollection : IList<ModuleInfo> { }
IModuleCollection
有一個實現類ModuleCollection.cs
,在該類的構造函數中執行加載模塊列表的操做:
public ModuleCollection() { var moduleJsonFiles = Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "modules"), "module.json", SearchOption.AllDirectories); foreach (var file in moduleJsonFiles) { var moduleInfo = JsonConvert.DeserializeObject<ModuleInfo>(File.ReadAllText(file)); if (moduleInfo != null) { //判斷是否已存在 if (_moduleInfos.Any(m => m.Name.Equals(moduleInfo.Name))) continue; var assemblyHelper = new AssemblyHelper(); //此處默認模塊命名空間前綴與當前項目相同 moduleInfo.AssembliesInfo = new ModuleAssembliesInfo { Domain = assemblyHelper.Load(m => m.Name.EndsWith("Module." + moduleInfo.Id + ".Domain")).FirstOrDefault(), Infrastructure = assemblyHelper.Load(m => m.Name.EndsWith("Module." + moduleInfo.Id + ".Infrastructure")).FirstOrDefault(), Application = assemblyHelper.Load(m => m.Name.EndsWith("Module." + moduleInfo.Id + ".Application")).FirstOrDefault(), Web = assemblyHelper.Load(m => m.Name.EndsWith("Module." + moduleInfo.Id + ".Web")).FirstOrDefault(), }; Check.NotNull(moduleInfo.AssembliesInfo.Domain, moduleInfo.Id + "模塊的Domain程序集未發現"); Check.NotNull(moduleInfo.AssembliesInfo.Infrastructure, moduleInfo.Id + "模塊的Infrastructure程序集未發現"); Check.NotNull(moduleInfo.AssembliesInfo.Application, moduleInfo.Id + "模塊的Application程序集未發現"); Check.NotNull(moduleInfo.AssembliesInfo.Web, moduleInfo.Id + "模塊的Web程序集未發現"); //加載模塊初始化器 var moduleInitializerType = moduleInfo.AssembliesInfo.Web.GetTypes().FirstOrDefault(t => typeof(IModuleInitializer).IsAssignableFrom(t)); if (moduleInitializerType != null && (moduleInitializerType != typeof(IModuleInitializer))) { moduleInfo.Initializer = (IModuleInitializer)Activator.CreateInstance(moduleInitializerType); } Add(moduleInfo); } } }
當項目啓動時,首先建立ModuleCollection
的實例,在它的構造函數中會加載全部模塊信息,而後使用單例模式注入,這樣就能夠在系統中隨時取用模塊信息了。
/// <summary> /// 添加模塊 /// </summary> /// <param name="services"></param> /// <param name="env"></param> /// <returns></returns> public static IModuleCollection AddModules(this IServiceCollection services, IHostingEnvironment env) { //建立模塊集合對象 var modules = new ModuleCollection(); services.AddSingleton<IModuleCollection>(modules); var cfgHelper = new ConfigurationHelper(); var cfg = cfgHelper.Load("module", env.EnvironmentName, true); //通用配置 services.Configure<ModuleCommonOptions>(cfg); foreach (var module in modules) { if (module == null) continue; services.AddApplicationServices(module); if (module.Initializer != null) { module.Initializer.ConfigureServices(services); module.Initializer.ConfigOptions(services, cfg.GetSection(module.Id)); services.AddSingleton(module); } } return modules; }
先看一下一個模塊中包含哪些信息:
模塊中的注入分爲兩類:
一、約定的
每一個模塊中都有配置項(Options)、實體(Entity)、倉儲(Repository)、數據庫上下文(DbContext)、工做單元(UnitOfWork)、服務(Service),他們都是約定好的,包括命名、目錄、用法等,因此使用者只須要按照規則去使用便可,不須要關心注入的事情,它們在系統中是自動注入的。
以數據訪問爲例,數據訪問相關的倉儲(Repository)、數據庫上下文(DbContext)、工做單元(UnitOfWork)是根據配置信息和模塊來自動進行注入的,同時都是以Scoped
方式注入。具體代碼查看Data.AspNetCore
項目。
二、自定義的
每一個模塊均可能會有一些獨有的須要注入的服務,那麼這些服務是屬於自定義的,須要開發者本身手動注入。好比
權限管理(Admin)
模塊中的權限驗證處理(PermissionValidateHandler.cs)
,該類實現IPermissionValidateHandler
接口,專門用於作權限驗證功能。
除了注入之外,每一個模塊還有獨有的中間件以及對某些功能的特殊配置,爲了把這些信息一塊兒集成到項目中,NetModular
抽象了一個IModuleInitializer
接口,該接口包括如下四個方法:
/// <summary> /// 模塊初始化器接口 /// </summary> public interface IModuleInitializer { /// <summary> /// 注入服務 /// </summary> /// <param name="services"></param> void ConfigureServices(IServiceCollection services); /// <summary> /// 配置中間件 /// </summary> /// <param name="app"></param> /// <param name="env"></param> void Configure(IApplicationBuilder app, IHostingEnvironment env); /// <summary> /// 配置MVC /// </summary> /// <param name="mvcOptions"></param> void ConfigureMvc(MvcOptions mvcOptions); /// <summary> /// 配置選項 /// </summary> /// <param name="services"></param> /// <param name="configuration"></param> void ConfigOptions(IServiceCollection services, IConfiguration configuration); }
方法說明:
一、ConfigureServices:用於注入服務
二、Configure:用於配置中間件
三、ConfigureMvc:用於配置MVC相關功能
四、ConfigOptions:用於配置模塊的配置項
在每一個模塊中,都必須包含一個IModuleInitializer
的實現ModuleInitializer
,已權限管理(Admin)
模塊爲例:
public class ModuleInitializer : IModuleInitializer { public void ConfigureServices(IServiceCollection services) { //權限驗證服務 services.AddScoped<IPermissionValidateHandler, PermissionValidateHandler>(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { } public void ConfigureMvc(MvcOptions mvcOptions) { // 審計日誌過濾器 mvcOptions.Filters.Add(typeof(AuditingFilter)); } public void ConfigOptions(IServiceCollection services, IConfiguration configuration) { // Admin配置項 services.Configure<AdminOptions>(configuration); } }
當系統在啓動的時候,會在指定的步驟,調用全部模塊的對應方法,好比當調用service.AddModules
方法時,會遍歷模塊並注入自定義服務和配置項,
public static IModuleCollection AddModules(this IServiceCollection services, IHostingEnvironment env) { var modules = new ModuleCollection(); services.AddSingleton<IModuleCollection>(modules); var cfgHelper = new ConfigurationHelper(); var cfg = cfgHelper.Load("module", env.EnvironmentName, true); services.Configure<ModuleCommonOptions>(cfg); // 遍歷模塊 foreach (var module in modules) { if (module == null) continue; services.AddApplicationServices(module); // 判斷IModuleInitializer實現是否存在 if (module.Initializer != null) { // 注入服務 module.Initializer.ConfigureServices(services); //配置配置項 module.Initializer.ConfigOptions(services, cfg.GetSection(module.Id)); services.AddSingleton(module); } } return modules; }
至此,模塊的全部信息都已集成到了系統當中~