Extending And Customizing Code First Models – Part 2 of 2

Posted on February 15, 2013. Filed under: Entity Framework, Visual Studio | Tags: , , , , |

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…

About these ads

Make a Comment

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

6 Responses to “Extending And Customizing Code First Models – Part 2 of 2”

RSS Feed for RoMiller.com Comments RSS Feed

I am surprised you omitted any discussion of performance. Startup costs? Runtime costs? Various sizes where this would be beneficial or not?

Like the post.. Like all of the ones I have read. One error I see here on the Down
public override void Down()
{
DropForeignKey(“dbo.MyBizCustomers”, “CustomerId”, “dbo.Customers”);
DropTable(“dbo.MyBizCustomers”);
}
should be
public override void Down()
{
DropForeignKey(“mybiz.Customers”, “CustomerId”, “dbo.Customers”);
DropTable(“mybiz.Customers”);
}

and that brings me to a question. I am creating something similar, With my project I want to control the table name, and I want to ensure their tables are in the correct schema. Is there any way of doing this other then adding my own createtable function in my extension class? something that would globally check the table/schema naming convention. this way it gets verified on the create model as well as during migration.

any thoughts on this would be great. thanks.

Hi,

Does this still apply to EF6 RTM? Reason I ask is I’ve implemented the above, but when my migrations run, I get a bunch of errors;

System.Data.DataException: An exception occurred while initializing the database. See the InnerException for details. —> System.Data.Entity.Core.MappingException: Schema specified is not valid. Errors:
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..
(0,0) : error 2025: XML Schema validation failed for mapping schema. Schema Error Information : The ‘http://schemas.microsoft.com/ado/2012/10/edm/migrations:IsSystem’ attribute is not declared..

Any help would be appreciated.

Cheers
Tony

In fact, I’ve just updated the EF nuget package in the sample code and get the same issue. Any help on how to solve this issue would be most appreciated.


Where's The Comment Form?

    About

    Rowan works as a Program Manager for the ADO.NET Entity Framework team at Microsoft. He speaks at technical conferences and blogs at romiller.com. Rowan lives in Seattle, Washington with his wife Athalie. Prior to moving to the US he resided in the small state of Tasmania in Australia. Outside of technology Rowan's passions include snowboarding, mountain biking, horse riding, rock climbing and pretty much anything else that involves being active. The primary focus of his life, however, is to follow Jesus.

    RSS

    Subscribe Via RSS

    • Subscribe with Bloglines
    • Add your feed to Newsburst from CNET News.com
    • Subscribe in Google Reader
    • Add to My Yahoo!
    • Subscribe in NewsGator Online
    • The latest comments to all posts in RSS

    Meta

Liked it here?
Why not try sites on the blogroll...

Follow

Get every new post delivered to your Inbox.

Join 159 other followers

%d bloggers like this: