ASP.NET Core 最佳做法

作者:Mike Rousos

本文提供將 ASP.NET Core 應用程式的效能和可靠性最大化的指導方針。

積極快取

本文的幾個部分會討論快取。 如需詳細資訊,請參閱 ASP.NET Core 中的快取概觀

了解經常性呼叫程式碼路徑

在本文中,經常性呼叫程式碼路徑會定義為經常呼叫的程式碼路徑,以及大部分執行時間發生的位置。 經常性呼叫程式碼路徑通常會限制應用程式向外延展和效能,並在本文中的數個部分中討論。

避免封鎖呼叫

ASP.NET Core 應用程式的設計應同時處理許多要求。 非同步 API 允許小型執行緒集區透過不等候封鎖呼叫來處理數千個並行要求。 執行緒可以處理另一個要求,而不是等候長時間執行的同步工作完成。

ASP.NET Core 應用程式中常見的效能問題是封鎖可能非同步的呼叫。 許多同步封鎖呼叫會導致執行緒集區耗盡和回應時間降級。

請勿透過呼叫 Task.WaitTask<TResult>.Result 來封鎖非同步執行。 請勿取得常見程式碼路徑中的鎖定。 ASP.NET Core 應用程式在架構為平行執行程式碼時效能最佳。 請勿呼叫 Task.Run 並立即等候。 ASP.NET Core 已在一般執行緒集區執行緒上執行應用程式程式碼,因此呼叫 Task.Run 只會造成額外不必要的執行緒集區排程。 即使排程的程式碼會封鎖執行緒,Task.Run 也不會防止其發生。

  • 經常性呼叫程式碼路徑變得非同步。
  • 如果非同步 API 可用,以非同步方式呼叫資料存取、I/O 和長時間執行的作業 API。
  • 請勿使用 Task.Run 將同步 API 設為非同步。
  • 讓控制器/Razor 頁面動作變得非同步。 整個呼叫堆疊屬於非同步,可受益於非同步/等候模式。

分析工具如 PerfView,可用來尋找經常新增至執行緒集區的執行緒。 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 事件表示已新增至執行緒集區的執行緒。

跨多個較小的頁面傳回大型集合

網頁不應一次載入大量資料。 傳回物件的集合時,請考慮是否可能導致效能問題。 判斷設計是否會產生下列不良結果:

新增分頁以減輕上述案例。 透過使用頁面大小和頁面索引參數,開發人員應該偏好傳回部分結果的設計。 需要詳盡的結果時,分頁應該用來以非同步方式填入結果批次,以避免鎖定伺服器資源。

如需分頁和限制傳回記錄數目的詳細資訊,請參閱:

傳回 IEnumerable<T>IAsyncEnumerable<T>

從動作傳回 IEnumerable<T> 會導致序列化程式進行同步集合反覆運算。 結果是封鎖呼叫,以及執行緒集區耗盡的可能性。 若要避免同步列舉,請在傳回可列舉之前使用 ToListAsync

從 ASP.NET Core 3.0 開始,IAsyncEnumerable<T> 可以作為非同步列舉的 IEnumerable<T> 替代方案。 如需詳細資訊,請參閱控制器動作傳回類型

最小化大型物件配置

.NET Core 記憶體回收行程會在 ASP.NET Core 應用程式中自動管理記憶體的配置和釋放。 自動記憶體回收通常表示開發人員不需要擔心釋放記憶體的方式或時機。 不過,清除未參考的物件需要 CPU 時間,因此開發人員應將經常性呼叫程式碼路徑中的物件配置降至最低。 大型物件上的記憶體回收成本特別高 (>= 85,000 個位元組)。 大型物件會儲存在大型物件堆積上,且需要完整 (第 2 代) 記憶體回收才能清除。 不同於第 0 代和第 1 代集合,第 2 代集合需要暫時暫停應用程式執行。 大型物件的頻繁配置和取消配置可能會導致效能不一致。

建議:

  • 考慮快取經常使用的大型物件。 快取大型物件可防止成本過高的配置。
  • 使用ArrayPool<T> 來儲存大型陣列以實作集區緩衝區。
  • 請勿經常性呼叫程式碼路徑上配置許多短期大型物件。

您可以在 PerfView 中檢閱記憶體回收 (GC) 統計資料並檢查下列項目來診斷如上述的記憶體問題:

  • 記憶體回收暫停時間。
  • 在記憶體回收中花費的處理器時間百分比。
  • 第 0、第 1 和第 2 代的記憶體回收數量。

如需詳細資訊,請參閱記憶體回收與效能

最佳化資料存取和 I/O

與資料存放區和其他遠端服務的互動通常是 ASP.NET Core 應用程式最慢的部分。 有效率地讀取和寫入資料對於良好的效能至關重要。

建議:

  • 以非同步方式呼叫所有資料存取 API。
  • 請勿擷取非必要的資料。 撰寫查詢以僅傳回目前 HTTP 要求所需的資料。
  • 如果可接受稍微過時的資料,考慮快取從資料庫或遠端服務擷取經常存取的資料。 視案例而定,請使用 MemoryCacheDistributedCache。 如需詳細資訊,請參閱 ASP.NET Core 中的回應快取
  • 盡可能減少網路來回行程。 目標是在單一呼叫中擷取必要的資料,而不是數個呼叫。
  • 存取資料以供唯讀之用時,請勿在 Entity Framework Core 中使用無追蹤查詢。 EF Core 可以更有效率地傳回無追蹤查詢的結果。
  • 篩選和彙總 LINQ 查詢 (例如使用 .Where.Select.Sum 陳述式),讓篩選由資料庫執行。
  • 考慮 EF Core 會在用戶端上解析某些查詢運算子,這可能會導致查詢執行的效率不佳。 如需詳細資訊,請參閱用戶端評估效能問題
  • 請勿在集合上使用投影查詢,這可能會導致執行「N + 1」SQL 查詢。 如需詳細資訊,請參閱最佳化相互關聯的子查詢

下列方法可能會改善大規模應用程式中的效能:

建議您先測量上述高效能方法的影響,再認可程式碼基底。 已編譯查詢的額外複雜度可能無法證明效能改善。

您可以檢閱使用 Application Insights 或分析工具存取資料所花費的時間,來偵測查詢問題。 大部分的資料庫也會針對經常執行的查詢提供統計資料。

使用 HttpClientFactory 的集區 HTTP 連線

雖然 HttpClient 會實作 IDisposable 介面,但其設計目的是要重複使用。 關閉 HttpClient 執行個體會在短時間內讓通訊端在 TIME_WAIT 狀態中保持開啟。 如果經常使用建立和處置 HttpClient 物件的程式碼路徑,應用程式可能會耗盡可用的通訊端。 HttpClientFactory 已在 ASP.NET Core 2.1 中引進,作為此問題的解決方案。 其會處理共用 HTTP 連線,以最佳化效能和可靠性。 如需詳細資訊,請參閱使用 HttpClientFactory 來實作復原性 HTTP 要求

建議:

讓一般程式碼路徑保持快速

您想要讓所有程式碼都快速完成。 經常呼叫的程式碼路徑是最佳化的重點。 包括:

  • 應用程式要求處理管線中的中介軟體元件,特別是中介軟體會在管線中早期執行。 這些元件對效能有很大的影響。
  • 針對每個要求執行的程式碼,或針對每個要求執行多次的程式碼。 例如,自訂記錄、授權處理常式或暫時性服務的初始化。

建議:

完成 HTTP 要求以外的長時間執行工作

對 ASP.NET Core 應用程式的大部分要求都可以由呼叫必要服務並傳回 HTTP 回應的控制器或頁面模型來處理。 對於涉及長時間執行工作的一些要求,最好讓整個要求-回應程序非同步。

建議:

  • 請勿在一般 HTTP 要求處理期間等候長時間執行的工作完成。
  • 考慮使用背景服務處理長時間執行的要求,或使用 Azure 函式跨處理程序處理。 跨處理程序完成工作對 CPU 密集型工作特別有幫助。
  • 使用即時通訊選項,例如 SignalR,以非同步方式與用戶端通訊。

縮製用戶端資產

具有複雜前端的 ASP.NET Core 應用程式經常提供許多 JavaScript、CSS 或影像檔。 您可以透過下列方式改善初始載入要求的效能:

  • 統合,也就是將多個檔案合併成一個。
  • 縮製,也就是藉由移除空白字元和註解來減少檔案大小。

建議:

  • 使用統合和縮製指導方針,其中提及相容的工具,並示範如何使用 ASP.NET Core 的 environment 標籤來處理 DevelopmentProduction 環境。
  • 考慮其他協力廠商工具,例如 Webpack,以進行複雜的用戶端資產管理。

壓縮回應

減少回應的大小通常會增加應用程式的回應性,且通常會大幅提高。 減少承載大小的其中一種方法是壓縮應用程式的回應。 如需詳細資訊,請參閱回應壓縮

使用最新的 ASP.NET Core 版本

每個新版本的 ASP.NET Core 都包含效能改進。 .NET Core 和 ASP.NET Core 中的最佳化表示較新版本通常優於較舊的版本。 例如,.NET Core 2.1 新增了對已編譯規則運算式的支援,並受益於 Span<T>。 ASP.NET Core 2.2 新增了 HTTP/2 支援。 ASP.NET Core 3.0 新增許多改善,以減少記憶體使用量並改善輸送量。 如果效能是優先考量,請考慮升級至目前版本的 ASP.NET Core。

最小化例外狀況

例外狀況應該很少見。 擲回和攔截例外狀況相對於其他程式碼流程模式速度很慢。 因此,不應該使用例外狀況來控制一般程式流程。

建議:

  • 請勿使用擲回或攔截例外狀況作為一般程式流程的方法,特別是在經常性呼叫程式碼路徑中。
  • 在應用程式中包含邏輯,以偵測及處理會導致例外狀況的情況。
  • 針對異常或非預期的狀況擲回或攔截例外狀況。

應用程式診斷工具,例如 Application Insights,可協助識別可能影響效能之應用程式中的常見例外狀況。

避免在 HttpRequest/HttpResponse 本文上同步讀取或寫入

ASP.NET Core 中的所有 I/O 都是非同步。 伺服器會實作 Stream 介面,其同時具有同步和非同步多載。 應優先使用非同步執行緒,以避免封鎖執行緒集區執行緒。 封鎖執行緒可能會導致執行緒集區耗盡。

請勿這麼做:下列範例會使用 ReadToEnd。 其會封鎖目前的執行緒來等候結果。 這是非同步中同步的範例。

public class BadStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public ActionResult<ContosoData> Get()
    {
        var json = new StreamReader(Request.Body).ReadToEnd();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }
}

在上述程式碼中,Get 以同步方式將整個 HTTP 要求本文讀入記憶體。 如果用戶端目前上傳緩慢,應用程式會執行非同步中同步。 應用程式執行非同步中同步,因為 Kestrel支援同步讀取。

執行此動作:下列範例會在讀取時使用 ReadToEndAsync 且不會封鎖執行緒。

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        return JsonSerializer.Deserialize<ContosoData>(json);
    }

}

上述程式碼會以非同步方式將整個 HTTP 要求本文讀入記憶體。

警告

如果要求很大,將整個 HTTP 要求本文讀入記憶體可能會導致記憶體不足 (OOM) 情況。 OOM 可能會導致拒絕服務。 如需詳細資訊,請參閱本文中的避免將大型要求本文或回應本文讀入記憶體

執行此動作:下列範例是使用非緩衝要求本文的完全非同步:

public class GoodStreamReaderController : Controller
{
    [HttpGet("/contoso")]
    public async Task<ActionResult<ContosoData>> Get()
    {
        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);
    }
}

上述程式碼會以非同步方式將要求本文還原序列化為 C# 物件。

偏好 ReadFormAsync 而不是 Request.Form

使用 HttpContext.Request.ReadFormAsync 取代 HttpContext.Request.FormHttpContext.Request.Form 僅能透過下列情況安全地讀取:

  • 表單已由呼叫 ReadFormAsync 讀取,且
  • 正在使用 HttpContext.Request.Form 讀取快取的表單值

請勿這麼做:下列範例使用 HttpContext.Request.FormHttpContext.Request.Form 會使用非同步中同步,且可能導致執行緒集區耗盡。

public class BadReadController : Controller
{
    [HttpPost("/form-body")]
    public IActionResult Post()
    {
        var form =  HttpContext.Request.Form;

        Process(form["id"], form["name"]);

        return Accepted();
    }

執行此動作:下列範例會使用 HttpContext.Request.ReadFormAsync,以非同步方式讀取表單本文。

public class GoodReadController : Controller
{
    [HttpPost("/form-body")]
    public async Task<IActionResult> Post()
    {
       var form = await HttpContext.Request.ReadFormAsync();

        Process(form["id"], form["name"]);

        return Accepted();
    }

避免將大型要求本文或回應本文讀入記憶體

在 .NET 中,每個大於或等於 85,000 個位元組的物件配置最終都會出現在大型物件堆積 (LOH) 中。 大型物件的高成本呈現在兩種方式:

  • 配置成本很高,因為必須清除新配置大型物件的記憶體。 CLR 可確保清除所有新配置物件的記憶體。
  • LOH 會與堆積的其餘部分一起收集。 LOH 需要完整的記憶體回收Gen2 收集

部落格文章簡潔地描述該問題:

配置大型物件時,其會標示為 Gen 2 物件。 針對小型物件非 Gen 0。 後果是,如果您在 LOH 中記憶體不足,GC 會清除整個受控堆積,而不只是 LOH。 因此,它會清除 Gen 0、Gen 1 和 Gen 2,包括 LOH。 這稱為完整記憶體回收,且是最耗時的記憶體回收。 對於許多應用程式而言,如此可以接受。 但絕對不適用於高效能的網頁伺服器,需要少數大型記憶體緩衝區來處理平均 Web 要求 (從通訊端讀取、解壓縮、解碼 JSON 等等)。

將大型要求或回應本文儲存至單一 byte[]string

  • 可能導致在 LOH 中迅速耗盡空間。
  • 可能會因為執行完整的 GCS 而造成應用程式的效能問題。

使用同步資料處理 API

使用僅支援同步讀取和寫入的序列化程式/還原序列化程式時 (例如,Json.NET):

  • 先以非同步方式將資料緩衝處理到記憶體中,再將其傳遞至序列化程式/還原序列化程式。

警告

如果要求很大,這可能會導致記憶體不足 (OOM) 的情況。 OOM 可能會導致拒絕服務。 如需詳細資訊,請參閱本文中的避免將大型要求本文或回應本文讀入記憶體

ASP.NET Core 3.0 預設會針對 JSON 序列化使用 System.Text.JsonSystem.Text.Json

  • 以非同步方式讀取和寫入 JSON。
  • 已針對 UTF-8 文字進行最佳化。
  • 效能通常高於 Newtonsoft.Json

請勿將 IHttpCoNtextAccessor.HttpCoNtext 儲存在欄位中

從要求執行緒存取時,IHttpCoNtextAccessor.HttpCoNtext 會傳回作用中要求的 HttpContextIHttpContextAccessor.HttpContext應儲存在欄位或變數中。

請勿這麼做:下列範例會將 HttpContext 儲存在欄位中,然後稍後嘗試使用它。

public class MyBadType
{
    private readonly HttpContext _context;
    public MyBadType(IHttpContextAccessor accessor)
    {
        _context = accessor.HttpContext;
    }

    public void CheckAdmin()
    {
        if (!_context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

上述程式碼會在建構函式中頻繁地擷取 null 或不正確的 HttpContext

執行此動作:下列範例:

  • IHttpContextAccessor 儲存在欄位中。
  • 在正確的時間使用 HttpContext 欄位,並檢查 null
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

    public void CheckAdmin()
    {
        var context = _accessor.HttpContext;
        if (context != null && !context.User.IsInRole("admin"))
        {
            throw new UnauthorizedAccessException("The current user isn't an admin");
        }
    }
}

請勿從多個執行緒存取 HttpCoNtext

HttpContext不是安全執行緒。 從多個執行緒平行存取 HttpContext 可能會導致非預期的行為,例如停止回應、當機和資料損毀。

請勿這麼做:下列範例會提出三個平行要求,並在傳出 HTTP 要求前後記錄傳入要求路徑。 要求路徑可從多個執行緒存取,可能為平行存取。

public class AsyncBadSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        var query1 = SearchAsync(SearchEngine.Google, query);
        var query2 = SearchAsync(SearchEngine.Bing, query);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }       

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.", 
                                    HttpContext.Request.Path);
            searchResults = _searchService.Search(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", 
                                    HttpContext.Request.Path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", 
                             HttpContext.Request.Path);
        }

        return await searchResults;
    }

執行此動作:下列範例會先從傳入要求複製所有資料,再提出三個平行要求。

public class AsyncGoodSearchController : Controller
{       
    [HttpGet("/search")]
    public async Task<SearchResults> Get(string query)
    {
        string path = HttpContext.Request.Path;
        var query1 = SearchAsync(SearchEngine.Google, query,
                                 path);
        var query2 = SearchAsync(SearchEngine.Bing, query, path);
        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);

        await Task.WhenAll(query1, query2, query3);

        var results1 = await query1;
        var results2 = await query2;
        var results3 = await query3;

        return SearchResults.Combine(results1, results2, results3);
    }

    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
                                                  string path)
    {
        var searchResults = _searchService.Empty();
        try
        {
            _logger.LogInformation("Starting search query from {path}.",
                                   path);
            searchResults = await _searchService.SearchAsync(engine, query);
            _logger.LogInformation("Finishing search query from {path}.", path);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed query from {path}", path);
        }

        return await searchResults;
    }

完成要求之後,請勿使用 HttpCoNtext

HttpContext 只有在 ASP.NET Core 管線中有作用中 HTTP 要求時才有效。 整個 ASP.NET Core 管線是執行每個要求的委派非同步鏈結。 從這個鏈結完成傳回 Task 時,系統會回收 HttpContext

請勿這麼做:下列範例會使用 async void,在達到第一個 await 時完成 HTTP 要求:

  • 在 ASP.NET Core 應用程式中,使用 async void一律是錯誤的做法。
  • 範例程式碼會在 HTTP 要求完成之後存取 HttpResponse
  • 延遲存取會使處理程序損毀。
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

        // The following line will crash the process because of writing after the 
        // response has completed on a background thread. Notice async void Get()

        await Response.WriteAsync("Hello World");
    }
}

執行此動作:下列範例會將 Task 傳回至架構,因此 HTTP 要求在動作完成之前不會完成。

public class AsyncGoodTaskController : Controller
{
    [HttpGet("/async")]
    public async Task Get()
    {
        await Task.Delay(1000);

        await Response.WriteAsync("Hello World");
    }
}

請勿在背景執行緒中擷取 HttpCoNtext

請勿這麼做:下列範例顯示關閉正在從 Controller 屬性擷取 HttpContext。 這是錯誤的做法,因為工作項目可以:

  • 在要求範圍之外執行。
  • 嘗試讀取錯誤的 HttpContext
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        var path = HttpContext.Request.Path;
        Log(path);
    });

    return Accepted();
}

執行此動作:下列範例:

  • 在要求期間複製背景工作中所需的資料。
  • 不會從控制器參考任何項目。
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

背景工作應實作為託管服務。 如需詳細資訊,請參閱搭配託管服務的背景工作

請勿擷取插入背景執行緒上控制器的服務

請勿這麼做:下列範例會顯示從 Controller 動作參數擷取 DbContext 的關閉。 這是一個錯誤的做法。 工作項目可以在要求範圍之外執行。 ContosoDbContext 的範圍限於要求,導致 ObjectDisposedException

[HttpGet("/fire-and-forget-1")]
public IActionResult FireAndForget1([FromServices]ContosoDbContext context)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        context.Contoso.Add(new Contoso());
        await context.SaveChangesAsync();
    });

    return Accepted();
}

執行此動作:下列範例:

  • 插入 IServiceScopeFactory,以便在背景工作項目中建立範圍。 IServiceScopeFactory 是單一資料庫。
  • 在背景執行緒中建立新的相依性插入範圍。
  • 不會從控制器參考任何項目。
  • 不會從傳入要求擷取 ContosoDbContext
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        await using (var scope = serviceScopeFactory.CreateAsyncScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

下列醒目提示的程式碼:

  • 建立背景作業的存留期範圍,並從中解析服務。
  • 從正確的範圍使用 ContosoDbContext
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        await using (var scope = serviceScopeFactory.CreateAsyncScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

在回應本文啟動之後,請勿修改狀態碼或標頭

ASP.NET Core 不會緩衝處理 HTTP 回應本文。 第一次寫入回應時:

  • 標頭會連同本文的該區塊一起傳送至用戶端。
  • 無法再變更回應標頭。

請勿這麼做:下列程式碼會嘗試在回應啟動之後新增回應標頭:

app.Use(async (context, next) =>
{
    await next();

    context.Response.Headers["test"] = "test value";
});

在上述程式碼中,如果 next() 已寫入回應,則 context.Response.Headers["test"] = "test value"; 會擲回例外狀況。

執行此動作:下列範例會先檢查 HTTP 回應是否已在修改標頭之前啟動。

app.Use(async (context, next) =>
{
    await next();

    if (!context.Response.HasStarted)
    {
        context.Response.Headers["test"] = "test value";
    }
});

執行此動作:下列範例會使用 HttpResponse.OnStarting 在回應標頭排清至用戶端之前設定標頭。

檢查回應是否未啟動,允許註冊將在寫入回應標頭之前叫用的回呼。 檢查回應是否已啟動:

  • 讓您能夠及時附加或覆寫標頭。
  • 不需要了解管線中的下一個中介軟體。
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

如果您已開始寫入回應本文,請勿呼叫 next()

只有在元件能夠處理及操作回應時,才會預期呼叫元件。

搭配 IIS 使用內含式裝載

使用同處理序裝載,ASP.NET Core 應用程式會在與其 IIS 工作者處理序相同的處理序中執行。 內含式裝載透過跨處理程序裝載提供改善的效能,因為要求不會透過回送介面卡進行 Proxy 處理。 回送介面卡是一種網路介面,會將傳出的網路流量傳回相同的電腦。 IIS 透過 Windows 處理序啟用服務 (WAS) 來執行處理程序管理。

專案預設為 ASP.NET Core 3.0 和更新版本中的內含式裝載模型。

如需詳細資訊,請參閱在使用 IIS 的 Windows 上裝載 ASP.NET Core

請勿假設 HttpRequest.ContentLength 不是 null

如果未收到 Content-Length 標頭,則 HttpRequest.ContentLength 為 null。 在此情況下,null 表示要求本文的長度未知;這並不表示長度為零。 因為所有與 null (除了 ==) 的比較都會傳回 false,例如比較 Request.ContentLength > 1024 時,當要求本文大小超過 1024 時可能會傳回 false。 不知道這個可能會導致應用程式中的安全性漏洞。 您可能會認為正在防範太大的要求,但實際上並非如此。

如需詳細資訊,請參閱這篇 StackOverflow 回答

可靠的 Web 應用程式模式

請參閱 The Reliable Web App Pattern for.NETYouTube 影片文章,以取得從頭建立新式、可靠、效能強、可測試、具成本效益及可調整的 ASP.NET Core 應用程式或重構現有應用程式的指導。