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]:内联路由约束的检验
ASP.NET Core的路由[2]:路由系统的核心对象——Router的更多相关文章
- 路由系统的核心对象——Router
路由系统的核心对象--Router ASP.NET Core应用中的路由机制实现在RouterMiddleware中间件中,它的目的在于通过路由解析为请求找到一个匹配的处理器,同时将请求携带的数据以路 ...
- asp.net core mvc 中间件之路由
asp.net core mvc 中间件之路由 路由中间件 首先看路由中间件的源码 先用httpContext实例化一个路由上下文,然后把中间件接收到的路由添加到路由上下文的路由集合 然后把路由上下文 ...
- ASP.NET Core框架深度学习(二) 管道对象
4.HttpContext 第一个对象 我们的ASP.NET Core Mini由7个核心对象构建而成.第一个就是大家非常熟悉的HttpContext对象,它可以说是ASP.NET Core应用开发中 ...
- ASP.NET Core中使用自定义路由
上一篇文章<ASP.NET Core中使用默认MVC路由>提到了如何使用默认的MVC路由配置,通过这个配置,我们就可以把请求路由到Controller和Action,通常情况下我们使用默认 ...
- ASP.NET Core Blazor Webassembly 之 路由
web最精妙的设计就是通过url把多个页面串联起来,并且可以互相跳转.我们开发系统的时候总是需要使用路由来实现页面间的跳转.传统的web开发主要是使用a标签或者是服务端redirect来跳转.那今天来 ...
- ASP.NET Core MVC 配置全局路由前缀
前言 大家好,今天给大家介绍一个 ASP.NET Core MVC 的一个新特性,给全局路由添加统一前缀.严格说其实不算是新特性,不过是Core MVC特有的. 应用背景 不知道大家在做 Web Ap ...
- 【转】ASP.NET Core MVC 配置全局路由前缀
本文地址:http://www.cnblogs.com/savorboard/p/dontnet-IApplicationModelConvention.html作者博客:Savorboard 前言 ...
- (9)ASP.NET Core 中的MVC路由二
1.URL生成 MVC应用程序可以使用路由的URL生成功能,生成指向操作(Action)的URL链接. IUrlHelper 接口用于生成URL,是MVC与路由之间的基础部分.在控制器.视图和视图组件 ...
- (8)ASP.NET Core 中的MVC路由一
1.前言 ASP.NET Core MVC使用路由中间件来匹配传入请求的URL并将它们映射到操作(Action方法).路由在启动代码(Startup.Configure方法)或属性(Controlle ...
随机推荐
- 如何选择PHP框架?
PHP是世界上最受欢迎的编程语言之—.最近发布的PHP7令这种服务器的编程语言比以前变得更好,更稳定了. PHP被广泛应用于重大的项目.例如Facebook就是使用PHP来维护和创建它们的内部系统的. ...
- Redis链表实现
链表在 Redis 中的应用非常广泛, 比如列表键的底层实现之一就是链表: 当一个列表键包含了数量比较多的元素, 又或者列表中包含的元素都是比较长的字符串时, Redis 就会使用链表作为列表键的底层 ...
- 解构C#游戏框架uFrame兼谈游戏架构设计
1.概览 uFrame是提供给Unity3D开发者使用的一个框架插件,它本身模仿了MVVM这种架构模式(事实上并不包含Model部分,且多出了Controller部分).因为用于Unity3D,所以它 ...
- 免费道路 bzoj 3624
免费道路(1s 128MB)roads [输入样例] 5 7 21 3 04 5 13 2 05 3 14 3 01 2 14 2 1 [输出样例] 3 2 04 3 05 3 11 2 1 题解: ...
- Android中使用ExpandableListView实现微信通讯录界面(完善仿微信APP)
之前的博文<Android中使用ExpandableListView实现好友分组>我简单介绍了使用ExpandableListView实现简单的好友分组功能,今天我们针对之前的所做的仿微信 ...
- 在Redhat上为.Net 项目构建基于Jenkins + Github + Mono 的持续集成环境
在Redhat enterprise 6.5 的服务器上,为在gutub 上的 .net 项目构建一个持续集成环境,用到了Jenkins和mono.因公司的服务器在内网,访问外网时要通过代理,所以在很 ...
- 扑面而来的碎片--图片3D炸裂效果初体验
之前逛园子的时候看到 ChokCoco 的爆炸效果作品:[BOOM]一款有趣的Javascript动画效果 (大神英文有没有拼错呀←.←),觉得蛮有意思的,效果如下: 不过觉得这个爆炸效果还是偏软了一 ...
- 尝试用canvas写小游戏
还是习惯直接开门见山,这个游戏是有一个老师抓作弊的学生,老师背身,点学生开始加分,老师会不定时回头,如果老师回头还在点学生在,就会被抓住,游戏game over. 1.写游戏首先是loading条,于 ...
- Thread.Sleep(0) vs Sleep(1) vs Yeild
本文将要提到的线程及其相关内容,均是指 Windows 操作系统中的线程,不涉及其它操作系统. 文章索引 核心概念 Thread.Yeild Thread.Sleep(0) Thread. ...
- Android Starting Window(Preview Window)
当打开一个Activity时,如果这个Activity所属的应用还没有在运行,系统会为这个Activity所属的应用创建一个进程,但进程的创建与初始化都需要时间,在这个动作完成之前系统要做什么呢?如果 ...