Asp.Net Core EndPoint 终结点路由工作原理解读
一、背景
在本打算写一篇关于identityserver4
的文章时候,却发现自己对endpoint
-终结点路由还不是很了解,故暂时先放弃了identityserver4
的研究和编写;所以才产生了今天这篇关于endpoint
(终结点路由) 的文章。
还是跟往常一样,打开电脑使用强大的google 和百度搜索引擎查阅相关资料,以及打开asp.net core 3.1 的源代码进行拜读,同时终于在我的实践及测试中对endpoint
有了不一样的认识,说到这里更加敬佩微软对asp.net core 3.x 的框架中管道模型的设计。
我先来提出以下几个问题:
- 当访问一个web 应用地址时,asp.net core 是怎么执行到
controller
的action
的呢? -
endpoint
跟普通路由又存在着什么样的关系? -
userouting()
、useauthorization()
、userendpoints()
这三个中间件的关系是什么呢? - 怎么利用
endpoint
终结者路由来拦截action 的执行并且记录相关操作日志?(时间有限,下一篇文章再来分享整理)
二、拜读源码解惑
startup
代码
我们先来看一下startup
中简化版的代码,代码如下:
public void configureservices(iservicecollection services) { services.addcontrollers(); } public void configure(iapplicationbuilder app, iwebhostenvironment env) { app.userouting(); app.useauthorization(); app.useendpoints(endpoints => { endpoints.mapcontrollers(); }); }
程序启动阶段:
- 第一步:执行services.addcontrollers()
将controller
的核心服务注册到容器中去 - 第二步:执行app.userouting()
将endpointroutingmiddleware
中间件注册到http管道中 - 第三步:执行app.useauthorization()
将authorizationmiddleware
中间件注册到http管道中 - 第四步:执行app.useendpoints(encpoints=>endpoints.mapcontrollers())
有两个主要的作用:
调用endpoints.mapcontrollers()
将本程序集定义的所有controller
和action
转换为一个个的endpoint
放到路由中间件的配置对象routeoptions
中
将endpointmiddleware
中间件注册到http管道中
app.userouting()
源代码如下:
public static iapplicationbuilder userouting(this iapplicationbuilder builder) { if (builder == null) { throw new argumentnullexception(nameof(builder)); } verifyroutingservicesareregistered(builder); var endpointroutebuilder = new defaultendpointroutebuilder(builder); builder.properties[endpointroutebuilder] = endpointroutebuilder; return builder.usemiddleware<endpointroutingmiddleware>(endpointroutebuilder); }
endpointroutingmiddleware
中间件代码如下:
internal sealed class endpointroutingmiddleware { private const string diagnosticsendpointmatchedkey = "microsoft.aspnetcore.routing.endpointmatched"; private readonly matcherfactory _matcherfactory; private readonly ilogger _logger; private readonly endpointdatasource _endpointdatasource; private readonly diagnosticlistener _diagnosticlistener; private readonly requestdelegate _next; private task<matcher> _initializationtask; public endpointroutingmiddleware( matcherfactory matcherfactory, ilogger<endpointroutingmiddleware> logger, iendpointroutebuilder endpointroutebuilder, diagnosticlistener diagnosticlistener, requestdelegate next) { if (endpointroutebuilder == null) { throw new argumentnullexception(nameof(endpointroutebuilder)); } _matcherfactory = matcherfactory ?? throw new argumentnullexception(nameof(matcherfactory)); _logger = logger ?? throw new argumentnullexception(nameof(logger)); _diagnosticlistener = diagnosticlistener ?? throw new argumentnullexception(nameof(diagnosticlistener)); _next = next ?? throw new argumentnullexception(nameof(next)); _endpointdatasource = new compositeendpointdatasource(endpointroutebuilder.datasources); } public task invoke(httpcontext httpcontext) { // there's already an endpoint, skip maching completely var endpoint = httpcontext.getendpoint(); if (endpoint != null) { log.matchskipped(_logger, endpoint); return _next(httpcontext); } // there's an inherent race condition between waiting for init and accessing the matcher // this is ok because once `_matcher` is initialized, it will not be set to null again. var matchertask = initializeasync(); if (!matchertask.iscompletedsuccessfully) { return awaitmatcher(this, httpcontext, matchertask); } var matchtask = matchertask.result.matchasync(httpcontext); if (!matchtask.iscompletedsuccessfully) { return awaitmatch(this, httpcontext, matchtask); } return setroutingandcontinue(httpcontext); // awaited fallbacks for when the tasks do not synchronously complete static async task awaitmatcher(endpointroutingmiddleware middleware, httpcontext httpcontext, task<matcher> matchertask) { var matcher = await matchertask; await matcher.matchasync(httpcontext); await middleware.setroutingandcontinue(httpcontext); } static async task awaitmatch(endpointroutingmiddleware middleware, httpcontext httpcontext, task matchtask) { await matchtask; await middleware.setroutingandcontinue(httpcontext); } } [methodimpl(methodimploptions.aggressiveinlining)] private task setroutingandcontinue(httpcontext httpcontext) { // if there was no mutation of the endpoint then log failure var endpoint = httpcontext.getendpoint(); if (endpoint == null) { log.matchfailure(_logger); } else { // raise an event if the route matched if (_diagnosticlistener.isenabled() && _diagnosticlistener.isenabled(diagnosticsendpointmatchedkey)) { // we're just going to send the httpcontext since it has all of the relevant information _diagnosticlistener.write(diagnosticsendpointmatchedkey, httpcontext); } log.matchsuccess(_logger, endpoint); } return _next(httpcontext); } // initialization is async to avoid blocking threads while reflection and things // of that nature take place. // // we've seen cases where startup is very slow if we allow multiple threads to race // while initializing the set of endpoints/routes. doing cpu intensive work is a // blocking operation if you have a low core count and enough work to do. private task<matcher> initializeasync() { var initializationtask = _initializationtask; if (initializationtask != null) { return initializationtask; } return initializecoreasync(); } private task<matcher> initializecoreasync() { var initialization = new taskcompletionsource<matcher>(taskcreationoptions.runcontinuationsasynchronously); var initializationtask = interlocked.compareexchange(ref _initializationtask, initialization.task, null); if (initializationtask != null) { // this thread lost the race, join the existing task. return initializationtask; } // this thread won the race, do the initialization. try { var matcher = _matcherfactory.creatematcher(_endpointdatasource); // now replace the initialization task with one created with the default execution context. // this is important because capturing the execution context will leak memory in asp.net core. using (executioncontext.suppressflow()) { _initializationtask = task.fromresult(matcher); } // complete the task, this will unblock any requests that came in while initializing. initialization.setresult(matcher); return initialization.task; } catch (exception ex) { // allow initialization to occur again. since datasources can change, it's possible // for the developer to correct the data causing the failure. _initializationtask = null; // complete the task, this will throw for any requests that came in while initializing. initialization.setexception(ex); return initialization.task; } } private static class log { private static readonly action<ilogger, string, exception> _matchsuccess = loggermessage.define<string>( loglevel.debug, new eventid(1, "matchsuccess"), "request matched endpoint '{endpointname}'"); private static readonly action<ilogger, exception> _matchfailure = loggermessage.define( loglevel.debug, new eventid(2, "matchfailure"), "request did not match any endpoints"); private static readonly action<ilogger, string, exception> _matchingskipped = loggermessage.define<string>( loglevel.debug, new eventid(3, "matchingskipped"), "endpoint '{endpointname}' already set, skipping route matching."); public static void matchsuccess(ilogger logger, endpoint endpoint) { _matchsuccess(logger, endpoint.displayname, null); } public static void matchfailure(ilogger logger) { _matchfailure(logger, null); } public static void matchskipped(ilogger logger, endpoint endpoint) { _matchingskipped(logger, endpoint.displayname, null); } } }
我们从它的源码中可以看到,endpointroutingmiddleware
中间件先是创建matcher
,然后调用matcher.matchasync(httpcontext)
去寻找endpoint,最后通过httpcontext.getendpoint()
验证了是否已经匹配到了正确的endpoint
并交个下个中间件继续执行!
app.useendpoints()
源代码
public static iapplicationbuilder useendpoints(this iapplicationbuilder builder, action<iendpointroutebuilder> configure) { if (builder == null) { throw new argumentnullexception(nameof(builder)); } if (configure == null) { throw new argumentnullexception(nameof(configure)); } verifyroutingservicesareregistered(builder); verifyendpointroutingmiddlewareisregistered(builder, out var endpointroutebuilder); configure(endpointroutebuilder); // yes, this mutates an ioptions. we're registering data sources in a global collection which // can be used for discovery of endpoints or url generation. // // each middleware gets its own collection of data sources, and all of those data sources also // get added to a global collection. var routeoptions = builder.applicationservices.getrequiredservice<ioptions<routeoptions>>(); foreach (var datasource in endpointroutebuilder.datasources) { routeoptions.value.endpointdatasources.add(datasource); } return builder.usemiddleware<endpointmiddleware>(); } internal class defaultendpointroutebuilder : iendpointroutebuilder { public defaultendpointroutebuilder(iapplicationbuilder applicationbuilder) { applicationbuilder = applicationbuilder ?? throw new argumentnullexception(nameof(applicationbuilder)); datasources = new list<endpointdatasource>(); } public iapplicationbuilder applicationbuilder { get; } public iapplicationbuilder createapplicationbuilder() => applicationbuilder.new(); public icollection<endpointdatasource> datasources { get; } public iserviceprovider serviceprovider => applicationbuilder.applicationservices; }
代码中构建了defaultendpointroutebuilder
终结点路由构建者对象,该对象中存储了endpoint
的集合数据;同时把终结者路由集合数据存储在了routeoptions
中,并注册了endpointmiddleware
中间件到http管道中;endpoint
对象代码如下:
/// <summary> /// represents a logical endpoint in an application. /// </summary> public class endpoint { /// <summary> /// creates a new instance of <see cref="endpoint"/>. /// </summary> /// <param name="requestdelegate">the delegate used to process requests for the endpoint.</param> /// <param name="metadata"> /// the endpoint <see cref="endpointmetadatacollection"/>. may be null. /// </param> /// <param name="displayname"> /// the informational display name of the endpoint. may be null. /// </param> public endpoint( requestdelegate requestdelegate, endpointmetadatacollection metadata, string displayname) { // all are allowed to be null requestdelegate = requestdelegate; metadata = metadata ?? endpointmetadatacollection.empty; displayname = displayname; } /// <summary> /// gets the informational display name of this endpoint. /// </summary> public string displayname { get; } /// <summary> /// gets the collection of metadata associated with this endpoint. /// </summary> public endpointmetadatacollection metadata { get; } /// <summary> /// gets the delegate used to process requests for the endpoint. /// </summary> public requestdelegate requestdelegate { get; } public override string tostring() => displayname ?? base.tostring(); }
endpoint
对象代码中有两个关键类型属性分别是endpointmetadatacollection
类型和requestdelegate
:
- endpointmetadatacollection:存储了
controller
和action
相关的元素集合,包含action
上的attribute
特性数据等 -
requestdelegate
:存储了action 也即委托,这里是每一个controller 的action 方法
再回过头来看看endpointmiddleware
中间件和核心代码,endpointmiddleware
的一大核心代码主要是执行endpoint 的requestdelegate
委托,也即controller
中的action
的执行。
public task invoke(httpcontext httpcontext) { var endpoint = httpcontext.getendpoint(); if (endpoint?.requestdelegate != null) { if (!_routeoptions.suppresscheckforunhandledsecuritymetadata) { if (endpoint.metadata.getmetadata<iauthorizedata>() != null && !httpcontext.items.containskey(authorizationmiddlewareinvokedkey)) { throwmissingauthmiddlewareexception(endpoint); } if (endpoint.metadata.getmetadata<icorsmetadata>() != null && !httpcontext.items.containskey(corsmiddlewareinvokedkey)) { throwmissingcorsmiddlewareexception(endpoint); } } log.executingendpoint(_logger, endpoint); try { var requesttask = endpoint.requestdelegate(httpcontext); if (!requesttask.iscompletedsuccessfully) { return awaitrequesttask(endpoint, requesttask, _logger); } } catch (exception exception) { log.executedendpoint(_logger, endpoint); return task.fromexception(exception); } log.executedendpoint(_logger, endpoint); return task.completedtask; } return _next(httpcontext); static async task awaitrequesttask(endpoint endpoint, task requesttask, ilogger logger) { try { await requesttask; } finally { log.executedendpoint(logger, endpoint); } } }
疑惑解答:
1. 当访问一个web 应用地址时,asp.net core 是怎么执行到controller
的action
的呢?
答:程序启动的时候会把所有的controller 中的action 映射存储到routeoptions
的集合中,action 映射成endpoint
终结者 的requestdelegate
委托属性,最后通过useendpoints
添加endpointmiddleware
中间件进行执行,同时这个中间件中的endpoint
终结者路由已经是通过routing
匹配后的路由。
2. endpoint
跟普通路由又存在着什么样的关系?
答:ednpoint
终结者路由是普通路由map 转换后的委托路由,里面包含了路由方法的所有元素信息endpointmetadatacollection
和requestdelegate
委托。
3. userouting()
、useauthorization()
、useendpoints()
这三个中间件的关系是什么呢?
答:userouing
中间件主要是路由匹配,找到匹配的终结者路由endpoint
;useendpoints
中间件主要针对userouting
中间件匹配到的路由进行 委托方法的执行等操作。useauthorization
中间件主要针对 userouting
中间件中匹配到的路由进行拦截 做授权验证操作等,通过则执行下一个中间件useendpoints()
,具体的关系可以看下面的流程图:
上面流程图中省略了一些部分,主要是把userouting
、useauthorization
、useendpoint
这三个中间件的关系突显出来。
最后我们可以在userouting() 和useendpoint() 注册的http 管道之间 注册其他牛逼的自定义中间件,以实现我们自己都有的业务逻辑
如果您觉的不错,请微信扫码关注 【dotnet 博士】公众号,后续给您带来更精彩的分享
以上如果有错误的地方,请大家积极纠正,谢谢大家的支持!!