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; }
至此,模块的全部信息都已集成到了系统当中~