У меня есть одна сущность с именем Team
и другая сущность League
. Теперь предположим, что я хочу создать между ними отношения «многие-многие», но с дополнительным столбцом/свойством, которое мне нужно определить из кода (НЕ сгенерировано Db, например, столбец DateAdded). Дополнительное свойство, которое я хотел бы иметь, — это Season
. Это то, что я пробовал до сих пор:
// Base entity contains the Id and a `DomainEvents` list accesible via `RegisterDomainEvent` method
public class Team : BaseEntity<Guid>
{
private Team()
{
_teamLeagues = new();
}
public Team(Guid id, string name) : this()
{
Id = Guard.Against.Default(id);
Name = Guard.Against.NullOrWhiteSpace(name);
// other properties omitted for brevity
}
public string Name { get; } = default!;
private readonly List<TeamLeague> _teamLeagues;
public IEnumerable<League> Leagues => _teamLeagues
.Select(x => x.League)
.ToList()
.AsReadOnly();
internal void AddLeague(TeamLeague league)
{
Guard.Against.Null(league);
_teamLeagues.Add(league);
}
}
public class League : BaseEntity<Guid>, IAggregateRoot
{
// Required by EF
private League()
{
_teamLeagues = new();
}
public League(Guid id, string name) : this()
{
Id = Guard.Against.Default(id);
Name = Guard.Against.NullOrWhiteSpace(name);
}
public string Name { get; } = default!;
private readonly List<TeamLeague> _teamLeagues;
public IEnumerable<Team> Teams => _teamLeagues
.Select(x => x.Team)
.ToList()
.AsReadOnly();
public void AddTeamForSeason(Team team, string season)
{
Guard.Against.Null(team);
Guard.Against.NullOrEmpty(season);
var teamLeague = new TeamLeague(Guid.NewGuid(), team, this, season);
_teamLeagues.Add(teamLeague);
team.AddLeague(teamLeague);
TeamAddedToLeagueForSeasonEvent teamAdded = new(teamLeague);
RegisterDomainEvent(teamAdded);
}
}
public class TeamLeague : BaseEntity<Guid>
{
private TeamLeague() { }
public TeamLeague(Guid id, Team team, League league, string? season) : this()
{
Id = Guard.Against.Default(id);
Team = Guard.Against.Null(team);
League = Guard.Against.Null(league);
Season = season;
}
public Team Team { get; }
public League League { get; }
public string? Season { get; } = default!;
}
Конфигурация:
public void Configure(EntityTypeBuilder<Team> builder)
{
builder.ToTable("Teams").HasKey(x => x.Id);
builder.Ignore(x => x.DomainEvents);
builder.Property(p => p.Id).ValueGeneratedNever();
builder.Property(p => p.Name).HasMaxLength(ColumnConstants.DEFAULT_NAME_LENGTH);
}
public void Configure(EntityTypeBuilder<League> builder)
{
builder.ToTable("Leagues").HasKey(x => x.Id);
builder.Ignore(x => x.DomainEvents);
builder.Property(p => p.Id).ValueGeneratedNever();
builder.Property(p => p.Name).HasMaxLength(ColumnConstants.DEFAULT_NAME_LENGTH);
builder
.HasMany(x => x.Teams)
.WithMany(x => x.Leagues)
.UsingEntity<TeamLeague>(
e => e.HasOne<Team>().WithMany(),
e => e.HasOne<League>().WithMany())
.Property(e => e.Season).HasColumnName("Season");
}
public void Configure(EntityTypeBuilder<TeamLeague> builder)
{
builder.ToTable("TeamLeagues").HasKey(x => x.Id);
builder.Ignore(x => x.DomainEvents);
builder.Property(p => p.Id).ValueGeneratedNever();
builder.Property(p => p.Season).HasMaxLength(ColumnConstants.DEFAULT_YEAR_LENGTH);
}
Теперь из контроллера API я хочу создать новую команду, и чтобы назначить ее лиге, ее также необходимо добавить в промежуточную таблицу.
// naive api controller, simple for demo reasons
[HttpPost]
public async Task<IActionResult> Create([FromRoute] Guid leagueId, Team team)
{
var league = await _leagueRepository.GetByIdAsync(leagueId);
league?.AddTeamForSeason(team, "2023");
await _leagueRepository.UpdateAsync(league!);
return Ok(league);
}
// League Repository
public override async Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
{
_dbContext.Set<T>().Update(entity);
await SaveChangesAsync(cancellationToken);
}
Но при этом я не могу обновить ни Season
, ни идентификатор (TeamLeague).
Как я могу изменить сопоставление (снова через свободный API), чтобы обновить этот дополнительный столбец в промежуточной таблице.
После многих часов исследований я нашел решение и хотел бы поделиться им на всякий случай, если кто-то еще захочет использовать DDD с правильной инкапсуляцией.
Таким образом, один из способов решить эту проблему — использовать эту конфигурацию для League
public class LeagueConfiguration : IEntityTypeConfiguration<League>
{
public void Configure(EntityTypeBuilder<League> builder)
{
builder
.HasMany(x => x.Teams)
.WithMany(x => x.Leagues)
.UsingEntity<TeamLeague>(
e => e.HasOne<Team>().WithMany("_teamLeagues"),
e => e.HasOne<League>().WithMany("_teamLeagues"))
.Property(e => e.Season).HasColumnName("Season");
}
}
Таким образом, мы сохраняем «чистую» нашу модель предметной области, сохраняя приватность List<TeamLeague> _teamLeagues
, но на этот раз с этой конфигурацией мы предоставляем доступ EFCore к нашему приватному полю, чтобы сопоставить его с промежуточной таблицей.