上周帮一个刚转行做开发的朋友搭环境,他盯着 Visual Studio 里密密麻麻的项目模板和 NuGet 包管理器,问了我一个很直接的问题:“我知道 C#、.NET、MVC、EF、SQL Server 这些词都很重要,但把它们堆在一起,到底怎么才能串成一个我能跑起来、能看懂、还能改的完整项目?”
这个问题很典型。很多新手在面对“全栈”这个词时,容易陷入两个误区:要么觉得必须把每个技术都学到专家级才能开始,要么跟着教程一步步做完了,却不知道每一步为什么这么做,项目结构为什么这样设计,一旦离开教程就无从下手。
这篇文章,我们就用C# + .NET 10 + ASP.NET Core MVC + EF Core + SQL Server这套经典且强大的组合,来回答这个问题。我们的目标不是罗列每个技术的百科条目,而是亲手搭建一个最小可用的“任务管理”Web应用,并在这个过程中,理解每一层技术如何分工协作,以及一个全栈项目从零到一的构建逻辑。你会发现,全栈开发的核心不是掌握无数碎片知识,而是理解数据如何从数据库出发,经过层层处理,最终呈现在用户浏览器上的完整链路。
1. 环境与项目初始化:不是安装软件,而是建立工作流
在写第一行代码之前,正确的环境准备能避免未来80%的“玄学”错误。我们的技术栈版本明确:.NET 10 SDK,SQL Server(2022或2019均可,本文以LocalDB为例,它轻量且包含在SQL Server安装中),以及一个IDE(Visual Studio 2022 或 VS Code)。
1.1 核心工具安装与验证
首先,确保你的机器上安装了 .NET 10 SDK。打开命令行,输入dotnet --version,确认输出为10.x.x。如果没有,去微软官网下载安装。这一步是基石,所有后续命令都依赖它。
接着是数据库。对于学习和开发,我强烈建议使用SQL Server Express LocalDB。它在安装 Visual Studio 时通常作为“数据存储和处理”工作负载的一部分被勾选安装。你也可以单独安装 SQL Server Express。安装后,你不需要手动启动一个数据库服务,LocalDB会在应用首次连接时自动启动实例,非常方便。验证方式:打开命令行,输入SqlLocalDB info,如果能看到实例列表(如MSSQLLocalDB),说明安装成功。
注意:如果你遇到连接问题,优先检查 LocalDB 实例名是否为
(localdb)\MSSQLLocalDB,这是默认实例名。很多新手教程忽略这一点,导致连接字符串报错。
1.2 创建项目:理解模板背后的结构
不要用 Visual Studio 的图形化向导直接创建一个包含所有功能的大项目。我们从最纯净的状态开始,手动添加所需功能,这样你才能看清每个组件的职责。
打开命令行,进入你的工作目录,执行:
dotnet new webapp -n TaskManager -f net10.0这个命令创建了一个名为TaskManager的 ASP.NET Core Razor Pages 项目模板。等等,我们不是要用 MVC 吗?是的,但 Razor Pages 模板是更简单的起点,我们可以手动将其改造成 MVC,这个过程本身极具教育意义。
进入项目文件夹:cd TaskManager。用你喜欢的编辑器(如 VS Code)打开它。观察初始结构:
Program.cs:应用的入口和主要配置(.NET 6+ 之后采用最小托管模型)。appsettings.json:配置文件,比如数据库连接字符串。Pages/:Razor Pages 的页面文件。wwwroot/:静态资源(CSS, JS, 图片)。
现在,我们要将其转变为 MVC 项目。首先,添加 MVC 支持所需的 NuGet 包和命名空间。实际上,对于 Web App 模板,MVC 服务已经隐含添加了,我们主要需要调整的是路由和文件夹结构。
1.3 改造为 MVC 结构
创建文件夹:在项目根目录下,创建以下文件夹:
Controllers/Models/Views/
修改
Program.cs,显式配置 MVC 服务并设置默认路由。找到var builder = WebApplication.CreateBuilder(args);这行之后,添加服务:
var builder = WebApplication.CreateBuilder(args); // 添加 MVC 服务 builder.Services.AddControllersWithViews(); var app = builder.Build(); // 配置 HTTP 请求管道 if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); // 配置 MVC 默认路由 app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();- 删除或清空
Pages文件夹(因为我们不再使用 Razor Pages)。现在,你的项目骨架已经是一个标准的 MVC 结构了。
为什么我们要手动改造,而不是直接用dotnet new mvc?因为理解一个框架的“组装”过程,远比记住一个固定模板更重要。当你自己添加了AddControllersWithViews和MapControllerRoute,你就明白了 MVC 模式是如何被注入到 ASP.NET Core 这个管道中的。下次遇到路由不工作、视图找不到的问题,你就能直接定位到是服务没注册还是路由模式不匹配。
2. 数据层构建:用 EF Core 在代码和数据库之间架桥
数据是应用的基石。这一层,我们的目标是:在 C# 中定义“任务”这个业务对象(Model),然后通过 EF Core 这个“翻译官”,自动在 SQL Server 中创建对应的数据表,并为我们提供便捷的数据操作方法。
2.1 定义数据模型 (Model)
在Models文件夹下,创建一个类TaskItem.cs:
namespace TaskManager.Models { public class TaskItem { public int Id { get; set; } // 主键 public string Title { get; set; } = string.Empty; public string? Description { get; set; } // 可空字段 public bool IsCompleted { get; set; } public DateTime CreatedDate { get; set; } = DateTime.Now; } }这个类简单直接,对应数据库里的一张表。Id属性会被 EF Core 默认识别为主键和自增列。注意string?的用法,它表示这个字段在数据库里允许为 NULL,在 C# 10+ 中这是推荐的可空引用类型写法,能帮助减少运行时空引用异常。
2.2 创建数据库上下文 (DbContext)
DbContext 是 EF Core 的核心,它代表与数据库的一次会话,负责管理模型、跟踪实体状态、执行数据库操作。
在Models文件夹下,创建AppDbContext.cs:
using Microsoft.EntityFrameworkCore; namespace TaskManager.Models { public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<TaskItem> Tasks { get; set; } // 这行代码至关重要! // 可选:在这里可以配置模型(如字符串默认长度、索引等) protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 示例:设置Title最大长度为100 modelBuilder.Entity<TaskItem>() .Property(t => t.Title) .HasMaxLength(100); } } }DbSet<TaskItem> Tasks这一行,就宣告了我们的应用中将有一个名为Tasks的集合,对应数据库里的Tasks表。EF Core 会通过它来执行所有 CRUD 操作。
2.3 连接数据库与依赖注入
现在,我们需要告诉应用使用哪个数据库,并把AppDbContext注册到依赖注入容器中,这样我们的控制器才能使用它。
- 安装 EF Core 包:在项目目录下执行命令。
dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools第一个包是 SQL Server 数据库提供程序,第二个包包含我们等下要用到的迁移工具命令。
- 配置连接字符串:打开
appsettings.json,添加一个连接字符串。
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TaskManagerDB;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }连接字符串解读:
Server=(localdb)\\mssqllocaldb:连接到本地的 LocalDB 实例。注意是双反斜杠,因为在 JSON 字符串中需要转义。Database=TaskManagerDB:数据库名称,如果不存在,EF Core 会创建它。Trusted_Connection=True:使用 Windows 身份验证。MultipleActiveResultSets=true:允许在同一连接上执行多个查询,对 MVC 应用很有用。TrustServerCertificate=True:本地开发时简化加密连接问题。
- 在
Program.cs中注册 DbContext: 在builder.Services.AddControllersWithViews();之后添加:
// 从配置中读取连接字符串 var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); // 将 AppDbContext 注册为服务,指定使用 SQL Server builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(connectionString));2.4 数据库迁移:让代码模型驱动数据库结构
这是 EF Core 最强大的特性之一。我们不需要手动写 SQL 建表语句。
- 创建迁移:在项目根目录打开命令行,执行:
dotnet ef migrations add InitialCreate这个命令会对比当前的AppDbContext(特别是其中的DbSet和OnModelCreating配置)与上一次迁移的状态(目前是第一次),生成一个名为InitialCreate的迁移文件,存放在Migrations/文件夹下。打开这个文件夹,你会看到XXXXXX_InitialCreate.cs文件,里面是 EF Core 为你生成的、将要在数据库上执行的 C# 代码(对应 SQL 操作)。
- 应用迁移,更新数据库:
dotnet ef database update这个命令会执行迁移文件中记录的 SQL 命令,在你的 SQL Server LocalDB 实例中创建TaskManagerDB数据库和Tasks表。
关键理解:迁移是 EF Core 的版本控制系统。每次你修改了数据模型(比如给
TaskItem加一个DueDate属性),就再执行dotnet ef migrations add AddDueDate和dotnet ef database update。这保证了你的代码模型和数据库结构始终同步,并且所有变更都可追溯。这是工程化开发的基础,务必养成习惯。
至此,数据层搭建完毕。我们没写一行 SQL,但数据库表已经就绪。这就是 ORM(对象关系映射)框架的价值:让开发者用熟悉的面向对象方式操作数据。
3. 业务逻辑与控制器:MVC 中的“交通指挥”
控制器是 MVC 模式中的“C”,它接收用户请求,协调模型(数据)和视图(界面)完成工作。在这一层,我们的目标是:创建TasksController,实现对任务项的增删改查(CRUD)操作,并处理好数据验证和异常。
3.1 创建控制器与依赖注入 DbContext
在Controllers文件夹上右键,添加 -> 控制器 -> MVC 控制器 - 空,命名为TasksController.cs。
控制器需要访问数据库,我们通过构造函数注入AppDbContext:
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using TaskManager.Models; namespace TaskManager.Controllers { public class TasksController : Controller { private readonly AppDbContext _context; // 依赖注入:框架会自动提供 AppDbContext 的实例 public TasksController(AppDbContext context) { _context = context; } // 后续的 Action 方法将在这里添加 } }private readonly AppDbContext _context;这个私有字段将用于所有 Action 方法中与数据库交互。依赖注入是 ASP.NET Core 的核心设计模式,它使代码更可测试、更松耦合。
3.2 实现 CRUD Action 方法
我们将实现五个核心 Action:
Index: 显示任务列表。Create(GET & POST): 显示创建表单和处理表单提交。Edit(GET & POST): 显示编辑表单和处理表单提交。Delete(GET & POST): 显示确认删除页面和处理删除请求。Details: 显示单个任务的详情。
Index - 列表页
// GET: Tasks public async Task<IActionResult> Index() { // 异步查询所有任务,按创建时间倒序排列 var tasks = await _context.Tasks.OrderByDescending(t => t.CreatedDate).ToListAsync(); return View(tasks); // 将任务列表传递给视图 }Create - 创建任务
// GET: Tasks/Create public IActionResult Create() { return View(); // 返回一个空表单视图 } // POST: Tasks/Create [HttpPost] // 这个属性表示此方法只响应 HTTP POST 请求 [ValidateAntiForgeryToken] // 防止跨站请求伪造攻击 public async Task<IActionResult> Create([Bind("Id,Title,Description,IsCompleted")] TaskItem taskItem) { if (ModelState.IsValid) // 检查数据注解验证是否通过 { taskItem.CreatedDate = DateTime.Now; _context.Add(taskItem); await _context.SaveChangesAsync(); // 异步保存到数据库 return RedirectToAction(nameof(Index)); // 创建成功后跳转到列表页 } return View(taskItem); // 如果验证失败,返回表单并显示错误信息 }Edit - 编辑任务
// GET: Tasks/Edit/5 public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var taskItem = await _context.Tasks.FindAsync(id); if (taskItem == null) { return NotFound(); } return View(taskItem); } // POST: Tasks/Edit/5 [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Description,IsCompleted,CreatedDate")] TaskItem taskItem) { if (id != taskItem.Id) { return NotFound(); } if (ModelState.IsValid) { try { _context.Update(taskItem); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!TaskItemExists(taskItem.Id)) { return NotFound(); } else { throw; } } return RedirectToAction(nameof(Index)); } return View(taskItem); } private bool TaskItemExists(int id) { return _context.Tasks.Any(e => e.Id == id); }Delete 和 Details方法逻辑类似,主要是根据 id 查询实体,然后或删除、或展示。
关键点:
- 异步编程:大量使用
async/await和ToListAsync(),SaveChangesAsync()。这是现代 Web 开发的标准做法,能提高服务器吞吐量,避免线程阻塞。- 模型绑定:
[Bind(...)]属性指定了哪些模型属性可以从请求中绑定,这是一种安全措施,防止过度发布攻击。- 模型验证:
ModelState.IsValid会检查模型上的数据注解(我们稍后在 Model 上添加)。这是保证数据有效性的第一道防线。- PRG 模式:Post-Redirect-Get。在 POST 请求处理成功后(如 Create, Edit, Delete),我们使用
RedirectToAction跳转到一个 GET 请求的页面(如 Index)。这可以防止用户刷新页面时重复提交表单。
3.3 为模型添加数据验证
回到TaskItem.cs,使用数据注解来添加简单的验证规则:
using System.ComponentModel.DataAnnotations; namespace TaskManager.Models { public class TaskItem { public int Id { get; set; } [Required(ErrorMessage = "任务标题是必填项")] [StringLength(100, ErrorMessage = "标题长度不能超过100个字符")] public string Title { get; set; } = string.Empty; [Display(Name = "任务描述")] public string? Description { get; set; } [Display(Name = "是否完成")] public bool IsCompleted { get; set; } [Display(Name = "创建时间")] [DataType(DataType.DateTime)] public DateTime CreatedDate { get; set; } = DateTime.Now; } }[Required],[StringLength]等注解会在表单提交时被 MVC 框架自动验证,并通过ModelState.IsValid反映结果。[Display]注解则用于在视图中生成更友好的字段标签。
控制器作为交通指挥,它不关心数据具体怎么存(那是 DbContext 的事),也不关心页面具体长什么样(那是视图的事),它只负责流程控制:接收什么请求,调用什么数据,返回什么视图或结果。这种清晰的分离正是 MVC 模式的价值。
4. 视图与前端呈现:用 Bootstrap 快速搭建可用的 UI
视图是 MVC 中的“V”,负责将数据渲染成 HTML 页面。我们将使用 ASP.NET Core 的 Razor 语法和 Bootstrap 5 框架来构建界面。目标是:创建与控制器 Action 对应的视图文件,实现任务列表、创建表单、编辑表单等页面,并利用 Bootstrap 使其美观且响应式。
4.1 布局与全局样式
首先,我们有一个共享的布局文件Views/Shared/_Layout.cshtml。在创建 MVC 项目时,这个文件可能不存在,我们可以从 Razor Pages 模板的Pages/Shared/_Layout.cshtml复制过来,或者手动创建。这里我们创建一个简化版,并集成 Bootstrap。
- 安装 Bootstrap:最简单的方式是使用 LibMan(库管理器)。在项目根目录创建
libman.json:
{ "version": "1.0", "defaultProvider": "cdnjs", "libraries": [ { "library": "bootstrap@5.3.0", "destination": "wwwroot/lib/bootstrap/" }, { "library": "jquery@3.7.1", "destination": "wwwroot/lib/jquery/" }, { "library": "jquery-validation@1.19.5", "destination": "wwwroot/lib/jquery-validation/" }, { "library": "jquery-validation-unobtrusive@4.0.0", "destination": "wwwroot/lib/jquery-validation-unobtrusive/" } ] }然后在命令行运行dotnet tool install -g Microsoft.Web.LibraryManager.Cli和libman restore,或者直接在 Visual Studio 的“解决方案资源管理器”中右键点击libman.json选择“恢复客户端库”。
- 创建
_Layout.cshtml:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - 任务管理器</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> <div class="container-fluid"> <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">任务管理器</a> <button class="navbar-toggler" type="button">@model IEnumerable<TaskManager.Models.TaskItem> @{ ViewData["Title"] = "任务列表"; } <h1>任务列表</h1> <p> <a asp-action="Create" class="btn btn-primary">创建新任务</a> </p> <table class="table table-striped table-hover"> <thead> <tr> <th>@Html.DisplayNameFor(model => model.Title)</th> <th>@Html.DisplayNameFor(model => model.Description)</th> <th>@Html.DisplayNameFor(model => model.IsCompleted)</th> <th>@Html.DisplayNameFor(model => model.CreatedDate)</th> <th>操作</th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td>@Html.DisplayFor(modelItem => item.Title)</td> <td>@Html.DisplayFor(modelItem => item.Description)</td> <td> <input class="form-check-input" type="checkbox" disabled @(item.IsCompleted ? "checked" : "")> </td> <td>@Html.DisplayFor(modelItem => item.CreatedDate)</td> <td> <a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">编辑</a> <a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-info">详情</a> <a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-outline-danger">删除</a> </td> </tr> } </tbody> </table>@model IEnumerable<TaskManager.Models.TaskItem>:指定此视图的模型类型是TaskItem的集合,这与控制器IndexAction 中return View(tasks);传递的类型一致。Html.DisplayNameFor和Html.DisplayFor是 HTML Helper,它们会使用 Model 上定义的[Display]注解来显示字段名和值,更规范。asp-route-id="@item.Id"用于在链接中传递参数(如任务的 Id)。
4.3 创建任务表单视图 (Create.cshtml 和 Edit.cshtml)
这两个视图非常相似,都是用来展示和提交表单的。我们以Create.cshtml为例:
@model TaskManager.Models.TaskItem @{ ViewData["Title"] = "创建任务"; } <h1>创建新任务</h1> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Create"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group mb-3"> <label asp-for="Title" class="control-label"></label> <input asp-for="Title" class="form-control" /> <span asp-validation-for="Title" class="text-danger"></span> </div> <div class="form-group mb-3"> <label asp-for="Description" class="control-label"></label> <textarea asp-for="Description" class="form-control"></textarea> <span asp-validation-for="Description" class="text-danger"></span> </div> <div class="form-group form-check mb-3"> <label class="form-check-label"> <input class="form-check-input" asp-for="IsCompleted" /> @Html.DisplayNameFor(model => model.IsCompleted) </label> </div> <div class="form-group"> <input type="submit" value="创建" class="btn btn-primary" /> <a asp-action="Index" class="btn btn-secondary">返回列表</a> </div> </form> </div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }这是整个 MVC 数据流闭环的关键视图:
- 表单标签:
<form asp-action="Create">会生成一个action指向/Tasks/Create的 POST 表单。 - 输入标签:
<input asp-for="Title">是 Tag Helper,它会根据TaskItem模型的Title属性,生成具有正确id,name和value的 HTML 输入框。name属性必须与控制器 Action 参数[Bind]列表中的属性名匹配,模型绑定才能成功。 - 验证标签:
<span asp-validation-for="Title">用于显示该字段的验证错误信息。<div asp-validation-summary="ModelOnly">用于显示模型级别的错误。 - 验证脚本:
@section Scripts引入了 jQuery 验证脚本,实现了客户端验证。用户在提交前就能看到错误提示,体验更好。服务端验证(ModelState.IsValid)仍然是最终保障。
Edit.cshtml视图几乎相同,只是form的asp-action变为Edit,并且通常需要一个隐藏域<input type="hidden" asp-for="Id" />来传递主键。
4.4 运行与调试
现在,所有部件都已就位。在命令行运行dotnet run,或从 IDE 启动项目。浏览器打开https://localhost:5001(具体端口看控制台输出),点击导航栏的“任务列表”和“新建任务”,尝试创建、编辑、删除任务。
你可能遇到的典型问题及排查思路:
- 404 错误:检查
Program.cs中的默认路由{controller=Home}/{action=Index}/{id?}。访问/Tasks时,会匹配到TasksController.Index()。 - 数据库连接错误:检查
appsettings.json中的连接字符串,特别是(localdb)\\mssqllocaldb的转义和实例名。查看输出窗口或日志,看是否有详细的 SQL 异常。 - 迁移未应用:确保执行了
dotnet ef database update。检查数据库TaskManagerDB和表Tasks是否已创建。 - 视图找不到:确保视图文件(
.cshtml)放在正确的Views/[ControllerName]文件夹下,且文件名与 Action 名一致(Index.cshtml,Create.cshtml)。
5. 从“跑通”到“可用”:全栈思维的深化与工程化考量
一个能运行的项目只是一个开始。要让这个“玩具”项目具备“可用”的雏形,我们需要思考几个更深层的问题,这恰恰是全栈开发者与功能实现者的分水岭。
5.1 理解数据流与职责分离
回顾我们构建的整个流程:
- 用户在浏览器点击“新建任务”(
/Tasks/CreateGET)。 - MVC 路由将请求路由到
TasksController.Create()GET 方法。 - 控制器执行方法,返回
Create.cshtml视图。 - 视图引擎渲染视图,生成包含空表单的 HTML 返回给浏览器。
- 用户填写表单并提交(
/Tasks/CreatePOST)。 - 模型绑定器将表单数据绑定到
TaskItem对象。 - 控制器再次执行
Create()POST 方法,进行模型验证,通过后调用_context.Add()和SaveChangesAsync()。 - EF Core将
TaskItem对象的状态变化翻译成 SQLINSERT语句,通过 ADO.NET 发送给SQL Server。 - SQL Server执行插入,返回结果。
- 控制器收到成功结果后,发出
RedirectToAction("Index")指令(HTTP 302)。 - 浏览器重定向到
/Tasks/Index,开始新一轮的请求-响应循环,展示更新后的列表。
每一层各司其职:
- 视图 (View):只负责展示和收集数据。不包含业务逻辑。
- 控制器 (Controller):协调者。接收请求,调用业务逻辑(本例中直接用了 DbContext,更复杂的项目会有 Service 层),决定返回什么响应(视图、JSON、重定向)。
- 模型 (Model):包含业务数据(实体类)和业务规则(验证注解)。
- EF Core (ORM):数据访问抽象层。将对象操作转换为 SQL。
- SQL Server (Database):数据的持久化存储。
理解这个闭环,你就理解了 MVC 和全栈协作的基本原理。
5.2 引入服务层:解耦控制器与数据访问
在当前的TasksController中,我们直接使用了AppDbContext。对于小型应用这没问题,但随着业务复杂,控制器会变得臃肿,且难以进行单元测试(因为控制器与数据库紧耦合)。更佳实践是引入一个服务层 (Service Layer)。
- 创建服务接口与实现: 在项目根目录创建
Services文件夹,添加ITaskService.cs和TaskService.cs。
// ITaskService.cs using TaskManager.Models; namespace TaskManager.Services { public interface ITaskService { Task<IEnumerable<TaskItem>> GetAllTasksAsync(); Task<TaskItem?> GetTaskByIdAsync(int id); Task CreateTaskAsync(TaskItem taskItem); Task UpdateTaskAsync(TaskItem taskItem); Task DeleteTaskAsync(int id); } } // TaskService.cs using Microsoft.EntityFrameworkCore; using TaskManager.Models; namespace TaskManager.Services { public class TaskService : ITaskService { private readonly AppDbContext _context; public TaskService(AppDbContext context) { _context = context; } public async Task<IEnumerable<TaskItem>> GetAllTasksAsync() { return await _context.Tasks.OrderByDescending(t => t.CreatedDate).ToListAsync(); } // 实现其他接口方法... } }- 在
Program.cs中注册服务:
builder.Services.AddScoped<ITaskService, TaskService>();- 修改控制器,注入服务:
public class TasksController : Controller { private readonly ITaskService _taskService; public TasksController(ITaskService taskService) { _taskService = taskService; } // Action 方法改为调用 _taskService public async Task<IActionResult> Index() { var tasks = await _taskService.GetAllTasksAsync(); return View(tasks); } }这样做的好处:
- 单一职责:控制器只关注 HTTP 和视图,服务类关注业务逻辑。
- 可测试性:可以轻松为服务类编写单元测试,或用 Mock 服务测试控制器。
- 可维护性:数据访问逻辑变化时,只需修改服务类,不影响控制器。
5.3 前端交互增强:使用 AJAX 提升体验
目前每次操作(创建、编辑、删除)都会导致页面完全刷新。我们可以用 jQuery 或 Fetch API 配合部分 Action 实现无刷新操作。
例如,实现一个异步删除:
- 在
TasksController中添加一个返回 JSON 的 Action:
[HttpDelete] public async Task<IActionResult> DeleteConfirmed(int id) { var taskItem = await _context.Tasks.FindAsync(id); if (taskItem == null) { return Json(new { success = false, message = "任务未找到。" }); } _context.Tasks.Remove(taskItem); await _context.SaveChangesAsync(); return Json(new { success = true, message = "删除成功。" }); }- 在
Index.cshtml的删除按钮上绑定点击事件,发送 AJAX 请求,并根据结果更新 DOM。
这引入了前后端分离的雏形,后端提供 API,前端负责交互和渲染。这也是现代 Web 开发的一种常见模式。
5.4 部署与后续方向
要让这个本地应用变成真正的“全栈”应用,你还需要考虑:
- 部署:可以将应用发布到 Azure App Service、Linux 服务器(使用 Nginx 反向代理)或 Docker 容器。数据库也需要部署到云数据库(如 Azure SQL Database)或服务器上的 SQL Server。
- 安全性:添加用户认证(ASP.NET Core Identity)、授权、防跨站脚本(XSS)和跨站请求伪造(CSRF)保护(我们已用了
[ValidateAntiForgeryToken])。 - 性能:对于大量数据,
Index页面的ToListAsync()可能成为瓶颈,需要考虑分页(Skip()和Take())。 - 日志与监控:使用
ILogger接口记录日志,集成 Application Insights 等监控工具。
从“跑通”到“可用”,再到“健壮”和“高效”,每一步都需要你在理解当前技术栈的基础上,不断引入新的工程化实践。这套C# + .NET + ASP.NET Core MVC + EF Core + SQL Server的组合,为你提供了一个坚实、现代且生态丰富的起点。它不仅仅是五个技术的堆砌,而是一个完整的、可演进的 Web 应用开发生态。当你理解了数据如何从表单流向数据库,再如何从数据库流回页面,你就掌握了全栈开发最核心的脉络。接下来,就是沿着这条脉络,去填充更复杂的业务逻辑、更优美的前端组件和更稳固的架构设计了。