源碼
GitHub:https://github.com/iamoldli/NetModular
演示地址
地址:https://nm.iamoldli.com
賬戶:admin
密碼:admin
前端框架演示地址(臨時)
地址:http://nm.demo.iamoldli.com/index.html
賬戶:admin
密碼:admin
目錄
1、開篇
2、快速創建一個業務模塊
3、數據訪問模塊介紹
4、模塊化實現思路
獲取官方源碼
為了方便查看源碼,我們先獲取下官方的源碼
下載 AspNetCore
源碼
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
的實例以單例模式注入到容器中。下面是相關的源碼:
源碼路徑:AspNetCore\src\Mvc.Core\src\DependencyInjection\MvcCoreServiceCollectionExtensions.cs
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"}
- Note:
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;
}
模塊中的依賴注入和中間件處理
先看一下一個模塊中包含哪些信息:
模塊中的注入分為兩類:
1、約定的
每個模塊中都有配置項(Options)、實體(Entity)、倉儲(Repository)、數據庫上下文(DbContext)、工作單元(UnitOfWork)、服務(Service),他們都是約定好的,包括命名、目錄、用法等,所以使用者只需要按照規則去使用即可,不需要關心注入的事情,它們在系統中是自動注入的。
以數據訪問為例,數據訪問相關的倉儲(Repository)、數據庫上下文(DbContext)、工作單元(UnitOfWork)是根據配置信息和模塊來自動進行注入的,同時都是以Scoped
方式注入。具體代碼查看Data.AspNetCore
項目。
2、自定義的
每個模塊都可能會有一些獨有的需要注入的服務,那么這些服務是屬於自定義的,需要開發者自己手動注入。比如
權限管理(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);
}
方法說明:
1、ConfigureServices:用於注入服務
2、Configure:用於配置中間件
3、ConfigureMvc:用於配置MVC相關功能
4、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;
}
至此,模塊的所有信息都已集成到了系統當中~