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。
最後算是我的的一點點小建議,更新速度能夠適當的慢一點,分一部分時間多關注一下這個生態圈。打個比方,在這個文明年代,你一我的會降龍十八掌,你會牛逼到沒朋友,沒有人敢跟你玩。java

框架介紹

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

本人用的是vs2019,目前好像最高是預覽版,建議你們就當前版原本說,正式開發工做仍是要慎用,穩定性比較差。仍是老套路,我可能只會抽取框架裏面1-2個重要的模塊實現加以詳細介紹。顧及可能有些朋友接觸ASPNETCORE時間不長,同時我也會針對框架裏面使用的某些基礎技術點作詳細介紹,好比DI容器、路由、中間件、視圖View等。這篇博客主要是介紹模塊化框架的具體實現,思路方面能夠參考個人上一篇文章。先上圖解決方案目錄結構
28e5276e06fbb070a1f45afdb9001918.png
整個工程主要分三大模塊,Infrastructure顧名思義就是整個項目的基礎功能和實現。Modules爲項目全部子模塊,根據業務劃分的相關模塊。UI裏面包含了ASPNETCOREMVC的基礎擴展和佈局。
可能有些朋友會問,爲何Modules目錄下面的模塊工程有對應的Abstractions工程對應?不要誤解不是全部都是一一對應。咱們在閱讀NETCORE和OrchardCore源碼的時候也常常會看到有對應的Abstractions工程,主要是針對基礎模塊更高層次的抽象。下面直接解讀代碼實現。git

模塊化實現

咱們先看看框架入口,Program.cs文件的main函數,看代碼github

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的定義。web

 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類型的定義。sql

 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裏面註冊的服務。下面咱們繼續看看這兩個方法的執行時機。json

 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方法的執行。api

 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。爲了更形象的表達,我在這裏引用楊曉東大大的一張圖。
5713dfbdbf63687f0645dcb37016480c.png
爲了驗證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模塊爲例。
a20cff5c910907aac08f7b127e81a463.png

 

從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,固然這個框架有點複雜,兩個篇幅過短,咱們主要是看看裏面比較核心的東西。最後謝謝你們的閱讀。

相關文章
相關標籤/搜索