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() 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(); 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, Actionconfigure, Func configureServices); 14 15 public object StartupInstance { get; } 16 public Func ConfigureServicesDelegate { get; } 17 public Action 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 Actionconfigure = _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 IListFeatureProviders { get; } = 4 new List (); 5 6 public IList ApplicationParts { get; } = new List (); 7 // 加载Feature 8 public void PopulateFeature (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 IEnumerableparts, 3 ControllerFeature feature) 4 { 5 foreach (var part in parts.OfType ()) 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, IComparable3 { 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 (); // 模块程序集集合 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(); 21 var appContext = application.ApplicationServices.GetRequiredService (); 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 ActionSelectionTableCurrent 6 { 7 get 8 { 9 // 间接调用 10 var actions = _actionDescriptorCollectionProvider.ActionDescriptors; 11 // 其他代码 12 } 13 } 14 }
下面我们接着看看IActionSelector的定义。
1 public interface IActionSelector 2 { 3 IReadOnlyListSelectCandidates(RouteContext context); 4 5 ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList 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 IOptionspagesOptionsAccessor, 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 EnumerateItems(string path) 50 { 51 path = NormalizeAndEnsureValidPath(path); 52 return EnumerateFiles(FileProvider.GetDirectoryContents(path), path, prefix: string.Empty); 53 } 54 // 递归获取目录下的Razor视图文件 55 private IEnumerable 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(IOptionsoptions) 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 : IConfigureOptions2 { 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(); 5 }
以上代码是我们自己框架里面的注册代码,AuthenticationMiddleware中间件的注册最终由application.UseMiddleware方法完成,该方法是IApplicationBuilder对象的扩展方法。
1 public static class UseMiddlewareExtensions 2 { 3 // 注册中间件,不带middleware类型type参数 4 public static IApplicationBuilder UseMiddleware(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(Funcmiddleware) 2 { 3 _components.Add(middleware); 4 return this; 5 } 6 7 private readonly IList > _components = new 8 List >();
注册逻辑就很简单了,直接添加中间件到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,当然这个框架有点复杂,两个篇幅太短,我们主要是看看里面比较核心的东西。最后谢谢大家的阅读。