ASP.NET Core Best Practices

Autor: Mike Rousos

Ten artykuł zawiera wskazówki dotyczące maksymalizacji wydajności i niezawodności aplikacji ASP.NET Core.

Pamięć podręczna agresywnie

Buforowanie omówiono w kilku częściach tego artykułu. Aby uzyskać więcej informacji, zobacz Omówienie buforowania w programie ASP.NET Core.

Omówienie ścieżek kodu gorącego

W tym artykule ścieżka kodu gorąca jest definiowana jako ścieżka kodu, która jest często wywoływana i gdzie występuje znaczna część czasu wykonywania. Ścieżki kodu gorącego zwykle ograniczają skalowanie aplikacji w poziomie i wydajność i są omawiane w kilku częściach tego artykułu.

Unikaj blokowania wywołań

aplikacje ASP.NET Core powinny być projektowane w celu jednoczesnego przetwarzania wielu żądań. Asynchroniczne interfejsy API umożliwiają małą pulę wątków do obsługi tysięcy współbieżnych żądań, nie czekając na wywołania blokujące. Zamiast czekać na wykonanie długotrwałego zadania synchronicznego, wątek może pracować nad innym żądaniem.

Typowym problemem z wydajnością w aplikacjach ASP.NET Core jest blokowanie wywołań, które mogą być asynchroniczne. Wiele synchronicznych wywołań blokowania prowadzi do głodu puli wątków i obniżonych czasów odpowiedzi.

Nie blokuj wykonywania asynchronicznego przez wywołanie metody Task.Wait lub Task<TResult>.Result. Nie pobieraj blokad w typowych ścieżkach kodu. aplikacje ASP.NET Core działają najlepiej, gdy są zaprojektowane do równoległego uruchamiania kodu. Nie dzwonij Task.Run i natychmiast czekaj na nie. ASP.NET Core uruchamia już kod aplikacji w normalnych wątkach puli wątków, dlatego wywoływanie Task.Run powoduje jedynie niepotrzebne planowanie puli wątków. Nawet jeśli zaplanowany kod zablokuje wątek, Task.Run nie zapobiega temu.

  • Wykonaj asynchroniczne ścieżki kodu gorącego.
  • Wywołaj dostęp do danych, operacje we/wy i długotrwałe interfejsy API operacji asynchronicznie, jeśli dostępny jest asynchroniczny interfejs API.
  • Nie należy używać Task.Run do tworzenia synchronicznego interfejsu API asynchronicznego.
  • Wykonaj asynchroniczne akcje kontrolera/Razor strony. Cały stos wywołań jest asynchroniczny, aby korzystać z wzorców asynchronicznych/await .

Profiler, taki jak PerfView, może służyć do znajdowania wątków często dodawanych do puli wątków. Zdarzenie Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start wskazuje wątek dodany do puli wątków.

Zwracanie dużych kolekcji na wielu mniejszych stronach

Strona internetowa nie powinna ładować jednocześnie dużych ilości danych. Podczas zwracania kolekcji obiektów należy rozważyć, czy może to prowadzić do problemów z wydajnością. Ustal, czy projekt może wygenerować następujące słabe wyniki:

Dodaj stronicowanie, aby wyeliminować powyższe scenariusze. Korzystając z parametrów rozmiaru strony i indeksu strony, deweloperzy powinni faworyzować projekt zwracania częściowego wyniku. Gdy wymagany jest wyczerpujący wynik, stronicowanie powinno być używane do asynchronicznego wypełniania partii wyników, aby uniknąć blokowania zasobów serwera.

Aby uzyskać więcej informacji na temat stronicowania i ograniczania liczby zwracanych rekordów, zobacz:

Zwracanie IEnumerable<T> lub IAsyncEnumerable<T>

IEnumerable<T> Powrót z akcji powoduje synchroniczną iterację kolekcji przez serializator. Wynikiem jest blokowanie wywołań i potencjalna funkcja głodu puli wątków. Aby uniknąć synchronicznej wyliczenia, przed zwróceniem wyliczenia należy użyć ToListAsync polecenia .

Począwszy od ASP.NET Core 3.0, IAsyncEnumerable<T> można użyć jako alternatywy dla IEnumerable<T> tego wyliczenia asynchronicznie. Aby uzyskać więcej informacji, zobacz Zwracane typy akcji kontrolera.

Minimalizuj alokacje dużych obiektów

Moduł odśmieceń pamięci platformy .NET Core automatycznie zarządza alokacją i zwalnianiem pamięci w aplikacjach platformy ASP.NET Core. Automatyczne odzyskiwanie pamięci zwykle oznacza, że deweloperzy nie muszą martwić się o to, jak lub kiedy pamięć jest zwolniona. Jednak czyszczenie obiektów bez wnioskowania zajmuje czas procesora CPU, więc deweloperzy powinni zminimalizować przydzielanie obiektów w ścieżce kodu gorącego. Odzyskiwanie pamięci jest szczególnie kosztowne w przypadku dużych obiektów (>= 85 000 bajtów). Duże obiekty są przechowywane na stercie dużych obiektów i wymagają pełnego (2. generacji) odzyskiwania pamięci do wyczyszczenia. W przeciwieństwie do kolekcji generacji 0 i generacji 1 kolekcja generacji 2 wymaga tymczasowego zawieszenia wykonywania aplikacji. Częste przydzielanie i anulowanie alokacji dużych obiektów może spowodować niespójną wydajność.

Rekomendacje:

  • Rozważ buforowanie dużych obiektów, które są często używane. Buforowanie dużych obiektów zapobiega kosztownym alokacjom.
  • Bufory puli są buforowane przy użyciu obiektu ArrayPool<T> do przechowywania dużych tablic.
  • Nie przydzielaj wielu krótkotrwałych dużych obiektów na gorących ścieżkach kodu.

Problemy z pamięcią, takie jak poprzednie, można zdiagnozować, przeglądając statystyki odzyskiwania pamięci (GC) w programie PerfView i sprawdzając:

  • Czas wstrzymania odzyskiwania pamięci.
  • Jaki procent czasu procesora jest poświęcany na odzyskiwanie pamięci.
  • Ile odzyskiwania pamięci jest generacji 0, 1 i 2.

Aby uzyskać więcej informacji, zobacz Odzyskiwanie pamięci i wydajność.

Optymalizowanie dostępu do danych i we/wy

Interakcje z magazynem danych i innymi usługami zdalnymi są często najwolniejszymi częściami aplikacji ASP.NET Core. Efektywne odczytywanie i zapisywanie danych ma kluczowe znaczenie dla dobrej wydajności.

Rekomendacje:

  • Wywołaj wszystkie interfejsy API dostępu do danych asynchronicznie.
  • Nie pobieraj więcej danych niż jest to konieczne. Pisanie zapytań w celu zwrócenia tylko danych niezbędnych do bieżącego żądania HTTP.
  • Należy rozważyć buforowanie często używanych danych pobranych z bazy danych lub usługi zdalnej, jeśli dane nieaktualne są akceptowalne. W zależności od scenariusza należy użyć usługi MemoryCache lub usługi DistributedCache. Aby uzyskać więcej informacji, zobacz Buforowanie odpowiedzi w programie ASP.NET Core.
  • Zminimalizuj rundy sieciowe. Celem jest pobranie wymaganych danych w jednym wywołaniu zamiast kilku wywołań.
  • Podczas uzyskiwania dostępu do danych tylko do odczytu należy używać zapytań bez śledzenia w programie Entity Framework Core. EF Core program może wydajniej zwracać wyniki zapytań bez śledzenia.
  • Filtruj i agreguj zapytania LINQ (na przykład z instrukcjami .Where, .Selectlub .Sum ), aby filtrowanie było wykonywane przez bazę danych.
  • Należy rozważyć EF Core rozwiązanie niektórych operatorów zapytań na kliencie, co może prowadzić do nieefektywnego wykonywania zapytań. Aby uzyskać więcej informacji, zobacz Problemy z wydajnością oceny klienta.
  • Nie używaj zapytań projekcji w kolekcjach, co może spowodować wykonywanie zapytań SQL "N + 1". Aby uzyskać więcej informacji, zobacz Optymalizacja skorelowanych podzapytania.

Poniższe podejścia mogą zwiększyć wydajność aplikacji o dużej skali:

Zalecamy pomiar wpływu powyższych podejść o wysokiej wydajności przed zatwierdzeniem bazy kodu. Dodatkowa złożoność skompilowanych zapytań może nie uzasadniać poprawy wydajności.

Problemy z zapytaniami można wykryć, przeglądając czas spędzony na uzyskiwaniu dostępu do danych za pomocą Szczegółowe informacje aplikacji lub narzędzi profilowania. Większość baz danych udostępnia również statystyki dotyczące często wykonywanych zapytań.

Buforowanie połączeń HTTP za pomocą elementu HttpClientFactory

Mimo że HttpClient implementuje IDisposable interfejs, jest przeznaczony do ponownego użycia. Zamknięte HttpClient wystąpienia pozostawiają gniazda otwarte w TIME_WAIT stanie przez krótki czas. Jeśli ścieżka kodu, która tworzy i usuwa HttpClient obiekty, jest często używana, aplikacja może wyczerpać dostępne gniazda. HttpClientFactory został wprowadzony w ASP.NET Core 2.1 jako rozwiązanie tego problemu. Obsługuje buforowanie połączeń HTTP w celu zoptymalizowania wydajności i niezawodności. Aby uzyskać więcej informacji, zobacz Używanie HttpClientFactory do implementowania odpornych żądań HTTP.

Rekomendacje:

  • Nie twórz ani nie usuwaj HttpClient wystąpień bezpośrednio.
  • Do pobierania HttpClient wystąpień należy użyć klasy HttpClientFactory. Aby uzyskać więcej informacji, zobacz Implementowanie odpornych żądań HTTP za pomocą elementu HttpClientFactory.

Szybkie utrzymywanie typowych ścieżek kodu

Chcesz, aby cały kod był szybki. Często nazywane ścieżki kodu są najbardziej krytyczne do optymalizacji. Są to:

  • Składniki oprogramowania pośredniczącego w potoku przetwarzania żądań aplikacji, szczególnie oprogramowanie pośredniczące jest uruchamiane na wczesnym etapie potoku. Te składniki mają duży wpływ na wydajność.
  • Kod wykonywany dla każdego żądania lub wiele razy na żądanie. Na przykład niestandardowe rejestrowanie, procedury obsługi autoryzacji lub inicjowanie usług przejściowych.

Rekomendacje:

  • Nie używaj niestandardowych składników oprogramowania pośredniczącego z długotrwałymi zadaniami.
  • Aby zidentyfikować gorące ścieżki kodu, należy użyć narzędzi profilowania wydajności, takich jak Narzędzia diagnostyczne programu Visual Studio lub Narzędzie PerfView.

Wykonywanie długotrwałych zadań poza żądaniami HTTP

Większość żądań do aplikacji ASP.NET Core może być obsługiwana przez kontroler lub model strony wywołujący niezbędne usługi i zwracając odpowiedź HTTP. W przypadku niektórych żądań obejmujących długotrwałe zadania lepiej jest wykonać cały proces odpowiedzi na żądanie asynchroniczne.

Rekomendacje:

  • Nie czekaj na ukończenie długotrwałych zadań w ramach zwykłego przetwarzania żądań HTTP.
  • Rozważ obsługę długotrwałych żądań z usługami w tle lub poza procesem za pomocą funkcji platformy Azure. Ukończenie pracy poza procesem jest szczególnie korzystne w przypadku zadań intensywnie korzystających z procesora CPU.
  • Używaj opcji komunikacji w czasie rzeczywistym, takich jak SignalR, do asynchronicznego komunikowania się z klientami.

Ujednolicanie zasobów klienta

ASP.NET aplikacje Core ze złożonymi frontonami często obsługują wiele plików javaScript, CSS lub image. Wydajność początkowych żądań ładowania można poprawić, wykonując następujące czynności:

  • Tworzenie pakietów, które łączy wiele plików w jeden.
  • Minimalizujące, co zmniejsza rozmiar plików przez usunięcie białych znaków i komentarzy.

Rekomendacje:

  • Skorzystaj z wytycznych dotyczących tworzenia pakietów i minyfikacji, które wspominają o zgodnych narzędziach i pokazują, jak używać tagu environment ASP.NET Core do obsługi środowisk Development i Production .
  • Należy rozważyć inne narzędzia innych firm, takie jak Webpack, na potrzeby złożonego zarządzania zasobami klienta.

Kompresowanie odpowiedzi

Zmniejszenie rozmiaru odpowiedzi zwykle zwiększa czas reakcji aplikacji, często dramatycznie. Jednym ze sposobów zmniejszenia rozmiarów ładunków jest kompresowanie odpowiedzi aplikacji. Aby uzyskać więcej informacji, zobacz Kompresja odpowiedzi.

Korzystanie z najnowszej wersji ASP.NET Core

Każda nowa wersja ASP.NET Core zawiera ulepszenia wydajności. Optymalizacje na platformie .NET Core i ASP.NET Core oznaczają, że nowsze wersje zwykle przewyższają starsze wersje. Na przykład platforma .NET Core 2.1 dodała obsługę skompilowanych wyrażeń regularnych i skorzystała z rozwiązania Span<T>. ASP.NET Core 2.2 dodano obsługę protokołu HTTP/2. ASP.NET Core 3.0 dodaje wiele ulepszeń , które zmniejszają użycie pamięci i zwiększają przepływność. Jeśli wydajność jest priorytetem, rozważ uaktualnienie do bieżącej wersji ASP.NET Core.

Minimalizuj wyjątki

Wyjątki powinny być rzadkie. Zgłaszanie i przechwytywanie wyjątków jest powolne w stosunku do innych wzorców przepływu kodu. W związku z tym wyjątki nie powinny być używane do kontrolowania normalnego przepływu programu.

Rekomendacje:

  • Nie należy używać zgłaszania lub przechwytywania wyjątków jako środka normalnego przepływu programu, zwłaszcza w ścieżkach kodu gorącego.
  • Uwzględnij logikę w aplikacji, aby wykrywać i obsługiwać warunki, które mogłyby spowodować wyjątek.
  • Zgłaszaj lub przechwytuj wyjątki dla nietypowych lub nieoczekiwanych warunków.

Narzędzia diagnostyczne aplikacji, takie jak application Szczegółowe informacje, mogą pomóc zidentyfikować typowe wyjątki w aplikacji, które mogą mieć wpływ na wydajność.

Unikaj synchronicznego odczytu lub zapisu w treści HttpRequest/HttpResponse

Wszystkie operacje we/wy w ASP.NET Core są asynchroniczne. Serwery implementują Stream interfejs, który ma zarówno synchroniczne, jak i asynchroniczne przeciążenia. Te asynchroniczne powinny być preferowane, aby uniknąć blokowania wątków puli wątków. Blokowanie wątków może prowadzić do głodu puli wątków.

Nie należy tego robić: W poniższym przykładzie użyto elementu ReadToEnd. Blokuje bieżący wątek do oczekiwania na wynik. Jest to przykład synchronizacji za pośrednictwem asynchronicznego.

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

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

W poprzednim kodzie Get synchronicznie odczytuje całą treść żądania HTTP do pamięci. Jeśli klient powoli przekazuje dane, aplikacja wykonuje synchronizację za pośrednictwem asynchronicznego. Aplikacja synchronizuje się za pośrednictwem asynchronicznego, ponieważ Kestrelnie obsługuje synchronicznych odczytów.

Wykonaj następujące czynności: w poniższym przykładzie użyto ReadToEndAsync metody i nie blokuje wątku podczas odczytywania.

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

}

Poprzedni kod asynchronicznie odczytuje całą treść żądania HTTP do pamięci.

Ostrzeżenie

Jeśli żądanie jest duże, odczytywanie całej treści żądania HTTP do pamięci może prowadzić do braku pamięci (OOM). OOM może spowodować odmowę usługi. Aby uzyskać więcej informacji, zobacz Unikanie odczytywania dużych treści żądań lub treści odpowiedzi w pamięci w tym artykule.

Wykonaj następujące czynności: Poniższy przykład jest w pełni asynchroniczny przy użyciu niebuforowanej treści żądania:

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

Poprzedni kod asynchronicznie deselizuje treść żądania do obiektu języka C#.

Preferuj narzędzie ReadFormAsync za pośrednictwem pliku Request.Form

Użyj HttpContext.Request.ReadFormAsync zamiast HttpContext.Request.Form. HttpContext.Request.Form można bezpiecznie odczytywać tylko z następującymi warunkami:

  • Formularz został odczytany przez wywołanie metody ReadFormAsynci
  • Buforowana wartość formularza jest odczytywana przy użyciu polecenia HttpContext.Request.Form

Nie należy tego robić: w poniższym przykładzie użyto metody HttpContext.Request.Form. HttpContext.Request.Form używa synchronizacji za pośrednictwem asynchronicznego i może prowadzić do głodu puli wątków.

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

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

        return Accepted();
    }

Wykonaj następujące czynności: poniższy przykład używa HttpContext.Request.ReadFormAsync metody do asynchronicznego odczytywania treści formularza.

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

Unikaj odczytywania dużych treści żądań lub treści odpowiedzi w pamięci

Na platformie .NET każda alokacja obiektu większa lub równa 85 000 bajtów kończy się w dużym stercie obiektu (LOH). Duże obiekty są kosztowne na dwa sposoby:

  • Koszt alokacji jest wysoki, ponieważ pamięć dla nowo przydzielonego dużego obiektu musi zostać wyczyszczone. ClR gwarantuje, że pamięć dla wszystkich nowo przydzielonych obiektów zostanie wyczyszczone.
  • LOH jest zbierane z resztą sterta. LOH wymaga pełnego odzyskiwania pamięci lub odzyskiwania generacji 2.

W tym wpisie w blogu opisano problem zwięźle:

Po przydzieleniu dużego obiektu jest on oznaczony jako obiekt 2. generacji. Nie gen 0 jak w przypadku małych obiektów. Konsekwencje są to, że jeśli zabraknie pamięci w LOH, GC czyści całą zarządzaną stertę, nie tylko LOH. W ten sposób czyści gen 0, Gen 1 i Gen 2, w tym LOH. Jest to nazywane pełnym odzyskiwaniem pamięci i jest najbardziej czasochłonnym odzyskiwaniem pamięci. W przypadku wielu aplikacji może to być akceptowalne. Ale zdecydowanie nie w przypadku serwerów internetowych o wysokiej wydajności, gdzie do obsługi średniego żądania internetowego jest potrzebnych kilka dużych buforów pamięci (odczyt z gniazda, dekompresuj, dekoduj JSwł. i nie tylko).

Przechowywanie dużej treści żądania lub odpowiedzi w jednym byte[] lub string:

  • Może spowodować szybkie wyczerpanie miejsca w LOH.
  • Może powodować problemy z wydajnością aplikacji z powodu uruchomionych pełnych kontrolerów domeny.

Praca z synchronicznym interfejsem API przetwarzania danych

W przypadku używania serializatora/de-serializatora, który obsługuje tylko synchroniczne odczyty i zapisy (na przykład Json.NET):

  • Buforuj dane do pamięci asynchronicznie przed przekazaniem ich do serializatora/de-serializatora.

Ostrzeżenie

Jeśli żądanie jest duże, może to prowadzić do braku pamięci (OOM). OOM może spowodować odmowę usługi. Aby uzyskać więcej informacji, zobacz Unikanie odczytywania dużych treści żądań lub treści odpowiedzi w pamięci w tym artykule.

program ASP.NET Core 3.0 domyślnie używa System.Text.Json serializacji JSON. System.Text.Json:

  • Odczyty i zapisy JSWŁ. asynchronicznie.
  • Jest zoptymalizowany pod kątem tekstu UTF-8.
  • Zazwyczaj wydajność jest wyższa niż Newtonsoft.Json.

Nie przechowuj obiektu IHttpContextAccessor.HttpContext w polu

Obiekt IHttpContextAccessor.HttpContext zwraca HttpContext aktywne żądanie po korzystaniu z wątku żądania. Element IHttpContextAccessor.HttpContext nie powinien być przechowywany w polu ani zmiennej.

Nie należy tego robić: poniższy przykład przechowuje HttpContext element w polu, a następnie próbuje go użyć później.

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

Powyższy kod często przechwytuje wartość null lub niepoprawną HttpContext w konstruktorze.

Wykonaj następujące czynności: Poniższy przykład:

  • Przechowuje element IHttpContextAccessor w polu.
  • HttpContext Używa pola w odpowiednim czasie i sprawdza element 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");
        }
    }
}

Nie należy uzyskiwać dostępu do obiektu HttpContext z wielu wątków

HttpContextnie jest bezpieczny wątkowo. Uzyskiwanie HttpContext dostępu z wielu wątków równolegle może spowodować nieoczekiwane zachowanie, takie jak zawieszanie się, awarie i uszkodzenie danych.

Nie należy tego robić: Poniższy przykład wykonuje trzy żądania równoległe i rejestruje ścieżkę żądania przychodzącego przed i po wychodzącym żądaniu HTTP. Ścieżka żądania jest uzyskiwana z wielu wątków, potencjalnie równolegle.

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

Zrób to: Poniższy przykład kopiuje wszystkie dane z żądania przychodzącego przed wykonaniem trzech żądań równoległych.

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

Nie używaj obiektu HttpContext po zakończeniu żądania

HttpContext jest prawidłowy tylko tak długo, jak istnieje aktywne żądanie HTTP w potoku ASP.NET Core. Cały potok ASP.NET Core jest asynchronicznym łańcuchem delegatów, który wykonuje każde żądanie. Po zakończeniu HttpContext powrotu Task z tego łańcucha element jest odzyskiwane.

Nie należy tego robić: W poniższym przykładzie użyto async void metody , która sprawia, że żądanie HTTP zostało ukończone po osiągnięciu pierwszego await żądania:

  • Użycie async void zawsze jestzłym rozwiązaniem w aplikacjach ASP.NET Core.
  • Przykładowy kod uzyskuje HttpResponse dostęp do obiektu po zakończeniu żądania HTTP.
  • Opóźniony dostęp powoduje awarię procesu.
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");
    }
}

Wykonaj następujące czynności: poniższy przykład zwraca element Task do platformy, więc żądanie HTTP nie zostanie ukończone, dopóki akcja nie zostanie ukończona.

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

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

Nie przechwytuj tekstu HttpContext w wątkach w tle

Nie należy tego robić: W poniższym przykładzie pokazano, że zamknięcie przechwytuje element HttpContext z Controller właściwości . Jest to zła praktyka, ponieważ element roboczy może:

  • Uruchom poza zakresem żądania.
  • Spróbuj odczytać niewłaściwy HttpContextelement .
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

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

    return Accepted();
}

Wykonaj następujące czynności: Poniższy przykład:

  • Kopiuje dane wymagane w zadaniu w tle podczas żądania.
  • Nie odwołuje się do niczego z kontrolera.
[HttpGet("/fire-and-forget-3")]
public IActionResult GoodFireAndForget()
{
    string path = HttpContext.Request.Path;
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        Log(path);
    });

    return Accepted();
}

Zadania w tle powinny być implementowane jako hostowane usługi. Aby uzyskać więcej informacji, zobacz Zadania w tle z hostowanymi usługami.

Nie przechwytuj usług wstrzykiwanych do kontrolerów w wątkach w tle

Nie należy tego robić: W poniższym przykładzie pokazano zamknięcie przechwytujące DbContext element z parametru Controller akcji. Jest to zła praktyka. Element roboczy może działać poza zakresem żądania. Element ContosoDbContext jest w zakresie żądania, co powoduje ObjectDisposedExceptionwystąpienie .

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

Wykonaj następujące czynności: Poniższy przykład:

  • Wprowadza element IServiceScopeFactory w celu utworzenia zakresu w elemencie roboczym w tle. IServiceScopeFactory jest singleton.
  • Tworzy nowy zakres wstrzykiwania zależności w wątku w tle.
  • Nie odwołuje się do niczego z kontrolera.
  • Nie przechwytuje ContosoDbContext elementu z żądania przychodzącego.
[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();
}

Następujący wyróżniony kod:

  • Tworzy zakres okresu istnienia operacji w tle i rozpoznaje z niej usługi.
  • Używa ContosoDbContext z prawidłowego zakresu.
[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();
}

Nie należy modyfikować kodu stanu ani nagłówków po rozpoczęciu treści odpowiedzi

ASP.NET Core nie buforuje treści odpowiedzi HTTP. Przy pierwszym zapisaniu odpowiedzi:

  • Nagłówki są wysyłane wraz z tym fragmentem treści do klienta.
  • Zmiana nagłówków odpowiedzi nie jest już możliwa.

Nie należy tego robić: Poniższy kod próbuje dodać nagłówki odpowiedzi po uruchomieniu odpowiedzi:

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

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

W poprzednim kodzie zostanie zgłoszony wyjątek, context.Response.Headers["test"] = "test value"; jeśli next() został zapisany w odpowiedzi.

Wykonaj to: Poniższy przykład sprawdza, czy odpowiedź HTTP została uruchomiona przed zmodyfikowanie nagłówków.

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

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

Wykonaj to: W poniższym przykładzie użyto HttpResponse.OnStarting polecenia , aby ustawić nagłówki przed opróżnieniu nagłówków odpowiedzi do klienta.

Sprawdzanie, czy odpowiedź nie została uruchomiona, umożliwia zarejestrowanie wywołania zwrotnego, które zostanie wywołane tuż przed zapisaniem nagłówków odpowiedzi. Sprawdzanie, czy odpowiedź nie została uruchomiona:

  • Zapewnia możliwość dołączania lub zastępowania nagłówków just in time.
  • Nie wymaga znajomości następnego oprogramowania pośredniczącego w potoku.
app.Use(async (context, next) =>
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers["someheader"] = "somevalue";
        return Task.CompletedTask;
    });

    await next();
});

Nie należy wywoływać metody next(), jeśli już rozpoczęto zapisywanie w treści odpowiedzi

Składniki oczekują wywołania tylko wtedy, gdy będzie można obsługiwać odpowiedź i manipulować nią.

Używanie hostingu w procesie z usługami IIS

W przypadku hostingu wewnątrz procesu aplikacja ASP.NET Core jest uruchamiana w tym samym procesie, co powiązany proces roboczy usług IIS. Hosting w procesie zapewnia lepszą wydajność hostingu poza procesem, ponieważ żądania nie są oparte na adapterze sprzężenia zwrotnego. Karta sprzężenia zwrotnego to interfejs sieciowy, który zwraca wychodzący ruch sieciowy z powrotem do tej samej maszyny. Usługi IIS obsługują zarządzanie procesami za pomocą usługi aktywacji procesów systemu Windows (WAS).

Projekty domyślne dla modelu hostingu w procesie w programie ASP.NET Core 3.0 lub nowszym.

Aby uzyskać więcej informacji, zobacz Host ASP.NET Core w systemie Windows z usługami IIS

Nie zakładaj, że właściwość HttpRequest.ContentLength nie ma wartości null

HttpRequest.ContentLength ma wartość null, jeśli Content-Length nagłówek nie zostanie odebrany. Wartość null w takim przypadku oznacza, że długość treści żądania nie jest znana; nie oznacza to, że długość wynosi zero. Ponieważ wszystkie porównania z wartością null (z wyjątkiem ==) zwracają wartość false, porównanie Request.ContentLength > 1024może na przykład zwracać false , gdy rozmiar treści żądania jest większy niż 1024. Nie wiedząc, że może to prowadzić do luk w zabezpieczeniach w aplikacjach. Możesz pomyśleć, że chronisz się przed zbyt dużymi żądaniami, gdy nie jesteś.

Aby uzyskać więcej informacji, zobacz tę odpowiedź stackOverflow.

Niezawodne wzorce aplikacji internetowej

Aby uzyskać wskazówki dotyczące tworzenia nowoczesnej, niezawodnej, wydajnej, wydajnej, ekonomicznej i skalowalnej aplikacji ASP.NET Core, od podstaw lub refaktoryzacji istniejącej aplikacji, zobacz Niezawodny wzorzecaplikacji internetowej for.NET YouTube.