深入探究ASP.NET Core異常處理中介軟體
阿新 • • 發佈:2020-06-29
### 前言
全域性異常處理是我們程式設計過程中不可或缺的重要環節。有了全域性異常處理機制給我們帶來了很多便捷,首先我們不用滿螢幕處理程式可能出現的異常,其次我們可以對異常進行統一的處理,比如收集異常資訊或者返回統一的格式等等。ASP.NET Core為我們提供了兩種機制去處理全域性異常,一是基於中介軟體的方式,二是基於Filter過濾器的方式。Filter過濾器的方式相對來說比較簡單,就是捕獲Action執行過程中出現的異常,然後呼叫註冊的Filter去執行處理異常資訊,在這裡就不過多介紹這種方式了,接下來我們主要介紹中介軟體的方式。
### 異常處理中介軟體
ASP.NET Core為我們提供了幾種不同處理異常方式的中介軟體分別是UseDeveloperExceptionPage、UseExceptionHandler、UseStatusCodePages、UseStatusCodePagesWithRedirects、UseStatusCodePagesWithReExecute。這幾種方式處理的思路是一致的都是通過捕獲該管道後續的管道執行過程中出現的異常,只是處理的方式不一樣。一般推薦全域性異常處理相關中介軟體寫到所有管道的最開始,這樣可以捕獲到整個執行管道過程中的異常資訊。接下來我們介紹一下最常用的三個異常處理中介軟體UseDeveloperExceptionPage、UseExceptionHandler、UseStatusCodePage。
#### UseDeveloperExceptionPage
UseDeveloperExceptionPage的使用場景大部分是開發階段,通過名稱我們就可以看出,通過它捕獲的異常會返回一個異常介面,它的使用方式很簡單
```cs
//這個判斷不是必須的,但是在正式環境中給使用者展示程式碼錯誤資訊,終究不是合理的
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
```
如果程式出現異常,出現的效果是這個樣子的
![](https://img2020.cnblogs.com/blog/2042116/202006/2042116-20200628163052872-2123530534.png)
這裡包含了非常詳細的異常堆疊資訊、請求引數、Cookie資訊、Header資訊、和路由終結點相關的資訊。接下來我們找到[UseDeveloperExceptionPage所在的擴充套件類](https://github.com/dotnet/aspnetcore/blob/v3.1.5/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageExtensions.cs)
```cs
public static class DeveloperExceptionPageExtensions
{
public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app)
{
return app.UseMiddleware();
}
public static IApplicationBuilder UseDeveloperExceptionPage(
this IApplicationBuilder app,
DeveloperExceptionPageOptions options)
{
return app.UseMiddleware(Options.Create(options));
}
}
```
我們看到有兩個擴充套件方法一個是無參的,另一個是可以傳遞DeveloperExceptionPageOptions的擴充套件方法,因為平時使用無參的方法所以我們看下DeveloperExceptionPageOptions包含了哪些資訊,找到[DeveloperExceptionPageOptions原始碼](https://github.com/dotnet/aspnetcore/blob/v3.1.5/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageOptions.cs)
```cs
public class DeveloperExceptionPageOptions
{
public DeveloperExceptionPageOptions()
{
SourceCodeLineCount = 6;
}
///
/// 展示出現異常程式碼的地方上下展示多少行的程式碼資訊,預設是6行
///
public int SourceCodeLineCount { get; set; }
///
/// 通過這個檔案提供程式我們可以猜測到,我們可以自定義異常錯誤介面
///
public IFileProvider FileProvider { get; set; }
}
```
接下來我們就看核心的[DeveloperExceptionPageMiddleware中介軟體](https://github.com/dotnet/aspnetcore/blob/v3.1.5/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs)大致是如何工作的
```cs
public class DeveloperExceptionPageMiddleware
{
private readonly RequestDelegate _next;
private readonly DeveloperExceptionPageOptions _options;
private readonly ILogger _logger;
private readonly IFileProvider _fileProvider;
private readonly DiagnosticSource _diagnosticSource;
private readonly ExceptionDetailsProvider _exceptionDetailsProvider;
private readonly Func _exceptionHandler;
private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html");
public DeveloperExceptionPageMiddleware(
RequestDelegate next,
IOptions options,
ILoggerFactory loggerFactory,
IWebHostEnvironment hostingEnvironment,
DiagnosticSource diagnosticSource,
IEnumerable filters)
{
_next = next;
_options = options.Value;
_logger = loggerFactory.CreateLogger();
//預設使用ContentRootFileProvider
_fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider;
//可以傳送診斷日誌
_diagnosticSource = diagnosticSource;
_exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _options.SourceCodeLineCount);
_exceptionHandler = DisplayException;
//構建IDeveloperPageExceptionFilter執行管道,說明我們同時還可以通過程式的方式捕獲異常資訊
foreach (var filter in filters.Reverse())
{
var nextFilter = _exceptionHandler;
_exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter);
}
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.UnhandledException(ex);
if (context.Response.HasStarted)
{
_logger.ResponseStartedErrorPageMiddleware();
throw;
}
try
{
//清除輸出相關資訊,將狀態碼設為500
context.Response.Clear();
context.Response.StatusCode = 500;
//核心處理
await _exceptionHandler(new ErrorContext(context, ex));
//傳送名稱為Microsoft.AspNetCore.Diagnostics.UnhandledException診斷日誌,我們可以自定義訂閱者處理異常
if (_diagnosticSource.IsEnabled("Microsoft.AspNetCore.Diagnostics.UnhandledException"))
{
_diagnosticSource.Write("Microsoft.AspNetCore.Diagnostics.UnhandledException", new { httpContext = context, exception = ex });
}
return;
}
catch (Exception ex2)
{
_logger.DisplayErrorPageException(ex2);
}
throw;
}
}
}
```
通過上面程式碼我們可以瞭解到我們可以通過自定義IDeveloperPageExceptionFilter的方式攔截到異常資訊做處理
```cs
public class MyDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
{
private readonly ILogger _logger;
public MyDeveloperPageExceptionFilter(ILogger logger)
{
_logger = logger;
}
public async Task HandleExceptionAsync(ErrorContext errorContext, Func next)
{
_logger.LogInformation($"狀態碼:{errorContext.HttpContext.Response.StatusCode},異常資訊:{errorContext.Exception.Message}");
await next(errorContext);
}
}
```
自定義的MyDeveloperPageExceptionFilter是需要注入的
```cs
services.AddSingleton();
```
同時還可以通過診斷日誌的方式處理異常資訊,我使用了Microsoft.Extensions.DiagnosticAdapter擴充套件包,所以可以定義強型別類
```cs
public class DiagnosticCollector
{
private readonly ILogger _logger;
public DiagnosticCollector(ILogger logger)
{
_logger = logger;
}
[DiagnosticName("Microsoft.AspNetCore.Diagnostics.UnhandledException")]
public void OnRequestStart(HttpContext httpContext, Exception exception)
{
_logger.LogInformation($"診斷日誌收集到異常,狀態碼:{httpContext.Response.StatusCode},異常資訊:{exception.Message}");
}
}
```
通過這裡可以看出,異常處理擴充套件性還是非常強的,這僅僅是.Net Core設計方式的冰山一角。剛才我們提到_exceptionHandler才是處理的核心,通過建構函式可知這個委託是通過DisplayException方法初始化的,接下來我們看這裡的相關實現
```cs
private Task DisplayException(ErrorContext errorContext)
{
var httpContext = errorContext.HttpContext;
var headers = httpContext.Request.GetTypedHeaders();
var acceptHeader = headers.Accept;
//如果acceptHeader不存在或者型別不是text/plain,將以字串的形式輸出異常,比如通過程式碼或者Postman的方式呼叫並未設定頭資訊
if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType)))
{
httpContext.Response.ContentType = "text/plain";
var sb = new StringBuilder();
sb.AppendLine(errorContext.Exception.ToString());
sb.AppendLine();
sb.AppendLine("HEADERS");
sb.AppendLine("=======");
foreach (var pair in httpContext.Request.Headers)
{
sb.AppendLine($"{pair.Key}: {pair.Value}");
}
return httpContext.Response.WriteAsync(sb.ToString());
}
//判斷是否為編譯時異常,比如檢視檔案可以動態編譯
if (errorContext.Exception is ICompilationException compilationException)
{
return DisplayCompilationException(httpContext, compilationException);
}
//處理執行時異常
return DisplayRuntimeException(httpContext, errorContext.Exception);
}
```
關於DisplayCompilationException我們這裡就不做過多解釋了,在Asp.Net Core中cshtml檔案可以動態編譯,有興趣的同學可以自行了解。我們重點看下DisplayRuntimeException處理
```cs
private Task DisplayRuntimeException(HttpContext context, Exception ex)
{
//獲取終結點資訊
var endpoint = context.Features.Get()?.Endpoint;
EndpointModel endpointModel = null;
if (endpoint != null)
{
endpointModel = new EndpointModel();
endpointModel.DisplayName = endpoint.DisplayName;
if (endpoint is RouteEndpoint routeEndpoint)
{
endpointModel.RoutePattern = routeEndpoint.RoutePattern.RawText;
endpointModel.Order = routeEndpoint.Order;
var httpMethods = endpoint.Metadata.GetMetadata()?.HttpMethods;
if (httpMethods != null)
{
endpointModel.HttpMethods = string.Join(", ", httpMethods);
}
}
}
var request = context.Request;
//往檢視還是個輸出的模型,對於我們上面截圖展示的資訊對應的資料
var model = new ErrorPageModel
{
Options = _options,
//異常詳情
ErrorDetails = _exceptionDetailsProvider.GetDetails(ex),
//查詢引數相關
Query = request.Query,
//Cookie資訊
Cookies = request.Cookies,
//頭資訊
Headers = request.Headers,
//路由資訊
RouteValues = request.RouteValues,
//終結點資訊
Endpoint = endpointModel
};
var errorPage = new ErrorPage(model);
//執行輸出檢視頁面,也就是我們看到的開發者頁面
return errorPage.ExecuteAsync(context);
}
```
其整體實現思路就是捕獲後續執行過程中出現的異常,如果出現異常則包裝異常資訊以及Http上下文和路由相關資訊到ErrorPageModel模型中,然後這個模型作為異常展示介面的資料模型進行展示。
#### UseExceptionHandler
UseExceptionHandler可能是我們在實際開發過程中使用最多的方式。UseDeveloperExceptionPage固然強大,但是返回的終究還是供開發者使用的介面,通過UseExceptionHandler我們可以通過自己的方式處理異常資訊,這裡就需要我自己編碼
```cs
app.UseExceptionHandler(configure =>
{
configure.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get();
var ex = exceptionHandlerPathFeature?.Error;
if (ex != null)
{
context.Response.ContentType = "text/plain;charset=utf-8";
await context.Response.WriteAsync(ex.ToString());
}
});
});
//或
app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = async context =>
{
var exceptionHandlerPathFeature = context.Features.Get();
var ex = exceptionHandlerPathFeature?.Error;
if (ex != null)
{
context.Response.ContentType = "text/plain;charset=utf-8";
await context.Response.WriteAsync(ex.ToString());
}
}
});
//或
app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandlingPath = new PathString("/exception")
});
```
通過上面的實現方式我們大概可以猜出擴充套件方法的幾種型別找到原始碼位置[ExceptionHandlerExtensions擴充套件類](https://github.com/dotnet/aspnetcore/blob/v3.1.5/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerExtensions.cs)
```cs
public static class ExceptionHandlerExtensions
{
public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app)
{
return app.UseMiddleware();
}
public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath)
{
return app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandlingPath = new PathString(errorHandlingPath)
});
}
public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, Action configure)
{
//建立新的執行管道
var subAppBuilder = app.New();
configure(subAppBuilder);
var exceptionHandlerPipeline = subAppBuilder.Build();
return app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = exceptionHandlerPipeline
});
}
public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, ExceptionHandlerOptions options)
{
return app.UseMiddleware(Options.Create(options));
}
}
```
通過UseExceptionHandler擴充套件方法我們可以知道這麼多擴充套件方法其實本質都是在構建[ExceptionHandlerOptions](https://github.com/dotnet/aspnetcore/blob/v3.1.5/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs)我們來看一下大致實現
```cs
public class ExceptionHandlerOptions
{
///
/// 指定處理異常的終結點路徑
///
public PathString ExceptionHandlingPath { get; set; }
///
/// 指定處理異常的終結點委託
///
public RequestDelegate ExceptionHandler { get; set; }
}
```
這個類非常簡單,要麼指定處理異常資訊的具體終結點路徑,要麼自定義終結點委託處理異常資訊。通過上面的使用示例可以很清楚的看到這一點,接下來我們檢視一下[ExceptionHandlerMiddleware中介軟體](https://github.com/dotnet/aspnetcore/blob/v3.1.5/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs)的大致實現
```cs
public class ExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ExceptionHandlerOptions _options;
private readonly ILogger _logger;
private readonly Func