简介本资源是一个基于.NET 6构建的完整Web API实战项目面向C#后端开发初学者与ASP.NET Core进阶学习者聚焦企业级API开发核心能力训练SQL Server数据库集成、JWT无状态身份认证及标准CRUD接口实现。项目包含665个文件主体为96个C#源码文件含Controller、Model、Service等分层逻辑、286个编译生成的DLL库、44个配置与元数据JSON文件以及CSProj工程文件、迁移脚本和Swagger文档配置整体包体34.81MB结构清晰、模块解耦便于理解现代.NET分层架构设计。内容预览显示项目已划分SqlSugarDB、DapperCore、NET6WebApi等多个子项目涵盖数据访问、业务逻辑与API宿主同时包含.bak数据库备份与编译中间产物开箱即用。目前已有1062人学习下载读者可直接运行调试、对照源码掌握EF Core/Dapper双数据访问方案、JWT令牌签发验证流程、全局异常处理与API版本化实践是深入理解.NET 6 Web API工程化落地的优质参考样本。1. 为什么一个“增删改查”接口要专门用 .NET 6 SQL Server JWT 重做一遍不是所有 CRUD 都叫“能上线的 CRUD”。我带过三届毕设看过上百份 WebAPI 课设——八成卡在「登录后拿不到用户身份」「删不掉数据但也不报错」「Postman 调通了前端一连就 401」。问题不在语法而在整套链路没闭环数据库连接池没配、JWT 密钥硬编码进代码、SQL Server 时间戳字段没处理、EF Core 迁移脚本一跑就丢表……这套方案不是教你怎么写GetById(int id)而是把从dotnet new webapi开始到docker run -p 5000:80 api-image跑通带权限校验的完整增删改查拆成可验证、可答辩、可部署的六个模块。适合正在赶毕设/课设、需要交一份“看起来像真做过项目”的同学也适合刚转 .NET 的开发者补全生产级 API 的落地细节。它不讲 JWT 原理但告诉你AddJwtBearer()里哪三个参数改错会导致前端永远收不到 token不展开 SQL Server 的事务隔离级别但明确写出TransactionScope在删除关联数据时怎么避免外键冲突。2. 技术选型与环境准备为什么是 .NET 6、SQL Server、JWT 而不是其他组合2.1 为什么锁定 .NET 6 而非 .NET 8 或 .NET Core 3.1.NET 6 是 LTS长期支持版本也是首个真正统一开发体验的版本单文件发布、最小 API 模式、原生 AOT 编译支持虽本项目未启用但为后续扩展留出余地。更重要的是它对 SQL Server 的 EF Core 6 支持最成熟——比如HasIndex(e e.Email).IsUnique()在迁移中能正确生成唯一约束而早期版本常静默失败又如DateTimeOffset类型在 SQL Server 中映射为datetimeoffset(7)精度可控避免 .NET Core 3.1 中因时区转换导致的DateTime值偏移。某高校课程设计明确要求“使用 LTS 版本”.NET 6 正好卡在这个平衡点比 .NET 5 更稳比 .NET 8 更少踩新特性坑。如果你用 .NET 8WebApplication.CreateBuilder()的配置方式已有细微变化如AddControllersAsServices()已废弃反而增加理解成本。2.2 为什么选 SQL Server 而非 SQLite 或 PostgreSQL本方案面向的是企业级课设/毕设常见场景需体现“真实数据库管理能力”。SQL Server 提供图形化 SSMS 工具学生可直观看到表结构、索引、执行计划其 T-SQL 语法如MERGE、OUTPUT子句在课程教学中覆盖度高且与 Windows 环境天然兼容——本地开发用 LocalDB轻量版 SQL Server部署时无缝切换至远程 SQL Server 实例无需改连接字符串逻辑。对比 SQLite虽免安装但不支持存储过程、行级安全策略无法体现“数据库权限设计”这一关键得分点PostgreSQL 虽开源强大但 Windows 下安装配置复杂学生常卡在pg_hba.conf权限或psql命令行导入失败。本方案中所有 SQL 脚本建库、建表、初始化数据均按 SQL Server 2019 语法编写含CREATE SEQUENCE用于主键生成ADD CONSTRAINT显式声明外键全部可直接粘贴进 SSMS 执行。2.3 为什么用 JWT 而非 Session 或 Cookie 认证课设答辩时导师常问“前后端分离怎么鉴权”JWT 是标准答案。它无状态、可跨域、易调试token 可直接在 jwt.io 解码且 .NET 6 内置支持完善。关键在于——它强制你思考安全边界密钥不能硬编码、token 过期时间必须可配、刷新机制需设计。本方案采用对称密钥HMAC-SHA256因课设场景无需公私钥体系密钥通过IConfiguration读取支持从appsettings.json、环境变量、Azure Key Vault预留扩展点注入token payload 中明确包含UserId、Role、jti防重放避免常见漏洞。注意不推荐在课设中用Microsoft.AspNetCore.Identity全家桶——它抽象层太厚学生容易陷入UserManager和SignInManager的调用迷宫反而说不清“token 到底在哪一步生成”。提示本方案所有技术选型均以“可解释、可演示、可截图答辩”为第一原则。比如 SQL Server 的“活动监视器”能实时展示连接数JWT 的 token 可用 Postman 的 “Authorization → Bearer Token” 直接粘贴测试——这些细节都是答辩时让导师点头的关键帧。3. 总体架构设计分层清晰、职责分明的六模块结构3.1 整体分层Controller → Service → Repository → Entity → DTO → Auth本方案采用经典六层结构但去掉过度设计的IUnitOfWork和IGenericRepository——课设阶段过度抽象反而增加理解负担。实际分层如下Controller 层仅负责 HTTP 协议适配接收请求、返回状态码、绑定模型不做业务逻辑。所有 Action 方法标注[Authorize]或[AllowAnonymous][ApiController]启用自动模型验证。Service 层核心业务逻辑所在。例如UserService.RegisterAsync()封装密码哈希、邮箱去重、角色分配OrderService.DeleteAsync()先检查订单状态再软删除。每个 Service 依赖IRepositoryT但 Repository 接口由 Service 定义而非全局定义避免“为抽象而抽象”。Repository 层基于 EF Core 的DbContext封装。不暴露DbSetT只提供GetByIdAsync()、ListAsync()、AddAsync()等方法。关键点所有查询方法默认.AsNoTracking()提高读性能写操作显式调用SaveChangesAsync()并捕获DbUpdateConcurrencyException。Entity 层纯 C# 类对应数据库表。关键设计Id为主键int或GuidCreatedAt/UpdatedAt为DateTimeOffsetIsDeleted用于软删除配合全局查询过滤器。DTO 层UserCreateDto、UserResponseDto等用于 Controller 与 Service 间数据传递。严格区分输入含[Required]验证和输出不含敏感字段如密码哈希。Auth 层独立JwtService类封装GenerateToken()、ValidateToken()、RefreshToken()。GenerateToken()中SecurityTokenDescriptor的Subject设置为new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Email) })确保User.Identity.Name可用。3.2 数据库实体设计兼顾范式与课设实用性以User和Post为例典型一对多关系实体类设计直击课设高频需求// Entities/User.cs public class User { public int Id { get; set; } [Required, EmailAddress, MaxLength(256)] public string Email { get; set; } null!; [Required, MinLength(6)] public string PasswordHash { get; set; } null!; public string FullName { get; set; } null!; public bool IsActive { get; set; } true; public DateTimeOffset CreatedAt { get; set; } DateTimeOffset.UtcNow; public DateTimeOffset? UpdatedAt { get; set; } public bool IsDeleted { get; set; } false; // 导航属性 public ICollectionPost Posts { get; set; } new ListPost(); } // Entities/Post.cs public class Post { public int Id { get; set; } [Required, MaxLength(100)] public string Title { get; set; } null!; [Required] public string Content { get; set; } null!; public int UserId { get; set; } // 外键 public User User { get; set; } null!; // 导航属性 public DateTimeOffset CreatedAt { get; set; } DateTimeOffset.UtcNow; public DateTimeOffset? UpdatedAt { get; set; } public bool IsDeleted { get; set; } false; }设计说明Email加[EmailAddress]验证PasswordHash不加[Required]因注册时传明文Service 层哈希后赋值避免模型绑定失败CreatedAt默认DateTimeOffset.UtcNowEF Core 迁移时生成GETUTCDATE()默认值解决本地时区偏差IsDeleted配合DbContext.OnModelCreating()中的全局查询过滤器modelBuilder.EntityUser().HasQueryFilter(u !u.IsDeleted)实现软删除——这是课设中体现“数据安全意识”的加分项UserId与User导航属性并存既满足外键约束又支持.Include(u u.Posts)预加载。3.3 JWT 认证流程图解从登录到接口调用的完整链路认证不是黑匣子。本方案将 JWT 流程拆解为可验证的四步登录请求POST /api/auth/loginBody 为{ email: ab.com, password: 123 }→ Controller 调用AuthService.LoginAsync()→ Service 校验邮箱密码 → 成功则调用JwtService.GenerateToken(user)生成 token。Token 生成JwtService.GenerateToken()构造SecurityTokenDescriptorvar tokenDescriptor new SecurityTokenDescriptor { Subject new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Email), new Claim(ClaimTypes.Role, user.Role), // 如 Admin new Claim(UserId, user.Id.ToString()), // 自定义声明Service 层常用 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // jti 防重放 }), Expires DateTime.UtcNow.AddMinutes(_jwtSettings.ExpiryInMinutes), // 从配置读取 SigningCredentials new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)), SecurityAlgorithms.HmacSha256Signature) };关键点Expires必须动态读取配置非硬编码jti每次登录唯一UserId声明便于后续接口直接获取 ID。响应返回Controller 返回Ok(new { token, expiresIn _jwtSettings.ExpiryInMinutes * 60 })前端存入 localStorage。受保护接口调用GET /api/posts请求头Authorization: Bearer token→[Authorize]特性触发JwtBearerHandler→ 验证签名、过期、jti需自行实现黑名单本方案用内存缓存ConcurrentDictionarystring, bool存已注销 token→ 验证通过后HttpContext.User可通过User.FindFirstValue(ClaimTypes.Name)获取邮箱。注意课设中务必演示jti黑名单机制。可在AuthService.LogoutAsync()中将当前 token 的jti加入缓存并在JwtBearerOptions.Events.OnTokenValidated中检查——这是体现“安全深度”的硬核细节。4. 核心模块实现从数据库迁移、JWT 配置到增删改查接口4.1 SQL Server 连接与 EF Core 迁移零错误建库脚本连接字符串必须支持开发与部署切换。appsettings.json中配置{ ConnectionStrings: { DefaultConnection: Server(localdb)\\mssqllocaldb;DatabaseApiDb;Trusted_Connectiontrue;MultipleActiveResultSetstrue }, JwtSettings: { Key: your-32-byte-secret-key-here-12345678901234567890123456789012, ExpiryInMinutes: 60 } }关键点MultipleActiveResultSetstrue启用 MARS避免DataReader未关闭导致的并发异常Trusted_Connectiontrue本地开发用 Windows 认证部署时改为User IDsa;PasswordxxxJWT 密钥长度必须为 32 字节256 位否则SymmetricSecurityKey构造失败——这是学生最常翻车点。EF Core 迁移命令在项目根目录执行# 初始化迁移生成第一个迁移文件 dotnet ef migrations add InitialCreate --project .\Api.csproj --startup-project .\Api.csproj # 应用迁移创建数据库和表 dotnet ef database update --project .\Api.csproj --startup-project .\Api.csproj生成的迁移文件InitialCreate.cs中Up(MigrationBuilder migrationBuilder)方法会自动添加migrationBuilder.CreateTable( name: Users, columns: table new { Id table.Columnint(type: int, nullable: false) .Annotation(SqlServer:Identity, 1, 1), Email table.Columnstring(type: nvarchar(256), maxLength: 256, nullable: false), PasswordHash table.Columnstring(type: nvarchar(max), nullable: false), FullName table.Columnstring(type: nvarchar(max), nullable: true), IsActive table.Columnbool(type: bit, nullable: false, defaultValue: true), CreatedAt table.ColumnDateTimeOffset(type: datetimeoffset, nullable: false, defaultValueSql: GETUTCDATE()), UpdatedAt table.ColumnDateTimeOffset(type: datetimeoffset, nullable: true), IsDeleted table.Columnbool(type: bit, nullable: false, defaultValue: false) }, constraints: table { table.PrimaryKey(PK_Users, x x.Id); table.UniqueConstraint(AK_Users_Email, x x.Email); // 唯一索引 });设计说明defaultValueSql: GETUTCDATE()确保数据库层面生成时间避免应用服务器时区差异UniqueConstraint显式声明邮箱唯一比[Index]特性更可靠。4.2 JWT 认证中间件配置三处关键代码在Program.cs中配置 JWT共三处不可省略// 1. 注册服务在 builder.Services 里 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer false, // 课设通常不验 Issuer ValidateAudience false, // 同上 ValidateLifetime true, // 必须为 true ValidateIssuerSigningKey true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[JwtSettings:Key]!)), ClockSkew TimeSpan.Zero // 严格校验过期避免宽容导致安全隐患 }; // 2. 添加事件验证通过后检查 jti 是否在黑名单 options.Events new JwtBearerEvents { OnTokenValidated context { var jti context.Principal?.FindFirstValue(JwtRegisteredClaimNames.Jti); if (jti ! null context.HttpContext.RequestServices .GetRequiredServiceIJwtBlacklistService() .IsBlacklisted(jti)) { context.Fail(Token has been revoked); } return Task.CompletedTask; } }; }); // 3. 启用认证中间件在 app.UseAuthentication() 之后 app.UseAuthentication(); app.UseAuthorization();避坑 / 常见问题 / 排查现象前端调用/api/posts返回 401但 token 在 jwt.io 解码正常。原因ClockSkew默认为 5 分钟若服务器时间与客户端偏差过大或 token 刚生成就立即使用毫秒级偏差可能被判定为“尚未生效”。解决设ClockSkew TimeSpan.Zero并确保服务器时间同步Windows 下运行w32tm /resync。现象HttpContext.User.Identity.IsAuthenticated始终为false。原因UseAuthentication()调用位置错误必须在UseRouting()之后、UseEndpoints()之前。解决严格按顺序UseRouting()→UseAuthentication()→UseAuthorization()→UseEndpoints()。现象User.FindFirstValue(ClaimTypes.Name)返回 null。原因Subject中未添加ClaimTypes.Name声明或ClaimTypes.Name拼写错误如写成name字符串。解决Subject必须用ClaimTypes.Name常量且ClaimsIdentity构造时传入数组。现象dotnet ef database update报错 “The underlying provider does not support the GetDatabaseName method”。原因DbContext构造函数中未正确注入DbContextOptions或OnConfiguring覆盖了依赖注入配置。解决删除OnConfiguring在Startup.cs或Program.cs中用options.UseSqlServer(connectionString)配置。现象POST /api/users创建用户后Email字段在数据库中为 NULL。原因DTO 中Email属性未加[Required]或Controller的[FromBody]绑定失败后静默忽略。解决DTO 必须加完整验证特性Controller 方法添加if (!ModelState.IsValid) return BadRequest(ModelState);。5. 接口实现与测试覆盖增删改查全场景的 Controller 代码5.1 用户注册与登录接口带密码哈希与邮箱验证AuthController.cs实现[ApiController] [Route(api/[controller])] public class AuthController : ControllerBase { private readonly IUserService _userService; private readonly IJwtService _jwtService; public AuthController(IUserService userService, IJwtService jwtService) { _userService userService; _jwtService jwtService; } [HttpPost(register)] [AllowAnonymous] public async TaskActionResult Register([FromBody] UserRegisterDto dto) { if (!ModelState.IsValid) return BadRequest(ModelState); try { var user await _userService.RegisterAsync(dto); var token _jwtService.GenerateToken(user); return Ok(new { token, expiresIn _jwtService.GetExpirySeconds() }); } catch (InvalidOperationException ex) when (ex.Message.Contains(email)) { return BadRequest(new { message Email already exists }); } catch (Exception ex) { // 生产环境应记录日志课设可简化 return StatusCode(500, new { message Registration failed }); } } [HttpPost(login)] [AllowAnonymous] public async TaskActionResult Login([FromBody] UserLoginDto dto) { if (!ModelState.IsValid) return BadRequest(ModelState); var user await _userService.ValidateCredentialsAsync(dto.Email, dto.Password); if (user null) return Unauthorized(new { message Invalid email or password }); var token _jwtService.GenerateToken(user); return Ok(new { token, expiresIn _jwtService.GetExpirySeconds() }); } }关键设计点RegisterAsync()内部调用PasswordHasherUser.HashPassword()而非BCrypt.Net等第三方库减少依赖ValidateCredentialsAsync()使用PasswordHasherUser.VerifyHashedPassword()校验避免明文比对异常处理区分业务异常邮箱重复与系统异常返回不同状态码和消息方便前端提示。5.2 文章增删改查接口带软删除与权限控制PostsController.cs实现管理员可删他人文章普通用户只能删自己[ApiController] [Route(api/[controller])] [Authorize] // 全局授权 public class PostsController : ControllerBase { private readonly IPostService _postService; public PostsController(IPostService postService) { _postService postService; } [HttpGet] public async TaskActionResultIEnumerablePostResponseDto GetAll() { var posts await _postService.GetAllAsync(); return Ok(posts); } [HttpGet({id:int})] public async TaskActionResultPostResponseDto GetById(int id) { var post await _postService.GetByIdAsync(id); if (post null) return NotFound(); return Ok(post); } [HttpPost] public async TaskActionResultPostResponseDto Create([FromBody] PostCreateDto dto) { // 从 JWT 中提取 UserId var userId int.Parse(User.FindFirstValue(UserId)!); var post await _postService.CreateAsync(dto, userId); return CreatedAtAction(nameof(GetById), new { id post.Id }, post); } [HttpPut({id:int})] public async TaskIActionResult Update(int id, [FromBody] PostUpdateDto dto) { var userId int.Parse(User.FindFirstValue(UserId)!); var success await _postService.UpdateAsync(id, dto, userId); if (!success) return Forbid(); // 无权修改他人文章 return NoContent(); } [HttpDelete({id:int})] public async TaskIActionResult Delete(int id) { var userId int.Parse(User.FindFirstValue(UserId)!); var role User.FindFirstValue(ClaimTypes.Role); var success await _postService.DeleteAsync(id, userId, role Admin); if (!success) return Forbid(); return NoContent(); } }设计说明CreateAsync()和UpdateAsync()的userId参数来自 JWT 声明而非请求 Body防止前端伪造DeleteAsync()接收isAdmin标志Service 层据此决定是否跳过用户 ID 校验CreatedAtAction()返回 201 状态码及 Location 头符合 REST 规范Postman 可直接点击跳转。5.3 Service 层关键逻辑软删除与并发控制PostService.cs中DeleteAsync实现public async Taskbool DeleteAsync(int id, int userId, bool isAdmin) { var post await _context.Posts .FirstOrDefaultAsync(p p.Id id !p.IsDeleted); if (post null) return false; // 权限校验 if (!isAdmin post.UserId ! userId) return false; // 软删除更新 IsDeleted 和 UpdatedAt post.IsDeleted true; post.UpdatedAt DateTimeOffset.UtcNow; try { await _context.SaveChangesAsync(); return true; } catch (DbUpdateConcurrencyException) { // 处理并发检查记录是否已被删除 var entry _context.Entry(post); if (entry.State EntityState.Detached) { // 记录已不存在视为删除成功 return true; } throw; } }为什么不用物理删除课设答辩时导师会问“如何保证数据可追溯”。软删除 UpdatedAt字段配合数据库审计日志SSMS 中可右键表 → “查看更改跟踪”能完整回答此问题。物理删除则无法回溯。6. 验收与答辩用这五步让导师当场打满分6.1 本地全流程验收清单每一步都可截图演示别只说“功能正常”要让导师亲眼看到每个环节的输出。按顺序执行以下五步每步截图保存建议用 Snipaste 截图带窗口标题栏步骤操作预期结果截图重点1. 数据库验证打开 SSMS → 连接(localdb)\mssqllocaldb→ 展开ApiDb→ 查看Users表数据显示Id,Email,PasswordHash哈希值非明文、CreatedAtUTC 时间表数据网格 列名突出IsDeleted 02. 注册登录验证Postman 发送POST /api/auth/register→POST /api/auth/login登录响应中token字段存在expiresIn为3600Postman 响应 JSON高亮token和expiresIn3. Token 解码验证将上步 token 粘贴到 https://jwt.ioHeader 显示HS256Payload 显示name,role,UserId,jti,exp时间戳转为北京时间jwt.io 界面Payload 区域标出jti和exp4. 受保护接口验证PostmanGET /api/postsHeaders 加Authorization: Bearer token返回200 OK及文章列表 JSONPostman 请求头 响应状态码 JSON 数据5. 权限拦截验证修改 Postman 中 token 的jti值任意改一个字符→ 再发GET /api/posts返回401 Unauthorized响应体含message: Invalid tokenPostman 响应状态码 错误消息提示答辩前务必用另一台电脑或虚拟机重装一遍环境从dotnet new webapi开始走完上述五步。很多同学答辩时发现“本地能跑换台电脑就崩”根源是appsettings.json里写了绝对路径或硬编码密钥。6.2 答辩话术设计用三个问题预判导师关注点导师不会逐行看代码但会问本质问题。提前准备好这三问的回答能极大提升专业感Q1“JWT 密钥怎么保证安全如果代码泄露密钥不就暴露了吗”A密钥绝不提交到 Git。我们在.gitignore中加入appsettings.Production.json生产环境密钥通过 Azure Key Vault 或 Docker 环境变量注入。课设中我们演示了两种方式开发用appsettings.Development.json密钥明文但 .gitignore 已排除部署时用--environment Production启动密钥从环境变量JWT_KEY读取。代码中builder.Configuration[JwtSettings:Key]会自动优先读取环境变量。Q2“软删除后统计文章总数时会不会把已删文章也算进去”A不会。我们在PostService.GetAllAsync()中使用await _context.Posts.Where(p !p.IsDeleted).ToListAsync()EF Core 生成的 SQL 自动加上WHERE [IsDeleted] 0。更重要的是我们在DbContext.OnModelCreating()中配置了全局查询过滤器modelBuilder.EntityPost().HasQueryFilter(p !p.IsDeleted)这样所有DbSetPost查询都默认过滤已删数据避免遗漏。Q3“如果两个用户同时编辑同一篇文章怎么保证数据不被覆盖”A我们实现了乐观并发控制。在Post实体中添加public byte[] RowVersion { get; set; }属性并在OnModelCreating中配置.IsRowVersion()。更新时EF Core 自动生成WHERE [RowVersion] p0条件若数据库中RowVersion已变被他人更新则抛出DbUpdateConcurrencyException。Service 层捕获此异常返回409 Conflict前端可提示“内容已被他人修改请刷新后重试”。6.3 代码质量自检答辩前必做的三件事别让低级错误毁掉努力。每次答辩前强制执行删掉所有Console.WriteLine和Debug.WriteLine课设中常见“调试语句忘删”导致接口响应体混入乱码。用 VS 的“查找全部”搜索WriteLine确认只在Program.cs的builder.Host.ConfigureLogging中有日志配置。检查appsettings.json的ConnectionStrings和JwtSettings是否为空用 Notepad 打开确认DefaultConnection值非空字符串Key长度为 32 字符可用在线工具 https://www.browserling.com/tools/text-to-hex 验证。用dotnet publish -c Release -r win-x64打包然后在干净 Windows 机器上运行Api.exe这是终极考验。若失败90% 是Microsoft.Data.SqlClient未正确打包.NET 6 默认不包含该 NuGet需在.csproj中显式添加PackageReference IncludeMicrosoft.Data.SqlClient Version5.1.5 /。从那以后我每次交课设都强制走一遍这三步先本地五步验收截图再模拟导师问三个问题最后 clean machine 打包验证。不是为了应付而是让“能跑”变成“经得起推敲”。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?