ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路由参数的形式解析出来供后续请求处理流程使用。但是具体的路由解析功能其实并没有直接实现在RouterMiddleware中间件中,而是由一个Router对象来完成的。[本文已经同步到《ASP.NET Core框架揭秘》之中]

目录
一、IRouter接口
二、RouteContext
三、RouteData
四、Route
五、RouteHandler
总结

一、IRouter接口

Router是我们对所有实现了IRouter接口的所有类型以及对应对象的统称,如下面所示的RouterMiddleware类型定义可以看出,当我们创建这个中间件对象的时候,我们需要指定这个Router。

   1: public class RouterMiddleware

   2: {

   3:     public RouterMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IRouter router);

   4:     public Task Invoke(HttpContext httpContext);

   5: }

除了检验请求是否与自身设置的路由规则相匹配,并在成功匹配的情况下解析出路由参数并指定请求处理器之外,Router的路由解析还为另一个领用场景服务,那就是根据自身的路由规则和提供的参数生成一个URL。我们把这两个方面称为路由的两个“方向”,它们分别对应着RouteDirection枚举的两个选项。针对这两个方向的路由解析分别实现在IRouter的如下两个方法(RouteAsync和GetVirtualPath),目前我们主要关注针对前者的RouteAsync方法。

   1: public interface IRouter

   2: {

   3:     Task RouteAsync(RouteContext context);

   4:     VirtualPathData GetVirtualPath(VirtualPathContext context);

   5: }

   6:  

   7: public enum RouteDirection

   8: {

   9:     IncomingRequest,

  10:     UrlGeneration

  11: }

如上面的代码片段所示,针对请求实施路由解析的RouteAsync方法的输入参数是一个类型为RouteContext的上下文对象。这个RouteContext实际上是对一个HttpContext对象的封装,Router可以利用它得到所有与当前请求相关的信息。如果Router完成路由解析并判断当前请求与自身的路由规则一致,那么它会将解析出来的路由参数转换成一个RouteData并存放到RouteContext对象代表的上下文之中,另一个一并被放入上下文的是代表当前请求处理器的RequestDelegate对象。下图基本上展示了RouteAsync方法试试路由解析的原理。

二、RouteContext

接下来我们来了解一下整个路由解析涉及到了几个核心类型,首先来看看为整个路由解析提供执行上下文的这个RouteContext类型。如上图所示,一个RouteContext上下文包含三个核心对象,一个是代表当前请求上下文的HttpContext对象,对应的属性是HttpContext。它实际上是作为路由解析的输入,并在RouteContext创建的时候以构造函数参数的形式提供。另外两个则是作为路由解析的输出,一个是代表存放路由参数的RouteData对象,另一个则是作为请求处理器的RequestDelegate对象,对应的属性分别是RouteData和Handler。

   1: public class RouteContext

   2: {

   3:     public HttpContext         HttpContext { get; }

   4:     public RouteData           RouteData { get; set; }

   5:     public RequestDelegate     Handler { get; set; }

   6:  

   7:     public RouteContext(HttpContext httpContext);

   8: }

三、RouteData

我们先来看看用于存放路由参数的RouteData类型。从数据来源的角度来讲,路由参数具有两种类型,一种是通过请求路径携带的参数,另一种则是Router对象自身携带的参数,这两种路由参数分别对应着RouteData的Values和DataTonkens属性。至于另一个属性Routers,则保存着实施路由解析并提供路由参数的所有Router对象。

   1: public class RouteData

   2: {

   3:     public RouteValueDictionary     Values { get; }

   4:     public RouteValueDictionary     DataTokens { get; }

   5:     public IList<IRouter>           Routers { get; }

   6: }

   7:  

   8: public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>

   9: {

  10:     public RouteValueDictionary(object values);   

  11:     …

  12: }

从上面的代码片可以看出,RouteData的Values和DataTokens属性的类型都是RouteValueDictionary,它实际上就是一个字典对象而已,其Key和Value分别代表路由参数的名称和值,而作为Key的字符串是不区分大小写的。值得一提的是RouteValueDictionary具有一个特殊的构造函数,作为唯一参数的是一个object类型的对象。如果我们指定的参数是一个RouteValueDictionary对象或者是一个元素类型为KeyValuePair<string, object>>的集合,指定的数据将会作为原始数据源。这个特性体现在如下所示的调试断言中。

   1: var values1 = new RouteValueDictionary() ;

   2: values1.Add("foo", 1);

   3: values1.Add("bar", 2);

   4: values1.Add("baz", 3);

   5:  

   6: var values2 = new RouteValueDictionary(values1);

   7: Debug.Assert(int.Parse(values2["foo"].ToString()) == 1);

   8: Debug.Assert(int.Parse(values2["bar"].ToString()) == 2);

   9: Debug.Assert(int.Parse(values2["baz"].ToString()) == 3);

  10:  

  11: values2 = new RouteValueDictionary(new Dictionary<string, object>

  12: {

  13:     ["foo"] = 1,

  14:     ["bar"] = 2,

  15:     ["baz"] = 3,

  16: });

  17: Debug.Assert(int.Parse(values2["foo"].ToString()) == 1);

  18: Debug.Assert(int.Parse(values2["bar"].ToString()) == 2);

  19: Debug.Assert(int.Parse(values2["baz"].ToString()) == 3);

RouteValueDictionary的这个构造函数的特殊之处其实并不止于此。除了将一个自身具有字典结构的对象作为原始数据源作为参数之外,我们还可以将一个普通的对象作为参数,在此情况下这个构造函数会解析定义在对象自身类型的所有属性定义,并将属性名称和值作为路由参数的名称和值。如下面的代码片段所示,我们创建一个匿名类型的对象并根据它来创建一个RouteValueDictionary,这种方式在MVC应用使用得比较多。

   1: RouteValueDictionary values = new RouteValueDictionary(new 

   2: {

   3:     Foo = 1,

   4:     Bar = 2,

   5:     Baz = 3

   6: });

   7:  

   8: Debug.Assert(int.Parse(values["foo"].ToString()) == 1);

   9: Debug.Assert(int.Parse(values["bar"].ToString()) == 2);

  10: Debug.Assert(int.Parse(values["baz"].ToString()) == 3);

由于RouteData被直接置于RouteContext这上下文中,所以任何可以访问到这个上下文的对象都可以随意地修改其中的路由参数,为了全局对象造成的“数据污染”问题,一种类型与“快照”的策略被应用到RouteData上。具体来说,我们为某个RouteData当前的状态创建一个快照,在后续的某个时刻我们利用这个快照让这个RouteData对象回复到当初的状态。

针对RouteData的这个快照通过具有如下定义的结构RouteDataSnapshot表示。当我们创建这个一个对象的时候,需要指定目标RouteData对象和当前的状态(Values、DataTokens和Routers)。当我们调用其Restore方法的时候,目标RouteData将会恢复到快照创建时的状态。我们可以直接调用RouteData的PushState为它自己创建一个快照。

   1: public struct RouteDataSnapshot

   2: {

   3:     public RouteDataSnapshot(RouteData routeData, RouteValueDictionary dataTokens, IList<IRouter> routers, RouteValueDictionary values);

   4:     public void Restore();

   5: }

   6:  

   7: public class RouteData

   8: {

   9:     public RouteDataSnapshot PushState(IRouter router, RouteValueDictionary values, RouteValueDictionary dataTokens);

  10: }

如下面的代码片段所示,我们创建了一个RouteData对象并调用其PushState方法为它创建了一个快照,调用该方法指定的三个参数均为null。虽然我们在后续步骤中修改了这个RouteData的状态,但是一旦我们调用了这个RouteDataSnapshot对象的Restore方法,这个RouteData将重新恢复到最初的状态。

   1: RouteData routeData = new RouteData();

   2: RouteDataSnapshot snapshot = routeData.PushState(null, null, null);

   3:  

   4: routeData.Values.Add("foo", 1);

   5: routeData.DataTokens.Add("bar", 2);

   6: routeData.Routers.Add(new RouteHandler(null));

   7:  

   8: snapshot.Restore();

   9: Debug.Assert(!routeData.Values.Any());

  10: Debug.Assert(!routeData.DataTokens.Any());

  11: Debug.Assert(!routeData.Routers.Any());

四、Route

除了IRouter这个最为基础的接口之外,路由系统中还定义了额外一些接口和抽象类,其中就包含如下这个INamedRouter接口。这个接口代表一个“具名的”Router,说白了就是这个Router具有一个通过属性Name表示的名字。

   1: public interface INamedRouter : IRouter

   2: {

   3:     string Name { get; }

   4: }

所有具体的Route基本上都最终继承自如下这个抽象基类RouteBase,前面演示实例体现的基于“路由模板”的路由解析策略就体现在这个类型中。如下面的代码片段所示,RouterBase实现了INamedRouter接口,所以它具有一个名称作为标识。它的ParsedTemplate属性返回的RouteTemplate对象表示这个路由模板,它的Defaults和Constraints则是针对以内联方式设置的默认值和约束的解析结果。针对内联约束的解析是利用一个InlineConstraintResolver对象来完成的,RouteBase的ConstraintResolver属性返回就是这么一个对象。RouteData的DataTokens来源于Router对象,对应的属性就是DataTokens。

   1: public abstract class RouteBase : INamedRouter

   2: {

   3:     public virtual string                           Name { get; protected set; }

   4:     public virtual RouteTemplate                    ParsedTemplate { get; protected set; }

   5:     protected virtual IInlineConstraintResolver     ConstraintResolver { get; set; }

   6:     

   7:     public virtual RouteValueDictionary                    DataTokens { get; protected set; }

   8:     public virtual RouteValueDictionary                    Defaults { get; protected set; }   

   9:     public virtual IDictionary<string, IRouteConstraint>   Constraints { get; protected set; } 

  10:  

  11:     public RouteBase(string template, string name, IInlineConstraintResolver constraintResolver, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens);

  12:  

  13:     public virtual Task RouteAsync(RouteContext context);

  14:     protected abstract Task OnRouteMatched(RouteContext context); 

  15:     …

  16: }

对于实现在 RouteAsync方法中针对入栈请求而进行的路由解析,RouteBase中的实现只负责判断是否给定的条件是否满足自身的路由规则,并在规则满足的情况下将解析出来的路由参数保存到RouteContext这个上下文中。至于满足路由规则情况下实施的后续操作, 则实现在抽象方法OnRouteMatched中。

我们在进行路由注册的时候经常使用的Route类型是具有如下定义的Route它是上面这个抽象类RouteBase子类。从如下的代码片段我们不难看出,一个Route对象其实是对另一个Router对象的封装,它自身并没有承载任何具体的路由功能。我们在创建这个Route对象的时候,需要提供这个被封装的Router,这个Router对象在重写的OnRouteMatched方法中被添加到RouteData的Routers属性中,随后它的RouteAsync方法被执行。

   1: public class Route : RouteBase

   2: {

   3:     private readonly IRouter _target;

   4:     public string RouteTemplate

   5:     {

   6:         get { return this.ParsedTemplate.TemplateText; }

   7:     }

   8:  

   9:     public Route(IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) : this(target, routeTemplate, null, null, null, inlineConstraintResolver){}

  10:  

  11:     public Route(IRouter target, string routeTemplate, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver) 

  12:     : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver){}

  13:  

  14:     public Route(IRouter target, string routeName, string routeTemplate, RouteValueDictionary defaults, IDictionary<string, object> constraints, RouteValueDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver) 

  15:        : base(routeTemplate, routeName, inlineConstraintResolver, defaults, constraints, dataTokens)

  16:     {

  17:         _target = target;

  18:     }

  19:  

  20:     protected override Task OnRouteMatched(RouteContext context)

  21:     {

  22:         context.RouteData.Routers.Add(_target);

  23:         return _target.RouteAsync(context);

  24:     }

  25:  

  26:     protected override VirtualPathData OnVirtualPathGenerated(VirtualPathContext context)

  27:     {

  28:         return _target.GetVirtualPath(context);

  29:     }    

  30: }

五、RouteHandler

一个Router在进行针对请求的路由解析过程中需要判断当前请求是否与自身设置的路由规则相匹配,并在匹配情况下将解析出来的路由参数存放与RouteContext这个上下文中,这些都实现在RouteBase这个基类中。由于Route派生于RouteBase,所以它自身也提供了这项基本功能。但是Router还具有另一个重要的任务,那就是在路由匹配情况下将作为处理器的RequestDelegate对象存放到RouteContext上下文中,这个任务最终落实到RouteHandler这个特殊的Router上。

RouteHandler是一种特殊的Router类型,它不仅实现了IRouter接口,还同时实现了另一个IRouteHandler接口,后者提供了一个GetRequestHandler方法根据表示当前请求上下文的HttpContext对象和封装了路由参数的RouteData对象得到一个RequestDelegate对象,后者将会用来处理当前请求。如下面的代码片段所示,我们创建一个RouteHandler对象是需要显式指定一个RequestDelegate对象,GetRequestHandler方法返回的正是这个对象。在实现的RouteAsync方法中,它将这个RequestDelegate赋值给RouteContext的Handler属性。

   1: public class RouteHandler : IRouteHandler, IRouter

   2: {

   3:     private readonly RequestDelegate _requestDelegate;

   4:  

   5:     public RouteHandler(RequestDelegate requestDelegate)

   6:     {

   7:         _requestDelegate = requestDelegate;

   8:     }

   9:  

  10:     public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData)

  11:     {

  12:         return _requestDelegate;

  13:     }    

  14:  

  15:     public Task RouteAsync(RouteContext context)

  16:     {

  17:         context.Handler = _requestDelegate;

  18:         return Task.CompletedTask;

  19:     }

  20: }

  21:  

  22: public interface IRouteHandler

  23: {

  24:     RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData);

  25: }

基类RouteBase能够确定当前请求是否与自身设置的路由规则相匹配,并在匹配的情况下设置路由参数,而RouteHandler只提供设置请求处理器的功能,但是一个真正的Router必须同时具有这两项功能,那么后者究竟是怎样一个对象呢?我们在上面介绍继承自RouteBase的Route类型时,我们说一个Route对象是对另一个Router对象的封装,那么被封装的Router如果是一个RouteHanlder,那么这个Route对象不就具有完整的路由解析功能了吗?

总结

我们介绍了一系列与Router相关的接口和类,包括IRouter、INameRouter和IRouteHandler接口,抽象类RouteBase,以及两个具体的Route和RouteHandler类性。这些与Router相关额接口和类性具有如下图所示的关系。


ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系
ASP.NET Core的路由[2]:路由系统的核心对象——Router
ASP.NET Core的路由[3]:Router的创建者——RouteBuilder
ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件
ASP.NET Core的路由[5]:内联路由约束的检验

ASP.NET Core的路由[2]:路由系统的核心对象——Router的更多相关文章

  1. 路由系统的核心对象——Router

    路由系统的核心对象--Router ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路 ...

  2. asp.net core mvc 中间件之路由

    asp.net core mvc 中间件之路由 路由中间件 首先看路由中间件的源码 先用httpContext实例化一个路由上下文,然后把中间件接收到的路由添加到路由上下文的路由集合 然后把路由上下文 ...

  3. ASP.NET Core框架深度学习(二) 管道对象

    4.HttpContext 第一个对象 我们的ASP.NET Core Mini由7个核心对象构建而成.第一个就是大家非常熟悉的HttpContext对象,它可以说是ASP.NET Core应用开发中 ...

  4. ASP.NET Core中使用自定义路由

    上一篇文章<ASP.NET Core中使用默认MVC路由>提到了如何使用默认的MVC路由配置,通过这个配置,我们就可以把请求路由到Controller和Action,通常情况下我们使用默认 ...

  5. ASP.NET Core Blazor Webassembly 之 路由

    web最精妙的设计就是通过url把多个页面串联起来,并且可以互相跳转.我们开发系统的时候总是需要使用路由来实现页面间的跳转.传统的web开发主要是使用a标签或者是服务端redirect来跳转.那今天来 ...

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

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

  7. 【转】ASP.NET Core MVC 配置全局路由前缀

    本文地址:http://www.cnblogs.com/savorboard/p/dontnet-IApplicationModelConvention.html作者博客:Savorboard 前言 ...

  8. (9)ASP.NET Core 中的MVC路由二

    1.URL生成 MVC应用程序可以使用路由的URL生成功能,生成指向操作(Action)的URL链接. IUrlHelper 接口用于生成URL,是MVC与路由之间的基础部分.在控制器.视图和视图组件 ...

  9. (8)ASP.NET Core 中的MVC路由一

    1.前言 ASP.NET Core MVC使用路由中间件来匹配传入请求的URL并将它们映射到操作(Action方法).路由在启动代码(Startup.Configure方法)或属性(Controlle ...

随机推荐

  1. Java 征途:行者的地图

    前段时间应因缘梳理了下自己的 Java 知识体系, 成文一篇望能帮到即将走进或正在 Java 世界跋涉的程序员们. 第一张,基础图 大约在 2003 年我开始知道 Java 的(当时还在用 Delph ...

  2. 在docker中运行ASP.NET Core Web API应用程序(附AWS Windows Server 2016 widt Container实战案例)

    环境准备 1.亚马逊EC2 Windows Server 2016 with Container 2.Visual Studio 2015 Enterprise(Profresianal要装Updat ...

  3. BootStrap_02之全局样式及组件

    1.BootStrap指定的四种屏幕尺寸: ①超大PC屏幕--lg(large):w>=1200px: ②中等PC屏幕--md(medium):1200px>w>=992px: ③P ...

  4. 了解PHP中的Array数组和foreach

    1. 了解数组 PHP 中的数组实际上是一个有序映射.映射是一种把 values 关联到 keys 的类型.详细的解释可参见:PHP.net中的Array数组    . 2.例子:一般的数组 这里,我 ...

  5. .NET平台开源项目速览(17)FluentConsole让你的控制台酷起来

    从该系列的第一篇文章 .NET平台开源项目速览(1)SharpConfig配置文件读写组件 开始,不知不觉已经到第17篇了.每一次我们都是介绍一个小巧甚至微不足道的.NET平台的开源软件,或者学习,或 ...

  6. SQL Server-聚焦在视图和UDF中使用SCHEMABINDING(二十六)

    前言 上一节我们讨论了视图中的一些限制以及建议等,这节我们讲讲关于在UDF和视图中使用SCHEMABINDING的问题,简短的内容,深入的理解,Always to review the basics. ...

  7. 12、Struts2表单重复提交

    什么是表单重复提交 表单的重复提交: 若刷新表单页面, 再提交表单不算重复提交. 在不刷新表单页面的前提下: 多次点击提交按钮 已经提交成功, 按 "回退" 之后, 再点击 &qu ...

  8. MVC CodeFirst简单的创建数据库(非常详细的步骤)

       最近在学习MVC的开发,相信有过开发经验的人初学一个新的框架时候的想法跟我一样最关心的就是这个框架如何架构,每个架构如何分工,以及最最关键的就是如何与数据库通信,再下来才是学习基础的页面设计啊等 ...

  9. Linux常用命令操作

    系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS ...

  10. Git初探--笔记整理和Git命令详解

    几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...