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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With