记得上一篇博客中跟你们分享的是基于ASP.NETMVC5,实际也就是基于NETFRAMEWORK平台实现的这么一个轻量级插件式框架。那么今天我主要分享的是本身工做中参考三方主流开源WEB框架OrchardCore、NopCore等,实现的另一个轻量级模块化WEB框架,固然这个框架就是基于当下微软力推和开源社区比较火爆的基础平台ASPNETCORE。
进入正题以前,我以为先有必要简单介绍一下ASPNETCORE这个平台你们比较关心的几个指标。
其一性能,话很少说直接看我的以为比较权威的性能测试网站https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=fortune,微软官方给出的数据性能是ASPNET的23倍。
其二生态,从NETCORE2.0开始,国内愈来愈多的大型互联网公司开始支持,好比百度云SDK、腾讯云SDK、腾讯的Tars 微服务平台、携程、阿里云等等。咱们能够看看相关的issue,以百度云为例 https://github.com/Baidu-AIP/dotnet-sdk/issues/3。
其三迁移,自NETCORE2.0开始,有愈来愈多的三方nuget包支持。
其四开源,使用的是MIT和Apache 2开源协议,文档协议遵循CC-BY。这就意味着任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再受权,或者销售。惟一的限制是,软件中必须包含上述版 权和许可提示,后者协议将会除了为用户提供版权许可以外,还有专利许可,而且受权是免费,无排他性的(任何我的和企业都能得到受权)而且永久不可撤销,相较于oracle对java和mysql的开源协议微软作出了最大的诚意。
其五跨平台,这也是真正意义上的跨平台,完全摒弃了.NET Framework这种提取目标框架API交集方式的PCL。.NETCORE微软全新设计了针对各平台CoreCLR运行时和统一的PCL.NET Standard。
最后算是我的的一点点小建议,更新速度能够适当的慢一点,分一部分时间多关注一下这个生态圈。打个比方,在这个文明年代,你一我的会降龙十八掌,你会牛逼到没朋友,没有人敢跟你玩。java
该框架采用的是ASPNETCORE2.2的版本,实现了日志管理、权限管理、模块管理、多语言、多主题、自动化任务管理等等功能。下面贴一张简单的动态图看看效果。mysql
本人用的是vs2019,目前好像最高是预览版,建议你们就当前版原本说,正式开发工做仍是要慎用,稳定性比较差。仍是老套路,我可能只会抽取框架里面1-2个重要的模块实现加以详细介绍。顾及可能有些朋友接触ASPNETCORE时间不长,同时我也会针对框架里面使用的某些基础技术点作详细介绍,好比DI容器、路由、中间件、视图View等。这篇博客主要是介绍模块化框架的具体实现,思路方面能够参考个人上一篇文章。先上图解决方案目录结构
整个工程主要分三大模块,Infrastructure顾名思义就是整个项目的基础功能和实现。Modules为项目全部子模块,根据业务划分的相关模块。UI里面包含了ASPNETCOREMVC的基础扩展和布局。
可能有些朋友会问,为何Modules目录下面的模块工程有对应的Abstractions工程对应?不要误解不是全部都是一一对应。咱们在阅读NETCORE和OrchardCore源码的时候也常常会看到有对应的Abstractions工程,主要是针对基础模块更高层次的抽象。下面直接解读代码实现。git
咱们先看看框架入口,Program.cs文件的main函数,看代码github
1 public static void Main(string[] args) 2 { 3 var host = WebHost.CreateDefaultBuilder(args) 4 .UseKestrel() 5 .UseStartup<Startup>() 6 .Build(); 7 8 host.Run(); 9 }
题外话,咱们以往在使用ASPNETMVC或者说ASPNETWEBFOREMS的时候,有看到或者定义过main函数吗?没有。由于它们的初始化工做由非托管的aspnet_isapi完成,aspnet_isapi是IIS的组成部分,经过COM级别的Class调用,而且aspnet_isapi并不是是面向用户编程的api接口,因此早期版本的ASPNET耦合了WebServer容器IIS。
代码很少,就简单的几行代码,完成了整个ASPNETCOREMVC基础框架和应用框架所须要的功能模块的初始化工做,而且启动KestrelServer的监听。整个WebHostBuilder经过标准的建造者模式实现,因为Startup是咱们框架程序的入口,下面咱们重点看看UseStartup方法和Startup对象。咱们先来看看ASPNETCOREMVC源码里面的UseStarup的定义。web
1 public static class WebHostBuilderExtensions 2 { 3 // 其余代码... 4 public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType) 5 { 6 //其余代码... 7 return hostBuilder 8 .ConfigureServices(services => 9 { 10 // 实现IStartup接口 11 if (typeof(IStartup).GetTypeInfo().IsAssignableFrom(startupType.GetTypeInfo())) 12 { 13 services.AddSingleton(typeof(IStartup), startupType); 14 } 15 else 16 { 17 // 常规方式 18 services.AddSingleton(typeof(IStartup), sp => 19 { 20 var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>(); 21 return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName)); 22 }); 23 } 24 }); 25 } 26 }
从UseStartup方法的定义,咱们了解到,ASPNETCore并无采用接口实现的方式为启动类型作强制性的约束,而仅仅是做为启动类型的定义提供了一个约定而已。一般咱们在定义中间件和服务注册类Startup时,直接将其命名为Startup,并未实现IStartup接口。因此咱们这里采用的是常规方式来定义和建立Startup。建立Startup对象是由ConventionBasedStartup完成,下面咱们看看ConventionBasedStartup类型的定义。sql
1 // ConventionBasedStartup 2 public class ConventionBasedStartup : IStartup 3 { 4 public ConventionBasedStartup(StartupMethods methods); 5 6 public void Configure(IApplicationBuilder app); 7 8 public IServiceProvider ConfigureServices(IServiceCollection services); 9 } 10 // StartupMethods 11 public class StartupMethods 12 { 13 public StartupMethods(object instance, Action<IApplicationBuilder> configure, Func<IServiceCollection, IServiceProvider> configureServices); 14 15 public object StartupInstance { get; } 16 public Func<IServiceCollection, IServiceProvider> ConfigureServicesDelegate { get; } 17 public Action<IApplicationBuilder> ConfigureDelegate { get; } 18 19 }
从ConventionBasedStartup的构造器来看,ConventionBasedStartup的建立是由StartupMethods对象来建立的,那么咱们如今颇有必要知道StartupMethods对象的建立。经过UseStartup的实现,咱们知道StartupMethods的建立者是一个类型为StartupLoader的对象。数据库
1 public class StartupLoader 2 { 3 // 其余成员... 4 public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName) 5 { 6 var configureMethod = FindConfigureDelegate(startupType, environmentName); 7 8 var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName); 9 10 // 其余代码... 11 12 var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance( 13 typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type), 14 hostingServiceProvider, 15 servicesMethod, 16 configureContainerMethod, 17 instance); 18 19 return new StartupMethods(instance, configureMethod.Build(instance), builder.Build()); 20 } 21 }
从以上代码片断能够看出,LoadMethods建立了StartupMethods,也就是咱们自定义的Starpup对象。一下有几个地方须要注意,1.对于Startup的建立咱们只是使用了诸多方法中的其中一种,调用UseStartup方法。固然ASPNETCORE具备多种方法建立Startup对象。2.Startup类型的命名约定,可携带环境名称environment,环境名称可在UseSetting里面指定,固然咱们通常采用显式的方式调用UseStartup方法。3.Startup类型用于注册服务和中间件的这两个方法约定,能够静态也可非静态,同时可携带环境名称。参数约定,只有Configure强制第一个参数为IApplicationBuilder。以上注意点有兴趣的朋友能够自行去研究源代码,下面咱们看看咱们自定义的Startup对象。编程
1 public class Startup 2 { 3 private readonly IConfiguration _configuration; 4 private readonly IHostingEnvironment _hostingEnvironment; 5 6 public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment) 7 { 8 _configuration = configuration; 9 _hostingEnvironment = hostingEnvironment; 10 } 11 // 注册服务 12 public IServiceProvider ConfigureServices(IServiceCollection services) 13 { 14 return services.AddApplicationServices(_configuration, _hostingEnvironment); 15 } 16 // 注册中间件 17 public void Configure(IApplicationBuilder application) 18 { 19 application.AddApplicationPipeline(); 20 } 21 }
对于Startup对象里面的两个方法我我的的理解是,一个生产一个消费。ConfigureServices负责建立服务,Configure负责建立中间件管道而且消费ConfigureServices里面注册的服务。下面咱们继续看看这两个方法的执行时机。json
1 public IWebHost Build() 2 { 3 // 其余代码 4 var host = new WebHost( 5 applicationServices, 6 hostingServiceProvider, 7 _options, 8 _config, 9 hostingStartupErrors); 10 try 11 { 12 host.Initialize(); // 13 return host; 14 } 15 catch 16 { 17 host.Dispose(); 18 throw; 19 } 20 } 21 22 private void EnsureApplicationServices() 23 { 24 if (_applicationServices == null) 25 { 26 EnsureStartup(); 27 _applicationServices = _startup.ConfigureServices(_applicationServiceCollection); // 执行ConfigureServices方法 28 } 29 }
Build()就是咱们定义在main函数里面的Build方法,经过以上代码片断,咱们能够看出Startup里面的ConfigureServices方法是在Build方法里面完成。咱们继续看看Configure方法的执行。api
1 private RequestDelegate BuildApplication() 2 { 3 try 4 { 5 Action<IApplicationBuilder> configure = _startup.Configure; 6 7 // 执行startup configure 8 configure(builder); 9 10 return builder.Build(); 11 } 12 }
BuildApplication()方法是在main函数里面的run函数间接调用的。到此对于Startup类型涉及的一些问题已经所有讲完,但愿你们不要以为啰嗦。下面咱们继续往下看模块的实现。
1 public static class ServiceCollectionExtensions 2 { 3 // 其余成员... 4 public static IServiceProvider AddApplicationServices(this IServiceCollection services, 5 IConfiguration configuration, IHostingEnvironment hostingEnvironment) 6 { 7 // 其余代码... 8 var mvcCoreBuilder = services.AddMvcCore(); 9 // 初始化模块及安装 10 mvcCoreBuilder.PartManager.InitializeModules(); 11 return serviceProvider; 12 } 13 }
在Startup的ConfigureServices里面咱们经过IServiceCollection(ASPNETCORE内置的DI容器,后续我会详细介绍其原理)的扩展方法初始化了模块Modules以及对Modules的安装。在介绍Modules具体实现以前,我以为有必要先介绍ASPNETCORE里面的ApplicationPartManager对象,由于咱们的模块Modules的实现就是基于这个对象实现的。下面咱们看看ApplicationPartManager对象的定义。
1 public class ApplicationPartManager 2 { 3 public IList<IApplicationFeatureProvider> FeatureProviders { get; } = 4 new List<IApplicationFeatureProvider>(); 5 6 public IList<ApplicationPart> ApplicationParts { get; } = new List<ApplicationPart>(); 7 // 加载Feature 8 public void PopulateFeature<TFeature>(TFeature feature); 9 // 加载程序集 10 internal void PopulateDefaultParts(string entryAssemblyName); 11 }
ApplicationPartManager的定义比较简单,标准的“两菜两汤”,其PopulateDefaultParts方法在咱们的Strarup里面的services.AddMvcCore()方法里面获得间接调用。看代码。
1 public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services) 2 { 3 var partManager = GetApplicationPartManager(services); 4 5 // 其余代码... 6 7 return builder; 8 } 9 10 private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services) 11 { 12 if (manager == null) 13 { 14 manager = new ApplicationPartManager(); 15 16 // 其余代码... 17 // 调用处 18 manager.PopulateDefaultParts(entryAssemblyName); 19 } 20 21 return manager; 22 }
ApplicationPartManager的主要职责就是在ASPNETCOREMVC启动时加载全部程序集,其中包括Controller。为了更形象的表达,我在这里引用杨晓东大大的一张图。
为了验证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,固然这个框架有点复杂,两个篇幅过短,咱们主要是看看里面比较核心的东西。最后谢谢你们的阅读。