C# WebAPI 连接 SQL Server 实战:从 Demo 到生产避坑指南

发布时间:2026/10/19 5:35:23
C# WebAPI 连接 SQL Server 实战:从 Demo 到生产避坑指南 简介这份 C# WebAPI 示例代码面向具备一定 .NET 基础的开发者用于快速理解基于 ASP.NET 构建 RESTful 风格 HTTP 服务的完整流程并演示如何与 SQL Server 数据库进行数据交互。示例涵盖 ApiController 控制器编写、HttpGet/HttpPost 等 HTTP 动词映射、Entity Framework 或 ADO.NET 数据访问层、模型类定义、Global.asax.cs 路由配置以及 HttpResponseMessage 返回值处理等核心知识点同时保留了用户认证与缓存机制尚未实现的原始状态便于读者在此基础上自行扩展 OAuth、JWT 或 Redis 缓存等进阶功能。资源以 zip 压缩包形式提供整体约 172.56MB目前已有 294 人学习下载。通过研读该示例开发者可掌握 RESTful API 设计原则、控制器与模型分层组织方式以及数据库读写的基本实现路径适合作为入门练手或项目脚手架参考。1. 从一份 C# WebAPI Demo 说起SQL Server 后端到底该怎么搭很多同学第一次接触 RESTful 接口开发都是从一个简单的 C# WebAPI Demo 开始的。但真正拿到一份能跑的 Demo 代码之后问题往往不是“怎么跑起来”而是“这套东西放到真实项目里到底能不能用”。我见过太多人把 Demo 里的内存集合一换直接接上 SQL Server结果第二天就翻车——连接池爆了、并发写冲突、返回的 JSON 循环引用报错一个都跑不掉。这份 C# WebAPI Demo 的核心价值就是给你一个从 Controller 到 SQL Server 的完整闭环用 Entity Framework 或 Dapper 做数据访问用 RESTful 风格暴露资源用依赖注入管理上下文生命周期。它适合两类人一是刚学完 C# 语法、想搞明白“接口到底怎么连数据库”的新手二是从其他语言转过来、想快速对齐 .NET 生态里 RESTful 落地方式的熟手。下面我就按自己拆项目的顺序把这份 Demo 从结构到参数、从跑通到避坑一层层拆开讲。2. 拆开 Demo 的目录结构Controller、DbContext 与依赖注入怎么串2.1 先看项目骨架三个必须理解的文件夹拿到一份 C# WebAPI Demo别急着 F5。我一般先扫一眼目录重点看三个地方Controllers、Models、Data或者叫Infrastructure。这三个文件夹基本决定了这份 Demo 的架构水平。Controllers里放的是对外暴露的 RESTful 端点。一个合格的 Demo 不会把所有逻辑塞进 Controller而是让 Controller 只负责接收请求、调用服务、返回结果。如果你看到 Controller 里直接new SqlConnection(...)那这份 Demo 大概率是教学级别的离生产还有距离。Models里放的是实体类和 DTO。注意区分这两者实体类对应数据库表DTO 对应接口的输入输出。Demo 里经常混用但你在实际项目里一定要分开否则一旦表结构变了接口契约就跟着崩。Data里放的是DbContext或者 Dapper 的仓储实现。这是整个项目跟 SQL Server 打交道的地方也是后面讲连接池、事务、并发时最需要关注的部分。2.2 依赖注入的生命周期AddDbContext 的坑最多在Program.cs.NET 6或Startup.cs里你会看到类似这样的注册代码// Program.cs - .NET 6 最小宿主模型 var builder WebApplication.CreateBuilder(args); // 注册 DbContext指定 SQL Server 提供程序 builder.Services.AddDbContextAppDbContext(options options.UseSqlServer( builder.Configuration.GetConnectionString(DefaultConnection), sqlOptions sqlOptions.EnableRetryOnFailure( maxRetryCount: 3, // 失败重试次数 maxRetryDelay: TimeSpan.FromSeconds(5), // 最大重试间隔 errorNumbersToAdd: null // 额外需要重试的错误号 ) ) ); builder.Services.AddControllers(); var app builder.Build(); app.MapControllers(); app.Run();这段代码看起来简单但有几个参数直接决定你后面会不会翻车。AddDbContext默认注册的生命周期是Scoped也就是每个 HTTP 请求一个实例。这本身没问题但如果你在某个单例服务里注入了AppDbContext就会直接抛InvalidOperationException——因为单例不能依赖 Scoped。我见过不止一个项目在这里栽跟头报错信息还特别绕新手根本反应不过来是生命周期的问题。EnableRetryOnFailure是 SQL Server 提供程序特有的弹性策略。maxRetryCount: 3意味着遇到瞬时故障比如网络抖动、数据库短暂不可用时会自动重试三次。但注意这个重试不是万能的——如果是 SQL 语法错误或者主键冲突重试多少次都没用反而会拖慢响应。我一般只在生产环境开这个本地调试时关掉免得掩盖真实错误。连接字符串的配置也有讲究。Demo 里经常直接写Serverlocalhost;DatabaseTestDb;Trusted_ConnectionTrue;但放到真实环境你至少要考虑MultipleActiveResultSetsTrueMARS和Connection Timeout这两个参数。MARS 允许多个命令在同一个连接上并发执行EF Core 在某些查询场景下会用到Connection Timeout默认 15 秒如果数据库响应慢请求会一直挂在那里把线程池拖垮。2.3 Controller 里的 RESTful 路由别把动词写进 URLRESTful 的核心是“用 HTTP 动词表达操作用 URL 表达资源”。但我在很多 Demo 里看到的是[HttpGet(GetUserById)]这种写法URL 里带动词这就不太 RESTful 了。正确的做法是用路由特性配合 HTTP 动词[ApiController] [Route(api/[controller])] public class UsersController : ControllerBase { private readonly AppDbContext _context; public UsersController(AppDbContext context) { _context context; } // GET api/users [HttpGet] public async TaskActionResultIEnumerableUserDto GetUsers() { return await _context.Users .Select(u new UserDto { Id u.Id, Name u.Name }) .ToListAsync(); } // GET api/users/5 [HttpGet({id})] public async TaskActionResultUserDto GetUser(int id) { var user await _context.Users.FindAsync(id); if (user null) return NotFound(); return new UserDto { Id user.Id, Name user.Name }; } // POST api/users [HttpPost] public async TaskActionResultUserDto CreateUser(CreateUserDto input) { var user new User { Name input.Name, Email input.Email }; _context.Users.Add(user); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetUser), new { id user.Id }, user); } }这里有几个细节值得说。[ApiController]特性会自动处理模型验证如果CreateUserDto里有[Required]标记的字段没传框架直接返回 400不需要你手动写if (!ModelState.IsValid)。CreatedAtAction会生成一个带Location头的 201 响应指向新建资源的 URL这是 RESTful 的标准做法但很多 Demo 直接返回Ok(user)严格来说不够规范。另外注意FindAsync和FirstOrDefaultAsync的区别。FindAsync会先查本地缓存再查数据库FirstOrDefaultAsync每次都查数据库。对于按主键查询的场景FindAsync更高效但如果你在同一个上下文里已经跟踪了该实体FindAsync会直接返回缓存实例可能拿到的是旧数据。这个坑在并发场景下特别隐蔽。3. 接上 SQL Server连接字符串、迁移与并发处理的实操细节3.1 连接字符串的五个关键参数Demo 里的连接字符串通常很简陋但真实项目里至少要关注这几个参数参数作用推荐值不设置的后果Connection Timeout建立连接的超时秒数30默认 15 秒网络慢时频繁超时Command Timeout命令执行的超时秒数60复杂查询直接抛超时异常Max Pool Size连接池最大连接数100默认 100高并发下排队Min Pool Size连接池最小连接数5默认 0首次请求有冷启动开销MultipleActiveResultSets是否允许多结果集TrueEF Core 某些查询会报错这些参数不是拍脑袋定的。Max Pool Size设成 100 意味着最多 100 个并发连接超出的请求会排队等待。如果你的接口平均响应时间是 100ms那理论上每秒能处理 1000 个请求但如果某个查询跑了 5 秒连接就被占住 5 秒吞吐量直接掉到 20。所以连接池大小要结合接口的 P99 响应时间来算不能随便抄。3.2 EF Core 迁移从代码到数据库表的自动化Demo 里如果用的是 EF Core通常会带一个Migrations文件夹。如果没有你需要自己生成# 安装 EF Core 工具如果还没装 dotnet tool install --global dotnet-ef # 在项目目录下创建初始迁移 dotnet ef migrations add InitialCreate # 把迁移应用到数据库 dotnet ef database updatemigrations add会根据你的实体类生成建表脚本database update则把脚本执行到 SQL Server 上。这里有个血泪经验如果你改了实体类但忘了加迁移程序跑起来不会报错但数据库表结构还是旧的查询时会抛Invalid column name。我一般会在 CI 流程里加一步dotnet ef migrations script把生成的 SQL 跟数据库实际结构做对比不一致就直接阻断构建。另外迁移文件里的Up和Down方法要成对写。Up是应用迁移Down是回滚。很多人只写Up结果出问题时没法回滚只能手动改数据库。我见过一个项目因为迁移没写Down上线后发现字段类型错了硬是花了两个小时手动修数据。3.3 并发冲突乐观锁与 DbUpdateConcurrencyExceptionSQL Server 后端最容易被忽视的就是并发。两个请求同时修改同一条记录后提交的会覆盖先提交的这就是“丢失更新”。EF Core 提供了乐观锁机制通过在实体上加[Timestamp]字段实现public class User { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } [Timestamp] public byte[] RowVersion { get; set; } // SQL Server 的 rowversion 类型 }RowVersion对应 SQL Server 的rowversion列每次更新自动递增。EF Core 在SaveChanges时会检查这个值是否变了如果变了就抛DbUpdateConcurrencyException。你需要捕获这个异常并决定怎么处理try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry ex.Entries.Single(); var dbValues await entry.GetDatabaseValuesAsync(); if (dbValues null) { // 记录已被删除 return NotFound(); } // 可以用 dbValues 覆盖当前值或者提示用户刷新 entry.OriginalValues.SetValues(dbValues); return Conflict(数据已被其他用户修改请刷新后重试); }这段代码的逻辑是先拿到数据库里的最新值如果记录已经不存在了返回 404如果存在把原始值更新为最新值然后返回 409 冲突。实际项目里你可以选择让用户刷新后重试也可以自动合并——但自动合并要小心搞不好会把别人的修改覆盖掉。4. 避坑与排查Demo 跑通之后最容易翻车的五个地方4.1 现象接口返回 JSON 时报“循环引用”原因实体类之间有导航属性比如User有OrdersOrder又有User序列化时互相引用无限递归。解决不要在 Controller 里直接返回实体类用 DTO 投影。如果非要用实体类在Program.cs里配置JsonSerializerOptions.ReferenceHandler ReferenceHandler.IgnoreCycles但这只是掩盖问题最好的办法还是 DTO。4.2 现象高并发下报“超时时间已到但尚未从池中获取连接”原因连接池耗尽。要么是Max Pool Size太小要么是有代码没释放连接比如手动new SqlConnection后忘了Dispose。解决先查有没有手动管理连接的地方确保用using包裹。如果都是 EF Core 管理的适当调大Max Pool Size同时检查有没有长事务——一个事务持有连接不放其他请求只能干等。4.3 现象迁移执行成功但查询时报“无效的列名”原因实体类改了迁移也加了但database update没执行或者执行到了错误的数据库。解决先dotnet ef migrations list看迁移状态再确认连接字符串指向的数据库对不对。我一般会在database update后跑一条SELECT * FROM __EFMigrationsHistory确认最新迁移已经记录在案。4.4 现象POST 请求返回 400但模型验证明明通过了原因[ApiController]自动验证时如果 DTO 里有不可为空的引用类型属性比如string Name而请求体里没传这个字段框架会直接返回 400错误信息在ProblemDetails里。解决要么把属性改成可空string? Name要么在 DTO 上加[Required]并确保客户端传值。注意 .NET 6 默认开启了可空引用类型这个坑在新项目里特别常见。4.5 现象本地跑得好好的部署到服务器后连不上 SQL Server原因连接字符串里的Serverlocalhost在服务器上指向的是服务器本机如果数据库在另一台机器上自然连不上。另外SQL Server 默认没开 TCP/IP 协议或者防火墙没放行 1433 端口。解决把Server改成实际的 IP 或主机名确认 SQL Server 配置管理器里 TCP/IP 已启用防火墙入站规则放行 1433。如果用的是命名实例还要确认 SQL Browser 服务在跑。5. 进阶技巧用 Dapper 替换 EF Core 做轻量查询以及接口压测验证5.1 什么时候该换 DapperEF Core 适合写操作和复杂对象图但如果你有一个报表接口要 join 五六张表、返回几十个字段EF Core 生成的 SQL 可能又长又慢。这时候 Dapper 更合适——它就是个 SQL 映射器你写什么 SQL 就执行什么没有翻译层。// 用 Dapper 做多表 join 查询 public async TaskIEnumerableOrderReportDto GetOrderReportAsync(DateTime start, DateTime end) { const string sql SELECT o.Id, o.OrderNo, u.Name AS CustomerName, SUM(oi.Amount) AS TotalAmount FROM Orders o INNER JOIN Users u ON o.UserId u.Id INNER JOIN OrderItems oi ON oi.OrderId o.Id WHERE o.CreatedAt BETWEEN Start AND End GROUP BY o.Id, o.OrderNo, u.Name ORDER BY o.CreatedAt DESC; using var connection new SqlConnection(_connectionString); return await connection.QueryAsyncOrderReportDto(sql, new { Start start, End end }); }注意using var connection这行——Dapper 不会自动管理连接你必须自己确保释放。QueryAsyncT会把结果集映射到OrderReportDto字段名要跟 SQL 的列名对上对不上就映射不上不会报错但值是 null。这个坑我踩过排查了半天才发现是列别名的问题。5.2 压测验证用 bombardier 看 P99 和错误率接口写完不是终点你得知道它能扛多少并发。我一般用bombardier做快速压测# 安装 bombardier以 Linux 为例 # 下载对应平台的二进制包后解压即可 # 对 GET 接口做 100 并发、持续 30 秒的压测 bombardier -c 100 -d 30s -m GET http://localhost:5000/api/users # 对 POST 接口压测带 JSON 请求体 bombardier -c 50 -d 30s -m POST \ -H Content-Type: application/json \ -b {name:test,email:testexample.com} \ http://localhost:5000/api/users重点看三个指标Reqs/sec吞吐量、Latency P9999% 请求的响应时间、Errors错误数。如果 P99 超过 500ms或者错误率超过 1%就得回头查 SQL 索引和连接池配置。我一般会先跑一轮基线改一个参数再跑一轮对比看效果——比如把Max Pool Size从 100 调到 200看吞吐量有没有提升如果没有说明瓶颈不在连接池。5.3 一个我坚持了多年的习惯每次改完数据访问层不管是换 ORM 还是调连接字符串我都会强制走一遍“迁移 → 压测 → 看日志”这三步。迁移确保表结构对压测确保性能没退化日志确保没有隐藏的异常被吞掉。有一次我改了一个索引压测通过但日志里全是DbUpdateConcurrencyException只是被全局异常处理器吞了没暴露出来。从那以后我每次压测都会同时开dotnet run的控制台输出盯着有没有异常刷屏。希望这份拆解能帮你把这份 C# WebAPI Demo 真正用起来而不是停留在“能跑就行”的阶段。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询