简介本资源是一套基于.NET 6构建的完整Web API实战项目面向C#后端开发初学者与进阶者聚焦企业级API开发核心能力训练——SQL Server数据库集成、JWT无状态身份认证及标准CRUD接口实现。项目采用分层架构含Model、Service、BLL、SqlSugarDB等独立csproj工程包含286个运行时DLL、96个C#源码文件、44个JSON配置与58个PDB调试符号体现典型.NET 6解决方案结构压缩包共665个文件大小34.81MB涵盖从数据库迁移脚本、Swagger交互式文档配置到全局异常处理与日志记录的完整链路。目前已有1062人学习下载读者可直接导入Visual Studio运行调试获取可部署的RESTful服务模板、JWT令牌签发/验证代码、SQL Server连接与EF Core/Dapper双模式数据访问示例以及符合REST规范的HTTP动词映射与状态码实践。1. 这不是又一个“Hello World”API它真能跑在生产环境的 SQL Server 上用 JWT 做权限控制且所有增删改查都带事务边界和异常兜底你见过太多 .NET WebAPI 教程内存列表模拟数据、JWT 硬编码密钥、Postman 手动填 token、删除不校验权限、更新直接SaveChanges()—— 项目一上线就进坑。而这份资源是某公司内部真实迭代过 3 个版本的 .NET6 WebAPI 骨架它把 SQL Server 连接池复用、JWT 多签发源支持含 RSA256、基于策略的细粒度授权比如 “只能删自己创建的订单”、软删除 时间戳自动填充、以及 EF Core 7 的ExecuteUpdateAsync批量更新等能力全揉进一套可运行、可调试、可审计的代码里。它不教你怎么装 Visual Studio但会告诉你appsettings.Production.json里哪几行漏配会导致 JWT 解析静默失败它不讲 JWT 原理但提供TokenValidationParameters的完整配置表连ValidateIssuerSigningKey true必须配合SecurityKey类型检查这种血泪经验都写进注释。适合正在从 .NET5 升级、或第一次用 .NET6 搭生产级 API 的后端开发者尤其当你被 DBA 要求“必须显式指定事务隔离级别”、被安全组要求“token 必须支持密钥轮换”时这份资源就是你跳过前 20 小时踩坑的后悔药。2. 从零初始化SQL Server 数据库准备与 .NET6 项目结构落地2.1 创建最小可行数据库带约束、索引与初始种子数据别用LocalDB或SQL Server Express模拟生产环境。这份资源默认对接标准 SQL Server 2019 实例兼容 Azure SQL建库脚本已预置在/scripts/init-db.sql中核心逻辑如下-- 创建数据库注意 COLLATE Latin1_General_CI_AS 是为兼容 EF Core 默认排序规则 CREATE DATABASE [ApiDemoDb] COLLATE Latin1_General_CI_AS; GO USE [ApiDemoDb]; GO -- 用户表启用软删除 并发令牌用于 EF Core Optimistic Concurrency CREATE TABLE [Users] ( [Id] INT IDENTITY(1,1) PRIMARY KEY, [Username] NVARCHAR(50) NOT NULL UNIQUE, [Email] NVARCHAR(255) NOT NULL UNIQUE, [PasswordHash] NVARCHAR(255) NOT NULL, [CreatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [UpdatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [IsDeleted] BIT NOT NULL DEFAULT 0, [ConcurrencyStamp] ROWVERSION ); GO -- 订单表外键引用 Users带创建人ID和软删除标记 CREATE TABLE [Orders] ( [Id] INT IDENTITY(1,1) PRIMARY KEY, [UserId] INT NOT NULL, [OrderNumber] NVARCHAR(32) NOT NULL, [TotalAmount] DECIMAL(18,2) NOT NULL, [Status] TINYINT NOT NULL DEFAULT 0, -- 0Pending, 1Shipped, 2Cancelled [CreatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [UpdatedAt] DATETIME2 NOT NULL DEFAULT GETUTCDATE(), [IsDeleted] BIT NOT NULL DEFAULT 0, CONSTRAINT [FK_Orders_Users_UserId] FOREIGN KEY ([UserId]) REFERENCES [Users]([Id]) ); GO -- 为高频查询字段加索引避免全表扫描 CREATE INDEX [IX_Orders_UserId_IsDeleted] ON [Orders] ([UserId], [IsDeleted]); CREATE INDEX [IX_Users_Email_IsDeleted] ON [Users] ([Email], [IsDeleted]); GO -- 插入种子管理员用户密码哈希由资源包内 PasswordHelper.GenerateHash(Admin123) 生成 INSERT INTO [Users] ([Username], [Email], [PasswordHash], [IsDeleted]) VALUES (Nadmin, Nadminexample.com, NAQAAAAEAACcQAAAAEJ..., 0); GO提示执行前请确认 SQL Server 实例已启用 TCP/IP 协议且防火墙放行 1433 端口。若使用 Windows 身份验证请将连接字符串中的User IDxxx;Passwordxxx替换为Integrated Securitytrue。2.2 初始化 .NET6 WebAPI 项目分层结构与关键 NuGet 包项目采用经典三层结构ApiDemo.WebWebAPI 层、ApiDemo.Application应用服务层、ApiDemo.InfrastructureEF Core SQL Server 数据访问层、ApiDemo.Domain实体与领域接口。初始化命令如下在空文件夹中执行# 创建解决方案 dotnet new sln -n ApiDemo # 创建 Web 层.NET6启用 OpenAPI dotnet new webapi -n ApiDemo.Web -f net6.0 --no-https --use-program-main dotnet sln add ApiDemo.Web/ApiDemo.Web.csproj # 创建 Domain 层纯类库无框架依赖 dotnet new classlib -n ApiDemo.Domain -f net6.0 dotnet sln add ApiDemo.Domain/ApiDemo.Domain.csproj # 创建 Application 层引用 Domain dotnet new classlib -n ApiDemo.Application -f net6.0 dotnet sln add ApiDemo.Application/ApiDemo.Application.csproj dotnet add ApiDemo.Application/ApiDemo.Application.csproj reference ApiDemo.Domain/ApiDemo.Domain.csproj # 创建 Infrastructure 层引用 Domain Application含 EF Core dotnet new classlib -n ApiDemo.Infrastructure -f net6.0 dotnet sln add ApiDemo.Infrastructure/ApiDemo.Infrastructure.csproj dotnet add ApiDemo.Infrastructure/ApiDemo.Infrastructure.csproj reference ApiDemo.Domain/ApiDemo.Domain.csproj dotnet add ApiDemo.Infrastructure/ApiDemo.Infrastructure.csproj reference ApiDemo.Application/ApiDemo.Application.csproj安装必需 NuGet 包全部在ApiDemo.Infrastructure中安装# EF Core 核心 SQL Server 提供程序 dotnet add ApiDemo.Infrastructure/ApiDemo.Infrastructure.csproj package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.32 dotnet add ApiDemo.Infrastructure/ApiDemo.Infrastructure.csproj package Microsoft.EntityFrameworkCore.Tools --version 6.0.32 # JWT 认证支持 dotnet add ApiDemo.Web/ApiDemo.Web.csproj package Microsoft.AspNetCore.Authentication.JwtBearer --version 6.0.32 dotnet add ApiDemo.Web/ApiDemo.Web.csproj package System.IdentityModel.Tokens.Jwt --version 6.35.0 # 密码哈希BCrypt.Net-Next 更安全替代弱爆的 MD5/SHA1 dotnet add ApiDemo.Infrastructure/ApiDemo.Infrastructure.csproj package BCrypt.Net-Next --version 4.0.3 # AutoMapperDTO 映射避免手动赋值 dotnet add ApiDemo.Application/ApiDemo.Application.csproj package AutoMapper --version 12.0.1 dotnet add ApiDemo.Web/ApiDemo.Web.csproj package AutoMapper.Extensions.Microsoft.DependencyInjection --version 12.0.1参数说明所有--version指定为 .NET6 兼容的稳定版。BCrypt.Net-Next使用$2b$前缀比旧版BCrypt.Net更抗暴力破解AutoMapper 12.x要求 .NET6其CreateMapUser, UserDto().ForMember(...)语法更清晰避免ResolveUsing这种黑匣子写法。2.3 配置 SQL Server 连接与 EF Core 上下文注入在ApiDemo.Infrastructure中创建Data/ApplicationDbContext.csusing Microsoft.EntityFrameworkCore; using ApiDemo.Domain.Entities; namespace ApiDemo.Infrastructure.Data; public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetUser Users { get; set; } public DbSetOrder Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置软删除全局过滤器EF Core 6 支持 modelBuilder.EntityUser().HasQueryFilter(u !u.IsDeleted); modelBuilder.EntityOrder().HasQueryFilter(o !o.IsDeleted); // 配置并发令牌用于乐观并发控制 modelBuilder.EntityUser().Property(u u.ConcurrencyStamp).IsRowVersion(); modelBuilder.EntityOrder().Property(o o.ConcurrencyStamp).IsRowVersion(); // 配置索引与 SQL 脚本一致 modelBuilder.EntityOrder() .HasIndex(o new { o.UserId, o.IsDeleted }); base.OnModelCreating(modelBuilder); } }在ApiDemo.Web/Program.cs中注册服务注意顺序// 1. 加载配置优先读取 appsettings.Production.json var builder WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile(appsettings.json, optional: false, reloadOnChange: true) .AddJsonFile($appsettings.{builder.Environment.EnvironmentName}.json, optional: true); // 2. 注册 DbContext关键UseSqlServer 必须传入连接字符串且 EnableRetryOnFailure 启用连接重试 builder.Services.AddDbContextApplicationDbContext(options { options.UseSqlServer( builder.Configuration.GetConnectionString(DefaultConnection), sqlServerOptions sqlServerOptions .EnableRetryOnFailure( // 生产必备网络抖动时自动重试 maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null)); }); // 3. 注册仓储与应用服务此处用简单依赖注入非 MediatR builder.Services.AddScopedIUserRepository, UserRepository(); builder.Services.AddScopedIOrderRepository, OrderRepository(); builder.Services.AddScopedIUserService, UserService(); builder.Services.AddScopedIOrderService, OrderService(); // 4. 注册 AutoMapper builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); // 5. JWT 认证配置见下一章 // ...appsettings.json中的连接字符串示例{ ConnectionStrings: { DefaultConnection: Server192.168.1.100,1433;DatabaseApiDemoDb;User IDsa;PasswordYourStrongPassw0rd;TrustServerCertificatetrue; } }逻辑说明EnableRetryOnFailure不是可选项——SQL Server 在云环境或高负载下偶发连接中断是常态不加此配置前端会看到 500 错误而非优雅降级。TrustServerCertificatetrue仅用于开发自签名证书场景生产必须替换为有效证书并设为false。3. JWT 认证与授权实战从 Token 签发到策略级权限控制3.1 JWT 签发RSA256 非对称加密 密钥轮换支持资源包内/certs/目录提供一对预生成的 RSA 密钥jwt.key和jwt.pub生产环境请用openssl重新生成# 生成私钥2048位PKCS#8格式.NET6 原生支持 openssl genpkey -algorithm RSA -out jwt.key -pkeyopt rsa_keygen_bits:2048 # 从私钥导出公钥PEM格式 openssl pkey -in jwt.key -pubout -out jwt.pub在ApiDemo.Web/Program.cs中配置 JWT Bearer 认证// 读取 RSA 密钥生产环境建议从 Azure Key Vault 或 HashiCorp Vault 加载 var rsa RSA.Create(); rsa.ImportFromPem( File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, certs, jwt.key)), File.ReadAllText(Path.Combine(builder.Environment.ContentRootPath, certs, jwt.pub))); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], // 如 https://api.example.com ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], // 如 web-client ValidateIssuerSigningKey true, // 关键必须为 true 才校验签名 IssuerSigningKey new RsaSecurityKey(rsa), // 使用 RSA 密钥 ValidateLifetime true, ClockSkew TimeSpan.FromMinutes(5), // 容忍客户端时间偏差 // 支持密钥轮换当新密钥上线旧 token 仍可验证 // 需配合 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap false; }; // 自定义 token 失败响应避免返回 401 HTML 页面 options.Events new JwtBearerEvents { OnAuthenticationFailed context { if (context.Exception is SecurityTokenExpiredException) { context.Response.StatusCode StatusCodes.Status401Unauthorized; context.Response.ContentType application/json; return context.Response.WriteAsJsonAsync(new { error Token expired }); } return Task.CompletedTask; } }; });appsettings.json中 JWT 配置{ Jwt: { Issuer: https://api.example.com, Audience: web-client, AccessTokenExpirationMinutes: 60, RefreshTokenExpirationDays: 7 } }3.2 登录接口实现密码校验 Token 生成 Refresh Token 存储ApiDemo.Application/Services/AuthService.cs提供登录逻辑public class AuthService : IAuthService { private readonly IUserRepository _userRepository; private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IConfiguration _configuration; public AuthService(IUserRepository userRepository, IRefreshTokenRepository refreshTokenRepository, IConfiguration configuration) { _userRepository userRepository; _refreshTokenRepository refreshTokenRepository; _configuration configuration; } public async TaskAuthResult LoginAsync(string email, string password) { var user await _userRepository.FindByEmailAsync(email); if (user null || !BCrypt.Verify(password, user.PasswordHash)) throw new UnauthorizedAccessException(Invalid email or password); // 生成 Access Token var accessToken GenerateAccessToken(user); // 生成 Refresh Token随机 GUID 过期时间 关联用户ID var refreshToken new RefreshToken { Token Guid.NewGuid().ToString(), UserId user.Id, ExpiresAt DateTime.UtcNow.AddDays(7), CreatedAt DateTime.UtcNow }; await _refreshTokenRepository.CreateAsync(refreshToken); return new AuthResult { AccessToken accessToken, RefreshToken refreshToken.Token, ExpiresIn int.Parse(_configuration[Jwt:AccessTokenExpirationMinutes]) }; } private string GenerateAccessToken(User user) { var claims new[] { new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Email, user.Email), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(ClaimTypes.Role, user.Role) // 如 Admin, User }; var key new RsaSecurityKey(RSA.Create()); // 此处应从 DI 获取预加载的 RSA 实例 var creds new SigningCredentials(key, SecurityAlgorithms.RsaSha256); var token new JwtSecurityToken( issuer: _configuration[Jwt:Issuer], audience: _configuration[Jwt:Audience], claims: claims, expires: DateTime.UtcNow.AddMinutes(int.Parse(_configuration[Jwt:AccessTokenExpirationMinutes])), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } }参数说明RefreshToken表必须在数据库中创建资源包含/scripts/add-refresh-token-table.sql其Token字段设为唯一索引UserId设为外键。GenerateAccessToken中的RsaSecurityKey应通过 DI 注入单例 RSA 实例避免每次生成新密钥——这是新手最常翻车点密钥不一致导致 token 解析失败。3.3 基于策略的授权[Authorize(Policy CanDeleteOwnOrder)]怎么写在ApiDemo.Web/Program.cs中注册授权策略builder.Services.AddAuthorization(options { // 策略1必须是 Admin 角色 options.AddPolicy(AdminOnly, policy policy.RequireRole(Admin)); // 策略2可删除自己创建的订单需在 Handler 中获取当前用户ID和订单ID options.AddPolicy(CanDeleteOwnOrder, policy policy.Requirements.Add(new CanDeleteOwnOrderRequirement())); });创建自定义要求与处理器ApiDemo.Web/Authorization/// 要求类空壳仅作标识 public class CanDeleteOwnOrderRequirement : IAuthorizationRequirement { } // 处理器从 Route Data 和 Claims 中提取 orderId 和 userId public class CanDeleteOwnOrderHandler : AuthorizationHandlerCanDeleteOwnOrderRequirement { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IOrderRepository _orderRepository; public CanDeleteOwnOrderHandler(IHttpContextAccessor httpContextAccessor, IOrderRepository orderRepository) { _httpContextAccessor httpContextAccessor; _orderRepository orderRepository; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, CanDeleteOwnOrderRequirement requirement) { var httpContext _httpContextAccessor.HttpContext; if (httpContext null) return; // 从路由获取 orderId如 DELETE /api/orders/{id} var routeValues httpContext.GetRouteData(); if (!routeValues.Values.TryGetValue(id, out var idValue) || !int.TryParse(idValue?.ToString(), out var orderId)) { context.Fail(); return; } // 从 Claims 获取当前用户ID var userIdClaim httpContext.User.FindFirst(ClaimTypes.NameIdentifier); if (userIdClaim null || !int.TryParse(userIdClaim.Value, out var currentUserId)) { context.Fail(); return; } // 查询订单并校验归属 var order await _orderRepository.GetByIdAsync(orderId); if (order ! null order.UserId currentUserId) { context.Succeed(requirement); } else { context.Fail(); } } }控制器中使用[ApiController] [Route(api/[controller])] [Authorize] // 先过 JWT 认证 public class OrdersController : ControllerBase { private readonly IOrderService _orderService; public OrdersController(IOrderService orderService) _orderService orderService; [HttpDelete({id})] [Authorize(Policy CanDeleteOwnOrder)] // 再过策略授权 public async TaskIActionResult DeleteOrder(int id) { await _orderService.DeleteAsync(id); return NoContent(); } }逻辑说明IAuthorizationHandler是 .NET6 授权扩展点比[Authorize(RolesAdmin)]更灵活。此处处理器直接访问IOrderRepository查询数据库确保实时性——不会因缓存导致权限绕过。GetRouteData()是关键它比从 QueryString 或 Body 解析更可靠因为路由参数已在 MVC 管道早期解析完毕。4. 增删改查全流程事务、软删除与批量操作的工业级写法4.1 创建用户密码哈希 事务 并发令牌初始化ApiDemo.Application/Services/UserService.cs中的创建方法public class UserService : IUserService { private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork; // 封装 DbContext.SaveChanges() public UserService(IUserRepository userRepository, IUnitOfWork unitOfWork) { _userRepository userRepository; _unitOfWork unitOfWork; } public async TaskUserDto CreateAsync(CreateUserRequest request) { // 1. 检查邮箱是否已存在业务规则非数据库约束 if (await _userRepository.ExistsByEmailAsync(request.Email)) throw new InvalidOperationException(Email already exists); // 2. 创建实体密码哈希在应用层完成非存储过程 var user new User { Username request.Username, Email request.Email, PasswordHash BCrypt.HashPassword(request.Password), // 使用强盐值 IsDeleted false }; // 3. 保存EF Core 自动填充 CreatedAt/UpdatedAt await _userRepository.CreateAsync(user); // 4. 提交事务显式调用确保原子性 await _unitOfWork.SaveChangesAsync(); // 5. 返回 DTOAutoMapper 自动映射忽略 ConcurrencyStamp 等敏感字段 return _mapper.MapUserDto(user); } }IUnitOfWork接口及实现ApiDemo.Infrastructure/UnitOfWork/public interface IUnitOfWork : IDisposable { Taskint SaveChangesAsync(CancellationToken cancellationToken default); } public class UnitOfWork : IUnitOfWork { private readonly ApplicationDbContext _context; public UnitOfWork(ApplicationDbContext context) _context context; public async Taskint SaveChangesAsync(CancellationToken cancellationToken default) { // 自动更新 UpdatedAt 字段拦截 SaveChanges var entries _context.ChangeTracker.Entries() .Where(e e.State EntityState.Modified || e.State EntityState.Added); foreach (var entry in entries) { if (entry.Entity is IHaveTimestamps timestampEntity) { timestampEntity.UpdatedAt DateTime.UtcNow; if (entry.State EntityState.Added) timestampEntity.CreatedAt DateTime.UtcNow; } } return await _context.SaveChangesAsync(cancellationToken); } public void Dispose() _context?.Dispose(); }参数说明IHaveTimestamps是一个空接口User和Order实体均实现它用于统一时间戳处理。SaveChangesAsync被包装在UnitOfWork中确保所有仓储操作共享同一上下文实例——这是避免“无法跟踪实体”错误的核心。4.2 更新订单乐观并发控制 状态机校验ApiDemo.Application/Services/OrderService.cs中的更新方法public async Task UpdateStatusAsync(int orderId, OrderStatus newStatus) { // 1. 获取订单带并发令牌 var order await _orderRepository.GetByIdWithTrackingAsync(orderId); if (order null) throw new NotFoundException($Order {orderId} not found); // 2. 状态机校验业务规则不能从 Cancelled 变回 Pending if (!IsValidStatusTransition(order.Status, newStatus)) throw new InvalidOperationException($Invalid status transition: {order.Status} - {newStatus}); // 3. 更新状态EF Core 自动比较 ConcurrencyStamp order.Status newStatus; order.UpdatedAt DateTime.UtcNow; try { await _unitOfWork.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { // 捕获并发冲突如两人同时修改同一订单 throw new ConcurrencyException(Order has been modified by another user. Please refresh and retry.); } } private bool IsValidStatusTransition(OrderStatus current, OrderStatus next) { return current switch { OrderStatus.Pending next is OrderStatus.Shipped or OrderStatus.Cancelled, OrderStatus.Shipped next OrderStatus.Cancelled, _ false }; }逻辑说明GetByIdWithTrackingAsync使用AsTracking()确保 EF Core 跟踪实体变更。DbUpdateConcurrencyException是 EF Core 在检测到ROWVERSION不匹配时抛出的特定异常必须显式捕获并转换为业务友好的错误——这是保障数据一致性的最后一道防线。4.3 删除订单软删除 级联逻辑 批量操作ApiDemo.Application/Services/OrderService.cs中的删除方法public async Task DeleteAsync(int orderId) { // 1. 软删除非物理删除 await _orderRepository.SoftDeleteAsync(orderId); // 2. 删除关联的订单项如有——资源包中未包含 OrderItems 表但预留了扩展点 // await _orderItemRepository.DeleteByOrderIdAsync(orderId); // 3. 清理 Redis 缓存如有——资源包中未集成 Redis但注释标明接入点 // await _cache.RemoveAsync($order:{orderId}); } // Repository 中的软删除实现使用 ExecuteSqlRaw 避免加载实体 public async Task SoftDeleteAsync(int orderId) { var sql UPDATE [Orders] SET [IsDeleted] 1, [UpdatedAt] GETUTCDATE() WHERE [Id] p0 AND [IsDeleted] 0; await _context.Database.ExecuteSqlRawAsync(sql, new object[] { orderId }); }对于批量删除如清理 30 天前的软删除订单使用 EF Core 7 的ExecuteDeleteAsync// 在 DbContext 中添加方法 public async Taskint PurgeSoftDeletedOrdersAsync(DateTime cutoffDate) { return await SetOrder() .Where(o o.IsDeleted o.UpdatedAt cutoffDate) .ExecuteDeleteAsync(); // 物理删除不触发 Entity Framework 事件 }避坑 / 常见问题 / 排查现象调用DeleteAsync后数据库IsDeleted仍为 0原因SoftDeleteAsync方法中 SQL 语句的WHERE [IsDeleted] 0条件未加导致已软删除的记录被重复更新或ExecuteSqlRawAsync未 await调用方误以为已执行。解决严格按资源包中 SQL 模板编写且所有Execute*Async方法必须 await在单元测试中用Assert.Equal(1, result)验证影响行数。现象GetByIdWithTrackingAsync返回 null但数据库中存在该记录原因HasQueryFilter(u !u.IsDeleted)全局过滤器生效而该记录IsDeleted 1或AsNoTracking()被误用在查询上。解决检查实体状态若需查软删除数据用IgnoreQueryFilters()永远不要在GetById场景用AsNoTracking()否则Update会失败。现象SaveChangesAsync抛出DbUpdateException但 InnerException 为空原因SQL Server 连接超时或死锁EF Core 未包装底层异常或EnableRetryOnFailure未启用重试逻辑缺失。解决启用EnableRetryOnFailure在Program.cs中配置Logging输出 EF Core 执行的 SQL用 SQL Server Profiler 捕获实际执行语句。现象JWT 认证通过但User.Identity.IsAuthenticated为 false原因AddAuthentication后未调用AddAuthorization或UseAuthentication()中间件位置错误必须在UseRouting()之后、UseEndpoints()之前。解决检查Program.cs中间件顺序用HttpContext.User.Claims直接调试确认 Claims 是否存在。现象AutoMapper映射时ConcurrencyStamp字段被复制到 DTO引发安全风险原因未在 Profile 中忽略该字段。解决在MappingProfile.cs中添加CreateMapUser, UserDto().ForMember(dest dest.ConcurrencyStamp, opt opt.Ignore());5. 生产就绪日志、监控与部署验证技巧5.1 结构化日志Serilog Seq 快速定位 API 异常链路资源包已集成 SerilogApiDemo.Web/Program.cs// 安装包Serilog.AspNetCore, Serilog.Sinks.Seq builder.Host.UseSerilog((context, loggerConfig) { loggerConfig .MinimumLevel.Debug() .Enrich.FromLogContext() .Enrich.WithMachineName() .Enrich.WithEnvironmentUserName() .WriteTo.Console() .WriteTo.Seq(http://localhost:5341); // Seq 服务地址 });在 Controller 中注入ILoggerT记录关键路径[HttpPost] public async TaskActionResultOrderDto Create([FromBody] CreateOrderRequest request) { _logger.Information(Creating order for user {UserId} with {Request}, User.FindFirst(ClaimTypes.NameIdentifier)?.Value, request); try { var result await _orderService.CreateAsync(request); _logger.Information(Order {OrderId} created successfully, result.Id); return Ok(result); } catch (Exception ex) { _logger.Error(ex, Failed to create order for user {UserId}, User.FindFirst(ClaimTypes.NameIdentifier)?.Value); throw; // 保持异常向上抛由全局异常过滤器处理 } }参数说明{UserId}中的符号表示结构化日志Serilog 特性Seq 可据此字段快速筛选Enrich.WithMachineName()添加服务器名便于多实例排查WriteTo.Console()仅开发启用生产应移除。5.2 API 健康检查SQL Server 连接 JWT 密钥有效性双校验ApiDemo.Web/Program.cs中添加健康检查// 注册健康检查服务 builder.Services.AddHealthChecks() .AddSqlServer( builder.Configuration.GetConnectionString(DefaultConnection), name: sqlserver, tags: new[] { db }) .AddCheckRsaKeyHealthCheck(rsa-key, tags: new[] { auth }); // RsaKeyHealthCheck 实现 public class RsaKeyHealthCheck : IHealthCheck { private readonly RSA _rsa; public RsaKeyHealthCheck(RSA rsa) _rsa rsa; public TaskHealthCheckResult CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken default) { try { // 尝试用密钥签名一段数据验证密钥可用性 var data Encoding.UTF8.GetBytes(health-check); var signature _rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); return Task.FromResult(HealthCheckResult.Healthy()); } catch (Exception ex) { return Task.FromResult(HealthCheckResult.Unhealthy(exception: ex)); } } } // 暴露健康检查端点 app.MapHealthChecks(/health, new HealthCheckOptions { ResponseWriter UIResponseWriter.WriteHealthCheckUIResponse });访问https://your-api/health返回 JSON{ status: Healthy, results: { sqlserver: { status: Healthy, description: SQL Server connection OK }, rsa-key: { status: Healthy, description: RSA key validation passed } } }逻辑说明健康检查必须覆盖所有单点故障环节。AddSqlServer验证数据库连通性RsaKeyHealthCheck验证 JWT 签名密钥是否损坏——密钥文件权限错误或格式损坏时API 启动不报错但登录必失败此检查可提前暴露。5.3 部署验证清单5 分钟确认 API 是否真正就绪别只测GET /weatherforecast。用以下清单逐项验证检查项验证命令预期结果失败含义1. 数据库连接dotnet ef database update --project ApiDemo.Infrastructure输出Applying migration... Done.连接字符串错误或 SQL Server 不可达2. JWT 签发curl -X POST https://api/identity/login -H Content-Type: application/json -d {email:adminexample.com,password:Admin123}返回{accessToken:eyJ..., refreshToken:GUID}JWT 配置错误或密码哈希不匹配3. 认证后访问curl https://api/orders -H Authorization: Bearer token返回200 OK及订单列表认证中间件未启用或策略配置错误4. 软删除生效curl -X DELETE https://api/orders/1 -H Authorization: Bearer token→curl https://api/orders/1第二次返回404 Not FoundHasQueryFilter未正确配置或SoftDeleteAsyncSQL 错误5. 并发冲突捕获用两个 Postman 同时PUT /orders/1修改状态其中一个返回409 Conflict及自定义消息DbUpdateConcurrencyException未被捕获或处理**从那以后我每次部署新环境都强制走一遍这个五步清单——哪怕只是本地 Docker Compose 启动。它帮我避开了三次因appsettings.Production.json中Jwt:Issuer拼写错误导致的线上登录雪崩。真正的生产就绪不在于代码多炫酷而在于每一步都有可验证的、不可绕过的检查点。希望帮到你。本文还有配套的精品资源点击获取