ASP.NET Core 效能優化最佳實踐

Newbe36524發表於2020-09-14

本文提供了 ASP.NET Core 的效能最佳實踐指南。

 

 

譯文原文地址:https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1

積極利用快取

這裡有一篇文件在多個部分中討論瞭如何積極利用快取。 有關詳細資訊,請參閱︰ https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1.

瞭解程式碼中的熱點路徑

在本文件中, 程式碼熱點路徑 定義為頻繁呼叫的程式碼路徑以及執行時間的大部分時間。 程式碼熱點路徑通常限制應用程式的擴充套件和效能,並在本文件的多個部分中進行討論。

避免阻塞式呼叫

ASP.NET Core 應用程式應設計為同時處理許多請求。 非同步 API 可以使用一個小池執行緒通過非阻塞式呼叫來處理數以千計的併發請求。 執行緒可以處理另一個請求,而不是等待長時間執行的同步任務完成。

ASP.NET Core 應用程式中的常見效能問題通常是由於那些本可以非同步呼叫但卻採用阻塞時呼叫而導致的。 同步阻塞會呼叫導致 執行緒池飢餓 和響應時間降級。

不要:

  • 通過呼叫 Task.Wait 或 Task.Result 來阻止非同步執行。
  • 在公共程式碼路徑中加鎖。 ASP.NET Core 應用程式應設計為並行執行程式碼,如此才能使得效能最佳。
  • 呼叫 Task.Run 並立即 await 。 ASP.NET Core 本身已經是線上程池執行緒上執行應用程式程式碼了,因此這樣呼叫 Task.Run 只會導致額外的不必要的執行緒池排程。 而且即使被排程的程式碼會阻止執行緒, Task.Run 也並不能避免這種情況,這樣做沒有意義。

要:

  • 確保 程式碼熱點路徑 全部非同步化。
  • 如在進行呼叫資料讀寫、I/O 處理和長時間操作的 API 時,存在可用的非同步 API。那麼務必選擇非同步 API 。 但是,不要 使用 Task.Run 來包裝同步 API 使其非同步化。
  • 確保 controller/Razor Page actions 非同步化。 整個呼叫堆疊是非同步的,就可以利用 async/await 模式的效能優勢。
    使用效能分析程式 ( 例如 PerfView) 可用於查詢頻繁新增到 執行緒池 的執行緒。 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 事件表示新執行緒被新增到執行緒池。

使用 IEumerable<T> 或 IAsyncEnumerable<T> 作為返回值

在 Action 中返回 IEumerable<T> 將會被序列化器中進行同步迭代 。 結果是可能導致阻塞或者執行緒池飢餓。 想要要避免同步迭代集合,可以在返回迭代集合之前使用 ToListAsync 使其非同步化。

從 ASP.NET Core 3.0 開始, IAsyncEnumerable<T> 可以用作為 IEumerable<T> 的替代方法,以非同步方式進行迭代。 有關更多資訊,請參閱 Controller Action 的返回值型別

儘可能少的使用大物件

.NET Core 垃圾收集器 在 ASP.NET Core 應用程式中起到自動管理記憶體的分配和釋放的作用。 自動垃圾回收通常意味著開發者不需要擔心如何或何時釋放記憶體。 但是,清除未引用的物件將會佔用 CPU 時間,因此開發者應最小化 程式碼熱點路徑 中的分配的物件。 垃圾回收在大物件上代價特大 (> 85 K 位元組) 。 大物件儲存在 large object heap 上,需要 full (generation 2) garbage collection 來清理。 與 generation 0 和 generation 1 不同,generation 2 需要臨時暫掛應用程式。 故而頻繁分配和取消分配大型物件會導致效能耗損。

建議 :

  • 要 考慮快取頻繁使用的大物件。 快取大物件可防止昂貴的分配開銷。
  • 要使用 ArrayPool<T> 作為池化緩衝區以儲存大型陣列。
  • 不要 在程式碼熱點路徑 上分配許多短生命週期的大物件。

可以通過檢視 PerfView 中的垃圾回收 (GC) 統計資訊來診斷並檢查記憶體問題,其中包括:

  • 垃圾回收掛起時間。
  • 垃圾回收中耗用的處理器時間百分比。
  • 有多少垃圾回收發生在 generation 0, 1, 和 2.

有關更多資訊,請參閱 垃圾回收和效能

優化資料操作和 I/O

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

建議 :

  • 要 以非同步方式呼叫所有資料訪問 API 。
  • 不要 讀取不需要的資料。 編寫查詢時,僅返回當前 HTTP 請求所必需的資料。
  • 要 考慮快取從資料庫或遠端服務檢索的頻繁訪問的資料 (如果稍微過時的資料是可接受的話) 。 根據具體的場景,可以使用 MemoryCache 或 DistributedCache。 有關更多資訊,請參閱 https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1.
  • 要 儘量減少網路往返。 能夠單次呼叫完成就不應該多次呼叫來讀取所需資料。
  • 要 在 Entity Framework Core 訪問資料以用作只讀情況時, 使用 no-tracking 方式查詢。 EF Core 可以更高效地返回 no-tracking 查詢的結果。
  • 要 使用過濾器和聚集 LINQ 查詢 (例如, .Where, .Select 或 .Sum 語句) ,以便資料庫執行過濾提高效能 。
  • 要 考慮 EF Core 可能在客戶端解析一些查詢運算子,這可能導致查詢執行效率低下。 有關更多資訊,請參閱 客戶端計算相關的效能問題
  • 不要 在集合上使用對映查詢,這會導致執行 “N + 1” SQL 查詢。 有關更多資訊,請參閱 優化子查詢

請參閱 EF 高效能專題 以瞭解可能提高應用效能的方法:

在程式碼提交之前,我們建議評估上述高效能方法的影響。 編譯查詢的額外複雜性可能無法一定確保效能提高。

可以通過使用 Application Insights 或使用分析工具檢視訪問資料所花費的時間來檢測查詢問題。 大多數資料庫還提供有關頻繁執行的查詢的統計資訊,這也可以作為重要參考。

通過 HttpClientFactory 建立 HTTP 連線池

雖然 HttpClient 實現了 IDisposable 介面,但它其實被設計為可以重複使用單個例項。 關閉 HttpClient 例項會使套接字在短時間內以 TIME_WAIT 狀態開啟。 如果經常建立和釋放 HttpClient 物件,那麼應用程式可能會耗盡可用套接字。 在 ASP.NET Core 2.1 中,引入了 HttpClientFactory 作為解決這個問題的辦法。 它以池化 HTTP 連線的方式從而優化效能和可靠性。

建議 :

確保公共程式碼路徑快若鷹隼

如果你想要所有的程式碼都保持高速, 高頻呼叫的程式碼路徑就是優化的最關鍵路徑。 優化措施包括:

  • 考慮優化應用程式請求處理管道中的 Middleware ,尤其是在管道中排在更前面執行的 Middleware 。 這些元件對效能有很大影響。
  • 考慮優化那些每個請求都要執行或每個請求多次執行的程式碼。 例如,自定義日誌,身份認證與授權或 transient 服務的建立等等。

建議 :

在 HTTP 請求之外執行長時任務

對 ASP.NET Core 應用程式的大多數請求可以由呼叫服務的 controller 或頁面模型處理,並返回 HTTP 響應。 對於涉及長時間執行的任務的某些請求,最好使整個請求 - 響應程式非同步。

建議 :

  • 不要把等待長時間執行的任務完成,作為普通 HTTP 請求處理的一部分。
  • 要 考慮使用 後臺服務 或 Azure Function 處理長時間執行的任務。 在應用外執行任務特別有利於 CPU 密集型任務的效能。
  • 要 使用實時通訊,如 SignalR,以非同步方式與客戶端通訊。

縮小客戶端資源

複雜的 ASP.NET Core 應用程式經常包含很有前端檔案例如 JavaScript, CSS 或圖片檔案。 可以通過以下方法優化初始請求的效能:

  • 打包,將多個檔案合併為一個檔案。
  • 壓縮,通過除去空格和註釋來縮小檔案大小。

建議 :

  • 要 使用 ASP.NET Core 的 內建支援 用於打包和壓縮客戶端資原始檔的元件。
  • 要 考慮其他第三方工具,如 Webpack,用於複雜客戶資產管理。

壓縮 Http 響應

減少響應的大小通常會顯著提高應用程式的響應性。 而減小內容大小的一種方法是壓縮應用程式的響應。 有關更多資訊,請參閱 響應壓縮

使用最新的 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 body 上同步讀取或寫入

ASP.NET Core 中的所有 I/O 都是非同步的。 伺服器實現了 Stream 介面,它同時具有同步和非同步的方法過載。 應該首選非同步方式以避免阻塞執行緒池執行緒。 阻塞執行緒會導致執行緒池飢餓。

不要使用如下操作: https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.ReadToEnd。 它會阻止當前執行緒等待結果。 這是 sync over async 的示例。

 
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 不 支援同步讀取。

應該採用如下操作: https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.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 request body 讀取到記憶體中。

[!WARNING] 如果請求很大,那麼將整個 HTTP request body 讀取到記憶體中可能會導致記憶體不足 (OOM) 。 OOM 可導致應用奔潰。 有關更多資訊,請參閱 避免將大型請求主體或響應主體讀取到記憶體中

應該採用如下操作: 使用不緩衝的方式完成 request body 操作:

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

 

上述程式碼採用非同步方式將 request body 序列化為 C# 物件。

優先選用 Request.Form 的 ReadFormAsync

應該使用 HttpContext.Request.ReadFormAsync 而不是 HttpContext.Request.Form。 HttpContext.Request.Form 只能在以下場景用安全使用。

  • 該表單已被 ReadFormAsync 呼叫,並且
  • 資料已經被從 HttpContext.Request.Form 讀取並快取

不要使用如下操作: 例如以下方式使用 HttpContext.Request.Form。 HttpContext.Request.Form 使用了 sync over async ,這將導致執行緒飢餓.

 
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();
    }

 

 

避免將大型 request body 或 response body 讀取到記憶體中

在 .NET 中,大於 85 KB 的物件會被分配在大物件堆 (LOH )。 大型物件的開銷較大,包含兩方面:

  • 分配大物件記憶體時需要對被分配的記憶體進行清空,這個操作成本較高。 CLR 會保證清空所有新分配的物件的記憶體。(將記憶體全部設定為 0)
  • LOH 只會在記憶體剩餘不足時回收。 LOH 需要在 full garbage collection 或者 Gen2 collection 進行回收。

此 博文 很好描述了該問題:

當分配大物件時,它會被標記為 Gen 2 物件。 而不像是 Gen 0 那樣的小物件。 這樣的後果是,如果你在使用 LOH 時耗盡記憶體, GC 會清除整個託管堆,而不僅僅是 LOH 部分。 因此,它將清理 Gen 0, Gen 1 and Gen 2 (包括 LOH) 。 這稱為 full garbage collection,是最耗時的垃圾回收。 對於很多應用,這是可以接受的。 但絕對不適用於高效能 Web 伺服器,因為高效能 Web 伺服器需要更多的記憶體用於處理常規 Web 請求 ( 從套接字讀取,解壓縮,解碼 JSON 等等 )。

天真地將一個大型 request 或者 response body 儲存到單個 byte[] 或 string 中:

  • 這可能導致 LOH 的剩餘空間快速耗盡。
  • 因此產生的 full GC 可能會導致應用程式的效能問題。

使用同步 API 處理資料

例如使用僅支援同步讀取和寫入的序列化器 / 反序列化器時 ( 例如, JSON.NET):

  • 將資料非同步緩衝到記憶體中,然後將其傳遞到序列化器 / 反序列化器。

[!WARNING] 如果請求較大,那麼可能導致記憶體不足 (OOM) 。 OOM 可導致應用奔潰。 有關更多資訊,請參閱 避免將大型請求主體或響應主體讀取到記憶體

ASP.NET Core 3.0 預設情況下使用 https://docs.microsoft.com/en-us/dotnet/api/system.text.json 進行 JSON 序列化,這將帶來如下好處。 https://docs.microsoft.com/en-us/dotnet/api/system.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

應該採用如下操作:

 
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 ,這將使得 HTTP 請求在第一個 await 時處理完成,進而就會導致:

  • 在 ASP.NET Core 應用程式中, 這是一個完全錯誤 的做法
  • 在 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。 這是一種錯誤做法,因為這將導致:

  • 程式碼執行在 Http 請求作用域之外。
  • 嘗試讀取錯誤的 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();
}

 

應該採用如下操作:

  • 在請求處理階段將後臺執行緒需要的資料全部進行復制。
  • 不要使用 controller 的所有引用
 
[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 中的服務

不要採用如下做法: 以下示例使用閉包從 controller 獲取 DbContext 進行操作。 這是一個錯誤的做法。 這將導致程式碼雲在請求的作用域之外。 而 ContocoDbContext 是基於請求作用域的,因此這樣將引發 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();
}

 

應該採用如下操作:

 
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

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

            await context.SaveChangesAsync();
        }
    });

    return Accepted();
}

 

以下高亮的的程式碼說明:

  • 為後臺操作建立新的作用域,並且從中獲取需要的服務。
  • 在正確的作用域中使用 ContocoDbContext,即只能在請求作用域中使用該物件。
 
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

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

            await context.SaveChangesAsync();
        }
    });

    return Accepted();
}

 

不要在響應正文已經開始傳送時嘗試修改 status code 或者 header

ASP.NET Core 不會緩衝 HTTP 響應正文。 當正文一旦開始傳送:

  • Header 就會與正文的資料包一起傳送到客戶端。
  • 此時就無法修改 header 了。

不要使用如下操作: 以下程式碼嘗試在響應啟動後新增響應頭:

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

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

 

在上述的程式碼中,如果 next() 已經開始寫入響應,則 context.Response.Headers["test"] = "test value"; 將會丟擲異常。

應該採用如下操作: 以下示例檢查 HTTP 響應在修改 Header 之前是否已啟動。

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

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

 

應該採用如下操作: 以下示例使用 HttpResponse.OnStarting 來設定 Header,這樣便可以在響應啟動時將 Header 一次性寫入到客戶端。

通過這種方式,響應頭將在響應開始時呼叫已註冊的回撥進行一次性寫入。 如此這般便可以:

  • 在恰當的時候進行響應頭的修改或者覆蓋。
  • 不需要了解管道中的下一個 middleware 的行為。
 
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

 

如果已開始寫入響應主體,則請不要呼叫 next ()

僅當後續元件能夠處理響應或時才呼叫它們,因此如果當前已經開始寫入響應主體,後續操作就已經不再需要,並有可能引發異常情況。

託管於 IIS 應該使用 In-process 模式

使用 in-process 模式託管, ASP.NET Core 應用程式將與 IIS 工作程式在同一程式中執行。 In-process 模式擁有比 out-of-process 更加優秀的效能表現,因為這樣不需要將請求通過迴環網路介面卡進行代理中轉。 迴環網路介面卡是將本機傳送的網路流量重新轉回本機的的網路介面卡。 IIS 程式管理由 Windows Process Activation Service (WAS) 來完成。

在 ASP.NET Core 3.0 和更高版本中的預設將採用 in-process 模式進行託管。

有關更多資訊,請參閱 在 Windows 上使用 IIS 託管 ASP.NET Core


Newbe.Translations

您所閱讀的當前文章源自於 Newbe.Translations 專案參與的翻譯貢獻,您可以通過右側連結一同參與該專案:https://www.newbe.pro/Newbe.Translations/Newbe.Translations/

翻譯內容具有一定的時效性,不會隨著原文內容實時更新,如果內容存在一定過時,您也可以聯絡我們。

 

 

相關文章