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 уже отслеживается контекстом. Может я что-то неправильно понимаю или может так вообще не делается, но мне хотелось бы разобраться с данной проблемой, буду рад если мне помогут.


Ответы (0 шт):