Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Note
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.
Warning
Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.
Według Mike Rousos
Ten artykuł zawiera wskazówki dotyczące maksymalizacji wydajności i niezawodności aplikacji ASP.NET Core.
Agresywne cachowanie
Buforowanie zostało omówione w kilku częściach tego artykułu. Aby uzyskać więcej informacji, zobacz Omówienie buforowania w programie ASP.NET Core.
Poznaj gorące ścieżki kodu
W tym artykule gorąca ścieżka kodu jest definiowana jako ścieżka kodu, która jest często wywoływana i w której spędzana jest znaczna część czasu wykonywania. Newralgiczne ścieżki kodu 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 pozwalają niewielkiej puli wątków obsługiwać tysiące 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ń blokujących prowadzi do wyczerpania puli wątków i wydłużenia 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 wywołuj Task.Run i natychmiast oczekuj na jego wynik. ASP.NET Core uruchamia już kod aplikacji w normalnych wątkach puli wątków, dlatego wywoływanie Task.Run powoduje jedynie niepotrzebne planowanie w puli wątków. Nawet jeśli kod zaplanowany do wykonania zablokuje wątek, Task.Run nie zapobiega temu.
- Spraw, abygorące ścieżki kodu były asynchroniczne.
- Wywołuj asynchronicznie interfejsy API dostępu do danych, operacji we/wy i operacji długotrwałych, jeśli dostępny jest asynchroniczny interfejs API.
- Nie należy używać Task.Run, aby uczynić synchroniczne API asynchronicznym.
- Uczyń akcje strony/kontrolera Razor asynchronicznymi. Cały stos wywołań jest asynchroniczny, aby korzystać z wzorców async/await.
- Rozważ użycie brokerów komunikatów, takich jak usługa Azure Service Bus , aby odciążyć długotrwałe wywołania
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, że wątek został dodany do puli wątków.
Zwracaj duże kolekcje 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:
- OutOfMemoryException lub wysokie zużycie pamięci
- Wyczerpanie puli wątków (zobacz poniższe uwagi na temat IAsyncEnumerable<T>)
- Długi czas odpowiedzi
- Częste odśmiecanie pamięci
Dodaj stronicowanie, aby ograniczyć skutki powyższych scenariuszy. Korzystając z parametrów rozmiaru strony i indeksu strony, deweloperzy powinni preferować rozwiązanie polegające na zwracaniu częściowych wyników. 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:
Zwróć IEnumerable<T> lub IAsyncEnumerable<T>
Zwrócenie IEnumerable<T> przez akcję powoduje synchroniczną iterację kolekcji przez serializator. Wynikiem jest blokowanie wywołań i możliwość zagłodzenia puli wątków. Aby uniknąć synchronicznego wyliczania, przed zwróceniem kolekcji wyliczalnej należy użyć ToListAsync.
Począwszy od ASP.NET Core 3.0, IAsyncEnumerable<T> można użyć jako alternatywy dla IEnumerable<T>, które wylicza asynchronicznie. Aby uzyskać więcej informacji, zobacz Typy zwracane akcji kontrolera.
Minimalizuj alokacje dużych obiektów
Moduł odśmiecania pamięci .NET automatycznie zarządza alokacją i zwalnianiem pamięci w aplikacjach ASP.NET Core. Automatyczne odśmiecanie pamięci zwykle oznacza, że deweloperzy nie muszą martwić się o to, jak lub kiedy pamięć jest zwolniona. Jednak czyszczenie obiektów bez referencji zajmuje czas procesora CPU, więc deweloperzy powinni zminimalizować przydzielanie obiektów w gorących ścieżkach kodu. 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 dealokacja dużych obiektów może spowodować niespójną wydajność.
Recommendations:
- Rozważ buforowanie dużych obiektów, które są często używane. Buforowanie dużych obiektów zapobiega kosztownym alokacjom.
- Buforuj bufory puli przy użyciu 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 opisane powyżej, można zdiagnozować, przeglądając statystyki odzyskiwania pamięci (GC) w programie PerfView i sprawdzając:
- Czas wstrzymania odśmiecania pamięci.
- Jaki procent czasu procesora jest poświęcany na odzyskiwanie pamięci.
- Ile operacji odśmiecania pamięci dotyczy generacji 0, 1 i 2?
Aby uzyskać więcej informacji, zobacz Odzyskiwanie pamięci i wydajność.
Optymalizacja dostępu do danych i I/O
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.
Recommendations:
- Wywołaj wszystkie interfejsy API dostępu do danych asynchronicznie.
- Nie pobieraj więcej danych niż jest to konieczne. Pisz zapytania 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 liczbę komunikacji sieciowych. Celem jest pobranie wymaganych danych w jednym wywołaniu zamiast kilku wywołań.
- Należy używać zapytań bez śledzenia w programie Entity Framework Core podczas uzyskiwania dostępu do danych tylko do odczytu. 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ć, że EF Core wykonuje niektóre operatory zapytań po stronie klienta, 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 podzapytań.
Poniższe podejścia mogą zwiększyć wydajność aplikacji o dużej skali:
Zalecamy pomiar wpływu wcześniejszych podejść o wysokiej wydajności przed wykonaniem commitu w bazie 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ą Application Insights lub za pomocą narzędzi profilowania. Większość baz danych udostępnia również statystyki dotyczące często wykonywanych zapytań.
Korzystanie z puli połączeń HTTP za pomocą HttpClientFactory
Mimo że HttpClient implementuje IDisposable interfejs, jest przeznaczony do ponownego użycia. Zamknięte HttpClient instancje pozostawiają gniazda otwarte w TIME_WAIT stanie przez krótki czas. Jeśli ścieżka kodu, która tworzy i zwalnia obiekty HttpClient, 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 zarządzanie pulą połączeń HTTP w celu zoptymalizowania wydajności i niezawodności. Aby uzyskać więcej informacji, zobacz Używanie HttpClientFactory do implementacji odpornych żądań HTTP.
Recommendations:
-
Nie twórz ani nie zwalniaj
HttpClientwystąpień bezpośrednio. -
Należy używać elementu HttpClientFactory do pobierania
HttpClientwystąpień. Aby uzyskać więcej informacji, zobacz Używanie HttpClientFactory do implementowania odpornych żądań HTTP.
Utrzymuj typowe ścieżki kodu szybkie
Chcesz, aby cały kod był szybki. Często wywoływane ścieżki kodu są najbardziej krytyczne do optymalizacji. Są to:
- Składniki oprogramowania pośredniczącego w potoku przetwarzania żądań aplikacji, zwłaszcza składniki oprogramowania pośredniczącego uruchamiane na wczesnym etapie potoku. Te składniki mają duży wpływ na wydajność.
- Kod wykonywany dla każdego żądania lub wielokrotnie w ramach jednego żądania. Na przykład niestandardowe logowanie, procedury obsługi autoryzacji lub inicjowanie usług nietrwałych.
Recommendations:
- Nie używaj niestandardowych komponentów middleware z długotrwałymi zadaniami.
- Należy użyć narzędzi profilowania wydajności, takich jak Narzędzia diagnostyczne programu Visual Studio lub PerfView, aby zidentyfikować gorące ścieżki kodu.
Wykonuj długotrwałe zadania poza żądaniami HTTP
Większość żądań do aplikacji ASP.NET Core może być obsługiwana przez kontroler lub model strony, który wywołuje niezbędne usługi i zwraca odpowiedź HTTP. W przypadku niektórych żądań obejmujących długotrwałe zadania lepiej jest uczynić cały proces żądanie–odpowiedź asynchronicznym.
Recommendations:
- Nie czekaj na ukończenie długotrwałych zadań w ramach zwykłego przetwarzania żądań HTTP.
- Rozważ obsługę długotrwałych żądań za pomocą usług działających w tle lub poza procesem, prawdopodobnie za pomocą funkcji platformy Azure i/lub brokera komunikatów, takiego jak Azure Service Bus. Wykonywanie 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.
Minifikowanie zasobów klienta
aplikacje ASP.NET Core ze złożonymi frontendami często udostępniają wiele plików JavaScript, CSS lub obrazów. Wydajność początkowych żądań ładowania można poprawić, wykonując następujące czynności:
- Pakietowanie, które łączy wiele plików w jeden.
- Minifikacja, która zmniejsza rozmiar plików przez usunięcie białych znaków i komentarzy.
Recommendations:
-
Skorzystaj z wytycznych dotyczących tworzenia pakietów i minyfikacji, które wspominają o zgodnych narzędziach i pokazują, jak używać tagu
environmentASP.NET Core do obsługi środowiskDevelopmentiProduction. - Należy rozważyć inne narzędzia innych firm, takie jak Webpack, na potrzeby złożonego zarządzania zasobami po stronie klienta.
Kompresowanie odpowiedzi
Zmniejszenie rozmiaru odpowiedzi zwykle zwiększa responsywność aplikacji, często dramatycznie. Jednym ze sposobów zmniejszenia rozmiarów payloadu jest kompresowanie odpowiedzi aplikacji. Aby uzyskać więcej informacji, zobacz Kompresja odpowiedzi HTTP.
Korzystanie z najnowszej wersji ASP.NET Core
Każda nowa wersja ASP.NET Core zawiera ulepszenia wydajności. Optymalizacje na platformie .NET 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 odniosła korzyści dzięki Span<T>. W 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.
Recommendations:
- Nie należy używać zgłaszania lub przechwytywania wyjątków jako sposobu normalnego sterowania przepływem programu, zwłaszcza w gorących ścieżkach kodu.
- Uwzględnij logikę w aplikacji, aby wykrywać i obsługiwać warunki, które mogłyby spowodować wystąpienie wyjątku.
- Rzucaj lub przechwytuj wyjątki dla nietypowych lub nieoczekiwanych warunków.
Narzędzia diagnostyczne aplikacji, takie jak Application Insights, mogą pomóc w zidentyfikowaniu typowych wyjątków w aplikacji, które mogą mieć wpływ na wydajność.
Unikaj synchronicznego odczytu lub zapisu na treści żądania/odpowiedzi HttpRequest/HttpResponse
Wszystkie operacje we/wy w ASP.NET Core są asynchroniczne. Serwery obsługują interfejs Stream, który ma zarówno synchroniczne, jak i asynchroniczne przeciążenia. Metody asynchroniczne należy preferować, aby uniknąć blokowania wątków puli wątków. Blokowanie wątków może prowadzić do wyczerpania puli wątków.
Nie należy tego robić: W poniższym przykładzie użyto elementu ReadToEnd. Blokuje bieżący wątek podczas oczekiwania na wynik. Jest to przykład sync over async.
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 wolno przesyła dane, aplikacja wykonuje synchronicznie operacje asynchroniczne. Aplikacja używa trybu synchronicznego zamiast asynchronicznego, ponieważ KestrelNIE obsługuje synchronicznych odczytów.
Wykonaj to: W poniższym przykładzie użyto metody ReadToEndAsync, która nie blokuje wątku podczas odczytu.
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.
Warning
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ć atak typu Denial of Service (DoS). 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 deserializuje treść żądania do obiektu C#.
Preferuj ReadFormAsync zamiast Request.Form
Użyj HttpContext.Request.ReadFormAsync zamiast HttpContext.Request.Form.
HttpContext.Request.Form można bezpiecznie odczytywać tylko w następujących warunkach:
- 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 HttpContext.Request.Form.
HttpContext.Request.Form używa synchronicznie na asynchronicznym 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 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 do pamięci
Na platformie .NET każda alokacja obiektu większa lub równa 85 000 bajtów kończy się na stercie dużych obiektów (LOH). Duże obiekty są kosztowne na dwa sposoby:
- Koszt alokacji jest wysoki, ponieważ pamięć dla nowo przydzielonego dużego obiektu musi zostać wyczyszczona. ClR gwarantuje, że pamięć dla wszystkich nowo przydzielonych obiektów zostanie wyczyszczona.
- LOH jest zbierany z resztą sterty. LOH wymaga pełnego odśmiecania pamięci lub odśmiecania pamięci generacji 2.
W tym wpisie w blogu opisano problem zwięźle:
Po przydzieleniu dużego obiektu jest on oznaczony jako obiekt generacji 2. Nie jest to 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 typowego żądania webowego potrzebnych jest kilka dużych buforów pamięci (odczyt z gniazda, dekompresja, dekodowanie JSON i nie tylko).
Przechowywanie dużej treści żądania lub odpowiedzi w pojedynczym byte[] lub string:
- Może spowodować szybkie wyczerpanie miejsca w LOH.
- Może powodować problemy z wydajnością aplikacji z powodu uruchamiania pełnych GC.
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.
Warning
Jeśli żądanie jest duże, może to prowadzić do braku pamięci (OOM). OOM może spowodować atak typu Denial of Service (DoS). Aby uzyskać więcej informacji, zobacz Unikanie odczytywania dużych treści żądań lub treści odpowiedzi w pamięci w tym artykule.
ASP.NET Core 3.0 domyślnie używa System.Text.Json do serializacji JSON. System.Text.Json:
- Odczytuje i zapisuje JSON asynchronicznie.
- Jest zoptymalizowany pod kątem tekstu UTF-8.
- Zazwyczaj ma wyższą wydajność niż
Newtonsoft.Json.
Nie przechowuj obiektu IHttpContextAccessor.HttpContext w polu
Obiekt IHttpContextAccessor.HttpContext zwraca kontekst HttpContext aktywnego żądania przy uzyskiwaniu dostępu z wątku żądania.
IHttpContextAccessor.HttpContext nie powinien być przechowywany w polu ani zmiennej.
Nie należy tego robić: poniższy przykład przechowuje element HttpContext 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 otrzymuje wartość null lub niepoprawną HttpContext w konstruktorze.
Zrób to: Poniższy przykład:
- Przechowuje IHttpContextAccessor w polu.
- Używa pola
HttpContextw odpowiednim czasie i sprawdza, czy występujenull.
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
HttpContext
nie jest bezpieczny wątkowo. Uzyskiwanie dostępu do HttpContext z wielu wątków równolegle może spowodować nieoczekiwane zachowanie, takie jak brak odpowiedzi serwera, awarie i uszkodzenie danych.
Nie należy tego robić: Poniższy przykład wykonuje trzy równoległe żądania i rejestruje ścieżkę żądania przychodzącego przed wysłaniem i po wysłaniu wychodzącego żądania HTTP. Ścieżka żądania jest dostępna 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 obsługuje każde żądanie. Gdy zakończy się Task zwrócone z tego łańcucha, HttpContext jest poddawane recyklingowi.
Nie należy tego robić: W poniższym przykładzie użyto metody async void, co powoduje ukończenie żądania HTTP po osiągnięciu pierwszego await:
- Użycie
async voidjest ZAWSZE złym rozwiązaniem w aplikacjach ASP.NET Core. - Przykładowy kod uzyskuje dostęp do
HttpResponsepo zakończeniu żądania HTTP. - Spóź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 frameworka, 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 HttpContext w wątkach w tle
Nie należy tego robić: W poniższym przykładzie pokazano, że zamknięcie przechwytuje element HttpContext z właściwości Controller. Jest to zła praktyka, ponieważ element roboczy może:
- Uruchom poza zakresem obsługi żądania.
- Próba odczytu niewłaściwego
HttpContextelementu.
[HttpGet("/fire-and-forget-1")]
public IActionResult BadFireAndForget()
{
_ = Task.Run(async () =>
{
await Task.Delay(1000);
var path = HttpContext.Request.Path;
Log(path);
});
return Accepted();
}
Zrób to: 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 domknięcie przechwytujące DbContext z parametru akcji Controller. Jest to zła praktyka. Element pracy może działać poza zakresem żądania. Element ContosoDbContext ma zakres żądania, co powoduje wystąpienie ObjectDisposedException.
[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();
}
Zrób to: Poniższy przykład:
- Wprowadza element IServiceScopeFactory w celu utworzenia kontekstu w elemencie roboczym w tle.
IServiceScopeFactoryjest singletonem. - Tworzy nowy zakres wstrzykiwania zależności w wątku w tle.
- Nie odwołuje się do niczego z kontrolera.
- Nie przechwytuje
ContosoDbContextelementu 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 na czas trwania operacji w tle i uzyskuje z niego usługi.
- Używa
ContosoDbContextz 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 przesyłania 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 element context.Response.Headers["test"] = "test value"; zgłosi wyjątek, jeśli składnik next() zapisał dane do odpowiedzi.
Wykonaj to: Poniższy przykład sprawdza, czy odpowiedź HTTP została uruchomiona przed zmodyfikowaniem 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 polecenia HttpResponse.OnStarting, aby ustawić nagłówki przed wysłaniem nagłówków odpowiedzi do klienta.
Sprawdzanie, czy odpowiedź nie została jeszcze rozpoczęta, umożliwia zarejestrowanie funkcji wywołania zwrotnego, która zostanie wywołana tuż przed zapisaniem nagłówków odpowiedzi. Sprawdzanie, czy odpowiedź jeszcze się nie rozpoczęła:
- Zapewnia możliwość dołączania lub zastępowania nagłówków tuż przed użyciem.
- Nie wymaga znajomości następnego oprogramowania pośredniczącego w potoku przetwarzania.
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 do treści odpowiedzi
Komponenty oczekują wywołania tylko wtedy, gdy będą mogły obsłużyć odpowiedź i manipulować nią.
Używanie hostingu w procesie wewnętrznym w 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ść niż hosting poza procesem, ponieważ żądania nie są przesyłane przez adapter pętli zwrotnej. Adapter pętli zwrotnej 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).
Domyślnym modelem hostowania dla projektów w ASP.NET Core 3.0 lub nowszym jest model wewnątrzprocesowy.
Aby uzyskać więcej informacji, zobacz Hostowanie ASP.NET Core w systemie Windows przy użyciu 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. Nieznajomość tego może prowadzić do luk w zabezpieczeniach w aplikacjach. Możesz myśleć, że chronisz się przed zbyt dużymi żądaniami, choć tak nie jest.
Aby uzyskać więcej informacji, zobacz tę odpowiedź StackOverflow.
Wzorce aplikacji internetowej dla przedsiębiorstw
Aby uzyskać wskazówki dotyczące tworzenia niezawodnej, bezpiecznej, wydajnej, testowalnej i skalowalnej aplikacji ASP.NET Core, zobacz wzorce aplikacji internetowych Enterprise. Dostępna jest kompletna przykładowa aplikacja internetowa jakości produkcyjnej, która implementuje wzorce.