Thursday, 14 November 2013

Creating an Entity Framework Code-First schema generator

I've not been using Entity Framework for a great deal of time and my current project is pre-version 1.0, so haven't quite settled into the whole Entity Framework migrations thing and I dare say, when I actually want to maintain the data in a database, I'll start using migrations. Until then I just want to blat a database whenever I want, but at the same time, I don't want to have to go creating test data every time the application is spun up, so using DropCreateDatabaseAlways isn't an option.

So the alternative is DropCreateDatabaseIfModelChanges, but what if I want to clear out all the data in the database, effectively going back to a default data set (pre-populated lookup/standing data tables)?

Luckily today, I've managed to get a console application I've been writing working. This console app will regenerate the database, the schema (table structure) and any seed data for me, making it really handy for a continuous integration scenario and when I want a fresh database on my development machine.

You'll more than likely have a database context class that inherits from DbContext and in there, perhaps in the constructor, you'll have a call to DbContextUtils<>.SetInitializer() or perhaps Database.SetInitializer() (or perhaps you've got it in your Global.asax Application_Start which just smelled too much like a violation of the Separation of Concerns principle to me). Regardless, in my scenario, I changed the hardwired usage of DropCreateDatabaseIfModelChanges in my database context constructor to a constructor-injected instance of IDatabaseInitializer. The beauty of this is now, depending on the Composition Root, I can inject a different database initialiser. Here's the database context:

public class ExampleDbContext : DbContext
{
    public DbSet Cases { get; set; }
    public DbSet Events { get; set; }

    public ExampleDbContext(
        IDatabaseInitializer<ExampleDbContext> databaseInitializer)
    {
        DbContextUtils<ExampleDbContext>
            .SetInitializer(databaseInitializer);
    }
}

So, in my Application_Start I can pass in my initialiser that derives from DropCreateDatabaseIfModelChanges (using Structuremap, of course other DI containers will work too) meaning my test data only gets wiped every time I change the model, which is safe and correct. Here is the structure map configuration:

    x.For<IDatabaseInitializer<ExampleDbContext>>()
        .Use<DropCreateDatabaseIfModelChanges>();

On the other hand, in my schema generator console app, I can new up my database context, passing in an initialiser derived from DropCreateDatabaseAlways<> and then call Database.Initialize(). Like so:

public class Program
{
    public static void Main(string[] args)
    {
        var dbContext = new ExampleDbContext(
            new DropCreateDatabaseAlways<ExampleDbContext>());

        dbContext.Database.Initialize(true);
    }
}

The last step in the puzzle, creating all your seed data every time a schema generation (or a regeneration by the application after a model change) fires, I've created a DataSeeder class, whose responsibility is to actually populate the data context's DbSets and then commit/save this:

public class DatabaseSeeder
{
    public void Seed(ExampleDbContext context)
    {
        new List<Cases>
        {
            new Case { ... },
        }.ForEach(c => context.Cases.Add(c));

        context.SaveChanges();
    }
}

This can then be called from classes that inherit from DropCreateDatabaseIfModelChanges and DropCreateDatabaseAlways for your application and the schema generator console application respectfully. Creating classes called DropCreateDatabaseAndDefaultDataIfModelChanges and DropCreateDatabaseAndDefaultDataAlways. You will want to override the Seed method in both implementations to call your DatabaseSeeder.Seed method. You can then go through and update the places where you've used the base classes with the new child classes you've implemented. Now when you run the schema generator and when the application detects a model change, the seed data should be created after the database structure is recreated.

Sweet!

A little word of warning, depending on your connection string, you may start getting exceptions stating that the CREATE DATABASE privilege is not present. If you're not using integrated security, ensure that the user you're connecting to the database as has the dbcreator privilege in SQL Server. Failing that, it will be some permissions problem.

No comments:

Post a Comment