Dealing with Entity Framework Code First Migrations in a configuration way

I’ve promised in one of my last posts, that I’m going to tell you about our way of using Code First Migrations. If you want to read about it, you can do it here.
For this purpose I’ve created some generic classes as described below. The main goal was to be able to configure Migrations from the web.config and to be able to unit test the migrations, so that when the deployment comes, some quality assurance already happens. A perfect stack overflow site provide perfect guidance on testing whether for every change a migration exists and testing all migrations up and down.
First I’ve created a generic Configuration class as follows including the calling of some data initializers:

using System.Collections.Generic;
using System.Data.Entity.Migrations;

namespace Common.Migrations
{
    /// <summary>
    /// Base class for migration configurations.
    /// </summary>
    /// <typeparam name="TContext">The type of the context.</typeparam>
    public abstract class MigrationsConfigurationBase<TContext> : DbMigrationsConfiguration<TContext> where TContext : System.Data.Entity.DbContext
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MigrationsConfigurationBase{TContext}"/> class.
        /// </summary>
        public MigrationsConfigurationBase()
        {
            ContextKey = typeof(TContext).Name;
        }

        /// <summary>
        /// Gets or sets a value indicating whether data inevitable for the working of the system is to be seeded or not.
        /// </summary>
        public bool SeedBaseData { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether seed demo data.
        /// </summary>
        public bool SeedDemoData { get; set; }

        /// <summary>
        /// Gets the initializers.
        /// </summary>
        protected virtual List<IDataInitializer> Initializers
        {
            get { return new List<IDataInitializer>(); }
        }

        /// <summary>
        /// Determines whether a seed is needed based on e.g.: existance of specific entities in the database.
        /// </summary>
        /// <param name="context">The context.</param>
        /// <returns> whether a seed is needed based on e.g.: existance of specific entities in the database.</returns>
        protected abstract bool SeedNeeded(TContext context);

        /// <summary>
        /// Seeds the specified context.
        /// </summary>
        /// <param name="context">The context.</param>
        protected override void Seed(TContext context)
        {
            InitializeSeed(context);
            if (SeedBaseData)
            {
                // No initialization is needed when we already have data there.
                if (SeedNeeded(context))
                {
                    var initializers = Initializers;

                    initializers.ForEach(x =>
                    {
                        x.Seed(context, SeedDemoData);
                        SaveChangesBetweenInitializerSeeds(context);
                    });
                }
            }
        }

        /// <summary>
        /// Initializes the seed.
        /// </summary>
        /// <param name="context">The context.</param>
        protected virtual void InitializeSeed(TContext context)
        {
            // No inevitable initialization needed.
        }

        /// <summary>
        /// Saves the changes between initializer seeds.
        /// </summary>
        /// <param name="context">The context.</param>
        protected virtual void SaveChangesBetweenInitializerSeeds(TContext context)
        {
            // Do nothing, as it is not always needed.
        }
    }
}

After that we need a database initializer deriving from MigrateDatabaseToLatestVersion to be able to get the configuration from the web.config:

using System.Configuration;
using System.Data.Entity;
using System.Reflection;

namespace Common.Migrations
{
    /// <summary>
    /// Database initializer for code first migrations
    /// </summary>
    /// <typeparam name="TContext">The type of the context.</typeparam>
    /// <typeparam name="TConfiguration">The type of the configuration.</typeparam>
    public class MigrationsDatabaseInitializer<TContext, TConfiguration> : MigrateDatabaseToLatestVersion<TContext, TConfiguration>
        where TContext : DbContext
        where TConfiguration : MigrationsConfigurationBase<TContext>, new()
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MigrationsDatabaseInitializer{TContext, TConfiguration}"/> class.
        /// </summary>
        /// <param name="seedBaseData">If true then data inevitable for the working of the system is to be seeded.</param>
        /// <param name="seedDemoData">The seed demo data.</param>
        /// <param name="allowAutomaticMigrations">The allow automatic migrations.</param>
        public MigrationsDatabaseInitializer(string seedBaseData, string seedDemoData, string allowAutomaticMigrations)
        {
            var config = GetConfig();
            config.SeedBaseData = ParseConfigBool(seedBaseData);
            config.SeedDemoData = ParseConfigBool(seedDemoData);
            config.AutomaticMigrationsEnabled = ParseConfigBool(allowAutomaticMigrations);
        }

        /// <summary>
        /// Parses the configuration bool.
        /// </summary>
        /// <param name="configValue">The configuration value.</param>
        /// <returns>The parsed configuration value.</returns>
        private bool ParseConfigBool(string configValue)
        {
            bool result;
            if (!bool.TryParse(configValue, out result))
            {
                throw new ConfigurationErrorsException(
                    string.Format(
                        "There is an error in the migration database initializer configuration for the value: {0}",
                        configValue));
            }

            return result;
        }

        /// <summary>
        /// Gets the configuration.
        /// </summary>
        /// <returns>The configuration</returns>
        private TConfiguration GetConfig()
        {
            var databaseInitializerType = typeof(MigrateDatabaseToLatestVersion<TContext, TConfiguration>);
            var configField = databaseInitializerType.GetField("_config", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
            if (configField != null)
            {
                return (TConfiguration)configField.GetValue(this);
            }

            return null;
        }
    }
}

It is probably unavoidable to see the hack using reflection for getting the config field. I just didn’t find a better way, if you have, please comment on this post.
After all these things now you only need to configure it from the web.config (using the string parameters of the MigrationsDatabaseInitializer) as follows:

  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
    <contexts>
      <context type="DAL.Context.Customer.CustomerContext, DAL">
        <databaseInitializer type="Common.Migrations.MigrationsDatabaseInitializer`2[[DAL.Context.Customer.CustomerContext, DAL], [DAL.Migrations.CustomerContextConfiguration, DAL]], DAL" >
          <parameters>
            <!-- Check the MigrationsDatabaseInitializer constructor parameters to see what the meaning of them is -->
            <parameter value="true" />
            <parameter value="true" />
            <parameter value="false" />
          </parameters>
        </databaseInitializer>
      </context>
    </contexts>
  </entityFramework>

If you wanna know more about configuring Initializers in the entity framework config section you can read it here. Another really interesting topic about migrations and multiple branches can be read here: http://stackoverflow.com/questions/10171658/lose-ef-code-first-migration-when-working-on-different-tfs-branches.
You’ll certainly wan’t to derive from the configuration class for you specific context and want to create initialzers after all, but I’m pretty sure you’ll find it out – if not please write me a comment!

Advertisements

About Tamas Nemeth

Husband and proud father of two daughters in Nürnberg. I'm working as a Senior Software Developer and an enthusiastic Clean-Coder. I spend most of my free time with my family (playing, hiking, etc...). I also play table-tennis and badminton sometimes...
This entry was posted in Technical Interest and tagged , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s