☰
EFCore + MySQL Web API 源码拆解:分层、配置与避坑
2026/10/27 7:17:04 网站建设 项目流程

简介:这是一份基于EF Core与MySQL构建的ASP.NET Web API项目源码,适合有一定.NET基础、希望学习如何将ORM框架与关系数据库整合进后端服务的开发者。示例覆盖了从应用入口配置、实体模型定义、API控制器编写到数据库迁移的完整链路,启动配置、路由处理以及EF Core的DbContext映射关系都很直观,可作为搭建RESTful API的第一份参考模板。资源包共22个文件,以9个C#源文件、4个JSON配置文件和解决方案文件为主,C#源码按入口、控制器、模型与数据上下文分层组织,JSON配置文件用来区分通用参数与开发环境设置,此外还包括项目文件、版本记录、用户设置和开源协议说明,压缩包整体仅213KB,结构精简,适合快速通读与二次开发。目前已有365人学习下载,借助该示例可以快速上手EF Core搭配MySQL的Web API项目,并在此基础上扩展认证、日志、分页等实用功能。

1. EFCore + MySQL 的 Web API 源码:为什么我劝你别从空模板开始

找这套源码的人,大概率卡在同一条路上:不是业务不会写,是环境跑不起来。我拆这份基于 EFCore 和 MySQL 的 ASP.NET Web API 项目源码时,第一反应不是看接口数量,而是看分层、连接串、迁移这三条线索能不能在十分钟内拉通。ASP.NET Web API 本身不难,难的是把 EFCore 的版本、MySQL 的认证、字符集、时区这些零碎问题一次理顺。这套资源的价值在于,它把 Web API、Entity Framework Core、MySQL 三者之间的那层胶水提前粘好了。适合两类人:一类是刚把 ASP.NET Core 语法过完、想找个完整后端项目练手的新手;另一类是要接外包或做内部工具,想直接拿一套可跑的工程改字段就上的人。下面我按自己的拆解习惯来讲:先看结构,再讲配置,然后是避坑,最后给一份验证清单。

2. 读项目先读分层:Controller、Service 与仓储各自管什么、不应管什么

拿到源码第一件事不是找接口文档,而是先看目录。这套资源的目录大概率长得像标准三层:Controllers、Services、Repositories、Models,外加一个存放 DbContext 和迁移文件的 Data 或 Infrastructure 目录。三层结构在简单的增删改查项目里容易被认为“过度设计”,但当接口数量超过十个、查询条件开始互相组合的时候,没有分层会出现一个很现实的问题:Controller 里塞满了 EF 查询,SQL 逻辑散落在各个 action 里,改一个字段要全文搜索。

2.1 Controller 层:只做参数绑定和结果返回

先看控制器层。一个标准的 Controller 应该是薄的,像下面这个示例一样,方法体里不应该出现_context.Products.Where(...)这种写法。

// Controllers/ProductController.cs [ApiController] [Route("api/[controller]")] public class ProductController : ControllerBase { private readonly IProductService _productService; public ProductController(IProductService productService) { _productService = productService; } // GET /api/product [HttpGet] public async Task<IActionResult> GetAll() { var items = await _productService.GetAllAsync(); return Ok(items); } }

这段代码的核心目的只有一个:把 HTTP 边界上的事做完,然后转交给服务层。控制器从构造函数拿服务实例,不在内部new任何东西,也不直接引用 DbContext。这样做的好处是,之后如果你想换数据库实现,或者对查询加缓存,控制器的代码一行都不用动。

初次写 Web API 的人常犯的毛病是把查询条件直接写在 Controller 里,分页参数一传错就拖垮数据库。把参数校验挡在 Service 层外面,Controller 只关心“参数来没来、结果怎么返回”,职责立刻清晰。读源码时如果发现 Controller 很厚,说明分层没拆到位,这部分后续要自己重构。

2.2 Service 层与仓储层:事务边界放在哪

服务层是业务逻辑的家,仓储层负责把 EF Core 的查询细节藏起来。一个典型的 Service 方法长这样。

// Services/ProductService.cs public class ProductService : IProductService { private readonly IProductRepository _repository; public ProductService(IProductRepository repository) { _repository = repository; } public async Task<List<Product>> GetAllAsync() { return await _repository.GetActiveAsync(); } public async Task<bool> UpdatePriceAsync(int id, decimal price) { var product = await _repository.GetByIdAsync(id); if (product == null) return false; product.Price = price; return await _repository.SaveChangesAsync() > 0; } }

这套源码里最值得抄的就是 Service 加 Repository 的组合。事务边界落在SaveChangesAsync这一下,一个 DbContext 实例内的多个写操作会被包进同一个数据库事务,不需要手动BeginTransaction。这里有个容易翻车的点:Repository 不能做成“透传”。所谓透传,就是仓储方法只写一行return _context.Products.Where(...)然后原样返回 IQueryable,让调用方继续拼接条件。这种写法会让查询逻辑漏到 Service 甚至 Controller,分层形同虚设。

正确做法是把业务过滤条件封装在仓储里,比如GetActiveAsync()内部过滤掉IsActive == false的数据,调用方完全感知不到这个条件的存在。这套源码里如果仓储方法名里带业务语义(Active、Visible、OwnedByUser),说明作者没在糊弄。

2.3 依赖注入的注册顺序:AddDbContext 与 AddScoped 的配合

依赖注入的注册代码集中在 Program.cs 里,看起来简单,但顺序和生命周期有三个细节值得留意。

// Program.cs 中的注册片段 builder.Services.AddDbContext<AppDbContext>(options => options.UseMySql(builder.Configuration.GetConnectionString("Default"), new MySqlServerVersion(new Version(8, 0, 36)))); builder.Services.AddScoped<IProductRepository, ProductRepository>(); builder.Services.AddScoped<IProductService, ProductService>();

注册顺序本身不影响 ASP.NET Core 的解析结果,但它影响排错效率。一个常见的运行时报错是“无法解析 IProductService”,原因大概率是注册了 Service 却忘了注册 Repository。把 AddDbContext 放在最前面、业务服务放后面的写法,是让你排查依赖链条时不用倒着翻代码。

生命周期是另一个容易踩的点。DbContext 默认是 Scoped 生命周期,也就是每个 HTTP 请求内共享同一个实例。那么仓储和服务也必须是 Scoped,不能注册成 Singleton。如果注册成 Singleton,多个请求会共享同一个 DbContext,撞出“DbContext 已被并发使用”的异常。这类问题在单用户本地测试时完全暴露不出来,一上生产就密集报错。

还要注意MySqlServerVersion这个参数,它是 Pomelo 驱动特有的。版本号要和你的 MySQL 实例实际版本匹配,比如本机装的是 MySQL 8.0,就传new Version(8, 0, 36)或你的实际小版本;如果乱传一个旧版本,EF Core 生成的 SQL 语法可能不兼容,到线上出现“这里对了那里错了”的玄学问题。下一章专门讲 MySQL 连接串和 Pomelo 这套配置链。

3. EFCore 连 MySQL:Pomelo 选型、连接串参数与三个环境差异

把 EFCore 接到 MySQL 上,第一步不是写连接串,而是选驱动。微软官方有一个MySql.EntityFrameworkCore,但社区里做 ASP.NET Core 的人几乎都用 Pomelo,原因很现实:官方驱动更新节奏慢,命名空间和 API 习惯跟社区主流用法差别大。Pomelo.EntityFrameworkCore.MySql 是事实标准,它的版本号直接跟 EF Core 主版本走,对应关系非常清晰。

EF Core 版本Pomelo 包版本
6.0Pomelo.EntityFrameworkCore.MySql 6.x
7.07.x
8.08.x

这个对应关系是选型时第一道红线。Pomelo 主版本和 EF Core 主版本不一致,编译可能过,运行起来各种诡异,比如AddDbContext里找不到UseMySql扩展方法。所以拿到源码第一件事就是打开 csproj 文件,核对这两个包的主版本号。

3.1 连接串参数说明

连接串的写法决定了你这套源码在本机能跑、在服务器不能跑,很多环境问题其实是连接串参数不对。一份可用的默认连接串大概长这样。

{ "ConnectionStrings": { "Default": "Server=127.0.0.1;Port=3306;Database=appdb;User=root;Password=123456;CharSet=utf8mb4;SslMode=None;ConnectionTimeout=15;" } }

关键参数逐个说。Server这里在本地开发写127.0.0.1比写localhost稳,因为 localhost 在部分机器上会优先解析到 IPv6::1,而 MySQL 默认只监听 IPv4。CharSet=utf8mb4是中文项目最容易被漏掉的参数,漏掉之后数据库里存的可能是一排问号。SslMode本地开发配None,少一层握手能省不少事;连云数据库时要按厂商要求改成Required,这个值不改连不上。

Password放进 appsettings.json 只适合本地开发。Git 提交前一定要确认它不在版本库里,我在实际项目里见过把生产库密码直接推到仓库的,后面整个团队都在改密码。常见做法是给 appsettings.Development.json 放一套开发库密码,生产库密码走环境变量或密钥管理服务,代码里用builder.Configuration.GetConnectionString("Default")去读就行,不写死。

3.2 本机、Docker、服务器三个环境的差异

同一个连接串,换一个环境就翻车,这是源码落地时的大概率事故。最常见的一种是:你在 Windows 本机把项目跑通了,部署到 Linux 服务器上却连不上数据库。三个环境分别说。

Windows 本机,前提是 MySQL 服务先启动。安装版 MySQL 会注册成 Windows 服务,用管理员身份打开命令行执行net start mysql(服务名可能带版本号后缀)确认状态。源码里连接串的Server=127.0.0.1在本机不用改。

Docker 环境,MySQL 通常跑在容器里。我会用下面这条命令起一个开发库。

docker run --name app-mysql \ -e MYSQL_ROOT_PASSWORD=123456 \ -e MYSQL_DATABASE=appdb \ -p 3306:3306 \ -d mysql:8.0

这条命令里-p 3306:3306把容器内的 MySQL 端口映射到宿主机,所以连接串仍然可以写Server=127.0.0.1。但要注意:容器里的 MySQL 默认时区是 UTC,你写入的DateTime会按 UTC 存,查出来比北京时间少八小时,后面第 4 章细说。需要固定时区的话,在容器启动参数里加-e TZ=Asia/Shanghai。

Linux 服务器是坑最多的环境。MySQL 装好后默认只监听127.0.0.1,外部机器连不上,需要改/etc/mysql/mysql.conf.d/mysqld.cnf里的bind-address,改成0.0.0.0后重启服务。然后检查防火墙,云厂商的安全组里得放行 3306 端口。看到“连接被拒”别急着怀疑代码,先 telnet 一下 3306 端口通不通。这套源码如果一开始是照着本机环境调的,部署到服务器需要改的就是这三处而不是代码。

4. 避坑手册:五条从装库到跑通接口的排查记录

这五条是我在真实环境里翻车翻出来的,不是理论清单。每一条都按现象、原因、解决的顺序来写,方便读者照着排查。

4.1 EFCore 与 Pomelo 主版本错配

现象:代码能编译,运行时抛InvalidOperationException,大致内容是找不到UseMySql扩展方法,或者提示某个 EFCore 方法不存在。

原因:Pomelo 包的主版本必须和 EF Core 主版本一致。项目里装了 EF Core 8 却装了 Pomelo 6.x,就会出现这种诡异情况,编译时不报错是因为部分 API 签名恰好兼容,一运行就露馅。

解决:用dotnet list package查看已安装的包版本,把 Pomelo 的版本号切到和 EF Core 主版本相同。比如 EF Core 是 8.x,Pomelo 就装 8.x。改完删掉 bin 和 obj 目录重新 build,避免旧编译产物干扰。

4.2 Add-Migration 报错找不到 Design 包

现象:执行dotnet ef migrations add Init时直接报错,提示目标项目没有引用Microsoft.EntityFrameworkCore.Design。

原因:dotnet ef命令行工具自身不带设计时功能,它要靠 Design 包来发现你的 DbContext 和实体模型。这个包在创建项目时不是默认带的。

解决:安装 Design 包,命令是dotnet add package Microsoft.EntityFrameworkCore.Design。装完后再跑dotnet ef命令。注意这个包在发布时不需要,所以 csproj 里一般配PrivateAssets="all",不影响发布后的运行时依赖。

4.3 MySQL 8.0 认证插件导致连接失败

现象:数据库服务都正常,程序启动时连接字符串报了Authentication method 'caching_sha2_password' not supported一类的错误。

原因:MySQL 8.0 默认的认证插件是caching_sha2_password,你安装的 MySQL 客户端库或驱动版本太旧不支持这种认证方式,就握手失败。

解决:先升级驱动,这是首选方案。如果驱动没法升,比如受限于 .NET 版本,就把那个库账号的认证方式改回老的mysql_native_password,SQL 如下。

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

改完连接串不变,重新跑一次程序就能通。这里提醒一句:如果是 5.7 版的 MySQL,不存在这个问题,直接忽略这条。

4.4 时间字段的 8 小时误差

现象:接口返回的时间和数据库里存的时间差 8 小时,或者反过来,Swagger 里看着是下午三点,库里变成了早上七点。

原因:MySQL 驱动对DateTimeOffset的处理容易出时区转换,加上容器默认 UTC 时区,本地东八区和 UTC 就这么互相拉扯。模型里字段类型是DateTimeOffset时尤其容易出问题。

解决:统一用DateTime类型,数据库字段用datetime(6),并在 EFCore 配置里指定列类型。存数据统一转 UTC,读出后在接口层按展示需求转本地时间,不要在模型里混用两种时间类型。

// AppDbContext 中针对实体字段的配置片段 modelBuilder.Entity<Product>() .Property(p => p.CreatedAt) .HasColumnType("datetime(6)");

4.5 中文乱码

现象:Swagger 里请求返回的中文正常,但手动去 MySQL 里查,看到的是???或者一堆乱码。

原因:连接串里没写CharSet,或者建库时没有显式指定字符集。MySQL 5.7 默认是latin1,8.0 默认是utf8mb4,如果你的库是在 5.7 的默认配置下建的,中文字符存进去就坏了。

解决:改两个地方。连接串里加CharSet=utf8mb4,再把数据库本身的字符集改掉。

ALTER DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

已有的表也需要改,用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。改完重新插入数据,不要在旧数据上继续写。字符集这个问题属于“当时没感觉、上线一个月才爆雷”的类型,最稳妥的做法是项目一开始建库就把字符集定死。

5. 迁移与种子数据:Code-First 建表、初始化与自动迁移的取舍

EF Core 的 Code-First 工作流是这套源码能不能快速复现的关键。源码里实体类、DbContext 都有,迁移文件夹存在与否直接决定你拿到手后要花多少时间把表建出来。如果源码带了一个完整的 Migrations 目录,那省事很多;如果没有,你得自己生成第一个迁移。

5.1 从实体到表:dotnet ef 命令的正确用法

大多数情况下,源码的 README 里会写着一句“运行 Update-Database 即可”,但前提是你本机装了 dotnet-ef 这个全局工具。第一次跑迁移命令之前先确认工具在不在。

dotnet tool install --global dotnet-ef dotnet ef migrations add Init --project src/App.WebApi --startup-project src/App.WebApi dotnet ef database update

第一条命令是装 dotnet-ef 全局工具,只需执行一次。第二条会在项目里生成一个 Migrations 目录,里面包含一个时间戳命名的迁移类,Up方法里是建表逻辑,Down方法里是回滚逻辑。生成后建议打开这个文件确认表结构符合预期,尤其看看字段类型、长度、是否可空。

--project和--startup-project这两个参数在多项目解决方案里是必须存在的。当一个解决方案里有 API 项目和类库项目时,EF Core 需要明确谁是存放 DbContext 的项目(通常是类库)、谁是启动项目(通常是 API)。漏掉其中一个,会出现“无法找到 DbContext”的报错。如果你遇到这个报错,优先检查启动项目路径是不是写对了。

5.2 用 HasData 做种子数据

有些源码会包含初始数据,比如系统配置表、默认用户角色,这时候你需要知道数据是怎么进到库里去的。EF Core 官方推荐的做法是在 DbContext 的OnModelCreating里用HasData来指定种子数据。

// AppDbContext.OnModelCreating 片段 modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "演示商品", Price = 19.9m, IsActive = true } );

用HasData的好处是数据会变成迁移的一部分。执行dotnet ef migrations add时,种子数据会被写进迁移文件的Up方法里,随后跟着Update-Database一次性写入数据库。这个过程是幂等的,不会出现在启动时反复插入重复数据的问题。

对比来看,有些项目会在启动时写一段逻辑判断“如果表空就插入数据”,这种写法在单实例下没问题,但一上多实例部署,两个实例同时判断表为空、同时插入,就会出现重复数据。所以源码里如果是用HasData的方式,这个工程思路是值得沿用的。注意种子数据里Id必须给固定值,这样迁移回放时才能正确匹配是插入还是更新,不给主键值 EF Core 会提示需要配置。

5.3 开发环境的自动迁移与生产环境的取舍

源码里 Program.cs 经常能看到一行db.Database.Migrate()。它的作用是启动时自动把没执行的迁移补齐,开发环境很省心,但生产环境有风险。

// Program.cs 开发环境片段 if (app.Environment.IsDevelopment()) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); db.Database.Migrate(); }

这里有两个需要注意的问题:一是Migrate()必须在CreateScope()里执行,不能直接从根容器拿 DbContext,因为它是 Scoped 生命周期,直接从根容器拿会出现释放冲突的报错。二是生产环境多实例部署时,两个实例同时启动、同时执行Migrate(),数据库迁移会发生冲突。

生产环境更稳妥的做法是:发布时手工执行一次dotnet ef database update,或者让数据库管理员把迁移生成的 SQL 脚本在维护窗口执行。自动迁移这个东西,开发里是后悔药,生产里是定时炸弹。这套源码如果已经写成自动迁移,就把它限制在IsDevelopment()分支里。

6. 半小时验证清单:跑通接口、复现 CRUD、改字段再迁移

拿到源码别急着改业务,先照着下面的清单验证一遍,确认整条链路是通的。这一步能在半小时内完成,却能把“环境没问题”和“代码有问题”两种状态彻底分开。

验证项操作预期结果
接口可达curl https://localhost:7200/api/product返回 JSON 数组
单条查询GET /api/product/1返回 Id=1 的对象
新增POST /api/product,body 传 JSON返回 200 且库里多一行
更新PUT /api/product/1,修改 Price再次 GET 能看到新值
删除DELETE /api/product/1再次 GET 返回 404
迁移可回放删库后执行 Update-Database所有表和种子数据重建

如果某个接口返回 500,先看控制台日志,再用第 4 章的清单对照排查。这里有一个最实用的排错技巧:把 EFCore 生成的 SQL 打到控制台上,马上就能看出实体映射和数据访问是不是符合预期。在AddDbContext配置里加一行即可。

options.UseMySql(connectionString, serverVersion) .LogTo(Console.WriteLine, LogLevel.Information);

LogTo会输出所有 EF Core 生成的 SQL 和执行耗时。调试阶段这是最快的手段。确认都通过后,可以再做一个进阶测试:在实体类上新增一个字段,执行一次dotnet ef migrations add,再用Update-Database推上去。这个动作能同时验证 EFCore 的映射能力和迁移流程是不是通的,也是你以后改任何业务字段的第一反应路径。

拆过几份类似的源码之后,我养成了一个说不上聪明的习惯:拿到任何 Web API 工程,先不看业务代码,先跑通一个只读接口,再谈分层,再谈优化。因为只有链路通了你才有资格评价它的结构;链路不通的时候,所有结构评价都是猜测。这套源码我验证下来的结论是:EFCore 和 MySQL 之间最容易翻车的配置段都被处理干净了,剩下的是你自己的业务。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询