Middleware Na tym blogu były już artykuły na temat AutoMapper, Swagger UI, IServiceCollection. Co jeszcze jest potrzebne do aplikacji ASP.NET Core.

W sumie to jest to coś na czym złapałem się na swoich webinarach. Przechwycenie wyjątków w każdej metodzie Controler-ów wydaje się głupie. Napisanie kodu, w który nie wystąpią wyjątki jest niemożliwe.

Gdzie powinniśmy je przechwytywać tak, aby zapisać je później do logów.

Na pomoc przychodzi Middleware, który także rozwiązanie inny problem. W końcu wypadałoby przechwytywać wyjątek w jednym miejscu. Nie może być tak, że przechwytujesz wyjątek 5 razy i wyrzucasz go dalej.

Napiszmy więc Middleware, który złapie wyjątki. Swoją drogą Middleware to potężne narzędzie. Mógłbyś tak na przykład napisać alternatywne przechwytywanie zapytani HTTP.

Najprostszy Middleware wygląda tak :

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext httpContext)
    {

        return _next(httpContext);
    }
}

RequestDelefate reprezentuje następną metodę, która się wykona przy danej operacji HTTP. Jeśli nie wywołasz tej delegaty to proces przetwarzania HTTP w aplikacji ASP.NET Core na tym etapie się skończy.

Oto kod instalacji takiego Middleware :

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }
} 

Na koniec pozostaje Ci dodać ten Middleware do potoku wykonawczego aplikacji ASP.NET Core. Robi się to w metodzie Configure w klasie Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseMyMiddleware();

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

Ciekawostka w Middleware możesz także robić wstrzykiwanie zależności. Oto przykład

public class My2Middleware
{
    private readonly RequestDelegate _next;
    private int i = 0;

    public My2Middleware(RequestDelegate next)
    {

        _next = next;
    }

    // IMyScopedService is injected into Invoke
    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        if (i == int.MaxValue)
            i = 0;

        svc.MyProperty = i++;

        if (httpContext.Request.GetDisplayUrl().Contains("givei"))
            await httpContext.Response.WriteAsync(i.ToString());

        await _next(httpContext);
    }
}

public interface IMyScopedService
{
    int MyProperty { get; set; }
}

public class MyScopedService : IMyScopedService
{
    public int MyProperty { get; set; }
}

Dodatkowo w tym Middleware mówię, że jeśli w adresie URL będzie gdziekolwiek napis "givei" to wtedy ma on zwrócić zmienną "i". Zmienna ta definiuje ile zrobiliśmy obecnie zapytań HTTP na naszej stronie.

Rozszerzamy nasz instalator o ten nowy Middleware

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }

    public static IApplicationBuilder UseMy2Middleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<My2Middleware>();
    }
}

Na koniec dodajemy kolejny Middleware do przepływu,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();
    app.UseMyMiddleware();
    app.UseMy2Middleware();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Na koniec jeszcze dodamy definicję wstrzykiwania zależności.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IMyScopedService, MyScopedService>();
}

Ten przykład rozwinę bardziej, ale na końcu zobaczysz, że przepływ aplikacji wygląda tak

Działanie Middleware

Przejdźmy teraz do Middleware, który będzie łapała wyjątki. Możemy cały potok następnych operacji otoczyć blokiem Try-Catch.

Potem zostaje nam już tylko obsłużyć wyjątek w Catch.

public class RestApiExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RestApiExceptionHandlingMiddleware> _logger;

    public RestApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<RestApiExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");

        var problemDetails = new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "Internal Server Error",
            Status = (int)HttpStatusCode.InternalServerError,
            Instance = context.Request.Path,
            Detail = "Internal server error occured!"
        };

        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var result = JsonSerializer.Serialize(problemDetails);

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(result);
    }
}

Pozostaje nam jeszcze ten Middleware zainstalować. Rozszerzamy więc naszą instalkę.

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseExceptionHandlerMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RestApiExceptionHandlingMiddleware>();
    }

    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<MyMiddleware>();
    }

    public static IApplicationBuilder UseMy2Middleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<My2Middleware>();
    }


}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseRouting();
    app.UseMyMiddleware();
    app.UseMy2Middleware();
    app.UseExceptionHandlerMiddleware();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

Warto zwrócić uwagę na kolejność wykonywania naszego potoku. Przykładowo nasz Middleware do przechwytywanie wyjątków byłby bezużyteczny, gdybyś napisał go w taki sposób.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseExceptionHandlerMiddleware();
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

Twój wyjątek zostałby wtedy przetworzony przez Middleware, który wyświetla ze stronę z błędem.

Dodajmy do naszego przykładu inne endpoint. Tak też na siłę możesz utworzyć swoje REST API :)

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", async context =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
    endpoints.MapGet("/api/game/1", async context =>
    {
        await context.Response.WriteAsync("Mortal Kombat");
    });
    endpoints.MapGet("/api/game", async context =>
    {
        throw new SecurityErrorException("No all games for you");
    });
});

Jak widzisz dla adresu "/api/game/" wystąpi wyjątek. Czy możemy napisać specjalną logikę tylko dla naszego wyjątku szukając go po typie? Oczywiście, że tak

public class RestApiExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RestApiExceptionHandlingMiddleware> _logger;

    public RestApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<RestApiExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {

        if (ex is SecurityErrorException)
        {
            _logger.LogError("SecurityErrorException");
            await context.Response.WriteAsync(ex.Message);
        }
        else
        {
            _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");

            var problemDetails = new ProblemDetails
            {
                Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
                Title = "Internal Server Error",
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = context.Request.Path,
                Detail = "Internal server error occured!"
            };

            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            var result = JsonSerializer.Serialize(problemDetails);

            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(result);
        }
    }
}

Zobaczmy jak działa aplikacja w praktyce.

Dla "/api/game" mamy grę Mortal Kombat.

mamy grę Mortal Kombat.

Dla "givei" mamy liczbę wszystkich naszych obecnych zapytań HTTP. Są one generowane przez Middleware "My2Middleware".

mamy liczbę wszystkich naszych obecnych zapytań HTTP

Dla "api/game" powinniśmy dostać nasz wyjątek, który zostanie obsłużony przez nasz Middleware.

Obsługa mojego wyjątku

Dodajmy jeszcze endpoint, który wyrzucić jakiś wyjątek w wyniku nielegalnej operacji. Oto dzielenie przez 0.

endpoints.MapGet("/api", async context =>
{
    var i = 0;
    await context.Response.WriteAsync((1 / i).ToString());
});

Mój Middleware też ten wyjątek przechwyci.

Łapanie dzielenie przez zero