When model has 1-many FK navigation and composite key we get into invalid state and fail on insert with non-obvious exception:
Violation of PRIMARY KEY constraint 'PK_dbo.ArubaTasks'. Cannot insert duplicate key in object 'dbo.ArubaTasks'.
Consider the following model:
public class ArubaRun
{
public int Id { get; set; }
public string Name { get; set; }
public int Purpose { get; set; }
public ICollection<ArubaTask> Tasks { get; set; }
}
public class ArubaTask
{
public int Id { get; set; }
public string Name { get; set; }
public bool Deleted { get; set; }
}
public class ArubaContext : DbContext
{
static ArubaContext()
{
Database.SetInitializer(new ArubaInitializer());
}
public DbSet<ArubaRun> Runs { get; set; }
public DbSet<ArubaTask> Tasks { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// composite key, non-integer key
modelBuilder.Entity<ArubaTask>().HasKey(k => new { k.Id, k.Name });
// need to map key explicitly, otherwise we get into invalid state
//modelBuilder.Entity<ArubaRun>().HasMany(r => r.Tasks).WithRequired().Map(m => { });
}
}
public class ArubaInitializer : DropCreateDatabaseIfModelChanges<ArubaContext>
{
private const int EntitiesCount = 10;
protected override void Seed(ArubaContext context)
{
var runs = InitializeRuns();
var tasks = InitializeTasks();
for (var i = 0; i < EntitiesCount; i++)
{
for (var j = 0; j < 3; j++)
{
runs[i].Tasks.Add(tasks[(i + j) % EntitiesCount]);
}
}
for (int i = 0; i < EntitiesCount; i++)
{
context.Runs.Add(runs[i]);
context.Tasks.Add(tasks[i]);
}
context.SaveChanges();
base.Seed(context);
}
private ArubaRun[] InitializeRuns()
{
var runs = new ArubaRun[EntitiesCount];
for (var i = 0; i < EntitiesCount; i++)
{
var run = new ArubaRun
{
Id = i,
Name = "Run Name" + i,
Purpose = i + 10,
Tasks = new List<ArubaTask>(),
};
runs[i] = run;
}
return runs;
}
private ArubaTask[] InitializeTasks()
{
var tasks = new ArubaTask[EntitiesCount];
for (int i = 0; i < EntitiesCount; i++)
{
var task = new ArubaTask
{
Id = i / 2,
Name = i % 2 == 0 ? "Foo" : "Bar",
Deleted = i % 3 == 0,
};
tasks[i] = task;
}
return tasks;
}
}
When we create database for it, we (incorrectly) pick Id par of Task key as a FK (see attached picture for details). We should have created a new FK column instead.
Stack trace:
System.Data.Entity.Core.Mapping.Update.Internal.DynamicUpdateCommand.Execute(System.Collections.Generic.Dictionary<int,object> identifierValues, System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<System.Data.Entity.Core.Mapping.Update.Internal.PropagatorResult,object>> generatedValues, System.Data.Entity.Internal.IDbCommandInterceptor commandInterceptor)
System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.Update()
System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update.AnonymousMethod__2(System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator ut)
System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update<int>(System.Data.Entity.Core.IEntityStateManager entityCache, int noChangesResult, System.Func<System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator,int> updateFunction, bool throwOnClosedConnection)
System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update(System.Data.Entity.Core.IEntityStateManager entityCache, bool throwOnClosedConnection)
System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore.AnonymousMethod__20()
System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction<int>(System.Func<int> func, bool throwOnExistingTransaction, bool startLocalTransaction)
System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore(System.Data.Entity.Core.Objects.SaveOptions options, bool throwOnExistingTransaction)
System.Data.Entity.Core.Objects.ObjectContext.SaveChanges.AnonymousMethod__12()
System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.ProtectedExecute<int>(System.Func<int> func)
System.Data.Entity.Infrastructure.ExecutionStrategy.Execute<int>(System.Func<int> func)
System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(System.Data.Entity.Core.Objects.SaveOptions options)
System.Data.Entity.Internal.InternalContext.SaveChanges()
System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
System.Data.Entity.DbContext.SaveChanges()
Violation of PRIMARY KEY constraint 'PK_dbo.ArubaTasks'. Cannot insert duplicate key in object 'dbo.ArubaTasks'.
Consider the following model:
public class ArubaRun
{
public int Id { get; set; }
public string Name { get; set; }
public int Purpose { get; set; }
public ICollection<ArubaTask> Tasks { get; set; }
}
public class ArubaTask
{
public int Id { get; set; }
public string Name { get; set; }
public bool Deleted { get; set; }
}
public class ArubaContext : DbContext
{
static ArubaContext()
{
Database.SetInitializer(new ArubaInitializer());
}
public DbSet<ArubaRun> Runs { get; set; }
public DbSet<ArubaTask> Tasks { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// composite key, non-integer key
modelBuilder.Entity<ArubaTask>().HasKey(k => new { k.Id, k.Name });
// need to map key explicitly, otherwise we get into invalid state
//modelBuilder.Entity<ArubaRun>().HasMany(r => r.Tasks).WithRequired().Map(m => { });
}
}
public class ArubaInitializer : DropCreateDatabaseIfModelChanges<ArubaContext>
{
private const int EntitiesCount = 10;
protected override void Seed(ArubaContext context)
{
var runs = InitializeRuns();
var tasks = InitializeTasks();
for (var i = 0; i < EntitiesCount; i++)
{
for (var j = 0; j < 3; j++)
{
runs[i].Tasks.Add(tasks[(i + j) % EntitiesCount]);
}
}
for (int i = 0; i < EntitiesCount; i++)
{
context.Runs.Add(runs[i]);
context.Tasks.Add(tasks[i]);
}
context.SaveChanges();
base.Seed(context);
}
private ArubaRun[] InitializeRuns()
{
var runs = new ArubaRun[EntitiesCount];
for (var i = 0; i < EntitiesCount; i++)
{
var run = new ArubaRun
{
Id = i,
Name = "Run Name" + i,
Purpose = i + 10,
Tasks = new List<ArubaTask>(),
};
runs[i] = run;
}
return runs;
}
private ArubaTask[] InitializeTasks()
{
var tasks = new ArubaTask[EntitiesCount];
for (int i = 0; i < EntitiesCount; i++)
{
var task = new ArubaTask
{
Id = i / 2,
Name = i % 2 == 0 ? "Foo" : "Bar",
Deleted = i % 3 == 0,
};
tasks[i] = task;
}
return tasks;
}
}
When we create database for it, we (incorrectly) pick Id par of Task key as a FK (see attached picture for details). We should have created a new FK column instead.
Stack trace:
System.Data.Entity.Core.Mapping.Update.Internal.DynamicUpdateCommand.Execute(System.Collections.Generic.Dictionary<int,object> identifierValues, System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<System.Data.Entity.Core.Mapping.Update.Internal.PropagatorResult,object>> generatedValues, System.Data.Entity.Internal.IDbCommandInterceptor commandInterceptor)
System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.Update()
System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update.AnonymousMethod__2(System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator ut)
System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update<int>(System.Data.Entity.Core.IEntityStateManager entityCache, int noChangesResult, System.Func<System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator,int> updateFunction, bool throwOnClosedConnection)
System.Data.Entity.Core.EntityClient.Internal.EntityAdapter.Update(System.Data.Entity.Core.IEntityStateManager entityCache, bool throwOnClosedConnection)
System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore.AnonymousMethod__20()
System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction<int>(System.Func<int> func, bool throwOnExistingTransaction, bool startLocalTransaction)
System.Data.Entity.Core.Objects.ObjectContext.SaveChangesToStore(System.Data.Entity.Core.Objects.SaveOptions options, bool throwOnExistingTransaction)
System.Data.Entity.Core.Objects.ObjectContext.SaveChanges.AnonymousMethod__12()
System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.ProtectedExecute<int>(System.Func<int> func)
System.Data.Entity.Infrastructure.ExecutionStrategy.Execute<int>(System.Func<int> func)
System.Data.Entity.Core.Objects.ObjectContext.SaveChanges(System.Data.Entity.Core.Objects.SaveOptions options)
System.Data.Entity.Internal.InternalContext.SaveChanges()
System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
System.Data.Entity.DbContext.SaveChanges()