ASP.NET Core的路由[2]:路由系统的核心对象——Router

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]:内联路由约束的检验

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-11-08 20:31:02

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

ASP.NET Core MVC 配置全局路由前缀_实用技巧

ASP.NET Core MVC 配置全局路由前缀 前言 大家好,今天给大家介绍一个 ASP.NET Core MVC 的一个新特性,给全局路由添加统一前缀.严格说其实不算是新特性,不过是Core MVC特有的. 应用背景 不知道大家在做 Web Api 应用程序的时候,有没有遇到过这种场景,就是所有的接口都是以 /api 开头的,也就是我们的api 接口请求地址是像这样的: http://www.example.com/api/order/333 或者是这样的需求 http://www.exa

ASP.NET Core的路由[5]:内联路由约束的检验

当某个请求能够被成功路由的前提是它满足某个Route对象设置的路由规则,具体来说,当前请求的URL不仅需要满足路由模板体现的路径模式,请求还需要满足Route对象的所有约束.路由系统采用IRouteConstraint接口来表示路由约束,所以我们在接下来的内容中将路由约束统称为RouteConstraint. 在大部分情况下,约束都是针对路由模板中定义的某个路由参数,其目的在于验证URL携带的某部分的内容是否有效.不过也有一些约束与路由参数无关,这些约束规范往往是除URL之前的其他请求元素,比如

ASP.NET Core的路由[1]:注册URL模式与HttpHandler的映射关系

ASP.NET Core的路由是通过一个类型为RouterMiddleware的中间件来实现的.如果我们将最终处理HTTP请求的组件称为HttpHandler,那么RouterMiddleware中间件的意义在于实现请求路径与对应HttpHandler之间的映射关系.对于传递给RouterMiddleware中间件的每一个请求,它会通过分析请求URL的模式并选择并提取对应的HttpHandler来处理该请求.除此之外,请求的URL还会携带相应参数,该中间件在进行路由解析过程中还会根据生成相应的路

ASP.NET Core的路由[3]:Router的创建者——RouteBuilder

在<注册URL模式与HttpHandler的映射关系>演示的实例中,我们总是利用一个RouteBuilder对象来为RouterMiddleware中间件创建所需的Router对象,接下来我们就着重来介绍这个对象.RouteBuilder是我们对所有实现了IRouteBuilder接口的所有类型以及对应对象的统称.[本文已经同步到<ASP.NET Core框架揭秘>之中] 目录 一.RouteBuilder 二.RouteCollection 三.多个Route共享同一个Handl

ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件

虽然ASP.NET Core应用的路由是通过RouterMiddleware这个中间件来完成的,但是具体的路由解析功能都落在指定的Router对象上,不过我们依然有必要以代码实现的角度来介绍一下这个中间件.在这之前,我们先来认识一个特殊的特性.[本文已经同步到<ASP.NET Core框架揭秘>之中] 让RouterMiddleware中间件委托Router完整整个路由工作之后,解析出来的路由参数会以一个RouteData对象的形式存储在RouteContext上下文中.但是RouteCont

ASP.NET Core框架揭秘(持续更新中…)

之前写了一系列关于.NET Core/ASP.NET Core的文章,但是大都是针对RC版本.到了正式的RTM,很多地方都发生了改变,所以我会将之前发布的文章针对正式版本的.NET Core 1.0进行改写.除此之外,我还会撰写一系列与此相关的文章,这些文章以ASP.NET Core为核心,我个人将它们分成三个主要的部分,即编程基础.支撑框架和管道详解.其中编程基础主要涉及与ASP.NET Core独特的编程模型和相关编程技巧.支撑框架则介绍支撑ASP.NET Core的多个独立的框架,比如依赖

ASP.NET Core 数据保护(Data Protection)中篇_实用技巧

前言  上篇主要是对 ASP.NET Core 的 Data Protection 做了一个简单的介绍,本篇主要是介绍一下API及使用方法.  API 接口  ASP.NET Core Data Protectio 主要对普通开发人员提供了两个接口,IDataProtectionProvider 和 IDataProtector.  我们先看一下这两个接口的关系: namespace Microsoft.AspNetCore.DataProtection { // // 摘要: // An in

ASP.NET Core管道深度剖析(3):管道是如何处理HTTP请求的?

我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成,所以从总体设计来讲是非常简单的,但是就具体的实现来说,由于其中涉及很多对象的交互,我想很少人能够地把它弄清楚.为了让读者朋友们能够更加容易地理解管道处理HTTP请求的总体流程,我们根据真实管道的实现原理再造了一个"模拟管道"并在此管道上开发了一个发布图片的应用,这篇文章旨在为你讲述管道是如何处理HTTP请求的 目录 一.HttpApplication     FeatureCollection     Ho

ASP.NET Core MVC/WebAPi如何构建路由?

前言 本节我们来讲讲ASP.NET Core中的路由,在讲路由之前我们首先回顾下之前所讲在ASP.NET Core中的模型绑定这其中有一个问题是我在项目当中遇见的,我们下面首先来看看这个问题. 回顾ASP.NET Core模型绑定 我们有这样一个场景:修改个人资料中的各个属性,此时每个属性的值的类型肯定是不一样的,所以我们将值定义为object,如下model. public class BlogViewModel { public string prop { get; set; } publi