ASP.NET Core. Маппинг между сущностью бд и доменной моделью со связью многие ко многим
Я учусь писать asp.net core web api и на данный момент пробую всю логику типо изменения свойств и коллекций делать в доменных моделях. Есть 2 сущности бд со связью многие ко многим:
public class EventDb
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime EventTime { get; set; }
public string Location { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public int MaxParticipants { get; set; }
public ICollection<ImageDb> Images { get; set; } = new List<ImageDb>();
public ICollection<ParticipantDb> Participants { get; set; } = new List<ParticipantDb>();
}
public class ParticipantDb
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Surname { get; set; } = string.Empty;
public DateTime BirthDay { get; set; }
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public ICollection<EventDb> Events { get; set; } = new List<EventDb>();
public ICollection<RoleDb> Roles { get; set; } = new List<RoleDb>();
}
И есть 2 доменные модели:
public class Event
{
private readonly List<Participant> _participants = new();
private readonly List<Image> _images = new();
public Guid Id { get; }
public string Name { get; private set; } = string.Empty;
public string Description { get; private set; } = string.Empty;
public DateTime EventTime { get; private set; }
public string Location { get; private set; } = string.Empty;
public string Category { get; private set; } = string.Empty;
public int MaxParticipants { get; private set; }
public IReadOnlyCollection<Participant> Participants => _participants;
public IReadOnlyCollection<Image> Images => _images;
private Event(Guid id, string name, string description, DateTime eventTime, string location, string category,
int maxParticipants)
{
Id = id;
Name = name;
Description = description;
EventTime = eventTime;
Location = location;
Category = category;
MaxParticipants = maxParticipants;
}
public static async Task<Event> Create(Guid id, string name, string description, DateTime eventTime, string location, string category,
int maxParticipants, CancellationToken cancellationToken)
{
var eventDomain = new Event(id, name, description, eventTime, location, category, maxParticipants);
var validator = new EventValidator();
await validator.ValidateAndThrowAsync(eventDomain, cancellationToken);
return eventDomain;
}
public void AddParticipant(Participant participant)
{
if (_participants.Count >= MaxParticipants)
{
throw new InvalidOperationException($"Maximum participants reached.");
}
if (_participants.Any(p => p.Id == participant.Id))
{
throw new InvalidOperationException($"Participant {participant.Id} already enrolled.");
}
_participants.Add(participant);
}
public void RemoveParticipant(Guid participantId)
{
var participant = _participants.Find(p => p.Id == participantId);
if (participant == null)
{
throw new InvalidOperationException($"Participant with Id {participantId} not found.");
}
_participants.Remove(participant);
}
public void AddImages(List<Image> images)
{
foreach (var image in images)
{
_images.Add(image);
}
}
}
public class Participant
{
private readonly List<Role> _roles = new();
private readonly List<Event> _events = new();
public Guid Id { get; }
public string Name { get; } = string.Empty;
public string Surname { get; } = string.Empty;
public DateTime BirthDay { get; }
public string Email { get; } = string.Empty;
public string PasswordHash { get; } = string.Empty;
public IReadOnlyCollection<Role> Roles => _roles;
public IReadOnlyCollection<Event> Events => _events;
private Participant(Guid id, string name, string surname, DateTime birthDay,
string email, string passwordHash)
{
Id = id;
Name = name;
Surname = surname;
BirthDay = birthDay;
Email = email;
PasswordHash = passwordHash;
}
public static async Task<Participant> Create(Guid id, string name, string surname, DateTime birthDay,
string email, string passwordHash, CancellationToken cancellationToken)
{
var participantDomain = new Participant(id, name, surname, birthDay, email, passwordHash);
var validator = new ParticipantValidator();
await validator.ValidateAndThrowAsync(participantDomain, cancellationToken);
return participantDomain;
}
}
Вот метод на получение события из бд:
public async Task<Event> GetEventById(Guid id, CancellationToken cancellationToken)
{
var eventEntity = await _dbContext.Events
.AsNoTracking()
.Include(e => e.Participants)
.Include(e => e.Images)
.AsSplitQuery()
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken);
if (eventEntity == null)
{
throw new EntityNotFoundException($"{nameof(eventEntity)} not found!");
}
return _mapper.Map<Event>(eventEntity);
}
А вот получение участника:
public async Task<Participant> GetParticipantByEmail(string email, CancellationToken cancellationToken)
{
var participantEntity = await _dbContext.Participants
.AsNoTracking()
.Include(e => e.Roles)
.FirstOrDefaultAsync(e => e.Email == email, cancellationToken);
if (participantEntity == null)
{
throw new EntityNotFoundException($"{nameof(participantEntity)} not found!");
}
return _mapper.Map<Participant>(participantEntity);
}
Вот метод, который добавляет участника в событие через доменную модель:
public async Task AddParticipant(Guid eventId, Guid participantId, CancellationToken cancellationToken)
{
var participantDomain = await _unitOfWork.ParticipantsRepository.GetParticipantById(participantId, cancellationToken);
var eventDomain = await _unitOfWork.EventsRepository.GetEventById(eventId, cancellationToken);
eventDomain.AddParticipant(participantDomain);
_unitOfWork.EventsRepository.UpdateEvent(eventDomain);
await _unitOfWork.SaveAsync(cancellationToken);
}
Проблема в том, что мне нужно доменную модель события уже с новым участником смапить обратно в сущность бд и обновить ее, чтобы в связующей таблице появилась связь между событием и добавленным участником. И я что-то не могу понять как это правильно делается.
Пробовал так:
public void UpdateEvent(Event eventDomain)
{
var eventEntity = _dbContext.Events
.Include(e => e.Participants)
.First(e => e.Id == eventDomain.Id);
_mapper.Map(eventDomain, eventEntity);
foreach (var entry in _dbContext.ChangeTracker.Entries())
{
Console.WriteLine($"{entry.Entity.GetType().Name} {entry.State}");
}
_dbContext.Update(eventEntity);
}
Тут я, поскольку ранее событие было получено с AsNoTracking(), получаю его из бд и делаю маппинг из доменной модели и обновляю, но тогда появляется ошибка, говорящая о том, что сущность ParticipantDb уже отслеживается контекстом. Может я что-то неправильно понимаю или может так вообще не делается, но мне хотелось бы разобраться с данной проблемой, буду рад если мне помогут.