1. 程式人生 > >ASP.NETCORE MVC模組化

ASP.NETCORE MVC模組化

ASP.NETCORE MVC模組化程式設計

前言

記得上一篇部落格中跟大家分享的是基於ASP.NETMVC5,實際也就是基於NETFRAMEWORK平臺實現的這麼一個輕量級外掛式框架。那麼今天我主要分享的是自己工作中參考三方主流開源WEB框架OrchardCore、NopCore等,實現的另外一個輕量級模組化WEB框架,當然這個框架就是基於當下微軟力推和開源社群比較火爆的基礎平臺ASPNETCORE。
進入正題之前,我覺得先有必要簡單介紹一下ASPNETCORE這個平臺大家比較關心的幾個指標。
其一效能,話不多說直接看個人覺得比較權威的效能測試網站https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=fortune,微軟官方給出的資料效能是ASPNET的23倍。

其二生態,從NETCORE2.0開始,國內越來越多的大型網際網路公司開始支援,比如百度雲SDK、騰訊雲SDK、騰訊的Tars 微服務平臺、攜程、阿里雲等等。我們可以看看相關的issue,以百度云為例 https://github.com/Baidu-AIP/dotnet-sdk/issues/3。
其三遷移,自NETCORE2.0開始,有越來越多的三方nuget包支援。
其四開源,使用的是MIT和Apache 2開源協議,文件協議遵循CC-BY。這就意味著任何人任何組織和企業任意處置,包括使用,複製,修改,合併,發表,分發,再授權,或者銷售。唯一的限制是,軟體中必須包含上述版 權和許可提示,後者協議將會除了為使用者提供版權許可之外,還有專利許可,並且授權是免費,無排他性的(任何個人和企業都能獲得授權)並且永久不可撤銷,相較於oracle對java和mysql的開源協議微軟做出了最大的誠意。
其五跨平臺,這也是真正意義上的跨平臺,徹底摒棄了.NET Framework這種提取目標框架API交集方式的PCL。.NETCORE微軟全新設計了針對各平臺CoreCLR執行時和統一的PCL.NET Standard。
最後算是個人的一點點小建議,更新速度可以適當的慢一點,分一部分時間多關注一下這個生態圈。打個比方,在這個文明年代,你一個人會降龍十八掌,你會牛逼到沒朋友,沒有人敢跟你玩。

框架介紹

該框架採用的是ASPNETCORE2.2的版本,實現了日誌管理、許可權管理、模組管理、多語言、多主題、自動化任務管理等等功能。下面貼一張簡單的動態圖看看效果。

本人用的是vs2019,目前好像最高是預覽版,建議大家就當前版本來說,正式開發工作還是要慎用,穩定性比較差。還是老套路,我可能只會抽取框架裡面1-2個重要的模組實現加以詳細介紹。顧及可能有些朋友接觸ASPNETCORE時間不長,同時我也會針對框架裡面使用的某些基礎技術點做詳細介紹,比如DI容器、路由、中介軟體、檢視View等。這篇部落格主要是介紹模組化框架的具體實現,思路方面可以參考我的上一篇文章。先上圖解決方案目錄結構


整個工程主要分三大模組,Infrastructure顧名思義就是整個專案的基礎功能和實現。Modules為專案所有子模組,根據業務劃分的相關模組。UI裡面包含了ASPNETCOREMVC的基礎擴充套件和佈局。
可能有些朋友會問,為什麼Modules目錄下面的模組工程有對應的Abstractions工程對應?不要誤解不是所有都是一一對應。我們在閱讀NETCORE和OrchardCore原始碼的時候也經常會看到有對應的Abstractions工程,主要是針對基礎模組更高層次的抽象。下面直接解讀程式碼實現。

模組化實現

我們先看看框架入口,Program.cs檔案的main函式,看程式碼

1 public static void Main(string[] args)
2         {
3             var host = WebHost.CreateDefaultBuilder(args)
4                 .UseKestrel()
5                 .UseStartup<Startup>()
6                 .Build();
7 
8             host.Run();
9         }

 

題外話,我們以往在使用ASPNETMVC或者說ASPNETWEBFOREMS的時候,有看到或者定義過main函式嗎?沒有。因為它們的初始化工作由非託管的aspnet_isapi完成,aspnet_isapi是IIS的組成部分,通過COM級別的Class呼叫,並且aspnet_isapi並非是面向使用者程式設計的api介面,所以早期版本的ASPNET耦合了WebServer容器IIS。
程式碼不多,就簡單的幾行程式碼,完成了整個ASPNETCOREMVC基礎框架和應用框架所需要的功能模組的初始化工作,並且啟動KestrelServer的監聽。整個WebHostBuilder通過標準的建造者模式實現,由於Startup是我們框架程式的入口,下面我們重點看看UseStartup方法和Startup物件。我們先來看看ASPNETCOREMVC原始碼裡面的UseStarup的定義。

 1 public static class WebHostBuilderExtensions
 2     {
 3         // 其他程式碼...
 4         public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
 5         {
 6             //其他程式碼...
 7             return hostBuilder
 8                 .ConfigureServices(services =>
 9                 {
10                     // 實現IStartup介面
11                     if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo()))
12                     {
13                         services.AddSingleton(typeof(IStartup), startupType);
14                     }
15                     else
16                     {
17                         // 常規方式
18                         services.AddSingleton(typeof(IStartup), sp =>
19                         {
20                             var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
21                             return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName));
22                         });
23                     }
24                 });
25         }
26     }

 

從UseStartup方法的定義,我們瞭解到,ASPNETCore並沒有採用介面實現的方式為啟動型別做強制性的約束,而僅僅是作為啟動型別的定義提供了一個約定而已。通常我們在定義中介軟體和服務註冊類Startup時,直接將其命名為Startup,並未實現IStartup介面。所以我們這裡採用的是常規方式來定義和建立Startup。建立Startup物件是由ConventionBasedStartup完成,下面我們看看ConventionBasedStartup型別的定義。

 1 // ConventionBasedStartup
 2 public class ConventionBasedStartup : IStartup
 3     {  
 4         public ConventionBasedStartup(StartupMethods methods);
 5         
 6         public void Configure(IApplicationBuilder app);
 7 
 8         public IServiceProvider ConfigureServices(IServiceCollection services);
 9     }
10     // StartupMethods
11     public class StartupMethods
12     {
13         public StartupMethods(object instance, Action<IApplicationBuilder> configure, Func<IServiceCollection, IServiceProvider> configureServices);
14 
15         public object StartupInstance { get; }
16         public Func<IServiceCollection, IServiceProvider> ConfigureServicesDelegate { get; }
17         public Action<IApplicationBuilder> ConfigureDelegate { get; }
18 
19     }

 

從ConventionBasedStartup的構造器來看,ConventionBasedStartup的建立是由StartupMethods物件來建立的,那麼我們現在很有必要知道StartupMethods物件的建立。通過UseStartup的實現,我們知道StartupMethods的建立者是一個型別為StartupLoader的物件。

 1 public class StartupLoader
 2     {
 3         // 其他成員...
 4         public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
 5         {
 6             var configureMethod = FindConfigureDelegate(startupType, environmentName);
 7 
 8             var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
 9             
10             // 其他程式碼...
11 
12             var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance(
13                 typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type),
14                 hostingServiceProvider,
15                 servicesMethod,
16                 configureContainerMethod,
17                 instance);
18 
19             return new StartupMethods(instance, configureMethod.Build(instance), builder.Build());
20         }
21     }

 

從以上程式碼片段可以看出,LoadMethods建立了StartupMethods,也就是我們自定義的Starpup物件。一下有幾個地方需要注意,1.對於Startup的建立我們只是使用了諸多方法中的其中一種,呼叫UseStartup方法。當然ASPNETCORE具有多種方法建立Startup物件。2.Startup型別的命名約定,可攜帶環境名稱environment,環境名稱可在UseSetting裡面指定,當然我們一般採用顯式的方式呼叫UseStartup方法。3.Startup型別用於註冊服務和中介軟體的這兩個方法約定,可以靜態也可非靜態,同時可攜帶環境名稱。引數約定,只有Configure強制第一個引數為IApplicationBuilder。以上注意點有興趣的朋友可以自行去研究原始碼,下面我們看看我們自定義的Startup物件。

 1 public class Startup
 2     {
 3         private readonly IConfiguration _configuration;
 4         private readonly IHostingEnvironment _hostingEnvironment;
 5 
 6         public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
 7         {
 8             _configuration = configuration;
 9             _hostingEnvironment = hostingEnvironment;
10         }
11         // 註冊服務
12         public IServiceProvider ConfigureServices(IServiceCollection services)
13         {
14             return services.AddApplicationServices(_configuration, _hostingEnvironment);
15         }
16         // 註冊中介軟體
17         public void Configure(IApplicationBuilder application)
18         {
19             application.AddApplicationPipeline();
20         }
21     }

 

對於Startup物件裡面的兩個方法我個人的理解是,一個生產一個消費。ConfigureServices負責建立服務,Configure負責建立中介軟體管道並且消費ConfigureServices裡面註冊的服務。下面我們繼續看看這兩個方法的執行時機。

 1 public IWebHost Build()
 2         {
 3             // 其他程式碼
 4             var host = new WebHost(
 5                 applicationServices,
 6                 hostingServiceProvider,
 7                 _options,
 8                 _config,
 9                 hostingStartupErrors);
10             try
11             {
12                 host.Initialize(); // 
13                 return host;
14             }
15             catch
16             {
17                 host.Dispose();
18                 throw;
19             }
20         }
21         
22         private void EnsureApplicationServices()
23         {
24             if (_applicationServices == null)
25             {
26                 EnsureStartup();
27                 _applicationServices = _startup.ConfigureServices(_applicationServiceCollection); // 執行ConfigureServices方法
28             }
29         }

 

Build()就是我們定義在main函式裡面的Build方法,通過以上程式碼片段,我們可以看出Startup裡面的ConfigureServices方法是在Build方法裡面完成。我們繼續看看Configure方法的執行。

 1 private RequestDelegate BuildApplication()
 2         {
 3             try
 4             {
 5                 Action<IApplicationBuilder> configure = _startup.Configure;
 6                 
 7                 // 執行startup configure
 8                 configure(builder);
 9 
10                 return builder.Build();
11             }
12         }

 

BuildApplication()方法是在main函式裡面的run函式間接呼叫的。到此對於Startup型別涉及的一些問題已經全部講完,希望大家不要覺得囉嗦。下面我們繼續往下看模組的實現。

 1 public static class ServiceCollectionExtensions
 2     {
 3         // 其他成員...
 4         public static IServiceProvider AddApplicationServices(this IServiceCollection services,
 5             IConfiguration configuration, IHostingEnvironment hostingEnvironment)
 6         {
 7             // 其他程式碼...
 8             var mvcCoreBuilder = services.AddMvcCore();
 9             // 初始化模組及安裝
10             mvcCoreBuilder.PartManager.InitializeModules();
11             return serviceProvider;
12         }  
13   }

 

在Startup的ConfigureServices裡面我們通過IServiceCollection(ASPNETCORE內建的DI容器,後續我會詳細介紹其原理)的擴充套件方法初始化了模組Modules以及對Modules的安裝。在介紹Modules具體實現之前,我覺得有必要先介紹ASPNETCORE裡面的ApplicationPartManager物件,因為我們的模組Modules的實現就是基於這個物件實現的。下面我們看看ApplicationPartManager物件的定義。

 1 public class ApplicationPartManager
 2     {
 3         public IList<IApplicationFeatureProvider> FeatureProviders { get; } =
 4             new List<IApplicationFeatureProvider>();
 5 
 6         public IList<ApplicationPart> ApplicationParts { get; } = new List<ApplicationPart>();
 7         // 載入Feature
 8         public void PopulateFeature<TFeature>(TFeature feature);
 9         // 載入程式集
10         internal void PopulateDefaultParts(string entryAssemblyName);
11     }

ApplicationPartManager的定義比較簡單,標準的“兩菜兩湯”,其PopulateDefaultParts方法在我們的Strarup裡面的services.AddMvcCore()方法裡面得到間接呼叫。看程式碼。

 1 public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services)
 2         {
 3             var partManager = GetApplicationPartManager(services);
 4             
 5             // 其他程式碼...
 6 
 7             return builder;
 8         }
 9         
10         private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services)
11         {
12             if (manager == null)
13             {
14                 manager = new ApplicationPartManager();
15 
16                 // 其他程式碼...
17                 // 呼叫處
18                 manager.PopulateDefaultParts(entryAssemblyName);
19             }
20 
21             return manager;
22         }

 

ApplicationPartManager的主要職責就是在ASPNETCOREMVC啟動時載入所有程式集,其中包括Controller。為了更形象的表達,我在這裡引用楊曉東大大的一張圖。

為了驗證Controller是由ApplicationPartManager所載入,我們繼續看程式碼。

 1 public void PopulateFeature(
 2             IEnumerable<ApplicationPart> parts,
 3             ControllerFeature feature)
 4         {
 5             foreach (var part in parts.OfType<IApplicationPartTypeProvider>())
 6             {
 7                 foreach (var type in part.Types)
 8                 {
 9                     if (IsController(type) && !feature.Controllers.Contains(type))
10                     {
11                         feature.Controllers.Add(type);
12                     }
13                 }
14             }
15         }

 

程式碼邏輯比較簡單,就是載入所有Controller到ControllerFeature,到現在為止,是不是覺得ASPNETCOREMVC實現模組化有眉目了?最後通過對ASPNETCOREMVC原始碼的跟蹤,最終找到PopulateFeature方法的呼叫是在MvcRouteHandler裡面的RouteAsync方法裡面獲取ActionDescriptor屬性時呼叫初始化的。至於Controller的建立那又是另外一個話題了,後續有時間再說。我們繼續往下看InitializeModules()方法的具體實現。在此之前我們需要看看moduleinfo型別的定義,它對應的是具體module工程下面的module.json檔案。

  1 // ModuleInfo定義,比較簡單我就不註釋了
  2 public partial class ModuleInfo : IModuleInfo, IComparable<ModuleInfo>
  3     {
  4         // 其他成員...
  5 
  6         [JsonProperty(PropertyName = "Group")]
  7         public virtual string Group { get; set; }
  8 
  9         [JsonProperty(PropertyName = "FriendlyName")]
 10         public virtual string FriendlyName { get; set; }
 11 
 12         [JsonProperty(PropertyName = "SystemName")]
 13         public virtual string SystemName { get; set; }
 14 
 15         [JsonProperty(PropertyName = "Version")]
 16         public virtual string Version { get; set; }
 17 
 18         [JsonProperty(PropertyName = "Author")]
 19         public virtual string Author { get; set; }
 20 
 21         [JsonProperty(PropertyName = "FileName")]
 22         public virtual string AssemblyFileName { get; set; }
 23 
 24         [JsonProperty(PropertyName = "Description")]
 25         public virtual string Description { get; set; }
 26 
 27         [JsonIgnore]
 28         public virtual bool Installed { get; set; }
 29 
 30         [JsonIgnore]
 31         public virtual Type ModuleType { get; set; }
 32 
 33         [JsonIgnore]
 34         public virtual string OriginalAssemblyFile { get; set; }
 35     }
 36 //InitializeModules
 37 public static void InitializeModules(this ApplicationPartManager applicationPartManager)
 38         {
 39               // 其他程式碼...
 40              // lock
 41             using (new ReaderWriteAsync(_async))
 42             {
 43                 var moduleInfos = new List<ModuleInfo>(); // 模組程式集集合
 44                 var incompatibleModules = new List<string>();  // 無效的模組程式集集合
 45 
 46                 try
 47                 {
 48                     var modulesDirectory = _fileProvider.MapPath(ModuleDefaults.Path);
 49                     _fileProvider.CreateDirectory(modulesDirectory);
 50                     // 從modules資料夾下獲取所有module,遍歷
 51                     foreach (var item in GetModuleInfos(modulesDirectory))
 52                     {
 53                         var moduleFile = item.moduleFile;
 54                         var moduleInfo = item.moduleInfo;
 55                         // 版本
 56                         if (!moduleInfo.SupportedVersions.Contains(NopVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase))
 57                         {
 58                             incompatibleModules.Add(moduleInfo.SystemName);
 59                             continue;
 60                         }
 61                         // module是否安裝
 62                         moduleInfo.Installed = ModulesInfo.InstalledModuleNames
 63                             .Any(o => o.Equals(moduleInfo.SystemName, StringComparison.InvariantCultureIgnoreCase));
 64 
 65                         try
 66                         {
 67                             var moduleDirectory = _fileProvider.GetDirectoryName(moduleFile);
 68                             // 獲取module主程式集
 69                             var moduleFiles = _fileProvider.GetFiles(moduleDirectory, "*.dll", false)
 70                                 .Where(file => IsModuleDirectory(_fileProvider.GetDirectoryName(file)))
 71                                 .ToList();
 72 
 73                             var mainModuleFile = moduleFiles.FirstOrDefault(file =>
 74                             {
 75                                 var fileName = _fileProvider.GetFileName(file);
 76                                 return fileName.Equals(moduleInfo.AssemblyFileName, StringComparison.InvariantCultureIgnoreCase);
 77                             });
 78 
 79                             if (mainModuleFile == null)
 80                             {
 81                                 incompatibleModules.Add(moduleInfo.SystemName);
 82                                 continue;
 83                             }
 84 
 85                             var moduleName = moduleInfo.SystemName;
 86 
 87                             moduleInfo.OriginalAssemblyFile = mainModuleFile;
 88                             // 是否需要新增到par't's,表示需要安裝的module
 89                             var addToParts = ModulesInfo.InstalledModuleNames.Contains(moduleName);
 90 
 91                             addToParts = addToParts || ModulesInfo.ModuleNamesToInstall.Any(o => o.SystemName.Equals(moduleName));
 92 
 93                             if (addToParts)
 94                             {
 95                                 var filesToParts = moduleFiles.Where(file =>
 96                                     !_fileProvider.GetFileName(file).Equals(_fileProvider.GetFileName(mainModuleFile)) &&
 97                                     !IsAlreadyLoaded(file, moduleName)).ToList();
 98                                 foreach (var file in filesToParts)
 99                                 {
100                                     applicationPartManager.AddToParts(file, modulesDirectory, config, _fileProvider);
101                                 }
102                             }
103 
104                             if (ModulesInfo.ModuleNamesToDelete.Contains(moduleName))
105                                 continue;
106 
107                             moduleInfos.Add(moduleInfo);
108                         }
109                         catch (Exception exception)
110                         {    
111                         }
112                     }
113                 }
114                 catch (Exception exception)
115                 {     
116                 }
117             }
118         }

 

InitializeModules方法modules初始化的具體實現邏輯是,1.在站點根目錄下的Modules檔案下獲取所有Module.json檔案和建立moduleinfo物件 2.獲取modulemain主檔案 3.提取需要安裝的module,並新增到我們上面介紹的parts裡面 4.最後修改moduleinfos裡面的module狀態並寫入快取檔案。以上就是module初始化和安裝的主要邏輯。接著往下我們來看看具體的module,這裡我們以Logging模組為例。

 

從logging工程目錄來看,每個module模組其實就是一個完整的ASPNETCOREMVC工程,同時具有獨立的DBContext資料庫訪問上下文物件。下面我們簡單介紹一下logging程式集裡面各資料夾下面的具體邏輯。
Controllers為該模組的所有Controller物件,Factories資料夾下的實體工廠主要是為Models資料夾下模型物件的建立服務的,Infrastructure資料夾下面主要是當前工程物件DI容器注入和當前工程下EFCORE資料庫上下文DBContext初始化,Map資料夾下主要是DB模型對映,Services裡面是該工程下領域物件的服務,Views檢視資料夾,Module.json是模組描述檔案,Models檔案其實際就是我們以前喜歡命名的ViewModel。可能有朋友會問,我們的領域物件在哪裡?在這裡我把領域物件封裝到了Logging.Abstractions工程裡面,包括某些需要約束的服務介面。下面我們介紹實現新的模組需要哪些操作。
1.在Modules資料夾下新增NETCORE類庫,引入相關nuget包。
2.生成路徑設定為根目錄下的Modules資料夾,包括view檔案也需要複製到這個目錄,因為返回view需要指定view的根目錄。
3.新增module.json檔案,同時複製到Modules資料夾下。
以上就是模組化的實現原理,當然在ASPNETCORE基礎平臺上面實現模組化程式設計有多種方式,這只是其中一種實現方式。下面我們來介紹第二種實現方式,在我的模組化框架裡也有實現,參考微軟開源框架OrchardCore。
對於ASPNETMVC或者說ASPNETMVCCORE基礎框架來說,要想實現模組化或者外掛系統,稍微那麼一點點麻煩的就是VIew,如果我們閱讀這兩個框架原始碼就能看出View其本身相關的邏輯和程式碼量要比Controller、Action、Route等等功能的程式碼量多得多,而且其自身邏輯也有一定的複雜度,比如檔案系統、動態編譯、快取、渲染等等。接下來我要講的這種方式非常類似我之前一篇文章裡面的實現方式,通過嵌入的View檢視資源並且重寫檔案系統提供程式,這裡甚至不需要擴充套件View的查詢邏輯。說到這裡,熟悉ASPNETCORE框架的朋友應該知道擴充套件點了。 既然是資原始檔,那我們就肯定要重寫部分Razor檔案系統,直接看程式碼,這次我們直接先看呼叫邏輯。

模組方式實現二
 1 public class ModuleEmbeddedFileProvider : IFileProvider
 2     {
 3         private readonly IModuleContext _moduleContext;
 4 
 5         public ModuleEmbeddedFileProvider(IModuleContext moduleContext);
 6 
 7         private ModuleApplication ModuleApp => _moduleContext.ModuleApplication;
 8         //遞迴資料夾,實現我們自定義的查詢路徑
 9         public IDirectoryContents GetDirectoryContents(string subpath);
10         // 獲取資原始檔
11         public IFileInfo GetFileInfo(string subpath);
12         
13         public IChangeToken Watch(string filter);
14 
15         private string NormalizePath(string path);
16     }
17      // 註冊
18     public void MiddlewarePipeline(IApplicationBuilder application)
19         {
20             var env = application.ApplicationServices.GetRequiredService<IHostingEnvironment>();
21             var appContext = application.ApplicationServices.GetRequiredService<IModuleContext>();
22             env.ContentRootFileProvider = new CompositeFileProvider(
23                 new ModuleEmbeddedFileProvider(appContext),
24                 env.ContentRootFileProvider);
25         }

 

ModuleEmbeddedFileProvider裡面的邏輯大概是這樣的,遞迴pages、areas目錄下的所有檔案,如果有我們定義的模組module,則通過Assembly獲取嵌入的資原始檔view。本著刨根問底的態度,通過ASPNETCORE原始碼,扒一扒它們的提供機制。
我們通過對框架原始碼的跟蹤,最終發現ModuleEmbeddedFileProvider物件的GetDirectoryContents方法是在ActionSelector物件裡面的屬性Current得到呼叫。

 1 internal class ActionSelector : IActionSelector
 2     {
 3        // 其他成員
 4 
 5         private ActionSelectionTable<ActionDescriptor> Current
 6         {
 7             get
 8             {
 9                 // 間接呼叫
10                 var actions = _actionDescriptorCollectionProvider.ActionDescriptors;
11                // 其他程式碼
12             }
13         }
14    }

 

下面我們接著看看IActionSelector的定義。

1 public interface IActionSelector
2     {
3         IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context);
4 
5         ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates);
6     }

IActionSelector就兩方法,獲取所有ActionDescriptors集合和匹配ActionDescriptor物件,這裡我們不討論Action匹配邏輯,我們繼續跟蹤程式碼往下看。

 1 internal class RazorProjectPageRouteModelProvider : IPageRouteModelProvider
 2     {
 3         private const string AreaRootDirectory = "/Areas";
 4         private readonly RazorProjectFileSystem _razorFileSystem;
 5         // 其他成員
 6 
 7         public RazorProjectPageRouteModelProvider(
 8             RazorProjectFileSystem razorFileSystem,
 9             IOptions<RazorPagesOptions> pagesOptionsAccessor,
10             ILoggerFactory loggerFactory)
11         {
12             // 其他程式碼
13             _razorFileSystem = razorFileSystem;
14         }
15 
16         public void OnProvidersExecuted(PageRouteModelProviderContext context);
17 
18         public void OnProvidersExecuting(PageRouteModelProviderContext context);
19         
20         // 我們定義的ModuleEmbeddedFileProvider就是在此處被呼叫
21         private void AddPageModels(PageRouteModelProviderContext context);
22         // 我們定義的ModuleEmbeddedFileProvider就是在此處被呼叫
23         private void AddAreaPageModels(PageRouteModelProviderContext context);
24     }
25    
26     internal class FileProviderRazorProjectFileSystem : RazorProjectFileSystem
27     {
28         // _fileProvider
29         private readonly RuntimeCompilationFileProvider _fileProvider;
30        // 我們自定義的FileProvider,後續我會驗證這個FileProvider是來源於我們自定義的ModuleEmbeddedFileProvider
31         public IFileProvider FileProvider => _fileProvider.FileProvider;
32         
33         public FileProviderRazorProjectFileSystem(RuntimeCompilationFileProvider fileProvider, IWebHostEnvironment hostingEnvironment)
34         {
35             // _fileProvider通過DI容器構造器注入
36             _fileProvider = fileProvider;
37             _hostingEnvironment = hostingEnvironment;
38         }
39         
40         // 獲取檢視檔案
41         public override RazorProjectItem GetItem(string path, string fileKind)
42         {
43             path = NormalizeAndEnsureValidPath(path);
44             var fileInfo = FileProvider.GetFileInfo(path);
45 
46             return new FileProviderRazorProjectItem(fileInfo, basePath: string.Empty, filePath: path, root: _hostingEnvironment.ContentRootPath, fileKind);
47         }
48         
49         public override IEnumerable<RazorProjectItem> EnumerateItems(string path)
50         {
51             path = NormalizeAndEnsureValidPath(path);
52             return EnumerateFiles(FileProvider.GetDirectoryContents(path), path, prefix: string.Empty);
53         }
54         // 遞迴獲取目錄下的Razor檢視檔案
55         private IEnumerable<RazorProjectItem> EnumerateFiles(IDirectoryContents directory, string basePath, string prefix)
56         {
57             if (directory.Exists)
58             {
59                 foreach (var fileInfo in directory)
60                 {
61                     if (fileInfo.IsDirectory)
62                     {
63                         var relativePath = prefix + "/" + fileInfo.Name;
64                         var subDirectory = FileProvider.GetDirectoryContents(JoinPath(basePath, relativePath));
65                         var children = EnumerateFiles(subDirectory, basePath, relativePath);
66                         foreach (var child in children)
67                         {
68                             yield return child;
69                         }
70                     }
71                     else if (string.Equals(RazorFileExtension, Path.GetExtension(fileInfo.Name), StringComparison.OrdinalIgnoreCase))
72                     {
73                         var filePath = prefix + "/" + fileInfo.Name;
74 
75                         yield return new FileProviderRazorProjectItem(fileInfo, basePath, filePath: filePath, root: _hostingEnvironment.ContentRootPath);
76                     }
77                 }
78             }
79         }
80     }

RazorProjectPageRouteModelProvider頁面路由提供程式,這個物件的AddPageModels方法呼叫了我們的ModuleEmbeddedFileProvider物件的GetDirectoryContents方法,如果是模組程式集嵌入的檢視資源,提供我們自定義的路徑查詢邏輯。至於GetFileInfo是在檢視首次發生編譯的時候呼叫。到這裡留給我們的還有最後一個問題,那就是我們的ModuleEmbeddedFileProvider是如何註冊到ASPNETCOREMVC基礎框架的。通過RazorProjectPageRouteModelProvider物件以上程式碼片段我們發現,該物件的FileProvider屬性來源於RuntimeCompilationFileProvider物件,下面我們看看該物件的定義。

 1 internal class RuntimeCompilationFileProvider
 2     {
 3         private readonly MvcRazorRuntimeCompilationOptions _options;
 4         private IFileProvider _compositeFileProvider;
 5 
 6         public RuntimeCompilationFileProvider(IOptions<MvcRazorRuntimeCompilationOptions> options)
 7         {
 8             // 構造器注入
 9             _options = options.Value;
10         }
11         // FileProvider
12         public IFileProvider FileProvider
13         {
14             get
15             {
16                 if (_compositeFileProvider == null)
17                 {
18                     _compositeFileProvider = GetCompositeFileProvider(_options);
19                 }
20 
21                 return _compositeFileProvider;
22             }
23         }
24         // 獲取FileProvider
25         private static IFileProvider GetCompositeFileProvider(MvcRazorRuntimeCompilationOptions options)
26         {
27             var fileProviders = options.FileProviders;
28             if (fileProviders.Count == 0)
29             {
30                 var message = Resources.FormatFileProvidersAreRequired(
31                     typeof(MvcRazorRuntimeCompilationOptions).FullName,
32                     nameof(MvcRazorRuntimeCompilationOptions.FileProviders),
33                     typeof(IFileProvider).FullName);
34                 throw new InvalidOperationException(message);
35             }
36             else if (fileProviders.Count == 1)
37             {
38                 return fileProviders[0];
39             }
40 
41             return new CompositeFileProvider(fileProviders);
42         }
43     }

我們自定義的ModuleEmbeddedFileProvider提供程式就是在GetCompositeFileProvider這個方法裡面獲取出來的。上面的options.FileProviders來源於我們上面的包裝物件CompositeFileProvider。通過MvcRazorRuntimeCompilationOptionsSetup物件的Configure方法新增進來。

1 internal class MvcRazorRuntimeCompilationOptionsSetup : IConfigureOptions<MvcRazorRuntimeCompilationOptions>
2     {
3         public void Configure(MvcRazorRuntimeCompilationOptions options)
4         {
5             // 我們自定義的ModuleEmbeddedFileProvider在這裡被新增進來的
6             options.FileProviders.Add(_hostingEnvironment.ContentRootFileProvider);
7         }
8     }

到此第二種模組化實現方式也算是全部講完了。做個簡單的總結,ASPNETCOREMVC實現模組化程式設計有多種方法實現,我列舉了兩種,也是我以前工作中使用的方式。1.通過ApplicationPartManager物件實現模組程式集的管理。2.通過擴充套件Razor檔案查詢系統,以嵌入資源的方式實現。由於篇幅的問題,我把本次講解再次壓縮,下面我們詳細分解中介軟體,至於路由、DI容器、View檢視下次有時間再跟大家一起分享。

中介軟體

中介軟體是什麼?中介軟體這個詞,我們很難給它下一個定義。我覺得它應該是要結合使用環境上下文才能確定其定義。在ASPNETCORE平臺裡面,中介軟體是一系列組成Request管道和Respose管道的獨立元件,以連結串列或者說委託鏈的形式構建。好了,解析就到此,大家都有自己的主觀理解。下面我們一起看看中介軟體的型別定義。

1 public interface IMiddleware
2     {
3         Task InvokeAsync(HttpContext context, RequestDelegate next);
4     }

IMiddleware接口裡面就定義了一個成員,InvokeAsync方法。該方法具有兩個引數,context為請求上下文,next為下一個中介軟體的輸入。說實話我在開發工作中從來沒有實現過該介面,當然微軟也沒有強制我們實現中介軟體必須要實現IMiddleware介面。其實整個ASPNETCORE平臺強調的是一種約定策略,稍後我會詳細介紹具體有哪些約定。讓我們開發者能更靈活、自由實現我們的需求。下面我們一起來看看,我們專案中使用的中介軟體。

 1 public class AuthenticationMiddleware
 2     {
 3         private  RequestDelegate _next;
 4 
 5         public AuthenticationMiddleware(IAuthenticationSchemeProvider schemes, RequestDelegate next)
 6         {
 7             Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
 8             _next = next ?? throw new ArgumentNullException(nameof(next));
 9         }
10         // ASPNETCORE全新認證提供程式
11         public IAuthenticationSchemeProvider Schemes { get; set; }
12 
13         public async Task Invoke(HttpContext context)
14         {
15             // 其他程式碼
16             // 呼叫下一個中介軟體
17             await _next(context);
18         }
19     }

以上就是我們在模組化框架裡面定義的認證中介軟體,是不是比較簡單?這也是開發工作中大部分朋友定義中介軟體的形式。IAuthenticationSchemeProvider是ASPNETCORE平臺全新設計的認證提供機制。有了自定義的中介軟體型別,下面我們來具體看看,中介軟體怎麼註冊到ASPNETCORE平臺管道里面去。

1 public static void UseAuthentication(this IApplicationBuilder application)
2         {
3             // 其他程式碼
4             application.UseMiddleware<AuthenticationMiddleware>();
5         }

以上程式碼是我們自己框架裡面的註冊程式碼,AuthenticationMiddleware中介軟體的註冊最終由application.UseMiddleware方法完成,該方法是IApplicationBuilder物件的擴充套件方法。

1 public static class UseMiddlewareExtensions
2     {
3         // 註冊中介軟體,不帶middleware型別type引數
4         public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args);
5         // 註冊中介軟體,帶有middleware引數
6         public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args);
7     }

UseMiddlewareExtensions物件裡面就包含兩個方法,註冊中介軟體,一個泛型一個非泛型,其實方法內部實現上沒有區別,註冊邏輯最終落在UseMiddleware非泛型方法之上。下面我們看看註冊方法的具體實現邏輯。

 1 public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
 2         {
 3             // 派生IMiddleware介面
 4             if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
 5             {
 6                 if (args.Length > 0)
 7                 {
 8                     throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
 9                 }
10 
11                 return UseMiddlewareInterface(app, middleware);
12             }
13             // 非派生IMiddleware介面實現
14             var applicationServices = app.ApplicationServices;
15             return app.Use(next =>
16             {
17                 var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
18                 var invokeMethods = methods.Where(m =>
19                     string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
20                     || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
21                     ).ToArray();
22 
23                 if (invokeMethods.Length > 1)
24                 {
25                     throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
26                 }
27 
28                 if (invokeMethods.Length == 0)
29                 {
30                     throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
31                 }
32 
33                 var methodInfo = invokeMethods[0];
34                 if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
35                 {
36                     throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
37                 }
38 
39                 var parameters = methodInfo.GetParameters();
40                 if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
41                 {
42                     throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
43                 }
44             });
45         }

從UseMiddleware方法的具體實現程式碼,我們可以看出,平臺內部爭對我們自定義middleware中介軟體,預設實現了兩種方式去完成我們的中介軟體註冊。第一種是實現imiddleware介面的中介軟體,第二種是按約定實現的中介軟體。接下來我們詳細討論約定方式實現的中介軟體的註冊機制。在介紹註冊之前,我們先看看沒有實現middeware介面的中介軟體,具體有哪些約定策略。自定義的middelware型別裡面必須包含一個且只有一個,公共例項並且取名為invoke或者invokeasync的這麼一個方法,同時返回值必須為Task型別,最後該方法的第一個引數必須為httpcontext型別。下面我們接著繼續看中介軟體的註冊。

1 public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
2         {
3             _components.Add(middleware);
4             return this;
5         }
6         
7         private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new 
8 List<Func<RequestDelegate, RequestDelegate>>();

註冊邏輯就很簡單了,直接新增中介軟體到List集合裡面去,並且返回IApplicationBuilder物件。到此我們的中介軟體只是註冊到平臺中介軟體集合裡面去,並未發生初始化哦。那麼我們註冊的所有中介軟體是在哪裡初始化的呢?我們回過頭來想想,上面我在分析系統入口Startup的執行機制的時候,是否還記得,它的Configure方法是在main函式的run方法裡面得到呼叫的,而一般情況下我們的中介軟體也都是在Configure方法裡面初始化的。所以我們回過頭來,繼續跟蹤main函式裡面的run方法。
通過跟蹤發現,run方法裡面間接呼叫了ApplicationBuilder.Build()方法,Build方法裡面就是初始化我們所有中介軟體的地方。

 1 public RequestDelegate Build()
 2                         {
 3                                 RequestDelegate app = context =>
 4                                 {
 5                                         // 其他程式碼
 6 
 7                                         context.Response.StatusCode = 404;
 8                                         return Task.CompletedTask;
 9                                 };
10                 
11                                 // 初始化中介軟體委託鏈
12                                 foreach (var component in _components.Reverse())
13                                 {
14                                         app = component(app);
15                                 }
16                                 // 返回第一個中介軟體
17                                 return app;
18                         }

初始化這個地方理解起來還是有那麼一點點拗哦。首先是把中介軟體集合反轉,然後遍歷並且開始初始化倒數第二個中介軟體(我這裡說的倒數第二個只是相對這個集合裡面的中介軟體而言),為什麼說是倒數第二個?仔細看上面程式碼,平臺定義了一個404的中介軟體,並且作為倒數第二個中介軟體的輸入,在倒數第二個中介軟體初始化的過程中把404中介軟體賦值給了自己的next屬性(稍後馬上介紹中介軟體的初始化),最後建立當前自己這個中介軟體的例項,傳遞給倒數第三個中介軟體初始化做為輸入,以此類推,直到整個中介軟體連結串列初始化完成,需要注意的地方,中介軟體的執行順序還是我們註冊的順序。體外話,其實這種方式跟webapi的HttpMessageHandler的實現DelegatingHandler有幾分相似,我只是說設計理念,具體實現還是差別很大。廢話不說了,接下來我們看看中介軟體的具體初始化工作。

 1 public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
 2         {
 3             // 其他程式碼
 4 
 5             var applicationServices = app.ApplicationServices;
 6             return app.Use(next =>
 7             {
 8                 // 其他程式碼
 9                 var ctorArgs = new object[args.Length + 1];
10                 ctorArgs[0] = next;
11                 Array.Copy(args, 0, ctorArgs, 1, args.Length);
12                 var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
13                 if (parameters.Length == 1)
14                 {
15                     return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
16                 }
17 
18                 var factory = Compile<object>(methodInfo, parameters);
19 
20                 return context =>
21                 {
22                     var serviceProvider = context.RequestServices ?? applicationServices;
23                     if (serviceProvider == null)
24                     {
25                         throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
26                     }
27 
28                     return factory(instance, context, serviceProvider);
29                 };
30             });
31         }

首先初始化引數陣列ctorArgs,並且把next輸入引數置為引數陣列的第一個元素,然後把傳遞進來的引數填充到後面元素。接下來就是當前中介軟體的建立過程,我們繼續看程式碼。

 1 public static object CreateInstance(IServiceProvider provider, Type instanceType, params object[] parameters)
 2         {
 3             int bestLength = -1;
 4             var seenPreferred = false;
 5 
 6             ConstructorMatcher bestMatcher = null;
 7 
 8             if (!instanceType.GetTypeInfo().IsAbstract)
 9             {
10                 foreach (var constructor in instanceType
11                     .GetTypeInfo()
12                     .DeclaredConstructors
13                     .Where(c => !c.IsStatic && c.IsPublic))
14                 {
15                     
16                     var matcher = new ConstructorMatcher(constructor);
17                     var isPreferred = constructor.IsDefined(typeof(ActivatorUtilitiesConstructorAttribute), false);
18                     var length = matcher.Match(parameters);
19                     // 其他程式碼
20                 }
21             }
22 
23             if (bestMatcher == null)
24             {
25                 var message = $"A suitable constructor for type '{instanceType}' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.";
26                 throw new InvalidOperationException(message);
27             }
28 
29             return bestMatcher.CreateInstance(provider);
30         }
31         // 匹配引數並且賦值
32         public int Match(object[] givenParameters)
33             {
34                 var applyIndexStart = 0;
35                 var applyExactLength = 0;
36                 for (var givenIndex = 0; givenIndex != givenParameters.Length; givenIndex++)
37                 {
38                     var givenType = givenParameters[givenIndex]?.GetType().GetTypeInfo();
39                     var givenMatched = false;
40 
41                     for (var applyIndex = applyIndexStart; givenMatched == false && applyIndex != _parameters.Length; ++applyIndex)
42                     {
43                         if (_parameterValuesSet[applyIndex] == false &&
44                             _parameters[applyIndex].ParameterType.GetTypeInfo().IsAssignableFrom(givenType))
45                         {
46                             givenMatched = true;
47                             _parameterValuesSet[applyIndex] = true;
48                             _parameterValues[applyIndex] = givenParameters[givenIndex];
49                             if (applyIndexStart == applyIndex)
50                             {
51                                 applyIndexStart++;
52                                 if (applyIndex == givenIndex)
53                                 {
54                                     applyExactLength = applyIndex;
55                                 }
56                             }
57                         }
58                     }
59 
60                     if (givenMatched == false)
61                     {
62                         return -1;
63                     }
64                 }
65                 return applyExactLength;
66             }

Match方法的大概邏輯是,從Args也就是我們註冊middelware傳遞進來的引數裡面獲取當前中介軟體構造器裡面所需的引數列表,但是這裡面有一種情況,構造器裡面的next引數在這裡是可以得到初始化操作。那中介軟體構造器有多個引數的話,其他引數在哪裡初始化?我們接著往下看 bestMatcher.CreateInstance(provider)。

 1 public object CreateInstance(IServiceProvider provider)
 2             {
 3                 for (var index = 0; index != _parameters.Length; index++)
 4                 {
 5                     if (_parameterValuesSet[index] == false)
 6                     {
 7                         var value = provider.GetService(_parameters[index].ParameterType);
 8                         if (value == null)
 9                         {
10                             if (!ParameterDefaultValue.TryGetDefaultValue(_parameters[index], out var defaultValue))
11                             {
12                                 throw new InvalidOperationException($"Unable to resolve service for type '{_parameters[index].ParameterType}' while attempting to activate '{_constructor.DeclaringType}'.");
13                             }
14                             else
15                             {
16                                 _parameterValues[index] = defaultValue;
17                             }
18                         }
19                         else
20                         {
21                             _parameterValues[index] = value;
22                         }
23                     }
24                 }
25 
26                 try
27                 {
28                     return _constructor.Invoke(_parameterValues);
29                 }
30                 catch (TargetInvocationException ex) when (ex.InnerException != null)
31                 {
32                 }
33                 #endif
34             }
35         }

非常直觀,當前中介軟體構造器引數列表裡面沒有初始化的引數,在這裡首先通過DI容器注入,也就是說在中介軟體初始化之前,額外的引數要先通過Startup註冊到DI容器,如果DI容器裡面也沒有獲取到這個引數,平臺將啟用終極解決版本,通過ParameterDefaultValue物件強勢反射建立。最後通過反射建立當前中介軟體例項,如果當前中介軟體的invoke方法只有一個引數,直接包裝成RequestDelegate物件返回。如果有多個引數,包裝成表示式樹返回。以上就是中介軟體常規用法的詳細介紹。需要了解更多的可以去自行研究原始碼。比較晚了,不寫了,本來打算想把我們框架裡面的AuthenticationMiddleware中介軟體的認證邏輯和原理也一併講完,算了還是下次吧。下次一起講解路由、DI、view檢視。

最後總結

本篇文章主要是介紹ASPNETCOREMVC實現模組化程式設計的實現方法,還有一些平臺原始碼的分析,希望有幫到的朋友點個贊,謝謝。下次打算花兩個篇幅講解微軟開源框架OrchardCore,當然這個框架有點複雜,兩個篇幅太短,我們主要是看看裡面比較核心的東西。最後謝謝大家的閱讀。