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

8.6 KiB

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.

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