您所代表的三种上下文通常与Bounded Context的设计相匹配,正如Steeve所指出的那样,这是在领域驱动设计中设计的。
很明显,有多种方法来实现这种情况,每种方法都有其优缺点。
我建议采用两种方法来遵守领域驱动设计的最佳实践,并具有极大的灵活性。
方法一:软分离
我在第一个有界上下文中定义了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
属性中的值。
现在让我们定义数据库上下文...
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")
{
}
}
最后,创建用户和建筑物的代码:
const string userName = "James";
var userGuid = Guid.NewGuid();
using (var db = new DatabaseContext())
{
db.Database.Initialize(true);
}
using (var userContext = new UserContext())
{
userContext.Users.Add(new User {Name = userName, Id = userGuid});
userContext.SaveChanges();
}
using (var buildingContext = new BuildingContext())
{
buildingContext.Buildings.Add(new Building {Id = Guid.NewGuid(), Location = "Switzerland", UserId = userGuid});
buildingContext.SaveChanges();
}
方法二:硬分离
在每个有界上下文中,我定义了一个User
类。接口强制实现共同属性。Martin Fowler在下面的示例中说明了这种方法:
![enter image description here](https://istack.dev59.com/yGyTE.webp)
用户有界上下文:
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; }
}
在这种情况下,在
BuildingContext
中拥有一个
Users
属性是完全可以接受的,因为用户也存在于建筑物的上下文中。
用法:
const string userName = "James";
var userGuid = Guid.NewGuid();
using (var userContext = new UserContext())
{
userContext.Users.Add(new User { Name = userName, Id = userGuid });
userContext.SaveChanges();
}
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
命令,为两个上下文(contexts)更新数据库即可!
针对“用户”类(User
)新增属性的请求进行编辑。
当一个有界上下文(bounded context)向“用户”类(User
)添加新属性时,它会逐步在后台添加新列,而不是重新定义整个表。这就是为什么此实现非常灵活的原因。
以下是迁移脚本示例,在有界上下文(bounded context)“Building”中给“用户”类(User
)添加了新属性 Accreditation
:
public partial class Accreditation : DbMigration
{
public override void Up()
{
AddColumn("dbo.Users", "Accreditation", c => c.String());
}
public override void Down()
{
DropColumn("dbo.Users", "Accreditation");
}
}