Wednesday, July 30, 2014

Entity Framework Code First: Custom data context initializer

While working with Entity Framework Code First at some point you will probably decide to create custom database initializer which allow you to create/recreate database based on flag in configuration file and automatically seed this database with data, stored procedures, views etc.

First lets define the strategy for database creation.

    public enum DatabaseInitializerStrategyEnum
    {
        DropCreateAlways,
        CreateIfNotExists,
        DropCreateDatabaseIfModelChanges
    }

Now we need an initializer for each strategy. There are already existing implementations of IDatabaseInitializer living in System.Data.Entity: CreateDatabaseIfNotExists, DropCreateDatabaseAlways and DropCreateDatabaseIfModelChanges. We want our custom initializers to inherit those existing ones.

Lets take a look at custom initializer implementation:

public class CreateDatabaseIfNotExistsInitializer<TContext> : CreateDatabaseIfNotExists<TContext> where TContext : DbContext
{
 protected Action<TContext> SeedHandler { get; set; }

 public CreateDatabaseIfNotExistsInitializer(Action<TContext> seed = null)
 {
  this.SeedHandler = seed;
 }

 protected override void Seed(TContext context)
 {
  base.Seed(context);

  if (this.SeedHandler != null)
  {
   this.SeedHandler(context);
  }
 }
}

The main feature here is theSeedHandler delegate which will ensure, that our code will be invoked when data is seeding.

The other two initializers (DropCreateDatabaseAlwaysInitializer and DropCreateDatabaseIfModelChangesInitializer) will be identical to the first one, the only difference will be in the class name.

Now we need something that will help us to set up those initializers. Lets make a DatabaseInitializer class, which will set up initializers depending on strategy:

    public class DatabaseInitializer
{
 public static void Initialize<TContext>(
  DatabaseInitializerStrategyEnum strategy = 
   DatabaseInitializerStrategyEnum.CreateIfNotExists,
  Action<TContext> seed = null)
  where TContext : DbContext
 {
  SqlConnection.ClearAllPools();
  var initializer = Create(strategy, seed);
  Database.SetInitializer(initializer);
 }

 private static IDatabaseInitializer<TContext> Create<TContext>(
     DatabaseInitializerStrategyEnum strategy, 
     Action<TContext> seed)
  where TContext : DbContext
 {
  switch (strategy)
  {
   case DatabaseInitializerStrategyEnum.CreateIfNotExists:
    return new CreateDatabaseIfNotExistsInitializer<TContext>(seed);

   case DatabaseInitializerStrategyEnum.DropCreateAlways:
    return new DropCreateDatabaseAlwaysInitializer<TContext>(seed);

   case DatabaseInitializerStrategyEnum.DropCreateDatabaseIfModelChanges:
    return new DropCreateDatabaseIfModelChangesInitializer<TContext>(seed);

   default:
    throw new ArgumentOutOfRangeException("strategy",
     string.Format("db initialize strategy {0} is not supported", strategy));
  }
 }
}

Now we have our database initializer which we can use to initialize our context. Lets make a base context which will use DatabaseInitializer listed above:

    public abstract class DatabaseContextBase : DbContext
    {
        protected static DatabaseInitializerStrategyEnum DefaultStrategy
        {
           get 
           { 
              return DatabaseInitializerStrategyEnum.CreateIfNotExists; 
           }
        }

        protected static void InitializeContext<TContext>(string strategyConfigKey = null, Action<TContext> seed = null)
            where TContext : DbContext
        {
            var strategyKey = string.IsNullOrEmpty(strategyConfigKey)
                ? null
                : ConfigurationManager.AppSettings[strategyConfigKey];

            var strategy = string.IsNullOrEmpty(strategyKey)
                ? DefaultStrategy
                : (DatabaseInitializerStrategyEnum)
                    Enum.Parse(typeof (DatabaseInitializerStrategyEnum), strategyKey);

            DatabaseInitializer.Initialize(strategy, seed);
        }
    }

This is the base context class and it will be inherited by our database context implementation. It will use AppSettings key with database creation strategy defined there:

  <appSettings>
    <add key="DatabaseStrategy" value="DropCreateAlways" />
  </appSettings>

All what’s left now is to create context itself and create helper which will seed appropriate data into the context.

For example we are going to have only one table in our database:

    [Table("Products")]
    public class Product
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [MaxLength(256)]
        public string Name { get; set; }

        [Required]
        [MaxLength(1024)]
        public string Description { get; set; }
    }

So the context is going to look like this:

    public partial class DbCustomContext : DatabaseContextBase
    {
        public static void Initialize()
        {
            InitializeContext<DbCustomContext>("DatabaseStrategy", 
                                                DbContextSeed.SeedHandler());
        }

        public IDbSet<Product> Products { get; set; }
    }

Notice, that the context uses DbContextSeed helper. Lets create this one too:

    public class DbContextSeed
    {
        public static Action<DbCustomContext> SeedHandler()
        {
            Action<DbCustomContext> seedHandler = delegate(DbCustomContext context)
            {
                SeedTestData(context);
                SeedWhatever(context);
            };

            return seedHandler;
        }

        private static void SeedTestData(DbCustomContext context)
        {
            // seed some data
            for (int i = 1; i < 4; i++)
            {
                var product = new Product
                {
                    Description = "Product description " + i,
                    Name = "Product " + i,
                };
                context.Products.Add(product);
            }
            context.SaveChanges();
        }
        
        private static void SeedWhatever(DbCustomContext context)
        {
            // do whatever
        }
    }

That’s it. Now you have database creation strategy customizable trough web/app.config, database seed helper which you can easily modify. Now all you need to do is to call Initialize() method and your custom context is ready for work:

            DbCustomContext.Initialize();

            using (var context = new DbCustomContext())
            {
                var products = context.Products.ToList();
            }

4 comments :

  1. What is the most appropriate place to call the method DbCustomContext.Initialize()?

    ReplyDelete
    Replies

    1. It really depends on what you are trying to achieve. One way to use it is to simply seed database directly in Application_Start() in Global.asax. Something like this: DbCustomContext.Initialize(); using (var db = new DbCustomContext()) { db.Database.Initialize(force: false); } The other way is to use, for instance, WebActivator, wrap the code above in static method of your helper class and call it before application starts. Keep in mind that in order to trigger db creation you should create data context in your code at some point "new DbCustomContext()". Here is the sample project (quick and dirty) which uses database context as described in this blog post. What happens there: context Initialize method is called via WebActivator; db creation is triggered when we actually call Index action in Home controller for the first time.

      Delete
  2. I really liked this post. You have a sample solution available for download?

    ReplyDelete
    Replies
    1. Yep, I've added the link in response to your previous comment. Don't forget to change user credentials in connection string in Web.config, though.

      Delete