《呈現(xiàn)錯誤信息》通過幾個簡單的實例演示了如何呈現(xiàn)一個錯誤頁面,該過程由3個對應的中間件來完成。下面先介紹用來呈現(xiàn)開發(fā)者異常頁面的DeveloperExceptionPageMiddleware中間件,該中間件在捕捉到后續(xù)處理過程中拋出的異常之后會返回一個媒體類型為text/html的響應,后者在瀏覽器上會呈現(xiàn)一個錯誤頁面。由于這是一個為開發(fā)者提供診斷信息的異常頁面,所以可以將其稱為開發(fā)者異常頁面(Developer Exception Page)。該頁面不僅會呈現(xiàn)異常的詳細信息(類型、消息和跟蹤堆棧等),還會出現(xiàn)與當前請求相關的上下文信息。如下所示的代碼片段是DeveloperExceptionPageMiddleware中間件的定義。更多關于ASP.NET Core的文章請點這里]
public class DeveloperExceptionPageMiddleware
{
? ? public DeveloperExceptionPageMiddleware(RequestDelegate next,
? ? ? ? IOptions<DeveloperExceptionPageOptions> options,
? ? ? ? ILoggerFactory loggerFactory, IWebHostEnvironment? hostingEnvironment,
? ? ? ? DiagnosticSource diagnosticSource,
? ? ? ? IEnumerable<IDeveloperPageExceptionFilter> filters);
? ? public Task Invoke(HttpContext context);
}
如上面的代碼片段所示,當我們創(chuàng)建一個DeveloperExceptionPageMiddleware對象的時候需要以參數的形式提供一個IOptions<DeveloperExceptionPageOptions>對象,而DeveloperExceptionPageOptions對象攜帶著為這個中間件指定的配置選項,具體的配置選項體現(xiàn)在如下所示的兩個屬性(FileProvider和SourceCodeLineCount)上。
public class DeveloperExceptionPageOptions
{
? ? public IFileProvider FileProvider { get; set; }
? ? public int SourceCodeLineCount { get; set; }
}
一、IDeveloperPageExceptionFilter
DeveloperExceptionPageMiddleware中間件在默認情況下總是會呈現(xiàn)一個包含詳細信息的錯誤頁面,如果我們希望在呈現(xiàn)錯誤頁面之前做一些額外的異常處理操作,或者希望完全按照自己的方式來處理異常,這個功能可以通過注冊相應IDeveloperPageExceptionFilter對象的方式來實現(xiàn)。IDeveloperPageExceptionFilter接口定義了如下所示的HandleExceptionAsync方法,用來實現(xiàn)自定義的異常處理操作。
public interface IDeveloperPageExceptionFilter
{
? ? Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next);
}
public class ErrorContext
{
? ? public HttpContext HttpContext { get; }
? ? public Exception Exception { get; }
? ? public ErrorContext(HttpContext httpContext, Exception exception);
}
HandleExceptionAsync方法提供的第一個參數是一個ErrorContext對象,它提供了當前的HttpContext上下文和拋出的異常。第二個參數表示的委托對象代表后續(xù)的異常操作,如果需要將拋出的異常分發(fā)給后續(xù)處理器做進一步處理,就需要顯式地調用Func<ErrorContext, Task>對象。在如下所示的演示實例中,我們通過實現(xiàn)IDeveloperPageExceptionFilter接口定義了一個FakeExceptionFilter類型,并將其注冊到依賴注入框架中。
public class Program
{
? ? public static void Main()
? ? {? ? ? ? ? ?
? ? ? ? Host.CreateDefaultBuilder()
? ? ? ? ? ? .ConfigureWebHostDefaults(builder => builder
? ? ? ? ? ? ? ? .ConfigureServices(svcs=>svcs.AddSingleton<IDeveloperPageExceptionFilter, FakeExceptionFilter>())
? ? ? ? ? ? ? ? .Configure(app => app
? ? ? ? ? ? ? ? ? ? .UseDeveloperExceptionPage()
? ? ? ? ? ? ? ? ? ? .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception...")))))
? ? ? ? ? ? .Build()
? ? ? ? ? ? .Run();
? ? }
? ? private class FakeExceptionFilter : IDeveloperPageExceptionFilter
? ? {
? ? ? ? public Task HandleExceptionAsync(ErrorContext errorContext, Func<ErrorContext, Task> next)
? ? ? ? ? ? => errorContext.HttpContext.Response.WriteAsync("Unhandled exception occurred!");
? ? }
}
在FakeExceptionFilter類型實現(xiàn)的HandleExceptionAsync方法僅在響應的主體內容中寫入了一條簡單的錯誤消息(Unhandled exception occurred!),并沒有顯式調用該方法的參數next代表的“后續(xù)異常處理器”,所以DeveloperExceptionPageMiddleware中間件默認提供的錯誤頁面并不會呈現(xiàn)出來,取而代之的就是下圖所示的由注冊IDeveloperPageExceptionFilter定制的錯誤頁面。(S1608)
16-7
二、顯示編譯異常信息
我們編寫的ASP.NET Core應用會先編譯成程序集,然后部署并啟動執(zhí)行,為什么運行過程中還會出現(xiàn)“編譯異?!保繌腁SP.NET Core應用層面來說,如果采用預編譯模式,也就是說我們部署的不是源代碼而是編譯好的程序集,運行過程中根本就不存在編譯異常的說法。但是在一個ASP.NET Core MVC應用中,視圖文件(.cshtml)是支持動態(tài)運行時編譯(Runtime Compilation)的。我們可以直接部署視圖源文件,應用在執(zhí)行過程中是可以動態(tài)地將它們編譯成程序集的。換句話說,由于視圖文件支持動態(tài)編譯,所以可以在部署環(huán)境下直接修改視圖文件的內容。
對于DeveloperExceptionPageMiddleware中間件來說,如果拋出的是普通的運行時異常,它會將異常自身的詳細信息和當前請求上下文信息以HTML文檔的形式呈現(xiàn)出來,前面演示的實例已經很好地說明了這一點。如果應用在動態(tài)編譯視圖文件時出現(xiàn)了編譯異常,最終呈現(xiàn)出來的錯誤頁面將具有不同的結構和內容,可以通過一個簡單的實例演示DeveloperExceptionPageMiddleware中間件針對編譯異常的處理。
為了支持運行時編譯,我們需要為應用添加針對NuGet包“Microsoft.AspNetCore.Mvc.Razor. RuntimeCompilation”的依賴,并通過修改項目文件(.csproj)將PreserveCompilationReferences屬性設置為True,如下所示的代碼片段是整個項目文件的定義。
<Project Sdk="Microsoft.NET.Sdk.Web">
? <PropertyGroup>
? ? <TargetFramework>netcoreapp3.0</TargetFramework>
? ? <PreserveCompilationReferences>true</PreserveCompilationReferences>
? </PropertyGroup>
? <ItemGroup>
? ? <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
? ? ? ? Version="3.0.0" />
? </ItemGroup>
</Project>
我們通過如下所示的代碼承載了一個ASP.NET Core MVC應用,并注冊了DeveloperException
PageMiddleware中間件。為了支持針對Razor視圖文件的運行時編譯,在調用IServiceCollection接口的AddControllersWithViews擴展方法得到返回的IMvcBuilder對象之后,可以進一步調用該對象的AddRazorRuntimeCompilation擴展方法。
public class Program
{
? ? public static void Main()
? ? {
? ? ? ? Host.CreateDefaultBuilder()
? ? ? ? ? ? .ConfigureWebHostDefaults(builder => builder
? ? ? ? ? ? ? ? .ConfigureServices(svcs => svcs
? ? ? ? ? ? ? ? ? ? .AddRouting()
? ? ? ? ? ? ? ? ? ? .AddControllersWithViews()
? ? ? ? ? ? ? ? ? ? .AddRazorRuntimeCompilation())
? ? ? ? ? ? ? ? .Configure(app => app
? ? ? ? ? ? ? ? ? ? .UseDeveloperExceptionPage()
? ? ? ? ? ? ? ? ? ? .UseRouting()
? ? ? ? ? ? ? ? ? ? .UseEndpoints(endpoints => endpoints.MapControllers())))
? ? ? ? .Build()
? ? ? ? .Run();
? ? }
}
我們定義了如下所示的HomeController,它的Action方法Index會直接調用View方法將默認的視圖呈現(xiàn)出來。根據約定,Action方法Index呈現(xiàn)出來的視圖文件對應的路徑應該是“~/views/home/index.cshtml”,我們?yōu)榇嗽谶@個路徑下創(chuàng)建了如下所示的視圖文件。其中,F(xiàn)oobar是一個尚未被定義的類型。
public class HomeController : Controller
{
? ? [HttpGet("/")]
? ? public IActionResult Index() => View();
}
~/views/home/index.cshtml:
@{
? ? var value = new Foobar();
}
當我們利用瀏覽器訪問HomeController的Action方法Index時,應用會動態(tài)編譯目標視圖。由于視圖文件中使用了一個未定義的類型,動態(tài)編譯會失敗,響應的錯誤信息會以下圖所示的形式出現(xiàn)在瀏覽器上。可以看出,錯誤頁面顯示的內容和結構與前面演示的實例是完全不一樣的,我們不僅可以從這個錯誤頁面中得到導致編譯失敗的視圖文件的路徑“Views/Home/Index.cshtml”,還可以直接看到導致編譯失敗的那一行代碼。不僅如此,這個錯誤頁面還直接將參與編譯的源代碼(不是定義在.cshtml文件中的原始代碼,而是經過轉換處理生成的C#代碼)呈現(xiàn)出來。毫無疑問,如此詳盡的錯誤頁面對于開發(fā)人員的糾錯是非常有價值的。
16-8
一般來說,動態(tài)編譯的過程如下:先將源代碼(類似于.cshtml這樣的模板文件)轉換成針對某種 .NET語言(如C#)的代碼,然后進一步編譯成IL代碼。動態(tài)編譯過程中拋出的異常類型一般會實現(xiàn)ICompilationException接口。如下面的代碼片段所示,該接口具有一個唯一的屬性CompilationFailures,它返回一個元素類型為CompilationFailure的集合。編譯失敗的相關信息被封裝在一個CompilationFailure對象之中,我們可以利用它得到源文件的路徑(SourceFilePath)和內容(SourceFileContent),以及源代碼轉換后交付編譯的內容。如果在內容轉換過程已經發(fā)生錯誤,在這種情況下的SourceFileContent屬性可能返回Null。
public interface ICompilationException
{
? ? IEnumerable<CompilationFailure> CompilationFailures { get; }
}
public class CompilationFailure
{
? ? public string SourceFileContent { get; }
? ? public string SourceFilePath { get; }
? ? public string CompiledContent { get; }
? ? public IEnumerable<DiagnosticMessage> Messages { get; }
? ? ...
}
CompilationFailure類型還有一個名為Messages的只讀屬性,它返回一個元素類型為DiagnosticMessage的集合,一個DiagnosticMessage對象承載著一些描述編譯錯誤的診斷信息。我們不僅可以借助DiagnosticMessage對象的相關屬性得到描述編譯錯誤的消息(Message和FormattedMessage),還可以得到發(fā)生編譯錯誤所在源文件的路徑(SourceFilePath)及范圍,StartLine屬性和StartColumn屬性分別表示導致編譯錯誤的源代碼在源文件中開始的行與列;EndLine屬性和EndColumn屬性分別表示導致編譯錯誤的源代碼在源文件中結束的行與列(行數和列數分別從1與0開始計數)。
public class DiagnosticMessage
{
? ? public string SourceFilePath { get; }
? ? public int StartLine { get; }
? ? public int StartColumn { get; }
? ? public int EndLine { get; }
? ? public int EndColumn { get; }
? ? public string Message { get; }
? ? public string FormattedMessage { get; }
? ? ...
}
從圖16-8可以看出,錯誤頁面會直接將導致編譯失敗的相關源代碼顯示出來。具體來說,它不僅將直接導致失敗的源代碼實現(xiàn)出來,還顯示前后相鄰的源代碼。至于相鄰源代碼應該顯示多少行,實際上是通過配置選項DeveloperExceptionPageOptions的SourceCodeLineCount屬性控制的。
public class Program
{
? ? public static void Main()
? ? {
? ? ? ? var options = new DeveloperExceptionPageOptions { SourceCodeLineCount = 3 };
? ? ? ? Host.CreateDefaultBuilder()
? ? ? ? ? ? .ConfigureWebHostDefaults(builder => builder
? ? ? ? ? ? ? ? .ConfigureServices(svcs => svcs
? ? ? ? ? ? ? ? ? ? .AddRouting()
? ? ? ? ? ? ? ? ? ? .AddControllersWithViews()
? ? ? ? ? ? ? ? ? ? .AddRazorRuntimeCompilation())
? ? ? ? ? ? ? ? .Configure(app => app
? ? ? ? ? ? ? ? ? ? .UseDeveloperExceptionPage(options)
? ? ? ? ? ? ? ? ? ? .UseRouting()
? ? ? ? ? ? ? ? ? ? .UseEndpoints(endpoints => endpoints.MapControllers())))
? ? ? ? .Build()
? ? ? ? .Run();
? ? }
}
對于前面演示的這個實例來說,如果將前后相鄰的3行代碼顯示在錯誤頁面上,我們可以采用如上所示的方式為注冊的DeveloperExceptionPageMiddleware中間件指定一個Developer
ExceptionPageOptions對象,并將它的SourceCodeLineCount屬性設置為3。與此同時,我們可以將視圖文件(index.cshtml)改寫成如下所示的形式,即在導致編譯失敗的那一行代碼前后分別添加4行代碼。
1:
2:
3:
4:
5:@{ var value = new Foobar();}
6:
7:
8:
9:
對于定義在視圖文件中的9行代碼,根據在注冊DeveloperExceptionPageMiddleware中間件時指定的規(guī)則,最終顯示在錯誤頁面上的應該是第2行至第8行。如果利用瀏覽器訪問相同的地址,這7行代碼會以下圖所示的形式出現(xiàn)在錯誤頁面上。值得注意的是,如果我們沒有對SourceCodeLineCount屬性做顯式設置,它的默認值為6。
16-9
三、DeveloperExceptionPageMiddleware
下面從DeveloperExceptionPageMiddleware類型的實現(xiàn)邏輯對該中間件針對異常頁面的呈現(xiàn)做進一步講解。如下所示的代碼片段只保留了DeveloperExceptionPageMiddleware類型的核心代碼,我們可以看到它的構造函數中注入了用來提供配置選項的IOptions<DeveloperExceptionPage
Options>對象和一組IDeveloperPageExceptionFilter對象。
public class DeveloperExceptionPageMiddleware
{
? ? private readonly RequestDelegate _next;
? ? private readonly DeveloperExceptionPageOptions _options;
? ? private readonly Func<ErrorContext, Task> _exceptionHandler;
? ? public DeveloperExceptionPageMiddleware(
? ? ? ? RequestDelegate next,
? ? ? ? IOptions<DeveloperExceptionPageOptions> options,
? ? ? ? ILoggerFactory loggerFactory,
? ? ? ? IWebHostEnvironment hostingEnvironment,
? ? ? ? DiagnosticSource diagnosticSource,
? ? ? ? IEnumerable<IDeveloperPageExceptionFilter> filters)
? ? {
? ? ? ? _next = next;
? ? ? ? _options = options.Value;
? ? ? ? _exceptionHandler = context => context.Exception is ICompilationException
? ? ? ? ? ? DisplayCompilationException()
? ? ? ? ? : DisplayRuntimeException();
? ? ? ? ...
? ? ? ? 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)
? ? ? ? {
? ? ? ? ? ? context.Response.Clear();
? ? ? ? ? ? context.Response.StatusCode = 500;
? ? ? ? ? ? await _exceptionHandler(new ErrorContext(context, ex));
? ? ? ? ? ? throw;
? ? ? ? }
? ? }
? ? private Task DisplayCompilationException();
? ? private Task DisplayRuntimeException();
}
被DeveloperExceptionPageMiddleware中間件用來作為異常處理器的是一個Func<ErrorContext, Task>對象,通過字段_exceptionHandler表示。當處理器在處理異常的時候,它會先調用注入的IDeveloperPageExceptionFilter對象,最后調用DisplayRuntimeException方法或者DisplayCompilation
Exception方法來呈現(xiàn)“開發(fā)者異常頁面”。如果某個注冊的IDeveloperPageExceptionFilter阻止了后續(xù)的異常處理,整個處理過程將會就此中止。
在Invoke方法中,DeveloperExceptionPageMiddleware中間件會直接將當前請求分發(fā)給后續(xù)的管道進行處理。如果拋出異常,它會根據該異常對象和當前HttpContext上下文創(chuàng)建一個ErrorContext對象,并將其作為參數調用作為異常處理器的Func<ErrorContext, Task>委托對象。該中間件最終會回復一個狀態(tài)碼為“500 Internal Server Error”的響應。
我們一般調用IApplicationBuilder 接口的如下所示的兩個UseDeveloperExceptionPage擴展方法來注冊DeveloperExceptionPageMiddleware中間件。我們可以利用作為配置選項的DeveloperExceptionPageOptions對象指定一個提供源文件的IFileProvider對象,也可以利用這個配置選項來控制導致異常源代碼的前后行數。
public static class DeveloperExceptionPageExtensions
{? ?
? ? public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app)
? ? ? ? => app.UseMiddleware<DeveloperExceptionPageMiddleware>();
? ? public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app,DeveloperExceptionPageOptions options)
? ? ? ? =>app.UseMiddleware<DeveloperExceptionPageMiddleware>(Options.Create(options));
}
亞馬遜測評 www.yisuping.cn