ASP.NET Core错误处理中间件[2]: 开发者异常页面
《呈现错误信息》通过几个简单的实例演示了如何呈现一个错误页面,该过程由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]: 开发者异常页面的更多相关文章
- ASP.NET Core错误处理中间件[1]: 呈现错误信息
NuGet包"Microsoft.AspNetCore.Diagnostics"中提供了几个与异常处理相关的中间件.当ASP.NET Core应用在处理请求过程中出现错误时,我们可 ...
- ASP.NET Core错误处理中间件[4]: 响应状态码页面
StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处 ...
- ASP.NET Core错误处理中间件[3]: 异常处理器
DeveloperExceptionPageMiddleware中间件错误页面可以呈现抛出的异常和当前请求上下文的详细信息,以辅助开发人员更好地进行纠错诊断工作.ExceptionHandlerMid ...
- 翻译 - ASP.NET Core 基本知识 - 中间件(Middleware)
翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-5.0 中间件是集成 ...
- ASP.NET Core 中的中间件
前言 由于是第一次写博客,如果您看到此文章,希望大家抱着找错误.批判的心态来看. sky! 何为中间件? 在 ASP.NET Framework 中应该都知道请求管道.可参考:浅谈 ASP.NET ...
- asp.net core 自定义异常处理中间件
asp.net core 自定义异常处理中间件 Intro 在 asp.net core 中全局异常处理,有时候可能不能满足我们的需要,可能就需要自己自定义一个中间件处理了,最近遇到一个问题,有一些异 ...
- ASP.NET Core 3.1 中间件
参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 ...
- Asp.Net Core 通过自定义中间件防止图片盗链的实例(转)
一.原理 要实现防盗链,我们就必须先理解盗链的实现原理,提到防盗链的实现原理就不得不从HTTP协议说起,在HTTP协议中,有一个表头字段叫referer,采用URL的格式来表示从哪儿链接到当前的网页或 ...
- 在Asp.net Core中使用中间件来管理websocket
介绍 ASP.NET Core SignalR是一个有用的库,可以简化Web应用程序中实时通信的管理.但是,我宁愿使用WebSockets,因为我想要更灵活,并且与任何WebSocket客户端兼容. ...
随机推荐
- Python中open函数怎么操作文件
在 Python 中,如果想要操作文件,首先需要创建或者打开指定的文件,并创建一个文件对象,而这些工作可以通过内置的 open() 函数实现. open() 函数用于创建或打开指定文件,该函数的常用语 ...
- 学好Spark/Kafka必须要掌握的Scala技术点(二)类、单例/伴生对象、继承和trait,模式匹配、样例类(case class)
3. 类.对象.继承和trait 3.1 类 3.1.1 类的定义 Scala中,可以在类中定义类.以在函数中定义函数.可以在类中定义object:可以在函数中定义类,类成员的缺省访问级别是:publ ...
- G1 收集器
基础知识 性能指标 在调优Java应用程序时,重点通常放在两个主要目标上:响应性 或 吞吐量. 响应性Responsiveness 是指应用程序对请求的数据做出响应的速度: 桌面用户界面对事件的响应速 ...
- 多个HDFS集群的fs.defaultFS配置一样,造成应用一直连接同一个集群的问题分析
背景 应用需要对两个集群中的同一目录下的HDFS文件个数和文件总大小进行比对,在测试环境中发现,即使两边HDFS目录下的数据不一样,应用日志显示两边始终比对一致,分下下来发现,应用连的一直是同一个集群 ...
- Maven项目中配置jdk版本
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> ...
- 拒绝造轮子!如何移植并使用Linux内核的通用链表(附完整代码实现)
在实际的工作中,我们可能会经常使用链表结构来存储数据,特别是嵌入式开发,经常会使用linux内核最经典的双向链表 list_head.本篇文章详细介绍了Linux内核的通用链表是如何实现的,对于经常使 ...
- 算法(Java实现)—— 动态规划算法
动态规划算法 应用场景-0-1背包问题 背包问题:有一个背包,容量为4磅,现有物品如下 物品 重量 价格 吉他(G) 1 1500 音响(S) 4 3000 电脑(L) 3 2000 要求: 达到目标 ...
- 我们为什么选择VUE来构建前端
很多使用过VUE的程序员,对VUE的评价是"Vue.js 兼具angular.js和react.js的优点,并剔除了它们的缺点". 那么,他真的值得这么高的评价嘛? Vue.js的 ...
- windows 任何软件出现异常有日志 w3wp.exe [10608]中发生了未处理的Microsoft .Net Framework异常
右键我的电脑 管理
- 使用CentOS8搭建私有NAS存储的一些建议
对于超过2TB的硬盘来说只能考虑GPT分区表,因此还是建议使用EFI来安装系统. 对于超过2TB的硬盘来说应该选择LVM,然后磁盘末尾预留出至少100G的空间用于将来方便维护安装个Windows系统之 ...