共用方式為


ASP.NET Core 9.0 的新功能

本文會重點說明 ASP.NET Core 9.0 最重要的變更,並附有相關文件的連結。

本文已針對 .NET 9 預覽版 7 進行了更新。

Blazor

本章節會說明 Blazor 的新功能。

.NET MAUIBlazor Hybrid 和 Web 應用程式解決方案範本

新的解決方案範本可讓您更輕鬆地建立共用相同 UI 的 .NET MAUI 原生和 Blazor Web 用戶端應用程式。 此範本會顯示如何建立可將程式碼重複使用最大化並以 Android、iOS、Mac、Windows 和 Web 為目標的用戶端應用程式。

此範本的主要功能包括:

  • 選擇 Web 應用程式的 Blazor 互動式轉譯模式的能力。
  • 自動建立適當的專案,包括 Blazor Web App (全域互動式自動轉譯) 和 .NET MAUIBlazor Hybrid 應用程式。
  • 建立的專案會使用共用 Razor 類別庫 (RCL) 來維護 UI 的 Razor 元件。
  • 包括的範例程式碼會示範如何使用相依性插入來為 Blazor Hybrid 應用程式和 Blazor Web App提供不同的介面實作。

若要開始使用,請安裝 .NET 9 SDK 並安裝包含範本的 .NET MAUI 工作負載:

dotnet workload install maui

使用下列命令從命令殼層中的專案範本建立解決方案:

dotnet new maui-blazor-web

範本也可在 Visual Studio 中使用。

注意

如果 Blazor 轉譯模式是在每頁/元件層級定義,則目前會發生例外狀況。 如需詳細資訊,請參閱 BlazorWebView 需要有方法可啟用覆寫 ResolveComponentForRenderMode (dotnet/aspnetcore #51235)

如需詳細資訊,請參閱 使用 Blazor Web App組建 .NET MAUIBlazor Hybrid 應用程式

靜態資產傳遞最佳化

MapStaticAssets 是新的中介軟體,可協助最佳化任何 ASP.NET Core 應用程式中靜態資產的傳遞,包括 Blazor 個應用程式。

如需詳細資訊,請參閱下列任一資源:

在執行階段偵測轉譯位置、互動性和指派的轉譯模式

我們已經引進一個新的 API,旨在簡化執行階段查詢元件狀態的流程。 此 API 可提供下列功能:

  • 確定元件的目前執行位置:這對於偵錯和最佳化元件效能尤其實用。
  • 檢查元件是否正在互動式環境中執行:這對於根據其環境的互動性而具有不同行為的元件很有幫助。
  • 為元件擷取已指派的轉譯模式:了解轉譯模式有助於最佳化轉譯流程並提高元件的整體效能。

如需詳細資訊,請參閱 ASP.NET Core Blazor 轉譯模式

已提高伺服器端重新連線體驗:

已對預設伺服器端重新連線體驗完成下列增強功能:

  • 當使用者巡覽回到線路已中斷的應用程式時,會立即嘗試重新連線,而不是等待下一個重新連線間隔期間。 這可提升瀏覽至瀏覽器索引標籤中已進入睡眠模式之應用程式時的使用者體驗。

  • 當重新連線嘗試到達伺服器,但伺服器已經釋放線路時,即會自動發生頁面重新整理。 這可以防止使用者必須手動重新整理頁面 (如有可能導致成功的重新連線)。

  • 重新連線計時會使用計算的輪詢策略。 預設情況下,在嘗試之間引進計算延遲之前,前幾次重新連線嘗試會在沒有重試間隔的情況下快速地連續發生。 您可以藉由指定函式來計算重試間隔來自訂重試間隔行為,如下列指數輪詢範例所示:

    Blazor.start({
      circuit: {
        reconnectionOptions: {
          retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
            previousAttempts >= maxRetries ? null : previousAttempts * 1000
        },
      },
    });
    
  • 預設重新連線 UI 的樣式已現代化。

如需詳細資訊,請參閱 ASP.NET Core BlazorSignalR 指引

簡化 Blazor Web App驗證狀態序列化

新的 API 可讓您更輕鬆地將驗證新增至現有的 Blazor Web App。 當您使用 個別帳戶 建立具有驗證的新 Blazor Web App ,並啟用 WebAssembly 型互動功能時,專案會在伺服器和用戶端專案中同時包含自訂 AuthenticationStateProvider 專案。

這些提供者會將使用者的驗證狀態流向瀏覽器。 在伺服器上進行驗證,而不是用戶端允許應用程式在預先轉譯期間,以及在初始化 NET WebAssembly 執行階段之前存取驗證狀態。

自訂 AuthenticationStateProvider 實作會使用保存元件狀態服務 (PersistentComponentState) 將驗證狀態序列化為 HTML 註解,並且從 WebAssembly 讀取它以建立新的 AuthenticationState 執行個體。

如果您已從 Blazor Web App 專案範本開始,並選取 [個人帳戶] 選項,但如果實作自己的專案或嘗試將驗證新增至現有專案而進行複製時需要大量程式碼,則此方法相當有效。 現在有 API,屬於 Blazor Web App 專案範本的一部分,在伺服器和用戶端專案中呼叫即可新增這項功能:

  • AddAuthenticationStateSerialization:新增必要的服務,以序列化伺服器上的驗證狀態。
  • AddAuthenticationStateDeserialization:新增必要的服務,以還原串行化瀏覽器中的驗證狀態。

根據預設,這些 API 只會序列化伺服器端名稱和角色宣告,以在瀏覽器中存取。 選項可以傳遞至 AddAuthenticationStateSerialization 以包括所有宣告。

如需詳細資訊,請參閱安全 ASP.NET Core 伺服器端 Blazor 應用程式的下列各節:

將靜態伺服器端轉譯 (SSR) 頁面新增至全域互動式 Blazor Web App

隨著 .NET 9 的發行,現在要將靜態 SSR 頁面新增至採用全域互動功能的應用程式變得更簡單。

只有當應用程式具有無法使用互動式伺服器或 WebAssembly 轉譯的特定頁面時,此方法才有用。 例如,對於依賴讀取/寫入 HTTP cookie 且只能在要求/回應週期(而不是互動式轉譯)中運作的頁面,可採用此方法。 對於使用互動式轉譯的頁面,您不應強制其使用靜態 SSR 轉譯,因為其效率較低且對終端使用者的回應較差。

使用以 @attributeRazor 指示詞指派的新 [ExcludeFromInteractiveRouting] 屬性來標記任何 Razor 元件頁面:

@attribute [ExcludeFromInteractiveRouting]

套用該屬性會導致瀏覽至該頁面時退出互動式路由處理。 輸入導覽會被強制執行完整頁面重新載入,而不是透過互動式路由處理來解析頁面。 完整頁面重新載入會強制最上層根元件 (通常是 App 元件 (App.razor)) 從伺服器重新轉譯,從而允許應用程式切換到不同的最上層轉譯模式。

HttpContext.AcceptsInteractiveRouting 擴充方法可讓元件偵測是否已將 [ExcludeFromInteractiveRouting] 套用至目前頁面。

App 元件中,使用下列範例中的模式:

  • 未以 [ExcludeFromInteractiveRouting] 標註的頁面預設為具有全域互動功能的 InteractiveServer 轉譯模式。 您可以使用 InteractiveWebAssemblyInteractiveAuto 來取代 InteractiveServer,以指定不同的預設全域轉譯模式。
  • [ExcludeFromInteractiveRouting] 標註的頁面會採用靜態 SSR (PageRenderMode 被指派 null)。
<!DOCTYPE html>
<html>
<head>
    ...
    <HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
    <Routes @rendermode="@PageRenderMode" />
    ...
</body>
</html>

@code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    private IComponentRenderMode? PageRenderMode
        => HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}

使用 HttpContext.AcceptsInteractiveRouting 擴充方法的替代方法是使用 HttpContext.GetEndpoint()?.Metadata 手動讀取端點中繼資料。

ASP.NET Core Blazor 轉譯模式中的參考文件會涵蓋這項功能。

建構函式插入

Razor 元件支援建構函式插入。

在下列範例中,部份 (code-behind) 類別會使用 主要建構函式 插入 NavigationManager 服務:

public partial class ConstructorInjection(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

如需詳細資訊,請參閱 ASP.NET Core Blazor 相依性插入

互動式伺服器元件的 WebSocket 壓縮

根據預設,互動式伺服器元件會啟用 WebSocket 連線的壓縮,並為 'self' 設定 frame-ancestors 內容安全性原則 (CSP) 指示詞組,這只允許將應用程式內嵌在啟用壓縮時或提供 WebSocket 內容的設定時,將應用程式內嵌在來源的 <iframe> 中。

ConfigureWebSocketOptions 設定為 null 可停用壓縮,這可降低 應用程式遭受攻擊的弱點,但可能會導致效能降低:

.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

使用 'none' 值設定較嚴格的 frame-ancestors CSP (需要單一引用),會允許 WebSocket 壓縮,但會防止瀏覽器將應用程式內嵌至任何 <iframe>:

.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")

如需詳細資訊,請參閱以下資源:

在 Blazor 中處理鍵盤組合事件

新的 KeyboardEventArgs.IsComposing 屬性會指出鍵盤事件是否為組合工作階段的一部分。 追蹤鍵盤事件的組合狀態對於處理國際字元輸入方法至關重要。

已將 OverscanCount 參數新增至 QuickGrid

QuickGrid 元件現在會公開 OverscanCount 屬性,該屬性會指定啟用虛擬化時,在可見區域之前和之後轉譯多少個其他資料列。

預設 OverscanCount 為 3。 下列範例會將 OverscanCount 增加為 4:

<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
    ...
</QuickGrid>

InputNumber 元件支援 type="range" 屬性

InputNumber<TValue> 元件支援 type="range" 屬性,該屬性會建立支援模型繫結與表單驗證的範圍輸入,通常轉譯為滑桿或轉盤控制項,而不是文字輸入框:

<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">
    <div>
        <label>
            Nacelle Count (2-6): 
            <InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2" 
                step="1" type="range" />
        </label>
    </div>
    <div>
        <button type="submit">Submit</button>
    </div>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private EngineSpecifications? Model { get; set; }

    protected override void OnInitialized() => Model ??= new();

    private void Submit() {}

    public class EngineSpecifications
    {
        [Required, Range(minimum: 2, maximum: 6)]
        public int NacelleCount { get; set; }
    }
}

每個伺服器專案有多個 Blazor Web App

.NET 10(2025 年 11 月),將考慮支援每個伺服器專案的多個 Blazor Web App。

如需詳細資訊,請參閱 針對每個伺服器專案的多個 Blazor Web 應用程式支援 (dotnet/aspnetcore #52216)。

SignalR

本章節會說明 SignalR 的新功能。

SignalR 中樞中的多型類型支援

中樞方法現在可以接受基底類別,而不是衍生類別,以啟用多型案例。 基底類型必須 加上註解才能允許多型

public class MyHub : Hub
{
    public void Method(JsonPerson person)
    {
        if (person is JsonPersonExtended)
        {
        }
        else if (person is JsonPersonExtended2)
        {
        }
        else
        {
        }
    }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
    public string Name { get; set; }
    public Person Child { get; set; }
    public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson
{
    public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson
{
    public string Location { get; set; }
}

SignalR 的改善活動

SignalR 現在有名為 Microsoft.AspNetCore.SignalR.Server 的 ActivitySource,其會發出中樞方法呼叫的事件:

  • 每個方法都是它自己的活動,因此在中樞方法呼叫期間發出活動的任何活動都位於中樞方法活動之下。
  • 中樞方法活動無父系。 這表示它們不會組合在長時間執行的 SignalR 連線下。

下列範例使用 .NET Aspire 儀表板OpenTelemetry 套件:

<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />

將下列啟動程式碼新增至 Program.cs 檔案:

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        if (builder.Environment.IsDevelopment())
        {
            // We want to view all traces in development
            tracing.SetSampler(new AlwaysOnSampler());
        }

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
    });

builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());

以下是 Aspire 儀表板的範例輸出:

SignalR 中樞方法呼叫事件的活動清單

SignalR 支援修剪與原生 AOT

延續自 .NET 8 開始的 原生 AOT 旅程 ,我們已針對 SignalR 用戶端與伺服器案例啟用修剪及原生預先 (AOT) 編譯支援。 您現可在使用 SignalR 即時 web 通訊的應用程式,使用原生 AOT 所帶來的效能優勢。

開始使用

安裝最新 .NET 9 SDK

使用下列命令,從命令殼層的 webapiaot 範本建立解決方案:

dotnet new webapiaot -o SignalRChatAOTExample

以下列 SignalR 程式碼來取代 Program.cs 檔案的內容:

using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
    o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
    <title>SignalR Chat</title>
</head>
<body>
    <input id="userInput" placeholder="Enter your name" />
    <input id="messageInput" placeholder="Type a message" />
    <button onclick="sendMessage()">Send</button>
    <ul id="messages"></ul>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
    <script>
        const connection = new signalR.HubConnectionBuilder()
            .withUrl("/chatHub")
            .build();

        connection.on("ReceiveMessage", (user, message) => {
            const li = document.createElement("li");
            li.textContent = `${user}: ${message}`;
            document.getElementById("messages").appendChild(li);
        });

        async function sendMessage() {
            const user = document.getElementById("userInput").value;
            const message = document.getElementById("messageInput").value;
            await connection.invoke("SendMessage", user, message);
        }

        connection.start().catch(err => console.error(err));
    </script>
</body>
</html>
""", "text/html"));

app.Run();

[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

上述範例會產生 10 MB 的原生 Windows 可執行檔,以及 10.9 MB 的 Linux 可執行檔。

限制

  • 目前僅支援 JSON 通訊協定:
    • 如上述程式代碼所示,使用 JSON 序列化與原生 AOT 的應用程式必須使用 System.Text.Json 來源產生器。
    • 這會遵循與最小 API 相同的方法。
  • 在 SignalR 伺服器,不支援 IAsyncEnumerable<T>ChannelReader<T> 類型的中樞方法參數,其中 T 是 ValueType (亦即struct)。 使用這些型別會導致在開發與已發佈的應用程式啟動時,發生執行階段例外狀況。 如需詳細資訊,請參閱 SignalR:在原生 AOT 使用 IAsyncEnumerable<T> 與 ChannelReader<T> 搭配 ValueTypes (dotnet/aspnetcore #56179)
  • 原生 AOT 不支援強型別中樞 (PublishAot)。 搭配原生 AOT 使用強型別中樞會導致在組建及發佈期間出現警告,以及執行階段例外狀況。 支援搭配修剪 (PublishedTrimmed) 使用強型別中樞。
  • 非同步傳回型別僅支援 TaskTask<T>ValueTaskValueTask<T>

最小 API

本節說明基本 API 的新功能。

已將 InternalServerErrorInternalServerError<TValue> 新增至 TypedResults

TypedResults 類別是從最小 API 傳回強型別 HTTP 狀態碼式回應的實用工具。 TypedResults 現在包含從端點傳回「500 內部伺服器錯誤」回應的 Factory 方法和類型。 以下是傳回 500 回應的範例:

var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));

app.Run();

在路由群組呼叫 ProducesProblemProducesValidationProblem

ProducesProblemProducesValidationProblem 擴充方法已更新,以支援其在路由群組上的使用。 這些方法表示路由群組的所有端點都可為 OpenAPI 中繼資料的目的傳回 ProblemDetailsValidationProblemDetails 回應。

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem();

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, boolean IsCompleted);

OpenAPI

本章節說明 OpenAPI 的新功能

適用於 OpenAPI 文件產生的內建支援

OpenAPI 規格是用於描述 HTTP API 的標準。 該標準可讓開發人員定義 API 的圖形,這些 API 可以插入用戶端產生器、伺服器產生器、測試工具、文件等等。 在 .NET 9 Preview 中,ASP.NET Core 可提供內建支援,以透過 Microsoft.AspNetCore.OpenApi 套件產生代表控制器型或最小 API 的 OpenAPI 文件。

下列醒目提示的程式碼會呼叫:

  • AddOpenApi,以將必要的相依性註冊到應用程式的 DI 容器中。
  • MapOpenApi,以在應用程式的路由中註冊必要的 OpenAPI 端點。
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();

使用下列命令在專案中安裝 Microsoft.AspNetCore.OpenApi 套件:

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

執行應用程式並瀏覽至 openapi/v1.json,以檢視產生的 OpenAPI 文件:

OpenAPI 文件

您也可以藉由新增 Microsoft.Extensions.ApiDescription.Server 套件,在建置階段產生 OpenAPI 文件:

dotnet add package Microsoft.Extensions.ApiDescription.Server --prerelease

在應用程式的專案檔中,新增下列項目:

<PropertyGroup>
  <OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
  <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>
</PropertyGroup>

在專案目錄中執行 dotnet build 並檢查所產生的 JSON 檔案。

建置階段的 OpenAPI 文件產生

ASP.NET Core 內建的 OpenAPI 文件產生,為各種自訂和選項提供支援。 它提供文件和作業轉換器,而且能夠管理相同應用程式的多份 OpenAPI 文件。

若要深入了解 ASP.NET Core 的新 OpenAPI 文件功能,請參閱新的 Microsoft.AspNetCore.OpenApi 文件

OpenAPI 套件的 Intellisense 完成增強功能

ASP.NET Core 的 OpenAPI 支援現在更容易存取且方便使用。 OpenAPI API 會以獨立套件的形式隨附,與共用架構區分。 到目前為止,這表示開發人員沒有適用於 OpenAPI API 的 Intellisense 等程式碼完成功能的便利性。

我們識別到此差距,引進了新的完成提供者和程式碼修正程式。 這些工具的設計目的是為了方便探索及使用 OpenAPI API,讓開發人員更輕鬆地將 OpenAPI 整合至其專案中。 完成提供者提供即時程式碼建議,而程式碼修正程式可協助修正常見的錯誤並改善 API 使用方式。 這項增強功能是我們持續致力於改善開發人員體驗並簡化 API 相關工作流程的一部分。

當使用者輸入 OpenAPI 相關 API 的陳述式時,完成提供者會顯示 API 的建議。 例如,在下列螢幕擷取畫面中,當使用者在支援的類型上輸入叫用陳述式時,會提供 AddOpenApiMapOpenApi 的完成,例如 IEndpointConventionBuilder

OpenAPI 完成

當接受完成且未安裝 Microsoft.AspNetCore.OpenApi 套件時,程式碼修正程式會提供快捷方式以自動在專案中安裝相依性。

自動套件安裝

支援參數和屬性上的 [Required][DefaultValue] 屬性

[Required][DefaultValue] 屬性套用至複雜類型內的參數或屬性時,OpenAPI 實作會將這些屬性對應至與參數或類型結構描述相關聯的 OpenAPI 文件中的 requireddefault 屬性。

例如,下列 API 會產生 Todo 類型的隨附結構描述。

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}
{
	"required": [
	  "title",
	  "description",
	  "createdOn"
	],
	"type": "object",
	"properties": {
	  "id": {
	    "type": "integer",
	    "format": "int32"
	  },
	  "title": {
	    "type": "string"
	  },
	  "description": {
	    "type": "string",
	    "default": "A new todo"
	  },
	  "createdOn": {
	    "type": "string",
	    "format": "date-time"
	  }
	}
}

OpenAPI 文件上的結構描述轉換器支援

內建 OpenAPI 支援現在隨附結構描述轉換器的支援,可用來修改 System.Text.Json 和 OpenAPI 實作所產生的結構描述。 如同文件和作業轉換器,結構描述轉換器可以在 OpenApiOptions 物件上進行註冊。 例如,下列範例程式碼示範如何使用結構描述轉換器,將範例新增至類型的結構描述。

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Microsoft.OpenApi.Any;

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi(options =>
{
    options.UseSchemaTransformer((schema, context, cancellationToken) =>
    {
        if (context.Type == typeof(Todo))
        {
            schema.Example = new OpenApiObject
            {
                ["id"] = new OpenApiInteger(1),
                ["title"] = new OpenApiString("A short title"),
                ["description"] = new OpenApiString("A long description"),
                ["createdOn"] = new OpenApiDateTime(DateTime.Now)
            };
        }
        return Task.CompletedTask;
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapPost("/todos", (Todo todo) => { });

app.Run();

class Todo
{
	public int Id { get; init; }
	public required string Title { get; init; }
	[DefaultValue("A new todo")]
	public required string Description { get; init; }
	[Required]
	public DateTime CreatedOn { get; init; }
}

改善 Microsoft.AspNetCore.OpenApi 的轉換器註冊 API

OpenAPI 轉換器支援修改 OpenAPI 文件、文件中的作業,或與 API 中類型相關聯的架構。 在 OpenAPI 文件註冊轉換器的 API 提供各種註冊轉換器的選項。

先前,下列 API 可用於註冊轉換器:

OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions UseTransformer<IOpenApiDocumentTransformer>()
OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>)
OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task>)

新 API 集合如下所示:

OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions AddDocumentTransformer<IOpenApiDocumentTransformer>()

OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer)
OpenApiOptions AddSchemaTransformer<IOpenApiSchemaTransformer>()

OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)
OpenApiOptions AddOperationTransformer<IOpenApiOperationTransformer>()

Microsoft.AspNetCore.OpenApi 支援修剪及原生 AOT

ASP.NET Core 新內建的 OpenAPI 支援現也支援修剪及原生 AOT。

開始使用

建立新 ASP.NET Core Web API (原生 AOT) 專案。

dotnet new webapiaot

新增 Microsoft.AspNetCore.OpenAPI 套件。

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

在此預覽版,您也需要新增最新 Microsoft.OpenAPI 套件,以避免修剪警告。

dotnet add package Microsoft.OpenApi

更新 Program.cs 以啟用產生 OpenAPI 文件。

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

發行應用程式。

dotnet publish

應用程式會使用原生 AOT 發佈,而不會發出警告。

支援在路由群組呼叫 ProducesProblemProducesValidationProblem

已更新路由群組的 ProducesProblemProducesValidationProblem 擴充方法。 這些方法可用來指出路由群組的所有端點都可為 OpenAPI 中繼資料的目的傳回 ProblemDetailsValidationProblemDetails 回應。

var app = WebApplication.Create();

var todos = app.MapGroup("/todos")
    .ProducesProblem(StatusCodes.Status500InternalServerError);

todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, bool IsCompleted);

ProblemValidationProblem 結果類型支援使用 IEnumerable<KeyValuePair<string, object?>> 值建構

在 .NET 9 之前,若要以最少 API 建構問題回報ValidationProblem 結果類型,需要實作 IDictionary<string, object?> 來初始化 errorsextensions 屬性。 在此版本,這些建構 API 支援取用 IEnumerable<KeyValuePair<string, object?>> 的多載。

var app = WebApplication.Create();

app.MapGet("/", () =>
{
    var extensions = new List<KeyValuePair<string, object?>> { new("test", "value") };
    return TypedResults.Problem("This is an error with extensions",
                                                       extensions: extensions);
});

感謝 GitHub 使用者 joegoldman2 提供此貢獻!

驗證與授權

本章節會說明驗證和授權的新功能。

OpenIdConnectHandler 新增支援推送授權要求 (PAR)

我們想要感謝 Duende SoftwareJoe DeCock 將推送授權要求 (PAR) 新增至 ASP.NET Core 的 OpenIdConnectHandler。 Joe 在其 API 提案說明啟用 PAR 的背景與動機如下:

推播的授權要求 (PAR) 是相對較新的 OAuth 標準,可藉由將授權參數從前端通道移至後端通道,來改善 OAuth 與 OIDC 流程的安全性。 也就是說,將授權參數從瀏覽器的重新導向 URL 移至後端的機器對機器 HTTP 直接呼叫。

這可防止瀏覽器網路攻擊者,使其無法:

  • 查看可能洩漏 PII 的授權參數。
  • 竄改這些參數。 例如,網路攻擊者可能會變更所要求的存取範圍。

推送授權參數也會讓要求 URL 保持簡短。 當使用更複雜的 OAuth 與 OIDC 功能時 (例如,豐富授權要求),授權參數可能會變得很長。 長 URL 會在許多瀏覽器與網路基礎結構造成問題。

OpenID Foundation 內的 FAPI 工作群組鼓勵使用 PAR。 例如,FAPI2.0 安全性設定檔需要使用 PAR。 許多從事開放銀行 (主要在歐洲)、醫療保健,以及其他具有高度安全性需求行業的團體都使用此安全性設定檔。

許多 identity 提供者支援 PAR,包括

針對 .NET 9,我們決定如下:如果 identity 提供者的探索文件公告支援 PAR,則我們預設啟用 PAR,因其應可為支援此功能的提供者增強安全性。 identity 提供者的探索文件通常位於 .well-known/openid-configuration。 如果造成問題,您可透過 OpenIdConnectOptions.PushedAuthorizationBehavior 停用 PAR,如下所示:

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect("oidc", oidcOptions =>
    {
        // Other provider-specific configuration goes here.

        // The default value is PushedAuthorizationBehavior.UseIfAvailable.

        // 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
        // and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
        // of type 'OpenIdConnectOptions' could be found
        oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
    });

若要確保僅當使用 PAR 時驗證才會成功,請改用 PushedAuthorizationBehavior.Require。 這項變更也會將新 OnPushAuthorization 事件引入 OpenIdConnectEvents,以便用來自訂推送授權要求或手動處理。 如需詳細資訊,請參閱 API 提案

OIDC 和 OAuth 參數自訂

OAuth 和 OIDC 驗證處理程式現在有個 AdditionalAuthorizationParameters 選項,可讓您更輕鬆地自訂通常包含在重新導向查詢字串中的授權訊息參數。 在 .NET 8 和更早版本中,這需要在自訂處理程式中要求自訂 OnRedirectToIdentityProvider 回撥或覆寫 BuildChallengeUrl 方法。 以下是 .NET 8 程式碼的範例:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.Events.OnRedirectToIdentityProvider = context =>
    {
        context.ProtocolMessage.SetParameter("prompt", "login");
        context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
        return Task.CompletedTask;
    };
});

上述範例現在可以簡化為下列程式碼:

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
    options.AdditionalAuthorizationParameters.Add("prompt", "login");
    options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});

設定 HTTP.sys 擴充驗證旗標

您現在可以使用新的 EnableKerberosCredentialCachingCaptureCredentials 屬性在 HTTP.sys AuthenticationManager 上設定 HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHINGHTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL HTTP.sys 旗標,以最佳化 Windows 驗證的處理方式。 例如:

webBuilder.UseHttpSys(options =>
{
    options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
    options.Authentication.EnableKerberosCredentialCaching = true;
    options.Authentication.CaptureCredentials = true;
});

其他

下列章節會說明其他的新功能。

新的 HybridCache 程式庫

HybridCache API 可縮小現有的 IDistributedCacheIMemoryCache API 中的一些差距。 它也會增加新功能,例如:

  • “Stampede” 保護可防止平行擷取相同工作。
  • 可設定的序列化。

HybridCache 旨在取代現有的 IDistributedCacheIMemoryCache 使用方式,並提供簡單的 API 來新增快取程式碼。 其可提供同時適用於同處理序和跨處理序快取的整合 API。

若要查看 HybridCache API 的簡化方式,請將它與使用 IDistributedCache 的程式碼進行比較。 以下是使用 IDistributedCache 的範例:

public class SomeService(IDistributedCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
        var bytes = await cache.GetAsync(key, token); // Try to get from cache.
        SomeInformation info;
        if (bytes is null)
        {
            // Cache miss; get the data from the real source.
            info = await SomeExpensiveOperationAsync(name, id, token);

            // Serialize and cache it.
            bytes = SomeSerializer.Serialize(info);
            await cache.SetAsync(key, bytes, token);
        }
        else
        {
            // Cache hit; deserialize it.
            info = SomeSerializer.Deserialize<SomeInformation>(bytes);
        }
        return info;
    }

    // This is the work we're trying to cache.
    private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
        CancellationToken token = default)
    { /* ... */ }
}

每次都要做很多工作,包括序列化之類的事情。 在快取遺漏案例中,您最終可能會有多個並行執行緒、全部都有快取遺漏、全部都擷取基礎資料、全部序列化,並且全部都將資料傳送至快取。

若要使用 HybridCache 簡化並改善此程式碼,我們首先需要新增程式庫 Microsoft.Extensions.Caching.Hybrid

<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />

註冊 HybridCache 服務,就像註冊 IDistributedCache 實作一樣:

builder.Services.AddHybridCache(); // Not shown: optional configuration API.

現在大部分的快取考量都可以卸除至 HybridCache

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync
        (string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // Unique key for this combination.
            async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
            token: token
        );
    }
}

我們可透過相依性插入提供 HybridCache 抽象類別的具體實作,但其目的是讓開發人員可以提供 API 的自訂實作。 HybridCache 實作可處理與快取相關的所有事宜,包括並行作業處理。 這裡的 cancel 權杖代表所有並行呼叫者的合併取消,而不只是我們可以看到的呼叫者取消 (也就是 token)。

您可以使用 TState 模式進一步最佳化高輸送量案例,以避免擷取到的變數和每個執行個體回撥所造成的一些額外負荷:

public class SomeService(HybridCache cache)
{
    public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
    {
        return await cache.GetOrCreateAsync(
            $"someinfo:{name}:{id}", // unique key for this combination
            (name, id), // all of the state we need for the final call, if needed
            static async (state, token) =>
                await SomeExpensiveOperationAsync(state.name, state.id, token),
            token: token
        );
    }
}

HybridCache 會使用已設定的 IDistributedCache 實作 (如果有的話),以進行次要的跨處理序快取,例如,使用 Redis。 但即使沒有 IDistributedCacheHybridCache 服務仍會提供同處理序快取和「踩踏」保護。

有關物件重複使用的備註

在使用 IDistributedCache 的一般現有程式碼中,每次從快取中擷取物件都會導致還原序列化。 此行為表示每個並行呼叫者都會取得物件的不同執行個體,而該執行個體無法與其他執行個體互動。 結果是執行緒安全性,因為對相同物件執行個體的並行修改不會產生風險。

由於會根據現有的 IDistributedCache 程式碼來調整許多 HybridCache 使用方式,因此 HybridCache 預設會保留此行為,以避免引進並行錯誤 (bug)。 不過,指定的使用案例原本就是安全執行緒:

  • 如果快取的類型是不可變的。
  • 如果程式碼不修改它們。

在這種情況下,請通知 HybridCache 透過下列方式可安全地重複使用執行個體:

  • 將類型標示為 sealed。 C# 中的 sealed 關鍵字表示無法繼承該類別。
  • [ImmutableObject(true)] 屬性套用於它。 [ImmutableObject(true)] 屬性表示物件的狀態在建立之後即無法變更。

藉由重複使用執行個體,HybridCache 即可降低與每次呼叫的還原序列化相關聯的 CPU 和物件配置額外負荷。 在快取物件很大或受到頻繁存取的案例中,這可以提高效能。

其他 HybridCache 個功能

如同 IDistributedCacheHybridCache 可支援使用 RemoveKeyAsync 方法來依索引鍵移除。

HybridCache 也會提供 IDistributedCache 實作的選用 API,以避免 byte[] 配置。 這項功能由 Microsoft.Extensions.Caching.StackExchangeRedisMicrosoft.Extensions.Caching.SqlServer 套件的預覽版本實作。

序列化會在註冊服務的過程中設定,可透過 WithSerializer.WithSerializerFactory 方法支援類型特定和一般化序列化程式,並可從 AddHybridCache 呼叫鏈結。 根據預設,程式庫會在內部處理 stringbyte[],並使用 System.Text.Json 處理其他所有項目,但您可以使用 protobuf、xml 或其他任何項目。

HybridCache 可支援舊版 .NET 執行階段,甚至包括 .NET Framework 4.7.2 和 .NET Standard 2.0。

如需 HybridCache 的詳細資訊,請參閱 ASP.NET Core 中的 HybridCache 程式庫

開發人員例外狀況頁面改進項目

當應用程式在開發期間擲回未處理的例外狀況時,會隨即顯示 ASP.NET Core 開發人員例外狀況頁面。 開發人員例外狀況頁面可提供例外狀況和要求的詳細資訊。

Preview 3 已將端點中繼資料新增至開發人員例外狀況頁面。 ASP.NET Core 會使用端點中繼資料來控制端點行為,例如路由、回應快取、速率限制、OpenAPI 產生等等。 下圖會顯示開發人員例外狀況頁面 Routing 區段中的新中繼資料資訊:

開發人員例外狀況頁面上的新中繼資料資訊

在測試開發人員例外狀況頁面時,已識別出生活品質略有改善。 它們已隨附於 Preview 4:

  • 更好的文字換行。 長 Cookie、查詢字串值和方法名稱無法再新增水平瀏覽器捲軸。
  • 在現代設計中可發現更大的文字。
  • 更一致的資料表大小。

下列動畫影像會顯示新的開發人員例外狀況頁面:

新的開發人員例外狀況頁面

字典偵錯改善

字典和其他索引鍵/值集合的偵錯顯示已改善版面配置。 索引鍵會顯示在偵錯工具的索引鍵資料行中,而不是與值串連。 下列影像顯示偵錯工具中字典的舊顯示與新顯示。

之前:

先前的偵錯工具體驗

新的偵錯工具體驗

ASP.NET Core 有許多索引鍵/值集合。 這個改良的偵錯體驗會套用至:

  • HTTP 標頭
  • 查詢字串
  • 表單
  • Cookie
  • 檢視資料
  • 路由資料
  • 功能

IIS 中應用程式回收期間的 503 修正程式

根據預設,當 IIS 收到回收或關機通知以及當 ANCM 告知受控伺服器啟動關機時,現在會有一秒的延遲。 延遲可透過 ANCM_shutdownDelay 環境變數或設定 shutdownDelay 處理常式設定來設定。 兩個值的單位都是毫秒。 延遲主要是為了減少以下情況下比賽的可能性:

  • IIS 尚未啟動移至新應用程式的佇列要求。
  • ANCM 會開始拒絕進入舊應用程式的新要求。

較緩慢的機器或 CPU 使用量較繁重的機器可能要調整此值,以降低 503 的可能性。

設定 shutdownDelay 的範例:

<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
  <handlerSettings>
    <!-- Milliseconds to delay shutdown by.
    this doesn't mean incoming requests will be delayed by this amount,
    but the old app instance will start shutting down after this timeout occurs -->
    <handlerSetting name="shutdownDelay" value="5000" />
  </handlerSettings>
</aspNetCore>

修正程式位於來自裝載套件組合且已全域安裝的 ANCM 模組中。

最佳化靜態 Web 資產傳遞

遵循提供靜態資產的生產最佳做法需要大量的工作和技術專長。 未經最佳化,例如壓縮、快取和指紋

  • 瀏覽器必須在每個頁面載入時提出其他要求。
  • 透過網路傳輸的位元組數超出了所需的位元組數。
  • 有時會將過時的檔案版本提供給用戶端。

建立高效能的 Web 應用程式需要最佳化資產來傳遞至瀏覽器。 可能的最佳化包括:

  • 提供指定的資產一次,直到檔案變更或瀏覽器清除其快取為止。 設定 ETag 標頭。
  • 在更新應用程式之後,防止瀏覽器使用舊版或過時的資產。 設定上次的修改標頭。
  • 設定適當的快取標頭
  • 使用快取中介軟體
  • 盡可能提供壓縮版本的資產。
  • 使用 CDN 來提供更接近使用者的資產。
  • 將提供給瀏覽器的資產大小降至最低。 此最佳化不包括縮製。

MapStaticAssets 是一個新的中介軟體,可協助最佳化應用程式中靜態資產的傳遞。 其設計目的是在與所有 UI 架構配合使用,包括 Blazor、Razor Pages 和 MVC。 通常是 UseStaticFiles 的直接替換項目:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();

app.Run();

MapStaticAssets 會透過結合建置和發佈時間流程來運作,以收集有關應用程式中所有靜態資源的資訊。 然後,執行階段庫會利用此資訊來有效地將這些檔案提供給瀏覽器。

在大部分情況下,MapStaticAssets 可以取代 UseStaticFiles,不過,它已針對服務應用程式在建置和發佈時間所了解的資產進行了最佳化。 如果應用程式提供來自其他位置的資產 (例如磁碟或內嵌資源),則應使用 UseStaticFiles

MapStaticAssets 提供了 UseStaticFiles 所沒有的以下優點:

  • 應用程式中所有資產的建置時間壓縮:
    • 開發期間的 gzip 和發佈期間的 gzip + brotli
    • 所有資產都會被壓縮,目標是將資產的大小降到最低。
  • 基於內容的 ETags:每個資源的 Etags 都是內容的 SHA-256 雜湊的 Base64 編碼字串。 這可確保瀏覽器只有在檔案的內容已變更時才會重新下載檔案。

下表顯示預設 Razor Pages 範本中 CSS 和 JS 檔案的原始和壓縮大小:

檔案 原始 Compressed 減少百分比
bootstrap.min.css 163 17.5 89.26%
jquery.js 89.6 28 68.75%
bootstrap.min.js 78.5 20 74.52%
總數 331.1 65.5 80.20%

下表顯示使用 Fluent UI Blazor 元件庫的原始和壓縮大小:

檔案 原始 Compressed 減少百分比
fluent.js 384 73 80.99%
fluent.css 94 11 88.30%
總數 478 84 82.43%

未壓縮的總大小為 478 KB,壓縮後的大小為 84 KB。

下表顯示使用 MudBlazorBlazor 元件庫的原始與壓縮大小:

檔案 原始 Compressed 減少
MudBlazor.min.css 541 37.5 93.07%
MudBlazor.min.js 47.4 9.2 80.59%
總數 588.4 46.7 92.07%

使用 MapStaticAssets 時,會自動進行最佳化。 當新增或更新程式庫時 (例如使用新的 JavaScript 或 CSS),資產將作為建置的一部分進行最佳化。 最佳化對於頻寬較低或連線不可靠的行動環境特別有利。

如需新檔案傳遞功能的詳細資訊,請參閱下列資源:

在伺服器上啟用動態壓縮與使用 MapStaticAssets

與伺服器上的動態壓縮相比,MapStaticAssets 具有以下優點:

  • 更簡單,因為沒有伺服器特定的組態。
  • 效能更高,因為資產會在建置時間被壓縮。
  • 可讓開發人員在建置過程中花費額外的時間來確保資產的大小最小。

考慮以下表格,將 MudBlazor 壓縮與 IIS 動態壓縮和 MapStaticAssets 進行比較:

IIS gzip MapStaticAssets MapStaticAssets 減少量
≅ 90 37.5 59%

ASP0026:當 [AllowAnonymous] 從「更遠」覆寫 [Authorize] 時,分析器會發出警告

[AllowAnonymous] 屬性相比,將 [Authorize] 屬性放置在「更接近」MVC 動作的位置會覆寫 [AllowAnonymous] 屬性並強制授權,這似乎是直觀的。 不過,不一定是這種情況。 重要的是屬性的相對順序。

下列程式碼顯示更接近 [Authorize] 屬性的範例,會由離得更遠的 [AllowAnonymous] 屬性覆寫。

[AllowAnonymous]
public class MyController
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on the class
    public IActionResult Private() => null;
}
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon
{
    [Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
    public IActionResult Private() => null;
}
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}

在 .NET 9 Preview 6 中,我們引進了分析器,其中會醒目顯示此類的執行個體,其中更接近 [Authorize] 屬性會由遠離 MVC 動作的 [AllowAnonymous] 屬性覆寫。 警告會指向已覆寫的 [Authorize] 屬性,並顯示下列訊息:

ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away

如果您看到此警告,則所要採取的正確動作取決於屬性背後的意圖。 如果不小心將端點公開給匿名使用者,則應該移除更遠的 [AllowAnonymous] 屬性。 如果 [AllowAnonymous] 屬性是要覆寫更接近的 [Authorize] 屬性,您可以在 [Authorize] 屬性之後重複 [AllowAnonymous] 屬性,以釐清意圖。

[AllowAnonymous]
public class MyController
{
    // This produces no warning because the second, "closer" [AllowAnonymous]
    // clarifies that [Authorize] is intentionally overridden.
    // Specifying AuthenticationSchemes can still be useful
    // for endpoints that allow but don't require authenticated users.
    [Authorize(AuthenticationSchemes = "Cookies")]
    [AllowAnonymous]
    public IActionResult Privacy() => null;
}

改善 Kestrel 連線計量

我們已藉由包含連線失敗原因的相關中繼資料,大幅改善 Kestrel 連線計量。 kestrel.connection.duration 計量現會在 error.type 屬性包含連線關閉原因。

以下是 error.type 值的小型樣本:

  • tls_handshake_failed - 連線需要 TLS,但 TLS 交握失敗。
  • connection_reset - 當要求正在進行時,用戶端意外關閉連線。
  • request_headers_timeout - Kestrel 關閉連線,因其未及時收到要求標頭。
  • max_request_body_size_exceeded - Kestrel 關閉連線,因為上傳資料超過大小上限。

先前,診斷 Kestrel 連線問題需要伺服器記錄詳細的低階記錄。 然而,產生及儲存記錄的成本可能很高,且很難在雜訊中找到正確資訊。

計量是更便宜的替代方案,可在實際執行環境開啟,且影響極小。 收集的計量可驅動儀表板與警示。 在利用計量於高階層識別問題之後,就可開始使用記錄與其他工具進行進一步調查。

我們預期改善的連線計量可在許多案例派上用場:

  • 調查連線存留期短所造成的效能問題。
  • 觀察對效能與穩定性造成影響的 Kestrel 持續外部攻擊。
  • 記錄嘗試對 Kestrel 進行的外部攻擊,而 Kestrel 的內建安全性強化功能阻止了這些攻擊。

如需詳細資訊,請參閱 ASP.NET Core 計量

自訂 Kestrel 具名管道端點

已使用進階自訂選項來改善 Kestrel 具名管道支援。 具名管道選項的新 CreateNamedPipeServerStream 方法可讓管道依端點進行自訂。

其中一項實用範例是,Kestrel 應用程式需要兩個具有不同存取安全性的管道端點。 CreateNamedPipeServerStream 選項可用來根據管道名稱建立具有自訂安全性設定的管道。

var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenNamedPipe("pipe1");
    options.ListenNamedPipe("pipe2");
});

builder.WebHost.UseNamedPipes(options =>
{
    options.CreateNamedPipeServerStream = (context) =>
    {
        var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndpoint.PipeName);

        return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
            NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
            context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
    };
});

ExceptionHandlerMiddleware 選項可根據例外狀況類型選擇狀態代碼

設定 ExceptionHandlerMiddleware 時的新選項可讓應用程式開發人員選擇,在要求處理期間發生例外狀況時,要傳回的狀態代碼。 新選項會變更來自 ExceptionHandlerMiddleware 回應 ProblemDetails 所設定的狀態代碼。

app.UseExceptionHandler(new ExceptionHandlerOptions
{
    StatusCodeSelector = ex => ex is TimeoutException
        ? StatusCodes.Status503ServiceUnavailable
        : StatusCodes.Status500InternalServerError,
});

在特定端點與要求退出 HTTP 計量

.NET 9 引進了針對特定端點與要求退出 HTTP 計量的功能。 對於經常被自動化系統呼叫的端點 (例如健康情況檢查),退出記錄計量可帶來益處。 這些要求通常不需要記錄計量。

藉由新增中繼資料,即可從計量排除對端點的 HTTP 要求。 任一:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();

已新增 MetricsDisabled 屬性至下列 IHttpMetricsTagsFeature,以便:

  • 用於要求未對應至端點的進階案例。
  • 動態停用特定 HTTP 要求的計量集合。
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
    var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
    if (metricsFeature != null &&
        context.Request.Headers.ContainsKey("x-disable-metrics"))
    {
        metricsFeature.MetricsDisabled = true;
    }

    await next(context);
});

刪除金鑰的資料保護支援

在 .NET 9 之前,資料保護金鑰無法透過設計來刪除,以防止資料外洩。 刪除金鑰會導致無法復原其受保護資料。 鑒於其大小較小,這些金鑰的累積通常僅造成極小影響。 然而,為容納非常長時間執行的服務,我們引進了刪除金鑰選項。 一般而言,應僅刪除舊金鑰。 僅當您可接受以資料遺失風險換取儲存空間時,才刪除金鑰。 我們建議應刪除資料保護金鑰。

using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();
services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)
{
    var utcNow = DateTimeOffset.UtcNow;
    var yearAgo = utcNow.AddYears(-1);

    if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
    {
        Console.WriteLine("Failed to delete keys.");
    }
    else
    {
        Console.WriteLine("Old keys deleted successfully.");
    }
}
else
{
    Console.WriteLine("Key manager does not support deletion.");
}

中介軟體支援具有索引鍵的 DI

中介軟體現於建構函式與 /InvokeInvokeAsync 方法中都支援具有索引鍵的 DI

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();

internal class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next,
        [FromKeyedServices("test")] MySingletonClass service)
    {
        _next = next;
    }

    public Task Invoke(HttpContext context,
        [FromKeyedServices("test2")]
            MyScopedClass scopedService) => _next(context);
}

信任 Linux 上的 ASP.NET Core HTTPS 開發憑證

在 Ubuntu 與 Fedora Linux 發行版本上,dotnet dev-certs https --trust 現在會將 ASP.NET Core HTTPS 開發憑證設定為受信任的憑證,用於:

  • Chromium 瀏覽器,例如 Google Chrome、Microsoft Edge 及 Chromium。
  • Mozilla Firefox 與 Mozilla 衍生瀏覽器。
  • .NET API,例如 HttpClient

先前,--trust 只能在 Windows 與 macOS 上運作。 每一使用者都會套用憑證信任。

若要在 OpenSSL 中建立信任,dev-certs 工具會:

  • 將憑證放入 ~/.aspnet/dev-certs/trust
  • 在目錄上,執行簡化的 OpenSSL c_rehash 工具版本。
  • 要求使用者更新 SSL_CERT_DIR 環境變數。

若要在 dotnet 中建立信任,此工具會將憑證放入 My/Root 憑證存放區。

若要在 NSS 資料庫中建立信任 (如果有的話),此工具會在 home 目錄搜尋 Firefox 設定檔、~/.pki/nssdb~/snap/chromium/current/.pki/nssdb。 對於找到的每個目錄,此工具都會對 nssdb 新增一個項目。