DOTang DOTang - 2 months ago 19
C# Question

How to handle one class used in multiple db contexts for one database in EF code first?

I am trying to make some module class library APIs that use EF in my repository layer. For any of that to work I need a dbcontext class in each class library. But what happens when I need one class to be referenced in each module? Take for example I have a users module whose db contexts includes:


  • Users

  • Groups

  • Roles



Then I have a locations module that includes:


  • Buildings

  • Locations

  • Rooms



Then say a third module for Equipment which has:


  • Equipment

  • Equipment Types

  • Work Orders



The latter two still need references to users, which is pretty much integral to every module. But I can't add two separate user classes to two contexts pointing to the same db, it's possible they could become out of sync then. So the obvious solution there is just have the latter 2 modules require the user module, and any class in those modules that needs the user just references the userID. This would break normalization though since it wouldn't be a foreign key so I am not sure how good that idea is.

Another possibility that popped into my head was having each module's dbcontext use an interface, and allow the person using the module to declare their own dbcontext and implement all those members, but I'm not sure that will work either.

I basically just want to make a collection of class library modules that define a common set of classes and API calls available to other programmers, while using EF as the base with the intent that this would all be stored in one DB. But I'm not quite sure how to accomplish that with how DbContexts work. What happens when you have multiple modules requiring the same object?

Answer

The three contexts you are representing typically match to Bounded Contexts as designed in the Domain-driven design, as rightly pointed by Steeve.

There are obviously multiple ways to implement this scenario and each one has their pros and cons.

I am suggesting two approaches to respect the best practices of the Domain-driven design and to have a great flexibility.

Approach #1: Soft-separation

I define a User class in the first bounded context and an interface representing a reference to a user in the second bounded context.

Let's define the user:

class User
{
    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

Other models that reference a user implements IUserRelated:

interface IUserRelated
{
    [ForeignKey(nameof(User))]
    Guid UserId { get; }
}

The design pattern recommends to not directly link two entities from two separated bounded contexts, but store their respective reference instead.

The Building class looks like:

class Building : IUserRelated
{
    [Key]
    public Guid Id { get; set; }

    public string Location { get; set; }
    public Guid UserId { get; set; }
}

As you can see, the Building model knows only the reference of a User. Nonetheless, the interface acts a foreign key and constraints the value inserted into this UserId property.

Let's now define the db contexts...

class BaseContext<TContext> : DbContext where TContext : DbContext
{
    static BaseContext()
    {
        Database.SetInitializer<TContext>(null);
    }

    protected BaseContext() : base("Demo")
    {

    }
}

class UserContext : BaseContext<UserContext>
{
    public DbSet<User> Users { get; set; }
}

class BuildingContext : BaseContext<BuildingContext>
{
    public DbSet<Building> Buildings { get; set; }
}

And the db context to initialize the database:

class DatabaseContext : DbContext
{
    public DbSet<Building> Buildings { get; set; }
    public DbSet<User> Users { get; set; }

    public DatabaseContext() : base("Demo")
    {
    }
}

And finally, the code that creates a user and a building:

// Defines some constants
const string userName = "James";
var userGuid = Guid.NewGuid();

// Initialize the db
using (var db = new DatabaseContext())
{
    db.Database.Initialize(true);
}

// Create a user
using (var userContext = new UserContext())
{
    userContext.Users.Add(new User {Name = userName, Id = userGuid});
    userContext.SaveChanges();
}

// Create a building linked to a user
using (var buildingContext = new BuildingContext())
{
    buildingContext.Buildings.Add(new Building {Id = Guid.NewGuid(), Location = "Switzerland", UserId = userGuid});
    buildingContext.SaveChanges();
}

Approach #2: Hard-separation

I define one User class in each of the bounded context. An interface enforces the common properties. This approach is illustrated by Martin Fowler as follows:

enter image description here

User bounded context:

public class User : IUser
{
    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

public class UserContext : BaseContext<UserContext>
{
    public DbSet<User> Users { get; set; }
}

Building bounded context:

public class User : IUser
{
    [Key]
    public Guid Id { get; set; }
}

public class Building
{
    [Key]
    public Guid Id { get; set; }

    public string Location { get; set; }

    public virtual User User { get; set; }
}

public class BuildingContext : BaseContext<BuildingContext>
{
    public DbSet<Building> Buildings { get; set; }

    public DbSet<User> Users { get; set; }
}

In this case, this is totally acceptable to have a Users property in the BuildingContext, as a user exists in the context of a building as well.

Usage:

    // Defines some constants
    const string userName = "James";
    var userGuid = Guid.NewGuid();

    // Create a user
    using (var userContext = new UserContext())
    {
        userContext.Users.Add(new User { Name = userName, Id = userGuid });
        userContext.SaveChanges();
    }

    // Create a building linked to a user
    using (var buildingContext = new BuildingContext())
    {
        var userReference = buildingContext.Users.First(user => user.Id == userGuid);

        buildingContext.Buildings.Add(new Building { Id = Guid.NewGuid(), Location = "Switzerland", User = userReference });
        buildingContext.SaveChanges();
    }

Playing with EF migrations is really easy. The migration script for the User bounded context (generated by EF):

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Users",
            c => new
                {
                    Id = c.Guid(nullable: false),
                    Name = c.String(),
                })
            .PrimaryKey(t => t.Id);

    }

    public override void Down()
    {
        DropTable("dbo.Users");
    }
}

The migration script for the Building bounded context (generate by EF). I have to remove the creation of the table Users, as the other bounded context has the responsibility of creating it. You can still check if the table doesn't exist before creating it for a modular approach:

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Buildings",
            c => new
                {
                    Id = c.Guid(nullable: false),
                    Location = c.String(),
                    User_Id = c.Guid(),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Users", t => t.User_Id)
            .Index(t => t.User_Id);
    }

    public override void Down()
    {
        DropForeignKey("dbo.Buildings", "User_Id", "dbo.Users");
        DropIndex("dbo.Buildings", new[] { "User_Id" });
        DropTable("dbo.Users");
        DropTable("dbo.Buildings");
    }
}

Apply the Upgrade-Database for the two contexts and you database is ready !

EDIT for OP request about adding new properties in the class User.

When a bounded context adds a new property to the class User, it incrementally adds a new column under the hood. It does not redefine the whole table. This is why this implementation is very versatile as well.

Here is an example of migration script where a new property Accreditation is added to the class User in the bounded context Building:

public partial class Accreditation : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.Users", "Accreditation", c => c.String());
    }

    public override void Down()
    {
        DropColumn("dbo.Users", "Accreditation");
    }
}