When defining models for use with the Entity Framework (EF), developers often include all of the classes to be used throughout the entire application. This might be a result of creating a new Database First model in the EF Designer and selecting all available tables and views from the database. For those of you using Code First to define your model, it might mean creating DbSet properties in a single DbContext for all of your classes or even unknowingly including classes that are related to those you’ve targeted.

When you’re working with a large model and a large application, there are numerous benefits to designing smaller, more-compact models that are targeted to specific application tasks, rather than having a single model for the entire solution. In this column, I’ll introduce you to a concept from domain-driven design (DDD)—Bounded Context—and show you how to apply it to build a targeted model with EF, focusing on doing this with the greater flexibility of the EF Code First feature. If you’re new to DDD, this a great approach to learn even if you aren’t committing fully to DDD. And if you’re already using DDD, you’ll benefit by seeing how you can use EF while following DDD practices.

Domain-Driven Design and Bounded Context

DDD is a fairly large topic that embraces a holistic view of software design. Paul Rayner, who teaches DDD workshops for Domain Language (DomainLanguage.com), puts it succinctly:

“DDD advocates pragmatic, holistic and continuous software design: collaborating with domain experts to embed rich domain models in the software—models that help solve important, complex business problems.”

DDD includes numerous software design patterns, one of which—Bounded Context—lends itself perfectly to working with EF. Bounded Context focuses on developing small models that target supporting specific operations in your business domain. In his book, “Domain-Driven Design” (Addison Wesley, 2003), Eric Evans explains that Bounded Context “delimits the applicability of a particular model. Bounding contexts gives team members a clear and shared understanding of what has to be consistent and what can develop independently.”

Smaller models provide many benefits, allowing teams to define clear boundaries relating to design and development responsibilities. They also lead to better maintainability—because a context has a smaller surface area, you have fewer side effects to worry about when making modifications. Furthermore, there’s a performance benefit when EF creates in-memory metadata for a model when it’s first loaded into memory.

Because I’m building bounded contexts with EF DbContext, I’ve referred to my DbContexts as “bounded DbContexts.” However, the two are not actually equivalent: DbContext is a class implementation whereas Bounded Context encompasses the larger concept within the complete design process. I’ll therefore refer to my DBContexts as “constrained” or “focused.”

Comparing a Typical EF DbContext to Bounded Context

While DDD is most commonly applied to large application development in complex business domains, smaller apps can also benefit from many of its lessons. For the sake of this explanation, I’ll focus on an application targeted to a specific subdomain: tracking sales and marketing for a company. Objects involved in this application might range from customers, orders and line items, to products, marketing, salespeople and even employees. Typically a DbContext would be defined to contain DbSet properties for every class in the solution that needs to be persisted to the database, as shown in Figure 1.

Figure 1 Typical DbContext Containing All Domain Classes in the Solution

    public class CompanyContext : DbContext
    {
        public DbSet Customers { get; set; }
        public DbSet Employees { get; set; }
        public DbSet SalaryHistories { get; set; }
        public DbSet Orders { get; set; }
        public DbSet LineItems { get; set; }
        public DbSet Products { get; set; }
        public DbSet Shipments { get; set; }
        public DbSet Shippers { get; set; }
        public DbSet ShippingAddresses { get; set; }
        public DbSet Payments { get; set; }
        public DbSet Categories { get; set; }
        public DbSet Promotions { get; set; }
        public DbSet Returns { get; set; }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Config specifies a 1:0..1 relationship between Customer and ShippingAddress
            modelBuilder.Configurations.Add(new ShippingAddressMap());
        }
    }

Imagine if this were a much more far-reaching application with hundreds of classes.And you might also have Fluent API configurations for some of those classes. That makes for an awful lot of code to wade through and manage in a single class. With such a large application, development might be divided up among teams. With this single company-wide DbContext, every team would need a subset of the code base that extends beyond their responsibilities. And any team’s changes to this context could affect another team’s work.

There are interesting questions you could ask yourself about this unfocused, overarching DbContext. For example, in the area of the application that targets the marketing department, do users have any need to work with employee salary history data? Does the shipping department need to access the same level of detail about a customer as a customer service agent? Would someone in the shipping department need to edit a customer record? For most common scenarios, the answer to these questions would generally be no, and this might help you see why it could make sense to have several DbContexts that manage smaller sets of domain objects.

A Focused DbContext for the Shipping Department

As DDD recommends working with smaller, more-focused models with well-defined context boundaries, let’s narrow the scope of this DbContext to target shipping department functions and only those classes needed to perform the relevant tasks.Therefore, you can remove some DbSet properties from the DbContext, leaving only those you’ll need to support business capabilities related to shipping. I’ve taken out Returns, Promotions, Categories, Payments, Employees and SalaryHistories:

    public class ShippingDeptContext : DbContext
    {
        public DbSet Shipments { get; set; }
        public DbSet Shippers { get; set; }
        public DbSet Customers { get; set; }
        public DbSet ShippingAddresses { get; set; }
        public DbSet Order { get; set; }
        public DbSet LineItems { get; set; }
        public DbSet Products { get; set; }
    }

EF Code First uses the details of ShippingContext to infer the model. Figure 2 shows a visualization of the model that will be created from this class, which I generated using the Entity Framework Power Tools beta 2. Now, let’s start fine-tuning the model.

Shrink EF Models with DDD Bounded Contexts-程序旅途

Figure 2 Visualized Model from First Pass at the ShippingContext

Tuning the DbContext and Creating More-Targeted Classes

There are still more classes involved in the model than I specified for shipping. By convention, Code First includes all classes that are reachable by other classes in the model. That’s why Category and Payment showed up even though I removed their DbSet properties. So I’ll tell the DbContext to ignore Category and Payment:

  protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Ignore();
  modelBuilder.Ignore();
  modelBuilder.Configurations.Add(new ShippingAddressMap());
}

This ensures that Category and Payment don’t get pulled into the model just because they’re related to Product and Order.

You can refine this DbContext class even more without affecting the resulting model.With these DbSet properties, it’s possible to explicitly query for each of these seven sets of data in your app. But if you think about the classes and their relationships, you might conclude that in this context it will never be necessary to query for the Shipping­Address directly—it can always be retrieved along with the Customer data.Even with no ShippingAddresses DbSet, you can rely on the same convention that automatically pulled in Category and Payment to pull ShippingAddress into the model because of its relationship to Customer. So you can remove the ShippingAddresses property without losing the database mappings to ShippingAddress. You might be able to justify removing others, but let’s focus on just this one:

    public class ShippingContext : DbContext
    {
        public DbSet Shipments { get; set; }
        public DbSet Shippers { get; set; }
        public DbSet Customers { get; set; }
        // Public DbSet ShippingAddresses { get; set; }
        public DbSet Order { get; set; }
        public DbSet LineItems { get; set; }
        public DbSet Products { get; set; }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        { 
            ...
        }
    }

Within the context of processing shipments, I don’t really need a full Customer object, a full Order object or a full LineItem object. I need only the Product to be shipped, the quantity (from LineItem), the Customer’s name and ShippingAddress and any notes that might be attached to the Customer or to the Order. I’ll have my DBA create a view that will return unshipped items—those with ShipmentId=0 or null. In the meantime, I can define a streamlined class that will map to that view with the relevant properties I anticipate needing:

    [Table("ItemsToBeShipped")]
    public class ItemToBeShipped
    {
        [Key]
        public int LineItemId { get; set; }
        public int OrderId { get; set; }
        public int ProductId { get; set; }
        public int OrderQty { get; private set; }
        public OrderShippingDetail OrderShippingDetails { get; set; }
    }

The logic of processing shipments requires querying for the ItemToBeShipped and then getting whatever Order details I might need along with the Customer and ShippingAddress. I could reduce my DbContext definition to allow me to query a graph starting with this new type and including the Order, Customer and Shipping­Address. However, because I know EF would achieve this with a SQL query designed to flatten the results and return repeated Order, Customer and Shipping­Address data along with each line item, I’ll let the programmer query for an order and bring back a graph with the Customer and ShippingAddress. But again, I don’t need all of the columns from the Order table, so I’ll create a class that’s better targeted for the shipping department, including information that could be printed on a shipping manifest. The class is the OrderShippingDetail class shown in Figure 3.

Figure 3 The OrderShippingDetail Class

    [Table("Orders")]
    public class OrderShippingDetail
    {
        [Key]
        public int OrderId { get; set; }
        public DateTime OrderDate { get; set; }
        public Nullable DueDate { get; set; }
        public string SalesOrderNumber { get; set; }
        public string PurchaseOrderNumber { get; set; }
        public Customer Customer { get; set; }
        public int CustomerId { get; set; }
        public string Comment { get; set; }
        public ICollection OpenLineItems { get; set; }
    }

Notice that my ItemToBeShipped class has a navigation property for OrderShippingDetail and OrderShippingDetail has one for Customer. The navigation properties will help me with graphs when querying and saving.

There’s one more piece to this puzzle. The shipping department will need to denote items as shipped and the LineItems table has a ShipmentId column that’s used to tie a line item to a shipment. The app will need to update that ShipmentId field when an item is shipped. I’ll create a simple class to take care of this task, rather than relying on the LineItem class that’s used for sales:

    [Table("LineItems")]
    public class LineItemShipment
    {
        [Key]
        public int LineItemId { get; set; }
        public int ShipmentId { get; set; }
    }

Whenever an item has been shipped, you can create a new instance of this class with the proper values and force the application to update the LineItem in the database. It will be important to design your application to use the class only for this purpose. If you attempt to insert a LineItem using this class instead of one that accounts for non-nullable table fields such as OrderId, the database will throw an exception.

After some more fine-tuning, my ShippingContext is now defined as:

    public class ShippingContext : DbContext
{
  public DbSet Shipments { get; set; }
  public DbSet Shippers { get; set; }
  public DbSet Order { get; set; }
  public DbSet ItemsToBeShipped { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    modelBuilder.Ignore();
    modelBuilder.Ignore();
    modelBuilder.Configurations.Add(new ShippingAddressMap());
  }
}

Using the Entity Framework Power Tools beta 2 again to create an EDMX, I can see in the Model Browser window (Figure 4) that Code First infers that the model contains the four classes specified by the DbSets, as well as Customer and ShippingAddress, which were discovered by way of navigation properties from the OrderShippingDetail class.

Shrink EF Models with DDD Bounded Contexts-程序旅途

Figure 4 Model Browser View of ShippingContext Entities as Inferred by Code First

Focused DbContext and Database Initialization

When using a smaller DbContext that supports specific Bounded Contexts in your application, it’s critical to keep in mind two EF Code First default behaviors with respect to database initialization.

The first is that Code First will look for a database with the name of the context.That’s not desirable when your application has a ShippingContext, a CustomerContext, a SalesContext and others. Instead, you want all DbContexts to point to the same database.

The second default behavior to consider is that Code First will use the model inferred by a DbContext to define the database schema. But now you have a DbContext that represents only a slice of the database. For this reason, you don’t want the DbContext classes to trigger database initialization.

It’s possible to solve both of these problems in the constructor of each context class.For example, here in the ShippingContext class you can have the constructor specify the DPSalesDatabase and disable database initialization:

public ShippingContext() : base("DPSalesDatabase")
{
  Database.SetInitializer(null);
}

However, if you have a lot of DbContext classes in your app, this will become a maintenance problem. A better pattern is to specify a base class that disables database initialization and sets the database at the same time:

  public class BaseContext
  DbContext where TContext : DbContext
{
  static BaseContext()
  {
    Database.SetInitializer(null);
  }
  protected BaseContext() : base("DPSalesDatabase")
  {}
}

Now my various context classes can implement the BaseContext instead of each having its own constructor:

 public class ShippingContext:BaseContext

If you’re doing new development and you want to let Code First create or migrate your database based on your classes, you’ll need to create an “uber-model” using a DbContext that includes all of the classes and relationships needed to build a complete model that represents the database. However, this context must not inherit from BaseContext. When you make changes to your class structures, you can run some code that uses the uber-context to perform database initialization whether you’re creating or migrating the database.

Exercising the Focused DbContext

With all of this in place, I created some automated integration tests to perform the following tasks:

  • Retrieve open line items.
  • Retrieve OrderShippingDetails along with Customer and Shipping data for orders with unshipped line items.
  • Retrieve an unshipped line item and create a new shipment. Set the shipment to the line item and insert the new shipment into the database while updating the line item in the database with the new shipment’s key value.

These are the most critical functions you’d need to perform for shipping products to customers who have ordered them. The tests all pass, verifying that my DbContext works as expected. The tests are included in the sample download for this article.

Wrapping Up

I’ve not only created a DbContext that’s focused specifically on supporting the shipping tasks, but thinking it through also helped me create more-efficient domain objects to be used with these tasks. Using DbContext to align my Bounded Context with my shipping subdomain in this way means I don’t have to wade through a quagmire of code to work on the shipping department feature of the application, and I can do what I need to the specialized domain objects without affecting other areas of the development effort.

You can see other examples of focusing DbContext using the DDD concept of Bounded Context in the book, “Programming Entity Framework: DbContext” (O’Reilly Media, 2011), which I coauthored with Rowan Miller.


 

Julie Lerman is a Microsoft MVP, .NET mentor and consultant who lives in the hills of Vermont. You can find her presenting on data access and other Microsoft .NET topics at user groups and conferences around the world. She blogs at thedatafarm.com/blogand is the author of “Programming Entity Framework” (2010) as well as a Code First edition (2011) and a DbContext edition (2012), all from O’Reilly Media. Follow her on Twitter at twitter.com/julielerman.