溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

深入淺析ASP.NET中的Core處理管道

發(fā)布時間:2020-11-23 16:10:18 來源:億速云 閱讀:187 作者:Leah 欄目:開發(fā)技術

這篇文章給大家介紹深入淺析ASP.NET中的Core處理管道,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

在 ASP.NET Core 的管道處理部分,實現(xiàn)思想已經不是傳統(tǒng)的面向對象模式,而是切換到了函數(shù)式編程模式。這導致代碼的邏輯大大簡化,但是,對于熟悉面向對象編程,而不是函數(shù)式編程思路的開發(fā)者來說,是一個比較大的挑戰(zhàn)。

處理請求的函數(shù)

在 ASP.NET Core 中,一次請求的完整表示是通過一個 HttpContext 對象來完成的,通過其 Request 屬性可以獲取當前請求的全部信息,通過 Response 可以獲取對響應內容進行設置。

對于一次請求的處理可以看成一個函數(shù),函數(shù)的處理參數(shù)就是這個 HttpContext 對象,處理的結果并不是輸出結果,結果是通過 Response 來完成的,從程序調度的角度來看,函數(shù)的輸出結果是一個任務 Task。

這樣的話,具體處理 Http 請求的函數(shù)可以使用如下的 RequestDelegate 委托進行定義。

public delegate Task RequestDelegate(HttpContext context);

在函數(shù)參數(shù) HttpContext 中則提供了此次請求的所有信息,context 的 Request 屬性中提供了所有關于該次請求的信息,而處理的結果則在 context 的 Response 中表示。通常我們會修改 Response 的響應頭,或者響應內容來表達處理的結果。

需要注意的是,該函數(shù)的返回結果是一個 Task,表示異步處理,而不是真正處理的結果。

參見:在 Doc 中查看 RequestDelegate 定義

我們從 ASP.NET Core 的源代碼中選取一段作為參考,這就是在沒有我們自定義的處理時,ASP.NET Core 最終的處理方式,返回 404。這里使用函數(shù)式定義。

RequestDelegate app = context =>
{
 // ......

 context.Response.StatusCode = StatusCodes.Status404NotFound;
 return Task.CompletedTask;
};

來源:在 GitHub 中查看 ApplicationBuilder 源碼

把它翻譯成熟悉的方法形式,就是下面這個樣子:

public Task app(HttpContext context)
{
 // ......

 context.Response.StatusCode = StatusCodes.Status404NotFound;
 return Task.CompletedTask;
};

這段代碼只是設置了 Http 的響應狀態(tài)碼為 404,并直接返回了一個已經完成的任務對象。

為了脫離 ASP.NET Core 復雜的環(huán)境,可以簡單地進行后繼的演示,我們自定義一個模擬 HttpContext 的類型 HttpContextSample 和相應的 RequestDelegate 委托類型。

在模擬請求的 HttpContextSample 中,我們內部定義了一個 StringBuilder 來保存處理的結果,以便進行檢查。其中的 Output 用來模擬 Response 來處理輸出。

而 RequestDelegate 則需要支持現(xiàn)在的 HttpContextSample。

using System.Threading.Tasks;
using System.Text;

public class HttpContextSample
{
 public StringBuilder Output { get; set; }
 public HttpContextSample() {
  Output = new StringBuilder();
 }
}
public delegate Task RequestDelegate(HttpContextSample context);

這樣,我們可以定義一個基礎的,使用 RequestDelegate 的示例代碼。

// 定義一個表示處理請求的委托對象
RequestDelegate app = context =>
{
 context.Output.AppendLine("End of output.");
 return Task.CompletedTask;
};

// 創(chuàng)建模擬當前請求的對象
var context1 = new HttpContextSample();
// 處理請求
app(context1);
// 輸出請求的處理結果
Console.WriteLine(context1.Output.ToString());

執(zhí)行之后,可以得到如下的輸出

End of output.

處理管道中間件

所謂的處理管道是使用多個中間件串聯(lián)起來實現(xiàn)的。每個中間件當然需要提供處理請求的 RequestDelegate 支持。在請求處理管道中,通常會有多個中間件串聯(lián)起來,構成處理管道。

深入淺析ASP.NET中的Core處理管道

但是,如何將多個中間件串聯(lián)起來呢?

可以考慮兩種實現(xiàn)方式:函數(shù)式和方法式。

方法式就是再通過另外的方法將注冊的中間件組織起來,構建一個處理管道,以后通過調用該方法來實現(xiàn)管道。而函數(shù)式是將整個處理管道看成一個高階函數(shù),以后通過調用該函數(shù)來實現(xiàn)管道。

方法式的問題是在后繼中間件處理之前需要一個方法,后繼中間件處理之后需要一個方法,這就是為什么 ASP.NET Web Form 有那么多事件的原因。

如果我們只是把后繼的中間件中的處理看成一個函數(shù),那么,每個中間件只需要分成 3 步即可:

  • 前置處理
  • 調用后繼的中間件
  • 后置處理
     

在 ASP.NET Core 中是使用函數(shù)式來實現(xiàn)請求的處理管道的。

在函數(shù)式編程中,函數(shù)本身是可以作為一個參數(shù)來進行傳遞的。這樣可以實現(xiàn)高階函數(shù)。也就是說函數(shù)的組合結果還是一個函數(shù)。

對于整個處理管道,我們最終希望得到的形式還是一個 RequestDelegate,也就是一個對當前請求的 HttpContext 進行處理的函數(shù)。

本質上來講,中間件就是一個用來生成 RequestDelegate 對象的生成函數(shù)。

為了將多個管道中間件串聯(lián)起來,每個中間件需要接收下一個中間件的處理請求的函數(shù)作為參數(shù),中間件本身返回一個處理請求的 RequestDelegate 委托對象。所以,中間件實際上是一個生成器函數(shù)。

使用 C# 的委托表示出來,就是下面的一個類型。所以,在 ASP.NET Core 中,中間件的類型就是這個 Func<T, TResult>。

Func<RequestDelegate, RequestDelegate>

在 Doc 中查看 Func<T, TResult> 的文檔

這個概念比較抽象,與我們所熟悉的面向對象編程方式完全不同,下面我們使用一個示例進行說明。

我們通過一個中間件來演示它的模擬實現(xiàn)代碼。下面的代碼定義了一個中間件,該中間件接收一個表示后繼處理的函數(shù),中間件的返回結果是創(chuàng)建的另外一個 RequestDelegate 對象。它的內部通過調用下一個處理函數(shù)來完成中間件之間的級聯(lián)。

// 定義中間件
Func<RequestDelegate, RequestDelegate> middleware1 = next => {
  // 中間件返回一個 RequestDelegate 對象
 return (HttpContextSample context) => {
  // 中間件 1 的處理內容
  context.Output.AppendLine("Middleware 1 Processing.");

  // 調用后繼的處理函數(shù)
  return next(context);
 };
};

把它和我們前面定義的 app 委托結合起來如下所示,注意調用中間件的結果是返回一個新的委托函數(shù)對象,它就是我們的處理管道。

// 最終的處理函數(shù)
RequestDelegate app = context =>
{
 context.Output.AppendLine("End of output.");
 return Task.CompletedTask;
};

// 定義中間件 1
Func<RequestDelegate, RequestDelegate> middleware1 = next =>
{
 return (HttpContextSample context) =>
 {
  // 中間件 1 的處理內容
  context.Output.AppendLine("Middleware 1 Processing.");

  // 調用后繼的處理函數(shù)
  return next(context);
 };
};

// 得到一個有一個處理步驟的管道
var pipeline1 = middleware1(app);
// 準備一個表示當前請求的對象
var context2 = new HttpContextSample();
// 通過管道處理當前請求
pipeline1(context2);
// 輸出請求的處理結果
Console.WriteLine(context2.Output.ToString());

可以得到如下的輸出

Middleware 1 Processing.
End of output.

繼續(xù)增加第二個中間件來演示多個中間件的級聯(lián)處理。

RequestDelegate app = context =>
{
 context.Output.AppendLine("End of output.");
 return Task.CompletedTask;
};

// 定義中間件 1
Func<RequestDelegate, RequestDelegate> middleware1 = next =>
{
 return (HttpContextSample context) =>
 {
  // 中間件 1 的處理內容
  context.Output.AppendLine("Middleware 1 Processing.");

  // 調用后繼的處理函數(shù)
  return next(context);
 };
};

// 定義中間件 2
Func<RequestDelegate, RequestDelegate> middleware2 = next =>
{

 return (HttpContextSample context) =>
 {
  // 中間件 2 的處理
  context.Output.AppendLine("Middleware 2 Processing.");

  // 調用后繼的處理函數(shù)
  return next(context);
 };
};

// 構建處理管道
var step1 = middleware1(app);
var pipeline2 = middleware2(step1);
// 準備當前的請求對象
var context3 = new HttpContextSample();
// 處理請求
pipeline2(context3);
// 輸出處理結果
Console.WriteLine(context3.Output.ToString());

當前的輸出

Middleware 2 Processing.
Middleware 1 Processing.
End of output.

如果我們把這些中間件保存到幾個列表中,就可以通過循環(huán)來構建處理管道。下面的示例重復使用了前面定義的 app 變量。

List<Func<RequestDelegate, RequestDelegate>> _components
 = new List<Func<RequestDelegate, RequestDelegate>>();
_components.Add(middleware1);
_components.Add(middleware2);

// 構建處理管道
foreach (var component in _components)
{
 app = component(app);
}

// 構建請求上下文對象
var context4 = new HttpContextSample();
// 使用處理管道處理請求
app(context4);
// 輸出處理結果
Console.WriteLine(context4.Output.ToString());

輸出結果與上一示例完全相同

Middleware 2 Processing.
Middleware 1 Processing.
End of output.

但是,有一個問題,我們后加入到列表中的中間件 2 是先執(zhí)行的,而先加入到列表中的中間件 1 是后執(zhí)行的。如果希望實際的執(zhí)行順序與加入的順序一致,只需要將這個列表再反轉一下即可。

// 反轉此列表
_components.Reverse();
foreach (var component in _components)
{
 app = component(app);
}

var context5 = new HttpContextSample();
app(context5);
Console.WriteLine(context5.Output.ToString());

輸出結果如下

Middleware 1 Processing.
Middleware 2 Processing.
End of output.

現(xiàn)在,我們可以回到實際的 ASP.NET Core 代碼中,把 ASP.NET Core 中 ApplicationBuilder 的核心代碼 Build() 方法抽象之后,可以得到如下的關鍵代碼。

注意 Build() 方法就是構建我們的請求處理管道,它返回了一個 RequestDelegate 對象,該對象實際上是一個委托對象,代表了一個處理當前請求的處理管道函數(shù),它就是我們所謂的處理管道,以后我們將通過該委托來處理請求。

public RequestDelegate Build()
{
 RequestDelegate app = context =>
 {
  // ......

  context.Response.StatusCode = StatusCodes.Status404NotFound;
  return Task.CompletedTask;
 };

 foreach (var component in _components.Reverse())
 {
  app = component(app);
 }

 return app;
}

完整的 ApplicationBuilder 代碼如下所示:

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Builder
{
 public class ApplicationBuilder : IApplicationBuilder
 {
  private const string ServerFeaturesKey = "server.Features";
  private const string ApplicationServicesKey = "application.Services";

  private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();

  public ApplicationBuilder(IServiceProvider serviceProvider)
  {
   Properties = new Dictionary<string, object&#63;>(StringComparer.Ordinal);
   ApplicationServices = serviceProvider;
  }

  public ApplicationBuilder(IServiceProvider serviceProvider, object server)
   : this(serviceProvider)
  {
   SetProperty(ServerFeaturesKey, server);
  }

  private ApplicationBuilder(ApplicationBuilder builder)
  {
   Properties = new CopyOnWriteDictionary<string, object&#63;>(builder.Properties, StringComparer.Ordinal);
  }

  public IServiceProvider ApplicationServices
  {
   get
   {
    return GetProperty<IServiceProvider>(ApplicationServicesKey)!;
   }
   set
   {
    SetProperty<IServiceProvider>(ApplicationServicesKey, value);
   }
  }

  public IFeatureCollection ServerFeatures
  {
   get
   {
    return GetProperty<IFeatureCollection>(ServerFeaturesKey)!;
   }
  }

  public IDictionary<string, object&#63;> Properties { get; }

  private T&#63; GetProperty<T>(string key)
  {
   return Properties.TryGetValue(key, out var value) &#63; (T)value : default(T);
  }

  private void SetProperty<T>(string key, T value)
  {
   Properties[key] = value;
  }

  public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
  {
   _components.Add(middleware);
   return this;
  }

  public IApplicationBuilder New()
  {
   return new ApplicationBuilder(this);
  }

  public RequestDelegate Build()
  {
   RequestDelegate app = context =>
   {
    // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
    // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
    var endpoint = context.GetEndpoint();
    var endpointRequestDelegate = endpoint&#63;.RequestDelegate;
    if (endpointRequestDelegate != null)
    {
     var message =
      $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
      $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
      $"routing.";
     throw new InvalidOperationException(message);
    }

    context.Response.StatusCode = StatusCodes.Status404NotFound;
    return Task.CompletedTask;
   };

   foreach (var component in _components.Reverse())
   {
    app = component(app);
   }

   return app;
  }
 }
}

見:在 GitHub 中查看 ApplicationBuilder 源碼

強類型的中間件

函數(shù)形式的中間件使用比較方便,可以直接在管道定義中使用。但是,如果我們希望能夠定義獨立的中間件,使用強類型的類來定義會更加方便一些。

public interface IMiddleware {
 public System.Threading.Tasks.Task InvokeAsync (
  Microsoft.AspNetCore.Http.HttpContext context, 
  Microsoft.AspNetCore.Http.RequestDelegate next);
}

在 Doc 中查看 IMiddleware 定義

我們定義的強類型中間件可以選擇實現(xiàn)裝個接口。

next 表示請求處理管道中的下一個中間件,處理管道會將它提供給你定義的中間件。這是將各個中間件連接起來的關鍵。

如果當前中間件需要將請求繼續(xù)分發(fā)給后繼的中間件繼續(xù)處理,只需要調用這個委托對象即可。否則,應用程序針對該請求的處理到此為止。

例如,增加一個可以添加自定義響應頭的中間件,如下所示:

using System.Threading.Tasks;

public class CustomResponseHeader: IMiddleware 
{
  // 使用構造函數(shù)完成服務依賴的定義
  public CustomResponseHeader()
  {
  }

  public Task InvodeAsync(HttpContextSample context, RequestDelegate next)
  {
    context.Output.AppendLine("From Custom Middleware.");

    return next(context);
  }
}

這更好看懂了,可是它怎么變成那個 Func<RequestDelegate, RequestDelegate> 呢?

在演示程序中使用該中間件。

List<Func<RequestDelegate, RequestDelegate>> _components
  = new List<Func<RequestDelegate, RequestDelegate>>();
_components.Add(middleware1);
_components.Add(middleware2);

var middleware3 = new CustomResponseHeader();
Func<RequestDelegate, RequestDelegate> middleware3 = next =>
{
  return (HttpContextSample context) =>
  {
    // 中間件 3 的處理
    var result = middleware3.InvodeAsync(context, next);
    return result;
  };
};
_components.Add(middleware3);

這樣開發(fā)者可以使用熟悉的對象方式開發(fā)中間件,而系統(tǒng)內部自動根據(jù)你的定義,生成出來一個 Func<RequestDelegate, RequestDelegate> 形式的中間件。

ASP.NET Core 使用該類型中間件的形式如下所示,這是提供了一個方便的擴展方法來完成這個工作。

 .UseMiddleware<CustomResponseHeader>();

按照約定定義中間件

除了實現(xiàn) IMiddleware 這個接口,還可以使用約定方式來創(chuàng)建中間件。

按照約定定義中間件不需要實現(xiàn)某個預定義的接口或者繼承某個基類,而是需要遵循一些約定即可。約定主要體現(xiàn)在如下幾個方面:

  • 中間件需要一個公共的有效構造函數(shù),該構造函數(shù)必須包含一個類型為 RequestDelegate 類型的參數(shù)。它代表后繼的中間件處理函數(shù)。構造函數(shù)不僅可以包含任意其它參數(shù),對 RequestDelegate 參數(shù)出現(xiàn)的位置也沒有任何限制。
  • 針對請求的處理實現(xiàn)再返回類型為 Task 的 InvokeAsync() 方法或者同步的 Invoke() 方法中,方法的第一個參數(shù)表示當前的請求上下文 HttpContext 對象,對于其他參數(shù),雖然約定并未進行限制,但是由于這些參數(shù)最終由依賴注入框架提供,所以,相應的服務注冊必須提供。
     

構造函數(shù)和 Invoke/InvokeAsync 的其他參數(shù)由依賴關系注入 (DI) 填充。

using System.Threading.Tasks;

public class RequestCultureMiddleware {
  private readonly RequestDelegate _next;

  public RequestCultureMiddleware (RequestDelegate next) {
    _next = next;
  }

  public async Task InvokeAsync (HttpContextSample context) {
    context.Output.AppendLine("Middleware 4 Processing.");

    // Call the next delegate/middleware in the pipeline
    await _next (context);
  }
}

在演示程序中使用按照約定定義的中間件。

Func<RequestDelegate, RequestDelegate> middleware4 = next => {
  return (HttpContextSample context) => {
    
    var step4 = new RequestCultureMiddleware(next);
    // 中間件 4 的處理
    var result = step4.InvokeAsync (context);
    return result;
  };
};
_components.Add (middleware4);

在 ASP.NET Core 中使用按照約定定義的中間件語法與使用強類型方式相同:

 .UseMiddleware<RequestCultureMiddleware >();

中間件的順序

中間件安裝一定順尋構造成為請求處理管道,常見的處理管道如下所示:

深入淺析ASP.NET中的Core處理管道

實現(xiàn) BeginRequest 和 EndRequest

理解了請求處理管道的原理,下面看它的一個應用。

在 ASP.NET 中我們可以使用預定義的 Begin_Request 和 EndRequest 處理步驟。

現(xiàn)在整個請求處理管道都是我們自己來進行構建了,那么怎么實現(xiàn) Begin_Request 和 EndRequest 呢?使用中間件可以很容易實現(xiàn)它。

首先,這兩個步驟是請求處理的第一個和最后一個步驟,顯然,該中間件必須是第一個注冊到管道中的。

所謂的 Begin_Request 就是在調用 next() 之間的處理了,而 End_Request 就是在調用 next() 之后的處理了。在 https://stackoverflow.com/questions/40604609/net-core-endrequest-middleware 中就有一個示例,我們將它修改一下,如下所示:

public class BeginEndRequestMiddleware
{
  private readonly RequestDelegate _next;

  public BeginEndRequestMiddleware(RequestDelegate next)
  {
    _next = next;
  }

  public void Begin_Request(HttpContext context) {
    // do begin request
  }

  public void End_Request(HttpContext context) {
    // do end request
  }

  public async Task Invoke(HttpContext context)
  {
    // Do tasks before other middleware here, aka 'BeginRequest'
    Begin_Request(context);

    // Let the middleware pipeline run
    await _next(context);

    // Do tasks after middleware here, aka 'EndRequest'
    End_Request();
  }
}

Register

public void Configure(IApplicationBuilder app)
{
  // 第一個注冊
  app.UseMiddleware<BeginEndRequestMiddleware>();

  // Register other middelware here such as:
  app.UseMvc();
}

關于深入淺析ASP.NET中的Core處理管道就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI