Extending And Customizing Code First Models – Part 2 of 2

Here is the scenario, your company ships a library or application that contains a Code First model for accessing the database. Your customers want to extend this model to include extra types/properties to meet their specific business needs. These types/properties will be stored in additional tables/columns in the application database.

In Part 1 we looked at how to extend the model, but required the corresponding changes to be manually applied to the database. In the second (and final) part of this series we’ll take a look at how migrations can be used by the team creating the core model, and the team extending the model.

Complete source code is provided for download at the end of this post.

Make sure you have read and understand Part 1 of this series before tackling this post.


Using Migrations in the Core Model

The good news is that the developers of Customer Stalker don’t need to do anything special to use Code First Migrations. They can just use the standard workflow – as shown in this walkthrough.

For example, the Customer Stalker team have added a few properties to track the address of each customer.

using System.Collections.Generic;

namespace CustomerStalker.Core
{
  public class Customer
  {
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string Zip { get; set; }

    public List Complaints { get; set; }
  }
}

They’ve used Migrations to apply these changes to the database, here is the resulting Migrations folder in their project.

Migrations


Allowing Customers Extending the Model to Supply Migrations

To use the code from this section you are going to need EF6 because we are making use of the new Multiple Contexts per Database feature.

The Customer Stalker team  is going to allow the company extending their model to provide some hand coded migrations. The first time the context is used in an application, they’ll ensure all custom migrations have been applied to the database. They’ll also make sure that any migrations the Customer Stalker team has created have also been applied.

Introducing the Extension Points

Customer Stalker start with a base class that custom migrations will derive from. This derives from DbMigration which allows companies extending the model to provide an Up and Down method.

It also implements IMigrationMetadata – something that is usually taken care of by the Migrations power shell commands. This interface is used by migrations to get metadata about ordering and the state of the model when a migration was generated (used to scaffold the next migration).

For ordering, authors of custom migrations provide a simple sequence number. This is then converted into a ordering number that looks somewhat like the numbers Migrations generates.

Unfortunately Migrations requires you to provide a target model – a limitation we’re planning to remove – but for the moment we’ll just put in the string for an empty model. Please don’t dwell on this code… best to move on before your eyes bleed too much 🙂

using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;

namespace CustomerStalker.Core.Migrations
{
  public abstract class CustomizingMigration : DbMigration, IMigrationMetadata
  {
    private const string _emptyModel =
      "H4sIAAAAAAAEAL1Y23LbNhB970z/gYOn9iGCJEdJ66GSsWW78bSyO6aT1wxErmRMcGEJ0BX7a33oJ/UXuryKF11o2e6bCKx2z1lwD47079//uB/XUjiPEBmu1ZSMBkPigPJ1wNVqSmK7fPMT+fjh++/cy0CunS9l3Ekah99UZkoerA1PKTX+A0hmBpL7kTZ6aQe+lpQFmo6Hw5/paEQBUxDM5TjuXawsl5A94ONMKx9CGzMx1wEIU6zjjpdldW6YBBMyH6Zknpzzv4hzJjjD4h6IJXHCt6efDXg20mrlhcxyJu6TEHB/yYSBAupp+LYv2uE4RUuZUtpiOq2OYksqHsjkEhnbBJlaxhVEGaUpORdMfUsXYW3r4dUXPLBF6CdurI4S4uQbKcGc/6DYudN/Yiven14bLzEW5JTYKN6Qf98L/GhMR8OMvOSrKONuiEPrTGiLSpdliq2JOsf2bhe2d8djazbtV0i2lGnEYNTvkQ4hsskdLAuY8zLhdbAVJz2cojhFRNAjg0sxrrlSZtyGqDhsG+FYEmfO1r+BWtmHKRlPJsS54msIypXijf+sOE4xlNVvYiHYQkC1fxhhC0+d3k44k9H4f4KTCUWJ5Jwrlk5GDQl+3I7k+aXxMYh9W4jhnm6cvE4zygFM626UkuZSWUoq3aGp7pyFIQKtaWyx4ni5wM7eeE9XO5nnoL7ZJ3pVJZQEtoLWbvqOBXDFI2MvmGULlvZiFshOWB/RLEu1tPP1JaghgiWK9HOOJK89SAkO8rhBNemmVPLBftVs1dsc4RV2VYKyWYOh6sUhFe7kyy5eJli0T5NmWsRSHSefu0vUZaZe4UnqupdDLhwN9PnSs9K2RaGev73Xq5BLW4fafsto5zVr3TDtWdh3h7dDquqVnrR0wy1m+LBh6wx1HkIcbMsjD9KBrg+F94eYCY58NwFzpvgSjL3X3wDNZzqFLQN4hDmjxgSih0PralIvm5aN2l6nlrdhSoKFxjskvwC+fq0m6XUl66Uc3WQXtsmLOrpOmac6ui7Opzq6QxmOcnTqkUX+A4u6nq5rCg4COGjYtlbLLNvzqzX8GNZZZJbsB8nWP75E+u2eayuhkyP49LNUXclzaf2XrHsBhq8285P+rlXgp0e+SVrGXKulLkmietTBlCHtFoNlAYrQWWT5kvkWt30wJnOdX5iIMeRSLiC4VrexDWN7ZgzIhUjqVF26v37mG5uY3dswm8uXoIAwOVKAW3UecxFUuK/yU6I9UqR6/AvgejZK6Lox3SqpMt1o1TNR0b4LCEEFeN/cgwwFJjO3ymOPsBvb4R42O+ZecIaTL02RY/P99I8Vmv6z8uE/KRLfnIsRAAA=";

    string IMigrationMetadata.Id
    {
      get { return string.Format("{0}_{1}", SequenceNo.ToString().PadLeft(15, '0'), GetType().Name); }
    }

    string IMigrationMetadata.Source
    {
      get { return null; }
    }

    string IMigrationMetadata.Target
    {
      get { return _emptyModel; }
    }

    public abstract int SequenceNo { get; }
  }
}

Next, the Customer Stalker team is adding some hooks to the ModelCustomizer they created in Part 1. These hooks allow the company extending the model to provide an assembly and namespace that contains their custom migrations.

They’re using DbMigrator to run these migrations from code – for more info on running migrations from code see Running & Scripting Migrations from Code.

They’re also using DbContextInfo to find the provider and connection string for the core context. This allows you to get such info about a context – taking into account all the conventions, connection strings in config files, etc. – without creating an instance of it.

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

namespace CustomerStalker.Core
{
  public class ModelCustomizer
  {

    // CODE FROM PART 1 EXCLUDED FOR BREVITY

    private static Assembly _migrationsAssembly;
    private static string _migrationsNamespace;

    public static void RegisterMigrations(Assembly assembly, string migrationsNamespace)
    {
      _migrationsAssembly = assembly;
      _migrationsNamespace = migrationsNamespace;
    }

    internal static void ApplyMigrations()
    {
      if (_migrationsAssembly != null && _migrationsNamespace != null)
      {
        var contextInfo = new DbContextInfo(typeof(CustomerStalkerContext));

        var extendedMigrator = new DbMigrator(new DbMigrationsConfiguration
        {
          ContextKey = "CustomerStalker.Core.Migrations.Extensions",
          ContextType = typeof(BlankContext),
          MigrationsAssembly = _migrationsAssembly,
          MigrationsNamespace = _migrationsNamespace,
          TargetDatabase = new DbConnectionInfo(contextInfo.ConnectionString, contextInfo.ConnectionProviderName)
        });

        extendedMigrator.Update();
      }
    }

    private class BlankContext : DbContext
    {
    }
  }
}

Now it’s time to create database initializer that will apply all the core migrations, and any custom ones, when CustomerStalkerContext is used for the first time in an AppDomain.

You’ll notice they are catching an AutomaticMigrationsDisabledException that may be thrown when applying the core migrations. If a company extends a model then it will no longer match the model when the last migration was created by the Customer Stalker team. This is fine because they assume the customer has written their own migrations to handle the required changes.

using CustomerStalker.Core.Migrations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;

namespace CustomerStalker.Core
{
  class CustomerStalkerInitializer : IDatabaseInitializer
  {
    public void InitializeDatabase(CustomerStalkerContext context)
    {
      var coreMigrator = new DbMigrator(new Configuration());
      try
      {
        coreMigrator.Update();
      }
      // If the model has been extended it won't match the model recorded in the last migration
      catch (AutomaticMigrationsDisabledException) { }

      ModelCustomizer.ApplyMigrations();
    }
  }
}

Finally, they register this initializer in a static constructor for CustomerStalkerContext, ensuring it is always set before attempting to use the context in the application.

using System.Data.Entity;

namespace CustomerStalker.Core
{
  internal class CustomerStalkerContext : DbContext
  {
    static CustomerStalkerContext()
    {
      Database.SetInitializer(new CustomerStalkerInitializer());
    }

    // EXISTING CODE EXCLUDED FOR BREVITY
  }
}

Using the Extension Points

At the end of Part 1, MyBiz had extended the Customer type. These extensions required a new mybiz.Customers table with a column for the new IsVIP property and a foreign key back to the core dbo.Customers table.

DatabaseThirdModification

Unfortunately they had to manually make these changes to the database. But now that can just add a custom migration.

using CustomerStalker.Core.Migrations;

namespace MyBiz.Migrations
{
  class AddMyBizCustomer : CustomizingMigration
  {
    public override void Up()
    {
      CreateTable("mybiz.Customers",
          c => new
          {
            CustomerId = c.Int(nullable: false),
            IsVIP = c.Boolean()
          })
          .PrimaryKey(t => t.CustomerId);

      AddForeignKey("mybiz.Customers", "CustomerId", "dbo.Customers", principalColumn: "CustomerId");
    }

    public override void Down()
    {
      DropForeignKey("dbo.MyBizCustomers", "CustomerId", "dbo.Customers");
      DropTable("dbo.MyBizCustomers");
    }

    public override int SequenceNo { get { return 1; } }
  }
}

Now they can update their test code from Part 1 to register their migrations.

ModelCustomizer.RegisterModelCustomization(
    mb =>
    {
      mb.Entity().Property(c => c.FirstName).HasMaxLength(400);
      mb.Entity().Property(c => c.LastName).HasMaxLength(400);

      mb.Entity().ToTable("mybiz.Customers");
    });

ModelCustomizer.RegisterTypeSubstitution<Customer, MyBizCustomer>();

ModelCustomizer.RegisterMigrations(typeof(AddMyBizCustomer).Assembly, typeof(AddMyBizCustomer).Namespace);

var service = new CustomerService();
var customer = new MyBizCustomer { FirstName = "Jane", LastName = "Doe", IsVIP = true };
service.AddCustomer(customer);
service.QuickAddCustomer("John", "Doe");

After running their test code, their changes will be automatically applied to the database. Looking in the __MigrationsHistory table they can see Migrations keeping track of which migrations (both core and custom) have been applied to the database.

MigrationHistory


Source Code

As promised… you can get the complete Visual Studio 2012 solution here…