剖析ASP.NET Core(Part 4)- 調用MVC中間件(譯)

原文:https://www.stevejgordon.co.uk/invoking-mvc-middleware-asp-net-core-anatomy-part-4 
發佈於:2017年5月
環境:ASP.NET Core 1.1web

本系列前三篇文章咱們研究了AddMvcCore,AddMvc和UseMvc做爲程序啓動的一部分所發生的事情。一旦MVC服務和中間件註冊到咱們的ASP.NET Core應用程序中,MVC就能夠開始處理HTTP請求。json

本文我想介紹當一個請求流入MVC中間件時所發生的初始步驟。這是一個至關複雜的領域,要分開來敘述。我將它拆分紅我認爲合理的流程代碼,忽略某些行爲分支和細節,讓本文容易理解。一些我忽略的實現細節我會重點指出,並在之後的文章中論述。api

和先前同樣,我使用原始的基於project.json(1.1.2)的MVC源碼,由於我尚未找到一種可靠的方法來調試MVC源碼,尤爲是包含其餘組件如路由。數組

好了讓咱們開始,看看MVC如何經過一個有效路由來匹配一個請求,而且最終執行一個可處理請求的動做(action)。快速回顧一下, ASP.NET Core程序在Startup.cs文件中配置了中間件管道(middleware pipeline),它定義了請求處理的流程。每一箇中間件將被按照必定順序調用,直到某個中間件肯定能提供適當的響應。服務器

MvcSandbox項目的配置方法以下:mvc

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    app.UseDeveloperExceptionPage();
    app.UseStaticFiles();
    loggerFactory.AddConsole();
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

假設以前的中間件(UseDeveloperExceptionPage,UseStaticFiles)都沒有處理請求,咱們經過調用UseMvc來到達MVC管道和中間件。一旦請求到達MVC管道,咱們碰到的中間件就是 RouterMiddleware。它的調用方法以下:app

public async Task Invoke(HttpContext httpContext)
{
    var context = new RouteContext(httpContext);
    context.RouteData.Routers.Add(_router);

    await _router.RouteAsync(context);

    if (context.Handler == null)
    {
        _logger.RequestDidNotMatchRoutes();
        await _next.Invoke(httpContext);
    }
    else
    {
        httpContext.Features[typeof(IRoutingFeature)] = new RoutingFeature()
        {
            RouteData = context.RouteData,
        };

        await context.Handler(context.HttpContext);
    }
}

Invoke所作的第一件事是將當前的HttpContext對象傳遞給構造函數,構造一個新的RouteContext。框架

public RouteContext(HttpContext httpContext)
{
    HttpContext = httpContext;
    RouteData = new RouteData();
}

HttpContext做爲參數傳遞給RouteContext,而後生成一個新的RouteData實例對象。async

返回Invoke方法,注入的IRouter(本例是在UseMvc設置期間建立的RouteCollection)被添加到RouteContext.RouteData對象上的IRouter對象列表中。值得強調的是RouteData對象爲其集合使用了延遲初始化模式,只有在它們被調用是才分配它們。這種模式體現了在如ASP.NET Core等大型框架中必須考慮的性能。ide

例如,下面是Routers如何定義屬性:

public IList<IRouter> Routers
{
    get
    {
        if (_routers == null)
        {
            _routers = new List<IRouter>();
        }

        return _routers;
    }
}

第一次訪問該屬性時,一個新的List將分配和存儲到一個內部字段。

返回Invoke方法,在RouteCollection上調用RouteAsync:

public async virtual Task RouteAsync(RouteContext context)
{
    // Perf: We want to avoid allocating a new RouteData for each route we need to process.
    // We can do this by snapshotting the state at the beginning and then restoring it
    // for each router we execute.
    var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null);

    for (var i = 0; i < Count; i++)
    {
        var route = this[i];
        context.RouteData.Routers.Add(route);

        try
        {
            await route.RouteAsync(context);

            if (context.Handler != null)
            {
                break;
            }
        }
        finally
        {
            if (context.Handler == null)
            {
                snapshot.Restore();
            }
        }
    }
}

首先RouteAsync經過RouteCollection建立一個RouteDataSnapshot。如註釋所示,不是每次路由處理都會分配一個RouteData對象。爲避免這種狀況,RouteData對象的快照會被建立一次,並容許每次迭代時重置它。這是ASP.NET Core團隊對性能考慮的另外一個例子。

snapshot經過調用RouteData類中的PushState實現:

public RouteDataSnapshot PushState(IRouter router, RouteValueDictionary values, RouteValueDictionary dataTokens)
{
    // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in
    // Array.CopyTo inside the List(IEnumerable<T>) constructor.
    List<IRouter> routers = null;
    var count = _routers?.Count;
    if (count > 0)
    {
        routers = new List<IRouter>(count.Value);
        for (var i = 0; i < count.Value; i++)
        {
            routers.Add(_routers[i]);
        }
    }

    var snapshot = new RouteDataSnapshot(
        this,
        _dataTokens?.Count > 0 ? new RouteValueDictionary(_dataTokens) : null, 
        routers,
        _values?.Count > 0 ? new RouteValueDictionary(_values) : null);

    if (router != null)
    {
        Routers.Add(router);
    }

    if (values != null)
    {
        foreach (var kvp in values)
        {
            if (kvp.Value != null)
            {
                Values[kvp.Key] = kvp.Value;
            }
        }
    }

    if (dataTokens != null)
    {
        foreach (var kvp in dataTokens)
        {
            DataTokens[kvp.Key] = kvp.Value;
        }
    }

    return snapshot;
}

首先建立一個List<IRoute>。爲了儘量的保持性能,只有在包含RouteData路由器的私有字段(_routers)中至少有一個IRouter時,纔會分配一個列表。若是是這樣,將使用正確的大小(特定大小)來建立一個新的列表,避免內部Array.CopyTo調用時改變底層數組的大小。從本質上講,這個方法如今有一個複製的RouteData的內部IRouter列表實例。

接下來 RouteDataSnapshot對象被建立。RouteDataSnapshot定義爲結構體(struct)。它的構造函數簽名以下所示:

public RouteDataSnapshot(
  RouteData routeData, 
  RouteValueDictionary dataTokens, 
  IList<IRouter> routers, 
  RouteValueDictionary values)

RouteCollection爲全部參數調用PushState,其值爲空值。在使用非空IRoute參數調用PushState方法的狀況下,它會被添加到路由器列表中。值和DataTokens以相同的方式處理。若是PushState參數中包含任何參數,則會更新RouteData上的Values和DataTokens屬性中的相應項。

最後,snapshot返回到RouteCollection中的RouteAsync。

接下來一個for循環開始,直到達到屬性數量值。 Count只是暴露了RouteCollection上的Routers(List <IRouter>)數量。

在循環內部,它首先經過值循環(value of the loop)得到一個route。以下:

public IRouter this[int index]
{
    get { return _routes[index]; }
}

這只是從列表中返回特定索引的IRouter。在MvcSandbox示例中,索引爲0的第一個IRouter是AttributeRoute。

在Try / Finally塊中,在IRouter(AttributeRoute)上調用RouteAsync方法。咱們最終但願找到一個匹配路由數據(route data)的合適的Handler(RequestDelegate)。

咱們將在後面的文章中深刻研究AttributeRoute.RouteAsync方法內部發生的事情,由於在那裏發生了不少事情,目前咱們尚未在MvcSandbox中定義任何AttributeRoutes。在咱們的例子中,由於沒有定義AttributeRoutes,因此Handler值保持爲空。

在finally塊內部,當Handler爲空時,在RouteDataSnapshot上調用Restore方法。此方法將在建立快照時將RouteData對象恢復到其狀態。因爲RouteAsync方法在處理過程當中可能已經修改了RouteData,所以能夠確保咱們回到對象的初始狀態。

在MvcSandbox示例中,列表中的第二個IRouter是名爲「default」的路由,它是Route的一個實例。這個類不覆蓋基類上的RouteAsync方法,所以將調用RouteBase抽象類中的代碼。

public virtual Task RouteAsync(RouteContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    EnsureMatcher();
    EnsureLoggers(context.HttpContext);

    var requestPath = context.HttpContext.Request.Path;

    if (!_matcher.TryMatch(requestPath, context.RouteData.Values))
    {
        // If we got back a null value set, that means the URI did not match
        return TaskCache.CompletedTask;
    }

    // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all
    // created lazily.
    if (DataTokens.Count > 0)
    {
        MergeValues(context.RouteData.DataTokens, DataTokens);
    }

    if (!RouteConstraintMatcher.Match(
        Constraints,
        context.RouteData.Values,
        context.HttpContext,
        this,
        RouteDirection.IncomingRequest,
        _constraintLogger))
    {
        return TaskCache.CompletedTask;
    }
    _logger.MatchedRoute(Name, ParsedTemplate.TemplateText);

    return OnRouteMatched(context);
}

首先調用私有方法EnsureMatcher,以下所示:

private void EnsureMatcher()
{
    if (_matcher == null)
    {
        _matcher = new TemplateMatcher(ParsedTemplate, Defaults);
    }
}

這個方法將實例化一個新的TemplateMatcher,傳入兩個參數。一樣,這彷佛是一個分配優化(allocation optimisation),只有在傳遞給構造函數的屬性可用時,纔會建立此對象。

若是你想知道這些屬性設置在哪裏,是發生在RouteBase類的構造函數內部。這個構造函數是在默認路由被調用時,由MvcSandbox啓動類的配置方法調用UseMvc擴展方法中的MapRoute而建立的。

RouteBase.RouteAsync方法內部,下一步調用的是EnsureLoggers:

private void EnsureLoggers(HttpContext context)
{
    if (_logger == null)
    {
        var factory = context.RequestServices.GetRequiredService<ILoggerFactory>();
        _logger = factory.CreateLogger(typeof(RouteBase).FullName);
        _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName);
    }
}

此方法從RequestServices獲取ILoggerFactory實例,並使用它來設置兩個ILogger。第一個是RouteBase類自己,第二個將由RouteConstraintMatcher使用。

接下來它存儲一個局部變量,該變量持有從HttpContext中獲取的請求的路徑。

再往下,調用TemplateMatcher中的TryMatch,傳入請求路徑以及任何路由數據。咱們將在另外一篇文章中深刻分析TemplateMathcer內部。如今,假設TryMatch返回true,咱們的例子中就是這種狀況。若是不匹配(TryMatch返回false)將返回TaskCache.CompletedTask,只是將任務(Task)設置爲完成。

再往下,若是有任何DataTokens(RouteValueDictionary對象)設置,則context.RouteData.DataTokens會按需更新。正如註釋中提到的,只有在值被實際更新的時候纔會這樣作。RouteData中的屬性DataTokens只是在其第一次被調用(lazily instantiated 延遲實例化)時建立。所以,在沒有更新的值時調用它可能會冒險分配一個新的不須要的RouteValueDictionary。

在咱們使用的MvcSandbox中,沒有DataTokens,因此MergeValues不會被調用。但爲了完整性,它的代碼以下:

private static void MergeValues(RouteValueDictionary destination, RouteValueDictionary values)
{
    foreach (var kvp in values)
    {
        // This will replace the original value for the specified key.
        // Values from the matched route will take preference over previous
        // data in the route context.
        destination[kvp.Key] = kvp.Value;
    }
}

當被調用時,它從RouteBase類的DataTokens參數中的RouteValueDictionary中循環任何值,並更新context.RouteData.DataTokens屬性上匹配鍵的目標值。

接下來,返回RouteAsync方法,RouteConstraintMatcher.Match被調用。這個靜態方法遍歷任何傳入的IRouteContaints,並肯定它們是否所有知足條件。Route constraints容許使用附加的匹配規則。例如,路由參數能夠被約束爲僅使用整數。咱們的示列中沒有約束,所以返回true。咱們將在另外一篇文章中查看帶有約束的URL。

ILoger的擴展方法MatchedRoute生成了一個logger項。這是一個有趣的模式,能夠根據特定需求重複使用更復雜的日誌消息格式。

這個類的代碼:

internal static class TreeRouterLoggerExtensions
{
    private static readonly Action<ILogger, string, string, Exception> _matchedRoute;

    static TreeRouterLoggerExtensions()
    {
        _matchedRoute = LoggerMessage.Define<string, string>(
            LogLevel.Debug,
            1,
            "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'.");
    }

    public static void MatchedRoute(
        this ILogger logger,
        string routeName,
        string routeTemplate)
    {
        _matchedRoute(logger, routeName, routeTemplate, null);
    }
}

當TreeRouterLoggerExtensions類第一次被構造時定義了一個action代理,該代理定義了日誌消息該如何格式化。

當MatchRoute擴展方法被調用時,將路由名和模板字符串做爲參數傳遞。而後將它們傳遞給_matchedRoute動做(Action)。該動做使用提供的參數建立調試級別的日誌項。在visual studio中調試時,你會看到它出如今輸出(output)窗口中。

返回RouteAsync;OnRouteMatched方法被調用。這被定義爲RouteBase上的一個抽象方法,因此實現來自繼承類。在咱們的例子中,它是Route類。OnRouteMatched的重寫方法以下:

protected override Task OnRouteMatched(RouteContext context)
{
    context.RouteData.Routers.Add(_target);
    return _target.RouteAsync(context);
}

其名爲_target的IRouter字段被添加到context.RouteData.Routers列表中。在這種狀況下,它是MVC的默認處理程序MvcRouteHandler。

而後在MvcRouteHandler上調用RouteAsync方法。該方法的細節至關重要,因此我保留下來做爲將來討論的主題。總之,MvcRouteHandler.RouteAsync將嘗試創建一個合適的處理請求的操做方法。有一件重要的事情要知道,當一個動做被發現時,RouteContext上的Handler屬性是經過lambda表達式定義的。咱們能夠再次深刻該代碼,但總結一下,RequestDelegate是一個接受HttpContext而且能夠處理請求的函數。

回到RouterMiddleware上的invoke方法,咱們可能已經有一個MVC已肯定的處理程序(handler)能夠處理請求。若是沒有,則調用_logger.RequestDidNotMatchRoutes()。這是咱們前面探討的logger擴展風格的另外一個例子。他將添加一條調試信息,指示路由不匹配。在這種狀況下,ASP.NET中間件管道中的下一個RequestDelegate被調用,由於MVC已經肯定它不能處理請求。

在客戶端web/api應用程序的常規配置中,在UseMvc以後不會再有任何中間件的定義。在這種狀況下,但咱們到達管道末端時,ASP.NET Core返回一個默認的404未找到的HTTP狀態碼響應。

在咱們有一個能夠處理請求路由的處理程序的狀況下,咱們將進入Invoke方法else塊。

一個新的RoutingFeature被實例化並被添加到HttpContext的Features集合中。簡單地說,features(特性)是ASP.NET Core的一個概念,它容許服務器定義接收請求的特徵。這包括數據在整個請求生命週期中的流動。像RouterMiddleware這樣的中間件能夠添加/修改特徵集合,並能夠將其用做經過請求傳遞數據的機制。在咱們的例子中,RouteContext中的RouteData做爲IRoutingFeature定義的一部分添加,以便其餘中間件和請求處理程序可使用它。

而後該方法調用Handler RequestDelegate,它將最終經過適當的MVC動做(action)來處理請求。到此爲止,本文就要結束了。接下來會發生什麼,以及我跳過的項目將構成本系列的下一部分。

小結:

咱們已經看到MVC是如何做爲中間件管道的一部分被調用的。一旦調用,MVC RouterMiddleware肯定MVC是否知道如何處理傳入的請求路徑和值。若是MVC有一個可用於處理請求中的路由和路由數據的動做,則使用此處理程序來處理請求並提供響應。

相關文章
相關標籤/搜索