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的版本,實現了日志管理、權限管理、模塊管理、多語言、多主題、自動化任務管理等等功能。下面貼一張簡單的動態圖看看效果。
54e76670ef92e1e04b9793430c96d6f9.gif

本人用的是vs2019,目前好像最高是預覽版,建議大家就當前版本來說,正式開發工作還是要慎用,穩定性比較差。還是老套路,我可能只會抽取框架里面1-2個重要的模塊實現加以詳細介紹。顧及可能有些朋友接觸ASPNETCORE時間不長,同時我也會針對框架里面使用的某些基礎技術點做詳細介紹,比如DI容器、路由、中間件、視圖View等。這篇博客主要是介紹模塊化框架的具體實現,思路方面可以參考我的上一篇文章。先上圖解決方案目錄結構
28e5276e06fbb070a1f45afdb9001918.png
整個工程主要分三大模塊,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。為了更形象的表達,我在這里引用楊曉東大大的一張圖。
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,當然這個框架有點復雜,兩個篇幅太短,我們主要是看看里面比較核心的東西。最后謝謝大家的閱讀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM