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