一、背景

在本打算写一篇关于identityserver4 的文章时候,却发现自己对endpoint -终结点路由还不是很了解,故暂时先放弃了identityserver4 的研究和编写;所以才产生了今天这篇关于endpoint (终结点路由) 的文章。

还是跟往常一样,打开电脑使用强大的google 和百度搜索引擎查阅相关资料,以及打开asp.net core 3.1 的源代码进行拜读,同时终于在我的实践及测试中对endpoint 有了不一样的认识,说到这里更加敬佩微软对asp.net core 3.x 的框架中管道模型的设计。

我先来提出以下几个问题:

  1. 当访问一个web 应用地址时,asp.net core 是怎么执行到controlleraction的呢?
  2. endpoint 跟普通路由又存在着什么样的关系?
  3. userouting()useauthorization()userendpoints() 这三个中间件的关系是什么呢?
  4. 怎么利用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()将本程序集定义的所有controlleraction转换为一个个的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:存储了controlleraction相关的元素集合,包含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 是怎么执行到controlleraction的呢?

答:程序启动的时候会把所有的controller 中的action 映射存储到routeoptions 的集合中,action 映射成endpoint终结者 的requestdelegate 委托属性,最后通过useendpoints 添加endpointmiddleware 中间件进行执行,同时这个中间件中的endpoint 终结者路由已经是通过routing匹配后的路由。

2. endpoint 跟普通路由又存在着什么样的关系?

答:ednpoint 终结者路由是普通路由map 转换后的委托路由,里面包含了路由方法的所有元素信息endpointmetadatacollectionrequestdelegate 委托。

3. userouting()useauthorization()useendpoints() 这三个中间件的关系是什么呢?

答:userouing 中间件主要是路由匹配,找到匹配的终结者路由endpointuseendpoints 中间件主要针对userouting 中间件匹配到的路由进行 委托方法的执行等操作。
useauthorization 中间件主要针对 userouting 中间件中匹配到的路由进行拦截 做授权验证操作等,通过则执行下一个中间件useendpoints(),具体的关系可以看下面的流程图:

上面流程图中省略了一些部分,主要是把useroutinguseauthorizationuseendpoint 这三个中间件的关系突显出来。

最后我们可以在userouting() 和useendpoint() 注册的http 管道之间 注册其他牛逼的自定义中间件,以实现我们自己都有的业务逻辑

如果您觉的不错,请微信扫码关注 【dotnet 博士】公众号,后续给您带来更精彩的分享

以上如果有错误的地方,请大家积极纠正,谢谢大家的支持!!