ASP.NET Core模块化前后端分离快速开发框架介绍之4、模块化实现思路

源码

GitHub:https://github.com/iamoldli/NetModular

演示地址

地址:http://129.211.40.240:6220
账户:admin
密码:admin

前端框架演示地址(临时)

地址:http://progqx5cu.bkt.clouddn.com/skins/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 中的应用程序部件

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;
}

至此,模块的所有信息都已集成到了系统当中~

原文首发:ASP.NET Core模块化前后端分离快速开发框架介绍之4、模块化实现思路

原文地址:https://www.cnblogs.com/oldli/p/10945137.html

时间: 2024-10-27 08:16:49

ASP.NET Core模块化前后端分离快速开发框架介绍之4、模块化实现思路的相关文章

.Net Core+Vue.js模块化前后端分离快速开发框架NetModular更新日志(2019-12-08)

源码 GitHub:https://github.com/iamoldli/NetModular 码云:https://gitee.com/laoli/NetModular 欢迎star~ 文档 https://nm.iamoldli.com/docs/ 演示地址 Demo:https://nm.iamoldli.com 账户:admin 密码:admin 后端 1.升级到 3.1 版本,线上Demo已更新 2.模块初始化器接口 IModuleInitializer 的 ConfigureSer

.NET Core前后端分离快速开发框架(Core.3.0+AntdVue)

.NET Core前后端分离快速开发框架(Core.3.0+AntdVue) 目录 引言 简介 环境搭建 开发环境要求 基础数据库构建 数据库设计规范 运行 使用教程 全局配置 快速开发 管理员登录 系统用户管理 系统角色管理 权限管理 接口秘钥管理 系统日志 单库事务 跨库事务 读写分离分库分表 常见疑问 如何进行联表查询 如何切换数据库类型 如何使用多个数据库 引言 时间真快,转眼今年又要过去了.回想今年,依次开源发布了Colder.Fx.Net.AdminLTE(254Star).Cold

ASP.NET WebApi+Vue前后端分离之允许启用跨域请求

前言: 这段时间接手了一个新需求,将一个ASP.NET MVC项目改成前后端分离项目.前端使用Vue,后端则是使用ASP.NET WebApi.在搭建完成前后端框架后,进行接口测试时发现了一个前后端分离普遍存在的问题跨域(CORS)请求问题.因此就有了这篇文章如何启用ASP.NET WebApi 中的 CORS 支持. 一.解决Vue报错:OPTIONS 405 Method Not Allowed问题: 错误重现: index.umd.min.js:1 OPTIONS http://local

基于Electron+.NET Core的前后端分离的跨平台桌面应用

Web做界面比原生桌面界面开发速度真心要快很多,而且组件也多. 分析: 1..NET Core和Electron都是跨平台的. 2.NET Core做后端很方便,但是没有GUI,Electron做桌面应用很方便,但是存在Node.js单线程跑CPU密集计算的弱点. 灵感: 那Electron启动后,调用NET Core程序运行在后台,作为本地后端,CPU密集计算直接扔给后端跑不就得了? 甚至于说,能不能Electron只负责展示层,NET Core负责所有的业务逻辑? 已知: NET Core通

从一张图开始,谈一谈.NET Core和前后端技术的演进之路

从一张图开始,谈一谈.NET Core和前后端技术的演进之路 邹溪源,李文强,来自长沙.NET技术社区 一张图 2019年3月10日,在长沙.NET 技术社区组织的技术沙龙<.NET Core和前后端分离那些事儿>上,我们曾经试图通过一系列抽丝剥茧的过程来引导大家在这条基于.NET Core的前后端分离有关的技术路线上持续发散,由于各种原因未能成功,因此,技术社区执行主席,李文强同学整理了一张图,并由溪源以专刊的形式来描述他所认为的技术演进之路.  这张图涉及到的知识点,涵盖了目前前后端技术近

[原创]基于VueJs的前后端分离框架搭建之完全攻略

首先请原谅本文标题取的有点大,但并非为了哗众取宠.本文取这个标题主要有3个原因,这也是写作本文的初衷: (1)目前国内几乎搜索不到全面讲解如何搭建前后端分离框架的文章,讲前后端分离框架思想的就更少了,而笔者希望在本文中能够全面.详细地阐述我们团队在前后端分离的摸索中所得到的搭建思路.最佳实践以及架构思想: (2)我们团队所搭建的前后端分离框架,并非只是将网上传播的知识碎片简单拼装,而是一开始就从全局出发,按照整个系统对前后端分离框架的最高期望进行设计,到目前为止,可以说我们的框架完全实现了对我们

ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目

 一.前言 这几年前端的发展速度就像坐上了火箭,各种的框架一个接一个的出现,需要学习的东西越来越多,分工也越来越细,作为一个 .NET Web 程序猿,多了解了解行业的发展,让自己扩展出新的技能树,对自己的职业发展还是很有帮助的.毕竟,现在都快到9102年了,如果你还是只会 Web Form,或许还是能找到很多的工作机会,可是,这真的不再适应未来的发展了.如果你准备继续在 .NET 平台下进行开发,适时开始拥抱开源,拥抱 ASP.NET Core,即使,现在工作中可能用不到. 雪崩发生时,没有一

分享一个前后端分离方案源码-前端angularjs+requirejs+dhtmlx 后端asp.net webapi

一.前言 半年前左右折腾了一个前后端分离的架子,这几天才想起来翻出来分享给大家.关于前后端分离这个话题大家也谈了很久了,希望我这个实践能对大家有点点帮助,演示和源码都贴在后面. 二.技术架构 这两年angularjs和reactjs算是比较火的项目了,而我选择angularjs并不是因为它火,而是因它的模块化.双向数据绑定.注入.指令等都是非常适合架构较复杂的前端应用,而且文档是相当的全,碰到问题基本上可以在网上都找到答案.所以前端基本思路就以angularjs为主.代码模块化,通过requir

前后端分离和模块化

前后端分离和模块化-58到家微信首页重构之路 微信钱包内的58到家全新首页已经上线,感兴趣的同学们可以在微信中打开“我的->钱包->58到家”查看. 58到家全新首页提出重构主要是为了解决以下问题: 每个城市开通的服务项目不同,有些内容是写死在tpl中,维护非常头疼: 开通新服务或者某些UI调整(比如更换服务项的图片造成更改雪碧图)时必须走代码上线流程: 原有的前端切图.后端写逻辑的开发模式造成开发周期拉长和上线流程繁琐: 原有配置后台操作复杂,且可配置细节不完善: 首页加载速度太慢,用户体验