如何首先在EF代码中为一个数据库处理多个数据库上下文中使用的一个类?

时间:2016-09-12 17:59:01

标签: c# entity-framework design-patterns ef-code-first dbcontext

我正在尝试制作一些在我的存储库层中使用EF的模块类库API。对于任何工作,我需要在每个类库中使用dbcontext类。但是当我需要在每个模块中引用一个类时会发生什么?举例来说,我有一个用户模块,其db上下文包括:

  • 用户
  • 群组
  • 角色

然后我有一个位置模块,其中包括:

  • 建筑物
  • 位置
  • 房间

然后说装备的第三个模块:

  • 设备
  • 设备类型
  • 工作订单

后两者仍然需要对用户的引用,这对每个模块都是不可或缺的。但我不能将两个单独的用户类添加到指向同一个数据库的两个上下文中,它们可能会变得不同步。所以显而易见的解决方案就是后两个模块需要用户模块,而那些需要用户的模块中的任何类只需引用userID。这会打破正常化,因为它不会成为外键,所以我不确定这个想法有多好。

另一种可能性突然出现在让每个模块的dbcontext使用一个接口,并允许使用该模块的人声明他们自己的dbcontext并实现所有这些成员,但我不确定也会工作。

我基本上只想创建一个类库模块的集合,这些模块定义了一组可用于其他程序员的通用类和API调用,同时使用EF作为基础,意图将所有这些存储在一个DB中。但我不太清楚如何通过DbContexts如何工作来实现这一目标。当多个模块需要相同的对象时会发生什么?

1 个答案:

答案 0 :(得分:4)

您所代表的三个上下文通常与Bounded Contexts中设计的Domain-driven design匹配,正如Steeve正确指出的那样。

显然有多种方法可以实现这种情况,每种方法各有利弊。

我建议采用两种方法来尊重域驱动设计的最佳实践并具有很大的灵活性。

方法#1:软分离

我在第一个有界上下文中定义了一个User类,并在第二个有界上下文中定义了一个对用户的引用的接口。

让我们定义用户:

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

    public string Name { get; set; }
}

引用用户的其他模型实现IUserRelated

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

设计模式建议不要直接链接两个分离的有界上下文中的两个实体,而是存储它们各自的引用。

Building类看起来像:

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

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

如您所见,Building模型只知道User的引用。尽管如此,该接口将作为外键并限制插入此UserId属性的值。

现在让我们定义db上下文...

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; }
}

用于初始化数据库的db上下文:

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

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

最后,创建用户和建筑物的代码:

// 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();
}

方法#2:硬分离

我在每个有界上下文中定义了一个User类。接口强制执行公共属性。 Martin Fowler将此方法说明如下:

enter image description here

用户有界的背景:

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; }
}

建立有界背景:

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; }
}

在这种情况下,在Users中拥有BuildingContext属性是完全可以接受的,因为用户也存在于建筑物的上下文中。

用法:

    // 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();
    }

使用EF迁移非常简单。用户界限上下文的迁移脚本(由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");
    }
}

构建有界上下文的迁移脚本(由EF生成)。我必须删除表Users的创建,因为其他有界上下文有责任创建它。在为模块化方法创建表之前,您仍然可以检查表是否存在:

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");
    }
}

为这两个上下文应用Upgrade-Database,数据库就绪了!

关于在类User中添加新属性的OP请求的EDIT。

当有界上下文向类User添加新属性时,它会逐渐添加一个新列。它没有重新定义整个表格。这就是为什么这个实现也是非常通用的。

以下是迁移脚本的示例,其中新属性Accreditation已添加到有界上下文User中的类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");
    }
}