ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ASP.NET Core Razor Pages 实战:从核心概念到完整网站构建

ASP.NET Core Razor Pages 实战:从核心概念到完整网站构建 大家好我是长期分享.NET技术栈的博主。在Web开发领域ASP.NET Core以其高性能和跨平台特性成为构建现代Web应用的首选框架之一。而Razor Pages作为其内置的页面编程模型相较于传统的MVC模式为构建以页面为中心的Web UI提供了更简洁、更直观的解决方案。很多刚接触ASP.NET Core的开发者面对MVC和Razor Pages可能会感到困惑不清楚如何选择或者在使用Razor Pages时遇到布局、数据绑定、表单处理等实际问题。本文将系统性地拆解ASP.NET Core Razor Pages从核心概念到项目实战手把手带你构建一个功能完整的网站并深入探讨最佳实践与常见问题确保无论是新手入门还是有经验的开发者优化项目结构都能从中获益。1. 背景与核心概念为什么选择Razor Pages在深入代码之前理解Razor Pages的定位和优势至关重要。它并非一个全新的框架而是构建在ASP.NET Core MVC基础之上的一种更高效的页面组织模式。1.1 什么是Razor PagesRazor Pages是一种基于页面的编程模型它将处理HTTP请求的代码PageModel类和呈现HTML的视图.cshtml文件紧密地耦合在一起。每个可访问的页面通常由一对文件组成一个.cshtml文件Razor视图和一个与之同名的.cshtml.cs文件PageModel类。这种“页面-模型”对Page-Model Pair的设计让处理特定页面逻辑的代码如数据获取、表单处理和UI渲染逻辑物理上相邻极大地提升了代码的可维护性和可读性。1.2 Razor Pages vs MVC如何选择这是开发者最常问的问题。简单来说MVC (Model-View-Controller)适用于大型、复杂的应用程序其中业务逻辑需要被多个视图View复用或者你需要严格的关注点分离。Controller作为中心调度器处理多个相关请求模型Model和视图View之间通过Controller连接。Razor Pages非常适合构建以内容为中心的网站、后台管理系统、数据驱动型表单页面。它将每个页面视为一个独立的单元页面自身的处理逻辑OnGet, OnPost直接内嵌在PageModel中。这减少了Controller和Action方法的“胶水代码”使得页面逻辑更加内聚。如果你的应用主要由一个个独立的页面组成如“关于我们”、“联系我们”、“产品列表”、“产品详情”那么Razor Pages通常是更简单、更直接的选择。ASP.NET Core项目模板也默认推荐使用Razor Pages来创建Web UI。1.3 核心架构组件理解以下几个核心组件是掌握Razor Pages的关键PageModel类每个页面的“后台代码”。它包含属性用于向视图传递数据和处理程序方法如OnGet用于初始化页面OnPost用于处理表单提交。Razor视图 (.cshtml)使用Razor语法混合HTML和C#代码负责UI渲染。它可以通过model指令绑定到对应的PageModel。路由默认情况下Razor Pages的文件系统结构即路由。例如位于/Pages/Products/Index.cshtml的页面可以通过URL/Products或/Products/Index访问。你也可以通过page指令自定义路由。布局 (Layout)和部分视图 (Partial Views)用于保持网站UI的一致性与MVC中的概念和用法相同。2. 环境准备与版本说明在开始实战前请确保你的开发环境已就绪。本文将使用当前最新的长期支持版LTS进行演示但核心概念适用于ASP.NET Core 3.1及更高版本。操作系统Windows 10/11, macOS, 或 Linux (如Ubuntu)开发环境.NET SDK请安装.NET 8.0 SDK或更高版本。你可以从 微软官网 下载。安装后在命令行运行dotnet --info确认版本。集成开发环境 (IDE)Visual Studio 2022(社区版免费)对Windows用户提供最完整的开发体验内置项目模板和调试工具。Visual Studio Code跨平台轻量级编辑器需安装C#扩展包。本文示例将主要使用命令行因此与IDE选择无关。项目结构我们将创建一个标准的ASP.NET Core Web应用项目。3. 核心语法、配置与原理拆解让我们深入Razor Pages的几个核心机制理解其工作原理。3.1page指令与路由page指令是Razor Pages文件的灵魂它必须放在.cshtml文件的第一行。它做了两件事将该文件标记为一个Razor Page而不是普通的Razor视图。定义该页面的路由。如果不指定参数则使用基于文件路径的默认路由。// 在 /Pages/Contact.cshtml 顶部 page // 默认路由为 /Contact model ContactModel h1联系我们/h1 // 在 /Pages/Products/Details.cshtml 顶部 page // 默认路由为 /Products/Details model DetailsModel // 自定义路由模板 page /product/{id:int} // 路由为 /product/5 并约束id为整数 model ProductDetailModel为什么这么做自定义路由让你可以创建更友好、更符合SEO规范的URL而不受物理文件夹结构的限制。3.2 PageModel 与处理程序方法PageModel类包含了页面的状态和处理逻辑。处理程序方法以On为前缀后跟HTTP动词如Get, Post, Put, Delete。// 文件Pages/Index.cshtml.cs using Microsoft.AspNetCore.Mvc.RazorPages; namespace MyWebApp.Pages { public class IndexModel : PageModel { // 公共属性用于向视图传递数据 public string Message { get; set; } public DateTime CurrentTime { get; set; } // GET请求处理程序初始化页面数据 public void OnGet() { Message 欢迎来到我的网站; CurrentTime DateTime.Now; } // POST请求处理程序处理表单提交 // 通过 [BindProperty] 特性将表单数据绑定到属性 [BindProperty] public UserInput Input { get; set; } public IActionResult OnPost() { if (!ModelState.IsValid) { // 验证失败返回当前页面并显示错误信息 return Page(); } // 处理有效数据例如保存到数据库 // ... // 重定向到其他页面防止重复提交 return RedirectToPage(/Success); } } public class UserInput { [Required] public string Name { get; set; } [EmailAddress] public string Email { get; set; } } }对应的视图Index.cshtml通过model IndexModel绑定并可以直接使用Model.Message和Model.CurrentTime。3.3 模型绑定与验证Razor Pages内置了强大的模型绑定和验证功能。如上例所示使用[BindProperty]特性可以将POST请求中的表单字段自动绑定到PageModel的属性上。配合数据注解如[Required],[EmailAddress],[StringLength]可以轻松实现客户端和服务端双重验证。在视图中使用asp-validation-for和asp-validation-summaryTag Helper可以自动渲染验证错误信息。3.4 依赖注入 (DI)ASP.NET Core的核心是依赖注入。在Razor Pages中你可以通过构造函数直接将服务注入到PageModel中。public class ProductsModel : PageModel { private readonly IProductRepository _productRepository; private readonly ILoggerProductsModel _logger; // 构造函数注入 public ProductsModel(IProductRepository productRepository, ILoggerProductsModel logger) { _productRepository productRepository; _logger logger; } public ListProduct ProductList { get; set; } public async Task OnGetAsync() { _logger.LogInformation(正在获取产品列表...); ProductList await _productRepository.GetAllAsync(); } }这种设计使得单元测试变得非常容易因为你可以轻松地用Mock对象替换实际的服务。4. 完整实战案例构建一个简单的产品展示网站现在我们将综合运用以上知识构建一个具有列表、详情、添加和搜索功能的产品网站。4.1 创建项目与基础结构打开命令行执行以下命令dotnet new webapp -n ProductWebsite -o ProductWebsite cd ProductWebsite这会创建一个使用Razor Pages的ASP.NET Core Web应用模板。项目结构如下ProductWebsite/ ├── Pages/ │ ├── Shared/ # 布局、部分视图 │ │ ├── _Layout.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── Index.cshtml # 主页 │ ├── Index.cshtml.cs │ ├── Privacy.cshtml │ └── Error.cshtml ├── wwwroot/ # 静态资源CSS, JS, 图片 ├── appsettings.json # 配置文件 └── Program.cs # 应用入口和服务配置4.2 定义数据模型与模拟仓储库为了简化我们不使用真实数据库而是创建一个内存中的产品列表。首先创建Models文件夹和Product类// 文件Models/Product.cs namespace ProductWebsite.Models { public class Product { public int Id { get; set; } [Required, StringLength(100)] public string Name { get; set; } [DataType(DataType.Currency)] public decimal Price { get; set; } public string Description { get; set; } public string Category { get; set; } } }接着创建服务接口和实现// 文件Services/IProductRepository.cs using ProductWebsite.Models; namespace ProductWebsite.Services { public interface IProductRepository { TaskListProduct GetAllAsync(); TaskProduct? GetByIdAsync(int id); Task AddAsync(Product product); TaskListProduct SearchAsync(string keyword); } } // 文件Services/InMemoryProductRepository.cs using ProductWebsite.Models; namespace ProductWebsite.Services { public class InMemoryProductRepository : IProductRepository { private static ListProduct _products new ListProduct { new Product { Id 1, Name 无线鼠标, Price 99.99M, Category电子产品, Description高性能无线鼠标 }, new Product { Id 2, Name 机械键盘, Price 299.00M, Category电子产品, Description青轴机械键盘 }, new Product { Id 3, Name 编程书籍, Price 89.00M, Category图书, DescriptionC#高级编程 } }; private static int _nextId 4; public TaskListProduct GetAllAsync() Task.FromResult(_products); public TaskProduct? GetByIdAsync(int id) Task.FromResult(_products.FirstOrDefault(p p.Id id)); public Task AddAsync(Product product) { product.Id _nextId; _products.Add(product); return Task.CompletedTask; } public TaskListProduct SearchAsync(string keyword) { if (string.IsNullOrWhiteSpace(keyword)) return GetAllAsync(); var result _products.Where(p p.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase) || p.Description.Contains(keyword, StringComparison.OrdinalIgnoreCase) || p.Category.Contains(keyword, StringComparison.OrdinalIgnoreCase) ).ToList(); return Task.FromResult(result); } } }在Program.cs中注册此服务// 文件Program.cs using ProductWebsite.Services; var builder WebApplication.CreateBuilder(args); // 添加服务到容器 builder.Services.AddRazorPages(); // 注册我们的仓储库为单例服务 builder.Services.AddSingletonIProductRepository, InMemoryProductRepository(); var app builder.Build(); // ... 后续配置中间件4.3 创建产品列表页 (Index)我们将修改默认的Index页面来展示产品列表和搜索框。首先更新PageModel// 文件Pages/Index.cshtml.cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using ProductWebsite.Models; using ProductWebsite.Services; namespace ProductWebsite.Pages { public class IndexModel : PageModel { private readonly IProductRepository _productRepository; private readonly ILoggerIndexModel _logger; public IndexModel(IProductRepository productRepository, ILoggerIndexModel logger) { _productRepository productRepository; _logger logger; } public ListProduct Products { get; set; } new(); [BindProperty(SupportsGet true)] // 支持从查询字符串绑定 public string SearchString { get; set; } public async Task OnGetAsync() { _logger.LogInformation(加载产品列表搜索关键词{SearchString}, SearchString); if (!string.IsNullOrEmpty(SearchString)) { Products await _productRepository.SearchAsync(SearchString); } else { Products await _productRepository.GetAllAsync(); } } } }然后更新Razor视图page model IndexModel { ViewData[Title] 产品首页; } div classrow mb-3 div classcol-md-6 h1 classdisplay-4产品列表/h1 /div div classcol-md-6 text-end a asp-page/Products/Create classbtn btn-primary添加新产品/a /div /div !-- 搜索框 -- form methodget div classinput-group mb-4 input typetext classform-control asp-forSearchString placeholder输入产品名称、描述或类别进行搜索... button classbtn btn-outline-secondary typesubmit搜索/button if (!string.IsNullOrEmpty(Model.SearchString)) { a asp-page./Index classbtn btn-outline-danger清除/a } /div /form !-- 产品列表 -- if (Model.Products.Any()) { div classrow row-cols-1 row-cols-md-3 g-4 foreach (var product in Model.Products) { div classcol div classcard h-100 div classcard-body h5 classcard-titleproduct.Name/h5 h6 classcard-subtitle mb-2 text-mutedproduct.Category/h6 p classcard-textproduct.Description/p p classcard-textstrong价格/strongproduct.Price.ToString(C)/p /div div classcard-footer text-end a asp-page/Products/Details asp-route-idproduct.Id classbtn btn-sm btn-outline-primary查看详情/a /div /div /div } /div } else { div classalert alert-info rolealert 没有找到相关产品。(string.IsNullOrEmpty(Model.SearchString) ? : $搜索关键词{Model.SearchString}) /div }4.4 创建产品详情页 (Details)在Pages文件夹下创建Products子文件夹然后添加Details.cshtml和Details.cshtml.cs。PageModel:// 文件Pages/Products/Details.cshtml.cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using ProductWebsite.Services; namespace ProductWebsite.Pages.Products { public class DetailsModel : PageModel { private readonly IProductRepository _productRepository; public DetailsModel(IProductRepository productRepository) { _productRepository productRepository; } public Models.Product Product { get; set; } public async TaskIActionResult OnGetAsync(int id) { Product await _productRepository.GetByIdAsync(id); if (Product null) { return NotFound(); // 返回404状态码 } return Page(); } } }视图page {id:int} model ProductWebsite.Pages.Products.DetailsModel { ViewData[Title] 产品详情; } div classcontainer mt-4 nav aria-labelbreadcrumb ol classbreadcrumb li classbreadcrumb-itema asp-page/Index首页/a/li li classbreadcrumb-item active aria-currentpageModel.Product.Name/li /ol /nav div classrow div classcol-md-8 h1Model.Product.Name/h1 p classlead类别Model.Product.Category/p hr / h4产品描述/h4 pModel.Product.Description/p /div div classcol-md-4 div classcard div classcard-body h5 classcard-title价格/h5 p classdisplay-6 text-primaryModel.Product.Price.ToString(C)/p div classd-grid gap-2 a asp-page/Index classbtn btn-outline-secondary返回列表/a /div /div /div /div /div /div注意page {id:int}它定义了一个路由参数id并约束其为整数。URL形如/Products/Details/1。4.5 创建添加产品页 (Create)同样在Pages/Products/文件夹下创建Create.cshtml和Create.cshtml.cs。PageModel:// 文件Pages/Products/Create.cshtml.cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using ProductWebsite.Models; using ProductWebsite.Services; namespace ProductWebsite.Pages.Products { public class CreateModel : PageModel { private readonly IProductRepository _productRepository; private readonly ILoggerCreateModel _logger; public CreateModel(IProductRepository productRepository, ILoggerCreateModel logger) { _productRepository productRepository; _logger logger; } [BindProperty] // 绑定表单数据 public Product Product { get; set; } public void OnGet() { // 初始化页面可以设置默认值 } public async TaskIActionResult OnPostAsync() { // 服务端验证 if (!ModelState.IsValid) { return Page(); } try { await _productRepository.AddAsync(Product); _logger.LogInformation(新产品已添加{ProductName} (ID: {ProductId}), Product.Name, Product.Id); // 使用TempData传递成功消息到下一个请求重定向后 TempData[SuccessMessage] $产品 {Product.Name} 添加成功; return RedirectToPage(/Index); } catch (Exception ex) { _logger.LogError(ex, 添加产品时发生错误); ModelState.AddModelError(string.Empty, 添加产品失败请重试或联系管理员。); return Page(); } } } }视图 (使用Tag Helper简化表单生成和验证)page model ProductWebsite.Pages.Products.CreateModel { ViewData[Title] 添加新产品; } div classcontainer mt-4 h2ViewData[Title]/h2 hr / form methodpost div asp-validation-summaryModelOnly classtext-danger/div div classform-group mb-3 label asp-forProduct.Name classform-label/label input asp-forProduct.Name classform-control / span asp-validation-forProduct.Name classtext-danger/span /div div classform-group mb-3 label asp-forProduct.Price classform-label/label input asp-forProduct.Price classform-control / span asp-validation-forProduct.Price classtext-danger/span /div div classform-group mb-3 label asp-forProduct.Category classform-label/label input asp-forProduct.Category classform-control / span asp-validation-forProduct.Category classtext-danger/span /div div classform-group mb-3 label asp-forProduct.Description classform-label/label textarea asp-forProduct.Description classform-control rows4/textarea span asp-validation-forProduct.Description classtext-danger/span /div div classform-group button typesubmit classbtn btn-primary保存/button a asp-page/Index classbtn btn-secondary取消/a /div /form /div section Scripts { {await Html.RenderPartialAsync(_ValidationScriptsPartial);} }注意视图底部的section Scripts它引入了客户端验证所需的JavaScript库jQuery Unobtrusive Validation这是由项目模板的_ValidationScriptsPartial.cshtml提供的。4.6 运行与验证在项目根目录运行dotnet run或使用Visual Studio按F5启动。打开浏览器访问https://localhost:5001或http://localhost:5000。首页应显示产品列表并可以使用搜索框。点击“查看详情”应跳转到产品详情页。点击“添加新产品”进入表单页填写信息并提交成功后应跳回首页并看到新添加的产品可能需要刷新。如果表单验证失败错误信息会显示在对应字段下方。5. 常见问题与排查思路在开发Razor Pages应用时你可能会遇到以下典型问题问题现象常见原因解决思路404 - 页面未找到1. 文件未放在Pages文件夹下或其子文件夹中。2. 文件名或文件夹名拼写错误。3.page指令缺失或路由模板配置错误。4. 未在Program.cs中调用app.MapRazorPages()。1. 确认物理路径符合约定如Pages/Products/Index.cshtml。2. 检查page指令是否位于.cshtml文件首行。3. 在Program.cs的app构建后检查是否有app.MapRazorPages()。模型绑定失败表单数据为null1. PageModel中的属性未标记[BindProperty]。2. 表单字段的name属性与PageModel属性名不匹配。3. 处理POST请求的方法不是OnPost或OnPostAsync。1. 为需要绑定的属性添加[BindProperty]。2. 使用Tag Helperasp-for自动生成正确的name和id。3. 确保处理POST的方法签名正确。客户端验证不生效1. 视图中未引入验证脚本。2. 模型类未使用数据注解特性如[Required]。3._ValidationScriptsPartial.cshtml未被渲染。1. 在表单所在视图的section Scripts中调用{await Html.RenderPartialAsync(_ValidationScriptsPartial);}。2. 确保模型属性有正确的数据注解。依赖注入的服务为null1. 服务未在Program.cs中注册。2. PageModel构造函数参数类型与注册的服务类型不匹配。3. 尝试在PageModel构造函数之外如属性初始化器使用注入的服务。1. 检查builder.Services.AddXxx()语句。2. 确保构造函数参数类型是接口如IProductRepository而非具体类除非注册的是具体类。3. 服务只能在构造函数注入后使用。TempData消息不显示1. 在读取TempData之前已经发生了另一个请求。2. 未在Program.cs中启用CookieTempDataProvider新模板默认已启用。1.TempData基于Session或Cookie只在下一个请求中有效。确保重定向后立即读取。2. 检查是否调用了builder.Services.AddRazorPages().AddCookieTempDataProvider()非必须但可配置。布局Layout未应用1. 视图文件中未设置Layout属性。2._ViewStart.cshtml文件被删除或修改。1. 在视图顶部使用{ Layout _Layout; }显式指定或依赖_ViewStart.cshtml的默认设置。2. 检查Pages目录下的_ViewStart.cshtml文件是否存在。6. 最佳实践与工程建议将Razor Pages用于生产级项目时遵循以下实践可以提升代码质量、安全性和可维护性。6.1 项目组织与结构按功能组织将相关的页面Page、PageModel、视图组件、服务接口和实现放在同一功能文件夹下。例如所有与订单相关的文件可以放在Features/Order目录下而不仅仅是Pages/Orders。这需要配合自定义的RazorPagesOptions来配置根目录。分离关注点即使PageModel将逻辑和视图关联也应遵循单一职责原则。将复杂的数据访问、业务规则、外部API调用抽象到独立的服务类Service或应用层中PageModel只负责协调和视图模型ViewModel的准备。使用视图模型ViewModels避免将领域模型Domain Model直接暴露给视图。为复杂的页面创建专用的视图模型ViewModel只包含视图渲染所需的数据。这提高了安全性和灵活性。6.2 安全考量过度发布攻击Overposting使用[BindProperty]时恶意用户可能提交表单中不存在的字段来修改你不希望更新的属性。对策是使用[BindProperty]的Include或Exclude参数或者更安全地使用输入模型Input Model即专门为表单提交创建一个类而不是绑定到完整的领域模型。public class CreateProductInput { [Required] public string Name { get; set; } // 只包含允许用户提交的字段 }跨站请求伪造CSRFRazor Pages默认启用了防伪令牌验证。确保你的表单中使用了form methodpostTag Helper会自动添加__RequestVerificationToken隐藏域。切勿对GET请求进行状态更改操作。输入验证始终同时使用客户端和服务端验证。客户端验证提供即时反馈服务端验证是安全必须的最后防线。使用内置的数据注解或IValidatableObject接口进行复杂验证。6.3 性能优化异步编程所有涉及I/O操作数据库查询、文件读写、网络请求的处理程序方法都应使用异步模式async/await如OnGetAsync、OnPostAsync。这能提高应用程序的吞吐量和可伸缩性。缓存策略对于不常变化的数据考虑使用响应缓存[ResponseCache]特性或内存缓存IMemoryCache来减少数据库查询和页面渲染开销。捆绑与压缩使用ASP.NET Core的静态文件中间件或前端构建工具如Webpack对CSS和JavaScript文件进行捆绑和压缩减少HTTP请求数量和传输大小。6.4 可测试性依赖注入如前所述通过构造函数注入依赖使得在单元测试中能够轻松使用Mock对象替换真实服务。分离逻辑将核心业务逻辑从PageModel中抽离到独立的、可测试的类中。PageModel应保持“瘦”主要职责是处理HTTP上下文和协调服务调用。使用PageContext和HttpContext避免在PageModel中直接使用静态HttpContext访问器。通过PageContext属性访问路由数据、模型状态等这更利于测试。6.5 配置与部署环境配置利用appsettings.{Environment}.json文件如appsettings.Production.json来管理不同环境开发、测试、生产的配置如数据库连接字符串、API密钥。健康检查在生产环境中添加健康检查端点通过AddHealthChecks()和MapHealthChecks()方便监控系统状态。错误处理在开发环境使用开发者异常页在生产环境务必使用自定义错误页面UseExceptionHandler中间件并记录详细的异常信息到日志系统避免向用户暴露敏感信息。掌握Razor Pages是高效构建ASP.NET Core Web UI的关键一步。它通过页面为中心的模型简化了开发流程但又不失灵活性和强大功能。从简单的CRUD页面到复杂的数据看板Razor Pages都能胜任。建议在理解本文示例的基础上尝试集成真实的数据库如Entity Framework Core添加用户身份认证与授权ASP.NET Core Identity以及探索视图组件View Components来构建可复用的UI模块。实践中遇到的具体问题多查阅官方文档和社区资源结合调试工具你就能快速定位并解决。
返回列表