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
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.
Dla "givei" mamy liczbę wszystkich naszych obecnych zapytań HTTP. Są one generowane przez Middleware "My2Middleware".
Dla "api/game" powinniśmy dostać nasz wyjątek, który zostanie obsłużony przez nasz Middleware.
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.