1. 程式人生 > >分享一個基於Net Core 3.1開發的模組化的專案

分享一個基於Net Core 3.1開發的模組化的專案

先簡單介紹下專案(由於重新基於模組化設計了整個專案,所以目前整個專案功能不多)

1.Asp.Net Core 3.1.2+MSSQL2019(LINUX版)

2.中介軟體涉及Redis、RabbitMQ等

3.完全模組化的設計,支援每個模組有獨立的靜態資原始檔

github開源地址:

https://github.com/yupingyong/mango

上一張專案結構圖:

 

 

 上圖中 Modules目錄下放的專案的模組

Mango.WebHost 承載整個專案執行

Mango.Framework 封裝整個專案模組化核心

下面我會分享實現模組化的幾個核心要點,更詳細的我會在後續的博文中陸續釋出.

框架如何去載入所寫的模組這是最核心的問題之一,好在Asp.Net Core MVC為模組化提供了一個部件管理類

Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager

 它支援從外部DLL程式集載入元件以及元件的管理.不過要從外部元件去獲取哪些是元件我們需要藉助一個工廠類ApplicationPartFactory,這個類支援從外部程式集得到對應的控制器資訊,核心程式碼如下:

 1         /// <summary>
 2         /// 向MVC模組新增外部應用模組元件
 3         /// </summary>
 4         /// <param name="mvcBuilder"></param>
 5         /// <param name="assembly"></param>
 6         private static void AddApplicationPart(IMvcBuilder mvcBuilder, Assembly assembly)
 7         {
 8             var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly);
 9             foreach (var part in partFactory.GetApplicationParts(assembly))
10             {
11                 mvcBuilder.PartManager.ApplicationParts.Add(part);
12             }
13             
14             var relatedAssemblies = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false);
15             foreach (var relatedAssembly in relatedAssemblies)
16             {
17                 partFactory = ApplicationPartFactory.GetApplicationPartFactory(relatedAssembly);
18                 foreach (var part in partFactory.GetApplicationParts(relatedAssembly))
19                 {
20                     mvcBuilder.PartManager.ApplicationParts.Add(part);
21                     mvcBuilder.PartManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(relatedAssembly));
22                 }
23             }
24         }

上面的程式碼展示瞭如何載入控制器資訊,但是檢視檔案在專案生成的時候是單獨的*.Views.dll檔案,我們接下來介紹如何載入檢視檔案,同樣還是用到了ApplicationPartManager類

mvcBuilder.PartManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(module.ViewsAssembly));

new一個CompiledRazorAssemblyPart物件表示新增進去的是檢視編譯檔案,完整的核心程式碼

 1         /// <summary>
 2         /// 新增MVC元件
 3         /// </summary>
 4         /// <param name="services"></param>
 5         /// <returns></returns>
 6         public static IServiceCollection AddCustomizedMvc(this IServiceCollection services)
 7         {
 8             services.AddSession();
 9 
10             var mvcBuilder = services.AddControllersWithViews(options=> {
11                 //新增訪問授權過濾器
12                 options.Filters.Add(new AuthorizationComponentFilter());
13             })
14                 .AddJsonOptions(options=> {
15                     options.JsonSerializerOptions.Converters.Add(new DateTimeToStringConverter());
16                 });    
17             foreach (var module in GlobalConfiguration.Modules)
18             {
19                 if (module.IsApplicationPart)
20                 {
21                     if (module.Assembly != null)
22                         AddApplicationPart(mvcBuilder, module.Assembly);
23                     if (module.ViewsAssembly != null)
24                         mvcBuilder.PartManager.ApplicationParts.Add(new CompiledRazorAssemblyPart(module.ViewsAssembly));
25                 }
26             }
27             return services;
28         }

那如何去載入程式集呢?這裡我使用了自定義的ModuleAssemblyLoadContext去載入程式集,這個類繼承自AssemblyLoadContext(它支援解除安裝載入過的程式集,但是部件新增到MVC中時,好像不支援動態解除安裝會出現異常,可能是我還沒研究透吧)

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Threading.Tasks;
 5 using System.Reflection;
 6 using System.Runtime.CompilerServices;
 7 using System.Runtime.Loader;
 8 
 9 namespace Mango.Framework.Module
10 {
11     public class ModuleAssemblyLoadContext : AssemblyLoadContext
12     {
13         public ModuleAssemblyLoadContext() : base(true)
14         {
15         }
16     }
17 }

在使用ModuleAssemblyLoadContext類載入程式集時,先使用FileStream把程式集檔案讀取出來(這樣能夠避免檔案一直被佔用,方便開發中編譯模組時報檔案被佔用的異常),載入檔案路徑時需要注意的問題一定要使用/(\在windows server下沒問題,但是如果在linux下部署就會出現問題),程式碼如下:

 1         /// <summary>
 2         /// 新增模組
 3         /// </summary>
 4         /// <param name="services"></param>
 5         /// <param name="contentRootPath"></param>
 6         /// <returns></returns>
 7         public static IServiceCollection AddModules(this IServiceCollection services, string contentRootPath)
 8         {
 9             try
10             {
11                 GlobalConfiguration.Modules = _moduleConfigurationManager.GetModules();
12                 ModuleAssemblyLoadContext context = new ModuleAssemblyLoadContext();
13                 foreach (var module in GlobalConfiguration.Modules)
14                 {
15                     var dllFilePath = Path.Combine(contentRootPath, $@"Modules/{module.Id}/{module.Id}.dll");
16                     var moduleFolder = new DirectoryInfo(dllFilePath);
17                     if (File.Exists(moduleFolder.FullName))
18                     {
19                         using FileStream fs = new FileStream(moduleFolder.FullName, FileMode.Open);
20                         module.Assembly = context.LoadFromStream(fs);
21                         //
22                         RegisterModuleInitializerServices(module, ref services);
23                     }
24                     else
25                     {
26                         _logger.Warn($"{dllFilePath} file is not find!");
27                     }
28                     //處理檢視檔案程式集載入
29                     var viewsFilePath = Path.Combine(contentRootPath, $@"Modules/{module.Id}/{module.Id}.Views.dll");
30                     moduleFolder = new DirectoryInfo(viewsFilePath);
31                     if (File.Exists(moduleFolder.FullName))
32                     {
33                         using FileStream viewsFileStream = new FileStream(moduleFolder.FullName, FileMode.Open);
34                         module.ViewsAssembly = context.LoadFromStream(viewsFileStream);
35                     }
36                     else
37                     {
38                         _logger.Warn($"{viewsFilePath} file is not find!");
39                     }
40                 }
41             }
42             catch (Exception ex)
43             {
44                 _logger.Error(ex);
45             }
46             return services;
47         }

上面簡單介紹瞭如何利用MVC自帶的部件管理類去載入外部程式集,這裡需要說明的一點的是每個模組我們採用建立區域的方式去區分模組,如下圖展示的賬號模組結構

 

 

 基於模組化開發我們可能碰到一個比較常見的需求就是,如果每個模組需要擁有自己獨立的靜態資原始檔呢?這種情況如何去解決呢?

好在MVC框架也提供了一個靜態資源配置方法UseStaticFiles,我們在Configure方法中啟用靜態資源元件時,可以自定義設定靜態檔案訪問的路徑,設定程式碼如下

 1             //設定每個模組約定的靜態檔案目錄
 2             foreach (var module in GlobalConfiguration.Modules)
 3             {
 4                 if (module.IsApplicationPart)
 5                 {
 6                     var modulePath = $"{env.ContentRootPath}/Modules/{module.Id}/wwwroot";
 7                     if (Directory.Exists(modulePath))
 8                     {
 9                         app.UseStaticFiles(new StaticFileOptions()
10                         {
11                             FileProvider = new PhysicalFileProvider(modulePath),
12                             RequestPath = new PathString($"/{module.Name}")
13                         });
14                     }
15                 }
16             }

上述程式碼片段中我們能夠看到通過new StaticFileOptions()新增配置項, StaticFileOptions中有兩個重要的屬性,只需要配置好這兩個就能滿足基本需求了

FileProvider:該屬性表示檔案的實際所在目錄(如:{env.ContentRootPath}/Modules/Mango.Module.Account/wwwroot)

RequestPath:該屬性表示檔案的請求路徑(如 /account/test.js 這樣訪問到就是 {env.ContentRootPath}/Modules/Mango.Module.Account/wwwroot下的test.js檔案)

這篇博文我就暫時只做一個模組化開發實現的核心程式碼展示和說明,更具體的只能在接下來的博文中展示了.

其實我也開發了一個前後分離的,只剩下鑑權,實現的核心和上面所寫的一樣,這裡我就只把開源地址分享出來,我後面還是會用業餘時間來繼續完成它

https://github.com/yupingyong/mango-open

該專案我已經在linux 上使用docker容器部署了,具體地址我就不釋出了(避免打廣告的嫌疑,我截幾張效果圖)

 

 

 

 

 

 結語:這個專案我會一個更新下去,接下去這個框架會向DDD發展.

因為喜歡.net 技術棧,所以願意在開發社群分享我的知識成果,也想向社群的人學習更好的編碼風格,更高一層程式設計技術.