原文:https://www.stevejgordon.co.uk/asp-net-core-mvc-anatomy-addmvccore
发布于:2017年3月
环境:ASP.NET Core 1.1

欢迎阅读新系列的第一部分,我将剖析MVC源代码,给大家展示隐藏在表面之下的工作机制。此系列将分析MVC的内部,如果觉得枯燥,可以停止阅读。但就我个人而言,也是经过反复阅读、调试甚至抓狂,直到最后理解ASP.NET MVC源代码(或者自认为理解),从中获益匪浅。通过了解框架的运作机制,我们可以更好的使用它们,更容易解决遇到的问题。

我会尽力给大家解释对源码的理解,我不能保证自己的理解和解释是100%正确,但我会竭尽所能。要知道简洁清晰的把一段代码解释清楚是很困难的,我将通过小块代码展示MVC源代码,并附源文件链接,方便大家追踪。阅读之后如果仍不理解,我建议你花些时间读读源代码,必要时亲自动手调试一下。我希望此系列会引起像我一样喜欢刨根问底的人的兴趣。

AddMvcCore

本文我将剖析AddMvcCore到底为我们做了什么,同时关注几个像 ApplicationPartManager这样的类。本文使用的project.json基于rel/1.1.2源代码,通过运行MVC Sandbox 项目进行调试。

注:MvcSandbox为ASP.Net Core MVC源码中的示列项目。

由于版本在不断更新,一些类和方法可能会改变,尤其是内部。请始终参考GitHub上的最新代码。对于MvcSandbox ConfigureServices我已作更新:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddMvcCore();
  4. }

AddMvcCore是IServiceCollection的扩展方法。通常把一组关联的服务注册到服务集合(services collection)时都使用扩展方法这种模式。在构建MVC应用时,有两个扩展方法用来注册MVC服务(MVC Services),AddMvcCore是其中之一。相对AddMvc方法,AddMvcCore提供较少的服务子集。一些不需要使用MVC所有特性的简单程序,就可以使用AddMvcCore。比如在构建REST APIs,就不需要Razor相关组件,我一般使用AddMvcCore。你也可以在AddMvcCore之后手动添加额外的服务,或者直接使用功能更多的AddMvc。AddMvcCore的实现:

  1. public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services)
  2. {
  3. if (services == null)
  4. {
  5. throw new ArgumentNullException(nameof(services));
  6. }
  7.  
  8. var partManager = GetApplicationPartManager(services);
  9. services.TryAddSingleton(partManager);
  10.  
  11. ConfigureDefaultFeatureProviders(partManager);
  12. ConfigureDefaultServices(services);
  13. AddMvcCoreServices(services);
  14.  
  15. var builder = new MvcCoreBuilder(services, partManager);
  16.  
  17. return builder;
  18. }

AddMvcCore做的第一件事就是通过GetApplicationPartManager静态方法获得ApplicationPartManager,把IServiceCollection作为参数传递给GetApplicationPartManager。
GetApplicationPartManager的实现:

  1. private static ApplicationPartManager GetApplicationPartManager(IServiceCollection services)
  2. {
  3. var manager = GetServiceFromCollection<ApplicationPartManager>(services);
  4. if (manager == null)
  5. {
  6. manager = new ApplicationPartManager();
  7.  
  8. var environment = GetServiceFromCollection<IHostingEnvironment>(services);
  9. if (string.IsNullOrEmpty(environment?.ApplicationName))
  10. {
  11. return manager;
  12. }
  13.  
  14. var parts = DefaultAssemblyPartDiscoveryProvider.DiscoverAssemblyParts(environment.ApplicationName);
  15. foreach (var part in parts)
  16. {
  17. manager.ApplicationParts.Add(part);
  18. }
  19. }
  20.  
  21. return manager;
  22. }

本方法首先检查当前已注册的服务中是否有ApplicationPartManager,通常情况是没有的,但极少情况你可能在调用AddMvcCore之前注册了其它服务。ApplicationPartManager如果不存在则创建一个新的。

接下来的代码是计算ApplicationPartManager的ApplicationParts属性值。首先从服务集合(services collection)中获得IHostingEnvironment。如果能够获IHostingEnvironment实例,则通过它获得程序名或封装名(application/assem bly name),然后传递给静态方法DefaultAssemblyPartDiscoveryProvider,DiscoverAssemblyParts方法将返回IEnumerable<ApplicationPart>。

  1. public static IEnumerable<ApplicationPart> DiscoverAssemblyParts(string entryPointAssemblyName)
  2. {
  3. var entryAssembly = Assembly.Load(new AssemblyName(entryPointAssemblyName));
  4. var context = DependencyContext.Load(Assembly.Load(new AssemblyName(entryPointAssemblyName)));
  5.  
  6. return GetCandidateAssemblies(entryAssembly, context).Select(p => new AssemblyPart(p));
  7. }

DiscoverAssemblyParts 首先通过封装名(assembly name)获得封装对象(Assembly Object)和DependencyContex。本例封装名为“MvcSandbox”。然后将这些值传递给GetCandidateAssemblies方法,GetCandidateAssemblies再调用GetCandidateLibraries方法。

  1. internal static IEnumerable<Assembly> GetCandidateAssemblies(Assembly entryAssembly, DependencyContext dependencyContext)
  2. {
  3. if (dependencyContext == null)
  4. {
  5. // Use the entry assembly as the sole candidate.
  6. return new[] { entryAssembly };
  7. }
  8.  
  9. return GetCandidateLibraries(dependencyContext)
  10. .SelectMany(library => library.GetDefaultAssemblyNames(dependencyContext))
  11. .Select(Assembly.Load);
  12. }
  13.  
  14. internal static IEnumerable<RuntimeLibrary> GetCandidateLibraries(DependencyContext dependencyContext)
  15. {
  16. if (ReferenceAssemblies == null)
  17. {
  18. return Enumerable.Empty<RuntimeLibrary>();
  19. }
  20.  
  21. var candidatesResolver = new CandidateResolver(dependencyContext.RuntimeLibraries, ReferenceAssemblies);
  22. return candidatesResolver.GetCandidates();
  23. }

解释一下GetCandidateLibraries做了什么:

它返回一个在<see cref=”ReferenceAssemblies”/>中引用的程序集列表,默认是我们引用的主要MVC程序集,不包含项目本身的程序集。

更明确一些,获得的程序集列表包含了我们的解决方案中引用的所有MVC程序集。

ReferenceAssemblies是一个定义在DefaultAssemblyPartDiscoveryProvider类中静态HashSet<string>,它包含13个MVC默认的程序集。

  1. internal static HashSet<string> ReferenceAssemblies { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
  2. {
  3. "Microsoft.AspNetCore.Mvc",
  4. "Microsoft.AspNetCore.Mvc.Abstractions",
  5. "Microsoft.AspNetCore.Mvc.ApiExplorer",
  6. "Microsoft.AspNetCore.Mvc.Core",
  7. "Microsoft.AspNetCore.Mvc.Cors",
  8. "Microsoft.AspNetCore.Mvc.DataAnnotations",
  9. "Microsoft.AspNetCore.Mvc.Formatters.Json",
  10. "Microsoft.AspNetCore.Mvc.Formatters.Xml",
  11. "Microsoft.AspNetCore.Mvc.Localization",
  12. "Microsoft.AspNetCore.Mvc.Razor",
  13. "Microsoft.AspNetCore.Mvc.Razor.Host",
  14. "Microsoft.AspNetCore.Mvc.TagHelpers",
  15. "Microsoft.AspNetCore.Mvc.ViewFeatures"
  16. };

GetCandidateLibraries使用CandidateResolver类定位并返回“候选人”。CandidateResolver 通过运行时对象(RuntimeLibrary objects)和ReferenceAssemblies构造。每个运行时对象依次迭代并添加到一个字典中,添加过程中检查依赖名(dependency name)是否唯一,如果不唯一则抛出异常。

  1. public CandidateResolver(IReadOnlyList<RuntimeLibrary> dependencies, ISet<string> referenceAssemblies)
  2. {
  3. var dependenciesWithNoDuplicates = new Dictionary<string, Dependency>(StringComparer.OrdinalIgnoreCase);
  4. foreach (var dependency in dependencies)
  5. {
  6. if (dependenciesWithNoDuplicates.ContainsKey(dependency.Name))
  7. {
  8. throw new InvalidOperationException(Resources.FormatCandidateResolver_DifferentCasedReference(dependency.Name));
  9. }
  10. dependenciesWithNoDuplicates.Add(dependency.Name, CreateDependency(dependency, referenceAssemblies));
  11. }
  12.  
  13. _dependencies = dependenciesWithNoDuplicates;
  14. }

每个依赖对象(既RuntimeLibrary)都作为新的依赖对象存储到字典中。这些对象包含一个DependencyClassification属性,用来筛选需要的libraries (candidates)。DependencyClassification是一个枚举类型:

  1. private enum DependencyClassification
  2. {
  3. Unknown = ,
  4. Candidate = ,
  5. NotCandidate = ,
  6. MvcReference =
  7. }

创建Dependency的时候,如果与ReferenceAssemblies HashSet匹配,则标记为MvcReference,其余标记为Unknown。

  1. private Dependency CreateDependency(RuntimeLibrary library, ISet<string> referenceAssemblies)
  2. {
  3. var classification = DependencyClassification.Unknown;
  4. if (referenceAssemblies.Contains(library.Name))
  5. {
  6. classification = DependencyClassification.MvcReference;
  7. }
  8.  
  9. return new Dependency(library, classification);
  10. }

当CandidateResolver.GetCandidates方法被调用时,结合ComputeClassification方法,遍历整个依赖对象树。每个依赖对象都将检查他的所有子项,直到匹配Candidate或者MvcReference,同时标记父依赖项为Candidate类型。遍历结束将返回包含identified candidates的IEnumerable<RuntimeLibrary>。例如本例,只有MvcSandbox程序集被标记为Candidate。

  1. public IEnumerable<RuntimeLibrary> GetCandidates()
  2. {
  3. foreach (var dependency in _dependencies)
  4. {
  5. if (ComputeClassification(dependency.Key) == DependencyClassification.Candidate)
  6. {
  7. yield return dependency.Value.Library;
  8. }
  9. }
  10. }
  11.  
  12. private DependencyClassification ComputeClassification(string dependency)
  13. {
  14. Debug.Assert(_dependencies.ContainsKey(dependency));
  15.  
  16. var candidateEntry = _dependencies[dependency];
  17. if (candidateEntry.Classification != DependencyClassification.Unknown)
  18. {
  19. return candidateEntry.Classification;
  20. }
  21. else
  22. {
  23. var classification = DependencyClassification.NotCandidate;
  24. foreach (var candidateDependency in candidateEntry.Library.Dependencies)
  25. {
  26. var dependencyClassification = ComputeClassification(candidateDependency.Name);
  27. if (dependencyClassification == DependencyClassification.Candidate ||
  28. dependencyClassification == DependencyClassification.MvcReference)
  29. {
  30. classification = DependencyClassification.Candidate;
  31. break;
  32. }
  33. }
  34.  
  35. candidateEntry.Classification = classification;
  36.  
  37. return classification;
  38. }
  39. }

DiscoverAssemblyParts将返回的candidates转换为新的AssemblyPart。此对象是对程序集的简单封装,只包含了如名称、类型等主要封装属性。后续我可能单独撰文分析此类。

最后,通过GetApplicationPartManager,AssemblyParts被加入到ApplicationPartManager。

  1. var parts = DefaultAssemblyPartDiscoveryProvider.DiscoverAssemblyParts(environment.ApplicationName);
  2. foreach (var part in parts)
  3. {
  4. manager.ApplicationParts.Add(part);
  5. }
  6. }
  7.  
  8. return manager;

返回的ApplicationPartManager实例通过AddMvcCore扩展方法加入到services collection。

接下来AddMvcCore把ApplicationPartManager作为参数调用静态ConfigureDefaultFeatureProviders方法,为ApplicationPartManager的FeatureProviders添加ControllerFeatureProvider。

  1. private static void ConfigureDefaultFeatureProviders(ApplicationPartManager manager)
  2. {
  3. if (!manager.FeatureProviders.OfType<ControllerFeatureProvider>().Any())
  4. {
  5. manager.FeatureProviders.Add(new ControllerFeatureProvider());
  6. }
  7. }

ControllerFeatureProvider将被用在ApplicationPar的实例中发现controllers。我将在后续的博文中介绍ControllerFeatureProvider。现在我们继续研究AddMovCore的最后一步。(ApplicationPartManager此时已更新)

首先调用私有方法ConfigureDefaultServices,通过Microsoft.AspNetCore.Routing提供的AddRouting扩展方法开启routing功能。它提供启用routing功能所需的必要服务和配置。本文不对此作详细描述。

AddMvcCore接下来调用另一个私有方法AddMvcCoreServices,该方法负责注册MVC核心服务,包括框架可选项,action 的发现、选择和调用,controller工厂,模型绑定和认证。

最后AddMvcCore通过services collection和ApplicationPartManager,构造一个新的MvcCoreBuilder对象。此类被叫做:

允许细粒度的配置MVC基础服务(Allows fine grained configuration of essential MVC services.)

AddMvcCore扩展方法返回MvcCoreBuilder,MvcCoreBuilder包含IserviceCollection和ApplicationPartManager属性。MvcCoreBuilder和其扩展方法用在AddMvcCore初始化之后做一些额外的配置。实际上,AddMvc方法中首先调用的是AddMvcCore,然后使用MvcCoreBuilder配置额外的服务。

小结

要把上述所有问题都解释清楚不是件容易的事,所以简单总结一下。本文我们分析的是MVC底层代码,主要实现了把MVCs需要的服务添加到IserviceCollection中。通过追踪ApplicationPartManager的创建过程,我们了解了MVC如何一步步创建内部应用模型(ApplicationModel)。虽然我们并没有看到很多实际的功能,但通过对startup的跟踪分析我们发现了许多有趣的东西,这为后续分析奠定了基础。

以上就是我对AddMvcCore的初步探究,共有46个附加项注册到IservceCollection中。下一篇我将进一步分析AddMvc扩展方法。

剖析ASP.NET Core MVC(Part 1)- AddMvcCore(译)的更多相关文章

  1. asp.net core mvc剖析:启动流程

    asp.net core mvc是微软开源的跨平台的mvc框架,首先它跟原有的MVC相比,最大的不同就是跨平台,然后又增加了一些非常实用的新功能,比如taghelper,viewcomponent,D ...

  2. 剖析ASP.NET Core(Part 2)- AddMvc(译)

    原文:https://www.stevejgordon.co.uk/asp-net-core-mvc-anatomy-addmvccore发布于:2017年3月环境:ASP.NET Core 1.1 ...

  3. ASP.NET Core MVC 源码学习:MVC 启动流程详解

    前言 在 上一篇 文章中,我们学习了 ASP.NET Core MVC 的路由模块,那么在本篇文章中,主要是对 ASP.NET Core MVC 启动流程的一个学习. ASP.NET Core 是新一 ...

  4. 在ASP.NET Core MVC中构建简单 Web Api

    Getting Started 在 ASP.NET Core MVC 框架中,ASP.NET 团队为我们提供了一整套的用于构建一个 Web 中的各种部分所需的套件,那么有些时候我们只需要做一个简单的 ...

  5. Bare metal APIs with ASP.NET Core MVC(转)

    ASP.NET Core MVC now provides a true "one asp.net" framework that can be used for building ...

  6. 扒一扒asp.net core mvc控制器的寻找流程

    不太会排版,大家将就看吧. asp.net core mvc和asp.net mvc中都有一个比较有意思的而又被大家容易忽略的功能,控制器可以写在非Web程序集中,比如Web程序集:"MyW ...

  7. 《精通 ASP.NET Core MVC (第七版)》开始发售

    学习 Web 开发技术很难吗?没有适合的学习资料,确实很枯燥,很难.如果有一本如同良师益友的优秀图书辅助,就很轻松,一点也不难! 对于优秀的技术图书来说,必须从读者的角度来编写,而不是从作者的角度来编 ...

  8. ASP.NET Core MVC/WebAPi 模型绑定探索

    前言 相信一直关注我的园友都知道,我写的博文都没有特别枯燥理论性的东西,主要是当每开启一门新的技术之旅时,刚开始就直接去看底层实现原理,第一会感觉索然无味,第二也不明白到底为何要这样做,所以只有当你用 ...

  9. ASP.NET Core MVC 配置全局路由前缀

    前言 大家好,今天给大家介绍一个 ASP.NET Core MVC 的一个新特性,给全局路由添加统一前缀.严格说其实不算是新特性,不过是Core MVC特有的. 应用背景 不知道大家在做 Web Ap ...

随机推荐

  1. 自动关闭IO流-jdk1.7版本

    public static void main(String[] args) throws IOException { try( FileInputStream fis = new FileInput ...

  2. linux命令(33):tail命令

    1.命令格式; tail[必要参数][选择参数][文件] 2.命令功能: 用于显示指定文件末尾内容,不指定文件时,作为输入信息进行处理.常用查看日志文件. 3.命令参数: -f 循环读取 -q 不显示 ...

  3. addeventlistener监听scroll跟touch

    这三个事件只在手机上生效 touchstart,手指开始触屏 touchmove,手指移动 touchend,手指触屏结束   这个事件在手机上跟在pc端都生效 scroll事件     addeve ...

  4. ES6 一种新的数据结构--Map跟Objct的区别

    var map1=new Map(); var keys={key:'val'}; map1.set(keys,'content'); ==> {Object {key: "val&q ...

  5. 半透明AlphaBlend

    AlphaBlend 函数功能:该函数用来显示透明或半透明像素的位图. 函数原型: BOOL AlphaBlend( HDC hdcDest, // handle to destination DC ...

  6. SnagIt截图后无法在编辑器打开,不显示截图内容的解决办法(转)

    方法1: 用SnagIt截图后,弹出的编辑器里不显示刚才截图的内容,解决办法如下: 完全退出Snagit和编辑器,删除以下文件夹: Win7用户 C:\Users\Administrator\AppD ...

  7. windows命令启动mysql

    找到mysql的安装位置,进入bin目录 dos输入  mysql -h localhost -uroot -p   ,在输入密码

  8. Linux上安装MongoDB

    使用本教程使用.rpm 软件包在红帽企业Linux或CentOS Linux版本6和7上安装MongoDB Community Edition . 平台支持 本安装指南仅支持64位系统.详细信息请参见 ...

  9. php jsonp跨域访问

    项目中有个上传图片需要实时预览的,但又是两个系统的访问,故想了一下解决方案: 在新系统中上传图片后处理设置session,旧系统跨域访问获取对应session,进行对应模板预览. 上传图片预览按钮对应 ...

  10. php打开错误日志

    ini_set("display_errors", "On"); error_reporting(E_ALL | E_STRICT);