Best Practices für ASP.NET Core

Von Mike Rousos

Dieser Artikel bietet Informationen zur Maximierung der Leistung und Zuverlässigkeit von ASP.NET Core-Apps.

Aggressives Zwischenspeichern

Die Zwischenspeicherung wird in mehreren Teilen dieses Artikels besprochen. Weitere Informationen finden Sie unter Übersicht über das Zwischenspeichern in ASP.NET Core.

Grundlegendes zum langsamsten Codepfad

In diesem Artikel ist der langsamste Codepfad als ein Codepfad definiert, der sehr häufig aufgerufen wird und in dem der Großteil der Ausführung erfolgt. Der langsamste Codepfad begrenzt üblicherweise die horizontale Skalierung und die Leistung einer App und wird in verschiedenen Teilen dieses Artikels besprochen.

Vermeiden von blockierenden Aufrufen

ASP.NET Core-Apps sollten so konzipiert werden, dass viele Anforderungen gleichzeitig verarbeitet werden können. Asynchrone APIs ermöglichen die parallele Verarbeitung Tausender Anforderungen in einem kleinen Pool von Threads, indem nicht auf blockierende Aufrufe gewartet wird. Anstatt darauf zu warten, dass eine synchrone Aufgabe mit langer Laufzeit abgeschlossen wird, kann der Thread eine andere Anforderung bearbeiten.

Ein häufiges Leistungsproblem in ASP.NET Core-Apps sind blockierende Aufrufe, die asynchron sein könnten. Viele synchrone blockierende Aufrufe führen zu einem Threadmangel im Pool und zu längeren Antwortzeiten.

Blockieren Sie die asynchrone Ausführung nicht durch Aufrufen von Task.Wait oder Task<TResult>.Result. Rufen Sie keine Sperren in häufig verwendeten Codepfaden ab. ASP.NET Core-Apps weisen die beste Leistung auf, wenn sie für die parallele Ausführung von Code konzipiert sind. Rufen Sie nichtTask.Run auf und warten unmittelbar auf seinen Abschluss. ASP.NET Core führt App-Code bereits in normalen Threads im Threadpool aus, daher führt ein Aufruf von Task.Run nur zu einer unnötigen zusätzlichen Reservierung des Threadpools. Auch wenn der geplante Code einen Thread blockieren würde, wird dies durch Task.Run nicht verhindert.

  • Legen Sie den langsamsten Codepfad als asynchron fest.
  • Rufen Sie APIs für Datenzugriff, E/A und Vorgänge mit langer Laufzeit asynchron auf, wenn eine asynchrone API verfügbar ist.
  • Verwenden Sie nichtTask.Run, um eine synchrone API als asynchron festzulegen.
  • Legen Sie Controller-/Razor Page-Aktionen als asynchron fest. Die gesamte Aufrufliste ist asynchron, um von async/await-Mustern zu profitieren.

Über einen Profiler wie z. B. PerfView können Threads ermittelt werden, die dem Threadpool häufig hinzugefügt werden. Das Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start-Ereignis gibt einen Thread an, der dem Threadpool hinzugefügt wurde.

Zurückgeben großer Auflistungen über mehrere kleinere Seiten hinweg

Eine Webseite sollte große Datenmengen nicht gleichzeitig laden. Berücksichtigen Sie beim Zurückgeben einer Auflistung von Objekten, ob dies zu Leistungsproblemen führen könnte. Ermitteln Sie, ob der Entwurf die folgenden unerwünschten Ergebnisse erzielen könnte:

Fügen Sie eine Paginierung hinzu, um die vorherigen Szenarien abzumildern. Bei der Verwendung von Seitenformat- und Seitenindexparametern sollten Entwickler das Design bevorzugen, bei dem Teilergebnisse zurückgegeben werden. Wenn ein vollständiges Ergebnis erforderlich ist, sollte die Paginierung verwendet werden, um Ergebnisbatches asynchron aufzufüllen und so das Sperren von Serverressourcen zu vermeiden.

Weitere Informationen zur Paginierung und zum Beschränken der Anzahl der zurückgegebenen Datensätze finden Sie hier:

Zurückgeben von IEnumerable<T> oder IAsyncEnumerable<T>

Das Zurückgeben von IEnumerable<T> aus einer Aktion führt zu einer synchronen Sammlungsiteration durch das Serialisierungsmodul. Das Ergebnis sind die Blockierung von Aufrufen und die potenzielle Außerkraftsetzung des Threadpools. Um eine synchrone Enumeration zu vermeiden, verwenden Sie ToListAsync vor dem Zurückgeben der Enumeration.

Ab ASP.NET Core 3.0 kann IAsyncEnumerable<T> als Alternative zum IEnumerable<T>-Element verwendet werden, das asynchron enumeriert. Weitere Informationen finden Sie unter Controller – Aktionsrückgabetypen.

Minimieren von Zuteilungen großer Objekte

Der .NET Core Garbage Collector verwaltet die Belegung und Freigabe von Arbeitsspeichers in ASP.NET Core-Apps automatisch. Eine automatische Garbage Collection bedeutet im Allgemeinen, dass Entwickler sich keine Gedanken darüber machen müssen, wie oder wann Arbeitsspeicher freigegeben wird. Das Bereinigen nicht referenzierter Objekte erfordert jedoch CPU-Zeit, sodass Entwickler die Zuteilung von Objekten im langsamsten Codepfad minimieren sollten. Die Garbage Collection ist bei großen Objekten (> = 85.000 KB) besonders ressourcenintensiv. Große Objekte werden im Heap für große Objekte gespeichert und erfordern zur Bereinigung eine vollständige Garbage Collection (Generation 2). Im Gegensatz zu den Garbage Collections der Generationen 0 und 1 erfordert eine Collection der Generation 2 die temporäre Aussetzung der App-Ausführung. Die häufige Zuteilung und Aufhebung der Zuteilung großer Objekte kann zu einer inkonsistenten Leistung führen.

Empfehlungen:

  • Erwägen Sie, große, häufig verwendete Objekte zwischenzuspeichern. Das Zwischenspeichern großer Objekte verhindert ressourcenintensive Zuteilungen.
  • Erstellen Sie Poolpuffer durch Verwenden von ArrayPool<T> zum Speichern großer Arrays.
  • Teilen Sie nicht viele kurzlebige große Objekte im langsamsten Codepfad zu.

Arbeitsspeicherprobleme wie die oben genannten können durch Überprüfen der Garbage Collection-Statistiken (GC) in PerfView diagnostiziert werden. Untersuchen Sie Folgendes:

  • Wartezeit für Garbage Collection
  • Prozentsatz der für die Garbage Collection aufgewendeten Prozessorzeit
  • Anzahl der Garbage Collection-Vorgänge in Generation 0, 1 und 2

Weitere Informationen finden Sie unter Garbage Collection und Leistung.

Optimieren von Datenzugriff und E/A

Interaktionen mit Datenspeicher- und anderen Remotediensten sind häufig die langsamsten Teile einer ASP.NET Core-App. Das effiziente Lesen und Schreiben von Daten ist für eine gute Leistung von entscheidender Bedeutung.

Empfehlungen:

  • Rufen Sie alle Datenzugriffs-APIs asynchron auf.
  • Rufen Sie nicht mehr Daten ab als notwendig. Schreiben Sie Abfragen, um nur die Daten zurückzugeben, die für die aktuelle HTTP-Anforderung erforderlich sind.
  • Erwägen Sie die Zwischenspeicherung von Daten, auf die häufig zugegriffen wird und die aus einer Datenbank oder einem Remotedienst abgerufen werden, wenn geringfügig veraltete Daten akzeptabel sind. Verwenden Sie je nach Szenario MemoryCache oder DistributedCache. Weitere Informationen finden Sie unter Zwischenspeichern von Antworten in ASP.NET Core.
  • Minimieren Sie Netzwerkroundtrips. Ziel ist es, die erforderlichen Daten in einem einzelnen Aufruf statt in mehreren Aufrufen abzurufen.
  • Verwenden Sie in Entity Framework Core Abfragen ohne Nachverfolgung, wenn nur zu Lesezwecken auf Daten zugegriffen wird. EF Core kann die Ergebnisse von Abfragen ohne Nachverfolgung effizienter zurückgeben.
  • Filtern und aggregieren Sie LINQ-Abfragen (z. B. mit .Where-, .Select- oder .Sum-Anweisungen), sodass die Filterung von der Datenbank durchgeführt wird.
  • Beachten Sie, dass EF Core einige Abfrageoperatoren auf dem Client auflöst, was in einer ineffizienten Abfrageausführung resultieren kann. Weitere Informationen finden Sie unter Leistungsprobleme bei der Clientauswertung.
  • Verwenden Sie keine Projektionsabfragen in Auflistungen – dies kann in der Ausführung von „n + 1“ SQL-Abfragen resultieren. Weitere Informationen finden Sie unter Optimierung korrelierter Unterabfragen.

Die folgenden Ansätze können die Leistung in Apps mit einem hohen Maß an Skalierung verbessern:

Es wird empfohlen, die Auswirkungen der oben beschriebenen Ansätze für hohe Leistung vor dem Committen der Codebasis zu messen. Die zusätzliche Komplexität kompilierter Abfragen wiegt die Leistungsverbesserung möglicherweise nicht auf.

Abfrageprobleme lassen sich ermitteln, indem überprüft wird, wie viel Zeit für den Zugriff auf Daten mit Application Insights oder mit Profilerstellungstools aufgewendet wird. Die meisten Datenbanken stellen auch Statistiken zu häufig ausgeführten Abfragen zur Verfügung.

HTTP-Poolverbindungen mit HttpClientFactory

Obwohl HttpClient die IDisposable-Schnittstelle implementiert, ist die Klasse für die Wiederverwendung konzipiert. Geschlossene HttpClient-Instanzen lassen Sockets für einen kurzen Zeitraum im Zustand TIME_WAIT offen. Wenn ein Codepfad, der HttpClient-Objekte erstellt und löscht, häufig verwendet wird, stehen möglicherweise nicht genügend Sockets für die App zur Verfügung. HttpClientFactory wurde in ASP.NET Core 2.1 als Lösung für dieses Problem eingeführt. Die Factory verarbeitet HTTP-Poolingverbindungen, um die Leistung und Zuverlässigkeit zu optimieren. Weitere Informationen finden Sie unter Verwenden von HttpClientFactory zum Implementieren resilienter HTTP-Anforderungen.

Empfehlungen:

Für Schnelligkeit häufiger Codepfade sorgen

Sie möchten, dass Ihr gesamter Code schnell verarbeitet wird. Häufig aufgerufene Codepfade sind diejenigen, deren Optimierung am wichtigsten ist. Dazu zählen unter anderem folgende Einstellungen:

  • Middlewarekomponenten in der Anforderungsverarbeitungspipeline der App, insbesondere Middleware, die zu einem frühen Zeitpunkt in der Pipeline ausgeführt wird. Diese Komponenten haben einen großen Einfluss auf die Leistung.
  • Code, der für jede Anforderung oder mehrmals pro Anforderung ausgeführt wird. Beispiele: benutzerdefinierte Protokollierung, Autorisierungshandler oder Initialisierung vorübergehender Dienste.

Empfehlungen:

Ausführen von Aufgaben mit langer Ausführungsdauer außerhalb von HTTP-Anforderungen

Die meisten Anforderungen an eine ASP.NET Core-App können von einem Controller oder Seitenmodell verarbeitet werden, der/das erforderliche Dienste aufruft und eine HTTP-Antwort zurückgibt. Bei einigen Anforderungen, die Aufgaben mit langer Ausführungsdauer umfassen, ist es besser, den gesamten Anforderungs-/Antwortprozess asynchron zu gestalten.

Empfehlungen:

  • Warten Sie nicht auf die Beendigung von Aufgaben mit langer Ausführungsdauer im Rahmen der normalen HTTP-Anforderungsverarbeitung.
  • Erwägen Sie, Anforderungen mit langer Ausführungsdauer mit Hintergrunddiensten oder prozessextern mit einer Azure-Funktion zu verarbeiten. Die externe Verarbeitung ist besonders bei CPU-intensiven Aufgaben von Vorteil.
  • Verwenden Sie Optionen für die Echtzeitkommunikation wie z. B. SignalR, um asynchron mit Clients zu kommunizieren.

Minimieren von Clientressourcen

ASP.NET Core-Apps mit komplexen Front-Ends verarbeiten häufig viele JavaScript-, CSS- oder Bilddateien. Die Leistung der anfänglichen Ladeanforderungen kann durch Folgendes verbessert werden:

  • Bündelung, bei der mehrere Dateien in eine kombiniert werden
  • Minimierung, bei der die Größe von Dateien durch Entfernen von Whitespaces und Kommentare reduziert wird

Empfehlungen:

  • Lesen Sie die Richtlinien zur Bündelung und Minimierung, die kompatible Tools auflisten und zeigen, wie das Tag environment von ASP.NET Core zum Verarbeiten von Development- und Production-Umgebungen verwendet wird.
  • Erwägen Sie andere Drittanbietertools wie z. B. Webpack zur Verwaltung komplexer Clientressourcen.

Komprimieren von Antworten

Eine Reduzierung der Antwortgröße steigert in der Regel die Reaktionsfähigkeit einer App, häufig sogar erheblich. Eine Möglichkeit zum Verringern der Nutzdatengrößen besteht darin, die Antworten einer App zu komprimieren. Weitere Informationen finden Sie unter Antwortkomprimierung.

Verwenden des neuesten ASP.NET Core-Release

Jedes neue Release von ASP.NET Core umfasst Leistungsverbesserungen. Optimierungen in .NET Core und ASP.NET Core bedeuten, dass neuere Versionen im Allgemeinen eine bessere Leistung bieten als ältere Versionen. Beispielsweise wurde in .NET Core 2.1 Unterstützung für kompilierte reguläre Ausdrücke hinzugefügt, und diese Version profitiert von Span<T>. In ASP.NET Core 2.2 wurde Unterstützung für HTTP/2 hinzugefügt. ASP.NET Core 3.0 enthält viele Verbesserungen, die die Arbeitsspeichernutzung reduzieren und den Durchsatz erhöhen. Wenn Leistung hohe Priorität hat, sollten Sie ein Upgrade auf die aktuelle Version von ASP.NET Core in Betracht ziehen.

Minimieren von Ausnahmen

Ausnahmen sollten nur selten vorkommen. Das Auslösen und Abfangen von Ausnahmen ist in Relation zu anderen Codeflowmustern langsam. Aus diesem Grund sollten Ausnahmen nicht verwendet werden, um den normalen Programmflow zu steuern.

Empfehlungen:

  • Verwenden Sie das Auslösen oder Abfangen von Ausnahmen nicht als Mittel im normalen Programmflow, insbesondere nicht im langsamsten Codepfad.
  • Verwenden Sie Logik in der App, um Bedingungen zu erkennen und zu behandeln, die eine Ausnahme verursachen würden.
  • Nutzen Sie das Auslösen oder Abfangen von Ausnahmen für ungewöhnliche oder unerwartete Bedingungen.

App-Diagnosetools wie Application Insights können dabei helfen, häufige Ausnahmen in einer App zu identifizieren, die sich auf die Leistung auswirken können.

Vermeiden synchroner Lese- oder Schreibvorgänge in HttpRequest/HttpResponse-Textkörpern

Alle E/A-Vorgänge in ASP.NET Core laufen asynchron ab. Server implementieren die Stream-Schnittstelle, die sowohl synchrone als auch asynchrone Überladungen bietet. Die asynchronen Überladungen sollten bevorzugt werden, um das Blockieren von Threadpoolthreads zu vermeiden. Das Blockieren von Threads kann zu einem Threadpoolmangel führen.

Gehen Sie nicht wie folgt vor: Das folgende Beispiel verwendet die Methode ReadToEnd. Diese blockiert den aktuellen Thread, um auf das Ergebnis zu warten. Dies ist ein Beispiel für das Prinzip synchron statt asynchron.

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

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

Im vorherigen Code liest Get den gesamten HTTP-Anforderungstext synchron in den Arbeitsspeicher. Wenn der Uploadvorgang auf dem Client nur langsam erfolgt, verfährt die App nach dem Prinzip „synchron statt asynchron“. Die App geht so vor, weil KestrelKEINE synchronen Lesevorgänge unterstützt.

Gehen Sie folgendermaßen vor: Das folgende Beispiel verwendet ReadToEndAsync und blockiert den Thread beim Lesen nicht.

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

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

}

Der oben stehende Code liest den gesamten HTTP-Anforderungstext asynchron in den Arbeitsspeicher.

Warnung

Bei umfangreichen Anforderungen kann das Lesen des gesamten HTTP-Anforderungstexts in den Arbeitsspeicher zu einer OOM-Bedingung (Out of Memory, nicht genügend Arbeitsspeicher) führen. OOM kann zu einer Dienstblockade (Denial Of Service) führen. Weitere Informationen finden Sie unter Vermeiden des Lesens umfangreicher Anforderungs- oder Antworttexte in den Arbeitsspeicher in diesem Artikel.

Gehen Sie folgendermaßen vor: Das folgende Beispiel ist vollständig asynchron und verwendet einen nicht gepufferten Anforderungstext:

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

Der obige Code deserialisiert den Anforderungstext asynchron in ein C#-Objekt.

Verwenden von ReadFormAsync anstatt Request.Form

Verwenden Sie HttpContext.Request.ReadFormAsync anstelle von HttpContext.Request.Form. HttpContext.Request.Form kann nur mit den folgenden Bedingungen sicher gelesen werden:

  • Das Formular wurde durch einen Aufruf von ReadFormAsync gelesen.
  • Der zwischengespeicherte Formularwert wird mithilfe von HttpContext.Request.Form gelesen.

Gehen Sie nicht wie folgt vor: Das folgende Beispiel verwendet HttpContext.Request.Form. HttpContext.Request.Form verfährt nach dem Prinzip synchron statt asynchron und kann zu einem Threadpoolmangel führen.

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

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

        return Accepted();
    }

Gehen Sie folgendermaßen vor: Das folgende Beispiel verwendet HttpContext.Request.ReadFormAsync, um den Formulartext asynchron zu lesen.

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

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

        return Accepted();
    }

Vermeiden des Lesens umfangreicher Anforderungs- oder Antworttexte in den Arbeitsspeicher

In .NET gelangt jede Objektzuteilung mit einer Größe von mindestens 85.000 Bytes in den Heap für große Objekte (Large Object Heap, LOH). Große Objekte sind aus zwei Gründen teuer:

  • Die Belegungskosten sind hoch, da der Arbeitsspeicher für ein neu zugeordnetes großes Objekt gelöscht werden muss. Die CLR garantiert, dass der Arbeitsspeicher für alle neu zugeordneten Objekte gelöscht wird.
  • Der LOH wird mit dem Rest des Heaps gesammelt. Der LOH erfordert eine vollständige Garbage Collection oder Garbage Collection der Generation 2.

Dieser Blogbeitrag bietet eine prägnante Beschreibung des Problems:

Wenn ein großes Objekt zugeordnet wird, wird es als Gen 2-Objekt markiert, nicht als Gen 0, wie bei kleinen Objekten. Daraus folgt: Wenn im LOH nicht mehr genügend Arbeitsspeicher vorhanden ist, bereinigt die Garbage Collection den gesamten verwalteten Heap, nicht nur den LOH. Es werden also alle Gen 0-, Gen 1- und Gen 2-Objekte bereinigt, einschließlich des LOH. Dies wird als vollständige Garbage Collection bezeichnet und ist die zeitaufwendigste Garbage Collection. Für viele Anwendungen kann dies akzeptabel sein. Keinesfalls akzeptabel ist es allerdings für Hochleistungswebserver, bei denen nur wenige große Arbeitsspeicherpuffer benötigt werden, um eine durchschnittliche Webanforderung zu verarbeiten (Lesen aus einem Socket, Dekomprimieren, Decodieren von JSON und weitere Vorgänge).

Speichern eines großen Anforderungs- oder Antworttexts in einem einzigen byte[]- oder string-Wert:

  • Dies kann dazu führen, dass schnell nicht mehr genügend Platz im LOH vorhanden ist.
  • Dies kann zu Leistungsproblemen bei der App führen, weil vollständige Garbage Collections ausgeführt werden.

Arbeiten mit einer API zur synchronen Datenverarbeitung

Gehen Sie bei Verwendung eines Serialisierungs-/Deserialisierungsmoduls, das nur synchrone Lese- und Schreibvorgänge unterstützt (z. B. Json.NET) folgendermaßen vor:

  • Puffern Sie die Daten asynchron in den Arbeitsspeicher, bevor Sie sie an das Serialisierungs-/Deserialisierungsmodul übergeben.

Warnung

Eine umfangreiche Anforderung kann zu einer OOM-Bedingung (Out of Memory, nicht genügend Arbeitsspeicher) führen. OOM kann zu einer Dienstblockade (Denial Of Service) führen. Weitere Informationen finden Sie unter Vermeiden des Lesens umfangreicher Anforderungs- oder Antworttexte in den Arbeitsspeicher in diesem Artikel.

ASP.NET Core 3.0 verwendet standardmäßig System.Text.Json zur JSON-Serialisierung. System.Text.Json:

  • Lese- und Schreibvorgänge in JSON erfolgen asynchron.
  • Der Code ist für UTF-8-Text optimiert.
  • In der Regel lässt sich eine höhere Leistung als mit Newtonsoft.Json erzielen.

IHttpContextAccessor.HttpContext nicht in einem Feld speichern

Der IHttpContextAccessor.HttpContext gibt den HttpContext der aktiven Anforderung zurück, wenn der Zugriff aus dem Anforderungsthread erfolgt. Der IHttpContextAccessor.HttpContext sollte nicht in einem Feld oder einer Variable gespeichert werden.

Gehen Sie nicht wie folgt vor: Das folgende Beispiel speichert den HttpContext in einem Feld und versucht ihn dann später zu verwenden.

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

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

Der vorangehende Code erfasst häufig einen NULL- oder falschen HttpContext im Konstruktor.

Gehen Sie folgendermaßen vor: Das folgende Beispiel führt Folgendes aus:

  • Der IHttpContextAccessor wird in einem Feld gespeichert.
  • Das Beispiel verwendet das Feld HttpContext zur richtigen Zeit und prüft auf null.
public class MyGoodType
{
    private readonly IHttpContextAccessor _accessor;
    public MyGoodType(IHttpContextAccessor accessor)
    {
        _accessor = accessor;
    }

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

Kein Zugriff auf HttpContext aus mehreren Threads

HttpContext ist nicht threadsicher. Der parallele Zugriff auf HttpContext aus mehreren Threads kann zu unerwartetem Verhalten wie Unterbrechungen, Abstürzen und Datenbeschädigungen führen.

Gehen Sie nicht wie folgt vor: Das folgende Beispiel führt drei parallele Anforderungen aus und protokolliert den eingehenden Anforderungspfad vor und nach der ausgehenden HTTP-Anforderung. Mehrere Threads greifen möglicherweise parallel auf den Anforderungspfad zu.

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

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

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

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

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

        return await searchResults;
    }

Gehen Sie folgendermaßen vor: Das folgende Beispiel kopiert alle Daten aus der eingehenden Anforderung, bevor die drei parallelen Anforderungen ausgeführt werden.

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

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

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

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

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

        return await searchResults;
    }

Keine Verwendung von HttpContext nach Abschluss der Anforderung

HttpContext ist nur gültig, solange eine aktive HTTP-Anforderung in der ASP.NET Core-Pipeline vorhanden ist. Die gesamte ASP.NET Core-Pipeline ist eine asynchrone Kette aus Delegaten, die jede Anforderung ausführt. Wenn die von dieser Kette zurückgegebene Task abgeschlossen ist, wird der HttpContext neu gestartet.

Gehen Sie nicht wie folgt vor: Das folgende Beispiel verwendet async void, wodurch die HTTP-Anforderung abgeschlossen wird, wenn das erste await-Element erreicht ist:

  • Die Verwendung von async void ist IMMER eine schlechte Gepflogenheit in ASP.NET Core-Apps.
  • Der Beispielcode greift auf die HttpResponse zu, nachdem die HTTP-Anforderung abgeschlossen ist.
  • Durch den späten Zugriff stürzt der Prozess ab.
public class AsyncBadVoidController : Controller
{
    [HttpGet("/async")]
    public async void Get()
    {
        await Task.Delay(1000);

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

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

Gehen Sie folgendermaßen vor: Das folgende Beispiel gibt eine Task an das Framework zurück, sodass die HTTP-Anforderung erst abgeschlossen wird, wenn die Aktion abgeschlossen ist.

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

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

HttpContext nicht in Hintergrundthreads erfassen

Gehen Sie nicht wie folgt vor: Das folgende Beispiel zeigt eine Schließung, die den HttpContext aus der Controller-Eigenschaft erfasst. Dies ist eine schlechte Gepflogenheit, weil für das Arbeitselement Folgendes passieren könnte:

  • Es könnte außerhalb des Anforderungsbereichs ausgeführt werden.
  • Es könnte versucht werden, den falschen HttpContext zu lesen.
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

    return Accepted();
}

Gehen Sie folgendermaßen vor: Das folgende Beispiel führt Folgendes aus:

  • Es kopiert die Daten, die während der Anforderung in der Hintergrundaufgabe erforderlich sind.
  • Es verweist auf nichts aus dem Controller.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

Hintergrundaufgaben sollten als gehostete Dienste implementiert werden. Weitere Informationen finden Sie unter Hintergrundaufgaben mit gehosteten Diensten.

Keine in die Controller in Hintergrundthreads eingefügten Dienste erfassen

Gehen Sie nicht wie folgt vor: Das folgende Beispiel zeigt eine Schließung, die den DbContext aus dem Controller-Aktionsparameter erfasst. Dies ist eine schlechte Gepflogenheit. Das Arbeitselement könnte außerhalb des Anforderungsbereichs ausgeführt werden. Der Bereich für den ContosoDbContext ist auf die Anforderung festgelegt, was zu einer ObjectDisposedException führt.

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

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

    return Accepted();
}

Gehen Sie folgendermaßen vor: Das folgende Beispiel führt Folgendes aus:

  • Es fügt eine IServiceScopeFactory ein, um einen Bereich im Hintergrundarbeitselement zu erstellen. IServiceScopeFactory ist ein Singleton.
  • Es erstellt einen neuen Bereich für die Abhängigkeitsinjektion im Hintergrundthread.
  • Es verweist auf nichts aus dem Controller.
  • Es erfasst nicht den ContosoDbContext aus der eingehenden Anforderung.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

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

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Der hervorgehobene Code führt Folgendes aus:

  • Er erstellt einen Bereich für die Lebensdauer des Hintergrundvorgangs und löst Dienste daraus auf.
  • Er verwendet ContosoDbContext aus dem richtigen Bereich.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

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

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Keine Änderung am Statuscode oder an den Headern nach dem Start des Antworttexts

ASP.NET Core puffert den HTTP-Antworttext nicht. Wenn die Antwort zum ersten Mal geschrieben wird, passiert Folgendes:

  • Die Header werden zusammen mit diesem Abschnitt des Textkörpers an den Client gesendet.
  • Es ist nicht mehr möglich, Antwortheader zu ändern.

Gehen Sie nicht wie folgt vor: Der folgende Code versucht, Antwortheader hinzuzufügen, nachdem die Antwort bereits gestartet wurde:

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

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

Im vorherigen Code löst context.Response.Headers["test"] = "test value"; eine Ausnahme aus, wenn next() in die Antwort geschrieben hat.

Gehen Sie folgendermaßen vor: Das folgende Beispiel überprüft, ob die HTTP-Antwort gestartet wurde, bevor die Header geändert werden.

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

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

Gehen Sie folgendermaßen vor: Das folgende Beispiel verwendet HttpResponse.OnStarting, um die Header festzulegen, bevor die Antwortheader an den Client geleert werden.

Die Überprüfung, ob die Antwort noch nicht gestartet wurde, ermöglicht die Registrierung eines Rückrufs, der unmittelbar vor dem Schreiben der Antwortheader aufgerufen wird. Die Überprüfung, ob die Antwort noch nicht gestartet wurde, ermöglicht Folgendes:

  • Sie bietet die Möglichkeit, Header zeitgenau anzufügen oder zu überschreiben.
  • Sie erfordert keine Kenntnis der nächsten Middleware in der Pipeline.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

Kein Aufruf von „next()“, wenn bereits mit dem Schreiben in den Antworttext begonnen wurde

Komponenten erwarten einen Aufruf nur dann, wenn sie die Antwort verarbeiten und bearbeiten können.

Verwenden von In-Process-Hosting mit IIS

Beim Einsatz von In-Process-Hosting wird eine ASP.NET Core-App im gleichen Prozess wie ihr IIS-Arbeitsprozess ausgeführt. Das In-Process-Hosting bietet eine bessere Leistung als das Out-of-Process-Hosting, da Anforderungen nicht über den Loopbackadapter weitergeleitet werden. Der Loopbackadapter ist eine Netzwerkschnittstelle, die ausgehenden Netzwerkdatenverkehr zum selben Computer zurückleitet. IIS erledigt das Prozessmanagement mit dem Windows-Prozessaktivierungsdienst (Process Activation Service, WAS).

Projekte werden standardmäßig im In-Process-Hostingmodell in ASP.NET Core 3.0 und höher ausgeführt.

Weitere Informationen finden Sie unter Hosten von ASP.NET Core unter Windows mit IIS.

Nicht davon ausgehen, dass HttpRequest.ContentLength nicht NULL ist

HttpRequest.ContentLength ist NULL, wenn der Content-Length-Header nicht empfangen wird. NULL bedeutet in diesem Fall, dass die Länge des Anforderungstexts nicht bekannt ist; es bedeutet nicht, dass die Länge null ist. Da alle Vergleiche mit NULL (mit Ausnahme von ==) „false“ zurückgeben, kann der Vergleich Request.ContentLength > 1024 beispielsweise false zurückgeben, wenn die Größe des Anforderungstexts 1024 übersteigt. Das Fehlen dieser Information kann zu Sicherheitslücken in Apps führen. Möglicherweise gehen Sie davon aus, dass Sie sich vor zu umfangreichen Anforderungen schützen, obwohl dies nicht der Fall ist.

Weitere Informationen finden Sie in dieser StackOverflow-Antwort.

Zuverlässige Web-App-Muster

Eine Anleitung zum Erstellen einer modernen, zuverlässigen, leistungsfähigen, testbaren, kosteneffizienten und skalierbaren ASP.NET Core-App finden Sie in den YouTube-Videos und im Artikel zur zuverlässigen Web App für .NET – ganz gleich, ob Sie eine App von Grund auf neu erstellen oder umgestalten möchten.