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

来源:http://www.cnblogs.com/lookerblue/archive/2017/12/17/8052152.html
-Advertisement-
Play Games

原文:https://www.stevejgordon.co.uk/invoking-mvc-middleware-asp-net-core-anatomy-part-4 發佈於:2017年5月環境:ASP.NET Core 1.1 本系列前三篇文章我們研究了AddMvcCore,AddMvc和Us ...


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

本系列前三篇文章我們研究了AddMvcCore,AddMvc和UseMvc作為程式啟動的一部分所發生的事情。一旦MVC服務和中間件註冊到我們的ASP.NET Core應用程式中,MVC就可以開始處理HTTP請求。

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

和先前一樣,我使用原始的基於project.json(1.1.2)的MVC源碼,因為我還沒有找到一種可靠的方法來調試MVC源碼,尤其是包含其他組件如路由。

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

MvcSandbox項目的配置方法如下:

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。它的調用方法如下:

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實例對象。

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

例如,下麵是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有一個可用於處理請求中的路由和路由數據的動作,則使用此處理程式來處理請求並提供響應。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 背水一戰 Windows 10 之 控制項(自定義控制項): 自定義控制項的 Layout 系統, 自定義控制項的控制項模板和事件處理的相關知識點 ...
  • 前言 ASP.NET Core 2.0 怎麼發佈到Ubuntu伺服器?又如何在伺服器上配置使用ASP.NET Core網站綁定到指定的功能變數名稱,讓外網用戶可以訪問呢? 步驟 第1步:準備工作 一臺Liunx伺服器:筆者用的是【[搬瓦工][1]】的VPS伺服器(CDN加速,支持支付寶,多機房選擇) 低配版 ...
  • 閱讀目錄 前言 成熟的解決方案 剖析 性能測試 結語 一、前言 在上一篇分散式系統系列中《分散式系統中的必備良藥 —— 服務治理》中闡述了服務治理的一些概念,那麼與服務治理配套的必然會涉及到RPC框架。在當前互聯網的大背景下,RPC的運用應該大家或多或少都有涉及,國內外的RPC框架也是百花齊放。那麼 ...
  • [MY NOTE] [轉載請註明出處] Reference Source: http://www.albahari.com/valuevsreftypes.aspx http://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in ...
  • Asp.net 中的狀態管理維護包含ViewState,cookie,session,application,cache五種方式,以下是它們的一些比較: 1.存在於客戶端還是服務端 客戶端: viewstate、cookie 服務端: session、application、cache *sessi ...
  • 轉載地址:http://gnucto.blog.51cto.com/3391516/998509 Redis與Memcached的區別 傳統MySQL+ Memcached架構遇到的問題 實際MySQL是適合進行海量數據存儲的,通過Memcached將熱點數據載入到cache,加速訪問,很多公司都曾 ...
  • 緩存這種能夠提升指令和數據讀取速度的特性,隨著本地電腦系統向分散式系統的擴展,在分散式計算領域中得到了廣泛的應用,稱為分散式緩存。 緩存這種能夠提升指令和數據讀取速度的特性,隨著本地電腦系統向分散式系統的擴展,在分散式計算領域中得到了廣泛的應用,稱為分散式緩存。 中文名分散式緩存外文名Distr ...
  • 最新在將原來寫的一些webSerivce轉換為WebApi,直接就用了ASP.Net Core 2.0的框架,在使用中,發現的與原有的asp.net不同的地方,通過搜索已經慢慢解決,記錄下來備用。 一、全局配置 在asp.net中,全局變更配置寫在web.config中,如下所示 在ASP.Net ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...