ASP.NET Core Diagnostics 中间件内嵌 Razor 视图的编译与再生成:RazorPageGenerator 开发工作流全解析
【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore
ASP.NET Core 源码仓库中的Diagnostics 中间件(开发者异常页、编译错误页等)在交付时不携带任何外部视图模板,其 HTML 页面全部以"预编译的 Razor 视图"形式内嵌进程序集。本文以仓库文档 src/Middleware/Diagnostics/src/README.md 为核心脉络,结合 RazorPageGenerator 工具源码 与中间件实现,系统讲解这套视图的生成原理、工具命令行用法以及修改*.cshtml后必须执行的再生成流程。读完本文,你将能够独立修改 Diagnostics 内嵌页面、正确触发视图重编译,并从源码级理解这套"写 Razor → 编译成 C# → 内嵌渲染"的工程化机制。
1. 关联文档说了什么:一段必须执行的"开发期"工序
在 src/Middleware/Diagnostics/src/README.md 中,正文只有"Development"一节,却点明了一个极易被贡献者忽略的关键事实:
Diagnostics middleware like
DeveloperExceptionPageuses compiled Razor views. After updating the*.cshtmlfile you must run the RazorPageGenerator tool to generate an updated compiled Razor view.
也就是说:
DeveloperExceptionPage这类诊断中间件使用的不是运行时动态编译的 Razor 页面,而是提前编译好的 C# 视图类;- 视图的"源代码"是
*.cshtml,但它只是编译输入; - 一旦修改了
*.cshtml,必须手动运行代码生成工具,产出更新后的编译视图,否则修改不会进入最终的程序集。
文档给出的执行命令(需在工具项目目录内运行):
dotnet run Microsoft.AspNetCore.Diagnostics.RazorViews path-to-aspnetcore-middleware-diagnostics-src其中第一个参数是生成类的根命名空间(Microsoft.AspNetCore.Diagnostics.RazorViews),第二个参数是 Diagnostics 中间件源码目录(即本仓库的src/Middleware/Diagnostics/src)。
这段文档虽然简短,但它背后是一个完整的"预编译 Razor 视图"工程体系。下面我们沿着仓库源码把它彻底讲透。
2. 为什么诊断页面要做成"预编译视图"?
2.1 运行时不依赖任何视图引擎
在 ASP.NET Core 常规的 MVC/Razor Pages 应用中,.cshtml通常在运行时由 Razor 引擎编译或由 SDK 在构建期预编译。而 Diagnostics 中间件是一个自包含的基础设施组件:它需要在异常发生时、甚至在应用自身已处于故障状态时,仍能稳定地输出 HTML 错误页。
从 Microsoft.AspNetCore.Diagnostics.csproj 可以看到该程序集的描述:
ASP.NET Core middleware for exception handling, exception display pages, and diagnostics information. Includes developer exception page middleware, exception handler middleware, runtime info middleware, status code page middleware, and welcome page middleware
它把异常处理、错误展示、状态码页、欢迎页等能力全部打包进一个不引用 Razor 运行时引擎的程序集。视图模板在开发期被编译成普通 C# 类后随程序集分发,渲染时仅需new一个视图对象、调用ExecuteAsync写出 HTML,无需加载任何cshtml文件或触发运行时编译——这正是错误场景下可靠性所要求的。
从该 csproj 中还能看到两个佐证:
<IsAspNetCoreApp>true</IsAspNetCoreApp> <IsTrimmable>true</IsTrimmable>作为Microsoft.AspNetCore.App共享框架的一部分且可裁剪,它不可能在运行时携带解析 Razor 语法所需的整套引擎与文件系统资源;把视图"烧"进程序集是最务实的选择。
2.2 共享的 Razor 视图基础设施
编译后的视图类都继承自同一套抽象基类。csproj 中有如下引用:
<Compile Include="$(SharedSourceRoot)RazorViews\*.cs" />该共享目录 src/Shared/RazorViews/BaseView.cs 定义了抽象基类Microsoft.Extensions.RazorViews.BaseView,它是所有生成视图的运行基座,核心成员包括:
Context、Request、Response:当前HttpContext及 HTTP 对象,供视图内部直接读写(如Response.ContentType、Response.StatusCode);Output:写入缓冲区的TextWriter;HtmlEncoder/UrlEncoder/JavaScriptEncoder:视图内编码 HTML、URL、脚本内容所用;ExecuteAsync(Stream)与ExecuteAsync(HttpContext):先向内存缓冲写入内容、整体编码为 UTF-8(无 BOM)后再拷贝到目标流/响应体。
同目录的 AttributeValue.cs、HelperResult.cs 则提供属性值与辅助片段的承载类型。可以说,这套共享代码定义了生成视图与运行环境之间的全部契约。
3. 仓库里的视图资产布局
Diagnostics 的视图散落在中间件子目录下的Views文件夹中,遵循"源cshtml+ 生成Designer.cs成对存放"的约定:
src/Middleware/Diagnostics/src/ ├── DeveloperExceptionPage/ │ ├── Views/ │ │ ├── ErrorPage.cshtml # 运行时异常页模板(源) │ │ ├── ErrorPage.Designer.cs # 由模板生成的编译视图类 │ │ ├── ErrorPage.css # 页面样式 │ │ ├── ErrorPage.js # 页面前端交互 │ │ ├── ErrorPageModel.cs # 视图模型(页面数据对象) │ │ ├── CompilationErrorPage.cshtml # 编译错误页模板(源) │ │ ├── CompilationErrorPage.Designer.cs │ │ └── CompilationErrorPageModel.cs │ ├── DeveloperExceptionPageMiddlewareImpl.cs │ ├── DeveloperExceptionPageExtensions.cs │ └── DeveloperExceptionPageOptions.cs ├── ExceptionHandler/ # IExceptionHandler 异常处理器 ├── StatusCodePage/ # 状态码页中间件 └── WelcomePage/ └── Views/ ├── WelcomePage.cshtml # 欢迎页模板(源) └── WelcomePage.Designer.cs值得注意的规律是:每个*.cshtml的旁边必定存在同名.Designer.cs,且.Designer.cs是携带// <auto-generated/>标记的机器产物——这与 README 描述的"更新 cshtml 后必须再生成"的工序一一对应。仓库中提交的.Designer.cs就是工具上一次运行的结果,任何人对cshtml的修改最终都要"落"到这些生成文件上才会生效。
3.1 模板如何引用静态资源:<%$ include: %>内联机制
打开 ErrorPage.cshtml 会发现,它没有以<link>、<script src>的方式引用 CSS/JS,而是:
<style> <%$ include: ErrorPage.css %> </style> ... <script> //<!-- <%$ include: ErrorPage.js %> //--> </script><%$ include: 文件名 %>是 RazorPageGenerator 识别的一种编译期内联指令:生成器在编译前读取指令指向的同目录文件,把其文本直接嵌入模板内容。这样样式与脚本会随视图一起被编译进 C# 字符串字面量,最终随程序集整体发布,页面零外部依赖、天然具备离线可用性。关于该指令的处理细节,见下文工具源码剖析(Program.cs 中的ProcessFileIncludes)。
4. 生成产物的形态:从 cshtml 到 C# 类
以编译错误页为例,对比源与产物的对应关系,最能直观理解这套机制。
CompilationErrorPage.cshtml 源模板顶部有一段页面级逻辑:
@{ Response.StatusCode = 500; Response.ContentType = "text/html; charset=utf-8"; Response.ContentLength = null; // Clear any prior Content-Length }而生成的 CompilationErrorPage.Designer.cs 会把这套内容翻译成强类型的视图类,其骨架为:
// <auto-generated/> #pragma warning disable 1591 namespace Microsoft.AspNetCore.Diagnostics.RazorViews { internal class CompilationErrorPage : Microsoft.Extensions.RazorViews.BaseView { public async override global::System.Threading.Tasks.Task ExecuteAsync() { Response.StatusCode = 500; Response.ContentType = "text/html; charset=utf-8"; Response.ContentLength = null; // Clear any prior Content-Length WriteLiteral("<!DOCTYPE html>\r\n<html>\r\n..."); // HTML 静态片段 Write(Resources.ErrorPageHtml_Title); // 表达式/资源 WriteLiteral(@"..."); // 继续输出 } public CompilationErrorPage(CompilationErrorPageModel model) { Model = model; } public CompilationErrorPageModel Model { get; set; } } }几个关键形态特征:
- 类名取自文件名:
ErrorPage.cshtml→internal class ErrorPage,CompilationErrorPage.cshtml→internal class CompilationErrorPage,全部生成在命名空间Microsoft.AspNetCore.Diagnostics.RazorViews下; - 基类固定:均继承
Microsoft.Extensions.RazorViews.BaseView(见 BaseView.cs); - 模板代码被切分为
WriteLiteral(静态 HTML)与Write(动态表达式/资源字符串)调用序列,Razor 的控制流(foreach、if)被保留为原生 C# 语句; - 构造函数接收模型对象(如
CompilationErrorPageModel),视图把渲染所需的数据以强类型字段暴露。
对比 ErrorPage.cshtml 及其对应模型 ErrorPageModel.cs 可以确认:模型承载的是渲染所需的数据集(错误明细、栈帧、Query/Cookie/Header、端点与路由信息),视图则负责把这些数据排版成 HTML。
5. 中间件运行时如何消费这些编译视图
预编译视图不是摆设,它们在中间件处理链路的关键节点被实例化并执行。以 DeveloperExceptionPageMiddlewareImpl.cs 为例,它可以同时处理两类异常:
编译错误(如 Razor 页面编译失败)——构造CompilationErrorPage并执行:
var model = new CompilationErrorPageModel(_options); var errorPage = new CompilationErrorPage(model); if (compilationException.CompilationFailures == null) { return errorPage.ExecuteAsync(context); }运行时异常——从ExceptionDetailsProvider取详情、填充模型后渲染ErrorPage:
var model = new ErrorPageModel { Options = _options, ErrorDetails = _exceptionDetailsProvider.GetDetails(ex), ... Title = title, }; var errorPage = new ErrorPage(model); return errorPage.ExecuteAsync(context);可以看到:中间件里根本不存在"视图查找/模板引擎"这一步,new ErrorPage(model)之后直接ExecuteAsync(context)把 HTML 写入响应——这正是 2.1 节所述的"自包含、高可靠"设计落地后的样子。同理,WelcomePage目录下的 WelcomePage.cshtml 与其生成类也由欢迎页中间件 WelcomePageMiddleware.cs 以相同方式渲染。
此外,csproj 中InternalsVisibleTo声明了对Microsoft.AspNetCore.Diagnostics.Tests的可见性,也说明这些视图类连同中间件实现,是被仓库内部测试直接覆盖验证的(此处不再展开其测试细节)。
6. 工具本体:RazorPageGenerator 源码剖析
README 提到的生成工具位于仓库 src/Middleware/tools/RazorPageGenerator,这是一个仅供内部使用的控制台程序(其 csproj 描述为 "Builds Razor pages for views in a project. For internal use only.",AssemblyName为dotnet-razorpagegenerator,IsShipping为false)。其全部逻辑集中在 Program.cs,下面按流程拆解。
6.1 命令行参数与入口校验
Program.Main对参数做了严格校验,参数不足时输出内嵌用法说明并返回退出码 1:
dotnet razorpagegenerator <root-namespace-of-views> [directory path [#line path prefix]]三个参数的含义:
root-namespace-of-views(必填):生成视图类的根命名空间,对 Diagnostics 即Microsoft.AspNetCore.Diagnostics.RazorViews;directory path(可选,默认当前目录):递归扫描哪个项目目录下的Views子文件夹;#line path prefix(可选):生成文件中#line指令使用的路径前缀,用于屏蔽开发者本机绝对路径。
扫描、生成与写盘过程由MainCore完成,最后打印 "N files successfully generated."。
6.2 编译引擎的定制:CreateProjectEngine
这是整个工具的核心。它基于Microsoft.AspNetCore.Razor.Language的RazorProjectEngine构建了一个高度定制化的编译环境:
RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => { builder .SetNamespace(rootNamespace) // 生成类命名空间 .SetBaseType("Microsoft.Extensions.RazorViews.BaseView") // 固定基类 .ConfigureClass((document, @class) => { @class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath); @class.Modifiers.Clear(); @class.Modifiers.Add("internal"); // 强制 internal }); SectionDirective.Register(builder); // 支持 @section builder.Features.Add(new SuppressChecksumOptionsFeature()); // 去掉 checksum builder.Features.Add(new SuppressMetadataAttributesFeature()); // 去掉元数据特性 builder.AddDefaultImports(@" @using System @using System.Threading.Tasks "); });对照 4 节看到的产物特征,可以一一印证:
SetBaseType("Microsoft.Extensions.RazorViews.BaseView")对应生成类统一的基类;- 类名 = 文件名(不含扩展名)、一律
internal,对应 Designer 文件中的internal class XxxPage; SuppressChecksumOptionsFeature关闭生成代码中的源校验和(checksum),SuppressMetadataAttributesFeature不生成 Razor 元数据特性,两者共同保证每次生成的.Designer.cs内容稳定、可 diff——这对象ErrorPage.cshtml/CompilationErrorPage.cshtml这类需入库、需被评审的生成文件至关重要;- 默认导入
System、System.Threading.Tasks,保证生成类无需逐文件重复using。
6.3 目录扫描与产物落盘:MainCore
var viewDirectories = Directory.EnumerateDirectories(targetProjectDirectory, "Views", SearchOption.AllDirectories);工具会递归搜索目标目录下所有名为Views的文件夹,对其中每个.cshtml/.razor项目项调用GenerateCodeFile。每个文件的处理逻辑为:
var projectItemWrapper = new FileSystemRazorProjectItemWrapper(projectItem, physicalPathPrefix); var codeDocument = projectEngine.Process(projectItemWrapper); var cSharpDocument = codeDocument.GetCSharpDocument(); ... var generatedCodeFilePath = Path.ChangeExtension(projectItem.PhysicalPath, ".Designer.cs");要点:
- 输出文件路径通过
Path.ChangeExtension(..., ".Designer.cs")得到——这正是第 3 节观察到的"cshtml 旁必然躺着一个同名.Designer.cs"的成因; - 即使 Razor 语法诊断存在错误,工具也不会中断,而是打印 "One or more parse errors encountered..." 后继续,便于一次性暴露所有视图的问题;
- 逐个视图在控制台输出 "Generating code file for view X... Done!",便于人工核对产物。
6.4 路径遮蔽与文件内联:FileSystemRazorProjectItemWrapper
GenerateCodeFile并不是直接处理源文件,而是包了一层FileSystemRazorProjectItemWrapper,它承担两项职责:
一是"路径遮蔽",防止开发者本机绝对路径泄漏进提交物:
// Mask the full name since we don't want a developer's local file paths to be committed. PhysicalPath = $"{physicalPathPrefix}{_source.FileName}";即生成代码中的物理路径只保留"前缀 + 文件名"(若指定#line path prefix则为前缀形式),保证仓库内生成文件的#line指向是仓库内统一可见的相对形式。
二是"内联指令展开",对应第 3.1 节的include语法:
var startMatch = "<%$ include: "; var endMatch = " %>"; ... var includeFileName = cshtmlContent.Substring(...); var includeFileContent = File.ReadAllText(Path.Combine(basePath, includeFileName)); cshtmlContent = string.Concat(前段, includeFileContent, 后段);它会循环扫描模板内容,把每一处<%$ include: ErrorPage.css %>形式的指令替换为该文件的实际内容(例如把 ErrorPage.css 与 ErrorPage.js 直接嵌进 ErrorPage.cshtml),随后才把拼接结果交给 Razor 引擎编译。这样静态资源与标记模板在编译期就合为一体。
7. 动手实践:修改视图后的完整再生成流程
回到 README 的指引,把整套流程落到实处。
7.1 触发再生成的场景
只要涉及以下任一视图源的改动,都必须重新运行生成器:
- ErrorPage.cshtml(开发者异常页:栈帧展示、Query/Cookie/Header/路由页签等);
- CompilationErrorPage.cshtml(编译错误页);
- WelcomePage.cshtml(欢迎页);
- 被上述模板内联引用的静态资产,如 ErrorPage.css、ErrorPage.js(它们的内容经由 include 指令被烘焙进生成的
.Designer.cs)。
注意:仅修改.cs(如模型ErrorPageModel.cs)不需要此流程;只有模板类内容发生变化才需要再生成。
7.2 执行命令
依据 README,先在工具项目目录中执行(Windows 路径写法参考):
cd src\Middleware\tools\RazorPageGenerator dotnet run Microsoft.AspNetCore.Diagnostics.RazorViews path-to-aspnetcore-middleware-diagnostics-src其中path-to-aspnetcore-middleware-diagnostics-src应替换为本仓库的 Diagnostics 源码目录,即src/Middleware/Diagnostics/src(实际执行时请替换为绝对路径或相对于工具目录的路径)。
若希望在生成文件的#line指令中统一使用相对路径前缀,可追加第三个参数(对应 Program.cs 帮助文本里的 "#line path prefix"),例如:
dotnet run Microsoft.AspNetCore.Diagnostics.RazorViews <diagnostics-src> ../Views/7.3 运行后应检查什么
运行结束时控制台会输出生成的视图文件数量("N files successfully generated.")。随后建议核对:
- 对应
.Designer.cs的时间戳与内容是否更新:例如修改ErrorPage.cshtml后,ErrorPage.Designer.cs 中的WriteLiteral/Write序列应与模板的新内容一致; .Designer.cs是入库文件:由于它携带// <auto-generated/>标记且会被提交,请在提交时将源模板与生成的 Designer 一并包含,保证仓库内二者始终同步(这正是不再生成就会"改了不生效"的根因);- 不引入本机绝对路径:生成文件中不应出现开发者本机的完整目录路径,只应有文件名或指定的相对前缀;
- 文件编码与可裁剪性:生成类依赖 BaseView.cs 提供的无 BOM UTF-8 写入与缓冲逻辑,无需手工干预。
7.4 常见误区
- 误以为修改 cshtml 后运行
dotnet build即可自动生效:Diagnostics 程序集不引用 Razor 编译目标,构建过程不会把.cshtml编译为视图;必须显式运行 RazorPageGenerator 更新.Designer.cs,改动才会随程序集生效; - 误以为生成工具属于对外 SDK 的一部分:该工具 csproj 中
IsShipping=false、ExcludeFromSourceOnlyBuild=true,定位是仓库内部开发工具,使用它的正确姿势是克隆本仓库后在src/Middleware/tools/RazorPageGenerator目录内按上述命令运行,而非法包后依赖 NuGet 引入。
8. 一条可复现的端到端对照链
把全文串起来,一条"模板 → 生成 → 渲染"的完整链路是:
- 开发者编辑 ErrorPage.cshtml(含
<%$ include: %>内联资源); - 运行
dotnet run Microsoft.AspNetCore.Diagnostics.RazorViews <diagnostics-src>,Program.cs 通过CreateProjectEngine按BaseView基类 + 固定命名空间 +internal规则生成代码,FileSystemRazorProjectItemWrapper负责路径遮蔽与 include 展开,最终在 ErrorPage.Designer.cs 落盘; - 应用运行期抛异常时,DeveloperExceptionPageMiddlewareImpl.cs 组装
ErrorPageModel并new ErrorPage(model); - 视图类基于 BaseView.cs 的
ExecuteAsync把编译好的 HTML 写入响应,浏览器呈现出带栈帧、请求头、Cookie、路由等页签的诊断页面。
这一链路解释了 README 那句要求的全部动机:模板不是运行时资产,而是编译期输入;只有理解了它,才能保证每次修改cshtml后都记得再生成.Designer.cs,让开发者异常页始终反映你的最新改动。
【免费下载链接】aspnetcoreASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.项目地址: https://gitcode.com/GitHub_Trending/as/aspnetcore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考