深入探究ASP.NET Core異常處理中介軟體

yi念之間發表於2020-06-29

前言

    全域性異常處理是我們程式設計過程中不可或缺的重要環節。有了全域性異常處理機制給我們帶來了很多便捷,首先我們不用滿螢幕處理程式可能出現的異常,其次我們可以對異常進行統一的處理,比如收集異常資訊或者返回統一的格式等等。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程式。對於其它終結點型別有點無能為力。
以上就是文章的全部內容,由於能力有限,如果存在理解不周之處請多多諒解。我覺得學習一個東西,如果你能瞭解到它的工作方式或者實現原理,肯定會對你的程式設計思路有所提升,看過的程式碼用過的東西可能會忘,但是思路一旦形成,將會改變你以後的思維方式。

?歡迎掃碼關注我的公眾號? 深入探究ASP.NET Core異常處理中介軟體

相關文章