ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EF Core与MySQL实战:从ORM原理到Web API分层与性能优化

EF Core与MySQL实战:从ORM原理到Web API分层与性能优化 简介这是一份基于EF Core与MySQL的ASP.NET Web API项目设计源码面向使用.NET Core开发RESTful接口的开发者。资源演示了EF Core作为ORM工具与MySQL数据库整合完成数据访问和HTTP响应服务体现了数据层与控制层分离的典型分层思想。压缩包内含22个文件以9个C#源文件、4个JSON配置文件和1个解决方案文件为主体其中C#源文件主要覆盖控制器、数据模型与数据库上下文JSON配置支持多环境切换另有项目文件、忽略文件、许可证、版本控制说明及使用说明文档总大小约213KB结构清晰便于检索。该资源已有364人学习下载。源码提供从程序入口启动配置到示例控制器的完整示例包含EF Core迁移脚本、环境相关配置及使用说明文档覆盖依赖注入、路由映射等关键实现同时保留数据库迁移快照与开源许可可帮助初学者快速建立Web API项目的分层认知也可作为实际项目的脚手架进行二次开发减少初期搭建成本。1. 基于EFCore和MySQL的ASP.NET Web API项目源码把数据库访问从手写SQL里解放出来我接手第一个外包项目时对方指定要“基于EFCore和Mysql的ASP.NET.Web.API项目设计源码”这种组合。那时候我还在用原生ADO.NET每个接口都要写200行数据访问代码后续改个字段要连带改三处。换成EF Core MySQL之后直观的感受是实体类就是表结构DbSet就是查询入口迁移脚本能自动把模型变更同步到数据库。这套技术栈真正解决的问题是让 .NET 团队用一套免费、跨平台、无需许可证的数据库快速搭出一个可维护的REST API服务。适合做毕业设计、企业内部系统、或者打算把现有SQL Server项目迁移到Linux服务器的开发者。2. 拆解项目骨架EF Core与MySQL在Web API里的分工与边界2.1 为什么选EF Core MySQL而不是Dapper SQLite先给结论EF Core是微软官方ORM对异步、事务、迁移、变更追踪都有完整支持MySQL是社区驱动的开源数据库云厂商几乎都提供托管实例Docker一键能跑起来。这两者加在一起等于整个数据层不需要额外花钱买许可证。我见过不少团队用Dapper理由是“性能比EF Core高”。这句话对但只对了一半。Dapper是微ORM需要手写SQL查询结果映射到实体EF Core默认会生成SQL并把实体状态跟踪起来。如果你的项目有超过二十个表每个表都有增删改查还要做分页、排序、联合查询用Dapper会让SQL字符串堆满整个Service层。EF Core虽然生成的SQL偶尔不完美但它的LINQ查询可以通过AsNoTracking、拆分查询、编译查询这些手段逼近Dapper的性能。还有一个很现实的原因招人。面试.NET岗位的人几乎都会在简历上写“熟悉EF Core”。你让他维护一层仓储接口和异步方法比让他读几百行拼接SQL要快得多。SQLite适合单机、嵌入式场景比如本地缓存、桌面工具。一旦涉及多用户并发写、主从复制、存储过程SQLite就吃力了。MySQL是真正的网络数据库有连接池、账号权限、慢查询日志、主从同步这些企业级特性。再加上.NET Core本身就是跨平台的MySQL在Linux上的支持也成熟这套组合从Windows开发机直接部署到Ubuntu或CentOS不会有SQL Server那样的平台绑定问题。2.2 源码项目的典型分层Controller → Service → Repository → DbContext标题里“设计源码”四个字重点不在代码数量而在分层。我推荐用四层结构不是说一定要用仓储模式而是要让每个文件的职责一眼能看出来。Controller层只做两件事接收HTTP参数返回HTTP结果。它不碰DbContext也不写业务规则。有些保姆级教程把查询逻辑直接写进控制器前期省事后期改个分页规则要动控制器写单元测试也困难。Service层是业务逻辑的收容所。比如“创建订单时扣库存”这种逻辑放在Service里Controller调一个CreateOrderAsync方法内部包含事务、校验、发送事件。这样接口的文档和测试都会好写很多。Repository层是数据访问的抽象。EF Core本身已经有了DbSet再加一层仓储的争议一直存在。我的态度是小项目不需要直接把DbContext注入Service大项目或者要换数据库的项目仓储层值得做因为你能在进入EF Core之前把查询整理成IQueryable方便组装条件。源码项目里我看到的大多是轻量仓储只包一层不做UnitOfWork。DbContext层是核心。它通过DbSet暴露实体通过OnModelCreating配置映射关系通过SaveChangesAsync统一提交。这一层也是连接字符串的消费者后面所有的坑都集中在这里。2.3 csproj配置包版本与命名空间的关系一个标准的Web API项目csproj里至少要有这几个包Project SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet8.0/TargetFramework Nullableenable/Nullable /PropertyGroup ItemGroup PackageReference IncludeMicrosoft.EntityFrameworkCore Version8.0.* / PackageReference IncludeMicrosoft.EntityFrameworkCore.Design Version8.0.* PrivateAssetsall/PrivateAssets /PackageReference PackageReference IncludeMicrosoft.EntityFrameworkCore.Tools Version8.0.* / PackageReference IncludePomelo.EntityFrameworkCore.MySql Version8.0.* / /ItemGroup /Project说明Pomelo.EntityFrameworkCore.MySql 是社区最成熟的EF Core MySQL驱动。它的命名空间是Microsoft.EntityFrameworkCore所以你在代码里写using Microsoft.EntityFrameworkCore;就能拿到扩展方法。这容易让新人困惑——“我明明没装微软的MySQL包为什么命名空间是对的”因为Pomelo就是借用微软的命名空间来提供UseMySql扩展。版本匹配有一个铁律Pomelo主版本要对应EF Core主版本。比如EF Core 8对应Pomelo 8.xEF Core 7对应Pomelo 7.x。混用会在程序集加载时报错报错信息大多是“Method not found”或者“Could not load file or assembly”。Version8.0.* 是NuGet的通配写法会自动拉取8.0系列最新补丁方便升级。另外Microsoft.EntityFrameworkCore.Design包只在开发时用用来生成迁移代码所以要加PrivateAssetsall避免发布到生产环境时带一堆没用的依赖。Tools包也是开发时依赖同样在这个ItemGroup里。3. 从零搭建项目数据库上下文、实体映射与迁移3.1 mysql安装配置教程先让数据库跑起来再谈EF Core很多新手直接在Windows上装MySQL一路下一步结果连字符集都选错了。我建议无论开发还是生产都用Docker跑MySQL这样mysql安装配置教程里最麻烦的“初始化权限、修改字符集、开放端口”三件事在Docker里一句话解决docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e MYSQL_DATABASEapidb \ mysql:8.0参数说明-p 3306:3306 把宿主机的3306端口映射到容器这样EF Core连接字符串里写localhost:3306就能访问。MYSQL_ROOT_PASSWORD 是root密码开发环境随便写生产环境一定要用强密码或密钥注入方式。MYSQL_DATABASE 会在容器首次启动时自动创建一个空库省得你再用命令行create database。在Linux服务器上装MySQLDocker是最稳的方式不用处理Systemd和yum源的问题。启动后可以用docker ps确认状态。如果端口被占用改成-p 3307:3306然后连接字符串里对应改端口。如果一定在Windows上装MySQL记住两个关键点。第一安装时选UTF-8字符集别默认latin1否则表里存中文会变成问号。第二安装完成后MySQL默认不开放远程访问需要执行GRANT ALL PRIVILEGES ON.TO root% IDENTIFIED BY 密码; 否则EF Core从本机连没问题换成测试服务器就连不上。3.2 定义DbContext与实体Fluent API还是Data Annotation实体类我习惯用Fluent API配置因为可以把表名、索引、字段长度的约束全部放在OnModelCreating里实体类本身只保留纯POCO。这样实体和配置分离重构起来不用改实体。一个简单的用户实体public class User { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public DateTime CreatedAt { get; set; } }对应DbContextpublic class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetUser Users SetUser(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityUser(entity { entity.ToTable(users); entity.HasKey(u u.Id); entity.Property(u u.Name).IsRequired().HasMaxLength(50); entity.Property(u u.Email).IsRequired().HasMaxLength(100); entity.HasIndex(u u.Email).IsUnique(); }); } }逻辑说明DbSet 是查询入口LINQ查询都从这个属性出发。ToTable(users) 指定表名MySQL库名通常小写表名也用下划线风格避免Linux下大小写敏感问题。HasIndex(u u.Email).IsUnique() 给邮箱建唯一索引这是业务常用的约束EF迁移会自动执行CREATE UNIQUE INDEX。对比Data Annotation方式Fluent API的优势在“动态配置”。比如根据环境变量决定某张表是否启用软删除过滤可以在配置里用条件判断Data Annotation写在实体属性上做不到这种动态性。3.3 注册DbContext与连接字符串ServerVersion与Pomelo的坑在Program.cs里注册DbContext是关键一步var builder WebApplication.CreateBuilder(args); var connectionString builder.Configuration.GetConnectionString(Default); builder.Services.AddDbContextAppDbContext(options { options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); options.LogTo(Console.WriteLine, LogLevel.Information); }); var app builder.Build(); app.Run();参数说明ServerVersion.AutoDetect(connectionString) 会通过连接字符串向服务器发送SELECT VERSION()自动识别MySQL版本。这个功能很方便但在数据库还没启动、或者网络不通时会抛异常。options.LogTo(Console.WriteLine, LogLevel.Information) 让EF Core把生成的SQL打到控制台开发阶段很有用能直接看到每次查询的实际SQL帮助判断是查询写得烂还是索引被忽略了。appsettings.json里这样写连接字符串{ ConnectionStrings: { Default: Serverlocalhost;Port3306;Databaseapidb;Userroot;Passwordroot123;CharSetutf8mb4;SslModeNone; } }注意点CharSetutf8mb4 必须加MySQL的utf8mb4才能存储完整的Unicode包括表情符号。SslModeNone 是我被坑过无数次的地方。Pomelo默认尝试建立SSL连接如果MySQL服务器没配SSL证书运行时会报“mysql ssl连接错误”相关异常。开发环境直接关掉SSL生产环境应该用PemCertificatePath指定证书而不是裸SSL。3.4 用迁移生成数据库Add-Migration与Update-Database实体和DbContext写好后进入迁移环节。先装好EF工具dotnet tool install --global dotnet-ef然后生成迁移脚本dotnet ef migrations add InitialCreate dotnet ef database update命令说明Add-Migration InitialCreate 会在项目里生成Migrations文件夹里面包含Up和Down两个方法Up描述从空库到当前版本的变更Down是回滚。database update 会直接在本机连接字符串指向的数据库执行迁移。生产环境不要执行这个用dotnet ef migrations script生成SQL脚本再交给DBA执行。迁移是EF Core相对于Dapper的巨大优势。用Dapper的时候表结构变了要手动写ALTER TABLE漏一句就线上事故。用迁移模型和数据库始终是同一描述团队里每个人改完模型都会生成新的迁移文件代码评审时能直接看SQL。4. 把业务接口跑通控制器、仓储与异步查询的落地写法4.1 仓储模式的取舍真需要还是过度设计在开始写接口之前先谈仓储这件事。很多“设计源码”项目把Repository和UnitOfWork当标配但如果你只做简单CRUD仓储层会变成透明的包装器等于把DbContext的DbSet又包了一层没增加价值。我的经验法则项目表数量少于15张不需要仓储Controller直接注入AppDbContext在Service里调用DbSet。项目有明确的分层要求和单元测试需求需要仓储。因为测试的时候可以用内存数据库替换仓储接口能让Mock变得舒服。项目可能换数据库比如从MySQL换到PostgreSQL仓储确实能降低替换成本但这属于远期投资要权衡。标题既然带了“设计源码”说明是想展示完整的架构所以我选择轻量仓储每个实体有一个接口接口只暴露查询和写入方法实现类包一层DbContext。4.2 一个完整的GET/POST接口示例先写仓储接口public interface IUserRepository { TaskUser GetByIdAsync(int id); TaskIEnumerableUser GetPagedAsync(int pageIndex, int pageSize); Task AddAsync(User user); }实现类public class UserRepository : IUserRepository { private readonly AppDbContext _context; public UserRepository(AppDbContext context) { _context context; } public async TaskUser GetByIdAsync(int id) { return await _context.Users.AsNoTracking().FirstOrDefaultAsync(u u.Id id); } public async TaskIEnumerableUser GetPagedAsync(int pageIndex, int pageSize) { return await _context.Users .OrderBy(u u.Id) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync(); } public async Task AddAsync(User user) { await _context.Users.AddAsync(user); await _context.SaveChangesAsync(); } }逻辑说明GetByIdAsync 使用了AsNoTracking()。对于只读查询关闭变更追踪可以减少内存开销EF Core不会再为返回的实体保留快照适合列表展示和详情查看。GetPagedAsync 演示了mysql排序和分页的标准写法OrderBy确保每次查询结果顺序一致Skip/Take在MySQL里会被翻译成LIMIT offset, count。AddAsync里的AddAsync只标记实体状态为Added真正写入数据库的是SaveChangesAsync。两个步骤分开有利于在业务层做提交控制。Controller层[ApiController] [Route(api/[controller])] public class UsersController : ControllerBase { private readonly IUserRepository _userRepository; public UsersController(IUserRepository userRepository) { _userRepository userRepository; } [HttpGet({id})] public async TaskIActionResult GetById(int id) { var user await _userRepository.GetByIdAsync(id); if (user null) { return NotFound(); } return Ok(user); } [HttpPost] public async TaskIActionResult Create([FromBody] UserDto dto) { var user new User { Name dto.Name, Email dto.Email, CreatedAt DateTime.UtcNow }; await _userRepository.AddAsync(user); return CreatedAtAction(nameof(GetById), new { id user.Id }, user); } }这里有一个隐性知识Create里的CreatedAtAction返回201同时包含Location头是REST API的标准行为。很多源码项目只返回Ok不写Location前端要额外解析不算错但不够规范。4.3 排序、分页与过滤mysql排序与分页的EF Core写法分页看似简单但接口参数设计有讲究。我见过把pageIndex和pageSize放在Query String里的做法也见过放Header里的我一般放在Query String用默认值兜底[HttpGet] public async TaskIActionResult GetList([FromQuery] int pageIndex 1, [FromQuery] int pageSize 20) { if (pageIndex 1) pageIndex 1; if (pageSize is 1 or 100) pageSize 20; var query _context.Users.AsNoTracking(); // 动态排序 var sortOrder Request.Query[sort].ToString(); query sortOrder switch { name_desc query.OrderByDescending(u u.Name), created_asc query.OrderBy(u u.CreatedAt), _ query.OrderBy(u u.Id) }; var total await query.CountAsync(); var items await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); return Ok(new { total, items }); }说明先CountAsync再Skip/Take是分页接口的标准执行顺序。Count占一次查询列表占一次一共两次SQL用EF Core做没有性能问题。动态排序用switch表达式避免用字符串拼接OrderBy这种反射写法。pageSize限制在100以内是为了防止别人一次性拉全表这是我在一个真实项目里被慢查询拖垮过才加上的。如果涉及关键字搜索可以用EF.Functions.Likeif (!string.IsNullOrEmpty(keyword)) { query query.Where(u EF.Functions.Like(u.Name, $%{keyword}%)); }EF.Functions.Like会被翻译成MySQL的LIKE而不是提前把整个表拉到内存再Contains过滤。这里要注意如果keyword包含%或_需要转义否则就会变成通配符查询结果不准确。5. 避坑与排查EF Core MySQL在真实项目里的5个高频问题以下每一条都是我实际踩过的按“现象→原因→解决”格式写方便你在搜索引擎里按关键词定位。5.1 mysql ssl连接错误Pomelo的默认SSL行为现象程序启动后第一次查询就抛异常错误信息类似“MySqlConnector: SSL Connection error”。原因Pomelo.EntityFrameworkCore.MySql 底层用 MySqlConnector默认SslMode是Preferred。如果服务器不支持SSL或者用自签名证书握手就会失败。很多开发环境MySQL没配SSL证书因此遇到这个问题的概率极高。解决在连接字符串里显式加SslModeNone这是开发环境最简单的方式。生产环境如果必须用SSL用PemCertificatePath指定CA证书路径不要直接设置Required否则自签名证书会直接拒连。5.2 mysql的数据库连接池耗尽Timeout与Max Pool Size现象系统运行一段时间后接口突然全部返回500日志里看到“Timeout expired. The timeout elapsed after 30000ms”。原因EF Core的DbContext默认不是线程安全的很多人习惯把DbContext注册成单例或者在一个请求里创建太多DbContext实例连接没有及时归还。MySQL连接池默认最大100如果每次查询都打开一个新连接且不释放池很快耗尽。解决DbContext必须注册成Scoped这是AddDbContext的默认行为不要改成单例。同时可以在连接字符串上设置Max Pool Size256;Connection Lifetime300Connection Lifetime让连接在超过300秒后强制断开避免MySQL的8小时超时问题。另外出现这种错误先用SHOW PROCESSLIST看当前连接数确认是不是有Sleep状态堆积。5.3 时区导致的时间偏移现象存的是DateTime.UtcNow读出来却比北京时间少了8小时或者前端显示时间跟数据库看到的不一致。原因MySQL的DateTime类型不带时区信息。EF Core默认把你本地时间的DateTime转换成UTC然后MySQL按服务器时区存储。如果你的服务器时区是UTC而应用本地时区是UTC8就会偏移。解决统一时间规范。我一般建议全链路用UTC存储前端展示时转本地时区。连接字符串里加Connection Timezone08:00也能让驱动做时区转换但数据库里存的是本地时间换服务器会出问题。更稳的做法是在应用代码里统一DateTime.UtcNow不在MySQL层做转换。5.4 迁移时提示“Unable to connect to any of the specified MySQL hosts”现象dotnet ef database update 报错但同一个连接字符串用Navicat连没问题。原因dotnet ef工具读取appsettings.json里的连接字符串有可能没按预期加载。常见原因是appsettings.json在子目录或者环境变量覆盖了配置。另一个原因MySQL只监听了127.0.0.1但dotnet ef从另一个网络接口访问被拒。解决先确认连接字符串确实没写错用MySQL Workbench连一下。然后检查MySQL绑定地址bind-address 0.0.0.0。如果是Docker确保端口映射正确。我一般在开发环境直接进入容器验证docker exec -it mysql8 mysql -u root -p确认密码和数据库名再回头查EF的连接字符串。5.5 批量更新性能循环SaveChanges还是批量扩展现象一次性更新几千行数据逐行调用SaveChangesAsync跑了十几秒。原因每次SaveChanges都会开一个事务往数据库发送一条UPDATE。几千行就是几千次网络往返当然慢。解决优先考虑EF Core 7以上支持的ExecuteUpdateAsync方法它可以在服务端生成一条UPDATE语句await _context.Users .Where(u u.LastLoginAt cutoff) .ExecuteUpdateAsync(setters setters.SetProperty(u u.IsActive, false));这个方法不经过ChangeTracker直接执行更新速度快到毫秒级。注意它返回受影响行数不会追踪实体。如果业务需要逐行触发事件再考虑循环SaveChanges加事务但这是少数情况。5.6 迁移状态脏了Migrations与数据库表对不上现象改模型后执行Add-Migration生成的迁移文件里包含之前已经执行过的变更或者update database提示“table already exists”。原因开发迭代中多人同时改模型迁移历史文件冲突或者有人手动ALTER TABLE导致数据库Schema和Migrations里的Snapshot不一致。EF Core用__EFMigrationsHistory表记录已执行的迁移一旦这张表和Migrations文件夹的迁移文件不一致就会乱。解决最稳妥的顺序是先把当前数据库用dotnet ef migrations script生成脚本比对差异差异太大就直接用dotnet ef database drop删库重来仅限开发环境。生产环境不要动历史迁移用新增迁移来修正比如生成一个空白迁移然后在Up方法里手动写修正SQL。这是EF Core的后悔药。6. 项目进阶性能调优、日志与部署验证6.1 从日志里看SQL效率EF Core的日志是性能调优的第一入口。除了options.LogTo还可以在appsettings.json里配置Logging级别输出每条SQL的执行时长{ Logging: { LogLevel: { Microsoft.EntityFrameworkCore.Database.Command: Information } } }看到超过500ms的SQL先看索引再查有没有N1。典型N1是循环里逐条访问导航属性解决方法是改成Include或Select让EF Core生成一条JOIN。6.2 用ExecuteUpdate和事务做批量操作批量把已取消的订单改为已归档同时插入日志用IDbContextTransaction包住两个操作await using var transaction await _context.Database.BeginTransactionAsync(); await _context.Orders .Where(o o.Status cancelled) .ExecuteUpdateAsync(setters setters.SetProperty(o o.Status, archived)); await _context.OperationLogs.AddAsync(new OperationLog { Content 归档取消订单 }); await _context.SaveChangesAsync(); await transaction.CommitAsync();ExecuteUpdateAsync不经过ChangeTracker所以第二个SaveChangesAsync不受第一个操作的实体快照影响。6.3 部署到Linux Docker的验证清单部署到生产前我习惯用Docker Compose把API和MySQL一起拉起来然后做三项验证健康检查返回200且能往返查询数据库连接字符串不用root单独建最小权限账号启动后确认没有SSL错误和连接池告警。最小权限账号建法CREATE USER api_user% IDENTIFIED BY Api123; GRANT SELECT, INSERT, UPDATE, DELETE ON apidb.* TO api_user%;如果要用MySQL Workbench做日常管理只需要连接账号和数据库名不要在代码里暴露root密码。源码项目里最好把连接字符串放到环境变量或Secret Manager而不是硬编码在appsettings.json。最后我在这个项目上踩过最大的坑是过早优化。一开始就想用分库分表、读写分离、Redis缓存结果MySQL单机跑了半年一点问题都没有。EF Core MySQL这套组合足够支撑到日均百万级请求之前。把日志、索引、连接池这些基本功做好比堆中间件更值钱。希望帮到你。本文还有配套的精品资源点击获取
返回列表