EF Core 2.0为何生成错误值?内存数据库单元测试遇键冲突
我之前也碰到过一模一样的坑!咱们来一步步捋清楚问题根源,再给出靠谱的解决办法:
核心原因分析
你手动给第一个Item设置了Id=1,但如果你的Item实体配置了自增主键(比如用[Key]+[DatabaseGenerated(DatabaseGeneratedOption.Identity)]特性,或者Fluent API的.UseIdentityColumn()),InMemory数据库的自增序列默认是从1开始的。当你调用_context.Add(newItem)时,它会尝试给新实体分配下一个自增值也就是1,但这个Id已经被你手动添加的实体占用了,自然就触发键冲突了。
具体解决方案
这里有几个实用的处理方式,你可以根据测试场景选择:
1. 让InMemory自动生成初始实体的Id(最省心)
如果你的测试逻辑不依赖固定的Id值,完全可以放弃手动设置初始数据的Id,让InMemory数据库自己分配:
// 测试里添加初始数据时不要手动设Id _context.Add(new Item(){Description="description"}); await _context.SaveChangesAsync(); // 此时这个Item的Id会自动是1,后续添加的新实体Id会依次是2、3...不会冲突
2. 重置InMemory的自增序列(适合必须固定初始Id的场景)
在你添加完初始测试数据后,手动调整自增序列的起始值,让它从现有最大Id+1开始。以EF Core为例,代码可以这么写:
// 在测试初始化方法中,添加完初始Item后执行 var maxId = _context.Items.Max(i => i.Id); var sequenceCmd = _context.Database.GetService<IRelationalConnection>().CreateCommand(); sequenceCmd.CommandText = $"ALTER SEQUENCE Items_Id_seq RESTART WITH {maxId + 1};"; await sequenceCmd.ExecuteNonQueryAsync();
这个操作会让后续新增的实体自动从maxId+1开始分配Id,完美避开已占用的主键。
3. 手动指定新实体的Id(临时应急方案)
如果只是简单测试,可以直接给新创建的newItem设置一个未被占用的Id:
public async Task CreateItem(ItemDM dm) { // 可以先查询现有最大Id,再+1赋值 var maxExistingId = await _context.Items.MaxAsync(i => i.Id); Item newItem = new Item() { Id = maxExistingId + 1, Name = dm.RelatedItem.Process.Name }; _context.Add(newItem); await _context.SaveChangesAsync(); }
不过这种方式不够灵活,一旦初始数据变动就得手动调整,不推荐长期用。
4. 修改实体的主键生成策略(业务允许的话)
如果你的业务场景里实体Id是手动分配的,而非自增,可以修改实体配置关闭自增:
// 在DbContext的OnModelCreating方法里用Fluent API配置 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Item>() .Property(i => i.Id) .ValueGeneratedNever(); // 关闭自增,完全手动控制Id }
这样不管你手动设置多少Id,都不会触发自增序列的冲突,但前提是业务逻辑确实支持手动指定Id。
额外提醒
每次测试结束后记得清理上下文数据,避免不同测试用例之间的污染,比如在测试的TearDown方法里调用:
await _context.Database.EnsureDeletedAsync();
内容的提问来源于stack exchange,提问作者IEnjoyEatingVegetables

