I have an entity that has an Auto-identity (int)
column. As part of the data-seed I want to use specific identifier values for the \"standard data\" in my syste
I created an alternate constructor for my DbContext
that takes a bool allowIdentityInserts
. I set that bool to a private field of the same name on the DbContext
.
My OnModelCreating
then "unspecifies" the Identity spec if I create the context in that "mode"
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
if(allowIdentityInsert)
{
modelBuilder.Entity<ChargeType>()
.Property(x => x.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
This allows me to insert Ids without changing my actual database identity spec. I still need to use the identity insert on/off trick you did, but at least EF will send Id values.
Can not be done without a second EF level model - copy the classes for the seeding.
As you said - your metadata says that the DB provides the value, which it does not during the seeding.
After experimenting several options found on this site, the following code worked for me (EF 6). Notice that it first attempts a normal update if the item already exists. If it does not, then tries a normal insert, if the error is due to IDENTITY_INSERT then tries the workaround. Notice also that db.SaveChanges will fail, hence the db.Database.Connection.Open() statement and optional verification step. Be aware this is not updating the context, but in my case it is not necessary. Hope this helps!
public static bool UpdateLeadTime(int ltId, int ltDays)
{
try
{
using (var db = new LeadTimeContext())
{
var result = db.LeadTimes.SingleOrDefault(l => l.LeadTimeId == ltId);
if (result != null)
{
result.LeadTimeDays = ltDays;
db.SaveChanges();
logger.Info("Updated ltId: {0} with ltDays: {1}.", ltId, ltDays);
}
else
{
LeadTime leadtime = new LeadTime();
leadtime.LeadTimeId = ltId;
leadtime.LeadTimeDays = ltDays;
try
{
db.LeadTimes.Add(leadtime);
db.SaveChanges();
logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
}
catch (Exception ex)
{
logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex.Message);
logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
if (ex.InnerException.InnerException.Message.Contains("IDENTITY_INSERT"))
{
logger.Warn("Attempting workaround...");
try
{
db.Database.Connection.Open(); // required to update database without db.SaveChanges()
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] ON");
db.Database.ExecuteSqlCommand(
String.Format("INSERT INTO[dbo].[LeadTime]([LeadTimeId],[LeadTimeDays]) VALUES({0},{1})", ltId, ltDays)
);
db.Database.ExecuteSqlCommand("SET IDENTITY_INSERT[dbo].[LeadTime] OFF");
logger.Info("Inserted ltId: {0} with ltDays: {1}.", ltId, ltDays);
// No need to save changes, the database has been updated.
//db.SaveChanges(); <-- causes error
}
catch (Exception ex1)
{
logger.Warn("Error captured in UpdateLeadTime({0},{1}) was caught: {2}.", ltId, ltDays, ex1.Message);
logger.Warn("Inner exception message: {0}", ex1.InnerException.InnerException.Message);
}
finally
{
db.Database.Connection.Close();
//Verification
if (ReadLeadTime(ltId) == ltDays)
{
logger.Info("Insertion verified. Workaround succeeded.");
}
else
{
logger.Info("Error!: Insert not verified. Workaround failed.");
}
}
}
}
}
}
}
catch (Exception ex)
{
logger.Warn("Error in UpdateLeadTime({0},{1}) was caught: {2}.", ltId.ToString(), ltDays.ToString(), ex.Message);
logger.Warn("Inner exception message: {0}", ex.InnerException.InnerException.Message);
Console.WriteLine(ex.Message);
return false;
}
return true;
}
If you use database first model, then you should change StoreGeneratedPattern property of ID column from Identity to None.
After that, as I replied here, this should help:
using (var transaction = context.Database.BeginTransaction())
{
var myThing = new ReferenceThing
{
Id = 1,
Name = "Thing with Id 1"
};
context.Set<ReferenceThing>.Add(myThing);
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON");
context.SaveChanges();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF");
transaction.Commit();
}
So I might have resolved this one by resorting to generating my own SQL insert statements that include the Id column. It feels like a terrible hack, but it works :-/
public class Seeder
{
public void Seed (DbContext context)
{
var myThing = new ReferenceThing
{
Id = 1,
Name = "Thing with Id 1"
};
context.Set<ReferenceThing>.Add(myThing);
context.Database.Connection.Open();
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing ON")
// manually generate SQL & execute
context.Database.ExecuteSqlCommand("INSERT ReferenceThing (Id, Name) " +
"VALUES (@0, @1)",
myThing.Id, myThing.Name);
context.Database.ExecuteSqlCommand("SET IDENTITY_INSERT ReferenceThing OFF")
}
}
Try adding this code to your DB Context "to keep it clean" so as to speak:
Usage Scenario example (Add ID 0 default records to entity type ABCStatus:
protected override void Seed(DBContextIMD context)
{
bool HasDefaultRecord;
HasDefaultRecord = false;
DBContext.ABCStatusList.Where(DBEntity => DBEntity.ID == 0).ToList().ForEach(DBEntity =>
{
DBEntity.ABCStatusCode = @"Default";
HasDefaultRecord = true;
});
if (HasDefaultRecord) { DBContext.SaveChanges(); }
else {
using (var dbContextTransaction = DBContext.Database.BeginTransaction()) {
try
{
DBContext.IdentityInsert<ABCStatus>(true);
DBContext.ABCStatusList.Add(new ABCStatus() { ID = 0, ABCStatusCode = @"Default" });
DBContext.SaveChanges();
DBContext.IdentityInsert<ABCStatus>(false);
dbContextTransaction.Commit();
}
catch (Exception ex)
{
// Log Exception using whatever framework
Debug.WriteLine(@"Insert default record for ABCStatus failed");
Debug.WriteLine(ex.ToString());
dbContextTransaction.Rollback();
DBContext.RollBack();
}
}
}
}
Add this helper class for Get Table Name extension method
public static class ContextExtensions
{
public static string GetTableName<T>(this DbContext context) where T : class
{
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
return objectContext.GetTableName<T>();
}
public static string GetTableName<T>(this ObjectContext context) where T : class
{
string sql = context.CreateObjectSet<T>().ToTraceString();
Regex regex = new Regex(@"FROM\s+(?<table>.+)\s+AS");
Match match = regex.Match(sql);
string table = match.Groups["table"].Value;
return table;
}
}
The code to add to the DBContext:
public MyDBContext(bool _EnableIdentityInsert)
: base("name=ConnectionString")
{
EnableIdentityInsert = _EnableIdentityInsert;
}
private bool EnableIdentityInsert = false;
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<DBContextIMD, Configuration>());
//modelBuilder.Entity<SomeEntity>()
// .Property(e => e.SomeProperty)
// .IsUnicode(false);
// Etc... Configure your model
// Then add the following bit
if (EnableIdentityInsert)
{
modelBuilder.Entity<SomeEntity>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<AnotherEntity>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
//Add this for Identity Insert
/// <summary>
/// Enable Identity insert for specified entity type.
/// Note you should wrap the identity insert on, the insert and the identity insert off in a transaction
/// </summary>
/// <typeparam name="T">Entity Type</typeparam>
/// <param name="On">If true sets identity insert on else set identity insert off</param>
public void IdentityInsert<T>(bool On)
where T: class
{
if (!EnableIdentityInsert)
{
throw new NotSupportedException(string.Concat(@"Cannot Enable entity insert on ", typeof(T).FullName, @" when _EnableIdentityInsert Parameter is not enabled in constructor"));
}
if (On)
{
Database.ExecuteSqlCommand(string.Concat(@"SET IDENTITY_INSERT ", this.GetTableName<T>(), @" ON"));
}
else
{
Database.ExecuteSqlCommand(string.Concat(@"SET IDENTITY_INSERT ", this.GetTableName<T>(), @" OFF"));
}
}
//Add this for Rollback changes
/// <summary>
/// Rolls back pending changes in all changed entities within the DB Context
/// </summary>
public void RollBack()
{
var changedEntries = ChangeTracker.Entries()
.Where(x => x.State != EntityState.Unchanged).ToList();
foreach (var entry in changedEntries)
{
switch (entry.State)
{
case EntityState.Modified:
entry.CurrentValues.SetValues(entry.OriginalValues);
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
case EntityState.Deleted:
entry.State = EntityState.Unchanged;
break;
}
}
}