Wprowadzenie
Middleware to mechanizm, w którym możemy dodać własny kod „pośredni” do przetwarzania requestów HTTP. W Web API tworzony jest cały pipeline, czyli zestaw różnych Middleware wykorzystywanych, gdy request trafia do naszej aplikacji. Tworząc nowy projekt typu Web API, otrzymujemy wygenerowany kod, który zawiera już kilka Middleware.
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
Każda z powyższych metod to extension method, natomiast właściwa rejestracja odbywa się za pomocą app.UseMiddleware();
.
Pułapki
.NET cały czas ewoluuje – koncepty są rozwijane, a nowe usprawnienia regularnie wprowadzane. O ile nowy kod (w aktualnej wersji .NET 9) generuje rejestrację o powyższej strukturze, to w poprzednich wersjach można spotkać się również z poniższą formą rejestracji. Ważne jest, aby obie metody zostały wywołane!
app.UseRouting();
app.UseEndpoints(x =>
{
x.MapControllers();
});
W powyższym kodzie najważniejsza informacja jest taka, że każdy inny middleware, który chcemy zarejestrować, MUSI zostać dodany przed wywołaniem metody UseEndpoints()
. Za chwilę wyjaśnię, dlaczego.
Własny Middleware
Obecnie mamy trzy różne sposoby na utworzenie własnego middleware (oczywiście, żeby było łatwo 😆). Możemy to zrobić:
- Za pomocą lambdy
- Tworząc klasę przez konwencję
- Implementując interfejs
Dostajemy do dyspozycji dwa parametry: HttpContext
– ponieważ jest to pipeline przetwarzania zapytań HTTP – oraz RequestDelegate
, co oznacza, że mamy kontrolę nad tym, jak pipeline będzie działał.
Właśnie dlatego wcześniej wspomniałem, że kolejność wywoływania middleware ma znaczenie. Warto również wiedzieć, że UseEndpoints()
jest ostatnim Middleware w pipeline. Mając kontrolę nad RequestDelegate
, możemy:
- Napisać kod przed wywołaniem kolejnego middleware,
- Napisać kod po wywołaniu kolejnego middleware,
- Lub w ogóle nie wywołać kolejnego middleware! – I to właśnie dzieje się w przypadku
UseEndpoints()
.
// Wersja z lambdą
app.Use(async (context, next) =>
{
Console.WriteLine("Middleware z lambdy - przed next");
await next(context);
Console.WriteLine("Middleware z lambdy - po next");
});
Wariant przez konwencję – bardzo ważne jest, że parametr HttpContext
w metodzie InvokeAsync
musi znajdować się na pierwszym miejscu! Dodatkowo, należy pamiętać o zarejestrowaniu naszego middleware w kontenerze za pomocą builder.Services
.
// Wersja przez konwencję
builder.Services.AddTransient<TestMiddleware>();
public class TestMiddleware(RequestDelegate next)
{
public Task InvokeAsync(HttpContext context)
{
Console.WriteLine("Middleware przez konwencję - przed next");
return next(context);
Console.WriteLine("Middleware z lambdy - po next");
}
}
app.UseMiddleware<TestMiddleware>();
Wariant przez interfejs – według mnie najlepszy, najmniej podatny na błędy 😆.
public class TestMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
Console.WriteLine("Middleware z interfejsu - przed next");
await next(context);
Console.WriteLine("Middleware z interfejsu - po next");
}
}
app.UseMiddleware<TestMiddleware>();
Dependency injection
Każda z metod ma wsparcie dla Dependency Injection, każda na swój sposób 😆. W przypadku lambdy, na obiekcie HttpContext
mamy dostęp do RequestServices
, które zwracają IServiceProvider
.
app.Use(async (context, next) =>
{
var db = context.RequestServices.GetRequiredService<MyDatabaseContext>();
//ciąg dalszy kodu
});
W przypadku tworzenia middleware przez konwencję, sprawa się komplikuje 😆. Możemy użyć Method Injection, podając kolejne parametry jako zależności. Mając konstruktor w naszej klasie, możemy również użyć konstruktora – natomiast jeśli próbuję to zrobić, dostaję błąd: Cannot resolve scoped service 'MyDatabaseContext’ from root provider – nawet zmieniając ServiceLifetime
mojej klasy!
public class TestMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context, MyDatabaseContext db)
{
}
}
Dla middleware z interfejsu wszystko działa zgodnie ze sztuką – przekazujemy wszystkie zależności do konstruktora naszej klasy i używamy ich w metodzie InvokeAsync
.
public class TestMiddleware(MyDatabaseContext db) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
}
}