ASP.NET Core MVC 源码学习:详解 Action 的匹配

前言

上一篇 文章中,我们已经学习了 ASP.NET Core MVC 的启动流程,那么 MVC 在启动了之后,当请求到达过来的时候,它是怎么样处理的呢? 又是怎么样把我们的请求准确的传达到我们的 Action 上呢? 那么,在这边文章中,我们一起跟踪源码看一下,框架都做了些什么东西。

Getting Started

我们知道,Startup.cs 中的 Configure(IApplicationBuilder app) 中,我们使用 app.UseMvc()

在 UseMVC() 代码执行的过程中,它可以接收一个 Action<IRouteBuilder> 形式的委托,我们使用这个委托可以进行自定义路由的配置,默认情况下,我们一般会如下进行配置:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

或者是你使用默认的 app.UseMvcWithDefaultRoute(),这个扩展方法在内部已经帮你做了上述代码的内容。

那我们今天就从这个 Route 的配置开始看起吧。

RouteContext 如何初始化?

在 IRouteBuilder 通过配置 IRouteBuilder,IRouteBuilder 在 Build() 之后会得到 Router 会得到 IRouter


public static IApplicationBuilder UseMvc(
    this IApplicationBuilder app,
    Action<IRouteBuilder> configureRoutes)
{

    // ......
    
    var routes = new RouteBuilder(app)
    {
        DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(),
    };

    configureRoutes(routes);

    routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));

    return app.UseRouter(routes.Build());
}

上面的代码有两个地方需要注意的。

第一个地方是 DefaultHandler,可以看到默认配置下,MVC 程序从 DI 中获取 MvcRouteHandler 路由处理程序来作为路由的默认处理程序。

第二个地方是 AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices)
,那么这个地方是干嘛的呢?

CreateAttributeMegaRoute 它返回了一个 IRouter ,主要是用来处理带 RouteAttribute 标记的 Action,我们来看一下这个方法:


public static IRouter CreateAttributeMegaRoute(IServiceProvider services)
{
    return new AttributeRoute(
        services.GetRequiredService<IActionDescriptorCollectionProvider>(),
        services,
        actions => 
        {
            var handler = services.GetRequiredService<MvcAttributeRouteHandler>();
            handler.Actions = actions;
            return handler;
        });
}

在方法内部,new 了一个 AttributeRoute 返回了回去,大家可以看到有一个参数 actions,它使用的是 MvcAttributeRouteHandler 这个处理程序,说明在实际调用过程中使用的是 MvcAttributeRouteHandler 进行的路由处理。

OK,我们总结一下关于 MVC 自己的几个路由处理程序,还是用一个图比较容易看的清楚,幸运的是,MVC 一共就这3个路由处理程序,我们已经全部接触到了。

MVC 框架针对于 IRouter 接口的实现有以下三个:

250417-20170327154104998-2053909747.png

提前告诉你,最左边绿色的那个 AttributeRoute 其实只是一个包装,在内部也是通过 MvcAttributeRouteHandler 或者 MvcRouteHandler 进行的处理。那么,现在关于路由的处理程序只剩下了两个,他们分别是:

默认处理程序: MvcRouteHandler,用来处理约定的 Action。

注解处理程序: MvcAttributeRouteHandler ,用来处理注解(Attribute)路由。

细心的同学可能注意到了, MvcAttributeRouteHandlerMvcRouteHandler 多了一个 Actions : ActionDescriptor[]属性。

我们再看一下这两个处理程序的 RouteAsync 方法,这个方法是路由组件的入口方法,我们通过一个对比工具来看一下两者之间的差距。

250417-20170327165845983-1432547048.png

图片看不清楚可以新标签打开

可以看到,这两个 RouteAsync 主要有两处差距,第一处就是 SelectBestCandidate 这个函数第二个参数

ActionDescriptor SelectBestCandidate(RouteContext context, IReadOnlyList<ActionDescriptor> candidates)
MvcRouteHandler:

250417-20170328101141967-1822557971.png

在这个流程中,显示调用了 IActionSelect 接口中的 SelectCandidates() 用来找到所有符合条件的候选 Action,然后调用了 SelectBestCandidate 找出最佳的一个。

程序走到这里,这里会有两个重点的地方,或者叫有疑问的地方?

1、 程序集中定义的 Action 是怎么找到的?

要想找到程序定义的所有 Action,那么首先需要找到 Controller,在上一篇文章中我们已经知道了有一个 MVC 程序用来管理 AssemblyPart 的东西叫 ApplicationPartManager ,它的里面存储了所有 MVC 框架在启动的时候加载的所有程序集,那么我们可以从这个程序集中找到需要的 Controller。下面这个流程图显示了查找Controller 的流程:

250417-20170329105655092-1818156607.png

GetControllerTypes 返回的是一个 IEnumerable<TypeInfo> 的集合,有了 Controller 之后,MVC 框架使用了一个对象来包装 Controller,因为在后续的流程中,除了需要 Controller 之外还需要其他的一些东西,比如 Filter, ApiExplorer 等。

ApplicationModel

ApplicationModel 就是MVC框架用来包装 ControllerFilter , ApiExplorer 等的一个Model 对象,我们来看一下它的定义:


public class ApplicationModel : IPropertyModel, IFilterModel, IApiExplorerModel
{
    public ApplicationModel()
    {
        ApiExplorer = new ApiExplorerModel();
        Controllers = new List<ControllerModel>();
        Filters = new List<IFilterMetadata>();
        Properties = new Dictionary<object, object>();
    }

    public ApiExplorerModel ApiExplorer { get; set; }
    public IList<ControllerModel> Controllers { get; private set; }
    public IList<IFilterMetadata> Filters { get; private set; }
    public IDictionary<object, object> Properties { get; }
}

ApplicationModel 里面关于 Controller 的包装是一个 IList<ControllerModel>,看一下 ControllerModel 的定义:


public class ControllerModel : ICommonModel, IFilterModel, IApiExplorerModel
{

    //......
    
    public IList<ActionModel> Actions { get; }
    public ApiExplorerModel ApiExplorer { get; set; }
    public ApplicationModel Application { get; set; }
    public IReadOnlyList<object> Attributes { get; }
    MemberInfo ICommonModel.MemberInfo => ControllerType;
    string ICommonModel.Name => ControllerName;
    public string ControllerName { get; set; }
    public TypeInfo ControllerType { get; }
    public IList<PropertyModel> ControllerProperties { get; }
    public IList<IFilterMetadata> Filters { get; }
    public IDictionary<string, string> RouteValues { get; }
    public IDictionary<object, object> Properties { get; }
    public IList<SelectorModel> Selectors { get; }
}

在 ASP.NET Core MVC 框架中,ApplicationModel 有下面几个提供者,他们用于初始化整个 ApplicationModel 的各个部分,我们还是分别看一下吧。

250417-20170329113253358-2047455213.png

AuthorizationApplicationModelProvider: 处理认证相关业务逻辑,在它的Executing方法中会将 AuthorizeFilter,AllowAnonymousFilter 等过滤器添加到 ApplicationModelProviderContext 里面的 ApplicationModel 里。

DefaultApplicationModelProvider:初始化 ControllerModel, 添加 Controller 相关的各种信息,添加用户自定义 Filter,遍历 ControllerTypes : 创建 ControllerModel --> 初始化Properties --> 初始化Parameters

CorsApplicationModelProvider:跨域资源相关逻辑,添加CorsAuthorizationFilterFactory,DisableCorsAuthorizationFilter,CorsAuthorizationFilterFactory,DisableCorsAuthorizationFilter等过滤器。

TempDataApplicationModelProvider: 添加 SaveTempDataPropertyFilterFactory 过滤器,存储Controller中的TempData信息,注意 TempDataAttribute 修饰的属性只能是基元类型或字符串。

构建ApplicationModel

MVC 框架通过 ControllerActionDescriptorProvider 中的 BuildModel() 这个方法进行 ApplicationModel 的构建:


internal protected ApplicationModel BuildModel()
{
    var controllerTypes = GetControllerTypes();
    var context = new ApplicationModelProviderContext(controllerTypes);

    for (var i = 0; i < _applicationModelProviders.Length; i++)
    {
        _applicationModelProviders[i].OnProvidersExecuting(context);
    }

    for (var i = _applicationModelProviders.Length - 1; i >= 0; i--)
    {
        _applicationModelProviders[i].OnProvidersExecuted(context);
    }

    return context.Result;
}

现在,我们已经有一个完整的 ApplicationModel 对象了。

有了 ApplicationModel 对象之后,会再进行一次约定的应用。比如以下Action重写路由的情况或者配置多个路由的情况。

2、ActionDescriptorCollection是怎么创建的?

ControllerActionDescriptor构建

ControllerActionDescriptor的构建是基于ApplicationModel对象的,下面我就画了一个流程图用来展示构建 ControllerActionDescriptor 的整个过程,就不过多描述了。

250417-20170330092816670-1185001736.png

截止到目前,我们会得到一个 IEnumerable<ControllerActionDescriptor> 集合对象。

在有了 ControllerActionDescriptor 之后,ActionDescriptorCollectionProvider 会提供一个属性,

public ActionDescriptorCollection ActionDescriptors
{
    get
    {
        if (_collection == null)
        {
            UpdateCollection();
        }

        return _collection;
    }
}

在这个属性中使用了 UpdateCollection 这个方法来更新 ActionDescriptorCollection


private void UpdateCollection()
{
    var context = new ActionDescriptorProviderContext();

    for (var i = 0; i < _actionDescriptorProviders.Length; i++)
    {
        _actionDescriptorProviders[i].OnProvidersExecuting(context);
    }

    for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)
    {
        _actionDescriptorProviders[i].OnProvidersExecuted(context);
    }

    _collection = new ActionDescriptorCollection(
        new ReadOnlyCollection<ActionDescriptor>(context.Results),
        Interlocked.Increment(ref _version));
}

OK , 现在我们有了 ActionDescriptorCollection , 之后的流程就比较简单了,但是会涉及到几个算法。

接下来,轮到 ActionSelectorDecisionTreeProvider 上场了,它主要是把 ActionDescriptorCollection,组装成为一个 IActionSelectionDecisionTree 对象以便于后续的查找匹配工作, IActionSelectionDecisionTree 的数据结构是一个多叉树,组装过程是使用了一个深度优先的递归算法。

我们回到起点,继续看这张图:

250417-20170328101141967-1822557971.png

现在 SelectCandidates 你应该能够看懂了:

public IReadOnlyList<ActionDescriptor> SelectCandidates(RouteContext context)
{
    //IActionSelectionDecisionTree 对象
    var tree = _decisionTreeProvider.DecisionTree;
    
    //使用的是一个多叉树查找算法,关于算法可以看我这篇博文:
    //http://www.cnblogs.com/savorboard/p/6582399.html
    return tree.Select(context.RouteData.Values);
}

接下来就是 SelectBestCandidates 这个流程:

1、遍历 Action 列表,评估 Action 的相关约束,返回匹配的 ActionDescriptor 列表。

2、从匹配的 ActionDescriptor 列表中返回最佳的 Action 列表,注意这里这个方法 SelectBestActions,它是一个虚方法,默认是没有实现的会直接返回上一步的结果,也就是说用户可以通过重写这个方法来自定义一些Action匹配规则。

3、如果SelectBestActions 返回的是一个ActionDescriptor,则直接返回,当路由系统匹配到多个 Action 的时候,那么 MVC 需要从这些 Action 候选者中选中最佳的哪一个,当两个动作通过路由匹配时,MVC必须消除歧义以选择“最佳”候选者,否则抛出 AmbiguousActionException 异常

最终 SelectBestCandidates 会返回一个 ActionDescriptor ,即需要执行的 Action。

后续流程的执行,我又画了一个图来表示,希望能够更加清晰一些:

250417-20170327220352342-1246687513.png

终于讲解结束了,心好累,如果你认为本篇文章对你有帮助的话,顺手点个【推荐】吧。

MvcAttributeRouteHandler:

下面是MvcAttributeRouteHandlerRouteAsync

250417-20170328101424904-575995496.png

可以看到,在 MvcAttributeRouteHandler 中,少了 SelectCandidates() 这个流程,取而代之的是用 Actions 的属性参数。 这个Actions 就比较简单了,就是MVC框架启动的时候配置的IRouter Action。

然后就是 SelectBestCandidates 这个流程了,参考上文的流程吧,都一样。

总结

本文详细描述了 MVC 在 Request 到达的时候是怎么样通过自定义的路由处理程序来选择一个Action 的,并且讲解了其中的过程。

如果你对 .NET Core 感兴趣可以关注我,我会定期在博客分享关于 .NET Core 的学习心得,如果你认为本篇文章对你有帮助的话,谢谢你的【推荐】。


本文地址:http://www.cnblogs.com/savorboard/p/aspnetcore-mvc-routing-action.html
作者博客:Savorboard
欢迎转载,请在明显位置给出出处及链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在线考试系统是一种环保的考试形式,所有的步骤都在电脑上完成。通过对国内高校对题库、考试、练习、考试数据分析、档案管理等的需求,从基本信息、试题建设、题库规划、移动练习、智能组卷、自定义输出、智能排考、数据分析等进行了全流程设计。用户可通过流程定义将现有的管理流程移植到考试系统中,也可自定义管理流程,从而解决学校教考分离的难点和痛点。 现在流行的考试系统都是基于云架构设计,引入了大量的国际先进的信息处理技术,并结合移动互联,支持目前流行的大部分音视频和图片格式,同时可对数学公式、化学公式直接进行处理,可实现传统考试到无纸化考试的完整切换。 在线考试系统可实现智能组卷、智能排考、智能阅卷等功能,用户可实时进行不同程度的干预和调整,从而使整个过程更符合用户要求。一线老师只负责按照考纲教学和试题库建设,考试管理人员负责组织试题建设和从试题库中抽取试卷考试,考试评价由系统根据一定的评价模型自动生成,从而实现了分散建设、集中管理和统一应用的目标。考场抽题组卷智能化、主客观题作答无纸化、主观题教师阅卷网络化、考试质量分析自动化,大大降低了考试过程的人为干预程度,保证了考试效果和教学评价的客观性和公正性。 任课老师可随时安排开放练习和阶段性测试,并通过后台随时查看学生练习情况和知识掌握情况,据此实时调整教学安排,从而使教学更有针对性。学生可通过移动终端随时进行课程练习和自测,从而即时进行知识巩固并且随时掌握自己对知识的掌握程度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值