呈现错误信息》通过几个简单的实例演示了如何呈现一个错误页面,该过程由3个对应的中间件来完成。下面先介绍用来呈现开发者异常页面的DeveloperExceptionPageMiddleware中间件,该中间件在捕捉到后续处理过程中抛出的异常之后会返回一个媒体类型为text/html的响应,后者在浏览器上会呈现一个错误页面。由于这是一个为开发者提供诊断信息的异常页面,所以可以将其称为开发者异常页面(Developer Exception Page)。该页面不仅会呈现异常的详细信息(类型、消息和跟踪堆栈等),还会出现与当前请求相关的上下文信息。如下所示的代码片段是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);
}

如上面的代码片段所示,当我们创建一个DeveloperExceptionPageMiddleware对象的时候需要以参数的形式提供一个IOptions<DeveloperExceptionPageOptions>对象,而DeveloperExceptionPageOptions对象携带着为这个中间件指定的配置选项,具体的配置选项体现在如下所示的两个属性(FileProvider和SourceCodeLineCount)上。

public class DeveloperExceptionPageOptions
{
public IFileProvider FileProvider { get; set; }
public int SourceCodeLineCount { get; set; }
}

一、IDeveloperPageExceptionFilter

DeveloperExceptionPageMiddleware中间件在默认情况下总是会呈现一个包含详细信息的错误页面,如果我们希望在呈现错误页面之前做一些额外的异常处理操作,或者希望完全按照自己的方式来处理异常,这个功能可以通过注册相应IDeveloperPageExceptionFilter对象的方式来实现。IDeveloperPageExceptionFilter接口定义了如下所示的HandleExceptionAsync方法,用来实现自定义的异常处理操作。

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上下文和抛出的异常。第二个参数表示的委托对象代表后续的异常操作,如果需要将抛出的异常分发给后续处理器做进一步处理,就需要显式地调用Func<ErrorContext, Task>对象。在如下所示的演示实例中,我们通过实现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类型实现的HandleExceptionAsync方法仅在响应的主体内容中写入了一条简单的错误消息(Unhandled exception occurred!),并没有显式调用该方法的参数next代表的“后续异常处理器”,所以DeveloperExceptionPageMiddleware中间件默认提供的错误页面并不会呈现出来,取而代之的就是下图所示的由注册IDeveloperPageExceptionFilter定制的错误页面。(S1608)

二、显示编译异常信息

我们编写的ASP.NET Core应用会先编译成程序集,然后部署并启动执行,为什么运行过程中还会出现“编译异常”?从ASP.NET Core应用层面来说,如果采用预编译模式,也就是说我们部署的不是源代码而是编译好的程序集,运行过程中根本就不存在编译异常的说法。但是在一个ASP.NET Core MVC应用中,视图文件(.cshtml)是支持动态运行时编译(Runtime Compilation)的。我们可以直接部署视图源文件,应用在执行过程中是可以动态地将它们编译成程序集的。换句话说,由于视图文件支持动态编译,所以可以在部署环境下直接修改视图文件的内容。

对于DeveloperExceptionPageMiddleware中间件来说,如果抛出的是普通的运行时异常,它会将异常自身的详细信息和当前请求上下文信息以HTML文档的形式呈现出来,前面演示的实例已经很好地说明了这一点。如果应用在动态编译视图文件时出现了编译异常,最终呈现出来的错误页面将具有不同的结构和内容,可以通过一个简单的实例演示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方法将默认的视图呈现出来。根据约定,Action方法Index呈现出来的视图文件对应的路径应该是“~/views/home/index.cshtml”,我们为此在这个路径下创建了如下所示的视图文件。其中,Foobar是一个尚未被定义的类型。

public class HomeController : Controller
{
[HttpGet("/")]
public IActionResult Index() => View();
} ~/views/home/index.cshtml:
@{
var value = new Foobar();
}

当我们利用浏览器访问HomeController的Action方法Index时,应用会动态编译目标视图。由于视图文件中使用了一个未定义的类型,动态编译会失败,响应的错误信息会以下图所示的形式出现在浏览器上。可以看出,错误页面显示的内容和结构与前面演示的实例是完全不一样的,我们不仅可以从这个错误页面中得到导致编译失败的视图文件的路径“Views/Home/Index.cshtml”,还可以直接看到导致编译失败的那一行代码。不仅如此,这个错误页面还直接将参与编译的源代码(不是定义在.cshtml文件中的原始代码,而是经过转换处理生成的C#代码)呈现出来。毫无疑问,如此详尽的错误页面对于开发人员的纠错是非常有价值的。

一般来说,动态编译的过程如下:先将源代码(类似于.cshtml这样的模板文件)转换成针对某种 .NET语言(如C#)的代码,然后进一步编译成IL代码。动态编译过程中抛出的异常类型一般会实现ICompilationException接口。如下面的代码片段所示,该接口具有一个唯一的属性CompilationFailures,它返回一个元素类型为CompilationFailure的集合。编译失败的相关信息被封装在一个CompilationFailure对象之中,我们可以利用它得到源文件的路径(SourceFilePath)和内容(SourceFileContent),以及源代码转换后交付编译的内容。如果在内容转换过程已经发生错误,在这种情况下的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),还可以得到发生编译错误所在源文件的路径(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可以看出,错误页面会直接将导致编译失败的相关源代码显示出来。具体来说,它不仅将直接导致失败的源代码实现出来,还显示前后相邻的源代码。至于相邻源代码应该显示多少行,实际上是通过配置选项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中间件时指定的规则,最终显示在错误页面上的应该是第2行至第8行。如果利用浏览器访问相同的地址,这7行代码会以下图所示的形式出现在错误页面上。值得注意的是,如果我们没有对SourceCodeLineCount属性做显式设置,它的默认值为6。

三、DeveloperExceptionPageMiddleware

下面从DeveloperExceptionPageMiddleware类型的实现逻辑对该中间件针对异常页面的呈现做进一步讲解。如下所示的代码片段只保留了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方法来呈现“开发者异常页面”。如果某个注册的IDeveloperPageExceptionFilter阻止了后续的异常处理,整个处理过程将会就此中止。

在Invoke方法中,DeveloperExceptionPageMiddleware中间件会直接将当前请求分发给后续的管道进行处理。如果抛出异常,它会根据该异常对象和当前HttpContext上下文创建一个ErrorContext对象,并将其作为参数调用作为异常处理器的Func<ErrorContext, Task>委托对象。该中间件最终会回复一个状态码为“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));
}

ASP.NET Core错误处理中间件[1]: 呈现错误信息
ASP.NET Core错误处理中间件[2]: 开发者异常页面
ASP.NET Core错误处理中间件[3]: 异常处理器
ASP.NET Core错误处理中间件[4]: 响应状态码页面

ASP.NET Core错误处理中间件[2]: 开发者异常页面的更多相关文章

  1. ASP.NET Core错误处理中间件[1]: 呈现错误信息

    NuGet包"Microsoft.AspNetCore.Diagnostics"中提供了几个与异常处理相关的中间件.当ASP.NET Core应用在处理请求过程中出现错误时,我们可 ...

  2. ASP.NET Core错误处理中间件[4]: 响应状态码页面

    StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处 ...

  3. ASP.NET Core错误处理中间件[3]: 异常处理器

    DeveloperExceptionPageMiddleware中间件错误页面可以呈现抛出的异常和当前请求上下文的详细信息,以辅助开发人员更好地进行纠错诊断工作.ExceptionHandlerMid ...

  4. 翻译 - ASP.NET Core 基本知识 - 中间件(Middleware)

    翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0 中间件是集成 ...

  5. ASP.NET Core 中的中间件

    前言   由于是第一次写博客,如果您看到此文章,希望大家抱着找错误.批判的心态来看. sky! 何为中间件? 在 ASP.NET Framework 中应该都知道请求管道.可参考:浅谈 ASP.NET ...

  6. asp.net core 自定义异常处理中间件

    asp.net core 自定义异常处理中间件 Intro 在 asp.net core 中全局异常处理,有时候可能不能满足我们的需要,可能就需要自己自定义一个中间件处理了,最近遇到一个问题,有一些异 ...

  7. ASP.NET Core 3.1 中间件

    参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 ...

  8. Asp.Net Core 通过自定义中间件防止图片盗链的实例(转)

    一.原理 要实现防盗链,我们就必须先理解盗链的实现原理,提到防盗链的实现原理就不得不从HTTP协议说起,在HTTP协议中,有一个表头字段叫referer,采用URL的格式来表示从哪儿链接到当前的网页或 ...

  9. 在Asp.net Core中使用中间件来管理websocket

    介绍 ASP.NET Core SignalR是一个有用的库,可以简化Web应用程序中实时通信的管理.但是,我宁愿使用WebSockets,因为我想要更灵活,并且与任何WebSocket客户端兼容. ...

随机推荐

  1. C 与 C++ 中 指向二维数组的指针进行指针运算

    二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有"缝隙".以下面的二维数组 nums 为例: 从概念上理解,nums 的分布像一个矩阵,但在 ...

  2. PHP语言基础知识

    目录 前言 第一章 PHP语言学习介绍 1.1 PHP部署安装环境 1.2 PHP代码工具选择 第二章 PHP代码基本语法 2.1 PHP函数知识介绍 2.2 PHP常量变量介绍 2.2.1 PHP变 ...

  3. LLVM程序分析日记之插桩BranchInst

    1. splitblockandinsertifthenelse() 一个代码例子:StackOverflow 2. SplitBlockAndInsertIfThen() 或者仅仅想插桩if the ...

  4. Idea中Web项目Jsp文件找不到类解决方法

    在src下创建package,java代码放到包中,编译时才能在WEB-INFO的classes文件夹中生成可识别的class文件 https://blog.csdn.net/youwanname/a ...

  5. css 09-CSS案例讲解:博雅互动

    09-CSS案例讲解:博雅互动 #前言 CSS已经学了一些基础内容了,我们来讲解一个小案例吧.以博雅互动的官网首页举例. #版心 首页的版心如下: 这里我们要普及一个概念,叫"版心" ...

  6. react第十三单元(react路由-react路由的跳转以及路由信息) #课程目标

    第十三单元(react路由-react路由的跳转以及路由信息) #课程目标 熟悉掌握路由的配置 熟悉掌握跳转路由的方式 熟悉掌握路由跳转传参的方式 可以根据对其的理解封装一个类似Vue的router- ...

  7. Flutter InkWell - Flutter每周一组件

    Flutter Inkwell使用详解 该文章属于[Flutter每周一组件]系列,其它组件可以查看该系列下的文章,该系列会不间断更新:所有组件的demo已经上传值Github: https://gi ...

  8. Java项目连接数据库Mysql报错create connection SQLException

    今天编写了一个Java项目,对数据库Mysql的表进行增删改查,然后遇到了这个问题 严重: create connection SQLException, url: jdbc:mysql://loca ...

  9. Django项目连接多个数据库配置

    1.设置数据库连接 pip install PyMySQL 2.在项目同名目录myproject/myproject下的__init__.py添加以下代码 import pymysql pymysql ...

  10. Java 从 Map 到 HashMap 的一步步实现

    Java 从 Map 到 HashMap 的一步步实现 一. Map 1.1 Map 接口 在 Java 中, Map 提供了键--值的映射关系.映射不能包含重复的键,并且每个键只能映射到一个值. 以 ...