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 der Pfade für heißen Code

In diesem Dokument wird ein heißer Codepfad als Codepfad definiert, der häufig aufgerufen wird und in dem ein Größerer Teil der Ausführungszeit auftritt. Heiße Codepfade beschränken in der Regel die Aufskalieren und Leistung von Apps 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 Sie die asynchrone Ausführung, indem Sie oder Task.Wait aufrufen Task<TResult>.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.
  • Rufen Sie Task.Run auf, und warten Sie sofort 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 Datenzugriff, E/A und Vorgänge mit langer Ausführungs laufzeit asynchron auf, wenn eine asynchrone API verfügbar ist. Verwenden Sie nicht , Task.Run um eine synchrone API asynchron zu machen.
  • Machen Sie Controller-/Razor 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 das Design 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 oder IEnumerable<T> 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 vor ToListAsync dem Zurückgeben des Aufzählbaren.

Ab ASP.NET Core 3.0 kann als Alternative zu verwendet werden, IAsyncEnumerable<T>IEnumerable<T> die asynchron 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 im Allgemeinen, 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 bei großen Objekten (> 85.000 Bytes) besonders teuer. 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 zum Speichern großer Arrays ArrayPool<T> , um Poolpuffer zu verwenden.
  • Weisen Sie nicht viele, kurzlebige große Objekte in heißen Codepfaden 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.
  • Wie viele Garbage Collections sind Generation 0, 1 und 2?

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 häufig abgerufener Daten aus einer Datenbank oder einem Remotedienst, wenn etwas veraltete Daten akzeptabel sind. Verwenden Sie je nach Szenario einen MemoryCache odereinen 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 für schreibgeschützte Zwecke auf Daten zugreifen. EF Core können die Ergebnisse von Abfragen ohne Nachverfolgung effizienter zurückgeben.
  • Filtern und aggregieren Sie LINQ-Abfragen (.Where.Selectz. B. mit -, - .Sum oder -Anweisungen), damit die Filterung von der Datenbank ausgefü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 überprüfen, die für den Zugriff auf Daten mit Application Insights oder mit Profilerstellungstools verstritt. Die meisten Datenbanken stellen auch Statistiken zu häufig ausgeführten Abfragen zur Verfügung.

Pool-HTTP-Verbindungen mit HttpClientFactory

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

Empfehlungen:

Halten Sie allgemeine Codepfade schnell.

Sie möchten, dass der gesamte Code schnell ist. Häufig aufgerufene Codepfade sind die wichtigsten zu optimierenden. Dazu gehören:

  • Middlewarekomponenten in der Anforderungsverarbeitungspipeline der App, insbesondere Middleware, werden früh 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. Beispielsweise benutzerdefinierte Protokollierung, Autorisierungshandler oder Initialisierung vorübergehender Dienste.

Empfehlungen:

Ausführen von Tasks mit langer Ausführungslaufzeit außerhalb von HTTP-Anforderungen

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

Empfehlungen:

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

Mindestwert für Clientressourcen

ASP.NET Core Apps mit komplexen Front-Ends stellen häufig viele JavaScript-, CSS- oder Imagedateien zur Verfügung. Die Leistung von anfänglichen Ladeanforderungen kann wie folgt verbessert werden:

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

Empfehlungen:

  • Verwenden Sie die Bündelungs- und Minification-Richtlinien, in denen kompatible Tools erwähnt werden und gezeigt wird, wie das Tag ASP.NET Core environment verwendet wird, um umgebungs- und Production -Developmentumgebungen zu verarbeiten.
  • Ziehen Sie andere Drittanbietertools wie Webpack für die komplexe Verwaltung von Clientobjekten in Betracht.

Komprimieren von Antworten

Die Reduzierung der Antwortgröße erhöht in der Regel die Reaktionsfähigkeit einer App, häufig drastisch. Eine Möglichkeit zum Reduzieren der Nutzlastgröß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 enthält Leistungsverbesserungen. Optimierungen in .NET Core und ASP.NET Core bedeuten, dass neuere Versionen im Allgemeinen ältere Versionen überholen. .NET Core 2.1 hat beispielsweise Unterstützung für kompilierte reguläre Ausdrücke hinzugefügt und von SpanT<> profitiert. 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 Priorität hat, sollten Sie ein Upgrade auf die aktuelle Version von ASP.NET Core in Erwägung ziehen.

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 langsamen 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 sowie bekannte Zuverlässigkeitsprobleme und Lösungen.

Vermeiden von synchronen Lese- oder Schreibzugriffen auf httpRequest/HttpResponse-Text

Alle E/A-Vorgänge in ASP.NET Core sind asynchron. Server implementieren die Stream -Schnittstelle, die über synchrone und asynchrone Überladungen verfügt. Die asynchronen Sollten bevorzugt werden, um blockierende Threadpoolthreads zu vermeiden. Blockierende Threads können zu einem Verhungern des Threadpools führen.

Gehen Sie wie hier nicht vor: Im folgenden Beispiel wird verwendet ReadToEnd. Der aktuelle Thread wird blockiert, um auf das Ergebnis zu warten. Dies ist ein Beispiel für die Synchronisierung über asynchron.

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 Get liest synchron den gesamten HTTP-Anforderungstext in den Arbeitsspeicher. Wenn der Client langsam hochgeladen wird, erfolgt die Synchronisierung der App über asynchron. Die App wird über asynchron synchronisiert, da Kestrelkeine synchronen Lesevorgängen unterstützt.

Gehen Sie wie hier vor: Im folgenden Beispiel wird der Thread beim Lesen verwendet ReadToEndAsync 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-Anforderungstext asynchron in den Arbeitsspeicher.

Warnung

Wenn die Anforderung groß ist, kann das Lesen des gesamten HTTP-Anforderungstexts 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 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 vorangehende Code de serialisiert den Anforderungstext asynchron in ein C#-Objekt.

ReadFormAsync gegenüber Request.Form bevorzugen

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

  • Das Formular wurde durch einen Aufruf ReadFormAsyncvon gelesen, und
  • Der zwischengespeicherte Formularwert wird mit gelesen. HttpContext.Request.Form

Gehen Sie wie hier nicht vor: Im folgenden Beispiel wird verwendet HttpContext.Request.Form. HttpContext.Request.Form verwendet die Synchronisierung über async und kann zu einem Verhungern des Threadpools 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 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 großer Anforderungs- oder Antwortkörper in den Arbeitsspeicher

In .NET wird jede Objektzuordnung, die größer als 85 KB ist, im großen Objektheap (Large Object Heap, LOH) abgelegt. Große Objekte sind auf zwei Arten teuer:

  • Die Zuordnungskosten 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.
  • LOH wird mit dem Rest des Heaps erfasst. LOH erfordert eine vollständige Garbage Collection oder Gen2-Sammlung.

In diesem Blogbeitrag wird das Problem kurz beschrieben:

Wenn ein großes Objekt zugeordnet wird, wird es als Gen 2-Objekt markiert. Nicht Gen 0 wie für kleine Objekte. Dies hat folgende Folgen: Wenn in LOH nicht genügend Arbeitsspeicher vorhanden ist, bereinigt GC den gesamten verwalteten Heap, nicht nur den LOH. Daher werden Gen 0, Gen 1 und Gen 2 einschließlich LOH bereinigt. Dies wird als vollständige Garbage Collection bezeichnet und ist die zeitaufwendigste Garbage Collection. Für viele Anwendungen kann dies akzeptabel sein. Aber definitiv nicht für Hochleistungswebserver, bei denen nur wenige große Speicherpuffer benötigt werden, um eine durchschnittliche Webanforderung zu verarbeiten (Lesen aus einem Socket, Dekomprimieren, Decodieren von JSON & mehr).

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

  • Es kann dazu führen, dass der Speicherplatz im LOH schnell knapp wird.
  • Kann zu Leistungsproblemen für die App führen, da 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. Sollte IHttpContextAccessor.HttpContextnicht in einem Feld oder einer Variablen gespeichert werden.

Gehen Sie wie hier nicht vor: Im folgenden Beispiel wird in HttpContext einem -Feld gespeichert und dann 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 zum richtigen Zeitpunkt 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 der Task von dieser Kette zurückgegebene abgeschlossen ist, wird wiederverwendet HttpContext .

Gehen Sie wie hier nicht vor: Im folgenden Beispiel wird verwendet async void , um die HTTP-Anforderung abzuschließen, 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 nach Abschluss der Aktion abgeschlossen wird.

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

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

Erfassen Sie 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 fehlerhafte Methode, da das Arbeitselement Folgendes könnte:

  • Wird außerhalb des Anforderungsbereichs ausgeführt.
  • Versuchen Sie, die falsche HttpContextzu 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 in der Hintergrundaufgabe während der Anforderung 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 Aktionsparameter Controller 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 ObjectDisposedExceptionfü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 löst eine Ausnahme aus, context.Response.Headers["test"] = "test value"; wenn next() in die 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 wird verwendet HttpResponse.OnStarting , 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 erwarten nur, dass sie aufgerufen werden, 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 verbesserte Leistung als Out-of-Process-Hosting, da Anforderungen nicht über den Loopbackadapter übermittelt werden. Der Loopbackadapter ist eine Netzwerkschnittstelle, die ausgehenden Netzwerkdatenverkehr zurück 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 ASP.NET Core auf Windows mit IIS.