Extending and Customizing Code First Models – Part 1 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 this post I’ll walk through an example using a company that delivers a CRM product called Customer Stalker. The company provides a library for Customer Stalker that includes a Code First model with some hooks to allow each business that uses Customer Stalker to customize the model. We’ll see how a company called MyBiz uses these hooks to extend the model. MyBiz will use inheritance to add extra properties to existing classes/tables that are part of the Customer Stalker model.

I’m just going to show one possible way you can combine the building blocks to solve this problem. There are plenty of different ways to architect a solution and you can take the principles I show to come up with a way that suits you. This is by no means production quality or well architected code, just a rough sample… take it for what it is 🙂

We’ll build on the example as we go, adding more and more functionality. Here are the main stages I’ll cover:

  • Part 1 (this post)
    In Part 1 we’ll assume that Customer Stalker does *not* use Code First Migrations to create/upgrade the database and MyBiz will manually add their additional tables/columns to the database. In Part 2 we’ll enable migrations.

    • The Code Before Enabling Customization – Before allowing any customization of the model, we’ll take a quick look at code from the Customer Stalker library and code written by MyBiz.
    • Basic Extension – We’ll start with the very basics that allow MyBiz code to provide instances of a custom derived type to the Customer Stalker library.
    • Substituting Custom Derived Types – Once MyBiz has added a derived type, they probably want the Customer Stalker library to always create their derived type, rather than the original base type. This means that all data in the database will have the custom properties – not just the instances created by MyBiz code.
    • Avoiding Modification of the Core Tables – Next, we’ll look at how MyBiz can add their customizations in a way that does not alter the tables that are created by the Customer Stalker library.
    • Customizing the Core Tables – Finally, we’ll show how MyBiz could customize tables/columns that are part of the core Customer Stalker model.
  • Part 2 (coming soon)
    • Adding Code First Migrations – We’ll allow Customer Stalker to use migrations to create/upgrade the database. We’ll also provide some hooks so that MyBiz can add their own migrations to apply their customizations.
    • Source Code Download – I’ll provide a Visual Studio solution that shows the complete Customer Stalker and MyBiz code bases.

The Code Before Enabling Customization

The Customer Stalker Code

Before we add any hooks here is the model that the Customer Stalker library is using. They have a couple of domain classes to track Customers and any Complaints they make.

using System;
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 List<Complaint> Complaints { get; set; }
  }

  public class Complaint
  {
    public int ComplaintId { get; set; }
    public DateTime DateRecieved { get; set; }
    public string Details { get; set; }
    public bool IsResovled { get; set; }

    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
  }
}

Customer Stalker uses a derived DbContext to turn these classes in a Code First model. They are using the Fluent API to configure the model – in this case just the maximum length of the name properties.

using System.Data.Entity;

namespace CustomerStalker.Core
{
  internal class CustomerStalkerContext : DbContext
  {
    public DbSet Customers { get; set; }
    public DbSet Complaints { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
      modelBuilder.Entity()
        .Property(c => c.FirstName)
        .HasMaxLength(200);

      modelBuilder.Entity()
        .Property(c => c.LastName)
        .HasMaxLength(200);
    }
  }
}

You may have noticed that that the context is internal. The Customer Stalker library exposes an API surface that restricts how businesses such as MyBiz interact with their library. Here is one of the services they expose.

namespace CustomerStalker.Core
{
  public class CustomerService
  {
    public void AddCustomer(Customer customer)
    {
      using (var db = new CustomerStalkerContext())
      {
        db.Customers.Add(customer);
        db.SaveChanges();
      }
    }

    public Customer QuickAddCustomer(string firstName, string lastName)
    {
      var customer = new Customer();
      customer.FirstName = firstName;
      customer.LastName = lastName;

      using (var db = new CustomerStalkerContext())
      {
        db.Customers.Add(customer);
        db.SaveChanges();
      }

      return customer;
    }
  }
}

MyBiz Code

MyBiz is getting familiar with the Customer Stalker library and has written the following code to test it out.

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

Here is what the Customer Stalker database would look like at MyBiz – this is the default Customer Stalker database with no customization.

DatabaseNoModifications


Basic Extension

Introducing the Extension Points

The Customer Stalker team is introducing a new ModelCustomizer class into their library. At the moment they just allow businesses to register some custom logic to further configure the Customer Stalker Code First model. They’ve also included an internal method to apply this customization to a model.

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

namespace CustomerStalker.Core
{
  public class ModelCustomizer
  {
    private static Action<DbModelBuilder> _modelCustomization;

    public static void RegisterModelCustomization(Action<DbModelBuilder> modelCustomization)
    {
      _modelCustomization = modelCustomization;
    }

    internal static void ApplyCustomization(DbModelBuilder modelBuilder)
    {
      if (_modelCustomization != null)
      {
        _modelCustomization(modelBuilder);
      }
    }
  }
}

They’ve also updated their context to apply any customization after they’ve finished configuring their model in the OnModelCreating method.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Customer>()
    .Property(c => c.FirstName)
    .HasMaxLength(200);

  modelBuilder.Entity<Customer>()
    .Property(c => c.LastName)
    .HasMaxLength(200);

  ModelCustomizer.ApplyCustomization(modelBuilder);
}

Using the Extension Points

Let’s see how MyBiz can use these extension points to store some extra information about Customers.

First up, MyBiz defines a domain class that derives from an existing class and adds any properties that MyBiz wants to track.

using CustomerStalker.Core;

namespace MyBiz.ExtendedModel
{
  public class MyBizCustomer : Customer
  {
    public bool IsVIP { get; set; }
  }
}

MyBiz also needs to modify their Customer Stalker database to include the new column – we’ll look at using Code First Migrations to do this in Part 2 (coming soon).

By default Code First uses the TPH pattern to store an inheritance hierarchy, hence the new Discriminator column in the Customers table. This allows Entity Framework to differentiate between Customers and MyBizCustomers. Later in this post we’ll take a look at how to introduce a derived type without modifying the existing table.

DatabaseFirstModification

Now MyBiz can use the hooks provided by Customer Stalker to add their new type to the model. Then they can insert MyBizCustomers using the Customer Stalker library.

ModelCustomizer.RegisterModelCustomization(
    mb =>
    {
      mb.Entity<MyBizCustomer>();
    });

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

When MyBiz runs the above code, they’ll see the following data in the Customers table in their database.

DataFirstModification

As expected, MyBiz can use the hooks to register their own derived type with extended properties and persist it to the database.


Substituting Custom Derived Types

One limitation of our solution thus far is that QuickAddCustomer doesn’t reason about the derived MyBizCustomer type and is just creating an instance of the base Customer type. This may not be ideal, because MyBiz may want all customers to have their extended properties, including those created internally by Customer Stalker.

Introducing the Extension Points

The Customer Stalker team is adding a RegisterTypeSubstitution method which allows a business to register a derived type to be used in place of a type from the base Customer Stalker model. There is also a Create method that internal Customer Stalker code can use to create types based on what substitutions have been applied.

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

namespace CustomerStalker.Core
{
  public class ModelCustomizer
  {
    private static Action<DbModelBuilder> _modelCustomization;
    private static Dictionary<Type, Type> _replacedTypes = new Dictionary<Type, Type>();

    public static void RegisterModelCustomization(Action<DbModelBuilder> modelCustomization)
    {
      _modelCustomization = modelCustomization;
    }

    internal static void ApplyCustomization(DbModelBuilder modelBuilder)
    {
      if (_modelCustomization != null)
      {
        _modelCustomization(modelBuilder);
      }
    }

    public static void RegisterTypeSubstitution<TOld, TNew>()
      where TOld : class
      where TNew : TOld
    {
      _replacedTypes.Add(typeof(TOld), typeof(TNew));
    }

    internal static TType Create<TType>()
    {
      if (_replacedTypes.ContainsKey(typeof(TType)))
      {
        return (TType)Activator.CreateInstance(_replacedTypes[typeof(TType)]);
      }

      return Activator.CreateInstance<TType>();
    }
  }
}

The QuickAddCustomer method is then updated to take type substitution into account.

public Customer QuickAddCustomer(string firstName, string lastName)
{
  var customer = ModelCustomizer.Create<Customer>();
  customer.FirstName = firstName;
  customer.LastName = lastName;

  using (var db = new CustomerStalkerContext())
  {
    db.Customers.Add(customer);
    db.SaveChanges();
  }

  return customer;
}

Using the Extension Points

MyBiz adds in a call to RegisterTypeSubstitution to their test code to register the MyBizCustomer to be used in place of Customer.

ModelCustomizer.RegisterModelCustomization(
    mb =>
    {
      mb.Entity<MyBizCustomer>();
    });

ModelCustomizer.RegisterTypeSubstitution<Customer, MyBizCustomer>();

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

And now Customer Stalker is able to create instances of the custom derived type so that all instances include the extended properties.

DataSecondModification


Avoiding Modification of the Core Tables

So far, the extra properties defined on a derived type have been stored in the core table that is created by Customer Stalker. In some scenarios this may not be acceptable. For example, Customer Stalker may not provide MyBiz with support for their product if they modify the core tables.

Fortunately Code First supports the TPT inheritance mapping pattern, which allows us to store the extended properties from a derived type in a separate table. We don’t need any additional hooks in the Customer Stalker library, just some different configuration when MyBiz customizes the model.

This new configuration will put all the extended properties of MyBizCustomer in a Customers table in the mybiz schema.

ModelCustomizer.RegisterModelCustomization(
    mb =>
    {
      mb.Entity<MyBizCustomer>().ToTable("mybiz.Customers");
    });

Here is what the modified database should look like. Notice that the extended tables primary key is also a foreign key that points to the row in the core table.

DatabaseThirdModification

When MyBiz runs their test code here is what gets inserted into the database.

DataThirdModification


Customizing the Core Tables

In addition to adding and configuring new types, the model customization hook allows MyBiz to customize the tables/columns of the core Customer Stalker model.

For example, MyBiz may operate in a part of the world where people have very long names. The maximum length of 200 that Customer Stalker configures for first and last name may not be enough. The following code allows MyBiz to override this with a maximum length of 400.

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

      mb.Entity<MyBizCustomer>().ToTable("mybiz.Customers");
    });

Remember that MyBiz would also need to make this change to the database schema. In Part 2 (coming soon) we’ll look at allowing MyBiz to make this change with Code First Migrations.

Leave a comment