EF6 vs Entity Framework Core:插入实体而不将主键(身份)重置为零

Posted

技术标签:

【中文标题】EF6 vs Entity Framework Core:插入实体而不将主键(身份)重置为零【英文标题】:EF6 vs Entity Framework Core : Insert an entity without resetting the primary key (identity) to zero 【发布时间】:2018-05-15 20:25:16 【问题描述】:

EF6:插入一个已经具有主键值的实体可以工作,并为主键分配一个新值。

EF Core:它尝试插入主键的值,但显然失败并出现 SqlException

当 IDENTITY_INSERT 设置为 OFF 时,无法为表“资产”中的标识列插入显式值。

我找到的解决方法是将 PK 重置为默认值(0 表示 int)。

例子:

        Asset asset = new Asset
        
            Id = 3,
            Name = "Toto",
        ;      

        using (AppDbContext context = new AppDbContext())
        
            asset.Id = 0; // needs to be reset
            context.Assets.Add(asset);
            context.SaveChanges();
        

我正在将解决方案从 EF 6 迁移到 EF Core,并且我想避免在插入的情况下手动重置所有 ID。上面的例子很简单,有时我要插入一个完整的图表,在我的情况下,这意味着重置图表的所有 PK 和 FK。

我能想到的唯一自动解决方案是使用反射来解析整个图形并重置 ID。但我认为这不是很有效...

我的问题:如何全局禁用该行为?

【问题讨论】:

我想说的是,由于重新插入具有重复键的对象的糟糕决定,试图让新系统做同样的事情也是一个糟糕的主意。正确的操作选择是正确修复所有代码。 那不是我的代码,我的工作是移植到 ef core,而不是重构。 我会亲自向任何人提出。了解真正糟糕的代码并试图让它发挥作用,这不是我想为之工作的公司。如果这是你的情况,我真的很抱歉,我希望有人知道如何解决这个问题。 “我的工作是移植,而不是修复”不是一个好态度,我给你的建议。 @TheUknown 将修改现有条目。 【参考方案1】:

不幸的是,目前(从 EF Core 2.0.1 开始)这种行为是不可控的。我想它应该是对 EF6 的改进,因为它允许您执行身份插入。

幸运的是,EF Core 构建在基于服务的架构上,它允许您((虽然不是那么容易)替换几乎所有方面。在这种情况下,负责的服务称为 IValueGenerationManager 并由 ValueGenerationManager 类实现。提供上述行为的行是

private static IEnumerable<IProperty> FindPropagatingProperties(InternalEntityEntry entry)    
    => entry.EntityType.GetProperties().Where(    
        property => property.IsForeignKey()    
                    && property.ClrType.IsDefaultValue(entry[property]));

private static IEnumerable<IProperty> FindGeneratingProperties(InternalEntityEntry entry)    
    => entry.EntityType.GetProperties().Where(    
        property => property.RequiresValueGenerator()    
                    && property.ClrType.IsDefaultValue(entry[property]));

特别是&amp;&amp; property.ClrType.IsDefaultValue(entry[property]) 条件。

如果这些方法是 virtual 会很好,但它们不是,所以为了删除该检查,您基本上需要复制几乎所有代码。

将以下代码添加到项目中的新代码文件中:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.ValueGeneration;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore

    public static class Extensions
    
        public static DbContextOptionsBuilder UseEF6CompatibleValueGeneration(this DbContextOptionsBuilder optionsBuilder)
        
            optionsBuilder.ReplaceService<IValueGenerationManager, EF6CompatibleValueGeneratorManager>();
            return optionsBuilder;
        
    


namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal

    public class EF6CompatibleValueGeneratorManager : ValueGenerationManager
    
        private readonly IValueGeneratorSelector valueGeneratorSelector;
        private readonly IKeyPropagator keyPropagator;

        public EF6CompatibleValueGeneratorManager(IValueGeneratorSelector valueGeneratorSelector, IKeyPropagator keyPropagator)
            : base(valueGeneratorSelector, keyPropagator)
        
            this.valueGeneratorSelector = valueGeneratorSelector;
            this.keyPropagator = keyPropagator;
        

        public override InternalEntityEntry Propagate(InternalEntityEntry entry)
        
            InternalEntityEntry chosenPrincipal = null;
            foreach (var property in FindPropagatingProperties(entry))
            
                var principalEntry = keyPropagator.PropagateValue(entry, property);
                if (chosenPrincipal == null)
                    chosenPrincipal = principalEntry;
            
            return chosenPrincipal;
        

        public override void Generate(InternalEntityEntry entry)
        
            var entityEntry = new EntityEntry(entry);
            foreach (var property in FindGeneratingProperties(entry))
            
                var valueGenerator = GetValueGenerator(entry, property);
                SetGeneratedValue(entry, property, valueGenerator.Next(entityEntry), valueGenerator.GeneratesTemporaryValues);
            
        

        public override async Task GenerateAsync(InternalEntityEntry entry, CancellationToken cancellationToken = default(CancellationToken))
        
            var entityEntry = new EntityEntry(entry);
            foreach (var property in FindGeneratingProperties(entry))
            
                var valueGenerator = GetValueGenerator(entry, property);
                SetGeneratedValue(entry, property, await valueGenerator.NextAsync(entityEntry, cancellationToken), valueGenerator.GeneratesTemporaryValues);
            
        

        static IEnumerable<IProperty> FindPropagatingProperties(InternalEntityEntry entry)
        
            return entry.EntityType.GetProperties().Where(property => property.IsForeignKey());
        

        static IEnumerable<IProperty> FindGeneratingProperties(InternalEntityEntry entry)
        
            return entry.EntityType.GetProperties().Where(property => property.RequiresValueGenerator());
        

        ValueGenerator GetValueGenerator(InternalEntityEntry entry, IProperty property)
        
            return valueGeneratorSelector.Select(property, property.IsKey() ? property.DeclaringEntityType : entry.EntityType);
        

        static void SetGeneratedValue(InternalEntityEntry entry, IProperty property, object generatedValue, bool isTemporary)
        
            if (generatedValue == null) return;
            entry[property] = generatedValue;
            if (isTemporary)
                entry.MarkAsTemporary(property, true);
        
    

然后在您的DbContext 派生类中覆盖OnConfiguring 并添加以下行:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)

    // ...
    optionsBuilder.UseEF6CompatibleValueGeneration();

就是这样 - 问题解决了。

【讨论】:

我已经在家里的一个小项目中尝试过(我不在工作)......哇,你是专业人士。感谢您拯救我的周末!【参考方案2】:

由于 EF Core 不支持拦截器,我的建议是“破解它”。

如果您覆盖SaveChanges 方法,您可以在每次保存时提供此功能,而无需大量重构。

public class MyContext : DbContext

    //...

    public override int SaveChanges()
    
        foreach (var dbEntityEntry in ChangeTracker.Entries<Asset>()
                                                   .Where(t => t.State == EntityState.Added))
        
            dbEntityEntry.Entity.Id = default(int);
        
        return base.SaveChanges();
    

这可能最终需要为每种类型创建一个循环,或者您可以发挥创意,但一般来说,这将完成您想要的,而无需对您的业务方法进行大量更改。

【讨论】:

以上是关于EF6 vs Entity Framework Core:插入实体而不将主键(身份)重置为零的主要内容,如果未能解决你的问题,请参考以下文章

ASPNet Entity Framework 6 - EF6,在同一个工作单元中混合异步和同步

VS 2017 + EF6 + MySQL5.7 建立实体模型闪退问题

使用 System.Data.SQLite 和 Entity Framework 6 的简单示例

如何将 Database First Entity Framework 4 升级到 6

使用 Entity Framework 6.1 和 MVC 5 从数据库中使用 Code First 后如何同步模型?

Entity Framework Core - 将小数精度和小数位数设置为所有小数属性[重复]