Automatic entity framework migrations in NopCommerce plugins

EntityFramework migrations is a cool feature that allows automatic upgrade of databases for your MVC applications. This tutorial lists steps to implement automatic EntityFramework migrations for your nopCommerce plugin.

Having developed a couple of plugins for nopCommerce, we've always found default nopCommerce plugin documentation missing out on one very important feature. That is Entity Framework Migrations support. While the method written in the documentation works well if future releases of your plugin won't be modifying the database much and you'd be providing upgrade scripts for the same, providing upgrade scripts either is just not good enough for a non-geek store owner. *For would he focus on running his own business or running your upgrade scripts?*Why not use Entity Framework Migrations(EFMigrations), aren't they cool? Exactly. While working on mobSocial, we thought that we'd include EFMigrations, so store owner won't have to rely on upgrade scripts for upgrading to latest versions. After all which applications would get frequent database upgrades than a social network? However the issue with EFMigrations was that store owners would require VisualStudio to upgrade their database to latest migrations to run commands (an even worse thing than running the upgrade scripts IMHO). Why not automate that thing, we thought. Let the store owner replace existing plugin and restart their nopCommerce installation and bam...database should upgrade to latest versions. Sounds interesting? Let's dive in to achieve that. For this tutorial, we'll create a simple nopCommerce plugin in which we’ll create a single entity. But the process is same for any plugin with any number of tables (link to plugin source provided at the end).

Step 1: Setup a nopCommerce plugin project (or use your existing project of course)

This should be fairly straightforward if you have worked with nopCommerce for quite a while. In case you are a beginner, I’d recommend you to read this nopCommerce tutorial and then this, which will give you a fair knowledge about creating a nopCommerce plugin.2016-05-01_163619 For the sake of brevity, we’d be creating only the files necessary to demonstrate the concept. We, therefore, stick to the following project structure.2016-05-02_124240 You don't see any Controllers, Models, Views directory here, because we don't need it. But you can create them for your plugin, for you'll definitely need them. You should use NuGet to install following required packages to your project.

  1. EntityFramework
  2. AutoFac

You should also add references to Nop.Core, Nop.Data and Nop.Services to your plugin. Needless to say, set the output directory of your project build to **....\Presentation\Nop.Web\Plugins\YourPluginNamespaceName\ **(or whatever the path to your plugins directory is).Of course you know that :P

Step 2: Create the entities

We create entities required by our plugin. For simplicity, we create one entity called EfEntity in the folder Domain. So in the file Domains\EfEntity.cs, include the code below.

using Nop.Core;

namespace Nop.Plugin.EfMigrationsDemo.Domains
{
    public class EfEntity : BaseEntity
    {
        public string Name { get; set; }

        public string Type { get; set; }
    }
}

Step 3: Create necessary configuration maps

Next we create the entity map for our entity. So open the file Data\EfEntityMap.cs and include the following code. It's fairly straightforward if you have followed nopCommerce documentation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Nop.Data.Mapping;
using Nop.Plugin.EfMigrationsDemo.Domains;

namespace Nop.Plugin.EfMigrationsDemo.Data
{
    public class EfEntityMap : NopEntityTypeConfiguration<EfEntity>
    {
        public EfEntityMap()
        {
            ToTable("EfEntity");

            Property(x => x.Name);
            Property(x => x.Type);
        }
    }
}

Step 4: Create Database Context

Now that we’ve entities and entity maps setup, we need a database context that’ll create the database structure for us. So open the file Data\EfMigrationsObjectContext.cs and use the following code in it.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
using Nop.Core;
using Nop.Data;
using Nop.Plugin.EfMigrationsDemo.Migrations;

namespace Nop.Plugin.EfMigrationsDemo.Data
{

    public class EfMigrationsObjectContext : DbContext, IDbContext
    {

        public EfMigrationsObjectContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {

        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new EfEntityMap());
            base.OnModelCreating(modelBuilder);
        }

        public void Install()
        {
           //nothing in here
        }

        public void Uninstall()
        {
            try
            {
               // DROP Tables via migrator. we just pass 0 to tell migrator to reset to original version
                var migrator = new DbMigrator(new Configuration());
                migrator.Update("0");

            }
            catch (Exception)
            {
                // ignored
            }

        }


        public IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity
        {
            throw new NotImplementedException();
        }

       public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters)
        {
            throw new NotImplementedException();
        }

        public void Detach(object entity)
        {
            throw new NotImplementedException();
        }

        IList<TEntity> IDbContext.ExecuteStoredProcedureList<TEntity>(string commandText, params object[] parameters)
        {
            throw new NotImplementedException();
        }

        IEnumerable<TElement> IDbContext.SqlQuery<TElement>(string sql, params object[] parameters)
        {
            throw new NotImplementedException();
        }

        public bool ProxyCreationEnabled
        {
            get
            {
                return this.Configuration.ProxyCreationEnabled;
            }
            set
            {
                this.Configuration.ProxyCreationEnabled = value;
            }
        }

        public bool AutoDetectChangesEnabled
        {
            get
            {
                return this.Configuration.AutoDetectChangesEnabled;
            }
            set
            {
                this.Configuration.AutoDetectChangesEnabled = value;
            }
        }
    }

}

Notice that we don't write anything in the Install method. This is because we won't be executing database tables creation queries directly. Instead, we'd ask Entity Framework migrations do that job for us.

Step 5: Setup Migrations

So far we just did what needs to be done for basic setup of every nopCommerce plugin that uses database. Now comes the interesting part. Because we won’t be using default nopCommerce's default ways of installing and uninstalling database tables, we’d be needing migrations in place. For setting up migrations, we’ve manually created a directory called Migrations and created two classes Configuration and MigrationsContextFactory. These two files control the installation and uninstallation of our database tables. In the Migrations\Configuration.cs file, paste the following code.

using System;
using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Nop.Core.Data;
using Nop.Plugin.EfMigrationsDemo.Data;

namespace Nop.Plugin.EfMigrationsDemo.Migrations
{
    internal sealed class Configuration : DbMigrationsConfiguration<EfMigrationsObjectContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;

            AutomaticMigrationDataLossAllowed = true;

            MigrationsAssembly = Assembly.GetExecutingAssembly();
            MigrationsNamespace = "Nop.Plugin.EfMigrationsDemo.Migrations";

            //specify database so that it doesn't throw error during migration. Otherwise, for some reasons it defaults to sqlce and gives error 
            var dataSettingsManager = new DataSettingsManager();
            var dataSettings = dataSettingsManager.LoadSettings();
            TargetDatabase = new DbConnectionInfo(dataSettings.DataConnectionString, "System.Data.SqlClient");

        }
    }
}

And in Migrations\MigrationsContextFactory.cs file, paste the following.

using System.Data.Entity.Infrastructure;
using Nop.Core.Data;
using Nop.Plugin.EfMigrationsDemo.Data;

namespace Nop.Plugin.EfMigrationsDemo.Migrations
{
    public class MigrationsContextFactory : IDbContextFactory<EfMigrationsObjectContext>
    {
        public EfMigrationsObjectContext Create()
        {
            var dataSettingsManager = new DataSettingsManager();
            var dataSettings = dataSettingsManager.LoadSettings();
            return new EfMigrationsObjectContext(dataSettings.DataConnectionString);
        }
    }
}

But what is that? Well, these two files tell EntityFramework about the configuration of the migration that our plugin will be performing.

Configuration.cs

This class simply inherits DbMigrationsConfiguration. That's generic class that accepts a generic parameter for a class that inherits DbContext for our plugin. For us, it is **EfMigrationsObjectContext **class. Note, how we set TargetDatabase in the constructor.

//specify database so that it doesn't throw error during migration. Otherwise, for some reasons it defaults to sqlce and gives error 
var dataSettingsManager = new DataSettingsManager();
var dataSettings = dataSettingsManager.LoadSettings();
TargetDatabase = new DbConnectionInfo(dataSettings.DataConnectionString, "System.Data.SqlClient");

This is required, otherwise for some weird reasons, EntityFramework starts looking for SqlCompact database.

MigrationsContextFactory.cs

We create this database context factory, so that EFMigrations when queries for a database context, instantiates the database context with appropriate database connection string. Because by default, DbContext can be instantiated with a parameterless constructor, we specifically ask our migrator to instantiate our plugin's database context with connection string.

Step 6: Run the migrator

Oh yes, we need to do that as well. Because we don't want the store owner to have to run any upgrade commands or scripts, we'll need migration code to automatically execute when plugin is loaded. There are a couple of ways of doing it. You can run that code using a IStartupTask provided by nopCommerce. You can also call migrator at any place that runs on application startup. For our case, we call this in **DependencyRegistrar.cs. **This is because nopCommerce calls the Register method of IDependencyRegistrar at application startup and so it's guaranteed to be called every time the application restarts. So open DependencyRegistrar.cs file and paste the following code.

using System;
using System.Collections.Generic;
using System.Data.Entity.Migrations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Autofac;
using Nop.Core.Configuration;
using Nop.Core.Data;
using Nop.Core.Infrastructure;
using Nop.Core.Infrastructure.DependencyManagement;
using Nop.Core.Plugins;
using Nop.Data;
using Nop.Plugin.EfMigrationsDemo.Data;
using Nop.Plugin.EfMigrationsDemo.Migrations;

namespace Nop.Plugin.EfMigrationsDemo
{
    public class DependencyRegistrar : IDependencyRegistrar
    {
        private const string ContextName = "nop_object_context_efmigrations";

        public void Register(ContainerBuilder builder, ITypeFinder typeFinder, NopConfig config)
        {
            //Load custom data settings
            var dataSettingsManager = new DataSettingsManager();
            var dataSettings = dataSettingsManager.LoadSettings();

            //Register custom object context
            builder.Register<IDbContext>(c => RegisterIDbContext(c, dataSettings)).Named<IDbContext>(ContextName).InstancePerRequest();
            builder.Register(c => RegisterIDbContext(c, dataSettings)).InstancePerRequest();

            var pluginFinderTypes = typeFinder.FindClassesOfType<IPluginFinder>();

            var isInstalled = false;

            foreach (var pluginFinderType in pluginFinderTypes)
            {
                var pluginFinder = Activator.CreateInstance(pluginFinderType) as IPluginFinder;
                var pluginDescriptor = pluginFinder.GetPluginDescriptorBySystemName("EfMigrations.Demo");

                if (pluginDescriptor != null && pluginDescriptor.Installed)
                {
                    isInstalled = true;
                    break;
                }
            }

            if (isInstalled)
            {
                //db migrations, lets update if needed
                var migrator = new DbMigrator(new Configuration());
                migrator.Update();
            }

        }

        /// <summary>
        /// Registers the I db context.
        /// </summary>
        /// <param name="componentContext">The component context.</param>
        /// <param name="dataSettings">The data settings.</param>
        /// <returns></returns>
        private EfMigrationsObjectContext RegisterIDbContext(IComponentContext componentContext, DataSettings dataSettings)
        {
            string dataConnectionStrings;

            if (dataSettings != null && dataSettings.IsValid())
                dataConnectionStrings = dataSettings.DataConnectionString;
            else
                dataConnectionStrings = componentContext.Resolve<DataSettings>().DataConnectionString;

            return new EfMigrationsObjectContext(dataConnectionStrings);

        }

        public int Order { get; }
    }
}

Notice the code in bold. We check if the plugin is installed and then instantiate and run our migrator. The migrator.Update() method updates the database with all the pending changes.

Step 7: Build and Install the plugin

Now build the plugin and if you are getting any errors (are you? :o), then something must be wrong with your keyboard or your code :D, Please recheck and try again. Once the project builds successfully, login to your nopCommerce admin and install the plugin.2016-05-02_122724

Tip: If you can’t see the plugin, don’t hesitate to click on Reload Plugins button.

Now check the database and you should see your entity tables created.2016-05-02_122838 Because we’ve used migrations to install the plugin, a __MigrationHistory table will also be created (if it doesn’t exist of course). Open this table to see an entry with the migration namespace that we setup above in step 5.2016-05-02_122944

Step 8: Check if modifications to entities are correctly applied

Now that we have installed the plugin (or your client has installed it on his server), we need to check if any changes made to entities will be automatically applied to database or not. We’ll add two columns to our EfEntity.

using System;
using Nop.Core;

namespace Nop.Plugin.EfMigrationsDemo.Domains
{
    public class EfEntity : BaseEntity
    {
        public string Name { get; set; }

        public string Type { get; set; }

        public DateTime DateCreated { get; set; }

        public DateTime DateUpdated { get; set; }
        
    }
}

And to its EfEntityMap as well.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Nop.Data.Mapping;
using Nop.Plugin.EfMigrationsDemo.Domains;

namespace Nop.Plugin.EfMigrationsDemo.Data
{
    public class EfEntityMap : NopEntityTypeConfiguration<EfEntity>
    {
        public EfEntityMap()
        {
            ToTable("EfEntity");

            Property(x => x.Name);
            Property(x => x.Type);
            Property(x => x.DateCreated).HasColumnType("datetime2");
            Property(x => x.DateUpdated).HasColumnType("datetime2");
        }
    }
}

Now rebuild the plugin and restart the nopCommerce application. This is required because we want all new dlls to be loaded by nopCommerce. If you have a successful build, go and check your database table again and you should see the updated database structure.2016-05-02_123610 Also if you see __MigrationHistory table, you’d see a new row with the updated migrations data.2016-05-02_123628 Awesome, isn't it? So now you can keep your worries of database upgrades aside and create automatically database up-gradable plugins for your nopCommerce.

Wait, what if I uninstall the plugin?

Of course when you uninstall the plugin, all the tables of your plugin should be removed isn't it? That's what happens right now. If you see the code of ** Data\EfMigrationsObjectContext.cs **in step 4 above, you'll see the following uninstall method.

public void Uninstall()
{
    try
    {
      // DROP Tables via migrator. we just pass 0 to tell migrator to reset to original version
      var migrator = new DbMigrator(new Configuration());
      migrator.Update("0");

    }
    catch (Exception)
    {
      // ignored
    }

}

Here, we instantiate a migrator and call the Update() method with parameter "0". This tells EntityFramework migrator to revert to the initial version of the database associated with particular database context. Cool isn't it? [download id="74"]

Get updates directly in your inbox

My collection of ideas, thoughts, stories or anything straight from my heart.

Subscribe to Anshul Sojatia

Please provide your email address to subscribe or Login with Twitter to engage in conversation.