简介
Razor 不是一种新的 C# 语言,也不是单纯的 HTML 模板。它是一套把 C#、HTML 和标记语法组合起来的语法与编译机制,ASP.NET Core MVC、Razor Pages 和 Blazor 都会用到。
cshtml
<h1>欢迎,@Model.Name</h1>
HTML 负责页面结构,@ 后面的 C# 负责动态内容。Razor 文件编译成 .NET 类型,再由服务器或组件运行时生成页面。
几个概念不要混为一谈:
text
Razor:语法和编译基础
.cshtml:MVC View、Razor Pages 常用文件
.razor:Blazor 组件常用文件
MVC View:Controller 选择视图并传入模型
Razor Pages:以页面为中心,页面和 PageModel 配对
Blazor:组件模型,可做交互式 UI
本文重点讲 ASP.NET Core Razor Pages 和 MVC View 的服务端渲染,通过商品目录 Demo 串起语法、模型绑定、验证、Tag Helper 和安全问题。
Razor 文件如何工作
Razor 文件默认是 HTML。解析器遇到 @ 后,根据上下文切换到 C# 表达式或 Razor 指令:
cshtml
<p>服务器时间:@DateTime.Now</p>
text
.cshtml / .razor 文件
↓ Razor 编译器
生成的 .NET 类型
↓ 运行
HTML 响应或组件 UI
.cshtml 通常由服务器渲染成 HTML;.razor 通常编译成 Blazor 组件,可以静态渲染,也可以启用交互式渲染。Razor 是语法,不等同于 MVC、Razor Pages 或 Blazor 应用模型。
Razor 常用语法
隐式表达式
简单表达式可直接跟在 @ 后面:
cshtml
<p>标题:@Model.Title</p>
<p>价格:@Model.Price.ToString("F2")</p>
显式表达式
表达式包含空格、运算符边界不清,或者容易被解析成 HTML 标签时,用括号标清边界:
cshtml
<p>含税价:@(Model.Price * 1.13m)</p>
<p>七天前:@(DateTime.Now.AddDays(-7))</p>
代码块与控制结构
@{ ... } 执行 C# 语句,但代码块本身不会输出:
cshtml
@{
var isWeekend = DateTime.Today.DayOfWeek is DayOfWeek.Saturday
or DayOfWeek.Sunday;
}
@if (isWeekend)
{
<p>周末模式</p>
}
else
{
<p>工作日模式</p>
}
<ul>
@foreach (var item in Model.Items)
{
<li>@item.Name</li>
}
</ul>
视图适合做格式化、简单判断和循环,不适合放数据库访问和复杂业务逻辑。
输出 @ 和 Razor 注释
cshtml
<p>联系:support@@example.com</p>
@* Razor 注释只留在源文件,不会发送给浏览器 *@
HTML 注释会出现在响应 HTML 中,Razor 注释不会。
常见指令
| 指令 | 用途 |
|---|---|
| @model | 声明 MVC 视图的强类型模型 |
| @page | 声明 Razor Pages 页面路由入口 |
| @using | 引入命名空间 |
| @inject | 向视图注入服务,通常应谨慎使用 |
| @section | 为布局定义页面专属区域 |
| @addTagHelper | 注册可用的 Tag Helper |
实战 Demo:Razor Pages 商品目录和搜索
做一个小商品列表,支持按名称搜索、显示价格和库存,表单使用 Tag Helper 生成字段。
创建项目
shell
dotnet new webapp -n RazorCatalog
cd RazorCatalog
项目核心结构:
text
Pages/
├── Index.cshtml
├── Index.cshtml.cs
├── Shared/
│ └── _Layout.cshtml
├── _ViewImports.cshtml
└── _ViewStart.cshtml
Index.cshtml 是页面标记,Index.cshtml.cs 是对应的 PageModel。模板已经配置好 Razor Pages 中间件和共享布局。
页面模型:Pages/Index.cshtml.cs
csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorCatalog.Pages;
public class IndexModel : PageModel
{
private static readonly List<Product> AllProducts =
[
new(1, "机械键盘", 399m, 12),
new(2, "无线鼠标", 129m, 0),
new(3, "4K 显示器", 1899m, 5),
new(4, "USB-C 扩展坞", 299m, 23)
];
[BindProperty(SupportsGet = true)]
public string? SearchTerm { get; set; }
public IReadOnlyList<Product> Products { get; private set; } =
Array.Empty<Product>();
public void OnGet()
{
Products = string.IsNullOrWhiteSpace(SearchTerm)
? AllProducts
: AllProducts
.Where(product => product.Name.Contains(
SearchTerm,
StringComparison.OrdinalIgnoreCase))
.ToArray();
}
public sealed record Product(int Id, string Name, decimal Price, int Stock);
}
OnGet 处理 GET 请求;SupportsGet 允许 SearchTerm 从查询字符串绑定;Products 是页面使用的强类型数据。静态集合只用于演示,真实项目应由服务或数据库提供数据。
页面标记:Pages/Index.cshtml
cshtml
@page
@model RazorCatalog.Pages.IndexModel
@{
ViewData["Title"] = "商品目录";
}
<h1>@ViewData["Title"]</h1>
<p>Razor Pages 商品搜索示例。</p>
<form method="get">
<label asp-for="SearchTerm">商品名称</label>
<input asp-for="SearchTerm" placeholder="例如:键盘" />
<button type="submit">搜索</button>
</form>
@if (Model.Products.Count == 0)
{
<p>没有找到匹配的商品。</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>商品</th>
<th>价格</th>
<th>库存</th>
<th>状态</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model.Products)
{
<tr>
<td>@product.Name</td>
<td>@product.Price.ToString("C")</td>
<td>@product.Stock</td>
<td>
@if (product.Stock > 0)
{
<span class="text-success">有货</span>
}
else
{
<span class="text-danger">暂时缺货</span>
}
</td>
</tr>
}
</tbody>
</table>
}
<p>搜索条件:@(Model.SearchTerm ?? "全部商品")</p>
启动:
shell
dotnet run
终端会显示本地访问地址。搜索"键盘"后,请求类似 /?SearchTerm=键盘。模型绑定把查询参数写入 SearchTerm,OnGet 完成筛选,Razor 再渲染为 HTML。
表单提交:模型绑定、验证和防伪
搜索适合使用 GET,因为条件放在 URL 中便于分享。新增、修改、删除等改变服务器状态的操作应使用 POST。
新增商品 PageModel
创建 Pages/Products/Create.cshtml.cs:
csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorCatalog.Pages.Products;
public class CreateModel : PageModel
{
[BindProperty]
public InputModel Input { get; set; } = new();
public string? SavedName { get; private set; }
public void OnGet()
{
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
{
return Page();
}
SavedName = Input.Name;
return Page();
}
public sealed class InputModel
{
[Required(ErrorMessage = "商品名称不能为空")]
[StringLength(40, MinimumLength = 2,
ErrorMessage = "名称长度需要在 2 到 40 个字符之间")]
public string Name { get; set; } = string.Empty;
[Range(typeof(decimal), "0.01", "999999",
ErrorMessage = "价格必须大于 0")]
public decimal Price { get; set; }
}
}
创建 Pages/Products/Create.cshtml:
cshtml
@page
@model RazorCatalog.Pages.Products.CreateModel
@{
ViewData["Title"] = "新增商品";
}
<h1>新增商品</h1>
<form method="post">
<div>
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" />
<span asp-validation-for="Input.Name"></span>
</div>
<div>
<label asp-for="Input.Price"></label>
<input asp-for="Input.Price" />
<span asp-validation-for="Input.Price"></span>
</div>
<button type="submit">保存</button>
</form>
@if (Model.SavedName is not null)
{
<p>已接收商品:@Model.SavedName</p>
}
@section Scripts {
<partial name="_ValidationScriptsPartial" />
}
BindProperty 让 POST 表单字段绑定到 Input;DataAnnotations 做基础验证;ModelState.IsValid 检查绑定和验证结果。这个 Demo 只回显名称,没有写数据库。
form method="post" 会由 ASP.NET Core Form Tag Helper 自动生成防伪令牌。Razor Pages 的 POST 表单会验证令牌,降低 CSRF 风险。使用自定义表单生成方式时,需要确认令牌仍然生成并验证。
MVC View 和 Razor Pages 怎么选
两者都用 .cshtml,共享 Razor 语法,但入口不同:
| 项目 | MVC Razor View | Razor Pages |
|---|---|---|
| 入口 | Controller Action | 页面路由和 PageModel |
| 文件组织 | Controller 与 Views 目录对应 | .cshtml 与 .cshtml.cs 配对 |
| 适合场景 | Controller 驱动、复杂路由和动作组织 | 页面为中心的表单和内容站点 |
| 请求处理 | Action 方法 | OnGet、OnPost 等 Handler |
MVC View 可用 @model 接收控制器传入的强类型模型:
cshtml
@model ProductListViewModel
@{
ViewData["Title"] = "商品";
}
<h1>@Model.Title</h1>
Razor Pages 使用 @page 声明可直接路由的页面:
cshtml
@page
@model RazorCatalog.Pages.IndexModel
布局和 Partial
布局统一定义导航、页脚和公共脚本。模板通常使用 Pages/Shared/_Layout.cshtml:
cshtml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - 商品站</title>
</head>
<body>
<header>
<nav>
<a asp-page="/Index">首页</a>
<a asp-page="/Products/Create">新增商品</a>
</nav>
</header>
<main class="container">
@RenderBody()
</main>
<footer>© @DateTime.Now.Year 商品站</footer>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
RenderBody 是页面主体插槽。页面通过 @section Scripts 提供脚本,由 RenderSectionAsync 放进布局指定的位置。_ViewStart.cshtml 可统一设置 Layout = "_Layout"。
重复的展示片段可抽成 Partial,例如商品卡片:
cshtml
@model RazorCatalog.Pages.IndexModel.Product
<article class="product-card">
<h2>@Model.Name</h2>
<p>价格:@Model.Price.ToString("C")</p>
<p>库存:@Model.Stock</p>
</article>
父页面调用:
cshtml
<partial name="_ProductCard" model="product" />
Partial 适合复用展示片段。需要独立查询和组织数据时,应考虑 View Component 或由页面模型准备好数据,而不是把复杂逻辑塞入 Partial。
Tag Helper:让服务端能力贴近 HTML
Tag Helper 让服务端代码参与 HTML 生成,语法仍接近 HTML:
| Tag Helper | 作用 |
|---|---|
| asp-for | 绑定模型属性,生成 name、id 等字段 |
| asp-validation-for | 显示字段验证信息 |
| asp-page | 生成 Razor Pages 链接 |
| asp-controller、asp-action | 生成 MVC 路由链接 |
| asp-route-id | 添加路由参数 |
| Form Tag Helper | 生成表单 action,并为 POST 表单注入防伪令牌 |
| partial | 渲染 Partial 文件 |
Web 项目模板通常在 _ViewImports.cshtml 中注册:
cshtml
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
如果 Tag Helper 没生效,检查 _ViewImports.cshtml 是否覆盖到目标目录、程序集是否正确,以及属性名称是否拼写正确。HTML Helper 仍然存在;Tag Helper 通常更接近 HTML,项目按团队习惯保持一致即可。
安全要点:默认编码和 Html.Raw
Razor 对普通变量表达式默认进行 HTML 编码:
cshtml
@{
var comment = "<script>alert('xss')</script>";
}
<p>@comment</p>
浏览器看到的是编码后的文本,不会把它当脚本执行。这是重要的 XSS 防护。
不要把不可信输入直接交给 Html.Raw:
cshtml
@Html.Raw(Model.UserComment)
Html.Raw 会跳过 HTML 编码。用户输入、数据库内容和查询参数都可能带有恶意标记;来自数据库不等于可信。确实需要展示富文本时,应先使用经过审查的 HTML 清洗流程。
普通 Razor 输出适用于 HTML 文本和多数属性上下文,不应把不可信数据直接拼入 JavaScript 源码。优先经 JSON 序列化,或放进 data-* 属性后再由脚本读取。
Razor 与 Blazor:.cshtml 和 .razor
两类文件共享 Razor 语法,但运行方式不同:
| 文件 | 常见用途 | 运行模型 |
|---|---|---|
| .cshtml | MVC View、Razor Pages | 主要生成服务端 HTML 响应 |
| .razor | Blazor 组件 | 组件树,可静态渲染或启用交互 |
Blazor 组件可以声明参数和泛型:
razor
@typeparam TItem
<h3>@Title</h3>
@code {
[Parameter]
public string Title { get; set; } = string.Empty;
[Parameter]
public IReadOnlyList<TItem> Items { get; set; } = Array.Empty<TItem>();
}
@code、@typeparam 和组件生命周期属于 Blazor 组件模型,不能把 Razor Pages 的 PageModel 生命周期直接套到 Blazor 组件上。
Razor 最终会编译成什么
Razor SDK 在构建时会把 Razor 文件编译成生成类。普通开发不需要查看生成代码;需要排查编译结果时,可在项目文件启用:
xml
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
生成文件在 obj 目录,路径会随目标框架和 SDK 变化。不要手工编辑生成代码。
常见错误
把业务逻辑写进视图
数据库查询、权限决策、计费和复杂筛选应放在 PageModel、Controller 或业务服务中;视图只负责展示。
复杂页面依赖 ViewBag
ViewBag 和 ViewData 适合少量临时信息。页面核心数据使用强类型 ViewModel,编译期更容易发现错误。
把 Razor 当成浏览器端模板
普通 .cshtml 在服务器端执行。页面加载后需要动态更新界面,应使用 JavaScript、HTMX、Blazor 等交互方案。
GET 请求做删除或修改
GET 用于读取;改变状态的操作使用 POST 等合适的 HTTP 方法,并验证防伪令牌和用户权限。
以为 Html.Raw 只是格式化
Html.Raw 会取消编码,对不可信数据使用可能直接制造 XSS 漏洞。
什么时候选择 Razor
Razor Pages 适合页面驱动的传统服务端网站、管理后台和表单应用;MVC View 适合 Controller 驱动、路由和动作组织明显的 Web 应用;Blazor 适合组件化交互 UI。
.cshtml 和 .razor 语法有交集,但请求生命周期、状态管理和交互模式不同。选型应看应用模型,不应只看文件扩展名。
总结
Razor 的基本规则:
text
HTML 默认原样输出
@ 开始 C# 表达式或 Razor 指令
@{ } 执行多行代码
@model / @page 声明模型或页面路由
Tag Helper 根据服务端信息生成 HTML
普通表达式默认 HTML 编码
一张 Razor Pages 的请求链路可以理解为:
text
HTTP 请求
↓
Razor Pages Handler 或 MVC Action
↓
准备强类型模型
↓
Razor 模板执行
↓
编码和 Tag Helper 渲染
↓
返回 HTML
实践中优先使用强类型模型,把业务逻辑留在服务层;表单使用模型绑定和验证;POST 操作保留防伪保护;对不可信文本保持默认编码,谨慎使用 Html.Raw。
参考资料: