## 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 _logger; // Bezpieczne wstrzykiwanie w konstruktorze - fabryka rozwiązuje to per-request public PerformanceTrackingMiddleware(ILogger 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(); var app = builder.Build(); app.UseHttpsRedirection(); // Dodanie Middleware do rurociągu żądań serwera Kestrel app.UseMiddleware(); app.MapRazorComponents() .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.