欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Asp.Net Core EndPoint 终结点路由工作原理解读

程序员文章站 2023-01-26 20:54:55
在本打算写一篇关于Identityserver4 的文章时候,却发现自己对EndPoint -终结点路由还不是很了解,故暂时先放弃了IdentityServer4 的研究和编写;所以才产生了今天这篇关于EndPoint (终结点路由) 的文章。 还是跟往常一样,打开电脑使用强大的Google 和百... ......

一、背景

在本打算写一篇关于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(),具体的关系可以看下面的流程图:
Asp.Net Core EndPoint 终结点路由工作原理解读
上面流程图中省略了一些部分,主要是把useroutinguseauthorizationuseendpoint 这三个中间件的关系突显出来。

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

如果您觉的不错,请微信扫码关注 【dotnet 博士】公众号,后续给您带来更精彩的分享
Asp.Net Core EndPoint 终结点路由工作原理解读

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