EF Core 2.0.1中非主键标识列AfterSaveBehavior属性值异常问询
没错,你发现的这个矛盾点确实是EF Core 2.0.1版本中的一个已知Bug——非主键的标识列明明在数据库层面是只读的,无法修改,但EF Core的元数据属性AfterSaveBehavior却被错误设置为PropertySaveBehavior.Save,同时IsReadOnlyAfterSave也为false,导致元数据的行为标识和实际数据库约束完全不匹配,进而出现修改时抛出异常的矛盾情况。
问题根源
在EF Core 2.0.x版本中,框架只正确处理了主键标识列的元数据行为:主键标识列的AfterSaveBehavior会被自动设为PropertySaveBehavior.Throw,IsReadOnlyAfterSave设为true,和数据库的只读逻辑保持一致。但对于非主键的标识列,框架遗漏了这个同步逻辑,导致元数据错误地标识该列可以被修改,而实际数据库的Identity约束会阻止修改操作,最终抛出DbUpdateException。
解决办法
1. 优先升级到EF Core 2.1及以上版本
这个问题在EF Core 2.1版本中已经被官方修复,升级后非主键标识列的元数据会被正确配置,和实际行为一致。你可以通过NuGet包管理器升级相关依赖:
Install-Package Microsoft.EntityFrameworkCore.Tools -Version 2.1.0 Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0
升级完成后,再运行你的测试用例,就能看到AfterSaveBehavior被正确设置为PropertySaveBehavior.Throw,IsReadOnlyAfterSave变为true,元数据和实际行为不再矛盾。
2. 临时 workaround(无法升级时)
如果因为项目限制暂时无法升级版本,你可以手动在模型配置中修正元数据的行为设置,让它和数据库的实际约束对齐:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<DataRecord>() .Property(i => i.ID) .ValueGeneratedNever(); modelBuilder.Entity<DataRecord>() .Property(i => i.IdentityField) .UseSqlServerIdentityColumn() // 手动设置正确的保存行为,和数据库约束匹配 .AfterSaveBehavior(PropertySaveBehavior.Throw) .IsReadOnlyAfterSave(true); }
这样修改后,EF Core的元数据就会正确反映该列的只读特性,避免代码逻辑中出现误解,同时也能在修改操作前就通过元数据判断出该列不可修改,提前规避异常。
你的验证测试整理(供参考)
你编写的xUnit测试用例非常清晰地验证了这个Bug,我把它整理成更易读的格式:
测试准备步骤
- 在Visual Studio 2017中创建.NET Core xUnit测试项目
- 通过包管理器控制台安装依赖:
Install-Package Microsoft.EntityFrameworkCore.Tools Install-Package Microsoft.EntityFrameworkCore.SqlServer
完整测试代码
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using System; using System.Linq; using System.Reflection; using Xunit; namespace EFCoreIdentityColumn { public class DataRecord { public int ID { get; set; } public int IdentityField { get; set; } } class IdentityContext : DbContext { public IdentityContext() { } public IdentityContext(DbContextOptions<IdentityContext> options) : base(options) { } public DbSet<DataRecord> DataRecords { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string conn = "Server=(localdb)\\mssqllocaldb;Database=TESTIDENTITY;Trusted_Connection=True;MultipleActiveResultSets=true"; optionsBuilder.UseSqlServer(conn); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<DataRecord>() .Property(i => i.ID) .ValueGeneratedNever(); modelBuilder.Entity<DataRecord>() .Property(i => i.IdentityField) .UseSqlServerIdentityColumn(); } } public class IdentityColumnXunitTest { [Fact] public void IdentitySaveBehaviorWithMSSQL() { using (var context = new IdentityContext()) { // 检查DataRecord.IdentityField的元数据属性 var identityPropInfo = typeof(DataRecord).GetTypeInfo().DeclaredProperties.First(p => p.Name == "IdentityField"); var identityIProperty = context.Model.FindEntityType(typeof(DataRecord)).FindProperty(identityPropInfo); Assert.Equal(ValueGenerated.OnAdd, identityIProperty.ValueGenerated); Assert.Equal(PropertySaveBehavior.Save, identityIProperty.BeforeSaveBehavior); Assert.Equal(PropertySaveBehavior.Save, identityIProperty.AfterSaveBehavior); Assert.False(identityIProperty.IsReadOnlyAfterSave); Assert.True(identityIProperty.SqlServer().ValueGenerationStrategy == SqlServerValueGenerationStrategy.IdentityColumn); Assert.False(identityIProperty.IsNullable); } } [Fact] public void IdentityColumnSaveBehaviorWithMSSQL() { using (var context = new IdentityContext()) { context.Database.ExecuteSqlCommand("TRUNCATE TABLE DataRecords"); DataRecord rec1 = new DataRecord(); var entry1 = context.Add(rec1); Assert.Equal(EntityState.Added, entry1.State); Assert.Equal(1, context.SaveChanges()); // 成功添加一条记录 Assert.Equal(0, context.DataRecords.First().ID); Assert.Equal(1, context.DataRecords.First().IdentityField); // 主键值保存后只读,修改会抛出异常 rec1.ID = 10; Assert.Throws<InvalidOperationException>(() => context.SaveChanges()); } using (var context = new IdentityContext()) { // 添加时手动指定标识列值会抛出异常 DataRecord rec2 = new DataRecord() { ID = 323, IdentityField = 5 }; var entry2 = context.Add(rec2); Assert.Equal(EntityState.Added, entry2.State); Assert.Throws<DbUpdateException>(() => context.SaveChanges()); } using (var context = new IdentityContext()) { DataRecord rec = new DataRecord() { ID = 299, }; var entry = context.Add(rec); Assert.Equal(EntityState.Added, entry.State); Assert.Equal(1, context.SaveChanges()); // 成功添加一条记录 Assert.Equal(299, context.DataRecords.First(i => i.ID == rec.ID).ID); Assert.Equal(2, context.DataRecords.First(i => i.ID == rec.ID).IdentityField); // 修改已保存的标识列值会抛出异常 Assert.True(context.Entry(rec).State == EntityState.Unchanged); rec.IdentityField = 544; Assert.True(context.Entry(rec).State == EntityState.Modified); Assert.Throws<DbUpdateException>(() => context.SaveChanges()); } } } }
执行数据库迁移
Add-Migration migration_name Update-Database
测试结果完美证实了元数据设置和实际行为的矛盾,通过升级版本或手动配置即可解决该问题。
内容的提问来源于stack exchange,提问作者maclaud

