1. 先搞清楚 Razor Pages 到底解决了什么问题,以及它和 MVC 的区别
如果你正在用 ASP.NET Core 开发网站,尤其是那种以页面为中心、交互逻辑不太复杂的应用,比如后台管理系统、内容展示站或者内部工具平台,那么 Razor Pages 绝对值得你优先考虑。它不是一个新东西,但很多人一上来就直奔 MVC,结果把简单的页面逻辑写在了 Controller 里,反而让项目结构变得臃肿。
Razor Pages 的核心价值在于“页面即单元”。在传统的 MVC 模式里,一个功能可能涉及 Controller、Model 和 View 三个文件,逻辑分散。而 Razor Pages 把处理某个特定页面(比如/Products/Edit)的所有东西——HTML 视图(Razor 页面)、页面模型(PageModel)和处理请求的方法(OnGet, OnPost)——都放在了一个.cshtml文件和一个对应的.cshtml.cs文件里。这就像给每个页面分配了一个专属的“小控制器”,代码的归属感更强,维护起来也更直观。
和 Web API 项目相比,Razor Pages 天生就是为了服务端渲染 HTML 页面设计的,它内置了页面模型绑定、表单处理、验证和视图生成,开箱即用。而 Web API 项目更专注于提供数据端点(Endpoints),返回 JSON/XML 等结构化数据。当然,在 ASP.NET Core 里,你完全可以在一个项目中混合使用 Razor Pages 和 Web API 控制器,根据场景选择最合适的工具。
所以,在决定用 Razor Pages 之前,先问自己:我的应用是不是主要由一个个具体的页面组成?每个页面是否有独立的表单提交、数据展示和业务逻辑?如果是,那么 Razor Pages 能让你写得更快,结构更清晰。如果应用的核心是提供一套纯数据接口给移动端或前端框架调用,那么直接从 Web API 项目模板开始可能更直接。
2. 从零开始:创建和运行你的第一个 Razor Pages 项目
理论说再多,不如动手跑起来。我建议直接从命令行开始,这样你对项目结构会有最清晰的认识。
2.1 环境准备与项目创建
首先,确保你安装了 .NET SDK(建议使用长期支持版本,如 .NET 8 或 .NET 9)。打开终端(PowerShell, CMD, 或 Bash),执行以下命令来创建一个新的 Razor Pages 项目:
dotnet new webapp -o MyFirstRazorApp cd MyFirstRazorApp这条命令使用webapp模板创建了一个名为MyFirstRazorApp的 Razor Pages 项目。-o参数指定了输出目录。
创建完成后,用你喜欢的 IDE(如 Visual Studio, VS Code, Rider)打开这个文件夹。我们快速浏览一下核心目录结构:
Pages/:这是 Razor Pages 的心脏。每个子文件夹通常对应一个路由段,里面的.cshtml和.cshtml.cs文件构成一个页面。Index.cshtml&Index.cshtml.cs: 对应网站根路径/。Privacy.cshtml&Privacy.cshtml.cs: 对应/Privacy路径。Shared/: 存放布局页_Layout.cshtml、局部视图等共享组件。_ViewImports.cshtml: 全局导入命名空间,类似 MVC 的。_ViewStart.cshtml: 指定默认布局页。
wwwroot/: 静态资源(CSS, JavaScript, 图片)的家。appsettings.json: 应用配置文件。Program.cs: 应用的入口和服务的配置(ASP.NET Core 6+ 使用最小托管模型,没有Startup.cs了)。
2.2 运行并理解默认页面
在项目根目录下,运行:
dotnet run控制台会输出应用监听的地址(通常是https://localhost:5001和http://localhost:5000)。用浏览器打开它,你会看到一个标准的 Bootstrap 风格的首页。
现在,打开Pages/Index.cshtml.cs文件,这是Index页面的页面模型(PageModel):
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace MyFirstRazorApp.Pages; public class IndexModel : PageModel { private readonly ILogger<IndexModel> _logger; public IndexModel(ILogger<IndexModel> logger) { _logger = logger; } public void OnGet() { // 处理 HTTP GET 请求 } }再看Pages/Index.cshtml文件,这是视图:
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <div class="text-center"> <h1 class="display-4">Welcome</h1> <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> </div>关键点解析:
@page指令:这是 Razor Page 的标识,必须放在第一行(注释除外)。它告诉框架这个文件是一个 Razor Page,而不是普通的 Razor 视图。@model IndexModel:指定这个页面使用的页面模型类型,建立了视图和后台代码的连接。OnGet()方法:当用户通过 GET 请求访问这个页面时,框架会自动调用这个方法。你可以在这里初始化页面数据。同理,处理表单提交会用到OnPost()方法。
这个简单的流程就是 Razor Pages 的基础:请求到来 -> 找到对应页面 -> 执行 PageModel 中的处理器方法(如OnGet)-> 渲染关联的.cshtml视图 -> 返回 HTML。
3. 核心环节实战:创建带表单和数据验证的页面
让我们创建一个有实际功能的页面,比如一个“添加产品”的页面。这会涉及到路由、表单绑定、模型验证和处理器方法。
3.1 创建页面和定义模型
首先,在Pages文件夹下创建一个新的子文件夹Products。然后,在Products文件夹里添加一个新的 Razor Page,可以右键添加,也可以用命令dotnet new page -n Create -o Pages/Products。你会得到Create.cshtml和Create.cshtml.cs。
我们先定义页面模型。编辑Create.cshtml.cs:
using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace MyFirstRazorApp.Pages.Products; public class CreateModel : PageModel { // 这是一个绑定属性,用于接收表单数据 [BindProperty] public ProductInputModel Product { get; set; } = new(); // 用于在 GET 请求时展示页面 public void OnGet() { } // 用于处理表单的 POST 提交 public IActionResult OnPost() { // 检查模型状态是否有效(即是否通过数据注解验证) if (!ModelState.IsValid) { // 如果验证失败,返回当前页面,页面上会显示验证错误信息 return Page(); } // 验证通过,这里通常是保存数据到数据库的逻辑 // 例如:_productService.Create(Product); _logger.LogInformation("Creating product: {Name}", Product.Name); // 重定向到其他页面(比如产品列表页),防止表单重复提交 return RedirectToPage("./Index"); } // 内部类,定义表单的数据结构 public class ProductInputModel { [Required(ErrorMessage = "产品名称是必填项")] [StringLength(100, MinimumLength = 3)] public string Name { get; set; } = string.Empty; [Required] [DataType(DataType.Currency)] [Range(0.01, 10000)] public decimal Price { get; set; } [StringLength(500)] public string? Description { get; set; } } }关键点解析:
[BindProperty]:这个特性至关重要。它告诉模型绑定器,在 POST 请求时,将表单数据绑定到Product属性上。没有它,OnPost方法里的Product会是null。- 数据注解(
[Required],[StringLength]等):在属性上定义验证规则。这些规则会在模型绑定后自动被框架验证,结果存储在ModelState中。 OnPost()方法:返回值是IActionResult。ModelState.IsValid检查验证结果。如果失败,return Page();会重新渲染当前页面,并且因为ModelState包含了错误信息,视图中的验证标签助手会自动显示这些错误。如果成功,则重定向(RedirectToPage),这是处理 POST 请求后避免重复提交的标准做法(Post-Redirect-Get 模式)。
3.2 构建表单视图
现在,编辑Create.cshtml来构建表单:
@page @model MyFirstRazorApp.Pages.Products.CreateModel @{ ViewData["Title"] = "添加产品"; } <h1>@ViewData["Title"]</h1> <form method="post"> <div class="form-group"> <label asp-for="Product.Name" class="control-label"></label> <input asp-for="Product.Name" class="form-control" /> <span asp-validation-for="Product.Name" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Product.Price" class="control-label"></label> <input asp-for="Product.Price" class="form-control" /> <span asp-validation-for="Product.Price" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Product.Description" class="control-label"></label> <textarea asp-for="Product.Description" class="form-control"></textarea> <span asp-validation-for="Product.Description" class="text-danger"></span> </div> <div class="form-group mt-3"> <button type="submit" class="btn btn-primary">提交</button> <a asp-page="./Index" class="btn btn-secondary">取消</a> </div> </form> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }关键点解析:
- 标签助手(Tag Helpers):
asp-for,asp-validation-for,asp-page这些是 Razor Pages 的利器。它们在服务器端渲染成标准的 HTML,但提供了强类型和智能提示。asp-for="Product.Name":会自动设置input的id,name属性,并与模型属性关联。asp-validation-for="Product.Name":会自动显示该属性关联的验证错误信息。asp-page="./Index":生成指向另一个 Razor Page 的正确链接。
- 验证脚本:
@section Scripts部分引入了 jQuery 非侵入式验证的脚本,使得客户端验证生效(在输入时即时提示)。这个_ValidationScriptsPartial是模板自带的。
现在运行项目,导航到/Products/Create,尝试提交空表单或无效数据,你会看到客户端和服务器端的验证都在工作。这是一个完整的、具有生产级验证功能的页面。
4. 深入理解路由、处理器方法与依赖注入
4.1 灵活的路由配置
默认情况下,页面的路由由其在Pages文件夹下的路径决定。Pages/Products/Create.cshtml对应路由/Products/Create。但你可以在@page指令中自定义:
@page "/goods/new-item"这样,页面就通过/goods/new-item访问,而不再是/Products/Create。你还可以添加路由参数:
@page "/Products/Edit/{id:int}"然后在 PageModel 中接收它:
public void OnGet(int id) { // 根据 id 获取产品信息 }4.2 多个处理器方法
一个页面不只有OnGet和OnPost。你可以有多个处理器方法来处理不同的操作。例如,一个页面有两个表单:
public IActionResult OnPostSave() { ... } // 处理“保存”按钮 public IActionResult OnPostDelete() { ... } // 处理“删除”按钮在视图中,通过表单的asp-page-handler来指定:
<form method="post" asp-page-handler="Save"> <form method="post" asp-page-handler="Delete">4.3 依赖注入(DI)的使用
ASP.NET Core 内置了强大的依赖注入容器。在 Razor Pages 中,你可以通过构造函数注入所需服务。这在Program.cs中配置。
例如,假设我们有一个IProductService:
- 在
Program.cs中注册服务:builder.Services.AddScoped<IProductService, ProductService>(); - 在 PageModel 中注入并使用:
public class IndexModel : PageModel { private readonly IProductService _productService; public List<Product> Products { get; set; } public IndexModel(IProductService productService) { _productService = productService; } public void OnGet() { Products = _productService.GetAllProducts(); } } - 在视图中显示:
@foreach (var product in Model.Products) { <p>@product.Name - @product.Price</p> }
这是将业务逻辑与页面表现分离的推荐做法,使 PageModel 保持精简,只负责协调视图和数据。
5. 常见问题排查与进阶实践要点
在实际开发中,你肯定会遇到一些典型问题。下面是我总结的几个高频排查点和进阶建议。
5.1 常见问题排查清单
页面返回 404?
- 首先检查文件位置和命名:页面文件必须在
Pages目录或其子目录下,且包含@page指令。 - 检查路由:是否在
@page指令中自定义了路由?访问的 URL 是否匹配? - 检查编译:项目是否成功编译?一个编译错误可能导致所有页面路由失效。
- 首先检查文件位置和命名:页面文件必须在
表单提交后,
[BindProperty]的属性为null?- 99% 的情况是表单字段的
name属性与模型属性路径不匹配。务必使用asp-for标签助手来生成表单控件,它会自动设置正确的name。手动写 HTML 很容易出错。 - 检查
OnPost方法是否被正确调用(是否有同名冲突?)。 - 检查模型属性是否是
public且有get; set;。
- 99% 的情况是表单字段的
验证总是失败(
ModelState.IsValid为 false),但看不出错误?- 在
OnPost方法内设置断点,检查ModelState的Errors集合。里面会有具体的错误信息。 - 常见原因:客户端验证脚本未加载(检查
_ValidationScriptsPartial是否引入);模型属性类型不匹配(如向int字段输入了文本)。
- 在
布局(Layout)或样式不生效?
- 检查
_ViewStart.cshtml文件是否指定了正确的布局页路径。 - 检查静态资源(CSS/JS)的路径。在 Razor 页面中,引用
wwwroot下的资源应使用~/路径,如<link rel="stylesheet" href="~/css/site.css" />。
- 检查
5.2 关于“启用远程验证(Remote Validation)”
搜索热词中提到了“asp.net core 如何启用远程验证 remote”。这在 Razor Pages 中同样适用。远程验证允许你在用户输入时,调用服务器端的一个方法来验证字段的唯一性(如用户名、邮箱是否已存在)。
假设我们要验证产品名称是否唯一:
- 在 PageModel 中创建一个用于远程验证的 Action 方法:
[AcceptVerbs("GET", "POST")] public IActionResult VerifyProductName(string name) { if (_productService.ProductNameExists(name)) { return Json($"产品名称 '{name}' 已存在。"); } return Json(true); } - 在模型属性上添加
[Remote]特性:[Required] [Remote(action: "VerifyProductName", pageHandler: null, HttpMethod = "GET")] public string Name { get; set; } = string.Empty;action参数指向上面那个方法名。注意,远程验证需要引入 jQuery 和 jQuery 验证脚本。
5.3 与 ASP.NET Core 9 及未来版本的兼容性
ASP.NET Core 的更新通常非常注重向后兼容。从 .NET 8 到 .NET 9,Razor Pages 的核心编程模型(PageModel, Tag Helpers, 路由)预计不会有颠覆性变化。主要升级可能集中在性能优化、新的 API 集成(如新的 Blazor 渲染模式)、以及底层 .NET 运行时的改进。
我的建议是:在开始一个新项目时,直接使用最新的长期支持(LTS)版本或当前稳定版。现有项目升级时,仔细阅读官方升级指南,重点关注Program.cs的配置方式、中间件顺序以及任何被标记为过时(Obsolete)的 API。Razor Pages 本身作为一个成熟的页面模型,其核心概念是稳定的。
5.4 生产环境考量
当你的 Razor Pages 应用要从学习走向生产时,需要关注以下几点:
- 配置管理:将连接字符串、API 密钥等敏感信息移出代码,使用
appsettings.{Environment}.json或环境变量,并通过IConfiguration接口读取。 - 日志记录:充分利用 ASP.NET Core 内置的日志系统(ILogger),在关键位置记录信息、警告和错误。
- 错误处理:使用
UseExceptionHandler中间件配置自定义错误处理页面,避免向用户暴露堆栈跟踪。 - 安全性:
- 始终使用 HTTPS。
- 注意防范跨站请求伪造(CSRF)。Razor Pages 表单中,
<form>标签助手默认会生成防伪令牌(<input type=”hidden” name=”__RequestVerificationToken”>),在OnPost方法上通常有[ValidateAntiForgeryToken]特性(默认隐式启用),务必保留。 - 对用户输入进行严格的验证和编码输出,防止 XSS 攻击。
- 性能:对于复杂的数据查询,考虑使用异步处理器方法(
OnGetAsync,OnPostAsync)以避免阻塞线程。
Razor Pages 提供了一条清晰、高效的路径来构建服务端渲染的 Web 应用。它的学习曲线平缓,尤其适合从传统 Web Forms 过渡或希望快速构建功能页面的开发者。关键在于理解“页面即单元”的思想,并熟练运用 PageModel、标签助手和模型绑定这三个核心武器。先从简单的 CRUD 页面做起,逐步引入更复杂的组件和架构模式,你会发现用它来组织以页面为核心的业务逻辑非常得心应手。