简体中文 繁體中文 English Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français Japanese

站内搜索

搜索

活动公告

通知:为庆祝网站一周年,将在5.1日与5.2日开放注册,具体信息请见后续详细公告
04-22 00:04
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

ASP.NET MVC 4项目实战教程构建高效可扩展Web应用程序的最佳实践

SunJu_FaceMall

3万

主题

1132

科技点

3万

积分

白金月票

碾压王

积分
32766

立华奏

发表于 2025-8-23 20:10:36 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

ASP.NET MVC 4是一个强大的Web应用程序框架,它基于Model-View-Controller(MVC)设计模式,使开发人员能够构建可测试、可维护且高效的应用程序。与传统的Web Forms相比,MVC模式提供了更好的控制、更清晰的关注点分离和更易于测试的代码结构。

本教程将带您深入了解ASP.NET MVC 4的核心概念和最佳实践,通过实际示例展示如何构建高效、可扩展的Web应用程序。我们将从基础概念开始,逐步深入到高级主题,包括依赖注入、安全性、性能优化和测试策略。

ASP.NET MVC 4基础

MVC架构概述

Model-View-Controller(MVC)是一种软件设计模式,它将应用程序分为三个主要组件:

• Model(模型):表示应用程序的数据和业务逻辑。
• View(视图):负责显示用户界面。
• Controller(控制器):处理用户输入,与模型交互并选择视图来呈现。

这种分离使得应用程序更易于管理、测试和修改。

ASP.NET MVC 4的核心组件

ASP.NET MVC 4框架包含以下核心组件:

1. 路由系统:将URL映射到控制器和动作方法。
2. 控制器:处理HTTP请求并生成响应。
3. 动作方法:控制器中的方法,用于执行特定操作。
4. 模型绑定:将HTTP请求数据映射到动作方法参数。
5. 视图引擎:生成HTML响应,默认使用Razor视图引擎。
6. 过滤器:提供在请求处理管道中执行额外逻辑的机制。

项目设置与配置

创建ASP.NET MVC 4项目

让我们开始创建一个新的ASP.NET MVC 4项目:

1. 打开Visual Studio。
2. 选择”文件” > “新建” > “项目”。
3. 在”新建项目”对话框中,选择”Web”模板,然后选择”ASP.NET MVC 4 Web应用程序”。
4. 输入项目名称,例如”MvcDemoApp”,然后点击”确定”。
5. 在”新建ASP.NET MVC 4项目”对话框中,选择”Internet应用程序”模板,确保Razor视图引擎被选中,然后点击”确定”。

项目结构分析

创建项目后,您将看到以下主要文件夹和文件:

• App_Data:用于存储数据库文件。
• App_Start:包含应用程序启动时执行的代码,如路由配置、捆绑配置等。
• Content:包含CSS文件和图像等静态内容。
• Controllers:包含控制器类。
• Models:包含模型类。
• Scripts:包含JavaScript文件。
• Views:包含视图文件,每个控制器有一个对应的文件夹。
• Global.asax:包含应用程序级别的事件处理程序。
• Web.config:包含应用程序配置设置。

配置路由

路由是ASP.NET MVC的核心功能之一,它定义了URL如何映射到控制器和动作方法。默认路由配置在App_Start\RouteConfig.cs文件中:
  1. public class RouteConfig
  2. {
  3.     public static void RegisterRoutes(RouteCollection routes)
  4.     {
  5.         routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
  6.         routes.MapRoute(
  7.             name: "Default",
  8.             url: "{controller}/{action}/{id}",
  9.             defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
  10.         );
  11.     }
  12. }
复制代码

这个默认路由模式{controller}/{action}/{id}将URL映射到控制器的动作方法,并传递一个可选的id参数。例如,URL/Home/Index/3将映射到HomeController的Index方法,并传递id值为3。

构建模型层

创建模型类

模型是应用程序的核心,它表示数据和业务逻辑。让我们创建一个简单的产品模型:
  1. public class Product
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string Description { get; set; }
  6.     public decimal Price { get; set; }
  7.     public int CategoryId { get; set; }
  8.     public virtual Category Category { get; set; }
  9. }
  10. public class Category
  11. {
  12.     public int Id { get; set; }
  13.     public string Name { get; set; }
  14.     public string Description { get; set; }
  15.     public virtual ICollection<Product> Products { get; set; }
  16. }
复制代码

使用Entity Framework

Entity Framework (EF) 是一个对象关系映射(ORM)框架,它使开发人员能够使用.NET对象与数据库交互。让我们配置EF来管理我们的产品数据:

首先,创建一个DbContext类:
  1. public class ProductContext : DbContext
  2. {
  3.     public ProductContext() : base("ProductContext")
  4.     {
  5.     }
  6.     public DbSet<Product> Products { get; set; }
  7.     public DbSet<Category> Categories { get; set; }
  8.     protected override void OnModelCreating(DbModelBuilder modelBuilder)
  9.     {
  10.         modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
  11.     }
  12. }
复制代码

然后,在Web.config文件中添加连接字符串:
  1. <connectionStrings>
  2.   <add name="ProductContext"
  3.        connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=ProductContext;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\ProductContext.mdf"
  4.        providerName="System.Data.SqlClient" />
  5. </connectionStrings>
复制代码

数据库迁移

Entity Framework Code First迁移允许您随着模型的变化逐步更新数据库架构。要启用迁移,请按照以下步骤操作:

1. 打开包管理器控制台(工具 > NuGet包管理器 > 包管理器控制台)。
2. 运行命令Enable-Migrations。
3. 运行命令Add-Migration InitialCreate以创建初始迁移。
4. 运行命令Update-Database以应用迁移并创建数据库。

当您更改模型类时,可以创建新的迁移并更新数据库:
  1. Add-Migration AddProductPrice
  2. Update-Database
复制代码

构建控制器层

创建控制器

控制器负责处理用户请求,与模型交互并返回视图。让我们创建一个产品控制器:
  1. public class ProductsController : Controller
  2. {
  3.     private ProductContext db = new ProductContext();
  4.     // GET: Products
  5.     public ActionResult Index()
  6.     {
  7.         var products = db.Products.Include(p => p.Category).ToList();
  8.         return View(products);
  9.     }
  10.     // GET: Products/Details/5
  11.     public ActionResult Details(int? id)
  12.     {
  13.         if (id == null)
  14.         {
  15.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  16.         }
  17.         Product product = db.Products.Find(id);
  18.         if (product == null)
  19.         {
  20.             return HttpNotFound();
  21.         }
  22.         return View(product);
  23.     }
  24.     // GET: Products/Create
  25.     public ActionResult Create()
  26.     {
  27.         ViewBag.CategoryId = new SelectList(db.Categories, "Id", "Name");
  28.         return View();
  29.     }
  30.     // POST: Products/Create
  31.     [HttpPost]
  32.     [ValidateAntiForgeryToken]
  33.     public ActionResult Create([Bind(Include = "Id,Name,Description,Price,CategoryId")] Product product)
  34.     {
  35.         if (ModelState.IsValid)
  36.         {
  37.             db.Products.Add(product);
  38.             db.SaveChanges();
  39.             return RedirectToAction("Index");
  40.         }
  41.         ViewBag.CategoryId = new SelectList(db.Categories, "Id", "Name", product.CategoryId);
  42.         return View(product);
  43.     }
  44.     // GET: Products/Edit/5
  45.     public ActionResult Edit(int? id)
  46.     {
  47.         if (id == null)
  48.         {
  49.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  50.         }
  51.         Product product = db.Products.Find(id);
  52.         if (product == null)
  53.         {
  54.             return HttpNotFound();
  55.         }
  56.         ViewBag.CategoryId = new SelectList(db.Categories, "Id", "Name", product.CategoryId);
  57.         return View(product);
  58.     }
  59.     // POST: Products/Edit/5
  60.     [HttpPost]
  61.     [ValidateAntiForgeryToken]
  62.     public ActionResult Edit([Bind(Include = "Id,Name,Description,Price,CategoryId")] Product product)
  63.     {
  64.         if (ModelState.IsValid)
  65.         {
  66.             db.Entry(product).State = EntityState.Modified;
  67.             db.SaveChanges();
  68.             return RedirectToAction("Index");
  69.         }
  70.         ViewBag.CategoryId = new SelectList(db.Categories, "Id", "Name", product.CategoryId);
  71.         return View(product);
  72.     }
  73.     // GET: Products/Delete/5
  74.     public ActionResult Delete(int? id)
  75.     {
  76.         if (id == null)
  77.         {
  78.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  79.         }
  80.         Product product = db.Products.Find(id);
  81.         if (product == null)
  82.         {
  83.             return HttpNotFound();
  84.         }
  85.         return View(product);
  86.     }
  87.     // POST: Products/Delete/5
  88.     [HttpPost, ActionName("Delete")]
  89.     [ValidateAntiForgeryToken]
  90.     public ActionResult DeleteConfirmed(int id)
  91.     {
  92.         Product product = db.Products.Find(id);
  93.         db.Products.Remove(product);
  94.         db.SaveChanges();
  95.         return RedirectToAction("Index");
  96.     }
  97.     protected override void Dispose(bool disposing)
  98.     {
  99.         if (disposing)
  100.         {
  101.             db.Dispose();
  102.         }
  103.         base.Dispose(disposing);
  104.     }
  105. }
复制代码

控制器最佳实践

以下是一些控制器最佳实践:

1. 保持控制器精简:控制器应该只协调模型和视图之间的交互,而不包含业务逻辑。
2. 使用依赖注入:避免在控制器中直接实例化依赖项,而是通过构造函数注入它们。
3. 使用异步操作:对于I/O密集型操作,使用异步动作方法以提高性能和可伸缩性。
4. 正确处理错误:使用try-catch块和错误过滤器来处理异常。
5. 使用模型绑定:利用ASP.NET MVC的模型绑定功能,而不是手动从请求中提取数据。

异步控制器

异步控制器可以提高应用程序的可伸缩性,特别是在处理I/O密集型操作时。以下是一个异步控制器的示例:
  1. public class ProductsController : Controller
  2. {
  3.     private ProductContext db = new ProductContext();
  4.     // GET: Products
  5.     public async Task<ActionResult> Index()
  6.     {
  7.         var products = await db.Products.Include(p => p.Category).ToListAsync();
  8.         return View(products);
  9.     }
  10.     // GET: Products/Details/5
  11.     public async Task<ActionResult> Details(int? id)
  12.     {
  13.         if (id == null)
  14.         {
  15.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  16.         }
  17.         Product product = await db.Products.FindAsync(id);
  18.         if (product == null)
  19.         {
  20.             return HttpNotFound();
  21.         }
  22.         return View(product);
  23.     }
  24.     // ... 其他动作方法
  25.     protected override void Dispose(bool disposing)
  26.     {
  27.         if (disposing)
  28.         {
  29.             db.Dispose();
  30.         }
  31.         base.Dispose(disposing);
  32.     }
  33. }
复制代码

构建视图层

Razor视图引擎

Razor是ASP.NET MVC的默认视图引擎,它提供了一种简洁、优雅的方式来生成HTML。Razor语法使用@符号来从HTML过渡到C#代码。

以下是一个基本的Razor视图示例:
  1. @model IEnumerable<MvcDemoApp.Models.Product>
  2. @{
  3.     ViewBag.Title = "Index";
  4. }
  5. <h2>Products</h2>
  6. <p>
  7.     @Html.ActionLink("Create New", "Create")
  8. </p>
  9. <table class="table">
  10.     <tr>
  11.         <th>
  12.             @Html.DisplayNameFor(model => model.Name)
  13.         </th>
  14.         <th>
  15.             @Html.DisplayNameFor(model => model.Description)
  16.         </th>
  17.         <th>
  18.             @Html.DisplayNameFor(model => model.Price)
  19.         </th>
  20.         <th>
  21.             @Html.DisplayNameFor(model => model.Category.Name)
  22.         </th>
  23.         <th></th>
  24.     </tr>
  25. @foreach (var item in Model) {
  26.     <tr>
  27.         <td>
  28.             @Html.DisplayFor(modelItem => item.Name)
  29.         </td>
  30.         <td>
  31.             @Html.DisplayFor(modelItem => item.Description)
  32.         </td>
  33.         <td>
  34.             @Html.DisplayFor(modelItem => item.Price)
  35.         </td>
  36.         <td>
  37.             @Html.DisplayFor(modelItem => item.Category.Name)
  38.         </td>
  39.         <td>
  40.             @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
  41.             @Html.ActionLink("Details", "Details", new { id=item.Id }) |
  42.             @Html.ActionLink("Delete", "Delete", new { id=item.Id })
  43.         </td>
  44.     </tr>
  45. }
  46. </table>
复制代码

布局和部分视图

布局类似于Web Forms中的母版页,它定义了网站的整体结构。默认布局位于Views\Shared\_Layout.cshtml:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <meta charset="utf-8" />
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>@ViewBag.Title - My ASP.NET Application</title>
  7.     @Styles.Render("~/Content/css")
  8.     @Scripts.Render("~/bundles/modernizr")
  9. </head>
  10. <body>
  11.     <div class="navbar navbar-inverse navbar-fixed-top">
  12.         <div class="container">
  13.             <div class="navbar-header">
  14.                 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
  15.                     <span class="icon-bar"></span>
  16.                     <span class="icon-bar"></span>
  17.                     <span class="icon-bar"></span>
  18.                 </button>
  19.                 @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
  20.             </div>
  21.             <div class="navbar-collapse collapse">
  22.                 <ul class="nav navbar-nav">
  23.                     <li>@Html.ActionLink("Home", "Index", "Home")</li>
  24.                     <li>@Html.ActionLink("About", "About", "Home")</li>
  25.                     <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
  26.                 </ul>
  27.             </div>
  28.         </div>
  29.     </div>
  30.     <div class="container body-content">
  31.         @RenderBody()
  32.         <hr />
  33.         <footer>
  34.             <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
  35.         </footer>
  36.     </div>
  37.     @Scripts.Render("~/bundles/jquery")
  38.     @Scripts.Render("~/bundles/bootstrap")
  39.     @RenderSection("scripts", required: false)
  40. </body>
  41. </html>
复制代码

部分视图是可重用的视图片段,它们可以包含在多个视图中。创建部分视图的步骤如下:

1. 在Views\Shared文件夹中,右键单击并选择”添加” > “视图”。
2. 勾选”创建为部分视图”复选框。
3. 输入视图名称,例如_ProductList,然后点击”添加”。

以下是一个部分视图的示例:
  1. @model IEnumerable<MvcDemoApp.Models.Product>
  2. <table class="table">
  3.     <tr>
  4.         <th>
  5.             @Html.DisplayNameFor(model => model.Name)
  6.         </th>
  7.         <th>
  8.             @Html.DisplayNameFor(model => model.Price)
  9.         </th>
  10.         <th></th>
  11.     </tr>
  12. @foreach (var item in Model) {
  13.     <tr>
  14.         <td>
  15.             @Html.DisplayFor(modelItem => item.Name)
  16.         </td>
  17.         <td>
  18.             @Html.DisplayFor(modelItem => item.Price)
  19.         </td>
  20.         <td>
  21.             @Html.ActionLink("Details", "Details", new { id=item.Id })
  22.         </td>
  23.     </tr>
  24. }
  25. </table>
复制代码

要在其他视图中使用这个部分视图,可以使用Html.Partial或Html.RenderPartial方法:
  1. @Html.Partial("_ProductList", Model)
复制代码

强类型视图和HTML助手

强类型视图使用@model指令指定模型类型,这提供了编译时类型检查和IntelliSense支持。HTML助手是生成HTML元素的方法,它们可以分为三类:

1. 标准HTML助手:如Html.TextBox、Html.DropDownList等。
2. 强类型HTML助手:如Html.TextBoxFor、Html.DropDownListFor等,它们使用lambda表达式指定模型属性。
3. 模板化HTML助手:如Html.DisplayFor和Html.EditorFor,它们根据模型属性的数据类型和元数据生成HTML。

以下是一个使用强类型HTML助手的表单示例:
  1. @model MvcDemoApp.Models.Product
  2. @{
  3.     ViewBag.Title = "Create";
  4. }
  5. <h2>Create Product</h2>
  6. @using (Html.BeginForm())
  7. {
  8.     @Html.AntiForgeryToken()
  9.    
  10.     <div class="form-horizontal">
  11.         <h4>Product</h4>
  12.         <hr />
  13.         @Html.ValidationSummary(true, "", new { @class = "text-danger" })
  14.         <div class="form-group">
  15.             @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
  16.             <div class="col-md-10">
  17.                 @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
  18.                 @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
  19.             </div>
  20.         </div>
  21.         <div class="form-group">
  22.             @Html.LabelFor(model => model.Description, htmlAttributes: new { @class = "control-label col-md-2" })
  23.             <div class="col-md-10">
  24.                 @Html.EditorFor(model => model.Description, new { htmlAttributes = new { @class = "form-control" } })
  25.                 @Html.ValidationMessageFor(model => model.Description, "", new { @class = "text-danger" })
  26.             </div>
  27.         </div>
  28.         <div class="form-group">
  29.             @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
  30.             <div class="col-md-10">
  31.                 @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
  32.                 @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
  33.             </div>
  34.         </div>
  35.         <div class="form-group">
  36.             @Html.LabelFor(model => model.CategoryId, "CategoryId", htmlAttributes: new { @class = "control-label col-md-2" })
  37.             <div class="col-md-10">
  38.                 @Html.DropDownList("CategoryId", null, htmlAttributes: new { @class = "form-control" })
  39.                 @Html.ValidationMessageFor(model => model.CategoryId, "", new { @class = "text-danger" })
  40.             </div>
  41.         </div>
  42.         <div class="form-group">
  43.             <div class="col-md-offset-2 col-md-10">
  44.                 <input type="submit" value="Create" class="btn btn-default" />
  45.             </div>
  46.         </div>
  47.     </div>
  48. }
  49. <div>
  50.     @Html.ActionLink("Back to List", "Index")
  51. </div>
  52. @section Scripts {
  53.     @Scripts.Render("~/bundles/jqueryval")
  54. }
复制代码

数据访问

Repository模式

Repository模式是一种设计模式,它抽象了数据访问层,使应用程序与数据存储解耦。使用Repository模式可以提高代码的可测试性和可维护性。

首先,定义一个通用的Repository接口:
  1. public interface IRepository<T> where T : class
  2. {
  3.     IEnumerable<T> GetAll();
  4.     T GetById(int id);
  5.     void Add(T entity);
  6.     void Update(T entity);
  7.     void Delete(T entity);
  8.     void Save();
  9. }
复制代码

然后,实现这个接口:
  1. public class Repository<T> : IRepository<T> where T : class
  2. {
  3.     protected ProductContext context;
  4.     protected DbSet<T> dbSet;
  5.     public Repository(ProductContext context)
  6.     {
  7.         this.context = context;
  8.         this.dbSet = context.Set<T>();
  9.     }
  10.     public virtual IEnumerable<T> GetAll()
  11.     {
  12.         return dbSet.ToList();
  13.     }
  14.     public virtual T GetById(int id)
  15.     {
  16.         return dbSet.Find(id);
  17.     }
  18.     public virtual void Add(T entity)
  19.     {
  20.         dbSet.Add(entity);
  21.     }
  22.     public virtual void Update(T entity)
  23.     {
  24.         dbSet.Attach(entity);
  25.         context.Entry(entity).State = EntityState.Modified;
  26.     }
  27.     public virtual void Delete(T entity)
  28.     {
  29.         if (context.Entry(entity).State == EntityState.Detached)
  30.         {
  31.             dbSet.Attach(entity);
  32.         }
  33.         dbSet.Remove(entity);
  34.     }
  35.     public virtual void Save()
  36.     {
  37.         context.SaveChanges();
  38.     }
  39. }
复制代码

接下来,创建特定的Repository接口和实现:
  1. public interface IProductRepository : IRepository<Product>
  2. {
  3.     IEnumerable<Product> GetProductsByCategory(int categoryId);
  4. }
  5. public class ProductRepository : Repository<Product>, IProductRepository
  6. {
  7.     public ProductRepository(ProductContext context) : base(context)
  8.     {
  9.     }
  10.     public IEnumerable<Product> GetProductsByCategory(int categoryId)
  11.     {
  12.         return context.Products.Where(p => p.CategoryId == categoryId).ToList();
  13.     }
  14. }
复制代码

Unit of Work模式

Unit of Work模式维护一个受业务事务影响的对象列表,并协调写入更改和解决并发问题。它通常与Repository模式一起使用。
  1. public interface IUnitOfWork : IDisposable
  2. {
  3.     IProductRepository ProductRepository { get; }
  4.     ICategoryRepository CategoryRepository { get; }
  5.     void Save();
  6. }
  7. public class UnitOfWork : IUnitOfWork
  8. {
  9.     private ProductContext context = new ProductContext();
  10.     private IProductRepository productRepository;
  11.     private ICategoryRepository categoryRepository;
  12.     public IProductRepository ProductRepository
  13.     {
  14.         get
  15.         {
  16.             if (this.productRepository == null)
  17.             {
  18.                 this.productRepository = new ProductRepository(context);
  19.             }
  20.             return productRepository;
  21.         }
  22.     }
  23.     public ICategoryRepository CategoryRepository
  24.     {
  25.         get
  26.         {
  27.             if (this.categoryRepository == null)
  28.             {
  29.                 this.categoryRepository = new CategoryRepository(context);
  30.             }
  31.             return categoryRepository;
  32.         }
  33.     }
  34.     public void Save()
  35.     {
  36.         context.SaveChanges();
  37.     }
  38.     private bool disposed = false;
  39.     protected virtual void Dispose(bool disposing)
  40.     {
  41.         if (!this.disposed)
  42.         {
  43.             if (disposing)
  44.             {
  45.                 context.Dispose();
  46.             }
  47.         }
  48.         this.disposed = true;
  49.     }
  50.     public void Dispose()
  51.     {
  52.         Dispose(true);
  53.         GC.SuppressFinalize(this);
  54.     }
  55. }
复制代码

使用Repository和Unit of Work

现在,我们可以更新控制器以使用Repository和Unit of Work模式:
  1. public class ProductsController : Controller
  2. {
  3.     private IUnitOfWork unitOfWork;
  4.     public ProductsController(IUnitOfWork unitOfWork)
  5.     {
  6.         this.unitOfWork = unitOfWork;
  7.     }
  8.     // GET: Products
  9.     public ActionResult Index()
  10.     {
  11.         var products = unitOfWork.ProductRepository.GetAll();
  12.         return View(products);
  13.     }
  14.     // GET: Products/Details/5
  15.     public ActionResult Details(int? id)
  16.     {
  17.         if (id == null)
  18.         {
  19.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  20.         }
  21.         Product product = unitOfWork.ProductRepository.GetById(id.Value);
  22.         if (product == null)
  23.         {
  24.             return HttpNotFound();
  25.         }
  26.         return View(product);
  27.     }
  28.     // GET: Products/Create
  29.     public ActionResult Create()
  30.     {
  31.         ViewBag.CategoryId = new SelectList(unitOfWork.CategoryRepository.GetAll(), "Id", "Name");
  32.         return View();
  33.     }
  34.     // POST: Products/Create
  35.     [HttpPost]
  36.     [ValidateAntiForgeryToken]
  37.     public ActionResult Create([Bind(Include = "Id,Name,Description,Price,CategoryId")] Product product)
  38.     {
  39.         if (ModelState.IsValid)
  40.         {
  41.             unitOfWork.ProductRepository.Add(product);
  42.             unitOfWork.Save();
  43.             return RedirectToAction("Index");
  44.         }
  45.         ViewBag.CategoryId = new SelectList(unitOfWork.CategoryRepository.GetAll(), "Id", "Name", product.CategoryId);
  46.         return View(product);
  47.     }
  48.     // GET: Products/Edit/5
  49.     public ActionResult Edit(int? id)
  50.     {
  51.         if (id == null)
  52.         {
  53.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  54.         }
  55.         Product product = unitOfWork.ProductRepository.GetById(id.Value);
  56.         if (product == null)
  57.         {
  58.             return HttpNotFound();
  59.         }
  60.         ViewBag.CategoryId = new SelectList(unitOfWork.CategoryRepository.GetAll(), "Id", "Name", product.CategoryId);
  61.         return View(product);
  62.     }
  63.     // POST: Products/Edit/5
  64.     [HttpPost]
  65.     [ValidateAntiForgeryToken]
  66.     public ActionResult Edit([Bind(Include = "Id,Name,Description,Price,CategoryId")] Product product)
  67.     {
  68.         if (ModelState.IsValid)
  69.         {
  70.             unitOfWork.ProductRepository.Update(product);
  71.             unitOfWork.Save();
  72.             return RedirectToAction("Index");
  73.         }
  74.         ViewBag.CategoryId = new SelectList(unitOfWork.CategoryRepository.GetAll(), "Id", "Name", product.CategoryId);
  75.         return View(product);
  76.     }
  77.     // GET: Products/Delete/5
  78.     public ActionResult Delete(int? id)
  79.     {
  80.         if (id == null)
  81.         {
  82.             return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  83.         }
  84.         Product product = unitOfWork.ProductRepository.GetById(id.Value);
  85.         if (product == null)
  86.         {
  87.             return HttpNotFound();
  88.         }
  89.         return View(product);
  90.     }
  91.     // POST: Products/Delete/5
  92.     [HttpPost, ActionName("Delete")]
  93.     [ValidateAntiForgeryToken]
  94.     public ActionResult DeleteConfirmed(int id)
  95.     {
  96.         Product product = unitOfWork.ProductRepository.GetById(id);
  97.         unitOfWork.ProductRepository.Delete(product);
  98.         unitOfWork.Save();
  99.         return RedirectToAction("Index");
  100.     }
  101.     protected override void Dispose(bool disposing)
  102.     {
  103.         if (disposing)
  104.         {
  105.             unitOfWork.Dispose();
  106.         }
  107.         base.Dispose(disposing);
  108.     }
  109. }
复制代码

实现依赖注入

依赖注入概述

依赖注入(DI)是一种设计模式,它允许我们将依赖项从外部注入到类中,而不是在类内部创建它们。这有助于实现控制反转(IoC),提高代码的可测试性和可维护性。

使用Unity容器

Unity是一个轻量级、可扩展的依赖注入容器。让我们在ASP.NET MVC 4应用程序中配置Unity:

1. 安装Unity NuGet包:Install-Package Unity.Mvc4
2. 在App_Start文件夹中创建UnityConfig.cs文件:

安装Unity NuGet包:
  1. Install-Package Unity.Mvc4
复制代码

在App_Start文件夹中创建UnityConfig.cs文件:
  1. public static class UnityConfig
  2. {
  3.     public static void RegisterComponents()
  4.     {
  5.         var container = new UnityContainer();
  6.         // 注册所有组件
  7.         container.RegisterType<IUnitOfWork, UnitOfWork>();
  8.         container.RegisterType<IProductRepository, ProductRepository>();
  9.         container.RegisterType<ICategoryRepository, CategoryRepository>();
  10.         DependencyResolver.SetResolver(new UnityDependencyResolver(container));
  11.     }
  12. }
复制代码

1. 在Global.asax.cs文件中调用UnityConfig.RegisterComponents():
  1. protected void Application_Start()
  2. {
  3.     AreaRegistration.RegisterAllAreas();
  4.     FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  5.     RouteConfig.RegisterRoutes(RouteTable.Routes);
  6.     BundleConfig.RegisterBundles(BundleTable.Bundles);
  7.     AuthConfig.RegisterAuth();
  8.    
  9.     UnityConfig.RegisterComponents();
  10. }
复制代码

依赖注入的好处

使用依赖注入有以下好处:

1. 提高可测试性:可以轻松地用模拟对象替换真实依赖项,使单元测试更容易编写。
2. 降低耦合度:类不再负责创建其依赖项,而是通过构造函数或属性接收它们。
3. 提高可维护性:依赖项的更改不会影响使用它们的类。
4. 提高可重用性:类可以在不同的上下文中重用,只需提供不同的依赖项实现。

安全性最佳实践

身份验证和授权

ASP.NET MVC 4提供了多种身份验证和授权机制:

1. Forms身份验证:基于cookie的身份验证,适用于大多数Web应用程序。
2. Windows身份验证:适用于Intranet应用程序。
3. OAuth和OpenID:用于第三方身份验证,如Facebook、Google等。

以下是如何在ASP.NET MVC 4中实现基于角色的授权:
  1. [Authorize(Roles = "Admin")]
  2. public class AdminController : Controller
  3. {
  4.     // 只有管理员可以访问这些动作方法
  5.     public ActionResult Index()
  6.     {
  7.         return View();
  8.     }
  9. }
  10. public class ProductsController : Controller
  11. {
  12.     // 任何经过身份验证的用户都可以访问
  13.     [Authorize]
  14.     public ActionResult Create()
  15.     {
  16.         return View();
  17.     }
  18.     // 允许匿名访问
  19.     [AllowAnonymous]
  20.     public ActionResult Details(int id)
  21.     {
  22.         return View();
  23.     }
  24. }
复制代码

防止常见攻击

ASP.NET MVC 4默认对Razor视图中的输出进行HTML编码,这有助于防止XSS攻击。但是,在使用Html.Raw方法时要小心:
  1. <!-- 危险:可能导致XSS攻击 -->
  2. @Html.Raw(Model.UserContent)
  3. <!-- 安全:输出被HTML编码 -->
  4. @Model.UserContent
复制代码

ASP.NET MVC 4提供了内置的CSRF保护。在表单中使用@Html.AntiForgeryToken(),并在动作方法上应用[ValidateAntiForgeryToken]特性:
  1. @using (Html.BeginForm())
  2. {
  3.     @Html.AntiForgeryToken()
  4.    
  5.     <!-- 表单字段 -->
  6. }
复制代码
  1. [HttpPost]
  2. [ValidateAntiForgeryToken]
  3. public ActionResult Create(Product product)
  4. {
  5.     // 处理表单提交
  6. }
复制代码

使用参数化查询或ORM(如Entity Framework)来防止SQL注入攻击:
  1. // 危险:容易受到SQL注入攻击
  2. string query = "SELECT * FROM Products WHERE Name = '" + productName + "'";
  3. // 安全:使用参数化查询
  4. string query = "SELECT * FROM Products WHERE Name = @productName";
  5. SqlCommand command = new SqlCommand(query, connection);
  6. command.Parameters.AddWithValue("@productName", productName);
  7. // 更安全:使用Entity Framework
  8. var products = db.Products.Where(p => p.Name == productName).ToList();
复制代码

安全配置

在Web.config文件中,可以配置以下安全设置:
  1. <system.web>
  2.   <!-- 启用SSL/TLS -->
  3.   <httpCookies requireSSL="true" httpOnlyCookies="true" />
  4.   
  5.   <!-- 禁用不必要的HTTP方法 -->
  6.   <httpProtocol>
  7.     <customHeaders>
  8.       <add name="X-Content-Type-Options" value="nosniff" />
  9.       <add name="X-Frame-Options" value="SAMEORIGIN" />
  10.       <add name="X-XSS-Protection" value="1; mode=block" />
  11.     </customHeaders>
  12.   </httpProtocol>
  13.   
  14.   <!-- 配置身份验证 -->
  15.   <authentication mode="Forms">
  16.     <forms loginUrl="~/Account/Login" timeout="2880" requireSSL="true" />
  17.   </authentication>
  18. </system.web>
复制代码

性能优化

缓存策略

缓存是提高Web应用程序性能的有效方法。ASP.NET MVC 4提供了多种缓存选项:

使用[OutputCache]特性缓存控制器的输出:
  1. [OutputCache(Duration = 3600, Location = OutputCacheLocation.Client)]
  2. public ActionResult Index()
  3. {
  4.     var products = db.Products.ToList();
  5.     return View(products);
  6. }
复制代码

使用System.Runtime.Caching.MemoryCache缓存数据:
  1. public ActionResult Index()
  2. {
  3.     var cacheKey = "Products";
  4.     var products = MemoryCache.Default.Get(cacheKey) as List<Product>;
  5.    
  6.     if (products == null)
  7.     {
  8.         products = db.Products.ToList();
  9.         
  10.         var policy = new CacheItemPolicy
  11.         {
  12.             AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
  13.         };
  14.         
  15.         MemoryCache.Default.Add(cacheKey, products, policy);
  16.     }
  17.    
  18.     return View(products);
  19. }
复制代码

对于大型应用程序,可以使用分布式缓存,如Redis或AppFabric缓存:
  1. public ActionResult Index()
  2. {
  3.     var cacheKey = "Products";
  4.     var products = Cache.Get(cacheKey) as List<Product>;
  5.    
  6.     if (products == null)
  7.     {
  8.         products = db.Products.ToList();
  9.         Cache.Add(cacheKey, products, null, DateTime.Now.AddHours(1),
  10.             Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
  11.     }
  12.    
  13.     return View(products);
  14. }
复制代码

捆绑和缩小

捆绑和缩小是减少HTTP请求数量和文件大小的有效方法。ASP.NET MVC 4提供了内置的捆绑和缩小功能。

在App_Start\BundleConfig.cs文件中配置捆绑:
  1. public class BundleConfig
  2. {
  3.     public static void RegisterBundles(BundleCollection bundles)
  4.     {
  5.         bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
  6.                     "~/Scripts/jquery-{version}.js"));
  7.         bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
  8.                     "~/Scripts/jquery.validate*"));
  9.         bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
  10.                     "~/Scripts/modernizr-*"));
  11.         bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
  12.                   "~/Scripts/bootstrap.js",
  13.                   "~/Scripts/respond.js"));
  14.         bundles.Add(new StyleBundle("~/Content/css").Include(
  15.                   "~/Content/bootstrap.css",
  16.                   "~/Content/site.css"));
  17.     }
  18. }
复制代码

在视图中使用捆绑:
  1. @Styles.Render("~/Content/css")
  2. @Scripts.Render("~/bundles/jquery")
  3. @Scripts.Render("~/bundles/bootstrap")
复制代码

异步处理

使用异步控制器和动作方法可以提高应用程序的可伸缩性,特别是在处理I/O密集型操作时:
  1. public async Task<ActionResult> Index()
  2. {
  3.     var products = await db.Products.ToListAsync();
  4.     return View(products);
  5. }
复制代码

优化数据库访问

优化数据库访问可以显著提高应用程序性能:

1. 使用延迟加载和预先加载:根据场景选择适当的加载策略。
2. 避免N+1查询问题:使用Include方法预先加载相关实体。
3. 使用存储过程:对于复杂查询,考虑使用存储过程。
4. 优化索引:确保数据库表有适当的索引。
  1. // 使用Include预先加载相关实体
  2. var products = await db.Products
  3.     .Include(p => p.Category)
  4.     .ToListAsync();
  5. // 使用Select只查询需要的字段
  6. var productNames = await db.Products
  7.     .Where(p => p.Price > 100)
  8.     .Select(p => p.Name)
  9.     .ToListAsync();
复制代码

测试策略

单元测试

单元测试是验证代码单元(如方法或类)是否按预期工作的过程。ASP.NET MVC 4的可测试性设计使得编写单元测试变得容易。

以下是一个使用NUnit和Moq的单元测试示例:
  1. [TestFixture]
  2. public class ProductsControllerTests
  3. {
  4.     private ProductsController controller;
  5.     private IUnitOfWork unitOfWork;
  6.     private IProductRepository productRepository;
  7.     private List<Product> products;
  8.     [SetUp]
  9.     public void Setup()
  10.     {
  11.         // 创建测试数据
  12.         products = new List<Product>
  13.         {
  14.             new Product { Id = 1, Name = "Product 1", Price = 10.00m },
  15.             new Product { Id = 2, Name = "Product 2", Price = 20.00m }
  16.         };
  17.         // 模拟Repository
  18.         productRepository = new Mock<IProductRepository>();
  19.         productRepository.Setup(r => r.GetAll()).Returns(products);
  20.         productRepository.Setup(r => r.GetById(It.IsAny<int>()))
  21.             .Returns<int>(id => products.FirstOrDefault(p => p.Id == id));
  22.         // 模拟UnitOfWork
  23.         unitOfWork = new Mock<IUnitOfWork>();
  24.         unitOfWork.Setup(u => u.ProductRepository).Returns(productRepository.Object);
  25.         // 创建控制器
  26.         controller = new ProductsController(unitOfWork.Object);
  27.     }
  28.     [Test]
  29.     public void Index_ReturnsAllProducts()
  30.     {
  31.         // Act
  32.         var result = controller.Index() as ViewResult;
  33.         var model = result.Model as List<Product>;
  34.         // Assert
  35.         Assert.IsNotNull(result);
  36.         Assert.AreEqual(2, model.Count);
  37.         Assert.AreEqual("Product 1", model[0].Name);
  38.         Assert.AreEqual("Product 2", model[1].Name);
  39.     }
  40.     [Test]
  41.     public void Details_ValidId_ReturnsProduct()
  42.     {
  43.         // Act
  44.         var result = controller.Details(1) as ViewResult;
  45.         var model = result.Model as Product;
  46.         // Assert
  47.         Assert.IsNotNull(result);
  48.         Assert.AreEqual(1, model.Id);
  49.         Assert.AreEqual("Product 1", model.Name);
  50.     }
  51.     [Test]
  52.     public void Details_InvalidId_ReturnsHttpNotFound()
  53.     {
  54.         // Act
  55.         var result = controller.Details(99);
  56.         // Assert
  57.         Assert.IsInstanceOf<HttpNotFoundResult>(result);
  58.     }
  59. }
复制代码

集成测试

集成测试验证多个组件一起工作时是否按预期执行。以下是一个使用Entity Framework的集成测试示例:
  1. [TestFixture]
  2. public class ProductRepositoryIntegrationTests
  3. {
  4.     private ProductContext context;
  5.     private IProductRepository repository;
  6.     [SetUp]
  7.     public void Setup()
  8.     {
  9.         // 创建内存数据库
  10.         Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
  11.         context = new ProductContext("Product_Test");
  12.         context.Database.CreateIfNotExists();
  13.         repository = new ProductRepository(context);
  14.     }
  15.     [TearDown]
  16.     public void TearDown()
  17.     {
  18.         context.Database.Delete();
  19.         context.Dispose();
  20.     }
  21.     [Test]
  22.     public void Add_Product_AddsToDatabase()
  23.     {
  24.         // Arrange
  25.         var product = new Product { Name = "Test Product", Price = 10.00m };
  26.         // Act
  27.         repository.Add(product);
  28.         repository.Save();
  29.         // Assert
  30.         var result = context.Products.FirstOrDefault(p => p.Name == "Test Product");
  31.         Assert.IsNotNull(result);
  32.         Assert.AreEqual(10.00m, result.Price);
  33.     }
  34.     [Test]
  35.     public void GetAll_ReturnsAllProducts()
  36.     {
  37.         // Arrange
  38.         context.Products.Add(new Product { Name = "Product 1", Price = 10.00m });
  39.         context.Products.Add(new Product { Name = "Product 2", Price = 20.00m });
  40.         context.SaveChanges();
  41.         // Act
  42.         var result = repository.GetAll();
  43.         // Assert
  44.         Assert.AreEqual(2, result.Count());
  45.     }
  46. }
复制代码

测试驱动开发(TDD)

测试驱动开发是一种开发方法,它要求在编写代码之前先编写测试。以下是TDD的基本步骤:

1. 编写失败的测试:编写一个测试,验证尚未实现的功能。
2. 编写最少的代码:编写足够的代码使测试通过。
3. 重构:改进代码,同时确保测试仍然通过。

以下是一个使用TDD开发产品搜索功能的示例:
  1. // 步骤1:编写失败的测试
  2. [Test]
  3. public void Search_ByKeyword_ReturnsMatchingProducts()
  4. {
  5.     // Arrange
  6.     var products = new List<Product>
  7.     {
  8.         new Product { Id = 1, Name = "Laptop", Description = "High performance laptop" },
  9.         new Product { Id = 2, Name = "Mouse", Description = "Wireless mouse" },
  10.         new Product { Id = 3, Name = "Keyboard", Description = "Mechanical keyboard" }
  11.     };
  12.    
  13.     var mockRepository = new Mock<IProductRepository>();
  14.     mockRepository.Setup(r => r.GetAll()).Returns(products);
  15.    
  16.     var controller = new ProductsController(mockUnitOfWork.Object);
  17.    
  18.     // Act
  19.     var result = controller.Search("laptop") as ViewResult;
  20.     var model = result.Model as List<Product>;
  21.    
  22.     // Assert
  23.     Assert.AreEqual(1, model.Count);
  24.     Assert.AreEqual("Laptop", model[0].Name);
  25. }
  26. // 步骤2:编写最少的代码使测试通过
  27. public ActionResult Search(string keyword)
  28. {
  29.     var products = unitOfWork.ProductRepository.GetAll();
  30.     var result = products.Where(p => p.Name.Contains(keyword)).ToList();
  31.     return View(result);
  32. }
  33. // 步骤3:重构代码
  34. public ActionResult Search(string keyword)
  35. {
  36.     if (string.IsNullOrEmpty(keyword))
  37.     {
  38.         return RedirectToAction("Index");
  39.     }
  40.    
  41.     var products = unitOfWork.ProductRepository.GetAll();
  42.     var result = products.Where(p =>
  43.         p.Name.Contains(keyword) || p.Description.Contains(keyword)).ToList();
  44.     return View(result);
  45. }
复制代码

部署与维护

部署策略

Visual Studio提供了Web部署功能,可以直接将应用程序部署到IIS服务器:

1. 在解决方案资源管理器中右键单击项目,选择”发布”。
2. 选择”Web部署”作为发布方法。
3. 输入服务器、站点名称和用户凭据。
4. 配置数据库连接字符串和其他设置。
5. 点击”发布”。

创建部署包以便手动部署:

1. 在解决方案资源管理器中右键单击项目,选择”发布”。
2. 选择”Web部署包”作为发布方法。
3. 指定包位置和IIS网站名称。
4. 点击”发布”。
5. 将生成的包复制到目标服务器并运行部署命令。

使用TeamCity、Jenkins或Azure DevOps等工具设置CI/CD流水线:

1. 配置源代码管理(如Git)。
2. 设置构建服务器,在代码提交时自动构建项目。
3. 配置自动测试,确保构建通过所有测试。
4. 设置自动部署到测试环境。
5. 配置批准流程,然后自动部署到生产环境。

监控和日志记录

使用日志记录框架(如NLog或log4net)记录应用程序事件:
  1. public class ProductsController : Controller
  2. {
  3.     private readonly IUnitOfWork unitOfWork;
  4.     private readonly ILogger logger;
  5.     public ProductsController(IUnitOfWork unitOfWork, ILogger logger)
  6.     {
  7.         this.unitOfWork = unitOfWork;
  8.         this.logger = logger;
  9.     }
  10.     public ActionResult Index()
  11.     {
  12.         try
  13.         {
  14.             var products = unitOfWork.ProductRepository.GetAll();
  15.             logger.Info("Retrieved {0} products", products.Count());
  16.             return View(products);
  17.         }
  18.         catch (Exception ex)
  19.         {
  20.             logger.Error(ex, "Error retrieving products");
  21.             throw;
  22.         }
  23.     }
  24. }
复制代码

使用Application Insights或New Relic等工具监控应用程序性能:

1. 安装NuGet包:Install-Package Microsoft.ApplicationInsights.Web
2. 在ApplicationInsights.config文件中配置检测键。
3. 在代码中添加自定义遥测:

安装NuGet包:
  1. Install-Package Microsoft.ApplicationInsights.Web
复制代码

在ApplicationInsights.config文件中配置检测键。

在代码中添加自定义遥测:
  1. public ActionResult Details(int id)
  2. {
  3.     var telemetry = new TelemetryClient();
  4.     var stopwatch = Stopwatch.StartNew();
  5.    
  6.     try
  7.     {
  8.         var product = unitOfWork.ProductRepository.GetById(id);
  9.         if (product == null)
  10.         {
  11.             telemetry.TrackEvent("ProductNotFound", new Dictionary<string, string> { { "ProductId", id.ToString() } });
  12.             return HttpNotFound();
  13.         }
  14.         
  15.         return View(product);
  16.     }
  17.     finally
  18.     {
  19.         stopwatch.Stop();
  20.         telemetry.TrackMetric("ProductDetailsLoadTime", stopwatch.ElapsedMilliseconds);
  21.     }
  22. }
复制代码

维护和更新

定期更新应用程序以修复错误、添加功能或提高性能:

1. 使用版本控制系统(如Git)管理代码。
2. 实施功能切换,以便在不重新部署的情况下启用或禁用功能。
3. 使用数据库迁移管理数据库架构更改。
4. 实施蓝绿部署或金丝雀发布策略,以减少部署风险。

保持应用程序安全:

1. 定期更新NuGet包以修复安全漏洞。
2. 监控安全公告和CVE(常见漏洞和暴露)。
3. 定期进行安全审计和渗透测试。
4. 实施Web应用程序防火墙(WAF)以防止常见攻击。

总结与展望

本教程详细介绍了ASP.NET MVC 4的核心概念和最佳实践,包括:

• MVC架构和ASP.NET MVC 4的核心组件
• 项目设置和配置
• 构建模型、控制器和视图
• 数据访问模式和Entity Framework
• 依赖注入和控制反转
• 安全性最佳实践
• 性能优化技术
• 测试策略和方法
• 部署和维护策略

通过遵循这些最佳实践,您可以构建高效、可扩展且易于维护的ASP.NET MVC 4应用程序。

未来发展方向

虽然ASP.NET MVC 4是一个强大的框架,但Microsoft已经发布了更新的版本,如ASP.NET MVC 5和ASP.NET Core MVC。这些新版本提供了许多改进和新功能,包括:

• 更好的性能和可伸缩性
• 跨平台支持(ASP.NET Core)
• 改进的依赖注入支持
• 标记助手(Tag Helpers)
• 视图组件(View Components)
• 内置的Web API支持

如果您正在开始一个新项目,建议考虑使用ASP.NET Core MVC,它是Microsoft的最新Web开发框架,结合了ASP.NET MVC、Web API和Web Pages的优点。

持续学习资源

要继续学习ASP.NET MVC和相关技术,以下资源可能会有所帮助:

1. Microsoft文档:https://docs.microsoft.com/en-us/aspnet/mvc
2. ASP.NET网站:https://www.asp.net/mvc
3. Pluralsight课程:提供各种ASP.NET MVC课程
4. Stack Overflow:解决特定问题的问答社区
5. GitHub:查看开源ASP.NET MVC项目

通过不断学习和实践,您可以掌握ASP.NET MVC的高级概念和技术,成为一名高效的Web开发人员。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

站长推荐上一条 /1 下一条

手机版|联系我们|小黑屋|TG频道|RSS |网站地图

Powered by Pixtech

© 2025-2026 Pixtech Team.

>