1. 程式人生 > >ASP.NET Core應用的錯誤處理[2]:DeveloperExceptionPageMiddleware中介軟體如何呈現“開發者異常頁面”

ASP.NET Core應用的錯誤處理[2]:DeveloperExceptionPageMiddleware中介軟體如何呈現“開發者異常頁面”

在《ASP.NET Core應用的錯誤處理[1]:三種呈現錯誤頁面的方式》中,我們通過幾個簡單的例項演示瞭如何呈現一個錯誤頁面,這些錯誤頁面的呈現分別由三個對應的中介軟體來完成,接下來我們將對這三個中介軟體進行詳細介紹。在開發環境呈現的異常頁面是通過一個型別為DeveloperExceptionPageMiddleware中介軟體實現的。[本文已經同步到《ASP.NET Core框架揭祕》之中]

   1: public class DeveloperExceptionPageMiddleware
   2: {
   3:     public DeveloperExceptionPageMiddleware(RequestDelegate next, IOptions<DeveloperExceptionPageOptions> options, 
   4:         ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment, DiagnosticSource diagnosticSource);
   5:     public Task Invoke(HttpContext context);
   6: }

如上面的程式碼片段所示,當我們建立一個DeveloperExceptionPageMiddleware物件的時候需要以引數的形式提供一個IOptions<DeveloperExceptionPageOptions>物件,而DeveloperExceptionPageOptions物件攜帶我們為這個中介軟體指定的配置選項,具體的配置選項體現在如下另個屬性(FileProvider和SourceCodeLineCount)。

   1: public class DeveloperExceptionPageOptions
   2: {
   3:     public IFileProvider     FileProvider { get; set; }
   4:     public int               SourceCodeLineCount { get; set; }
   5: }

一般來說我們總是通過呼叫ApplicationBuilder的擴充套件方法UseDeveloperExceptionPage方法來註冊這個DeveloperExceptionPageMiddleware中介軟體,這兩個擴充套件方法過載採用如下的方式建立並註冊這個DeveloperExceptionPageMiddleware中介軟體。

   1: public static class DeveloperExceptionPageExtensions
   2: {    
   3:     public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app)
   4:     {
   5:         return app.UseMiddleware<DeveloperExceptionPageMiddleware>();
   6:     }    
   7:     public static IApplicationBuilder UseDeveloperExceptionPage(this IApplicationBuilder app,DeveloperExceptionPageOptions options)
   8:     {
   9:         return app.UseMiddleware<DeveloperExceptionPageMiddleware>(Options.Create(options));
  10:     }
  11: }

在《ASP.NET Core應用的錯誤處理[1]:三種呈現錯誤頁面的方式》例項演示中,我們並不曾使用過DeveloperExceptionPageOptions這個物件,對於定義在這個型別中的這兩個屬性,我想很多人都不知道它們究竟可以用作哪方面的配置。要很清楚地解答這個問題,就需要從 DeveloperExceptionPageMiddleware中介軟體處理的兩種異常型別說起。總的來說,該中介軟體處理的異常大體上可以分為兩類,它們分別是“執行時異常”和“編譯異常”,後者型別實現了ICompilationException介面,如下的程式碼片段基本上體現了異常處理在DeveloperExceptionPageMiddleware中介軟體中的實現。

   1: public class DeveloperExceptionPageMiddleware
   2: {
   3:     private RequestDelegate _next;
   4:     public async Task Invoke(HttpContext context)
   5:     {
   6:         try
   7:         {
   8:             await _next(context);
   9:         }
  10:         catch(Exception ex)
  11:         {
  12:             context.Response.Clear();
  13:             context.Response.StatusCode = 500;
  14:  
  15:             ICompilationException compilationException = ex as ICompilationException;
  16:             if (null != compilationException)
  17:             {
  18:                 await DisplayCompilationException(context, compilationException);
  19:             }
  20:             else
  21:             {
  22:                 await DisplayRuntimeException(context, ex);
  23:             }
  24:         }
  25:     }
  26:  
  27:     private Task DisplayRuntimeException(HttpContext context, Exception ex);
  28:      private Task DisplayCompilationException(HttpContext context,ICompilationException compilationException) ;
  29: }

一、 處理編譯異常

我想很多人會很疑惑:我們編寫一個ASP.NET Core應用應該是先編譯成程式集,然後再部署並啟動執行,為什麼執行過程中還會出現“編譯異常”呢?從ASP.NET Core應用層面來說,我們採用的是“預編譯”,也就說我們部署的不是原始碼而是編譯好的程式集,所以執行過程中根本就不存在“編譯異常”一說。但是不要忘了在一個ASP.NET Core MVC應用中,檢視檔案(.cshtml)是支援“動態編譯”的。也就是說我們可以直接部署檢視原始檔,應用在執行過程中是可以動態地編譯它們的。換句話說,由於檢視檔案支援動態編譯,我們是可以在部署環境直接修改檢視檔案的。

對於DeveloperExceptionPageMiddleware中介軟體來說,對於普通的執行時異常,它會採用HTML文件的形式將異常自身的詳細資訊和當前請求的資訊以HTML文件的形式呈現出來,我們前面演示的例項已經很好的說明了這一點。如果應用在動態編譯檢視檔案中出現了編譯異常,最終呈現出來的錯誤頁面將具有不同的結構和內容,我們不防也通過一個簡單的例項來演示一下DeveloperExceptionPageMiddleware中介軟體針對編譯異常的處理。

我們通過如下所示的程式碼啟動了一個ASP.NET Core MVC應用,並通過呼叫ApplicationBuilder的擴充套件方法UseDeveloperExceptionPage註冊了DeveloperExceptionPageMiddleware中介軟體。對應定義在HomeController中的Action方法Index來說,它會負責將對應的檢視呈現出來。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseContentRoot(Directory.GetCurrentDirectory())
   8:             .ConfigureServices(svcs => svcs.AddMvc())
   9:             .Configure(app => app
  10:                 .UseDeveloperExceptionPage()
  11:                 .UseMvc())
  12:             .Build()
  13:             .Run();
  14:     }
  15: }
  16:  
  17: public class HomeController : Controller
  18: {
  19:     [HttpGet("/")]
  20:     public IActionResult Index()
  21:     {
  22:         return View();
  23:     }
  24: }

根據約定,Action方法Index呈現出來的檢視檔案對應的路徑應該是“~/views/home/index.cshtml”,我們為此在這個路徑下建立這個檢視檔案。為了能夠在動態編譯過程中出現編譯異常,我們在這個檢視檔案中編寫了如下三行程式碼,Foobar是一個尚未被建立的型別

   1: @{ 
   2:     var value = new Foobar();
   3: }

當我們利用瀏覽器訪問HomeController的Action方法Index的時候,應用會動態編譯目標檢視,由於檢視檔案中使用了一個不曾不定義的型別,動態編譯會失敗,響應的錯誤資訊會以如圖7所示的形式出現在瀏覽器上。可以看出錯誤頁面顯示的內容和結構與前面演示的例項是完全不一樣的,我們不僅可以從這個錯誤頁面中得到導致編譯失敗的檢視檔案的路徑(“Views/Home/Index.cshtml”),還可以直接看到導致編譯失敗的那一行程式碼。不僅如此,這個錯誤頁面還直接將參與編譯的原始碼(不是定義在.cshtml檔案中的原始程式碼,而是經過轉換處理生成的C#程式碼)。毫無疑問,這個如此詳盡的錯誤頁面對於相信開發人員的糾錯針對是非常有價值的。

7

一般來說,動態編譯的整個過程由兩個步驟組成,它先是將原始碼(類似於.cshtml這樣的模板檔案)轉換成針對某種.NET語言(比如C#)的程式碼,然後進一步地編譯成IL程式碼。動態編譯過程中丟擲的異常型別一般會實現ICompilationException介面。如下面的程式碼片段所示,該介面值具有一個唯一的屬性CompilationFailures,它返回一個元素型別為CompilationFailure的集合。編譯失敗的相關資訊被封裝在一個CompilationFailure物件之中,我們可以利用它得到原始檔的路徑(SourceFilePath)和內容(SourceFileContent),以及原始碼轉換後交付編譯的內容。如果在內容轉換過程就已經發生錯誤,那麼SourceFileContent屬性可能返回Null。

   1: public interface ICompilationException
   2: {
   3:     IEnumerable<CompilationFailure> CompilationFailures { get; }
   4: }
   5:  
   6: public class CompilationFailure
   7: {
   8:     public string                             SourceFileContent {  get; }
   9:     public string                             SourceFilePath {  get; }
  10:     public string                             CompiledContent {  get; }
  11:     public IEnumerable<DiagnosticMessage>     Messages {  get; }
  12:
  13: }

CompilationFailure型別還具有一個名為Messages的只讀屬性,它返回一個元素型別為DiagnosticMessage的集合,一個DiagnosticMessage物件承載著一些描述編譯錯誤的診斷資訊。我們不僅可以藉助DiagnosticMessage物件的相關屬性得到描述編譯錯誤的訊息(Message和FormattedMessage),還可以得到發生編譯錯誤所在原始檔的路徑(SourceFilePath)以及範圍,StartLine、StartColumn、EndLine和EndColumn屬性分別表示導致編譯錯誤的原始碼在原始檔中開始和結束的行與列(行數和列數分別從1和0開始計數)。

   1: public class DiagnosticMessage
   2: {
   3:     public string     SourceFilePath {  get; }
   4:     public int        StartLine {  get; }
   5:     public int        StartColumn {  get; } 
   6:     public int        EndLine {  get; }
   7:     public int        EndColumn {  get; }
   8:  
   9:     public string     Message {  get; }      
  10:     public string     FormattedMessage {  get; } 
  11:
  12: }

從上圖可以看出,錯誤頁面會直接將導致編譯失敗的相關原始碼顯示出來。具體來說,它不僅僅會將直接導致失敗的原始碼實現出來,還會同時顯示前後相鄰的原始碼。至於相鄰原始碼應該顯示多少行,實際上是通過DeveloperExceptionPageOptions的SourceCodeLineCount屬性控制的。

   1: public class Program
   2: {
   3:     <