深入探究ASP.NET Core異常處理中間件

前言

    全局異常處理是我們編程過程中不可或缺的重要環節。有了全局異常處理機制給我們帶來了很多便捷,首先我們不用滿屏幕處理程序可能出現的異常,其次我們可以對異常進行統一的處理,比如收集異常信息或者返回統一的格式等等。ASP.NET Core為我們提供了兩種機制去處理全局異常,一是基於中間件的方式,二是基於Filter過濾器的方式。Filter過濾器的方式相對來說比較簡單,就是捕獲Action執行過程中出現的異常,然後調用註冊的Filter去執行處理異常信息,在這裡就不過多介紹這種方式了,接下來我們主要介紹中間件的方式。

異常處理中間件

    ASP.NET Core為我們提供了幾種不同處理異常方式的中間件分別是UseDeveloperExceptionPage、UseExceptionHandler、UseStatusCodePages、UseStatusCodePagesWithRedirects、UseStatusCodePagesWithReExecute。這幾種方式處理的思路是一致的都是通過捕獲該管道後續的管道執行過程中出現的異常,只是處理的方式不一樣。一般推薦全局異常處理相關中間件寫到所有管道的最開始,這樣可以捕獲到整個執行管道過程中的異常信息。接下來我們介紹一下最常用的三個異常處理中間件UseDeveloperExceptionPage、UseExceptionHandler、UseStatusCodePage。

UseDeveloperExceptionPage

UseDeveloperExceptionPage的使用場景大部分是開發階段,通過名稱我們就可以看出,通過它捕獲的異常會返回一個異常界面,它的使用方式很簡單

//這個判斷不是必須的,但是在正式環境中給用戶展示代碼錯誤信息,終究不是合理的
if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

如果程序出現異常,出現的效果是這個樣子的

這裡包含了非常詳細的異常堆棧信息、請求參數、Cookie信息、Header信息、和路由終結點相關的信息。接下來我們找到UseDeveloperExceptionPage所在的擴展類

public static class DeveloperExceptionPageExtensions
{
    public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app)
    {
        return app.UseMiddleware<DeveloperExceptionPageMiddleware>();
    }

    public static IApplicationBuilder UseDeveloperExceptionPage(
        this IApplicationBuilder app,
        DeveloperExceptionPageOptions options)
    {
        return app.UseMiddleware<DeveloperExceptionPageMiddleware>(Options.Create(options));
    }
}

我們看到有兩個擴展方法一個是無參的,另一個是可以傳遞DeveloperExceptionPageOptions的擴展方法,因為平時使用無參的方法所以我們看下DeveloperExceptionPageOptions包含了哪些信息,找到DeveloperExceptionPageOptions源碼

public class DeveloperExceptionPageOptions
{
    public DeveloperExceptionPageOptions()
    {
        SourceCodeLineCount = 6;
    }
    /// <summary>
    /// 展示出現異常代碼的地方上下展示多少行的代碼信息,默認是6行
    /// </summary>
    public int SourceCodeLineCount { get; set; }

    /// <summary>
    /// 通過這個文件提供程序我們可以猜測到,我們可以自定義異常錯誤界面
    /// </summary>
    public IFileProvider FileProvider { get; set; }
}

接下來我們就看核心的DeveloperExceptionPageMiddleware中間件大致是如何工作的

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<ErrorContext, Task> _exceptionHandler;
    private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html");

    public DeveloperExceptionPageMiddleware(
        RequestDelegate next,
        IOptions<DeveloperExceptionPageOptions> options,
        ILoggerFactory loggerFactory,
        IWebHostEnvironment hostingEnvironment,
        DiagnosticSource diagnosticSource,
        IEnumerable<IDeveloperPageExceptionFilter> filters)
    {
        _next = next;
        _options = options.Value;
        _logger = loggerFactory.CreateLogger<DeveloperExceptionPageMiddleware>();
        //默認使用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的方式攔截到異常信息做處理

public class MyDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter
{
    private readonly ILogger<MyDeveloperPageExceptionFilter> _logger;
    public MyDeveloperPageExceptionFilter(ILogger<MyDeveloperPageExceptionFilter> logger)
    {
        _logger = logger;
    }

    public async Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
    {
        _logger.LogInformation($"狀態碼:{errorContext.HttpContext.Response.StatusCode},異常信息:{errorContext.Exception.Message}");
        await next(errorContext);
    }
}

自定義的MyDeveloperPageExceptionFilter是需要注入的

services.AddSingleton<IDeveloperPageExceptionFilter,MyDeveloperPageExceptionFilter>();

同時還可以通過診斷日誌的方式處理異常信息,我使用了Microsoft.Extensions.DiagnosticAdapter擴展包,所以可以定義強類型類

public class DiagnosticCollector
{
    private readonly ILogger<DiagnosticCollector> _logger;
    public DiagnosticCollector(ILogger<DiagnosticCollector> 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方法初始化的,接下來我們看這裡的相關實現

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處理

private Task DisplayRuntimeException(HttpContext context, Exception ex)
{
    //獲取終結點信息
    var endpoint = context.Features.Get<IEndpointFeature>()?.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<IHttpMethodMetadata>()?.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我們可以通過自己的方式處理異常信息,這裡就需要我自己編碼

app.UseExceptionHandler(configure =>
{
    configure.Run(async context =>
    {
        var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
        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<IExceptionHandlerPathFeature>();
        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擴展類

public static class ExceptionHandlerExtensions
{
    public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ExceptionHandlerMiddleware>();
    }

    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<IApplicationBuilder> 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<ExceptionHandlerMiddleware>(Options.Create(options));
    }
}

通過UseExceptionHandler擴展方法我們可以知道這麼多擴展方法其實本質都是在構建ExceptionHandlerOptions我們來看一下大致實現

public class ExceptionHandlerOptions
{
    /// <summary>
    /// 指定處理異常的終結點路徑
    /// </summary>
    public PathString ExceptionHandlingPath { get; set; }
    
    /// <summary>
    /// 指定處理異常的終結點委託
    /// </summary>
    public RequestDelegate ExceptionHandler { get; set; }
}

這個類非常簡單,要麼指定處理異常信息的具體終結點路徑,要麼自定義終結點委託處理異常信息。通過上面的使用示例可以很清楚的看到這一點,接下來我們查看一下ExceptionHandlerMiddleware中間件的大致實現

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ExceptionHandlerOptions _options;
    private readonly ILogger _logger;
    private readonly Func<object, Task> _clearCacheHeadersDelegate;
    private readonly DiagnosticListener _diagnosticListener;

    public ExceptionHandlerMiddleware(
        RequestDelegate next,
        ILoggerFactory loggerFactory,
        IOptions<ExceptionHandlerOptions> options,
        DiagnosticListener diagnosticListener)
    {
        _next = next;
        _options = options.Value;
        _logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
        _clearCacheHeadersDelegate = ClearCacheHeaders;
        _diagnosticListener = diagnosticListener;
        //ExceptionHandler和ExceptionHandlingPath不同同時不存在
        if (_options.ExceptionHandler == null)
        {
            if (_options.ExceptionHandlingPath == null)
            {
                throw new InvalidOperationException(Resources.ExceptionHandlerOptions_NotConfiguredCorrectly);
            }
            else
            {
                _options.ExceptionHandler = _next;
            }
        }
    }

    public Task Invoke(HttpContext context)
    {
        ExceptionDispatchInfo edi;
        try
        {
            var task = _next(context);
            //task未完成情況
            if (!task.IsCompletedSuccessfully)
            {
                return Awaited(this, context, task);
            }
            return Task.CompletedTask;
        }
        catch (Exception exception)
        {
            edi = ExceptionDispatchInfo.Capture(exception);
        }
        return HandleException(context, edi);
        
        //處理未完成task
        static async Task Awaited(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)
        {
            ExceptionDispatchInfo edi = null;
            try
            {
                //等待完成
                await task;
            }
            catch (Exception exception)
            {
                //收集異常信息
                edi = ExceptionDispatchInfo.Capture(exception);
            }
            if (edi != null)
            {
                await middleware.HandleException(context, edi);
            }
        }
    }
}

通過這段處理我們可以看出所有的異常處理都指向當前類的HandleException方法

private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi)
{
    _logger.UnhandledException(edi.SourceException);
    // 如果輸出已經開始執行了,後續的代碼就沒必要執行了,直接重新拋出
    if (context.Response.HasStarted)
    {
        _logger.ResponseStartedErrorHandler();
        edi.Throw();
    }

    PathString originalPath = context.Request.Path;
    //如果指定處理異常的終結點,將異常處理交給指定的終結點去處理
    if (_options.ExceptionHandlingPath.HasValue)
    {
        //將處理路徑指向,異常處理終結點路徑
        context.Request.Path = _options.ExceptionHandlingPath;
    }
    try
    {
        //清除原有HTTP上下文信息,為了明確指定程序出現異常,防止異常未被處理而後續當做正常操作執行
        ClearHttpContext(context);
        //將異常信息包裝成ExceptionHandlerFeature,後續處理程序獲取異常信息都是通過ExceptionHandlerFeature
        var exceptionHandlerFeature = new ExceptionHandlerFeature()
        {
            //異常信息
            Error = edi.SourceException,
            //原始路徑
            Path = originalPath.Value,
        };
        //將包裝的ExceptionHandlerFeature放入到上下文中,後續處理程序可通過HttpContext獲取異常信息
        context.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
        context.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);
        //設置狀態碼
        context.Response.StatusCode = 500;
        context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
        //調用給定的異常處理終結點處理異常信息
        await _options.ExceptionHandler(context);
        //同樣也可以發送診斷日誌,可以利用處理程序返回輸出,診斷日誌記錄異常將職責分離
        if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled("Microsoft.AspNetCore.Diagnostics.HandledException"))
        {
            _diagnosticListener.Write("Microsoft.AspNetCore.Diagnostics.HandledException", new { httpContext = context, exception = edi.SourceException });
        }
        return;
    }
    catch (Exception ex2)
    {
        _logger.ErrorHandlerException(ex2);
    }
    finally
    {
        //異常處理結束後,恢復原始的請求路徑,供後續執行程序能拿到原始的請求信息
        context.Request.Path = originalPath;
    }
    //如果異常沒被處理則重新拋出
    edi.Throw();
}

最後還有一段清除上下文和清除輸出緩存的方法,因為程序一旦發生了異常,可能創建了新的終結點,所以執行管道會有所調整,所以需要重新計算​。​而且異常信息保留輸出緩存是沒有意義的。​

private static void ClearHttpContext(HttpContext context)
{
    context.Response.Clear();
    //因為可能創建了新的終結點,所以執行管道會有所調整,所以需要重新計算
    context.SetEndpoint(endpoint: null);
    var routeValuesFeature = context.Features.Get<IRouteValuesFeature>();
    routeValuesFeature?.RouteValues?.Clear();
}

private static Task ClearCacheHeaders(object state)
{
    //清除輸出緩存相關
    var headers = ((HttpResponse)state).Headers;
    headers[HeaderNames.CacheControl] = "no-cache";
    headers[HeaderNames.Pragma] = "no-cache";
    headers[HeaderNames.Expires] = "-1";
    headers.Remove(HeaderNames.ETag);
    return Task.CompletedTask;
}

從上面的代碼我們可以看出UseExceptionHandler要比UseDeveloperExceptionPage實現方式簡單很多。其大致思路就是捕獲後續管道執行異常,如果存在異常則將異常包裝成ExceptionHandlerFeature,放入到Http上下文中。之所以相對簡單主要原因還是UseExceptionHandler最終處理異常由我們自定義的終結點去處理,所以它只是負責包裝異常相關信息,並將它交於我們定義的異常處理終結點。

UseStatusCodePages

無論是UseDeveloperExceptionPage還是UseExceptionHandler都是通過捕獲異常的方式去處理異常信息,UseStatusCodePages則是通過Http狀態碼去判斷是否為成功的返回並進行處理,使用方式如下

app.UseStatusCodePages();
//或
app.UseStatusCodePages("text/plain;charset=utf-8", "狀態碼:{0}");
//或
app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain;charset=utf-8";
    await context.HttpContext.Response.WriteAsync($"狀態碼:{context.HttpContext.Response.StatusCode}");
});
//或
app.UseStatusCodePages(new StatusCodePagesOptions { HandleAsync = async context=> {
    context.HttpContext.Response.ContentType = "text/plain;charset=utf-8";
    await context.HttpContext.Response.WriteAsync($"狀態碼:{context.HttpContext.Response.StatusCode}");
}});
//或
app.UseStatusCodePages(configure =>
{
    configure.Run(async context =>
    {
        await context.Response.WriteAsync($"狀態碼:{context.Response.StatusCode}");
    });
});

接下來我們查看一下UseStatusCodePages擴展方法相關實現

public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options)
{
    return app.UseMiddleware<StatusCodePagesMiddleware>(Options.Create(options));
}

public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app)
{
    return app.UseMiddleware<StatusCodePagesMiddleware>();
}

public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler)
{
    return app.UseStatusCodePages(new StatusCodePagesOptions
    {
        HandleAsync = handler
    });
}

public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat)
{
    return app.UseStatusCodePages(context =>
    {
        var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode);
        context.HttpContext.Response.ContentType = contentType;
        return context.HttpContext.Response.WriteAsync(body);
    });
}

雖然擴展方法比較多,但是本質都是組裝StatusCodePagesOptions,所以我們直接查看源碼

public class StatusCodePagesOptions
{
    public StatusCodePagesOptions()
    {
        //初始化
        HandleAsync = context =>
        {
            var statusCode = context.HttpContext.Response.StatusCode;
            var body = BuildResponseBody(statusCode);
            context.HttpContext.Response.ContentType = "text/plain";
            return context.HttpContext.Response.WriteAsync(body);
        };
    }

    private string BuildResponseBody(int httpStatusCode)
    {
        //組裝默認消息模板
        var internetExplorerWorkaround = new string(' ', 500);
        var reasonPhrase = ReasonPhrases.GetReasonPhrase(httpStatusCode);
        return string.Format(CultureInfo.InvariantCulture, "Status Code: {0}{1}{2}{3}",
                                                                httpStatusCode,
                                                                string.IsNullOrWhiteSpace(reasonPhrase) ? "" : "; ",
                                                                reasonPhrase,
                                                                internetExplorerWorkaround);
    }
    public Func<StatusCodeContext, Task> HandleAsync { get; set; }
}

看着代碼不少,其實都是嚇唬人的,就是給HandleAsync一個默認值,這個默認值里有默認的輸出模板。接下來我們查看一下StatusCodePagesMiddleware中間件源碼

public class StatusCodePagesMiddleware
{
    private readonly RequestDelegate _next;
    private readonly StatusCodePagesOptions _options;

    public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options)
    {
        _next = next;
        _options = options.Value;
    }

    public async Task Invoke(HttpContext context)
    {
        //初始化StatusCodePagesFeature
        var statusCodeFeature = new StatusCodePagesFeature();
        context.Features.Set<IStatusCodePagesFeature>(statusCodeFeature);

        await _next(context);

        if (!statusCodeFeature.Enabled)
        {
            return;
        }
        //這個範圍外的Http狀態碼直接忽略,不受程序處理只處理值為400-600之間的狀態碼
        if (context.Response.HasStarted
            || context.Response.StatusCode < 400
            || context.Response.StatusCode >= 600
            || context.Response.ContentLength.HasValue
            || !string.IsNullOrEmpty(context.Response.ContentType))
        {
            return;
        }
        //將狀態信息包裝到StatusCodeContext,傳遞給自定義處理終結點
        var statusCodeContext = new StatusCodeContext(context, _options, _next);
        await _options.HandleAsync(statusCodeContext);
    }
}

這個中間件的實現思路更為簡單,主要就是攔截請求判斷Http狀態碼,判斷是否是400-600,也就是4xx 5xx相關的狀態碼,如果符合則包裝成StatusCodeContext,交由自定義的終結點去處理。

總結

關於常用異常處理中間件我們介紹到這裡就差不多了,接下來我們總結對比一下三種中間件的異同和大致實現的方式

  • UseDeveloperExceptionPage中間件主要工作方式就是捕獲後續中間件執行異常,如果存在異常則將異常信息包裝成ErrorPageModel視圖模型,然後通過這個模型去渲染開發者異常界面。
  • UseExceptionHandler中間件核心思路和UseDeveloperExceptionPage類似都是通過捕獲後續中間件執行異常,不同之處在於UseExceptionHandler將捕獲的異常信息包裝到ExceptionHandlerFeature然後將其放入Http上下文中,後續的異常處理終結點通過Http上下文獲取到異常信息進行處理。
  • UseStatusCodePages中間件相對於前兩種中間件最為簡單,其核心思路就是獲取執行完成後的Http狀態碼判斷是否是4xx 5xx相關,如果是則執行自定義的狀態碼攔截終結點。這個中間件核心是圍繞StatusCode其實並不包含處理異常相關的邏輯,所以整體實現相對簡單。

最後我們再來總結下使用中間件的方式和使用IExceptionFilter的方式的區別

  • 中間件的方式是對整個請求執行管道進行異常捕獲,主要是負責整個請求過程中的異常捕獲,其生命周期更靠前,捕獲異常的範圍更廣泛。畢竟MVC只是Asp.Net Core終結點的一種實現方式,目前Asp.Net Core還可以處理GRPC Signalr等其它類型的終結點信息。
  • IExceptionFilter主要是針對Action執行過程中的異常,畢竟終結點只是中間件的一種形式,所以可處理範圍比較有限,主要適用於MVC程序。對於其它終結點類型有點無能為力。

以上就是文章的全部內容,由於能力有限,如果存在理解不周之處請多多諒解。我覺得學習一個東西,如果你能了解到它的工作方式或者實現原理,肯定會對你的編程思路有所提升,看過的代碼用過的東西可能會忘,但是思路一旦形成,將會改變你以後的思維方式。

👇歡迎掃碼關注我的公眾號👇