122 lines
8.6 KiB
Markdown
122 lines
8.6 KiB
Markdown
## Wstęp merytoryczny: Zaawansowana mechanika rurociągu Middleware
|
|
|
|
W procesie transformacji aplikacji z **.NET Framework 4.8** do nowoczesnego ekosystemu **ASP.NET Core**, dogłębne zrozumienie rurociągu oprogramowania pośredniczącego (Middleware) jest kluczowe. Architektura ta, choć z pozoru prosta, kryje w sobie zaawansowane mechanizmy zarządzania cyklem życia wstrzykiwanych zależności (DI) oraz asynchronicznego przetwarzania strumieni odpowiedzi HTTP. Wymaga to precyzji, zwłaszcza w kontekście współdzielenia stanu żądania i obsługi momentu, w którym odpowiedź zostaje wysłana do klienta.
|
|
|
|
W niniejszym rozdziale przeanalizujemy precyzyjne zasady instancjonowania Middleware oraz bezpieczne techniki pracy z obiektem `HttpResponse`, eliminując ryzyko błędów architektonicznych i wyjątków w czasie wykonywania.
|
|
|
|
## Analiza Porównawcza: Metody implementacji Middleware
|
|
|
|
| **Koncepcja Middleware** | **Cykl życia instancji** | **Wstrzykiwanie usług o cyklu Scoped / Transient** |
|
|
| ------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
| **Podejście konwencyjne (Legacy/Standard)** | **Singleton** (tworzony raz podczas budowy rurociągu). | Wstrzykiwane wyłącznie jako parametry metody `Invoke` / `InvokeAsync`. |
|
|
| **Zależności w konstruktorze (Konwencja)** | **Singleton** | Próba wstrzyknięcia usługi Scoped do konstruktora prowadzi do antywzorca *Captive Dependency*. |
|
|
| **Interfejs `IMiddleware`** | **Scoped** (tworzony przez fabrykę per żądanie HTTP). | Bezpieczne wstrzykiwanie bezpośrednio przez konstruktor. |
|
|
|
|
## Głębokie Nurkowanie (Deep Dive): Cykl życia i modyfikacja odpowiedzi
|
|
|
|
### 1. Cykl życia usług w Middleware konwencyjnym
|
|
W architekturze ASP.NET Core najczęściej spotykanym podejściem (znanym ze starszych wersji frameworka) jest Middleware oparty na konwencji. Taka klasa jest instancjonowana tylko raz, w momencie wywołania `app.Build()` i konfiguracji serwera Kestrel. Z tego powodu jej konstruktor zachowuje się jak **Singleton**.
|
|
|
|
Jeżeli spróbujemy wstrzyknąć do tego konstruktora usługę o cyklu życia *Scoped* (np. kontekst Entity Framework Core), usługa ta zostanie "uwięziona" (Captive Dependency) na cały czas życia aplikacji, co nieuchronnie doprowadzi do wycieków pamięci i wyjątków współbieżności. Aby temu zapobiec, usługi *Scoped* muszą być wstrzykiwane jako parametry sygnatury metody asynchronicznej `InvokeAsync`. Metoda ta jest wywoływana dla każdego żądania HTTP, co pozwala kontenerowi DI na dostarczenie zależności o właściwym, krótkim cyklu życia.
|
|
|
|
### 2. Stan odpowiedzi HTTP i blokada modyfikacji
|
|
Każde wywołanie delegata `await next(context)` przekazuje sterowanie do kolejnego elementu rurociągu, który może wygenerować odpowiedź (np. wyrenderować komponent Blazor Server lub zwrócić plik). Gdy sterowanie wraca do naszego Middleware (po słowie kluczowym `await`), odpowiedź mogła już zostać rozpoczęta (tzw. *Response has started*).
|
|
|
|
W środowisku webowym (w przeciwieństwie do synchronicznego zwracania widoków w .NET 4.8), serwer Kestrel może przesyłać odpowiedź w modelu strumieniowym (Streaming) do klienta jeszcze przed zakończeniem przetwarzania całego rurociągu. W takim scenariuszu:
|
|
* Odczyt właściwości takich jak `context.Response.ContentType` może zwrócić nieprecyzyjne dane lub zostać zignorowany, jeśli nagłówki zostały już wysłane do przeglądarki.
|
|
* Próba **modyfikacji** nagłówków HTTP, statusu lub struktury odpowiedzi po jej rozpoczęciu bezwzględnie wyrzuci wyjątek systemowy `InvalidOperationException: Headers are read-only, response has already started`.
|
|
|
|
## Dobre Praktyki i Antywzorce
|
|
|
|
* **Antywzorzec:** Sprawdzanie i modyfikowanie stanu `HttpResponse` po wywołaniu `await next()` bez weryfikacji flagi gotowości.
|
|
* *Rozwiązanie:* Jeżeli Middleware musi dodać nagłówki lub zmodyfikować typ odpowiedzi, powinien to zrobić przed wywołaniem `next()`. Jeśli modyfikacja zależy od wyniku dalszego przetwarzania, należy użyć zdarzenia `context.Response.OnStarting`, które framework wywoła tuż przed fizycznym wysłaniem nagłówków do klienta.
|
|
* **Antywzorzec (Architektura 4.8):** W aplikacjach `System.Web` globalny stan często przechowywano w klasach statycznych. Przenoszenie tego nawyku do konstruktora konwencyjnego Middleware'u w .NET 10 jest destrukcyjne.
|
|
* *Dobra Praktyka:* Stosowanie interfejsu `IMiddleware`. Narzuca on silne typowanie i sprawia, że cała instancja Middleware jest powoływana w cyklu *Scoped* (per żądanie HTTP), umożliwiając bezpieczne korzystanie ze wstrzykiwania przez konstruktor.
|
|
|
|
## Laboratorium kodu: Bezpieczny PerformanceTrackingMiddleware
|
|
|
|
Poniższy kod przedstawia ewolucję logiki przechwytującej (znanej z dawnego `HttpModule`), zaimplementowaną z użyciem nowoczesnego interfejsu `IMiddleware`. Zapewnia on pełne bezpieczeństwo cyklu życia dla wstrzykiwanego logera (Scoped/Transient) oraz chroni przed wyjątkami modyfikacji wysłanej odpowiedzi.
|
|
|
|
```csharp
|
|
using System.Diagnostics;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ModernApp.Middleware;
|
|
|
|
// Użycie IMiddleware gwarantuje cykl życia "Scoped" (instancja na każde żądanie HTTP)
|
|
public class PerformanceTrackingMiddleware : IMiddleware
|
|
{
|
|
private readonly ILogger<PerformanceTrackingMiddleware> _logger;
|
|
|
|
// Bezpieczne wstrzykiwanie w konstruktorze - fabryka rozwiązuje to per-request
|
|
public PerformanceTrackingMiddleware(ILogger<PerformanceTrackingMiddleware> logger)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
// Bezpieczna rejestracja logiki przed wysłaniem nagłówków.
|
|
// OnStarting gwarantuje, że kod wykona się DOKŁADNIE w momencie formowania odpowiedzi,
|
|
// zabezpieczając nas przed wyjątkiem "Headers are read-only".
|
|
context.Response.OnStarting(() =>
|
|
{
|
|
var contentType = context.Response.ContentType ?? string.Empty;
|
|
|
|
// Przykład warunkowego dodania nagłówka zabezpieczającego
|
|
if (contentType.Contains("text/html", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
context.Response.Headers.Append("X-Performance-Tracking", "Enabled");
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
// Przekazanie sterowania dalej do rurociągu (np. Endpointów API lub Blazora)
|
|
await next(context);
|
|
|
|
stopwatch.Stop();
|
|
|
|
// Po powrocie z 'next' możemy bezpiecznie czytać parametry READ-ONLY do logów,
|
|
// jednak nie wolno nam modyfikować już obiektu Response,
|
|
// a same własności mogły ulec strumieniowaniu.
|
|
if (!context.Response.HasStarted)
|
|
{
|
|
// Fallback dla specyficznych scenariuszy, w których odpowiedź jest wstrzymana
|
|
}
|
|
|
|
_logger.LogInformation(
|
|
"Przetwarzanie ścieżki {Path} zakończono w {Elapsed} ms. Kod statusu: {StatusCode}",
|
|
context.Request.Path,
|
|
stopwatch.ElapsedMilliseconds,
|
|
context.Response.StatusCode);
|
|
}
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
// Program.cs - Rejestracja
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Middleware oparty o interfejs IMiddleware MUSI być zarejestrowany w kontenerze DI
|
|
builder.Services.AddScoped<PerformanceTrackingMiddleware>();
|
|
|
|
var app = builder.Build();
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
// Dodanie Middleware do rurociągu żądań serwera Kestrel
|
|
app.UseMiddleware<PerformanceTrackingMiddleware>();
|
|
|
|
app.MapRazorComponents<App>()
|
|
.AddInteractiveServerRenderMode();
|
|
|
|
app.Run();
|
|
```
|
|
|
|
## Wnioski architektoniczne
|
|
|
|
Porzucenie modelu stanu powiązanego z cyklem życia okna (Desktop) lub scentralizowanych modułów IIS na rzecz rurociągu Middleware w .NET 10 wymaga precyzyjnego zarządzania zależnościami. Implementacja logiki współdzielonej poprzez interfejs `IMiddleware` chroni inżynierów przed powstawaniem trudnych do wykrycia błędów związanych z *Captive Dependencies* w konstruktorach. Ponadto, asynchroniczna natura serwera Kestrel wymusza traktowanie obiektu odpowiedzi HTTP jako strumienia, do którego metadane (takie jak typ zawartości) należy dodawać wyłącznie z użyciem zdarzenia `OnStarting`, co trwale zapobiega naruszaniu specyfikacji protokołu HTTP i awariom potoku wykonawczego. |