ASP.NET Core模塊化先後端分離快速開發框架介紹之四、模塊化實現思路

源碼

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 中的應用程序部件前端框架

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

模塊中的依賴注入和中間件處理

先看一下一個模塊中包含哪些信息:

結構圖

模塊中的注入分爲兩類:

一、約定的

每一個模塊中都有配置項(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;
}

至此,模塊的全部信息都已集成到了系統當中~

原文首發:ASP.NET Core模塊化先後端分離快速開發框架介紹之四、模塊化實現思路

相關文章
相關標籤/搜索