天天看點

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,當然這個架構有點複雜,兩個篇幅太短,我們主要是看看裡面比較核心的東西。最後謝謝大家的閱讀。