Les trois contextes que vous représentez correspondent généralement aux Bounded Contexts tels que conçus dans la conception pilotée par le domaine, comme l'a judicieusement souligné Steeve.
Évidemment, il existe plusieurs façons d'implémenter ce scénario et chacune a ses avantages et ses inconvénients.
Je propose deux approches pour respecter les meilleures pratiques de la conception pilotée par le domaine et avoir une grande flexibilité.
Approche #1 : Séparation souple
Je définis une classe User
dans le premier contexte borné et une interface représentant une référence à un utilisateur dans le deuxième contexte borné.
Définissons l'utilisateur :
class User
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
}
D'autres modèles qui référencent un utilisateur implémentent IUserRelated
:
interface IUserRelated
{
[ForeignKey(nameof(User))]
Guid UserId { get; }
}
Le modèle recommande de ne pas lier directement deux entités de deux contextes bornés séparés, mais de stocker leur référence respective à la place.
La classe Building
ressemble à ceci :
class Building : IUserRelated
{
[Key]
public Guid Id { get; set; }
public string Location { get; set; }
public Guid UserId { get; set; }
}
Comme vous pouvez le voir, le modèle Building
ne connaît que la référence d'un User
. Néanmoins, l'interface agit comme une clé étrangère et contraint la valeur insérée dans cette propriété UserId
.
Définissons maintenant les contextes de base...
class BaseContext : DbContext where TContext : DbContext
{
static BaseContext()
{
Database.SetInitializer(null);
}
protected BaseContext() : base("Demo")
{
}
}
class UserContext : BaseContext
{
public DbSet Users { get; set; }
}
class BuildingContext : BaseContext
{
public DbSet Buildings { get; set; }
}
Et le contexte de base pour initialiser la base de données :
class DatabaseContext : DbContext
{
public DbSet Buildings { get; set; }
public DbSet Users { get; set; }
public DatabaseContext() : base("Demo")
{
}
}
Et enfin, le code qui crée un utilisateur et un bâtiment :
// Définit quelques constantes
const string userName = "James";
var userGuid = Guid.NewGuid();
// Initialise la base de données
using (var db = new DatabaseContext())
{
db.Database.Initialize(true);
}
// Créer un utilisateur
using (var userContext = new UserContext())
{
userContext.Users.Add(new User {Name = userName, Id = userGuid});
userContext.SaveChanges();
}
// Créer un bâtiment lié à un utilisateur
using (var buildingContext = new BuildingContext())
{
buildingContext.Buildings.Add(new Building {Id = Guid.NewGuid(), Location = "Switzerland", UserId = userGuid});
buildingContext.SaveChanges();
}
Approche #2 : Séparation rigide
Je définis une classe User
dans chacun des contextes bornés. Une interface impose les propriétés communes. Cette approche est illustrée par Martin Fowler comme suit :
Contexte borné de l'utilisateur :
public class User : IUser
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
}
public class UserContext : BaseContext
{
public DbSet Users { get; set; }
}
Contexte borné du bâtiment :
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
{
public DbSet Buildings { get; set; }
public DbSet Users { get; set; }
}
Dans ce cas, il est tout à fait acceptable d'avoir une propriété Users
dans le BuildingContext
, car un utilisateur existe également dans le contexte d'un bâtiment.
Utilisation :
// Définit quelques constantes
const string userName = "James";
var userGuid = Guid.NewGuid();
// Créer un utilisateur
using (var userContext = new UserContext())
{
userContext.Users.Add(new User { Name = userName, Id = userGuid });
userContext.SaveChanges();
}
// Créer un bâtiment lié à un utilisateur
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();
}
Travailler avec les migrations EF est vraiment facile. Le script de migration pour le contexte borné de l'utilisateur (généré par 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");
}
}
Le script de migration pour le contexte borné du bâtiment (généré par EF). Je dois supprimer la création de la table Users
, car l'autre contexte borné a la responsabilité de la créer. Vous pouvez toujours vérifier si la table n'existe pas avant de la créer pour une approche modulaire :
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");
}
}
Appliquez le Upgrade-Database
pour les deux contextes et votre base de données est prête !
MODIFICATION à la demande de l'auteur concernant l'ajout de nouvelles propriétés dans la classe User
.
Lorsqu'un contexte borné ajoute une nouvelle propriété à la classe User
, il ajoute de manière incrémentielle une nouvelle colonne en interne. Il ne redéfinit pas toute la table. C'est pourquoi cette implémentation est également très polyvalente.
Voici un exemple de script de migration où une nouvelle propriété Accreditation
est ajoutée à la classe User
dans le contexte borné 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");
}
}