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