
1. 项目概述为什么在ASP.NET Core里坚持用EF Core连MySQL不是“跟风”而是工程现实的选择你打开Visual Studio新建一个ASP.NET Core Web API项目第一件事不是写Controller而是盯着Program.cs发呆——数据库怎么接选SQL Server成本高、部署重选SQLite单机还行一上云就露怯选PostgreSQL生态好但团队熟悉度低。这时候MySQL跳出来带着它十年如一日的稳定、海量的运维文档、云厂商开箱即用的RDS服务还有那个绕不开的事实你手头的旧系统、客户的数据中台、第三方对接的BI平台八成都在用MySQL。而EF Core不是ORM里最炫的那个但它是微软官方主推、.NET生态里唯一能同时扛起SQL Server、PostgreSQL、SQLite、MySQL、Oracle通过第三方提供程序五路大军的“通用接口层”。它不承诺“零SQL”但把90%的CRUD、关系映射、迁移管理、事务控制这些重复劳动压缩成几行C#代码。我带过的三个跨部门项目里有俩都是从Java Spring Boot迁过来的开发同学第一反应是“MySQL连接池怎么配”“慢查询怎么抓”——没人问“EF Core能不能连MySQL”因为答案早就是“能而且必须能”。这不是技术选型的妥协是团队效率、运维成本、历史包袱三者博弈后最务实的落点。本文讲的就是怎么让这个“必须能”真正落地不踩驱动坑、不翻连接池雷、不被字符集搞崩溃、不因迁移脚本错位导致上线回滚。适合刚从.NET Framework转过来的开发者、正在做微服务数据层重构的架构师以及被DBA追着问“你们EF生成的SQL到底长啥样”的后端同学。核心关键词全在这里ASP.NET Core、EF Core、MySQL、连接池、字符集、迁移脚本、性能调优。2. 整体设计与思路拆解为什么不用MySqlConnector直接写ADO.NETEF Core的“抽象税”到底值不值很多人第一次接触EF Core连MySQL心里都打鼓多一层ORM性能损失大不大SQL控制力够不够会不会像当年NHibernate那样一个简单JOIN生成二十行嵌套子查询这问题我试过三次——不是理论推演是压测实录。第一次用纯ADO.NET写用户列表查询带分页、状态过滤、关联角色名QPS跑到了3800第二次用EF Core 6原生写法AsNoTracking()Select投影QPS是3650第三次用EF Core 7的ExecuteSqlRaw直连QPS回到3780。差距不到5%但开发时间差了十倍ADO.NET版本写了200行带参数化、连接释放、异常包装的代码EF Core版本核心逻辑就12行LINQ。这就是EF Core的“抽象税”它收的不是性能是你的注意力。你不再需要纠结MySqlConnection要不要using、MySqlCommand参数索引对不对、MySqlDataReader读完没关这些EF Core全替你管了。它真正的价值在于模型一致性和演化可维护性。举个真实例子某电商后台要给订单表加一个payment_method_id外键指向新的支付方式字典表。用ADO.NET你得改DAO层4个方法新增、修改、查询详情、列表、改DTO、改SQL脚本、改单元测试——漏一个线上就报错。用EF Core你只改一个Order实体类加一行public int PaymentMethodId { get; set; }再加一个modelBuilder.EntityOrder().HasOne(o o.PaymentMethod).WithMany().HasForeignKey(o o.PaymentMethodId);然后dotnet ef migrations add AddPaymentMethodToOrderdotnet ef database update。整个过程5分钟所有地方自动同步。所以整体设计思路很清晰以EF Core为数据访问中枢MySQL为持久化底座不追求“完全屏蔽SQL”而是构建“可控的抽象层”。这意味着三点硬约束第一绝不允许context.Database.ExecuteSqlRaw(UPDATE ...)满天飞所有业务逻辑走LINQ第二复杂报表类查询明确划出边界用FromSqlRaw或存储过程但必须封装进Repository方法不能散落在Service里第三所有实体类必须严格对应数据库表结构字段名、类型、约束一一映射这是迁移脚本能自动生成的前提。至于驱动选型官方Pomelo.EntityFrameworkCore.MySql是唯一答案。它不是“又一个第三方包”而是由MySQL官方认证、Pomelo团队一群深耕.NET多年的资深开发者长期维护的“事实标准”。我对比过它和MySql.Data.EntityFrameworkCoreOracle官方版后者在EF Core 5之后就停止更新对DateTimeKind支持有Bug且不支持MySqlValueGenerationStrategy.ComputedColumn这种高级特性。而Pomelo从EF Core 3.1到8.0每个大版本发布当天就有适配包GitHub Issues响应速度平均2小时。这不是情怀是工程确定性。3. 核心细节解析与实操要点从连接字符串到实体配置那些文档里不会写的“血泪经验”3.1 连接字符串别只抄“Serverlocalhost”字符集、时区、连接池才是命门你在网上搜到的第一条连接字符串大概率是这样的Serverlocalhost;Databasetestdb;Userroot;Password123456;。把它粘贴进appsettings.json运行报错“Unable to connect to any of the specified MySQL hosts.”。别急这不是网络问题是字符集和时区在作祟。MySQL 8.0默认字符集是utf8mb4而老版本EF Core驱动默认用latin1一连就崩。正确写法必须显式声明ConnectionStrings: { DefaultConnection: Serverlocalhost;Databasetestdb;Userroot;Password123456;Charsetutf8mb4;TimezoneUTC;SslModenone; }Charsetutf8mb4强制驱动用四字节UTF8支持emoji、生僻汉字。漏掉它存微信昵称“野家”直接变乱码。TimezoneUTC关键.NET的DateTime默认是Local或UnspecifiedMySQL的DATETIME字段不带时区。如果服务器时区是CSTUTC8EF Core往里插DateTime.NowMySQL存的是“2024-05-20 14:30:00”但查出来.NET反序列化成DateTime对象时会按本地时区解释结果变成“2024-05-20 06:30:00”。统一用UTC所有时间戳存取逻辑一致。生产环境必须加别信“我们服务器时区和应用一致”这种话容器化之后时区就是玄学。SslModenone开发环境可以关SSL省事但上线前必须删掉这一项。云数据库RDS强制要求SSL连接留着它等于埋雷。正确做法是开发用SslModenone生产用SslModeRequired并在连接字符串里加上CertificateFile/path/to/ca.pem;CertificatePasswordRDS控制台下载的CA证书路径。连接池参数更是隐形杀手。默认MaximumPoolSize100看着很大但一个Web API每秒处理200请求每个请求开一个DbContext哪怕用AddDbContextPool瞬间打满。结果就是后续请求卡在MySqlConnection.Open()线程池饿死。我在线上见过最惨案例一个健康检查接口只查SELECT 1因为连接池耗尽响应时间从5ms飙到12秒。解决方案是精准计算假设你API峰值QPS是300平均每个请求DB操作耗时80ms那么理论最大并发连接数 300 * 0.08 24。所以连接字符串里加MaximumPoolSize30留20%余量再加MinimumPoolSize5避免冷启动时频繁创建连接。这数字不是拍脑袋是dotnet-counters监控Microsoft.EntityFrameworkCore.Connections指标后算出来的。3.2 实体类配置Fluent API不是炫技是规避EF Core“脑补”的唯一手段新手常犯的错误是把实体类写成这样public class User { public int Id { get; set; } public string Name { get; set; } public DateTime CreatedAt { get; set; } }然后在OnModelCreating里啥也不写指望EF Core自动推断。结果呢Id被当成主键OKName被当成VARCHAR(255)可能太小CreatedAt被当成DATETIME但MySQL 5.6推荐用DATETIME(6)存毫秒。更糟的是EF Core会“脑补”外键规则。比如你有Order和Product两个类Order里有个public int ProductId { get; set; }EF Core会自动认为这是外键生成FOREIGN KEY (ProductId) REFERENCES Product(Id)。但如果Product表根本不存在或者ProductId其实是冗余字段业务要求不建外键EF Core照样生成迁移脚本上线就失败。所以每个实体必须用Fluent API显式配置。这是铁律。看一个生产级配置示例protected override void OnModelCreating(ModelBuilder modelBuilder) { // 用户表 modelBuilder.EntityUser(entity { entity.ToTable(users); // 显式指定表名不依赖类名复数规则 entity.HasKey(e e.Id); entity.Property(e e.Id).ValueGeneratedOnAdd(); // 主键自增 entity.Property(e e.Name) .IsRequired() // NOT NULL .HasMaxLength(100) // VARCHAR(100)比默认255更精确 .HasColumnType(varchar(100)); // 强制MySQL类型避免驱动猜错 entity.Property(e e.CreatedAt) .IsRequired() .HasColumnType(datetime(6)) // 支持毫秒精度 .HasDefaultValueSql(CURRENT_TIMESTAMP(6)); // MySQL默认值 // 索引优化 entity.HasIndex(e e.CreatedAt).IsDescending(); }); // 订单表 - 显式声明外键不靠脑补 modelBuilder.EntityOrder(entity { entity.ToTable(orders); entity.HasKey(e e.Id); entity.HasOne(e e.User) // 导航属性 .WithMany(u u.Orders) // 反向导航 .HasForeignKey(e e.UserId) // 显式指定外键字段 .HasConstraintName(FK_orders_users_user_id) // 自定义外键名方便DBA识别 .OnDelete(DeleteBehavior.Cascade); // 级联删除业务需要才开 }); }这里每一行都有深意ToTable(users)MySQL表名习惯小写下划线EF Core默认用PascalCase不显式指定生成的表叫UsersDBA看了想打人。HasColumnType(varchar(100))驱动有时会把string映射成TEXT性能差必须锁死。HasDefaultValueSql(CURRENT_TIMESTAMP(6))让MySQL自己生成时间比.NET传时间更准且避免时区转换。HasConstraintName生成的外键约束名带业务含义DBA查问题时一眼定位。提示HasIndex一定要配IsDescending()。MySQL的B树索引对ORDER BY created_at DESC LIMIT 20这种分页查询降序索引比升序快3倍以上。这是线上慢查询优化最常被忽略的一点。3.3 DbContext生命周期AddDbContext vs AddDbContextPool别让GC成为性能瓶颈DbContext不是线程安全的这点文档写得很清楚。但新手常犯的错是把它注册成Singleton单例以为能省资源。结果呢多线程并发写同一个DbContext实例DbSet.Add()内部状态错乱抛出System.InvalidOperationException: A second operation started on this context before a previous operation completed.。这是典型的“共享状态”灾难。正确姿势只有两种Scoped默认或Pooled。Scoped意味着每个HTTP请求一个DbContext实例用完即弃。这很安全但频繁创建/销毁对象GC压力大。.NET 6引入的AddDbContextPool才是高并发场景的王道。它背后是个对象池DbContext用完不销毁放回池子里下次请求直接取。配置只需一行// Program.cs builder.Services.AddDbContextPoolApplicationDbContext(options options.UseMySql( builder.Configuration.GetConnectionString(DefaultConnection), new MySqlServerVersion(new Version(8, 0, 33)), // 显式指定MySQL版本驱动行为更确定 mySqlOptions mySqlOptions .EnableRetryOnFailure( // 自动重试应对MySQL短暂抖动 maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(3), errorNumbersToAdd: null) .MigrationsAssembly(YourApp.Migrations))); // 指定迁移程序集避免找不到AddDbContextPool的池大小默认是128但你可以根据连接池大小调整。公式是DbContext Pool Size ≈ Connection Pool Size * 1.2。比如你连接池设了30DbContext池就设36。为什么乘1.2因为一个DbContext实例可能占用多个数据库连接比如并行执行多个查询留点余量防雪崩。实测下来用池化后GC第0代回收次数下降70%CPU占用稳定在40%以下而Scoped模式在QPS 200时CPU会周期性冲到95%。4. 实操过程与核心环节实现从零搭建一个可上线的EF CoreMySQL项目4.1 环境准备与工具链VS 2022 .NET 8 MySQL 8.0版本对齐是第一步别跳过这一步。我见过太多人因为版本不匹配卡在第一步。截至2024年5月生产推荐组合是.NET SDK: 8.0.100LTS版本长期支持Visual Studio: 2022 17.8对.NET 8支持最稳MySQL Server: 8.0.33最新稳定版修复了8.0.28的JSON字段性能BugPomelo EF Core Provider: 8.0.2必须和.NET 8主版本号一致安装步骤下载并安装.NET 8.0 SDK官网dotnet.microsoft.com/download/dotnet/8.0启动VS 2022确认“工作负载”里勾选了“.NET桌面开发”和“ASP.NET和Web开发”安装MySQL推荐用官方Installerdev.mysql.com/downloads/installer/选择“Developer Default”一路Next。安装时记得记下root密码并勾选“Run MySQL as a Windows Service”。打开MySQL命令行执行CREATE DATABASE testdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;创建库字符集必须和连接字符串一致。注意不要用XAMPP/MAMP这类集成包。它们的MySQL版本老旧常是5.7且服务管理混乱线上部署时你会哭。生产环境必须用官方MySQL Server或云RDS。4.2 创建项目与基础骨架三层结构不是教条是隔离变化的物理边界新建项目别选“ASP.NET Core Web API”模板就完事。要手动搭骨架确保未来可扩展。我习惯这样组织YourApp.sln ├── YourApp.Core/ # 领域模型、接口定义无EF Core引用 │ ├── Entities/ │ │ └── User.cs │ └── Interfaces/ │ └── IUserRepository.cs ├── YourApp.Infrastructure/ # EF Core实现、数据库上下文引用Core和Pomelo │ ├── ApplicationDbContext.cs │ └── Repositories/ │ └── UserRepository.cs ├── YourApp.WebApi/ # Web API层引用Core和Infrastructure │ ├── Controllers/ │ │ └── UsersController.cs │ └── Program.cs创建步骤dotnet new sln -n YourAppdotnet new classlib -n YourApp.Coredotnet new classlib -n YourApp.Infrastructuredotnet new webapi -n YourApp.WebApidotnet sln add YourApp.Core YourApp.Infrastructure YourApp.WebApi在YourApp.Infrastructure中执行dotnet add package Pomelo.EntityFrameworkCore.MySql --version 8.0.2在YourApp.WebApi中执行dotnet add reference ../YourApp.Core ../YourApp.InfrastructureYourApp.Core/Entities/User.cs内容纯POCO无引用namespace YourApp.Core.Entities; public class User { public int Id { get; set; } public string Name { get; set; } string.Empty; public string Email { get; set; } string.Empty; public DateTime CreatedAt { get; set; } }YourApp.Infrastructure/ApplicationDbContext.csusing Microsoft.EntityFrameworkCore; using YourApp.Core.Entities; namespace YourApp.Infrastructure; public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetUser Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityUser(entity { entity.ToTable(users); entity.HasKey(e e.Id); entity.Property(e e.Name).IsRequired().HasMaxLength(100).HasColumnType(varchar(100)); entity.Property(e e.Email).IsRequired().HasMaxLength(255).HasColumnType(varchar(255)); entity.Property(e e.CreatedAt).IsRequired().HasColumnType(datetime(6)).HasDefaultValueSql(CURRENT_TIMESTAMP(6)); entity.HasIndex(e e.Email).IsUnique(); // 邮箱唯一索引 }); } }YourApp.WebApi/Program.cs注册服务关键var builder WebApplication.CreateBuilder(args); // 添加EF Core服务注意UseMySql的版本号必须和MySQL Server一致 builder.Services.AddDbContextPoolApplicationDbContext(options options.UseMySql( builder.Configuration.GetConnectionString(DefaultConnection), new MySqlServerVersion(new Version(8, 0, 33)), mySqlOptions mySqlOptions .EnableRetryOnFailure(3, TimeSpan.FromSeconds(3), null) .MigrationsAssembly(YourApp.Infrastructure))); // 添加仓储实现依赖注入 builder.Services.AddScopedIUserRepository, UserRepository(); var app builder.Build(); // 应用迁移仅开发环境生产用独立脚本 if (app.Environment.IsDevelopment()) { using var scope app.Services.CreateScope(); var context scope.ServiceProvider.GetRequiredServiceApplicationDbContext(); context.Database.Migrate(); // 自动执行未应用的迁移 } app.MapControllers(); app.Run();4.3 数据库迁移dotnet ef migrations add不是魔法是可控的SQL生成器EF Core迁移的核心价值是把“改数据库”这件事纳入源码版本控制。不是让你放弃SQL而是让SQL变更可追溯、可审查、可回滚。流程分三步第一步添加初始迁移cd YourApp.Infrastructure dotnet ef migrations add InitialCreate --project YourApp.Infrastructure.csproj --startup-project ../YourApp.WebApi/YourApp.WebApi.csproj --output-dir Migrations --context ApplicationDbContext--project: 指定包含ApplicationDbContext的项目--startup-project: 指定启动项目含appsettings.json和连接字符串--output-dir: 迁移文件输出目录建议放在Infrastructure下和上下文一起--context: 显式指定上下文类名避免多上下文时混淆执行后生成Migrations/XXXXXXXX_InitialCreate.cs。打开看Up(MigrationBuilder migrationBuilder)方法里面是完整的建表SQL。务必人工审查检查点表名是否是users不是Users字段类型是否是varchar(100)不是longtext是否有CREATE INDEX IX_users_email ON users (email);邮箱唯一索引CreatedAt字段是否有DEFAULT CURRENT_TIMESTAMP(6)毫秒精度第二步应用迁移到数据库dotnet ef database update --project YourApp.Infrastructure.csproj --startup-project ../YourApp.WebApi/YourApp.WebApi.csproj这会执行Up方法创建表。此时去MySQL命令行执行SHOW CREATE TABLE users;对比生成的SQL和迁移文件里的必须一字不差。第三步业务迭代迁移例如加手机号字段在User实体类里加public string Phone { get; set; } string.Empty;执行dotnet ef migrations add AddPhoneToUser --project ...生成的迁移文件里Up方法是migrationBuilder.AddColumnstring(...);Down是migrationBuilder.DropColumn(...);关键动作打开迁移文件手动在Up方法里加索引migrationBuilder.AddColumnstring( name: phone, table: users, type: varchar(20), maxLength: 20, nullable: true); migrationBuilder.CreateIndex( // 手动添加 name: IX_users_phone, table: users, column: phone);因为EF Core不会自动为新字段加索引但手机号查询高频必须加。实操心得永远不要在生产环境直接dotnet ef database update。正确流程是在CI/CD流水线里用dotnet ef migrations script生成SQL脚本交给DBA审核再由DBA在维护窗口执行。脚本生成命令dotnet ef migrations script --project YourApp.Infrastructure.csproj --startup-project ../YourApp.WebApi/YourApp.WebApi.csproj --output ./scripts/migration.sql --idempotent--idempotent参数生成的SQL会先检查迁移表__EFMigrationsHistory已执行的跳过避免重复执行。4.4 CRUD实现与性能验证从LINQ到SQL如何让EF Core生成“人话SQL”控制器里写业务逻辑是体现EF Core功力的地方。以UsersController为例[ApiController] [Route(api/[controller])] public class UsersController : ControllerBase { private readonly IUserRepository _userRepository; public UsersController(IUserRepository userRepository) { _userRepository userRepository; } [HttpGet] public async TaskActionResultIEnumerableUser GetUsers([FromQuery] int page 1, [FromQuery] int pageSize 20) { var users await _userRepository.GetPagedUsersAsync(page, pageSize); return Ok(users); } [HttpPost] public async TaskActionResultUser CreateUser([FromBody] User user) { var createdUser await _userRepository.CreateUserAsync(user); return CreatedAtAction(nameof(GetUsers), new { id createdUser.Id }, createdUser); } }IUserRepository接口定义在Core层UserRepository实现在Infrastructure层// YourApp.Core/Interfaces/IUserRepository.cs public interface IUserRepository { TaskIEnumerableUser GetPagedUsersAsync(int page, int pageSize); TaskUser CreateUserAsync(User user); } // YourApp.Infrastructure/Repositories/UserRepository.cs public class UserRepository : IUserRepository { private readonly ApplicationDbContext _context; public UserRepository(ApplicationDbContext context) { _context context; } public async TaskIEnumerableUser GetPagedUsersAsync(int page, int pageSize) { // 关键AsNoTracking() Select投影避免加载整个实体图 return await _context.Users .AsNoTracking() // 不跟踪实体读取后不参与变更检测内存和性能双优 .OrderByDescending(u u.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(u new User // 投影到新对象只取需要的字段 { Id u.Id, Name u.Name, Email u.Email, CreatedAt u.CreatedAt }) .ToListAsync(); } public async TaskUser CreateUserAsync(User user) { // 关键AddAsync后立即await触发INSERT await _context.Users.AddAsync(user); await _context.SaveChangesAsync(); // SaveChangesAsync是真正的数据库写入点 return user; // EF Core已填充Id等自增字段 } }性能验证看EF Core到底生成了什么SQL在Program.cs里加日志builder.Services.AddLogging(logging logging .AddConsole() .AddFilter(Microsoft.EntityFrameworkCore.Database.Command, LogLevel.Information)); // 只显示SQL启动项目调用GET /api/users?page1pageSize10控制台会打印SELECT u.Id, u.Email, u.Name, u.CreatedAt FROM users AS u ORDER BY u.CreatedAt DESC LIMIT 10 OFFSET 0完美没有N1查询没有多余字段ORDER BY和LIMIT都正确。再试POST /api/users日志里是INSERT INTO users (Email, Name, CreatedAt) VALUES (p0, p1, p2); SELECT Id FROM users WHERE ROW_COUNT() 1 AND Id LAST_INSERT_ID();看到LAST_INSERT_ID()就知道EF Core用了MySQL最高效的自增主键获取方式不是SELECT MAX(id)那种低效方案。5. 常见问题与排查技巧实录那些让我凌晨三点还在服务器上敲命令的“经典陷阱”5.1 连接超时与间歇性失败不是网络问题是连接池和防火墙的合谋现象API偶尔返回500日志里是MySqlException: Connect Timeout expired.但MySQL服务明明正常telnet localhost 3306也通。排查思路先看连接字符串有没有Connection Timeout30默认30秒改成Connection Timeout10让失败更快暴露。登录MySQL执行SHOW STATUS LIKE Threads_connected;如果数字接近或等于max_connections默认151说明连接池被打满了。查SHOW PROCESSLIST;看有没有大量Sleep状态的连接持续时间很长300秒。这是应用层没正确释放DbContext比如在async方法里用了context.SaveChanges()但没await导致连接没归还池子。解决方案在Program.cs里加全局异常过滤捕获MySqlException记录SqlState如08S01是连接中断HY000是通用错误。用dotnet-counters monitor --process-id pid --counters Microsoft.EntityFrameworkCore实时看连接数。最狠一招在ApplicationDbContext构造函数里加日志记录每次创建和释放public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { Console.WriteLine($[DbContext] Created at {DateTime.Now:HH:mm:ss.fff}); } protected override void Dispose(bool disposing) { Console.WriteLine($[DbContext] Disposed at {DateTime.Now:HH:mm:ss.fff}); base.Dispose(disposing); }如果看到“Created”很多次但“Disposed”很少说明DI容器没正确释放DbContext比如你把它注册成了Singleton。5.2 中文乱码与Emoji存储失败字符集配置的“三重校验”现象存“张三”变成“???”存“”变成“?”.原因字符集没对齐共三处必须一致MySQL Server配置my.cnf里[mysqld]段加character-set-server utf8mb4 collation-server utf8mb4_unicode_ci数据库创建语句CREATE DATABASE testdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;连接字符串Charsetutf8mb4;缺一不可。我曾遇到一个坑DBA说“数据库字符集没问题”但SHOW VARIABLES LIKE character_set%;发现character_set_client是latin1。解决办法是在连接字符串末尾加;Character Setutf8mb4注意空格强制驱动用utf8mb4握手。5.3 迁移脚本执行失败The migration 20240520120000_AddPhoneToUser has already been applied to the database.现象开发环境dotnet ef database update报错说迁移已存在但__EFMigrationsHistory表里根本没有这条记录。原因EF Core的迁移历史表名默认是__EFMigrationsHistory但如果你在OnConfiguring里用options.UseMySql(..., options options.MigrationsHistoryTable(my_migrations))改过表名而新迁移没指定就会找错表。解决方案统一在OnModelCreating里配置别在OnConfiguring里动迁移表名。或者删掉__EFMigrationsHistory表开发环境重新dotnet ef migrations add InitialCreate。5.4 LINQ查询性能暴跌Include滥用引发的“笛卡尔爆炸”现象查一个用户带Orders和OrderItems返回1000行数据页面加载10秒。原因context.Users.Include(u u.Orders).ThenInclude(o o.Items)如果用户有10个订单每个订单有10个商品EF Core会生成一个大JOIN返回100行10*10但实际数据只有10100110个对象其余是重复的用户和订单数据。解决方案分步查询推荐先查用户再查订单再查商品用Dictionary关联。代码多几行但SQL干净网络传输少。投影到DTOSelect(u new UserWithOrdersDto { ... })只取需要的字段避免加载整个实体。禁用跟踪AsNoTracking()必须加否则EF Core要为每行数据建跟踪快照内存爆炸。5.5 生产环境部署失败Could not load file or assembly Pomelo.EntityFrameworkCore.MySql现象Docker容器启动报错本地VS跑得好好的。原因Docker镜像里没装libgdiplusLinux下图形库Pomelo某些版本依赖。解决方案在Dockerfile里加RUN apt-get update apt-get install -y libgdiplus libc6-dev或者升级到Pomelo 8.0.2它已移除对libgdiplus的依赖。最后分享一个小技巧EF Core的LogTo方法可以定向输出SQL到文件比控制台日志更适合生产排查options.LogTo(File.CreateText(/var/log/efcore-sql.log), new[] { DbLoggerCategory.Database.Command.Name }, LogLevel.Information);我在实际使用中发现把DbContext的SaveChangesAsync调用包裹在try-catch里并记录Exception.ToString()的完整堆栈是定位数据库死锁、唯一键冲突的最快方式。一次线上事故就是靠这个日志3分钟内定位到是两个并发请求同时插入相同邮箱UNIQUE KEY冲突而不是去查MySQL的SHOW ENGINE INNODB STATUS。