Crear código primero, muchos a muchos, con campos adicionales en la tabla de asociación

Tengo este escenario:

public class Member { public int MemberID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public virtual ICollection Comments { get; set; } } public class Comment { public int CommentID { get; set; } public string Message { get; set; } public virtual ICollection Members { get; set; } } public class MemberComment { public int MemberID { get; set; } public int CommentID { get; set; } public int Something { get; set; } public string SomethingElse { get; set; } } 

¿Cómo configuro mi asociación con la API fluida ? ¿O hay una mejor manera de crear la tabla de asociación?

No es posible crear una relación de muchos a muchos con una tabla de combinación personalizada. En una relación muchos a muchos EF administra la tabla de unión internamente y oculta. Es una tabla sin una clase Entity en su modelo. Para trabajar con una tabla de combinación con propiedades adicionales, deberá crear realmente dos relaciones de uno a varios. Podría verse así:

 public class Member { public int MemberID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public virtual ICollection MemberComments { get; set; } } public class Comment { public int CommentID { get; set; } public string Message { get; set; } public virtual ICollection MemberComments { get; set; } } public class MemberComment { [Key, Column(Order = 0)] public int MemberID { get; set; } [Key, Column(Order = 1)] public int CommentID { get; set; } public virtual Member Member { get; set; } public virtual Comment Comment { get; set; } public int Something { get; set; } public string SomethingElse { get; set; } } 

Si ahora desea buscar todos los comentarios de los miembros con LastName = “Smith”, por ejemplo, puede escribir una consulta como esta:

 var commentsOfMembers = context.Members .Where(m => m.LastName == "Smith") .SelectMany(m => m.MemberComments.Select(mc => mc.Comment)) .ToList(); 

…o…

 var commentsOfMembers = context.MemberComments .Where(mc => mc.Member.LastName == "Smith") .Select(mc => mc.Comment) .ToList(); 

O para crear una lista de miembros con el nombre “Smith” (suponemos que hay más de uno) junto con sus comentarios, puede usar una proyección:

 var membersWithComments = context.Members .Where(m => m.LastName == "Smith") .Select(m => new { Member = m, Comments = m.MemberComments.Select(mc => mc.Comment) }) .ToList(); 

Si desea buscar todos los comentarios de un miembro con MemberId = 1:

 var commentsOfMember = context.MemberComments .Where(mc => mc.MemberId == 1) .Select(mc => mc.Comment) .ToList(); 

Ahora también puede filtrar por las propiedades en su tabla de unión (lo que no sería posible en una relación muchos a muchos), por ejemplo: Filtrar todos los comentarios del miembro 1 que tienen un 99 en propiedad Something :

 var filteredCommentsOfMember = context.MemberComments .Where(mc => mc.MemberId == 1 && mc.Something == 99) .Select(mc => mc.Comment) .ToList(); 

Debido a la carga lenta, las cosas pueden ser más fáciles. Si tiene un Member cargado, debería poder obtener los comentarios sin una consulta explícita:

 var commentsOfMember = member.MemberComments.Select(mc => mc.Comment); 

Supongo que la carga diferida buscará los comentarios automáticamente detrás de las escenas.

Editar

Solo por diversión, algunos ejemplos más sobre cómo agregar entidades y relaciones y cómo eliminarlas en este modelo:

1) Cree un miembro y dos comentarios de este miembro:

 var member1 = new Member { FirstName = "Pete" }; var comment1 = new Comment { Message = "Good morning!" }; var comment2 = new Comment { Message = "Good evening!" }; var memberComment1 = new MemberComment { Member = member1, Comment = comment1, Something = 101 }; var memberComment2 = new MemberComment { Member = member1, Comment = comment2, Something = 102 }; context.MemberComments.Add(memberComment1); // will also add member1 and comment1 context.MemberComments.Add(memberComment2); // will also add comment2 context.SaveChanges(); 

2) Agregue un tercer comentario de member1:

 var member1 = context.Members.Where(m => m.FirstName == "Pete") .SingleOrDefault(); if (member1 != null) { var comment3 = new Comment { Message = "Good night!" }; var memberComment3 = new MemberComment { Member = member1, Comment = comment3, Something = 103 }; context.MemberComments.Add(memberComment3); // will also add comment3 context.SaveChanges(); } 

3) Crear un nuevo miembro y relacionarlo con el comentario2 existente:

 var comment2 = context.Comments.Where(c => c.Message == "Good evening!") .SingleOrDefault(); if (comment2 != null) { var member2 = new Member { FirstName = "Paul" }; var memberComment4 = new MemberComment { Member = member2, Comment = comment2, Something = 201 }; context.MemberComments.Add(memberComment4); context.SaveChanges(); } 

4) Crear una relación entre member2 y comment3 existentes:

 var member2 = context.Members.Where(m => m.FirstName == "Paul") .SingleOrDefault(); var comment3 = context.Comments.Where(c => c.Message == "Good night!") .SingleOrDefault(); if (member2 != null && comment3 != null) { var memberComment5 = new MemberComment { Member = member2, Comment = comment3, Something = 202 }; context.MemberComments.Add(memberComment5); context.SaveChanges(); } 

5) Eliminar esta relación de nuevo:

 var memberComment5 = context.MemberComments .Where(mc => mc.Member.FirstName == "Paul" && mc.Comment.Message == "Good night!") .SingleOrDefault(); if (memberComment5 != null) { context.MemberComments.Remove(memberComment5); context.SaveChanges(); } 

6) Eliminar member1 y todas sus relaciones a los comentarios:

 var member1 = context.Members.Where(m => m.FirstName == "Pete") .SingleOrDefault(); if (member1 != null) { context.Members.Remove(member1); context.SaveChanges(); } 

Esto elimina las relaciones en MemberComments también porque las relaciones de uno a muchos entre MemberComments y MemberComments y entre Comment y MemberComments se configuran con eliminación en cascada por convención. Y este es el caso porque MemberId y CommentId en MemberComment se detectan como propiedades de clave externa para las propiedades de navegación de Member y Comment , y dado que las propiedades de FK son de tipo no anulable, se requiere la relación que finalmente causa la instalación de eliminación en cascada. Tiene sentido en este modelo, creo.

Excelente respuesta de Slauma.

Solo publicaré el código para hacer esto usando la asignación de API fluida.

 public class User { public int UserID { get; set; } public string Username { get; set; } public string Password { get; set; } public ICollection UserEmails { get; set; } } public class Email { public int EmailID { get; set; } public string Address { get; set; } public ICollection UserEmails { get; set; } } public class UserEmail { public int UserID { get; set; } public int EmailID { get; set; } public bool IsPrimary { get; set; } } 

En su clase derivada de DbContext puede hacer esto:

 public class MyContext : DbContext { protected override void OnModelCreating(DbModelBuilder builder) { // Primary keys builder.Entity().HasKey(q => q.UserID); builder.Entity().HasKey(q => q.EmailID); builder.Entity().HasKey(q => new { q.UserID, q.EmailID }); // Relationships builder.Entity() .HasRequired(t => t.Email) .WithMany(t => t.UserEmails) .HasForeignKey(t => t.EmailID) builder.Entity() .HasRequired(t => t.User) .WithMany(t => t.UserEmails) .HasForeignKey(t => t.UserID) } } 

Tiene el mismo efecto que la respuesta aceptada, con un enfoque diferente, que no es mejor ni peor.

EDITAR: Cambié CreatedDate de bool a DateTime.

EDIT 2: debido a la falta de tiempo, coloqué un ejemplo de una aplicación en la que estoy trabajando para asegurarme de que esto funcione.

@Esteban, el código que proporcionó es correcto, gracias, pero incompleto, lo he probado. Faltan propiedades en la clase “UserEmail”:

  public UserTest UserTest { get; set; } public EmailTest EmailTest { get; set; } 

Publiqué el código que probé si alguien está interesado. Saludos

 using System.Data.Entity; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Web; #region example2 public class UserTest { public int UserTestID { get; set; } public string UserTestname { get; set; } public string Password { get; set; } public ICollection UserTestEmailTests { get; set; } public static void DoSomeTest(ApplicationDbContext context) { for (int i = 0; i < 5; i++) { var user = context.UserTest.Add(new UserTest() { UserTestname = "Test" + i }); var address = context.EmailTest.Add(new EmailTest() { Address = "address@" + i }); } context.SaveChanges(); foreach (var user in context.UserTest.Include(t => t.UserTestEmailTests)) { foreach (var address in context.EmailTest) { user.UserTestEmailTests.Add(new UserTestEmailTest() { UserTest = user, EmailTest = address, n1 = user.UserTestID, n2 = address.EmailTestID }); } } context.SaveChanges(); } } public class EmailTest { public int EmailTestID { get; set; } public string Address { get; set; } public ICollection UserTestEmailTests { get; set; } } public class UserTestEmailTest { public int UserTestID { get; set; } public UserTest UserTest { get; set; } public int EmailTestID { get; set; } public EmailTest EmailTest { get; set; } public int n1 { get; set; } public int n2 { get; set; } //Call this code from ApplicationDbContext.ConfigureMapping //and add this lines as well: //public System.Data.Entity.DbSet UserTest { get; set; } //public System.Data.Entity.DbSet EmailTest { get; set; } internal static void RelateFluent(System.Data.Entity.DbModelBuilder builder) { // Primary keys builder.Entity().HasKey(q => q.UserTestID); builder.Entity().HasKey(q => q.EmailTestID); builder.Entity().HasKey(q => new { q.UserTestID, q.EmailTestID }); // Relationships builder.Entity() .HasRequired(t => t.EmailTest) .WithMany(t => t.UserTestEmailTests) .HasForeignKey(t => t.EmailTestID); builder.Entity() .HasRequired(t => t.UserTest) .WithMany(t => t.UserTestEmailTests) .HasForeignKey(t => t.UserTestID); } } #endregion 

TLDR; (semi relacionado con un error del editor EF en EF6 / VS2012U5) si genera el modelo desde DB y no puede ver la tabla m: m atribuida: elimine las dos tablas relacionadas -> Guardar .edmx -> Generar / agregar desde la base de datos – > Guardar.

Para aquellos que vinieron aquí preguntándose cómo obtener una relación de muchos a muchos con columnas de atributos para mostrar en el archivo EF .edmx (ya que actualmente no se mostraría y se trataría como un conjunto de propiedades de navegación), Y usted generó estas clases de su tabla de base de datos (o base de datos, primero en jerga MS, creo).

Elimine las 2 tablas en cuestión (para tomar el ejemplo de OP, miembro y comentario) en su .edmx y agréguelos nuevamente a través de ‘Generar modelo desde la base de datos’. (es decir, no intente dejar que Visual Studio los actualice – eliminar, guardar, agregar, guardar)

Luego creará una tercera mesa en línea con lo que se sugiere aquí.

Esto es relevante en los casos en que se agrega una relación pura de muchos a muchos al principio, y los atributos se diseñan en el DB más adelante.

Esto no fue inmediatamente claro en este hilo / Google. Así que solo ponlo ahí ya que este es el enlace # 1 en Google que busca el problema, pero que viene del lado de DB primero.

Una forma de resolver este error es colocar el atributo ForeignKey en la parte superior de la propiedad que desea como clave externa y agregar la propiedad de navegación.

Nota: En el atributo ForeignKey , entre paréntesis y comillas dobles, coloque el nombre de la clase a la que se hace referencia de esta manera.

enter image description here

Quiero proponer una solución en la que se puedan alcanzar los dos sabores de una configuración de muchos a muchos.

La “captura” es que necesitamos crear una vista que se dirija a la Tabla de EntitySet , ya que EF valida que la tabla de un esquema se puede asignar como máximo una vez por EntitySet .

Esta respuesta se sum a lo que ya se dijo en respuestas anteriores y no anula ninguno de esos enfoques, sino que se basa en ellos.

El modelo:

 public class Member { public int MemberID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public virtual ICollection Comments { get; set; } public virtual ICollection MemberComments { get; set; } } public class Comment { public int CommentID { get; set; } public string Message { get; set; } public virtual ICollection Members { get; set; } public virtual ICollection MemberComments { get; set; } } public class MemberCommentView { public int MemberID { get; set; } public int CommentID { get; set; } public int Something { get; set; } public string SomethingElse { get; set; } public virtual Member Member { get; set; } public virtual Comment Comment { get; set; } } 

La configuración:

 using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.ModelConfiguration; public class MemberConfiguration : EntityTypeConfiguration { public MemberConfiguration() { HasKey(x => x.MemberID); Property(x => x.MemberID).HasColumnType("int").IsRequired(); Property(x => x.FirstName).HasColumnType("varchar(512)"); Property(x => x.LastName).HasColumnType("varchar(512)") // configure many-to-many through internal EF EntitySet HasMany(s => s.Comments) .WithMany(c => c.Members) .Map(cs => { cs.ToTable("MemberComment"); cs.MapLeftKey("MemberID"); cs.MapRightKey("CommentID"); }); } } public class CommentConfiguration : EntityTypeConfiguration { public CommentConfiguration() { HasKey(x => x.CommentID); Property(x => x.CommentID).HasColumnType("int").IsRequired(); Property(x => x.Message).HasColumnType("varchar(max)"); } } public class MemberCommentViewConfiguration : EntityTypeConfiguration { public MemberCommentViewConfiguration() { ToTable("MemberCommentView"); HasKey(x => new { x.MemberID, x.CommentID }); Property(x => x.MemberID).HasColumnType("int").IsRequired(); Property(x => x.CommentID).HasColumnType("int").IsRequired(); Property(x => x.Something).HasColumnType("int"); Property(x => x.SomethingElse).HasColumnType("varchar(max)"); // configure one-to-many targeting the Join Table view // making all of its properties available HasRequired(a => a.Member).WithMany(b => b.MemberComments); HasRequired(a => a.Comment).WithMany(b => b.MemberComments); } } 

El contexto:

 using System.Data.Entity; public class MyContext : DbContext { public DbSet Members { get; set; } public DbSet Comments { get; set; } public DbSet MemberComments { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Configurations.Add(new MemberConfiguration()); modelBuilder.Configurations.Add(new CommentConfiguration()); modelBuilder.Configurations.Add(new MemberCommentViewConfiguration()); OnModelCreatingPartial(modelBuilder); } } 

De la respuesta de Saluma (@Saluma)

Si ahora desea buscar todos los comentarios de los miembros con LastName = “Smith”, por ejemplo, puede escribir una consulta como esta:

Esto todavía funciona …

 var commentsOfMembers = context.Members .Where(m => m.LastName == "Smith") .SelectMany(m => m.MemberComments.Select(mc => mc.Comment)) .ToList(); 

… pero ahora también podría ser …

 var commentsOfMembers = context.Members .Where(m => m.LastName == "Smith") .SelectMany(m => m.Comments) .ToList(); 

O para crear una lista de miembros con el nombre “Smith” (suponemos que hay más de uno) junto con sus comentarios, puede usar una proyección:

Esto todavía funciona …

 var membersWithComments = context.Members .Where(m => m.LastName == "Smith") .Select(m => new { Member = m, Comments = m.MemberComments.Select(mc => mc.Comment) }) .ToList(); 

… pero ahora también podría ser …

 var membersWithComments = context.Members .Where(m => m.LastName == "Smith") .Select(m => new { Member = m, m.Comments }) .ToList(); 

Si desea eliminar un comentario de un miembro

 var comment = ... // assume comment from member John Smith var member = ... // assume member John Smith member.Comments.Remove(comment); 

Si desea Include() los comentarios de un miembro

 var member = context.Members .Where(m => m.FirstName == "John", m.LastName == "Smith") .Include(m => m.Comments); 

Todo esto se siente como azúcar sintáctica, sin embargo, le ofrece algunas ventajas si está dispuesto a pasar por la configuración adicional. De cualquier manera, parece ser capaz de obtener lo mejor de ambos enfoques.