Freigeben über


Neuerungen in ASP.NET Core 9.0

In diesem Artikel werden die wichtigsten Änderungen in ASP.NET Core 9.0 aufgezeigt und Links zur relevanten Dokumentation bereitgestellt.

Optimierung der Übermittlung statischer Ressourcen

MapStaticAssets Routingendpunktkonventionen sind ein neues Feature, das die Bereitstellung statischer Ressourcen in ASP.NET Core-Apps optimiert.

Informationen zur Bereitstellung statischer Ressourcen für Blazor Apps finden Sie unter ASP.NET Core Blazor static files.

Die Einhaltung bewährter Methoden für die Bereitstellung statischer Ressourcen erfordert einen erheblichen Arbeitsaufwand und technisches Fachwissen. Ohne Optimierungen wie Komprimierung, Zwischenspeichern und Fingerabdrücke:

  • Der Browser muss zusätzliche Anforderungen an jeden Seiteladevorgang stellen.
  • Mehr Bytes als erforderlich werden über das Netzwerk übertragen.
  • Manchmal werden veraltete Versionen von Dateien für Clients bereitgestellt.

Das Erstellen von leistungsfähigen Web-Apps erfordert die Optimierung der Ressourcenübermittlung an den Browser. Mögliche Optimierungen sind:

  • Eine bestimmte Ressource wird einmal bereitgestellt, bis sich die Datei ändert oder der Browser seinen Cache leert. Legen Sie den ETag-Header fest.
  • Es wird verhindert, dass der Browser alte oder veraltete Ressourcen verwendet, nachdem eine Anwendung aktualisiert wurde. Legen Sie den Header Zuletzt geändert fest.
  • Legen Sie den richtigen Zwischenspeicherungsheader fest.
  • Verwenden Sie die Zwischenspeicherungs-Middleware.
  • Stellen Sie wenn möglich komprimierte Versionen der Ressourcen bereit.
  • Verwenden Sie einen CDN, um die Ressourcen näher an die Benutzerin oder den Benutzer zu bringen.
  • Es minimiert die Größe der Ressourcen, die dem Browser zur Verfügung gestellt werden. Diese Optimierung beinhaltet keine Minimierung.

MapStaticAssets ist ein neues Feature, das die Bereitstellung statischer Objekte in einer App optimiert. Sie ist für alle UI-Frameworks geeignet, einschließlich Blazor, Razor Pages und MVC. Es ist in der Regel ein Drop-In-Ersatz für 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();

Der Vorgang von MapStaticAssets besteht in der Kombination von Build- und Veröffentlichungsprozessen, um Informationen über alle statischen Ressourcen in einer Anwendung zu sammeln. Diese Informationen werden dann von der Laufzeitbibliothek verwendet, um diese Dateien effizient an den Browser zu liefern.

MapStaticAssets kann UseStaticFiles in den meisten Situationen ersetzen, ist aber für die Bereitstellung der Ressourcen optimiert, die der App zum Zeitpunkt der Erstellung und Veröffentlichung bekannt sind. Wenn die App Ressourcen von anderen Speicherorten bereitstellt, z. B. von der Festplatte oder von eingebetteten Ressourcen, sollte UseStaticFiles verwendet werden.

MapStaticAssets bietet die folgenden Vorteile, die UseStaticFiles nicht hat:

  • Buildzeitkomprimierung für alle Ressourcen in der App:
    • gzip während der Entwicklung und gzip + brotli während der Veröffentlichung.
    • Alle Ressourcen werden mit dem Ziel komprimiert, die Größe der Ressourcen auf ein Minimum zu reduzieren.
  • Inhaltsbasierte ETags: Die Etags für jede Ressource sind die Base64-codierte Zeichenfolge des SHA-256-Hashs des Inhalts. Dadurch wird sichergestellt, dass der Browser eine Datei nur dann erneut lädt, wenn sich der Inhalt geändert hat.

In der folgenden Tabelle sind die ursprünglichen und komprimierten Größen der CSS- und JS-Dateien in der Razor Pages-Standardvorlage aufgeführt:

Datei Ursprünglich Compressed % Reduzierung
bootstrap.min.css 163 17,5 89,26 %
jquery.js 89,6 28 68,75 %
bootstrap.min.js 78,5 20 74,52 %
Gesamt 331,1 65,5 80,20 %

Die folgende Tabelle zeigt die ursprünglichen und komprimierten Größen mithilfe der Fluent UI Blazor Komponentenbibliothek:

Datei Ursprünglich Compressed % Reduzierung
fluent.js 384 73 80,99 %
fluent.css 94 11 88,30 %
Gesamt 478 84 82,43 %

Ergebnis: 478 KB unkomprimiert zu 84 KB komprimiert.

Die folgende Tabelle zeigt die originalen und komprimierten Größen mithilfe der MudBlazorBlazor-Komponentenbibliothek:

Datei Ursprünglich Compressed Reduzierung
MudBlazor.min.css 541 37,5 93,07 %
MudBlazor.min.js 47,4 9.2 80,59 %
Gesamt 588,4 46,7 92,07 %

Optimierung erfolgt automatisch bei Verwendung von MapStaticAssets. Wenn eine Bibliothek hinzugefügt oder aktualisiert wird, z. B. mit neuem JavaScript oder CSS, werden die Ressourcen als Teil des Builds optimiert. Die Optimierung ist besonders für mobile Umgebungen von Vorteil, die eine geringere Bandbreite oder unzuverlässige Verbindungen haben können.

Weitere Informationen über die neuen Funktionen zur Bereitstellung von Dateien finden Sie in den folgenden Ressourcen:

Aktivieren der dynamischen Komprimierung auf dem Server gegenüber der Verwendung von MapStaticAssets

MapStaticAssets hat die folgenden Vorteile gegenüber der dynamischen Komprimierung auf dem Server:

  • Es ist einfacher, da keine serverspezifische Konfiguration vorhanden ist.
  • Es ist leistungsfähiger, da die Ressourcen zur Erstellungszeit komprimiert werden.
  • Es ermöglicht der Entwicklerin oder dem Entwickler, während des Buildprozesses zusätzliche Zeit damit zu verbringen, sicherzustellen, dass die Ressourcen die Mindestgröße aufweisen.

Betrachten Sie die folgende Tabelle, in der die MudBlazor-Komprimierung mit der dynamischen IIS-Komprimierung und MapStaticAssets verglichen wird:

IIS gzip MapStaticAssets MapStaticAssets Reduktion
≅ 90 37,5 59 %

Blazor

In diesem Abschnitt werden neue Features für Blazor beschrieben.

.NET MAUIBlazor Hybrid und Web App-Lösungsvorlage

Eine neue Lösungsvorlage erleichtert die Erstellung von .NET MAUI-nativen und Blazor-Web-Client-Apps, die dieselbe Benutzeroberfläche verwenden. Diese Vorlage zeigt, wie Sie Client-Apps erstellen, welche die Wiederverwendung von Code maximieren und Android, iOS, Mac, Windows und Web verwenden.

Zu den wichtigsten Funktionen dieser Vorlage gehören:

  • Die Möglichkeit, einen interaktiven Blazor-Rendering-Modus für die Web-App zu wählen.
  • Automatische Erstellung der entsprechenden Projekte, einschließlich einer Blazor Web App (globales interaktives Autorendering) und einer .NET MAUIBlazor Hybrid-App.
  • Die erstellten Projekte verwenden eine freigegebene Razor Klassenbibliothek (RCL), um die Komponenten der Benutzeroberfläche Razor zu verwalten.
  • Beispielcode ist enthalten, der veranschaulicht, wie Abhängigkeitsinjektion verwendet wird, um verschiedene Schnittstellenimplementierungen für die Blazor Hybrid-App und die Blazor Web App bereitzustellen.

Um loszulegen, installieren Sie das .NET 9 SDK und installieren Sie den Workload .NET MAUI, der die Vorlage enthält:

dotnet workload install maui

Erstellen Sie eine Projektmappe aus der Projektvorlage in einer Befehlsshell mithilfe des folgenden Befehls:

dotnet new maui-blazor-web

Die Vorlage ist auch in Visual Studio verfügbar.

Hinweis

Derzeit tritt eine Ausnahme auf, wenn Blazor-Rendermodi auf Seiten-/Komponentenebene definiert sind. Weitere Informationen finden Sie unter BlazorWebView benötigt eine Möglichkeit, ResolveComponentForRenderMode (dotnet/aspnetcore #51235) zu aktivieren.

Weitere Informationen finden Sie unter Erstellen einer .NET MAUIBlazor Hybrid-App mit einer Blazor Web App.

Erkennen des Renderorts, der Interaktivität und des zugewiesenen Rendermodus zur Laufzeit

Wir haben eine neue API eingeführt, die das Abfragen von Komponentenzuständen zur Laufzeit vereinfacht. Diese API bietet die folgenden Funktionen:

  • Ermitteln Sie den aktuellen Ausführungsort der Komponente: Dies kann hilfreich für das Debuggen und Optimieren der Komponentenleistung sein.
  • Überprüfen Sie, ob die Komponente in einer interaktiven Umgebung ausgeführt wird: Dies kann für Komponenten hilfreich sein, die je nach Interaktivität ihrer Umgebung unterschiedliche Verhaltensweisen aufweisen.
  • Abrufen des zugewiesenen Rendermodus für die Komponente: Das Verständnis des Rendermodus kann dabei helfen, den Renderingprozess zu optimieren und die Gesamtleistung einer Komponente zu verbessern.

Weitere Informationen finden Sie unter ASP.NET CoreBlazor-Rendermodi.

Verbesserte serverseitige erneute Verbindung:

Die folgenden Verbesserungen wurden an der serverseitigen Standardverbindungsumgebung vorgenommen:

  • Wenn der Benutzer zurück zu einer App mit einer getrennten Verbindung navigiert, wird die erneute Verbindung sofort versucht, anstatt auf die Dauer des nächsten erneuten Verbindungsintervalls zu warten. Dies verbessert die Benutzererfahrung, wenn Sie in einer Browser-Registerkarte, die in den Ruhezustand übergegangen ist, zu einer App navigieren.

  • Wenn ein erneuter Verbindungsversuch den Server erreicht, der Server aber bereits den Schaltkreis freigegeben hat, tritt automatisch eine Seitenaktualisierung auf. Dadurch wird verhindert, dass der Benutzer die Seite manuell aktualisieren muss, wenn dies wahrscheinlich zu einer erfolgreichen erneuten Verbindung führt.

  • Die Zeitdauer für die erneute Verbindung verwendet eine berechnete Backoffstrategie. Standardmäßig treten die ersten Wiederholungsversuche in schneller Folge ohne Wiederholungsintervall auf, bevor berechnete Verzögerungen zwischen Versuchen eingeführt werden. Sie können das Wiederholungsintervallverhalten anpassen, indem Sie eine Funktion zum Berechnen des Wiederholungsintervalls angeben, wie das folgende exponentielle Backoff-Beispiel veranschaulicht:

    Blazor.start({
      circuit: {
        reconnectionOptions: {
          retryIntervalMilliseconds: (previousAttempts, maxRetries) => 
            previousAttempts >= maxRetries ? null : previousAttempts * 1000
        },
      },
    });
    
  • Das Design der Standard-Benutzeroberfläche für die Wiederverbindung wurde modernisiert.

Weitere Informationen finden Sie in den Anleitungen zu ASP.NET Core BlazorSignalR.

Vereinfachte Serialisierung des Authentifizierungsstatus für Blazor Web Apps

Neue APIs erleichtern das Hinzufügen der Authentifizierung zu einer bestehenden Blazor Web App. Wenn Sie eine neue Blazor Web App mit Authentifizierung über Einzelkonten erstellen und die WebAssembly-basierte Interaktivität aktivieren, enthält das Projekt sowohl im Server- als auch im Client-Projekt einen benutzerdefinierten AuthenticationStateProvider.

Diese Anbieter geben den Authentifizierungsstatus des Benutzers an den Browser weiter. Die Authentifizierung auf dem Server statt auf dem Client ermöglicht der Anwendung den Zugriff auf den Authentifizierungsstatus während des Prerenderings und vor der Initialisierung der .NET WebAssembly-Laufzeit.

Die benutzerdefinierten AuthenticationStateProvider-Implementierungen verwenden den Dienst Persistent Component State (PersistentComponentState), um den Authentifizierungsstatus in HTML-Kommentare zu serialisieren und ihn von WebAssembly zurückzulesen, um eine neue AuthenticationState-Instanz zu erstellen.

Dies funktioniert gut, wenn Sie von der Projektvorlage Blazor Web App ausgehen und die Option Individuelle Konten ausgewählt haben, aber es ist eine Menge Code, den Sie implementieren oder kopieren müssen, wenn Sie versuchen, die Authentifizierung zu einem bestehenden Projekt hinzuzufügen. Es gibt jetzt APIs, die jetzt Teil der Blazor Web App-Projektvorlage sind, die in Server- und Clientprojekten aufgerufen werden können, um diese Funktionalität hinzuzufügen:

Diese API serialisiert standardmäßig nur die serverseitigen Namens- und Rollenansprüche für den Zugriff im Browser. Eine Option kann an AddAuthenticationStateSerialization übergeben werden, um alle Ansprüche einzuschließen.

Weitere Informationen finden Sie in den folgenden Abschnitten ASP.NET Kernauthentifizierung Blazor und Autorisierung:

Hinzufügen statischer serverseitiger Renderingseiten (SSR) zu einer global interaktiven Blazor Web App

Mit der Veröffentlichung von .NET 9 ist es jetzt einfacher, statische SSR-Seiten zu Apps hinzuzufügen, die globale Interaktivität übernehmen.

Dieser Ansatz ist nur hilfreich, wenn bestimmte Seiten der App nicht mit interaktivem Server- oder WebAssembly-Rendering funktionieren. Dieser Ansatz eignet sich z. B. für Seiten, die vom Lesen/Schreiben von HTTP-Cookies abhängig sind und nur in einem Anforderungs-/Antwortzyklus funktionieren können, aber nicht mit interaktivem Rendering. Für Seiten, die mit interaktivem Rendering funktionieren, sollten Sie kein statisches SSR-Rendering erzwingen, da es weniger reaktionsfähig und weniger effizient für den Endbenutzer ist.

Markieren Sie eine beliebige Razor Komponentenseite mit dem neuen[ExcludeFromInteractiveRouting] Attribut, das der @attributeRazor Direktive zugewiesen ist:

@attribute [ExcludeFromInteractiveRouting]

Durch das Anwenden des Attributs wird die Navigation zu der Seite über das interaktive Routing beendet. Die eingehende Navigation wird gezwungen, die Seite vollständig neu zu laden, anstatt die Seite über das interaktive Routing aufzulösen. Das vollständige Neuladen zwingt die Stammkomponente der obersten Ebene, in der Regel die App-Komponente (App.razor), vom Server erneut zu rendern, sodass die App zu einem anderen Rendermodus auf oberster Ebene wechseln kann.

Mit RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting der Erweiterungsmethode kann die Komponente erkennen, ob das [ExcludeFromInteractiveRouting] Attribut auf die aktuelle Seite angewendet wird.

Verwenden Sie in der App-Komponente das Muster im folgenden Beispiel:

  • Seiten, die nicht mit dem Attribut versehen sind, das [ExcludeFromInteractiveRouting] standardmäßig für den InteractiveServer Rendermodus mit globaler Interaktivität verwendet wird. Sie können InteractiveServer durch InteractiveWebAssembly oder InteractiveAuto ersetzen, um einen anderen standardmäßigen globalen Rendermodus anzugeben.
  • Mit dem [ExcludeFromInteractiveRouting] Attribut kommentierte Seiten übernehmen statische SSR (PageRenderMode wird zugewiesen 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;
}

Eine Alternative zur Verwendung der RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting-Erweiterungsmethode besteht darin, Endpunktmetadaten manuell mithilfe von HttpContext.GetEndpoint()?.Metadata zu lesen.

Dieses Feature wird in der Referenzdokumentation in ASP.NET Core Blazor Rendermodi behandelt.

Constructor Injection

Razor Komponenten unterstützen die Konstruktoreinfügung.

Im folgenden Beispiel fügt die partielle (CodeBehind)-Klasse den NavigationManager Dienst mithilfe eines primären Konstruktors ein:

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

Weitere Informationen finden Sie unter Abhängigkeitsinjektion in ASP.NET Core Blazor.

Websocket-Komprimierung für interaktive Serverkomponenten

Standardmäßig aktivieren interaktive Serverkomponenten die Komprimierung für WebSocket-Verbindungen und legen eine frame-ancestors-CSP-Anweisung (Content Security Policy) auf 'self' fest, die nur das Einbetten der App in <iframe> vom Ursprung ermöglicht, von dem die App bei aktivierter Komprimierung bereitgestellt wird oder wenn eine Konfiguration für den WebSocket-Kontext bereitgestellt wird.

Komprimierung kann durch Festlegen ConfigureWebSocketOptions auf null, wodurch die Sicherheitsanfälligkeit der App zum Angriff reduziert wird, dies kann jedoch zu einer verringerten Leistung führen:

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

Konfigurieren Sie einen strikteren frame-ancestors CSP mit einem Wert von 'none' (einfache Anführungszeichen erforderlich), was die WebSocket-Komprimierung zulässt, aber verhindert, dass Browser die App in eine der <iframe>folgenden Werte einbetten:

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

Weitere Informationen finden Sie in den folgenden Ressourcen:

Behandeln von Tastaturkompositionsereignissen in Blazor

Die neue KeyboardEventArgs.IsComposing-Eigenschaft gibt an, ob das Tastaturereignis Teil einer Kompositionssitzung ist. Das Nachverfolgen des Kompositionszustands von Tastaturereignissen ist entscheidend für die Behandlung internationaler Zeicheneingabemethoden.

Parameter OverscanCount zu QuickGrid hinzugefügt

Die QuickGrid-Komponente macht jetzt eine OverscanCount-Eigenschaft verfügbar, die angibt, wie viele zusätzliche Zeilen vor und nach dem sichtbaren Bereich gerendert werden, wenn die Virtualisierung aktiviert ist.

Der Standard OverscanCount ist „3“. Das folgende Beispiel erhöht OverscanCount um 4:

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

InputNumber-Komponente unterstützt das Attribut type="range"

Die Komponente InputNumber<TValue> unterstützt jetzt das Attribut type="range", mit dem eine Bereichseingabe erstellt wird, die Modellbindung und Formularprüfung unterstützt und in der Regel als Schieberegler oder Wählscheibe anstelle eines Textfelds dargestellt wird:

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

SignalR

In diesem Abschnitt werden neue Features für SignalR beschrieben.

Polymorphe Typunterstützung in SignalR-Hubs

Hubmethoden können nun eine Basisklasse anstelle der abgeleiteten Klasse akzeptieren, um polymorphe Szenarien zu ermöglichen. Der Basistyp muss annotiert werden, um Polymorphismus zu ermöglichen.

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

Verbesserte Aktivitäten für SignalR

SignalR verfügt jetzt über eine ActivitySource mit dem Namen Microsoft.AspNetCore.SignalR.Server, die Ereignisse für Hubmethodenaufrufe ausgibt:

  • Jede Methode ist eine eigene Aktivität, sodass alles, was während des Hubmethodenaufrufs eine Aktivität ausgibt, unter der Hubmethodenaktivität liegt.
  • Hubmethodenaktivitäten verfügen nicht über ein übergeordnetes Element. Dies bedeutet, dass sie nicht unter der langlebigen SignalR-Verbindung gebündelt werden.

Das folgende Beispiel verwendet das .NET AspireDashboard und die OpenTelemetry-Pakete:

<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" />

Fügen Sie der Datei Program.cs den folgenden Startcode hinzu:

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

Im Folgenden finden Sie eine Beispielausgabe vom Aspire-Dashboard:

Aktivitätsliste für SignalR Hub Methodenaufrufe

SignalR unterstützt das Kürzen und natives AOT

Als Fortsetzung der nativen AOT-Journey, die in .NET 8 begann, verfügen wir nun über Unterstützung zum Kürzen und für die native AOT-Kompilierung (AOT = Ahead-of-Time) für SignalR-Client- und Server-Szenarien. Sie können jetzt die Leistungsvorteile der Verwendung von nativem AOT in Anwendungen nutzen, die SignalR für die Echtzeit-Webkommunikation verwenden.

Erste Schritte

Installieren Sie das neueste .NET 9 SDK.

Erstellen Sie eine Lösung aus der webapiaot-Vorlage in einer Befehlszeile mit dem folgenden Befehl:

dotnet new webapiaot -o SignalRChatAOTExample

Ersetzen Sie den Inhalt der Program.cs-Datei durch den folgenden SignalR-Code:

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

Das obige Beispiel erzeugt eine native Windows-Ausführungsdatei von 10 MB und eine Linux-Ausführungsdatei von 10,9 MB.

Begrenzungen

  • Derzeit wird nur das JSON-Protokoll unterstützt:
    • Wie im obigen Code dargestellt, müssen Apps, die JSON-Serialisierung und natives AOT verwenden, den System.Text.Json-Quellengenerator verwenden.
    • Dies folgt dem gleichen Ansatz wie minimale APIs.
  • Auf dem SignalR-Server werden Hub-Methodenparameter vom Typ IAsyncEnumerable<T> und ChannelReader<T>, wobei T ein ValueType (struct) ist, nicht unterstützt. Die Verwendung dieser Typen führt zu einer Laufzeitausnahme beim Start in der Entwicklung und in der veröffentlichten App. Weitere Informationen finden Sie unter SignalR: Verwenden von IAsyncEnumerable<T> und ChannelReader<T> mit ValueTypes in nativen AOT (dotnet/aspnetcore #56179).
  • Stark typisierte Hubs werden vom nativen AOT (PublishAot) nicht unterstützt. Die Verwendung von stark typisierten Hubs mit nativem AOT führt zu Warnungen während der Erstellung und Veröffentlichung sowie zu einer Laufzeitausnahme. Die Verwendung stark typierter Hubs mit Kürzen (PublishedTrimmed) wird unterstützt.
  • Nur Task, Task<T>, ValueTask oder ValueTask<T> werden für asynchrone Rückgabetypen unterstützt.

Minimale APIs

In diesem Abschnitt werden neue Features für minimale APIs beschrieben.

InternalServerError und InternalServerError<TValue> wurden zu TypedResults hinzugefügt.

Die Klasse TypedResults ist ein hilfreiches Mittel für die Rückgabe von stark typisierten HTTP-Statuscode-basierten Antworten aus einer minimalen API. TypedResults enthält jetzt Factorymethoden und Typen für die Rückgabe von „500 Interner Serverfehler“-Antworten von Endpunkten. Hier ist ein Beispiel, das eine 500-Antwort zurückgibt:

var app = WebApplication.Create();

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

app.Run();

Rufen Sie ProducesProblem und ProducesValidationProblem auf Routengruppen auf.

Die Erweiterungsmethoden ProducesProblem und ProducesValidationProblem verfügen nun über eine aktualisierte Version, die ihre Verwendung in Routengruppen unterstützt. Diese Methoden zeigen an, dass alle Endpunkte in einer Routengruppe als Antwort ProblemDetails oder ValidationProblemDetails für die Zwecke von OpenAPI-Metadaten zurückgegeben werden können.

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

Die Ergebnistypen Problem und ValidationProblem unterstützen die Konstruktion mit IEnumerable<KeyValuePair<string, object?>>-Werten

Vor .NET 9 erforderten die Ergebnistypen Problem und ValidationProblem in minimalen APIs, dass die Eigenschaften errors und extensions mit einer Implementierung von IDictionary<string, object?> initialisiert werden. In diesem Release unterstützen diese Konstruktions-APIs Überladungen, die IEnumerable<KeyValuePair<string, object?>> verbrauchen.

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

Vielen Dank an GitHub-Benutzer joegoldman2 für diesen Beitrag!

OpenAPI

In diesem Abschnitt werden neue Funktionen für OpenAPI beschrieben.

Integrierte Unterstützung für die OpenAPI-Dokumentgenerierung

Die OpenAPI-Spezifikation ist ein Standard zur Beschreibung von HTTP-APIs. Mit dem Standard können Entwickler die Form der APIs definieren, die an Clientgeneratoren, Servergeneratoren, Testtools, Dokumentationen und vieles mehr angeschlossen werden können. In .NET 9 bietet ASP.NET Core integrierte Unterstützung für das Generieren von OpenAPI-Dokumenten, die controllerbasierte oder minimale APIs über das Microsoft.AspNetCore.OpenApi-Paket darstellen.

Der folgende hervorgehobene Code ruft Folgendes auf:

  • AddOpenApi zum Registrieren der erforderlichen Abhängigkeiten im DI-Container der App.
  • MapOpenApi zum Registrieren der erforderlichen OpenAPI-Endpunkte in den Routen der App.
var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

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

app.Run();

Installieren Sie das Microsoft.AspNetCore.OpenApi-Paket im Projekt mithilfe des folgenden Befehls:

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

Führen Sie die App aus, und navigieren Sie zu openapi/v1.json, um das generierte OpenAPI-Dokument anzuzeigen:

OpenAPI-Dokument

OpenAPI-Dokumente können auch zur Buildzeit generiert werden, indem Sie das Microsoft.Extensions.ApiDescription.Server-Paket hinzufügen:

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

Um den Speicherort der ausgegebenen OpenAPI-Dokumente zu ändern, legen Sie den Zielpfad in der OpenApiDocumentsDirectory-Eigenschaft in der Projektdatei der App fest:

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

Führen Sie dotnet build aus und überprüfen Sie die generierte JSON-Datei im Projektverzeichnis.

OpenAPI-Dokumentgenerierung zur Erstellungszeit

In ASP.NET Core integrierte OpenAPI-Dokumentgenerierung bietet Unterstützung für verschiedene Anpassungen und Optionen. Es stellt Dokument-, Betriebs- und Schematransformatoren bereit und bietet die Möglichkeit, mehrere OpenAPI-Dokumente für dieselbe Anwendung zu verwalten.

Weitere Informationen zu ASP.NET den neuen OpenAPI-Dokumentfunktionen von Core finden Sie in den neuen Microsoft.AspNetCore.OpenApi-Dokumenten.

Microsoft.AspNetCore.OpenApi unterstützt Kürzung und natives AOT

Die neue integrierte OpenAPI-Unterstützung in ASP.NET Core unterstützt jetzt auch Kürzen und natives AOT.

Erste Schritte

Erstellen Sie ein neues ASP.NET Core-Web-API-Projekt (natives AOT).

dotnet new webapiaot

Fügen Sie das Microsoft.AspNetCore.OpenAPI-Paket hinzu.

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

Für diese Vorschau müssen Sie auch das neueste Microsoft.OpenAPI-Paket hinzufügen, um Kürzungswarnungen zu vermeiden.

dotnet add package Microsoft.OpenApi

Aktualisieren Sie Program.cs, um das Generieren von OpenAPI-Dokumenten zu aktivieren.

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

Veröffentlichen Sie die App.

dotnet publish

Die App veröffentlicht natives AOT ohne Warnungen.

Authentifizierung und Autorisierung

In diesem Abschnitt werden neue Features für Authentifizierung und Autorisierung beschrieben.

OpenIdConnectHandler fügt Unterstützung für Pushed Authorization Requests (PAR) hinzu.

Wir möchten Joe DeCock von Duende Software für das Hinzufügen von Pushed Authorization Requests (PAR) zu OpenIdConnectHandler von ASP.NET Core danken. Joe hat den Hintergrund und die Motivation für die Aktivierung von PAR in seinem API-Vorschlag wie folgt beschrieben:

Pushed Authorization Requests (PAR) ist ein relativ neuer OAuth-Standard, der die Sicherheit von OAuth- und OIDC-Flows verbessert, indem Autorisierungsparameter vom Front-Channel zum Back-Channel verschoben werden. Das heißt, die Verschiebung von Autorisierungsparametern von Umleitungs-URLs im Browser zu direkten Computer-zu-Computer-HTTP-Aufrufen im Backend.

Dadurch wird verhindert, dass Cyberkriminelle im Browser:

  • Autorisierungsparameter sehen, die personenbezogene Daten preisgeben könnten.
  • Die Parameter manipulieren. Beispielsweise könnte der Cyberangriff den Umfang des angeforderten Zugriffs ändern.

Durch die Übertragung der Autorisierungsparameter bleiben die Anforderungs-URLs außerdem kurz. Autorisierungsparameter können sehr lang werden, wenn komplexere OAuth- und OIDC-Funktionen wie Rich Authorization Requests verwendet werden. Lange URLs verursachen in vielen Browsern und Netzwerkinfrastrukturen Probleme.

Die Verwendung von PAR wird von der FAPI-Arbeitsgruppe innerhalb der OpenID Foundation gefördert. Das FAPI2.0-Sicherheitsprofil erfordert beispielsweise die Verwendung von PAR. Dieses Sicherheitsprofil wird von vielen Gruppen verwendet, die sich mit Open Banking (hauptsächlich in Europa), im Gesundheitswesen und in anderen Branchen mit hohen Sicherheitsanforderungen befassen.

PAR wird von einer Reihe von identity-Anbietern unterstützt, darunter:

Für .NET 9 haben wir uns entschieden, PAR standardmäßig zu aktivieren, wenn das Ermittlungsdokument des Identitätsanbieters die Unterstützung von PAR ankündigt, da es Anbietern, die PAR unterstützen, eine verbesserte Sicherheit bieten sollte. Das Discovery-Dokument des identity-Anbieters befindet sich in der Regel unter .well-known/openid-configuration. Wenn dies zu Problemen führt, können Sie PAR über OpenIdConnectOptions.PushedAuthorizationBehavior wie folgt deaktivieren:

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

Um sicherzustellen, dass die Authentifizierung nur bei Verwendung von PAR erfolgreich ist, verwenden Sie stattdessen PushedAuthorizationBehavior.Require. Diese Änderung führt auch ein neues OnPushAuthorization-Ereignis in OpenIdConnectEvents ein, mit dem die Push-Autorisierungsanforderung angepasst oder manuell bearbeitet werden kann. Weitere Informationen finden Sie im API-Vorschlag.

Anpassung der OIDC- und OAuth-Parameter

Die OAuth- und OIDC-Authentifizierungs-Handler verfügen jetzt über eine AdditionalAuthorizationParameters Option, mit der Sie die Parameter der Autorisierungsnachricht, die normalerweise Teil des Redirect-Query-Strings sind, einfacher anpassen können. In .NET 8 und früher erfordert dies einen benutzerdefinierten OnRedirectToIdentityProvider Callback oder eine überschriebene BuildChallengeUrl Methode in einem benutzerdefinierten Handler. Hier ist ein Beispiel für .NET 8 Code:

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

Das vorangegangene Beispiel kann nun zu folgendem Code vereinfacht werden:

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

Konfigurieren Sie die erweiterten Authentifizierungs-Flags von HTTP.sys

Sie können nun die HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING und HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL HTTP.sys-Flags konfigurieren, indem Sie die neuen EnableKerberosCredentialCaching und CaptureCredentials Eigenschaften der HTTP.sys AuthenticationManager verwenden, um die Handhabung der Windows-Authentifizierung zu optimieren. Zum Beispiel:

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

Verschiedenes

In den folgenden Abschnitten werden verschiedene neue Features beschrieben.

Neue HybridCache-Bibliothek

Wichtig

HybridCacheist derzeit noch in der Vorschau, wird aber vollständig nach .NET 9.0 in einer zukünftigen Nebenversion von .NET-Erweiterungen veröffentlicht.

Die HybridCache-API überbrückt einige Lücken in den IDistributedCache- und IMemoryCache-APIs. Außerdem werden neue Funktionen hinzugefügt, z. B.:

  • "Stampede"-Schutz, um parallele Abrufe derselben Arbeit zu verhindern.
  • Konfigurierbare Serialisierung.

HybridCache ist als Drop-In-Ersatz für vorhandene IDistributedCache- und IMemoryCache-Nutzung konzipiert und bietet eine einfache API zum Hinzufügen von neuem Zwischenspeicherungscode. Es bietet eine einheitliche API für die In-Process- und Out-of-Process-Zwischenspeicherung.

Um zu sehen, wie die HybridCache-API vereinfacht wird, vergleichen Sie sie mit Code, der IDistributedCache verwendet. Hier sehen Sie ein Beispiel dafür, wie die Verwendung von IDistributedCache aussieht:

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)
    { /* ... */ }
}

Das ist viel Arbeit, um jedes Mal richtig zu liegen, einschließlich Dinge wie Serialisierung. Und im Szenario „Cachefehler“ könnten Sie mit mehreren gleichzeitigen Threads enden, die alle einen Cachefehler erhalten, alle zugrunde liegenden Daten abrufen, serialisieren und alle diese Daten an den Cache senden.

Um diesen Code mit HybridCache zu vereinfachen und zu verbessern, müssen wir zuerst die neue Bibliothek Microsoft.Extensions.Caching.Hybrid hinzufügen:

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

Registrieren Sie den HybridCache-Dienst, wie Sie eine IDistributedCache-Implementierung registrieren würden:

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

Jetzt können die meisten Zwischenspeicherungsbedenken auf HybridCache entladen werden:

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

Wir stellen eine konkrete Implementierung der abstrakten Klasse HybridCache über Abhängigkeitsinjektion bereit, aber es ist beabsichtigt, dass Entwickler benutzerdefinierte Implementierungen der API bereitstellen können. Die HybridCache-Implementierung befasst sich mit allem, was mit dem Caching zusammenhängt, einschließlich der Handhabung gleichzeitiger Operationen. Das cancel-Token steht hier für die kombinierte Annullierung aller gleichzeitigen Anrufer –- nicht nur für die Annullierung des Anrufers, den wir sehen können (d. h. token).

Szenarien mit hohem Durchsatz können mithilfe des TState-Musters weiter optimiert werden, um den Aufwand von erfassten Variablen und Rückrufen pro Instanz zu vermeiden:

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 verwendet ggf. die konfigurierte IDistributedCache-Implementierung für sekundäre Out-of-Process-Speicherung, z. B. mithilfe von Redis. Aber auch ohne IDistributedCache bietet der HybridCache-Dienst weiterhin In-Process-Cache- und Stampede-Schutz.

Ein Hinweis zur Wiederverwendung von Objekten

Im typischen vorhandenen Code, der IDistributedCache verwendet, führt jeder Abruf eines Objekts aus dem Cache zur Deserialisierung. Dieses Verhalten bedeutet, dass jeder gleichzeitige Aufrufer eine separate Instanz des Objekts erhält, die nicht mit anderen Instanzen interagieren kann. Das Ergebnis ist Threadsicherheit, da es kein Risiko für gleichzeitige Änderungen an derselben Objektinstanz gibt.

Da viele HybridCache-Verwendungen von vorhandenem IDistributedCache-Code angepasst werden, behält HybridCache dieses Verhalten standardmäßig bei, um die Einführung von Parallelitätsfehlern zu vermeiden. Ein bestimmter Anwendungsfall ist jedoch inhärent threadsicher:

  • Wenn die zwischengespeicherten Typen unveränderlich sind.
  • Wenn der Code sie nicht ändert.

Informieren Sie in solchen Fällen HybridCache, dass es sicher ist, Instanzen wiederzuverwenden, indem Sie:

  • Den Typ als sealed markieren. Das Schlüsselwort sealed in C# bedeutet, dass die Klasse nicht geerbt werden kann.
  • Anwenden des [ImmutableObject(true)]-Attributs. Das Attribut [ImmutableObject(true)] gibt an, dass der Status des Objekts nach der Erstellung nicht mehr geändert werden kann.

Durch die erneute Verwendung von Instanzen kann HybridCache den Mehraufwand der CPU- und Objektzuordnungen verringern, die mit der Deserialisierung pro Aufruf verbunden sind. Dies kann zu Leistungsverbesserungen in Szenarien führen, in denen die zwischengespeicherten Objekte groß sind oder häufig aufgerufen werden.

Weitere HybridCache-Funktionen

Wie IDistributedCache unterstützt HybridCache das Entfernen von Schlüsseln mit einer RemoveKeyAsync-Methode.

HybridCache stellt auch optionale APIs für IDistributedCache-Implementierungen bereit, um byte[]-Zuordnungen zu vermeiden. Dieses Feature wird von den Vorschauversionen der Microsoft.Extensions.Caching.StackExchangeRedis- und Microsoft.Extensions.Caching.SqlServer-Pakete implementiert.

Die Serialisierung wird als Teil der Registrierung des Diensts konfiguriert, wobei typspezifische und generalisierte Serialisierer über die WithSerializer- und .WithSerializerFactory-Methoden, verkettet vom AddHybridCache-Aufruf, unterstützt werden. Standardmäßig behandelt die Bibliothek string und byte[] intern und verwendet System.Text.Json für alles weitere, aber Sie können protobuf, xml oder etwas anderes verwenden.

HybridCache unterstützt ältere .NET-Runtimes bis zu .NET Framework 4.7.2 und .NET Standard 2.0.

Weitere Informationen zu HybridCache finden Sie unter HybridCache-Bibliothek in ASP.NET Core

Verbesserungen der Seite mit Ausnahmen für Entwickler

Die ASP.NET Core-Seite mit Ausnahmen für Entwickler wird angezeigt, wenn eine App während der Entwicklung eine unbehandelte Ausnahme auslöst. Die Seite mit Ausnahmen für Entwickler bietet ausführliche Informationen zur Ausnahme und zur Anforderung.

In Preview 3 wurden Endpunktmetadaten auf der Entwicklerausnahmeseite hinzugefügt. ASP.NET Core verwendet Endpunkt-Metadaten, um das Verhalten von Endpunkten zu steuern, z. B. Routing, Antwort-Caching, Ratenbegrenzung, OpenAPI-Generierung und mehr. Die folgende Abbildung zeigt die neuen Metadaten-Informationen im Abschnitt Routing auf der Entwicklerausnahmeseite:

Die neuen Metadaten-Informationen auf der Entwicklerausnahmeseite.

Beim Testen der Ausnahmeseite für Entwickler wurden kleine Verbesserungen der Lebensqualität identifiziert. Sie wurden in Preview 4 ausgeliefert:

  • Besserer Textumbruch. Bei langen Cookies, Abfragezeichenfolgenwerten und Methodennamen werden keine horizontalen Browser-Bildlaufleisten mehr hinzugefügt.
  • Größerer Text, der in modernen Designs zu finden ist.
  • Konsistentere Tabellengrößen.

Die folgende animierte Abbildung zeigt die neue Seite mit Ausnahmen für Entwickler:

Die neue Seite mit Ausnahmen für Entwickler

Verbesserungen beim Debuggen von Wörterbuchen

Die Debuganzeige von Wörterbüchern und anderen Schlüsselwertsammlungen verfügt über ein verbessertes Layout. Der Schlüssel wird in der Schlüsselspalte des Debuggers angezeigt, anstatt mit dem Wert verkettet zu werden. Die folgenden Bilder stellen die alte und die neue Anzeige eines Wörterbuchs im Debugger dar.

Vorher:

Die bisherige Debugger-Oberfläche

Nachher:

Die neue Debugger-Oberfläche

ASP.NET Core verfügt über viele Schlüsselwertauflistungen. Diese verbesserte Debugerfahrung gilt für:

  • HTTP-Kopfzeilen
  • Abfragezeichenfolgen
  • Formulare
  • Cookies
  • Anzeigen von Daten
  • Routendaten
  • Features

Fix für 503-Apps während der Wiederverwendung von Apps in IIS

Standardmäßig gibt es jetzt eine Verzögerung von einer Sekunde, wenn IIS über eine Wiederverwendung oder ein Herunterfahren benachrichtigt wird und wenn ANCM den verwalteten Server anweist, das Herunterfahren zu initiieren. Die Verzögerung kann über die Umgebungsvariable ANCM_shutdownDelay oder durch Festlegen der Handlereinstellung shutdownDelay konfiguriert werden. Beide Werte sind in Millisekunden. Die Verzögerung soll in erster Linie die Wahrscheinlichkeit eines Wettrennens verringern, bei dem:

  • IIS keine Warteschlangenanforderungen gestartet hat, um zur neuen App zu wechseln.
  • ANCM mit der Ablehnung neuer Anforderungen beginnt, die in die alte App gelangen.

Für langsamere Computer oder Computer mit höherer CPU-Auslastung muss dieser Wert möglicherweise angepasst werden, um die Wahrscheinlichkeit 503 zu verringern.

Beispiel der Einstellung 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>

Der Fix befindet sich im global installierten ANCM-Modul, das aus dem Hostingbundle stammt.

ASP0026: Analysetool, das warnt, wenn [Authorize] von [AllowAnonymous] von „weiter entfernt“ außer Kraft gesetzt wird

Es erscheint intuitiv, dass ein [Authorize]-Attribut, das "näher" an einer MVC-Aktion platziert ist als ein [AllowAnonymous]-Attribut, das [AllowAnonymous]-Attribut außer Kraft setzt und eine Autorisierung erzwingt. Dies ist jedoch nicht unbedingt der Fall. Was zählt, ist die relative Reihenfolge der Attribute.

Der folgende Code zeigt Beispiele, in denen ein näher gelegenes [Authorize]-Attribut durch ein weiter entferntes [AllowAnonymous]-Attribut außer Kraft gesetzt wird.

[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
{
}

In .NET 9 Preview 6 haben wir ein Analysetool eingeführt, das solche Fälle hervorhebt, in denen ein näheres [Authorize]-Attribut durch ein [AllowAnonymous]-Attribut überschrieben wird, das weiter von einer MVC-Aktion entfernt ist. Die Warnung verweist auf das überschriebene [Authorize]-Attribut mit der folgenden Meldung:

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

Was Sie tun müssen, wenn Sie diese Warnung sehen, hängt von der Absicht ab, die hinter den Attributen steht. Das weiter entfernte [AllowAnonymous]-Attribut sollte entfernt werden, wenn es den Endpunkt ungewollt anonymen Benutzerinnen und Benutzern zugänglich macht. Wenn das [AllowAnonymous]-Attribut ein näheres [Authorize]-Attribut außer Kraft setzen soll, können Sie das [AllowAnonymous]-Attribut nach dem [Authorize]-Attribut wiederholen, um die Absicht zu verdeutlichen.

[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;
}

Verbesserte Kestrel-Verbindungsmetriken

Wir haben die Verbindungsmetriken von Kestrel erheblich verbessert, indem wir Metadaten dazu einschließen, warum eine Verbindung fehlgeschlagen ist. Die Metrik kestrel.connection.duration enthält nun den Grund für das Schließen einer Verbindung im Attribut error.type.

Hier ist ein kleines Beispiel für die error.type-Werte:

  • tls_handshake_failed – Für die Verbindung ist TLS erforderlich, und der TLS-Handshake ist fehlgeschlagen.
  • connection_reset – Die Verbindung wurde unerwartet vom Client geschlossen, während Anforderungen ausgeführt wurden.
  • request_headers_timeout – Kestrel hat die Verbindung geschlossen, da sie keine Anforderungsheader rechtzeitig empfangen hat.
  • max_request_body_size_exceeded – Kestrel hat die Verbindung geschlossen, da hochgeladene Daten die maximale Größe überschritten haben.

Zuvor war die Diagnose von Kestrel-Verbindungsproblemen erforderlich, um eine detaillierte Protokollierung auf niedriger Ebene aufzuzeichnen. Allerdings kann es teuer sein, Protokolle zu generieren und zu speichern, und es kann schwierig sein, die richtigen Informationen Im Datenrauschen zu finden.

Metriken sind eine viel günstigere Alternative, die in einer Produktionsumgebung mit minimalen Auswirkungen aktiv bleiben können. Gesammelte Metriken können Dashboards und Warnungen steuern. Sobald ein Problem auf hoher Ebene mit Metriken identifiziert wurde, kann eine weitere Untersuchung mithilfe der Protokollierung und anderer Tools beginnen.

Wir gehen davon aus, dass verbesserte Verbindungsmetriken in vielen Szenarien nützlich sind:

  • Untersuchen von Leistungsproblemen, die durch kurze Verbindungslebensdauern verursacht werden.
  • Beobachten laufender externer Angriffe auf Kestrel, die sich auf Leistung und Stabilität auswirken.
  • Aufzeichnen von versuchten externen Angriffen auf Kestrel, die durch die integrierte Sicherheitshärtung von Kestrel verhindert wurden.

Weitere Informationen finden Sie unter ASP.NET Core-Metriken.

Anpassen von Kestrel-Named-Pipe-Endpunkten

Die Named Pipe-Unterstützung von Kestrel verfügt über verbesserte, erweiterte Anpassungsoptionen. Mit der neuen CreateNamedPipeServerStream-Methode für die Named Pipe-Optionen können Pipes pro Endpunkt angepasst werden.

Ein Beispiel für eine nützliche Anwendung ist eine Kestrel-App, die zwei Pipe-Endpunkte mit unterschiedlicher Zugriffssicherheit erfordert. Die CreateNamedPipeServerStream Option kann verwendet werden, um Pipes mit benutzerdefinierten Sicherheitseinstellungen zu erstellen, je nach Pipename.

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-Option zur Auswahl des Statuscodes basierend auf dem Ausnahmetyp

Eine neue Option bei der Konfiguration des ExceptionHandlerMiddleware ermöglicht es App-Entwickelnden, den Statuscode auszuwählen, der zurückgegeben werden soll, wenn während der Anfragebearbeitung eine Ausnahme auftritt. Die neue Option ändert den Statuscode, der in der Antwort ProblemDetails von ExceptionHandlerMiddleware festgelegt wird.

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

Abmelden von HTTP-Metriken für bestimmte Endpunkte und Anforderungen

.NET 9 bietet die Möglichkeit, HTTP-Metriken für bestimmte Endpunkte und Anforderungen abzumelden. Das Abmelden von Aufzeichnungsmetriken ist von Vorteil für Endpunkte, die häufig von automatisierten Systemen aufgerufen werden, z. B. Integritätsprüfungen. Aufzeichnungsmetriken für diese Anforderungen sind im Allgemeinen nicht erforderlich.

HTTP-Anforderungen an einen Endpunkt können durch Hinzufügen von Metadaten von Metriken ausgeschlossen werden. Entweder:

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

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

Die Eigenschaft MetricsDisabled wurde zu IHttpMetricsTagsFeature hinzugefügt für:

  • Erweiterte Szenarien, in denen eine Anforderung keinem Endpunkt zugeordnet ist.
  • Dynamisches Deaktivieren der Metriksammlung für bestimmte HTTP-Anforderungen.
// 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);
});

Datenschutzunterstützung für das Löschen von Schlüsseln.

Vor .NET 9 waren Datenschutzschlüssel nicht löschbar, um Datenverlust zu verhindern. Wenn Sie einen Schlüssel löschen, sind die geschützten Daten unwiederbringlich verloren. Angesichts ihrer geringen Größe hat die Anhäufung dieser Schlüssel im Allgemeinen minimale Auswirkungen. Um jedoch extrem lang laufende Dienste zu berücksichtigen, verfügen wir über die Option, Schlüssel zu löschen. Im Allgemeinen sollten nur alte Schlüssel gelöscht werden. Löschen Sie Schlüssel nur, wenn Sie das Risiko eines Datenverlusts im Austausch für Speicherplatzersparnisse in Kauf nehmen können. Wir empfehlen, Datenschutzschlüssel nicht zu löschen.

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.");
}

Middleware unterstützt Keyed DI

Middleware unterstützt jetzt Keyed DI sowohl im Konstruktor als auch in der Invoke/InvokeAsync Methode:

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

Das ASP.NET Core HTTPS-Entwicklungszertifikat als vertrauenswürdig einstufen

Auf Ubuntu- und Fedora-basierten Linux-Distributionen konfiguriert dotnet dev-certs https --trust nun das ASP.NET Core HTTPS-Entwicklungszertifikat als vertrauenswürdiges Zertifikat für:

  • Chromium-Browser, z. B. Google Chrome, Microsoft Edge und Chromium.
  • Mozilla Firefox und Mozilla abgeleitete Browser.
  • .NET APIs, zum Beispiel, HttpClient

Zuvor funktionierte --trust nur unter Windows und macOS. Eine Zertifikats-Vertrauensstellung wird pro Benutzender angewendet.

Um eine Vertrauenstellung in OpenSSL herzustellen, wird das dev-certs-Tool verwendet:

  • Platziert das Zertifikat in ~/.aspnet/dev-certs/trust
  • Führt eine vereinfachte Version von OpenSSL's c_rehash tool auf dem Verzeichnis aus.
  • Fordert den Benutzenden auf, die Umgebungsvariable SSL_CERT_DIR zu aktualisieren.

Um eine Vertrauensstellung in dotnet aufzubauen, legt das Tool das Zertifikat im My/Root-Zertifikatspeicher ab.

Um die Vertrauensstellung in NSS-Datenbanken herzustellen, falls vorhanden, durchsucht das Tool das home-Verzeichnis nach Firefox-Profilen, ~/.pki/nssdb und ~/snap/chromium/current/.pki/nssdb. Für jedes gefundene Verzeichnis fügt das Tool einen Eintrag in das Verzeichnis nssdb ein.

Vorlagen wurden auf die neuesten Versionen bootstrap, jQuery und jQuery Validation aktualisiert.

Die ASP.NET Core-Projektvorlagen und -bibliotheken wurden aktualisiert, um die neuesten Versionen von Bootstrap, jQuery und jQuery Validation zu verwenden, insbesondere:

  • Bootstrap 5.3.3
  • jQuery 3.7.1
  • jQuery Validation 1.21.0