You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core 2.0.1中非主键标识列AfterSaveBehavior属性值异常问询

关于EF Core 2.0.1中非主键标识列AfterSaveBehavior属性的Bug分析与解决方法

没错,你发现的这个矛盾点确实是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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:44:08