ASP.NET Core Bewährte Methoden für die Leistung

Von Mike Rousos

Dieser Artikel enthält Richtlinien für bewährte Methoden für die Leistung ASP.NET Core.

Aggressiver Cache

Das Zwischenspeichern wird in mehreren Teilen dieses Dokuments erläutert. Weitere Informationen finden Sie unter Zwischenspeichern von Antworten in ASP.NET Core.

Verstehen von Pfaden für heißen Code

In diesem Dokument wird ein heißer Codepfad als Codepfad definiert, der häufig aufgerufen wird und an dem ein Teil der Ausführungszeit auftritt. Heiße Codepfade schränken in der Regel die Aufskalieren und Leistung von Apps ein und werden in mehreren Teilen dieses Dokuments erläutert.

Vermeiden von blockierenden Aufrufen

ASP.NET Core Apps sollten so konzipiert sein, dass sie viele Anforderungen gleichzeitig verarbeiten. 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 bei ASP.NET Core-Apps ist das Blockieren von Aufrufen, die asynchron sein können. Viele synchrone blockierende Aufrufe führen zu einem Threadmangel im Pool und zu längeren Antwortzeiten.

Vermeiden Sie Folgendes:

  • Blockieren der asynchronen Ausführung durch Aufrufen von Task.Wait oder Task.Result.
  • Abrufen von Sperren in allgemeinen Codepfaden. ASP.NET Core-Apps sind am leistungsreichsten, wenn sie so entworfen wurden, dass Code parallel ausgeführt wird.
  • Aufrufen von Task.Run und sofortiges Warten darauf. 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.

Empfohlene Vorgehensweise:

  • Machen Sie heiße Codepfade asynchron.
  • Rufen Sie ApIs für Den Datenzugriff, E/A und Vorgänge mit langer Laufzeit asynchron auf, wenn eine asynchrone API verfügbar ist. Verwenden Sie Task.Run nicht, um eine synchrone API asynchron zu machen.
  • Machen Sie Razor Controller-/Seitenaktionen asynchron. 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 Sammlungen auf mehreren kleineren Seiten

Eine Webseite sollte keine großen Datenmengen gleichzeitig laden. Wenn Sie eine Auflistung von -Objekten zurückgeben, sollten Sie überlegen, ob dies zu Leistungsproblemen führen kann. Bestimmen Sie, ob der Entwurf die folgenden schlechten Ergebnisse liefern könnte:

Fügen Sie pagination hinzu, um die oben genannten Szenarien zu minimieren. Mithilfe von Seitengrößen- und Seitenindexparametern sollten Entwickler den Entwurf der Rückgabe eines Teilergebnis bevorzugen. Wenn ein vollständiges Ergebnis erforderlich ist, sollte die Pagination verwendet werden, um Batches von Ergebnissen asynchron zu füllen, um das Sperren von Serverressourcen zu vermeiden.

Weitere Informationen zum Auslagerungs- und Begrenzen der Anzahl zurückgegebener Datensätze finden Sie unter:

Geben Sie IEnumerable<T> oder zurück. IAsyncEnumerable<T>

Die IEnumerable<T> Rückgabe von einer Aktion führt zu einer synchronen Auflistungsiteration durch das Serialisierungsizer. Das Ergebnis sind die Blockierung von Aufrufen und die potenzielle Außerkraftsetzung des Threadpools. Um synchrone Enumerationen zu vermeiden, verwenden Sie ToListAsync vor dem Zurückgeben des Aufzählbaren.

Ab ASP.NET Core 3.0 kann als Alternative zu verwendet werden, die asynchron IAsyncEnumerable<T> IEnumerable<T> aufzählt. Weitere Informationen finden Sie unter Rückgabetypen für Controlleraktion.

Minimieren von Zuordnungen für große Objekte

Der .NET Core-Garbage Collector verwaltet die Speicherbelegung und -freigabe automatisch in ASP.NET Core Apps. Die automatische Garbage Collection bedeutet in der Regel, dass Entwickler sich keine Gedanken darüber machen müssen, wie oder wann Arbeitsspeicher freigegeben wird. Das Bereinigen von objekten ohne Verweis nimmt jedoch CPU-Zeit in Anspruch, sodass Entwickler die Zuweisung von Objekten in Pfaden mit heißem Code minimieren sollten. Die Garbage Collection ist besonders teuer für große Objekte (> 85.000 Bytes). Große Objekte werden auf dem großen Objekthap gespeichert und erfordern eine vollständige Garbage Collection (Generation 2) für die Bereinigung. Im Gegensatz zu Sammlungen der Generation 0 und 1 erfordert eine Sammlung der Generation 2 eine vorübergehende Unterbrechung der App-Ausführung. Häufige Zuordnungen und das Entfernen der Zuordnung großer Objekte können zu inkonsistenter Leistung führen.

Empfehlungen:

  • Erwägen Sie das Zwischenspeichern großer Objekte, die häufig verwendet werden. Das Zwischenspeichern großer Objekte verhindert teure Zuordnungen.
  • Verwenden Sie einen ArrayPool <T> zum Speichern großer Arrays, um Poolpuffer zu verwenden.
  • Weisen Sie nicht viele, kurzlebige große Objekte in Pfaden mit heißem Code zu.

Arbeitsspeicherprobleme wie die vorherige können durch Überprüfen der Garbage Collection-Statistiken (GC) in PerfView und Durchsichten von Folgenden diagnostiziert werden:

  • Pausenzeit der Garbage Collection.
  • Der Prozentsatz der Prozessorzeit für die Garbage Collection.
  • Gibt an, wie viele Garbage Collections die Generation 0, 1 und 2 sind.

Weitere Informationen finden Sie unter Garbage Collection und Leistung.

Optimieren des Datenzugriffs und der E/A

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

Empfehlungen:

  • Rufen Sie alle Datenzugriffs-APIs asynchron auf.
  • Rufen Sie nicht mehr Daten ab, als erforderlich sind. Schreiben Sie Abfragen, um nur die Daten zurück zu geben, die für die aktuelle HTTP-Anforderung erforderlich sind.
  • Erwägen Sie das Zwischenspeichern von Daten, auf die häufig zugegriffen wird, die aus einer Datenbank oder einem Remotedienst abgerufen werden, wenn etwas veraltete Daten akzeptabel sind. Verwenden Sie je nach Szenario einen MemoryCache oder einen DistributedCache. Weitere Informationen finden Sie unter Zwischenspeichern von Antworten in ASP.NET Core.
  • Minimieren Sie Netzwerk-Roundtrips. Ziel ist es, die erforderlichen Daten in einem einzigen Aufruf und nicht in mehreren Aufrufen abzurufen.
  • Verwenden Sie Abfragen ohne Nachverfolgung in Entity Framework Core, wenn Sie auf Daten für schreibgeschützte Zwecke zugreifen. EF Core können die Ergebnisse von Abfragen ohne Nachverfolgung effizienter zurückgeben.
  • Filtern und aggregieren Sie LINQ-Abfragen (z. B. mit -, - oder -Anweisungen), damit die .Where .Select .Sum Filterung von der Datenbank durchgeführt wird.
  • Beachten Sie, EF Core einige Abfrageoperatoren auf dem Client auflösen, was zu einer ineffizienten Abfrageausführung führen kann. Weitere Informationen finden Sie unter Leistungsprobleme bei der Clientauswertung.
  • Verwenden Sie keine Projektionsabfragen für Sammlungen, was zur Ausführung von "N + 1" SQL führen kann. Weitere Informationen finden Sie unter Optimierung korrelierter Unterabfragen.

Unter EF High Performance finden Sie Ansätze, die die Leistung in apps mit hoher Skalierung verbessern können:

Es wird empfohlen, die Auswirkungen der oben genannten Hochleistungsansätze zu messen, bevor Sie die Codebasis committen. Die zusätzliche Komplexität kompilierter Abfragen kann die Leistungsverbesserung möglicherweise nicht rechtfertigen.

Abfrageprobleme können erkannt werden, indem sie die Zeit für den Zugriff auf Daten mit Application Insights oder mit Profilerstellungstools überprüfen. Die meisten Datenbanken stellen auch Statistiken zu häufig ausgeführten Abfragen zur Verfügung.

Pool-HTTP-Verbindungen mit HttpClientFactory

Obwohl HttpClient die -Schnittstelle IDisposable implementiert, ist sie für die Wiederverwendung konzipiert. Geschlossene Instanzen lassen Sockets für kurze Zeit im HttpClient TIME_WAIT Zustand geöffnet. Wenn häufig ein Codepfad verwendet wird, mit dem Objekte erstellt und verworfen werden, kann die App HttpClient die verfügbaren Sockets aufschöpfungen. HttpClientFactory wurde in ASP.NET Core 2.1 als Lösung für dieses Problem eingeführt. Er verarbeitet das Pooling von HTTP-Verbindungen, um Leistung und Zuverlässigkeit zu optimieren.

Empfehlungen:

Schnelles Verwenden gängiger Codepfade

Sie möchten, dass der ganze Code schnell ist. Häufig aufgerufene Codepfade sind die wichtigsten, die optimiert werden müssen. Dazu zählen unter anderem folgende Einstellungen:

  • Middlewarekomponenten in der Anforderungsverarbeitungspipeline der App, insbesondere Middleware, werden frühzeitig in der Pipeline ausgeführt. Diese Komponenten haben große Auswirkungen auf die Leistung.
  • Code, der für jede Anforderung oder mehrmals pro Anforderung ausgeführt wird. Beispiel: benutzerdefinierte Protokollierung, Autorisierungshandler oder Initialisierung vorübergehender Dienste.

Empfehlungen:

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

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

Empfehlungen:

  • Warten Sie nicht, bis Aufgaben mit langer Ausführungszeit im Rahmen der normalen HTTP-Anforderungsverarbeitung abgeschlossen sind.
  • Erwägen Sie die Verarbeitung von Anforderungen mit langer Ausführungsleistung mit Hintergrunddiensten oder außer Betrieb mit einer Azure-Funktion. Das Out-of-Process-Abschließen der Arbeit ist besonders für CPU-intensive Aufgaben von Vorteil.
  • Verwenden Sie Echtzeitkommunikationsoptionen wie , um SignalR asynchron mit Clients zu kommunizieren.

Minify-Clientressourcen

ASP.NET Core-Apps mit komplexen Front-Ends bedienen häufig viele JavaScript-, CSS- oder Imagedateien. Die Leistung anfänglicher Auslastungsanforderungen kann durch:

  • Bündelung, bei der mehrere Dateien in einer Datei kombiniert werden.
  • Verkleinern, wodurch die Größe von Dateien reduziert wird, indem Leerzeichen und Kommentare entfernt werden.

Empfehlungen:

  • Verwenden Sie die Richtlinien zur Bündelung und Minierung,in denen kompatible Tools erwähnt werden und die Verwendung des Tags ASP.NET Core für die Handhabung von - und environment Development -Umgebungen Production veranschaulicht wird.
  • Ziehen Sie andere Tools von Drittanbietern, z. B. Webpack,für die verwaltung komplexer Client-Ressourcen in Betracht.

Komprimieren von Antworten

Das Reduzieren der Antwortgröße erhöht in der Regel die Reaktionsfähigkeit einer App, häufig erheblich. Eine Möglichkeit, die Nutzlastgröße zu reduzieren, besteht in der Komprimierung der Antworten einer App. Weitere Informationen finden Sie unter Antwortkomprimierung.

Verwenden des neuesten ASP.NET Core Release

Jedes neue Release von ASP.NET Core leistungsverbesserungen. Optimierungen in .NET Core und ASP.NET Core, dass neuere Versionen in der Regel ältere Versionen übertrifft. .NET Core 2.1 hat beispielsweise Unterstützung für kompilierte reguläre Ausdrücke hinzugefügt und von Span profitiert. <T> ASP.NET Core 2.2 wurde Unterstützung für HTTP/2 hinzugefügt. ASP.NET Core 3.0 bietet viele Verbesserungen, die die Speicherauslastung reduzieren und den Durchsatz verbessern. Wenn die Leistung eine Priorität hat, sollten Sie ein Upgrade auf die aktuelle Version von ASP.NET Core.

Minimieren von Ausnahmen

Ausnahmen sollten selten sein. Das Auslösen und Abfangen von Ausnahmen ist relativ zu anderen Codeflussmustern langsam. Aus diesem Grund sollten Ausnahmen nicht verwendet werden, um den normalen Programmablauf zu steuern.

Empfehlungen:

  • Verwenden Sie das Auslösen oder Abfangen von Ausnahmen nicht als Mittel des normalen Programmablaufs, insbesondere in langsamsten Codepfaden.
  • Schließen Sie Logik in die App ein, um Bedingungen zu erkennen und zu behandeln, die eine Ausnahme verursachen würden.
  • Auslösen oder Abfangen von Ausnahmen bei ungewöhnlichen oder unerwarteten 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.

Leistung und Zuverlässigkeit

Die folgenden Abschnitte enthalten Tipps zur Leistung und bekannte Zuverlässigkeitsprobleme und Lösungen.

Vermeiden synchroner Lese- oder Schreibzugriffe auf httpRequest/HttpResponse-Text

Alle E/A-Vorgänge in ASP.NET Core asynchron. Server implementieren die Stream -Schnittstelle, die sowohl synchrone als auch asynchrone Überladungen hat. Die asynchronen sollten bevorzugt werden, um das Blockieren von Threadpoolthreads zu vermeiden. Blockierende Threads können zu einem Verhungern des Threadpools führen.

Führen Sie dies nicht aus: Im folgenden Beispiel wird ReadToEnd verwendet. Der aktuelle Thread wird blockiert, um auf das Ergebnis zu warten. Dies ist ein Beispiel für die Synchronisierung über asynchrone.

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

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

Im vorangehenden Code liest Get synchron den gesamten HTTP-Anforderungskörper in den Arbeitsspeicher. Wenn der Client langsam hochgeladen wird, wird die Synchronisierung über asynchron ausgeführt. Die App wird asynchron synchronisiert, Kestrel da synchrone Lese lese nicht unterstützt.

Gehen Sie wie hier vor: Im folgenden Beispiel wird ReadToEndAsync der Thread beim Lesen verwendet und nicht blockiert.

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 vorangehende Code liest den gesamten HTTP-Anforderungskörper asynchron in den Arbeitsspeicher.

Warnung

Wenn die Anforderung groß ist, kann das Lesen des gesamten HTTP-Anforderungskörpers in den Arbeitsspeicher zu einer OOM-Bedingung (Out of Memory) führen. OOM kann zu einem Denial-of-Service-Angriff führen. Weitere Informationen finden Sie unter Vermeiden des Lesens großer Anforderungs- oder Antwortkörper in den Arbeitsspeicher in diesem Dokument.

Gehen Sie wie hier vor: Das folgende Beispiel ist mit einem nicht gepufferten Anforderungskörper vollständig asynchron:

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

Mit dem vorangehenden Code wird der Anforderungskörper asynchron in ein C#-Objekt deseriiert.

ReadFormAsync gegenüber Request.Form bevorzugen

Verwenden Sie HttpContext.Request.ReadFormAsync anstelle von HttpContext.Request.Form. HttpContext.Request.Form kann mit den folgenden Bedingungen sicher schreibgeschützt sein:

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

Führen Sie dies nicht aus: Im folgenden Beispiel wird HttpContext.Request.Form verwendet. HttpContext.Request.Form verwendet Synchronisierung über asynchron und kann zu einem Threadpoolhunger 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 wie hier vor: Im folgenden Beispiel wird HttpContext.Request.ReadFormAsync verwendet, um den Formularkörper 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 großer Anforderungs- oder Antwortkörper in den Arbeitsspeicher

In .NET wird jede Objektzuordnung größer als 85 KB im großen Objekthap(LOH) enden. Große Objekte sind auf zwei Arten teuer:

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

In diesem Blogbeitrag wird das Problem kurz beschrieben:

Wenn ein großes Objekt zugeordnet wird, wird es als Gen 2-Objekt gekennzeichnet. Nicht Gen 0 wie für kleine Objekte. Die Folgen sind, dass gc nicht nur den gesamten verwalteten Heap, sondern auch den gesamten verwalteten Heap bereinigt, wenn im LOH nicht mehr genügend Arbeitsspeicher verfügbar ist. Daher werden Gen 0, Gen 1 und Gen 2 einschließlich LOH bereinigt. Dies wird als vollständige Garbage Collection bezeichnet und ist die zeitaufwändigste Garbage Collection. Für viele Anwendungen kann dies akzeptabel sein. Aber definitiv nicht für Hochleistungswebserver, bei denen wenige große Speicherpuffer erforderlich sind, um eine durchschnittliche Webanforderung zu verarbeiten (Lesen aus einem Socket, Dekomprimieren, Decodieren von JSON & mehr).

Naives Speichern eines großen Anforderungs- oder Antwortkörpers in einem einzelnen byte[] oder string :

  • Dies kann dazu führen, dass der Speicherplatz im LOH schnell knapp wird.
  • Dies kann zu Leistungsproblemen für die App führen, weil vollständige GCs ausgeführt werden.

Arbeiten mit einer synchronen Datenverarbeitungs-API

Bei Verwendung eines Serialisierungsprogramms/Deserialisierungsprogramms, das nur synchrone Lese- und Schreibvorgänge unterstützt (z. B. Json.NET):

  • Puffern Sie die Daten asynchron in den Arbeitsspeicher, bevor Sie sie an das Serialisierungsprogramm/Deserialisierungsprogramm übergeben.

Warnung

Wenn die Anforderung groß ist, kann dies zu einer OoM-Bedingung (Out of Memory) führen. OOM kann zu einem Denial-of-Service-Angriff führen. Weitere Informationen finden Sie unter Vermeiden des Lesens großer Anforderungs- oder Antwortkörper in den Arbeitsspeicher in diesem Dokument.

ASP.NET Core 3.0 verwendet System.Text.Json standardmäßig für die 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.

Speichern Sie IHttpContextAccessor.HttpContext nicht in einem Feld.

IHttpContextAccessor.HttpContext gibt den HttpContext der aktiven Anforderung zurück, wenn über den Anforderungsthread darauf zugegriffen wird. IHttpContextAccessor.HttpContextSollte nicht in einem Feld oder einer Variablen gespeichert werden.

Gehen Sie wie hier nicht vor: Im folgenden Beispiel wird in einem -Feld gespeichert HttpContext und anschließend versucht, es 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 null oder falsch HttpContext im Konstruktor.

Gehen Sie wie hier vor: Das folgende Beispiel:

  • Speichert die IHttpContextAccessor in einem Feld.
  • Verwendet das HttpContext Feld zur richtigen Zeit und überprü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");
        }
    }
}

Nicht von mehreren Threads aus auf HttpContext zugreifen

HttpContext ist NICHT threadsicher. Der HttpContext parallele Zugriff von mehreren Threads kann zu undefiniertem Verhalten wie Hängen, Abstürzen und Datenbeschädigungen führen.

Gehen Sie wie hier nicht vor: Im folgenden Beispiel werden drei parallele Anforderungen gestellt und der eingehende Anforderungspfad vor und nach der ausgehenden HTTP-Anforderung protokolliert. Auf den Anforderungspfad wird von mehreren Threads aus zugegriffen, möglicherweise parallel.

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 wie hier vor: Im folgenden Beispiel werden alle Daten aus der eingehenden Anforderung kopiert, bevor die drei parallelen Anforderungen gestellt 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;
    }

Verwenden Sie httpContext nicht, nachdem die Anforderung abgeschlossen ist.

HttpContextist 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 von Delegaten, die jede Anforderung ausführt. Wenn die Task von dieser Kette zurückgegebene abgeschlossen HttpContext ist, wird wiederverwendet.

Gehen Sie wie hier nicht vor: Im folgenden Beispiel async void wird verwendet, wodurch die HTTP-Anforderung abgeschlossen wird, wenn die erste await erreicht wird:

  • Dies ist immer eine fehlerhafte Methode in ASP.NET Core-Apps.
  • Greift auf zu, HttpResponse nachdem die HTTP-Anforderung abgeschlossen ist.
  • Stürzt den 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 wie hier vor: Im folgenden Beispiel wird ein Task an das Framework zurückgegeben, 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");
    }
}

Erfassen Sie den HttpContext nicht in Hintergrundthreads.

Gehen Sie wie hier nicht vor: Das folgende Beispiel zeigt, dass ein Abschluss die HttpContext von der Controller -Eigenschaft erfasst. Dies ist eine ungültige Vorgehensweise, da das Arbeitselement Folgendes könnte:

  • Wird außerhalb des Anforderungsbereichs ausgeführt.
  • Versuchen Sie, die falsche zu HttpContext 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 wie hier vor: Das folgende Beispiel:

  • Kopiert die daten, die während der Anforderung in der Hintergrundaufgabe erforderlich sind.
  • Verweist auf nichts vom 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.

Erfassen Sie keine Dienste, die in Hintergrundthreads in die Controller eingefügt werden.

Gehen Sie wie hier nicht vor: Das folgende Beispiel zeigt, dass ein Abschluss das DbContext aus dem Controller Aktionsparameter erfasst. Dies ist eine fehlerhafte Methode. Das Arbeitselement kann außerhalb des Anforderungsbereichs ausgeführt werden. Der ContosoDbContext Bereich ist auf die Anforderung gerichtet, was zu einem 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 wie hier vor: Das folgende Beispiel:

  • Fügt ein IServiceScopeFactory ein, um einen Bereich im Hintergrundarbeitselement zu erstellen. IServiceScopeFactory ist ein Singleton.
  • Erstellt einen neuen Abhängigkeitsinjektionsbereich im Hintergrundthread.
  • Verweist auf nichts vom Controller.
  • Erfasst nicht die ContosoDbContext aus der eingehenden Anforderung.
[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

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

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Der folgende hervorgehobene Code:

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

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

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

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Ändern Sie den Statuscode oder die Header nicht, nachdem der Antworttext gestartet wurde.

ASP.NET Core puffert den HTTP-Antworttext nicht. Beim ersten Schreiben der Antwort:

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

Gehen Sie wie hier nicht 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 vorangehenden Code context.Response.Headers["test"] = "test value"; löst eine Ausnahme aus, wenn in die next() Antwort geschrieben wurde.

Gehen Sie wie hier vor: Im folgenden Beispiel wird ü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 wie hier vor: Im folgenden Beispiel HttpResponse.OnStarting wird verwendet, um die Header festzulegen, bevor die Antwortheader an den Client geleert werden.

Wenn Sie überprüfen, ob die Antwort nicht gestartet wurde, können Sie einen Rückruf registrieren, der direkt vor dem Schreiben von Antwortheadern aufgerufen wird. Überprüfen, ob die Antwort nicht gestartet wurde:

  • Bietet die Möglichkeit, Header just-in-time anzufügen oder zu überschreiben.
  • 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();
});

Rufen Sie next() nicht auf, wenn Sie bereits mit dem Schreiben in den Antworttext begonnen haben.

Komponenten werden nur aufgerufen, 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. In-Process-Hosting bietet eine verbesserte Leistung gegenüber Out-of-Process-Hosting, da Anforderungen nicht über den Loopbackadapter übermittelt werden. Der Loopbackadapter ist eine Netzwerkschnittstelle, die ausgehenden Netzwerkdatenverkehr an denselben Computer zurückgibt. IIS erledigt das Prozessmanagement mit dem Windows-Prozessaktivierungsdienst (Process Activation Service, WAS).

Projekte verwenden standardmäßig das In-Process-Hostingmodell in ASP.NET Core 3.0 und höher.

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