呈现错误信息》通过几个简单的实例演示了如何呈现一个错误页面,该过程由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. 从用SwiftUI搭建项目说起

    前言 后续这个SwiftUI分类的文章全部都是针对SwiftUI的日常学习和理解写的,自己利用Swift写的第二个项目也顺利上线后续的需求也不是特着急,最近正好有空就利用这段时间补一下自己对Swift ...

  2. MySQL5.7 主主复制配置

    MySQL 5.7 主主复制配置 一.简介: MySQL主主复制其实就是两个MySQL主从复制组合到一起,接着我的上一篇博客<centos 7 配置 mysql 5.7 主从复制>配置即可 ...

  3. Tensorflow环境配置&安装

    Tensorflow环境配置&安装 明知故犯,是不想有遗憾. 背景:Tensorflow 环境配置和安装. 一.安装 Anaconda 二.建立.激活.安装.验证.使用 Tensorflow ...

  4. Filezilla账号设置多个文件夹

    问题描述 一个账号,使用多个文件目录.虽然可以这样设置,但是实际上客户端使用时只能使用一个目录(主目录). 所以想找一种方式,访问一个主目录时,也可以访问到其他的目录. 官网示例 https://wi ...

  5. asp.net url参数中有中文request.querystring 乱码

    说明: 从这点我们发现:所有的参数输入,都调用了一次:HttpUtility.UrlDecode(str2, encoding); 结论出来了: 当客户端js对中文以utf-8编码提交到服务端时,用R ...

  6. python初学者-使用if条件语句判断成绩等级

    x = int(input("x=")) if x<0 or x > 100:#如果不在0--100区间内输出error print("error" ...

  7. Listary效率快捷键

    快捷键 打开搜索框快捷键: 双击Ctrl键 上一个项目:向上键 下一个项目:向下键/Tap 打开动作:Enter 属性窗口:Ctrl+O (查询)关键字 作用范围:搜索框 使用方法:命令+空格+关键字 ...

  8. struts文件上传拦截器分析

    struts有默认的文件拦截器,一般配置maximumSize就可以了. 知道原理,我们可以写一个类继承它,实现自己的配置上传文件大小的方式.   然后细究页面上传文件的时候,发现了一些问题. act ...

  9. Json串的字段如果和类中字段不一致,如何映射、转换?

    Json串是我们现在经常会遇到的一种描述对象的字符串格式.在用Java语言开发的功能中,也经常需要做Json串与Java对象之间的转换. fastjson就是经常用来做Json串与Java对象之间的转 ...

  10. grpc系列- protobuf详解

    Protocol Buffers 是一种与语言.平台无关,可扩展的序列化结构化数据的方法,常用于通信协议,数据存储等等.相较于 JSON.XML,它更小.更快.更简单,因此也更受开发人员的青眯. 基本 ...