Вопрос

I am looking into Migrations in an effort to clean up our deployment processes. The less manual intervention required when pushing a change to production the better.

I have run into 3 major snags with the migrations system. They are show stoppers if I can not figure out a clean way around them.

1. How do I add Seed data per migration:

I execute the command "add-migration" which scaffolds a new migration file with Up and Down functions. Now, I want to automatically make changes to the data with both Up and Down changes. I don't want to add the Seed data to the Configuration.Seed method as this runs for all migrations which ends in all sorts of duplication problems.

2. If the above is not possible, how do I avoid duplications?

I have an enum that I loop through to add the values to the database.

foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
    context.Access.AddOrUpdate(
        new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
    );
}
context.SaveChanges();

Even though I am using AddOrUpdate, I still get duplicates in the database. The above code brings me to my 3rd and final problem:

3. How can I seed Primary Keys?

My enumerable with the above code is:

public class Access
{
    public enum Level
    {
        None = 10,
        Read = 20,
        ReadWrite = 30
    }
    public int AccessId { get; set; }
    public string Name { get; set; }
}

I am specifying the values that I want as my primary key, but Entity Framework seems to ignore it. They still end up being 1,2,3. How do I get it to be 10,20,30?

Are these limitations of EF at the moment or are they intentional constraints to prevent some other kind of catastrophe I am not seeing?

Это было полезно?

Решение

  1. When I have fixed data that I want to insert with a migration, I put the inserts directly in the Up() migration using calls to Sql("Insert ..."). See the note halfway down this page: how to insert fixed data
  2. You prevent duplicates in the Seed method by calling the AddOrUpdate overload that takes an identifier expression specifying the natural key - see this answer and this blog entry.
  3. Primary keys that are integers are created as identity fields by default. To specify otherwise use the [DatabaseGenerated(DatabaseGeneratedOption.None)] attribute

I think this is a good explanation of Initializer and Seed methods

Here is an example of how to use the AddOrUpdate method:

foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
    context.Access.AddOrUpdate(
        x => x.Name, //the natural key is "Name"
        new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
    );
}

Другие советы

As a possible solution to item 1, I made an implementation of the IDatabaseInitializer strategy which will run the Seed method of each pending migration only, you will need to implement a custom IMigrationSeed interface in each of your DbMigration classes, the Seed method will then be implemented right after Up and Down methods of every migration class.

This helps to solve two problems for me:

  1. Group Database Model Migration with Database Data Migration (or Seeding)
  2. Check what part of the Seed migration code should really be running, not checking data in the database but using already known data which is the database model that was just created.

The interface looks like this

public interface IMigrationSeed<TContext>
{
    void Seed(TContext context);
}

Below is the new implementation that will call this Seed method

public class CheckAndMigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration>
    : IDatabaseInitializer<TContext>
    where TContext : DbContext
    where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>, new()
{
    public virtual void InitializeDatabase(TContext context)
    {
        var migratorBase = ((MigratorBase)new DbMigrator(Activator.CreateInstance<TMigrationsConfiguration>()));

        var pendingMigrations = migratorBase.GetPendingMigrations().ToArray();
        if (pendingMigrations.Any()) // Is there anything to migrate?
        {
            // Applying all migrations
            migratorBase.Update();
            // Here all migrations are applied

            foreach (var pendingMigration in pendingMigrations)
            {
                var migrationName = pendingMigration.Substring(pendingMigration.IndexOf('_') + 1);
                var t = typeof(TMigrationsConfiguration).Assembly.GetType(
                    typeof(TMigrationsConfiguration).Namespace + "." + migrationName);

                if (t != null 
                   && t.GetInterfaces().Any(x => x.IsGenericType 
                      && x.GetGenericTypeDefinition() == typeof(IMigrationSeed<>)))
                {
                    // Apply migration seed
                    var seedMigration = (IMigrationSeed<TContext>)Activator.CreateInstance(t);
                    seedMigration.Seed(context);
                    context.SaveChanges();
                }
            }
        }
    }
}

The good thing here is you have a real EF context to manipulate Seed Data, just like standard EF Seed implementation. However this can get strange if for example you decide to delete a table that was Seeded in a previous migration, you will have to refactor your existing Seed code accordingly.

EDIT: As an alternative to implement the seed method after the Up and Down, you can create a partial class of the same Migration class, I found this useful as it allows me to safely delete the migration class when I want to re-seed the same migration.

Hi I have found a very useful information for your problem in this link: Safari Books Online

"1. How do I add Seed data per migration:" As you see in the example you need to create a new confiugration for seeding. This seed Configuration must be called after migration.

public sealed class Configuration : DbMigrationsConfiguration
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(SafariCodeFirst.SeminarContext context)
    {
        //  This method will be called after migrating to the latest version.

        //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
        //  to avoid creating duplicate seed data. E.g.
        //
        //    context.People.AddOrUpdate(
        //      p => p.FullName,
        //      new Person { FullName = "Andrew Peters" },
        //      new Person { FullName = "Brice Lambson" },
        //      new Person { FullName = "Rowan Miller" }
        //    );
        //
    }
}

"2. If the above is not possible, how do I avoid duplications?"

AddOrUpdate Must help you exactly to avoding the duplicates if you get an error here you might have a configuration error post the call stack please. See the example!

"3. How can I seed Primary Keys?"

Here it is also on your key definition. If your key DatabaseGenerated(DatabaseGeneratedOption.Identity) than you do not have to provide it. In some other senarios you need to create a new one it is depending on the key type.

"Are these limitations of EF at the moment or are they intentional constraints to prevent some other kind of catastrophe I am not seeing?"
Not that I know!

OK, so with a bit of bashing I have managed to bash EF into submission. Here is what I did:

1. There is no way that I found to see data for a specific migration. It all must go into the common Configuration.Seed method.

2. To avoid duplicates I had to do 2 things. For my enums, I wrote the following seed code:

foreach (var enumValue in Enum.GetValues(typeof(Access.Level)))
{
    var id = (int)enumValue;
    var val = enumValue.ToString();

    if(!context.Access.Any(e => e.AccessId == id))
        context.Access.Add(
            new Access { AccessId = id, Name = val }
        );
}
context.SaveChanges();

So basically, just checking if it exists and adding if not

3. In order for the above to work, you need to be able to insert Primary Key Values. Luckily for me this table will always have the same static data so I could deactivate the auto increment. To do that, the code looks like:

public class Access
{
    public enum Level
    {
        None = 10,
        Read = 20,
        ReadWrite = 30
    }

    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int AccessId { get; set; }
    public string Name { get; set; }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top