Требуется связь «многие ко многим» для нескольких свойств одного объекта EF Core 2.2

У меня есть сущность для Users и сущность для Projects.

Мне нужно иметь возможность назначать нескольким пользователям 3 разных свойства списка в моем объекте проекта. Мне удалось сделать это успешно для одного свойства (отношения «многие ко многим») с помощью объекта соединения. Я мог бы указать UserType в таблице пользователей и просто использовать одно свойство, но я могу столкнуться со сценариями, в которых пользователи могут выполнять более одной роли (типа), и тогда это не сработает.

Я думал, что могу просто поместить UserType в таблицу соединений (сущность), но я не знаю, как создать эту сущность в моем DBContext.

Вот что у меня есть, это работает с одним определенным свойством:

Объект проекта:

public class Project : IInt32Identity
{
    public int Id { get; set; }
    public string ProjectName { get; set; }
    ...
    public bool ProjectActive { get; set; }
    public List<ProjectFile> ProjectFiles { get; set; }
    public List<ProjectUsers> ProjectUsers { get; set; }
    public DateTime ProjectCreatedDate { get; set; }
    public DateTime ProjectModifiedDate { get; set; }
}

Пользовательская сущность:

public class User : IInt32Identity
{
    public int Id { get; set; }
    public string UserEmail { get; set; }
    ...
    public List<ProjectUsers> ProjectUsers { get; set; }
    public DateTime UserCreatedDate { get; set; }
    public DateTime UserLastLoggedInDate { get; set; }
    public DateTime UserModifiedDate { get; set; }
}

Присоединяйтесь:

public class ProjectUsers
{
    public int UserId { get; set; }
    public User User { get; set; }
    public int ProjectId { get; set; }
    public Project Project { get; set; }
}

И мой OnModelCreating() в моем DBContext

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ProjectUsers>()
            .HasKey(bc => new { bc.UserId, bc.ProjectId });
        modelBuilder.Entity<ProjectUsers>()
            .HasOne(bc => bc.User)
            .WithMany(b => b.ProjectUsers)
            .HasForeignKey(bc => bc.UserId);
        modelBuilder.Entity<ProjectUsers>()
            .HasOne(bc => bc.Project)
            .WithMany(c => c.ProjectUsers)
            .HasForeignKey(bc => bc.ProjectId);
    }

Все работает отлично, как я сказал выше, но вот что я хотел бы:

Объект проекта:

public class Project : IInt32Identity
{
    public int Id { get; set; }
    public string ProjectName { get; set; }
    ...
    public bool ProjectActive { get; set; }
    public List<ProjectFile> ProjectFiles { get; set; }
    public List<ProjectUsers> ProjectClients { get; set; }
    public List<ProjectUsers> ProjectBuilders { get; set; }
    public List<ProjectUsers> ProjectDesigners { get; set; }
    public DateTime ProjectCreatedDate { get; set; }
    public DateTime ProjectModifiedDate { get; set; }
}

UserEntity то же самое.

Присоединяйтесь:

public class ProjectUsers
{
    public int UserId { get; set; }
    public User User { get; set; }
    public int ProjectId { get; set; }
    public Project Project { get; set; }
    public string UserType { get; set; }
}

Где я потерялся, так это в коде OnModelBinding(), и я также не уверен, что EF будет достаточно умен, чтобы правильно заполнять списки на основе этого мета-свойства UserType.

Любая помощь или руководство будет принята с благодарностью.

ТИА

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
1 419
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Может показаться возможным рассматривать ProjectUser как базовый класс/сущность и создавать разные классы/сущности/типы для ProjectClient, ProjectBuilder и ProjectDesigner, которые унаследованы от ProjectUser. Затем вы создаете таблицы для каждого типа и отношения один ко многим к проекту. Обычно это называется подходом Таблица по типам (TPT).

Однако ТРТ еще не реализован в EF Core.

Вы по-прежнему можете добиться этого с помощью Таблица на иерархию (TPH), но у вас будет только один список в проекте для всех пользователей проекта, где UserId, ProjectId и UserType становятся сложным ключом. Клиенты проекта, строители и дизайнеры будут рассчитывать свойства из этого одного списка пользователей проекта.

Сущности

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<ProjectUser> ProjectUsers { get; set; }

    public IEnumerable<ProjectUser> ProjectClients => this.ProjectUsers
        .Where(x => x.UserType == "Client");
    public IEnumerable<ProjectUser> ProjectBuilders => this.ProjectUsers
        .Where(x => x.UserType == "Builder");
    public IEnumerable<ProjectUser> ProjectDesigners => this.ProjectUsers
        .Where(x => x.UserType == "Designer");
}

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }

    public virtual ICollection<ProjectUser> UserProjects { get; set; }
}

public class ProjectUser
{
    public int UserId { get; set; }
    public virtual User User { get; set; }

    public int ProjectId { get; set; }
    public virtual Project Project { get; set; }

    public string UserType { get; set; }
}

Конфигурации

public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
    public void Configure(EntityTypeBuilder<Project> builder)
    {
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Name).IsRequired();
        builder.HasIndex(x => x.Name).IsUnique();

        builder.Ignore(x => x.ProjectBuilders);
        builder.Ignore(x => x.ProjectClients);
        builder.Ignore(x => x.ProjectDesigners);

        builder.ToTable("Project");
    }
}

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Email).IsRequired();
        builder.HasIndex(x => x.Email).IsUnique();

        builder.ToTable("User");
    }
}

public class ProjectUserConfiguration : IEntityTypeConfiguration<ProjectUser>
{
    public void Configure(EntityTypeBuilder<ProjectUser> builder)
    {
        builder.HasKey(x => new { x.ProjectId, x.UserId, x.UserType });
        builder.Property(x => x.UserType).IsRequired();

        builder.HasOne(x => x.Project)
            .WithMany(x => x.ProjectUsers)
            .HasForeignKey(x => x.ProjectId);

        builder.HasOne(x => x.User)
            .WithMany(x => x.UserProjects)
            .HasForeignKey(x => x.UserId);
    }
}

Ключевое слово virtual предназначено для поддержки ленивой загрузки. Если вы не выполняете ленивую загрузку, вам не обязательно иметь virtual. Также вам нужно [NotMapped] эти 3 вычисляемых свойства, что аналогично использованию .Ignore в разговорном API.

Дбконтекст

public class AppDbContext : DbContext
{
    public DbSet<Project> Projects { get; set; }
    public DbSet<User> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.ApplyConfiguration(new ProjectConfiguration());
        modelBuilder.ApplyConfiguration(new UserConfiguration());
        modelBuilder.ApplyConfiguration(new ProjectUserConfiguration());
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        optionsBuilder
            .UseLazyLoadingProxies()
            .UseSqlServer("Data Source=.\\SQLEXPRESS;Initial Catalog=DL.SO.ProjectUsersDemo;Integrated Security=True;MultipleActiveResultSets=False;");
    }
}

Здесь нет ничего особенного. После того, как вы добавите миграцию и обновите базу данных, ваша база данных должна выглядеть так:

После заполнения базы данных образцами данных, хотя это сложно показать здесь, вы можете видеть, что эти 3 списка заполнены правильными данными:

Вот это да. Потрясающий ответ. Большое спасибо. Меня временно перевели в другой проект, но почти все это имеет смысл и кажется, что так и должно быть, поэтому я отмечу это как ответ. Постараюсь в ближайшее время все это подключить. Однако вопрос (извините, что копилку здесь, но это связано). Чтобы запросить это по проекту, я бы использовал что-то вроде этого? return _context.Projects.Where(x => x.Id == id).Include(x => x.ProjectUsers).ThenInclude(b => b.User).FirstOrDefault();

Blair Holmes 30.05.2019 15:31

Если вы не выполняете ленивую загрузку, как в моем примере, то да, вам нужно это сделать Include. И если ожидается, что идентификатор будет иметь один результат, я бы сделал что-то вроде return _context.Projects.Include(x => x.ProjectUsers).ThenInclude(b => b.User).Where(x => x.Id == id).SingleOrDefault();. Если вы уже включили отложенную загрузку, вам не нужно выполнять какие-либо из этих включений. Просто сделайте что-нибудь вроде return _context.Projects.SingleOrDefault(x => x.Id == id);.

David Liang 30.05.2019 20:34

Другие вопросы по теме