ASP.NET Web API路由解析
阅读原文时间:2023年07月10日阅读:1

本篇文章比较长,仔细思考阅读下来大约需要15分钟,涉及类图有可能在手机显示不完整,可以切换电脑版阅读。

做.Net有好几年时间了从ASP.NET WebForm到ASP.NET MVC再到ASP.NET WebAPI,我相信这些Web框架很多人普遍都用过,还是比较强大的,只不过随着发展和迭代以及前端的兴起,有的小伙伴可能觉得已经过时了, 因为现在流行的Web框架太多太多,当然不局限于.net,随便张口就来的框架.

  1. Java的Spring & Hibernate

  2. Python的Django

  3. Node的Express和Koa

最重要的不是用了多少,知道多少,而是有多少沉淀,在业务领域虽然可能存在过时了,但是从技术的角度还是有一些东西值得我们学习的,鉴于我是做.Net开发的,所以分享一下自己学习.Net相关的Web框架.


1.ASP.NET管道

ASP.NET管道前面已经有一篇简单介绍的,写的时候可能有些地方没有考虑全面后续有时间会陆续适当修改一下,感兴趣可以看看,欢迎给出反馈和讨论,以至于互相更好的进步.

2.Web API 路由解析

如果单纯从ASP.NET WebAPI来看可能说的比较宽泛,上一篇只分享ASP.NET管道模型,后面发现太单调了,比较抽象,平时工作也用的比较少,不太容易理解,然后就准备将ASP.NET WebAPI这一部分也一起分享出来.

因为可以说Asp.NET WebAPI就是ASP.NET管道的延伸,当然WebAPI也分为2部分

  1. SelfHost:自宿主(独立的一套路由)

  2. WebHost :ASP.NET管道这一套路由

这次我要分享的是基于WebHost模式的,加上平时用的比较多,所以结合着一起分享,至于SelfHost是什么,其实就是自宿主,可以寄宿在Windows服务、Winform程序和控制台程序,不依赖于Asp.net管道,用过WCF的应该知道,有点相似,就不做深入的讨论和探究.

在分享之前,我还是得把我准备要分享内容的大纲列出来,因为这样可以一目了然知道分享知识点的中心思想,也避免了自己在分享过程中跑题

在后面我会使用简称来说明它是什么程序,例如 WebAPI Mvc,因为它的全称大小写都有写的确实有点累……

  1. [Web API 路由注册]

  2. [Web API 路由处理]

  3. [Web API 管道组装]

  4. [Web API 管道扩展]


1.针对于WebAPI 路由注册 和路由处理我会通过阅读源代码以及边说明的的方式来阐述.

2.针对于管道组装和扩展虽然我们在阅读源码时会接触到它,但我仍然会用Demo的方式来说明它,因为我觉得 它设计的真的很棒,忍不住要单独拎出来说一下,虽然有可能把自己讲懵,但我仍然想尝试一下.


1. Web API 路由简介

一个ASP.NET的Web应用具有一个全局的路由表,它是通过一个RouteTable类中的一个类型为RouteCollection的Routes静态属性来表示的.

为什么这么说呢?或许这样说不太好理解,用一个简单的例子来说明

//在WebApiConfig.Register中修改注册路由.
public static void Register(HttpConfiguration config)
        {
            // Web API 路由
            //config.Routes.MapHttpRoute(
            //    name: "DefaultApi",
            //    routeTemplate: "api/{controller}/{Action}/{id}",
            //    defaults: new { id = RouteParameter.Optional }
            //);

            //验证路由是否加到全局路由表
            var ro = new { id = RouteParameter.Optional };
            //获取默认WebAPI路由处理器
            IRouteHandler routeHandler = HttpControllerRouteHandler.Instance;
            RouteValueDictionary defaults = new RouteValueDictionary(ro);
            Route route = new Route("api/{controller}/{Action}/{id}", defaults, routeHandler);
            RouteTable.Routes.Add("DefaultApi", route);
        }

在针对于路由表这一点上面不仅仅只有ASP.NET WebAPI是这么做的,他同样适用于ASP.NET WebForm 和ASP.NET MVC 同样可以直接在应用程序启动时网路由表中直接添加数据,不过此处只为了证实上述描述,在日常开发中是否可以这样用,完全取决于你自己.

思考的问题

1.我们思考RouteCollection为什么是静态的? `

因为静态对象会一直存在内存中,直到程序池下一次回收之前.

2.我们在用WebAPI开发应用接口的的时候大家有没有思考过,我们前端Url请求是怎么到达我们对应的控制器和Action的?

其实是根据路由来控制的,至于怎么控制,怎么实现,我后面会慢慢介绍.

3.既然是路由控制,那怎么路由呢?

先注册路由,再将路由和处理器绑定.

然后用户请求根据请求的Url 匹配对应的处理器,再由处理器进行路由模板规则解析.

再根据解析到的规则找到Contrller和Action.

2. Web API 路由注册

首先声明 特性路由 此次不做分享,后续有机会再补上…

简单的表示如下图,当然其中还有很多东西,只是画的比较简陋,

但是我们作为使用框架的开发人员能看到的就只有很简单的几句代码

,不幸的是连这个代码都是框架生成的。

注册方式

1.我们在web应用程序启动时,首先调用GlobalConfiguration.Configure(WebApiConfig.Register)方法,这个方法接收的参数是一个Action 作为参数,看到Action我们的第一反应这里应该是作为回调,在内部执行,但是给我们预留了扩展空间.

2.转到框架定义的WebApiConfig.Register方法,毫无疑问他有一个HttpConfiguration类型的参数,而这个参数是在执行GlobalConfiguration.Configure时内部传入的.

GlobalConfiguration

1.根据上面的代码,我们打开源码找到GlobalConfiguration这个类来看,他是个静态类,它包含3个重要的静态属性Configuration、DefaultHandler、DefaultServer,这三个属性在静态类被构造时就已经初始化了,在路由注册阶段只介绍Configuration,剩下2个在路由解析部分在分享.

2.第一个属性Configuration正是我们上面执行回调扩展代码需要的参数,这个对象有一个HttpRouteCollection类型的Routes属性,而属性有一个MapHttpRoute扩展方法来往路由表中添加数据,第二张图就是正在注册路由,而我们开发人员做的仅仅只需要在这里设置一下路由规则.

3.我们继续看一下HttpConfiguration类型的Configuration是怎么初始化的,我们先展开它,查看代码看到他是由内部的CreateConfiguration方法创建的,而创建它需要一些额外的类型.

注册流程步骤

1.HttpConfiguration被构造时需要的一些额外类型就是传入一个HostedHttpRouteCollection对象,在内部它将传入的对像赋值给内部的routes属性,供外部使用,走到这里说明在用户注册路由的Register方法中config.Routes是一个HostedHttpRouteCollection对象.

2.接下来我们继续看映射路由的扩展方法MapHttpRoute,上面的标记的这个就是一个继承自HttpRouteCollection的HostedHttpRouteCollection,利用它调用CreateRoute方法返回一个IHttpRoute实例.

3.继续转到HostedHttpRouteCollection类中的CreateRoute内部看到它返回一个HostedHttpRoute而它就是实现IHttpRoute的实例.

4.在HostedHttpRoute构造时,内部有一个Route类型的OriginalRoute属性,它被赋值为继承自(Route:RouteBase)的HttpWebRoute类型.

5.在HttpWebRoute初始构造时传入了一个路由模板和一个IRouteHandler类型的HttpControllerRouteHandler以及一个IHttpRoute类型的HostedHttpRoute,然后程序返回,注意此处的IRouteHandler可以理解为是路由的处理器.

6.返回到MapHttpRoute后对应的IHttpRoute类型的变量route被设置为一个HostedHttpRoute对象,最终在HostedHttpRouteCollection对象上添加 路由模板名字处理程序.

至此我们前面的路由注册部分已经做了一个简单的介绍,下一节进入路由处理,如果在阅读过程中有任何疑问欢迎随时和我讨论, 强烈建议在阅读本篇分享建立了粗略的知识点之后,有时间的话自己先下载WebAPI源码进行阅读,这样可以帮助更好的理解.


3. Web API 路由处理

上一节我们分享了webAPI中路由注册的过程,知道了WebAPI中添加路由映射的方式,最终映射在路由表的是,路由模板和路由处理器组成一个Route对象,然后将它添加进去,接下来这一节就介绍WebAPI程序如何对路由请求进行处理.

在此我们需要把目光转移到ASP.NET管道模型中去,因为请求处理的核心部件是由ASP.NET管道中的一个叫

UrlRoutingModule 的伙计在负责进行着核心的工作.

是的,它是一个 HttpModule 因为它继承自 IHttpModule,并且实现扩展了 HttpApplication 的PostResolveRequestCache事件,而WebAPI后面的一切都是围绕着这个扩展事件展开,同样MVC & WebForm也是如此.

如果您不知道我标记高亮的类和接口是什么,您可能需要去仔细阅读 ASP.NET 管道模型简析这一篇分享.

我们打开UrlRoutingModule类找到PostResolveRequestCache事件订阅,路由处理这一部分全部都是围绕着以下代码展开.

处理流程步骤

简单的描述一下:

  1. 当请求到达时根据上下文找到路由数据RouteData.

  2. 在RouteData中找到路由处理器IRouteHandler,而这个IRouteHandler,正是我们上一节路由注册中步骤的第6步操作中提到的HttpControllerRouteHandler.

  3. 将RouteData和IRouteHandler一起组装成请求上下文RequestContext.

  4. 路由处理器中的GetHttpHandler根据传入的请求上下文,找到一个"请求处理器"IHttpHandler.

  5. 其实最终由请求处理器是一个叫HttpControllerHandler的接管后续的操作,由这里开始正式进入了我们的WebAPI处理管道.

到目前为止,假设我认为您已经清楚了上面的几个概念和描述的基本步骤,接下来我们进入到方法的内部,去看一下究竟是如何一步步做到的。

获取路由对象

1.进入到RouteCollection的GetRouteData方法,首先迭代器中获取继承自RouteBase对象的实例.

2.因为我们在注册路由时已经给定(路由注册流程第5步),毫无疑问类型为RouteBase的current获取到的是一个HttpWebRoute对象,即返回RouteData数据,最终调用的是HttpWebRoute中的重写方法GetRouteData,用类图表示

classDiagram
RouteBase <|-- Route
Route <|-- HttpWebRoute
RouteBase: +GetRouteData()()
class Route{
+GetRouteData()
}
class HttpWebRoute{
+GetRouteData()
}

3.由于HttpWebRoute中在注册路由时,它的HttpRoute属性在构造时指定为HostedHttpRoute类型,那么 GetRouteData方法将会调用标记处,Route中的GetRouteData()方法

4.方法中做法很简单,将当前Route类和注册时传入类型为IRouteHandler的HttpControllerRouteHandler处理器组装成一个RouteData对象返回.

获取HttpHandler

1.接下来就是从处理器中获取请求处理器HttpHandler,我们在上面不止一次说过,路由处理器是一个IRouteHandler类型的HttpControllerRouteHandler实例,那么它是如何返回"请求处理器"的呢?

2.我们看到获取请求处理器的代码很简单,在GetHttpHandler中直接创建了一个对象返回,而此刻的HttpControllerHandler的创建,就是标志着我们的http请求将正式被处理,而它的本质仅仅只是一个HttpHandler.

HttpControllerHandler执行

1.进入HttpControllerHandler内部,我们看到它在构造时接收2个参数,一个是routeData,一个是handler参数,方法执行时将routeData转为HostedHttpRouteData对象,再将handler转化为HttpMessageInvoker对象,然后赋值给内部私有变量_server.

2.看到这个类型为HttpMessageHandler的参数是不是有点眼熟,在路由注册的第3步,我们在GlobalConfiguration类中看到过它.

classDiagram
class GlobalConfiguration{
+Configuration:HttpConfiguration
+DefaultHandler:HttpMessageHandler
+DefaultServer:HttpServer
}

3.现在可能还不知道它的作用,继续往下看,一定会慢慢清晰的,我们知道HttpHandler会被框架默认执行ProcessRequest方法,此处是返回一个Task的异步方法ProcessRequestAsync,在它内部调用了ProcessRequestAsyncCore

4.注意上面标记的 await _server.SendAsync(request, cancellationToken)这行代码,在这里它接收一个请求上下文参数,然后正式开始它的执行.

5.在第1步中看到这个_server是一个GlobalConfiguration.DefaultServer转化后的实例,现在我们需要回到GlobalConfiguration去正式看一下DefaultServer了,因为这一切都与它密不可分.

private static Lazy<HttpServer> _defaultServer = CreateDefaultServer();
public static HttpServer DefaultServer
{
    get { return _defaultServer.Value; }
}

private static Lazy<HttpServer> CreateDefaultServer()
{
    return new Lazy<HttpServer>(() => new
    HttpServer(_configuration.Value, _defaultHandler.Value));
}

6.看了DefaultServer的由来,它是一个继承自DelegatingHandler类型为 HttpServer 的属性,它其实就是我们WebAPI管道的头部,而 HttpServer 中的SendAsync方法就是我们所有请求处理的第一站,此时同样标志着Http请求正式被WebAPI管道接管并开始处理.

4. Web API 管道组装

我们在学习Web API 管道组装之前,需要先认识一些基本的类概念和它们之间的关联,先看下面类图大概扫一眼混个眼熟,因为api管道就是下面几个组成.

classDiagram
HttpMessageHandler <|-- DelegatingHandler
HttpMessageHandler <|-- HttpRoutingDispatcher
DelegatingHandler <|-- HttpServer
HttpRoutingDispatcher <|-- HttpControllerDispatcher
HttpMessageHandler : # SendAsync()

class DelegatingHandler{
- innerHandler:HttpMessageHandler
+ innerHandler:HttpMessageHandler
# SendAsync()
}
class HttpRoutingDispatcher{
# SendAsync()
}
class HttpServer{
Initialize()
SendAsync()
}
class HttpControllerDispatcher{
# SendAsync()
}

当然可能还需要一些设计模式的功底,因为这会使你更加容易理解它的设计思想,在一开始介绍中我说过我担心把自己讲懵,因为我在学习它的时候花了相当长一段时间去看、去理解直到现在我只能说尝试着去分享.

除了上面类图中的我们还需要认识2个类型:

  1. HttpRequestMessage 代表http消息在API请求传递的实例.

  2. HttpResponseMessage 代表http消息在API中被处理后返回传递的实例.

HttpMessageHandler

要认识API管道就必须先了解 HttpMessageHandlerDelegatingHandler 这2个抽象类,我们先介绍HttpMessageHandler,在msdn中官方文档的定义是 "HTTP 消息处理程序的基类",个人觉得可以仅仅把它当做一个约束吧,跟接口一样,只不过比接口和类的关系更加紧密.

1.我们在在类中标记了,它有一个在命名空间内受保护的异步抽象方法,接收HttpRequestMessage,返回一个Task ,在这我们相当于定义了要被api处理消息和返回消息的约束,后面所有派生的类必须遵从这个约定.

DelegatingHandler

DelegatingHandler在msdn文档中的定义是将 HTTP 响应消息的处理委托给另一处理程序(称为“内部处理程序”)的 HTTP 处理程序的类型

从字面上理解就是自己不对消息做任何处理,把处理的动作交给别人,那它是如何将处理动作交给别人的呢?

我们先看一下里面的代码有如下3个特点:

  1. 首先它继承自HttpMessageHandler,实现了SendAsync方法.

  2. 它有一个类型是HttpMessageHandler的公共InnerHandler属性.

  3. 我们看SendAsync方法内部只是调用了InnerHandler属性的SendAsync方法.

上面我们说过HttpMessageHandler是一个"消息处理基类",既然接收基类,那InnerHandler属性可以代表任何继承自HttpMessageHandler的实例.

即说明此处的this.InnerHandler.SendAsync()代表的是HttpMessageHandler或者DelegatingHandler的子类实例中的SendAsync

到这里我只是想尽可能的引导说明它是如何将此处的消息处理委托给别人的,而这正是DelegatingHandler的核心作用,至于它究竟是如何实现委托的呢?

在此处实现思想可以理解为 装饰器模式 其核心做法为 一个对象包含指向另一个对象的引用,并将部分工作委派给引用对象,继承中的对象则继承了父类的行为,它们自己能够完成这些工作.

HttpServer

1.在HttpControllerHandler执行部分中,我们接触过HttpServer并且了解到它继承自DelegatingHandler,知道它的SendAsync方法是被作为HttpControllerHandler(httphandler)执行的入口.

2.可以看到HttpServer构造时内部默认创建了一个HttpRoutingDispatcher类并把它赋值给内部类型为HttpMessageHandler的只读属性_dispatcher

目前我们只需要记住这2步操作就好了,对于HttpRoutingDispatcher内部的一些事情我们下一节会介绍它.

3.在DelegatingHandler中我们知道了它是如何将消息处理委派给别人,如下图我简单绘制了整个API消息管道的类关系.

4.现在我们要思考的是框架如何将这些步骤串联起来,使它有条不紊的执行的呢?现在将目光转到HttpServer中的SendAsync方法,在这里面能找到我们需要的答案.

5.他有一个EnsureInitialized方法,注释告诉我们了它是 请求初始化服务的,如何初始化呢?我们转到里面

6.在这里利用HttpClientFactory.CreatePipeline()方法传入了一个HttpRoutingDispatcher的实例和一个Collection集合,并返回一个HttpMessageHandler给InnerHandler.

这里需要解释并且记录其实就是利用工厂创建一个消息处理实例,然后将父类DelegatingHandler的InnerHandle属性设置为HttpRoutingDispatcher的实例.

7.继续回到SendAsync方法,我们看到它调用了base.SendAsync()也就是调用父类的SendAsync方法,内部竟然是调用innerHandler,也就是说它本身没有处理消息,而是把处理的动作委派给HttpRoutingDispatcher来实现.

HttpRoutingDispatcher 和 HttpControllerDispatcher

上一节我们知道在HttpServer中开始执行消息处理,是由其父类DelegatingHandler将处理动作委派给HttpRoutingDispatcher类的实例方法SendAsync来实现最终调用的,我们现在来看看在HttpRoutingDispatcher中究竟有什么.

5. Web API 管道扩展

如何扩展webAPI管道

1.首先继承自DelegatingHandler

2.重写父类方法

3.在HttpConfiguration类的MessageHandlers注册

按照上面3步操作就可以完成我们自定义对API管道的扩展.

我们下一部分将解释它是如何将我们自定义的管道添加到内置管道的.

注册原理

1.转到管道工厂方法内部,我们看到首先将HttpRoutingDispatcher赋值到局部变量pipeline

2.标记"扩展集合" 参数存储的是自定义管道的集合,内部在注册时首先将集合反转,也就是原来 1 2 3顺序的自定义管道变为3 2 1的顺序.

3.然后开始迭代,将HttpRoutingDispatcher的引用传递给顺序为3的自定义管道.

4.将引用赋值完成后,在将顺序为3的自定义管道赋值给局部变量pipleline,最后返回,以此类推形成了一个委托链,在调用时呈链式调用依次执行.