with one net many has framework first code asp c# entity-framework ef-code-first many-to-many

c# - net - one to many entity framework



Primero cree el código, muchos a muchos, con campos adicionales en la tabla de asociación (6)

@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; }

Publico el código que he probado 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<UserTestEmailTest> 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<UserTestEmailTest> 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<yournamespace.UserTest> UserTest { get; set; } //public System.Data.Entity.DbSet<yournamespace.EmailTest> EmailTest { get; set; } internal static void RelateFluent(System.Data.Entity.DbModelBuilder builder) { // Primary keys builder.Entity<UserTest>().HasKey(q => q.UserTestID); builder.Entity<EmailTest>().HasKey(q => q.EmailTestID); builder.Entity<UserTestEmailTest>().HasKey(q => new { q.UserTestID, q.EmailTestID }); // Relationships builder.Entity<UserTestEmailTest>() .HasRequired(t => t.EmailTest) .WithMany(t => t.UserTestEmailTests) .HasForeignKey(t => t.EmailTestID); builder.Entity<UserTestEmailTest>() .HasRequired(t => t.UserTest) .WithMany(t => t.UserTestEmailTests) .HasForeignKey(t => t.UserTestID); } } #endregion

Tengo este escenario:

public class Member { public int MemberID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public virtual ICollection<Comment> Comments { get; set; } } public class Comment { public int CommentID { get; set; } public string Message { get; set; } public virtual ICollection<Member> 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 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 unión personalizada. En una relación de muchos a muchos, EF administra la tabla de unión de forma interna y oculta. Es una tabla sin clase de entidad en tu modelo. Para trabajar con una tabla de este tipo con propiedades adicionales, tendrá que crear realmente dos relaciones 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<MemberComment> MemberComments { get; set; } } public class Comment { public int CommentID { get; set; } public string Message { get; set; } public virtual ICollection<MemberComment> 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 encontrar 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" (asumimos 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 encontrar 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 de muchos a muchos), por ejemplo: Filtrar todos los comentarios del miembro 1 que tienen un 99 en la propiedad Something :

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

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

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

Supongo que la carga lenta tomará los comentarios automáticamente entre bastidores.

Editar

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

1) Crear 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) Añadir 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 comentario existente2:

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 el miembro existente2 y el comentario3:

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 miembro1 y todas sus relaciones con los comentarios:

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

Esto también elimina las relaciones en MemberComments porque las relaciones de uno a muchos entre Member 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 foránea para las propiedades de navegación Member y Comment y dado que las propiedades FK son de tipo no anulable int la relación es necesaria, lo que finalmente provoca la configuración en cascada-delete. Tiene sentido en este modelo, creo.


Quiero proponer una solución donde se puedan lograr ambos tipos de configuración de muchos a muchos.

El "retén" es que necesitamos crear una vista que se EntitySet la tabla de EntitySet , ya que EF valida que la tabla de un esquema se pueda asignar como máximo una vez por EntitySet .

Esta respuesta se suma a lo que ya se ha dicho en las respuestas anteriores y no invalida ninguno de esos enfoques, 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<Comment> Comments { get; set; } public virtual ICollection<MemberCommentView> MemberComments { get; set; } } public class Comment { public int CommentID { get; set; } public string Message { get; set; } public virtual ICollection<Member> Members { get; set; } public virtual ICollection<MemberCommentView> 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<Member> { 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<Comment> { public CommentConfiguration() { HasKey(x => x.CommentID); Property(x => x.CommentID).HasColumnType("int").IsRequired(); Property(x => x.Message).HasColumnType("varchar(max)"); } } public class MemberCommentViewConfiguration : EntityTypeConfiguration<MemberCommentView> { 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<Member> Members { get; set; } public DbSet<Comment> Comments { get; set; } public DbSet<MemberCommentView> 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 encontrar todos los comentarios de los miembros con Apellido = "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" (asumimos 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 quieres 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áctico, sin embargo, le ofrece algunos beneficios si está dispuesto a pasar por la configuración adicional. De cualquier manera, parece que eres capaz de obtener lo mejor de ambos enfoques.


Una forma de resolver este error es colocar el atributo ForeignKey encima 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.


Excelente respuesta por Slauma.

Solo publicaré el código para hacer esto usando el mapeo API fluido.

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

En tu clase derivada de DbContext puedes hacer esto:

public class MyContext : DbContext { protected override void OnModelCreating(DbModelBuilder builder) { // Primary keys builder.Entity<User>().HasKey(q => q.UserID); builder.Entity<Email>().HasKey(q => q.EmailID); builder.Entity<UserEmail>().HasKey(q => new { q.UserID, q.EmailID }); // Relationships builder.Entity<UserEmail>() .HasRequired(t => t.Email) .WithMany(t => t.UserEmails) .HasForeignKey(t => t.EmailID) builder.Entity<UserEmail>() .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: He cambiado CreatedDate de bool a DateTime.

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


TLDR; (semi-relacionado con un error del editor EF en EF6 / VS2012U5) si genera el modelo desde la base de datos 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 .edmx de EF (ya que actualmente no se mostraría y se trataría como un conjunto de propiedades de navegación), Y generó estas clases de su tabla de base de datos (o base de datos primero en MS lingo, 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 a partir de 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 los que se agrega una relación pura de muchos a muchos al principio, y los atributos se diseñan en la base de datos más adelante.

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