ASP.NET Core: Middleware vs ActionFilters

Czas czytania ~ 150 sekund

Jedną z ważniejszych zmian wprowadzonych przez ASP.NET Core w stosunku do poprzednich wersji ASP.NET jest koncepcja Middleware. Autorzy nowego frameworka nie ukrywają inspiracji nodejs, w którym Express, a więc framework do budowy aplikacji webowych również posiada bardzo podobny mechanizm, również o nazwie Middleware.

Middleware w ASP.NET Core to nic innego jak możliwość wstawienia własnej logiki do całego cyklu przetwarzania żądania HTTP. To, czy nasz kod wykona się przed kontrolerem czy po, zależy od tego, kiedy wywołamy callback (inspiracja nodejs).

 

Własny middleware

Stwórzmy prosty komponent, którego zadaniem będzie zalogowanie wszystkich nagłówków HTTP Referer, które przyjdą na nasz serwer i będą różne od adresu URL hosta strony. Będzie to więc oznaczało, że nasze zasoby są linkowane z innych stron. Dodatkowo zablokujemy wszystkie żądania z Refererem mickl.net ponieważ nie podoba nam się ta strona (autor przynudza).

public class RefererHeaderAnalyser
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    const string BlacklistedHost = "mickl.net";

    public RefererHeaderAnalyser(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        StringValues referer;
        if (context.Request.Headers.TryGetValue("Referer", out referer))
        {
            var host = context.Request.Host;
            var refererValue = referer.First();
            if (!refererValue.Contains(host.ToString()))
            {
                _logger.LogInformation("We're linked from: ", refererValue);
                if (refererValue.Contains(BlacklistedHost))
                {
                    context.Response.StatusCode = 403;
                    return;
                }
            }                
        }

        await _next.Invoke(context);
    }
}

Mamy prostą klasę, która nie musi nawet po niczym dziedziczyć / niczego implementować. Ważne jest, by wystawiała publiczną, asynchroniczną metodą Invoke. Mamy także dostęp do wszystkich serwisów rejestrowanych w kontenerze IoC. W samej metodzie możemy wykorzystać HttpContext. Middleware umożliwia short circuiting, czyli możemy od razu zakończyć dalsze przetwarzanie żądania, nie wywołując callbacka next. Dodatkowo, pod linią kodu z awaitem możemy wykonywać dalsze instrukcje, które wywołają się po przetworzeniu żądania przez kontroler i wszystkie inne middleware zdefiniowane po obecnym.

Kolejność definiowania Middleware ma spore znaczenie, między innymi ze względu na wspomniany short circuiting. Definiujemy je poprzez dodanie extension methods na interfejsie IApplicationBuilder.

public static IApplicationBuilder UseRefererAnalyser(this IApplicationBuilder builder)
{
    return builder.UseMiddleware<RefererHeaderAnalyser>();
}

Samą metodę wywołujemy w klasie Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{          
    app.UseExceptionHandler("/Home/Error");
    app.UseRefererAnalyser();
    
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

W przypadku powyżej nasz middleware wykona się przed Mvc, a więc niektóre żądania mogą nigdy nie trafić do kontrolera. Ważne jest, by tym najbardziej zewnętrznym był Exception Handler, a także, by możliwie wcześnie wywoływały się komponenty związane z autentykacją czy CORS (np. dlatego, że nie ma sensu renderować Razorem HTML-a użytkownikom, którzy nie mają do nich dostępu).

ActionFilters

Innym mechanizmem, który umożliwia wykonywanie kodu przed i po wejściu do kontrolera są ActionFilters. Filtry wykonają się później niż Middleware i mamy w nich już dostęp do argumentów metod, a więc przed nimi wykona się również model binder ASP.NET Core.

Filtry są klasami implementującymi IActionFilter, które należy dodać do kontenera IoC. Deklarujemy je poprzez atrybut ServiceFilter umieszczany nad metodami kontrolera.

public class PayloadLoggerFilter : IActionFilter
{
    private ILogger<PayloadLoggerFilter> _logger;

    public PayloadLoggerFilter(ILogger<PayloadLoggerFilter> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        foreach(var value in context.ActionArguments.Values)
        {
            _logger.LogInformation("Payload value {@value}", value);
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        if(context.HttpContext.Response.StatusCode < 299)
        {
            _logger.LogInformation("Action executed successfully");
        }            
    }
}

Zastosowanie:

[ServiceFilter(typeof(PayloadLoggerFilter))]
public IActionResult MyMethod(MyMethodDto dto)
{
    return Ok();
}

Podsumowanie

Wydaje się, że middleware mają bardziej ogólny, globalny charakter niż filtry. Możemy je zdefiniować raz (dla wszystkich requestów, albo metodą Map tylko dla tych, które spełniają określony pattern URL). W filtrach mamy więcej danych i możemy je definiować na znacznie niższym poziomie ogólności (nad metodami kontrolera), a więc będą lepiej pasować do bardziej dedykowanych fragmentów kodu.

Edit: Po otrzymaniu recenzji od stałego czytelnika, dopisuję jeszcze, dla jasności, że filtry również pozwalają na short circuiting, co mogło nie wynikać wprost bezpośrednio z tekstu. Dziękuję Marcin.

Advertisements

2 thoughts on “ASP.NET Core: Middleware vs ActionFilters

    • Ja bym nawet poszedł dalej i nazwał middleware następcą dla modułów, W linku, który podałeś napisane jest nawet wprost “Modules, handlers, Global.asax.cs, Web.config (except for IIS configuration) and the application life cycle are gone”. Dlatego moją intencją było bardziej porównanie czegoś, w czym mogą wybierać programiści nowego ASP.NET, zwłaszcza, że pewną klasę problemów można rozwiązać przy użyciu obu mechanizmów.

      Dziękuję za komentarz i pozdrawiam.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s