4
votes

EF6: inserting an entity that already has a value for the primary key works and assigns a new value to the primary key.

EF Core : it tries to insert the value of the primary key and obviously fails with a SqlException:

Cannot insert explicit value for identity column in table 'Asset' when IDENTITY_INSERT is set to OFF.

The workaround I found is to reset the PK to the default value (0 for an int).

Example :

        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();
        }

I'm migrating a solution from EF 6 to EF Core and I'd like to avoid resetting manually all the IDs in case of an insertion. The example above is really simple, sometimes I have a whole graph to insert, in my case that would mean resetting all the PKs and FKs of the graph.

The only automatic solution I can think of is using reflection to parse the whole graph and reset the IDs. But i don't think that's very efficient...

My question: how to disable that behaviour globally?

2
I'd pretty much say that because of the terrible decision to reinsert a object with a duplicate key, trying to make the new system do the same thing is also a terrible idea. The right choice of action is to fix all the code correctly.Erik Philips
That's not my code, my job is to port to ef core, not to refactor.Akli
I would personally bring it up with whoever. Knowing about literally bad code and just trying to make it work, isn't a company I'd want to work for. If that's your case I'm truly sorry, I hope someone knows how to solve that problem then.Erik Philips
"My job is to port, not to fix" isn't a good attitude, my advice to you.abatishchev
@TheUknown that would modify the existing entry.Akli

2 Answers

5
votes

Unfortunately currently (as of EF Core 2.0.1) this behavior is not controllable. I guess it's supposed to be improvement from EF6 since it allows you to perform identity inserts.

Fortunately EF Core is build on service based architecture which allows you ( (although not so easily) to replace almost every aspect. In this case, the responsible service is called IValueGenerationManager and is implemented by ValueGenerationManager class. The lines that provide the aforementioned behavior are

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]));

specifically the && property.ClrType.IsDefaultValue(entry[property]) condition.

It would have been nice if these methods were virtual, but they are not, so in order to remove that check you basically need to copy almost all code.

Add the following code to a new code file in your project:

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);
        }
    }
}

then override OnConfiguring in your DbContext derived class and add the following line:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    // ...
    optionsBuilder.UseEF6CompatibleValueGeneration();
}

and that's it - problem solved.

2
votes

Since EF Core doesn't support interceptors, my recommendation is to "hack it".

If you overwrite the SaveChanges method you can provide this functionality on every save without a massive refactor.

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();
    }
}

This may end up needing a loop for each type, or maybe you can get creative, but generally speaking this will accomplish what you want without making massive changes to your business methods.