Files
Desktop2.0/e-book/Rurociąg Middleware.md

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.