别把 Razor 当成几段 HTML:C#.NET Razor Pages、MVC 视图与实战详解

简介

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。

参考资料:

相关推荐
宝桥南山7 小时前
Microsoft Fabric - 简单尝试一下Microsoft Fabric .NET SDK
microsoft·微软·.net·.netcore·powerbi·fabric
专注仿真7 小时前
CMO模型文档-活动单元基类
c#·模型·cmo·活动单元基类
我是苏苏9 小时前
Agent 开发实战 :C#实现语义检索!向量数据库Qdrant的下载安装及调试步骤
c#·ai编程
光泽雨10 小时前
IEnumerable<T> 详细讲解
c#·c
Lost of 程序猿10 小时前
.NET 10 船端单机部署与离线升级实战:从闪退事故到无感回滚
c#·asp.net·.net
淡海水11 小时前
11-04-Unity-Span-foreach-LINQ的分配与热路径边界
unity·c#·linq·foreach·span
慧都小妮子11 小时前
Word/Excel/PPT 如何稳定导出 Markdown?文档 SDK 三线能力拆解
.net·markdown·知识库·aspose·rag·文档转换·文档互操作
唐青枫1 天前
对象到底住在哪里?C#.NET 托管堆从分配、GC 到内存排查
c#·.net
fogota1 天前
Emoji与Segoe MDL2 Assets的比较
c#·wpf