EF 6.1.3 - Cannot insert duplicate key in object - asp.net

I searched on site for solving my problem, but I still haven't solved it.
I have 2 entities:
public class Article
{
[Key]
public Guid ID { get; set; }
public Guid? ApprovedBy_ID { get; set; }
public Guid CreatedBy_ID { get; set; }
public virtual Profile ApprovedBy { get; set; }
public virtual Profile CreatedBy { get; set; }
//New guid for new article
public Article()
{
ID = Guid.NewGuid();
}
}
public class Profile
{
[Key]
public Guid ID { get; set; }
[Required]
[StringLength(100)]
public string FullName { get; set; }
public Profile()
{
ID = Guid.NewGuid();
}
}
And here is my insert-logic:
private readonly iContext context;
public ArticleLogic()
{
context = new iContext();
}
public IEnumerable<Article> GetAllArticle()
{
return context.Articles.Include("Categories").Include("Pictures").Include("ApprovedBy").Include("CreatedBy").Include("Template");
}
public Article AddArticle(Article article)
{
try
{
Profile pf = context.Profiles.First();
context.Profiles.Attach(pf);
Article art = new Article();
art.Title = article.Title;
art.Description = article.Description;
art.Content = article.Content;
art.Tag = article.Tag;
art.Template = article.Template;
//pf has ID = '0816f19c-31c1-4103-8f51-ba422beab1c0' (first row in database)
art.CreatedBy = pf;
art.CreatedBy_ID = pf.ID;
context.Articles.Add(art);
context.SaveChanges();
return article;
}
catch (Exception ex)
{
//But ex throw error duplicate ID = '7aa1d064-54ff-47b9-807d-db422fa71f8c' (second row in database)
Debug.WriteLine(ex.StackTrace);
throw ex;
}
}
DB Context:
public myContext()
: base("name=abc")
{
this.Configuration.LazyLoadingEnabled = true;
this.Configuration.ProxyCreationEnabled = false;
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Role> Roles { get; set; }
public virtual DbSet<Template> Templates { get; set; }
public virtual DbSet<Article> Articles { get; set; }
public virtual DbSet<Picture> Pictures { get; set; }
public virtual DbSet<Profile> Profiles { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//One-to-many: Article - Picture (nullable)
modelBuilder.Entity<Picture>()
.HasOptional<Article>(c => c.Article)
.WithMany(p => p.Pictures)
.HasForeignKey(f => f.ArticleId);
////One-to-many: Role - Profile
modelBuilder.Entity<Profile>()
.HasRequired<Role>(i => i.Role)
.WithMany(i => i.Profiles)
.HasForeignKey(f => f.RoleID);
///Many-to-many: Article - Category
modelBuilder.Entity<Article>()
.HasMany(t => t.Categories)
.WithMany(t => t.Articles)
.Map(m => {
m.ToTable("ArticleCategories");
m.MapLeftKey("ArticleId");
m.MapRightKey("CategoryId");
});
//
modelBuilder.Entity<Article>()
.HasOptional(a => a.ApprovedBy)
.WithMany(a => a.ArticleApprovedBy)
.HasForeignKey(f=>f.ApprovedBy_ID)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Article>()
.HasRequired(a => a.CreatedBy)
.WithMany(a => a.ArticleCreatedBy)
.HasForeignKey(f => f.CreatedBy_ID)
.WillCascadeOnDelete(false);
base.OnModelCreating(modelBuilder);
}
I am using CodeFirst EF 6.1.3 and I am getting an error: Violation of PRIMARY KEY constraint 'PK_Profiles'. Cannot insert duplicate key in object 'dbo.Profiles' like this link Entity Framework Code First - Cannot insert duplicate key in object 'dbo.T_CRProviders'?
Can anyone help me? Thanks.

context thinks pf is new and should be inserted into the DB. Show the instantiation and configuration of context - do you have change tracking disabled?

You need to tell EF that a Profile can be referenced multiple times by Article:
modelBuilder.Entity<Article>()
.HasOptional(a=>a.ApprovedBy)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<Article>()
.HasOptional(a=>a.CreatedBy)
.WithMany()
.WillCascadeOnDelete(false);
The HasOptional could also be HasRequired if the property is mandatory (not null in the DB).
Adding WithMany is a key here if you want (as you do in your sample) be able to associate a profile to multiple properties of Article (and multiple Articles too).
PS: I wrote this code by memory, so something could need adjustment.

Your line:
context.Profiles.Attach(pf);
will attach the pf, however it is already tracked by EF. You just got it from the database. The pf will be reinserted by EF, and this fails. Just remove the line and you will be fine.
Add/Attach and Entity States

Related

EF6 Many-to-many can not delete

I have created table using EF6 core using below code.
The problem is, UserA , UserB created and assigned the different claims. No issue at all. When assigning existing claims (for example UserB is assigned UserA's claims --> [tying to do] delete UserB's claims & Changing UserB's subject to UserA's subject) then I am deleting UserB's claim by subjectId this create the primary key duplicate issue in UserUserClaims tables bcos SubjectId and ClaimId already existing
public class User
{
[Key]
public Guid UserId { get; set; }
[MaxLength(200)]
public string Username { get; set; }
[Required]
public Guid Subject { get; set; }
public ICollection<UserClaim> Claims { get; set; } = new List<UserClaim>();
public List<UserClaimRelation> UserClaimRelations { get; set; }
}
public class UserClaim
{
[Key]
public Guid UserClaimId { get; set; }
[MaxLength(250)]
[Required]
public string Type{ get; set; }
[Required]
public Guid Subject { get; set; }
[MaxLength(250)]
[Required]
public string Value { get; set; }
public ICollection<User> Users { get; set; }
public List<UserClaimRelation> UserClaimRelations { get; set; }
}
public class UserClaimRelation
{
public Guid UserClaimId{ get; set; }
public UserClaim Claim { get; set; }
public Guid UserSubject{ get; set; }
public User User { get; set; }
}
public class MyDbContext: DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<UserClaim> UserClaims { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>()
.HasMany(p => p.Claims)
.WithMany(p => p.Users)
.UsingEntity<UserClaimRelation>(
j => j
.HasOne(pt => pt.Claim)
.WithMany(t => t.UserClaimRelations)
.HasPrincipalKey(f => f.UserClaimId),
j => j.HasOne(p => p.User)
.WithMany(t => t.UserClaimRelations)
.HasPrincipalKey( t => t.Subject),
j => j.HasKey( t => new {t.UserClaimId, t.UserSubject})
);
}
I am trying to delete the Claims by SubjectId
public async Task<bool> DeleteUserClaimsBySubjectAsync(Guid subject)
{
if (subject == Guid.Empty)
{
throw new ArgumentNullException(nameof(subject));
}
var claims = await _context.UserClaims
.Where(x => x.Subject == subject).ToListAsync();
foreach (var claim in claims)
{
_context.UserClaims.Remove(claim);
}
//_context.UserClaims.RemoveRange(claims);
return (await _context.SaveChangesAsync() > 0);
}
While deleting claims in DeleteUserClaimsBySubjectAsync I am getting the below error
SqlException: Violation of PRIMARY KEY constraint 'PK_UserClaimRelation'. Cannot insert duplicate key in object 'dbo.UserClaimRelation'. The duplicate key value is (13229d33-99e0-41b3-b18d-4f72127e3971, 8acbbd40-1608-41f2-de59-08d9b58b83d3).
SQL Profiler Query
exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [UserClaimRelation] ([UserClaimId], [UserSubject])
VALUES (#p0, #p1),
(#p2, #p3),
(#p4, #p5),
(#p6, #p7),
(#p8, #p9);
DELETE FROM [UserClaims]
WHERE [UserClaimId] = #p10 AND [ConcurrencyStamp] = #p11;
SELECT ##ROWCOUNT;
DELETE FROM [UserClaims]
WHERE [UserClaimId] = #p12 AND [ConcurrencyStamp] = #p13;
SELECT ##ROWCOUNT;
DELETE FROM [UserClaims]
WHERE [UserClaimId] = #p14 AND [ConcurrencyStamp] = #p15;
SELECT ##ROWCOUNT;
DELETE FROM [UserClaims]
WHERE [UserClaimId] = #p16 AND [ConcurrencyStamp] = #p17;
SELECT ##ROWCOUNT;
DELETE FROM [UserClaims]
WHERE [UserClaimId] = #p18 AND [ConcurrencyStamp] = #p19;
SELECT ##ROWCOUNT;
',N'#p0 uniqueidentifier,#p1 uniqueidentifier,#p2 uniqueidentifier,#p3 uniqueidentifier,#p4 uniqueidentifier,#p5 uniqueidentifier,#p6 uniqueidentifier,#p7 uniqueidentifier,#p8 uniqueidentifier,#p9 uniqueidentifier,#p10 uniqueidentifier,#p11 nvarchar(4000),#p12 uniqueidentifier,#p13 nvarchar(4000),#p14 uniqueidentifier,#p15 nvarchar(4000),#p16 uniqueidentifier,#p17 nvarchar(4000),#p18 uniqueidentifier,#p19 nvarchar(4000)',
#p0='6203BBC3-BD52-4E32-3DDD-08D9B86AB745',#p1='D860EFCA-22D9-47FD-8249-791BA61B07C7',
#p2='80B958C5-FF44-400F-3DDE-08D9B86AB745',#p3='D860EFCA-22D9-47FD-8249-791BA61B07C7',
#p4='A844CB59-5A00-488A-3DDF-08D9B86AB745',#p5='D860EFCA-22D9-47FD-8249-791BA61B07C7',#p6='794AB806-3215-45AF-3DE0-08D9B86AB745',#p7='D860EFCA-22D9-47FD-8249-791BA61B07C7',#p8='80EFF0DF-AE56-419A-3DE1-08D9B86AB745',#p9='D860EFCA-22D9-47FD-8249-791BA61B07C7',#p10='1A48CF6E-E1CD-4042-3DE4-08D9B86AB745',#p11=N'742674d8-3c59-4be4-ac09-38c6804acb66',#p12='258ABD95-6B2A-45CC-3DE6-08D9B86AB745',#p13=N'd206316a-71af-46b7-92e4-bddca669ad87',#p14='4FB1D2E3-18BE-4AFC-3DE3-08D9B86AB745',#p15=N'3eb17fb7-998b-47e1-a97f-3640cbd82b7a',#p16='6C2B9A18-5C8F-4068-3DE5-08D9B86AB745',#p17=N'415b2cc1-b50a-4ec8-872d-13b6342dcd33',#p18='951CEEA6-D057-4588-3DE2-08D9B86AB745',#p19=N'3dfd76dd-d515-4daa-893c-f1ae97aa063a'
Ok, so you want many-to-many relationship here - multiple user can have multiple different claims and multiple claims can be assigned to multiple users.
So first - Users:
public class User
{
[Key]
public Guid UserId { get; set; }
[MaxLength(200)]
public string Username { get; set; }
[Required]
public Guid Subject { get; set; }
public ICollection<UserClaim> Claims { get; set; } = new List<UserClaim>();
}
Then, UserClaims
public class UserClaim
{
[Key]
public Guid UserClaimId { get; set; }
[MaxLength(250)]
[Required]
public string Type{ get; set; }
[Required]
public Guid Subject { get; set; }
[MaxLength(250)]
[Required]
public string Value { get; set; }
public ICollection<User> Users { get; set; }
}
Notice that there is no navigation property leading to UserClaimRelation in any of the above. In the matter of fact, we remove this UserClaimRelation as an separate entity model entirely - it is not needed for this example. You can map it if you really want, but lets focus on making this work.
Now, the relationship setup:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasMany(x => x.Claims)
.WithMany(x => x.Users)
.UsingEntity<Dictionary<string, object>>(
"UserClaimRelation",
x => x.HasOne<User>().WithMany(),
x => x.HasOne<UserClaim>().WithMany(),
x => x.ToTable("UserClaimRelation"));
}
Now, after updating / recreating / reapplying migrations to the database (depends on your way of doing things) you should be able to delete the claims by subject without modifying your DeleteUserClaimsBySubjectAsync method.
This should set it all up and create intermediary table.
Validate this code, I was typing from memory
I applied the code. Tables created like below image. But when I try to update an userA's subject with userB's Subject (same claims of userB) I got the below error
Updating
SqlException: Violation of PRIMARY KEY constraint 'PK_UserClaimRelation'. Cannot insert duplicate key in object 'dbo.UserClaimRelation'. The duplicate key value is (bd7eb87f-2a5f-4921-758e-08d9b97d3121, 13229d33-99e0-41b3-b18d-4f72127e3971). bcos its trying to insert in UserClaimRelation, same as the profiler query I given initially.

"Sequence contains no matching element" while expanding child navigation property with OData & AutoMapper

The exception "Sequence contains no matching element" occurs while I am trying to expand navigation property using the following query: http://localhost:5000/odata/users?$expand=roles. Request without $expand keyword works. The DTO object is received from AutoMapper with very simple mapping configuration.
Please look at the relevant sources:
Entity Framework Entities:
public class User {
public string Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public virtual ICollection<UserRole> UserRoles { get; set; }
}
public class Role {
public string Id { get; set; }
public string Name { get; set; }
}
public class UserRole {
public string UserId { get; set; }
public string RoleId { get; set; }
}
DTO models:
public class UserModel {
public string Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public IEnumerable<RoleModel> Roles { get; set; }
}
public class RoleModel
{
public string Id { get; set; }
public string Name { get; set; }
}
AutoMapper configuration:
CreateMap<UserRole, RoleModel>()
.ForMember(x => x.Id, x => x.MapFrom(m => m.UserId))
.ForMember(x => x.Name, x => x.MapFrom(m => m.Role.Name));
CreateMap<User, UserModel>()
.ForMember(dest => dest.Roles, opts => opts.MapFrom(src => src.UserRoles))
.ReverseMap()
.ForMember(dest => dest.UserRoles, o => o.Ignore());
Controller action method:
[HttpGet]
[EnableQuery]
public IQueryable<UserModel> Get() =>
_userManager.Users.ProjectTo<UserModel>(_mapper.ConfigurationProvider);
Actually, exception is thrown by Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.CorrelatedCollectionOptimizingVisitor while it trying to find WhereClause in expression generated by AutoMapper. This line:
var originalCorrelationPredicate = collectionQueryModel.BodyClauses
.OfType<WhereClause>()
.Single(c => c.Predicate is NullSafeEqualExpression);
If I change the configuration of AutoMapper to the following below, exception is not thrown but Roles subquery is executed separately for every User record:
CreateMap<User, UserModel>()
.ForMember(dest => dest.Roles, opts =>
opts.MapFrom(src =>
src.UserRoles.Select(ur => new RoleModel {
Id = ur.UserId,
Name = ur.Role.Name
})))
.ReverseMap()
.ForMember(dest => dest.UserRoles, o => o.Ignore());
I have checked one more idea: if I am changing action method to call ToList() method on ProjectTo() result then only two queries are executed on DB and no errors are occured. That is why I think the reason for the error in using of OData and AutoMapper at the same time.
What am I doing wrong?
Added:
I have checked the execution plan as proposed by #Lucian and found that the only difference between expression that was generated by AutoMapper and another one that was written by hand is the sub-query call.
Hand-written variant (works as it should):
.......
Roles = .Call System.Linq.Enumerable.Select(
$x.UserRoles,
.Lambda
#Lambda2<System.Func`2[Test.Data.UserRole,Test.Models.RoleModel]>),
.......
Generated by AutoMapper:
.......
Roles = .Call System.Linq.Enumerable.ToList(.Call
System.Linq.Enumerable.Select(
$x.UserRoles,
.Lambda
#Lambda2<System.Func`2[Test.Data.UserRole,Test.Models.RoleModel]>)),
.......
Does anyone know how to force AutoMapper not to generate a call to the ToList()?

Entity Framework 7 - accessing related entities

My problem is accessing related entities in Web App on 2nd level relation. Didn't find proper answer related to EF7.
Let's take following sample of 3 classes: one-to-many - x - many-to-one.
public class Person {
public int PersonId { get; set; }
public string Name { get; set; }
public virtual ICollection<Buy> Buys { get; set; } = new List<Buy>();
}
public class Buy {
public int BuyId { get; set; }
public int PersonId { get; set; }
public int BookId { get; set; }
public virtual Person Person { get; set; }
public virtual Book Book { get; set; }
}
public class Book {
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Buy> Buys { get; set; } = new List<Buy>();
}
With context.
public class MyContext : DbContext {
public DbSet<Person> People { get; set; }
public DbSet<Book> Books { get; set; }
public DbSet<Buy> Buys { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Person>()
.Collection(c => c.Buys).InverseReference(c => c.Person)
.ForeignKey(c => c.PersonId);
modelBuilder.Entity<Book>()
.Collection(i => i.Buys).InverseReference(i => i.Book)
.ForeignKey(i => i.BookId);
}
Person Controller - Details view.
// _context present in given scope
public IActionResult Details(int? id) {
var person = _context.People
.Include(c => c.Buys)
.Where(c => c.PersonId == id)
.FirstOrDefault();
}
With this configuration I intended to be able to fetch from Person model, not only Buys info, but also related further books. Like following in part of View.
#model <project>.Models.Person
// (...)
#Html.DisplayFor(model => model.PersonId) // ok - person
#Html.DisplayFor(model => model.PersonName) // ok - person
#foreach (var item in Model.Buys) {
#Html.DisplayFor(modeItem => item.BuyId) // ok - buy
#Html.DisplayFor(modelItem => item.Book.Name) // null - book
}
Do I need to code additional references in fluent API or do further includes in entity model to be able to access Item data from Person level?
You should include Book like you did with Buys. I mean:
var person = _context.People
.Include(c => c.Buys.Select(x => x.Book))
.Where(c => c.PersonId == id)
.FirstOrDefault();
But actually if you working with MVC it's better to create ViewModel Class that have all data that you need on particular View rather than have your EF classes on View.
In EF 7 beta7 you can also use the ThenInclude method to include several levels:
var person = _context.People
.Include(c => c.Buys)
.ThenInclude(b=>b.Book)
.FirstOrDefault(c => c.PersonId == id);

entity framework multiple relationships

I'm new to Entity Framework and I have probably a simple question.
I have simplified the structure at a maximum to be clear (I hope I am).
Imagine that I just need to create a simple "Enterprise" class, with only a Name.
Then another class named "Worker" with also just a Name for the worker.
A worker should belong to an Enterprise.
An Enterprise must have a manager (who is a Worker).
So here is how I imagine these simple classes :
public class Worker
{
public int WorkerId { get; set; }
public String Name { get; set; }
public int EnterpriseId { get; set; } // ForeignKey for Enterprise
public Enterprise Enterprise { get; set; }
}
public class Enterprise
{
public int EnterpriseId { get; set; }
public String Name { get; set; }
public Worker Manager { get; set; }
public List<Worker> Workers { get; set; }
}
I'd like these classes to result in the following DB structure :
Table Worker
WorkerId (PK, int, not null)
Name (varchar(128), not null)
EnterpriseId (FK, int)
Table Enterprise
EnterpriseId (PK, int, not null)
Name (varchar(128), not null)
Manager (FK, int)
I tried many things with modelBuilder, but I never obtain what I want.
Is there a solution with Fluent API to do what I want to do?
Thank you very much for your help.
This will not get you what you want (in Db) - but is what I recommend...
public ICollection<Worker> Workers { get; set; } // instead of List<>
// ...
modelBuilder.Entity<Worker>()
.HasRequired(x => x.Enterprise)
.WithMany(x => x.Workers)
.HasForeignKey(x => x.EnterpriseId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Enterprise>()
.HasOptional(x => x.Manager)
.WithOptionalPrincipal() // x => x.DefaultForEntity)
.WillCascadeOnDelete(false);
You can use it like:
var enterprise = new Enterprise { Manager = new Worker { Name = "Manager", }, };
enterprise.Workers = new[]
{
enterprise.Manager,
new Worker{ Name = "Worker1", },
new Worker{ Name = "Worker2", },
new Worker{ Name = "Worker3", },
new Worker{ Name = "Worker4", },
new Worker{ Name = "Worker5", },
};
db.Enterprises.Add(enterprise);
db.SaveChanges();
var enterprises = db.Enterprises.ToList();
This is exactly what you want...
modelBuilder.Entity<Worker>()
.HasRequired(x => x.Enterprise)
.WithMany(x => x.Workers)
.HasForeignKey(x => x.EnterpriseId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Enterprise>()
.HasKey(x => x.EnterpriseId);
modelBuilder.Entity<Enterprise>()
.HasOptional(x => x.Manager)
.WithOptionalDependent() // x => x.DefaultForEntity)
.WillCascadeOnDelete(false);
...but will not work - due to cyclical references (EF error).
Here is a pretty detailed example for a similar / identical solution...
Entity Framework One-to-Many with Default
I don't know how you intend to get the manager object, but my guess is you need to use Inheritance to make your design optimal. Try this:
public abstract class Employee
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EmployeeId{ get; set; }
public String Name { get; set; }
[ForeignKey("Enterprise"), DatabaseGenerated(DatabaseGeneratedOption.None)]
public int EnterpriseId { get; set; } // ForeignKey for Enterprise
public Enterprise Enterprise { get; set; }
}
[Table("Workers")] // Table per Type (TPT), This will be your Table name in your database
public class Worker : Employee
{
//Add properties only related to workers
}
[Table("Managers")] // Table per Type (TPT). This will be your Table name in your database
public class Manager : Employee
{
//Add properties only related to Managers
}
public class Enterprise
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EnterpriseId { get; set; }
public String Name { get; set; }
public virtual ICollection<Employee> Employees{ get; set; }
}
Note: Sorry this is done using Property Mapping
Link: Here is a link to simple Fluent Mapping example
Link : Read about Table per Type (TPT) Inheritance here

Code first, customizing the join table [duplicate]

I have this scenario:
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; }
}
How do I configure my association with fluent API? Or is there a better way to create the association table?
It's not possible to create a many-to-many relationship with a customized join table. In a many-to-many relationship EF manages the join table internally and hidden. It's a table without an Entity class in your model. To work with such a join table with additional properties you will have to create actually two one-to-many relationships. It could look like this:
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; }
}
If you now want to find all comments of members with LastName = "Smith" for example you can write a query like this:
var commentsOfMembers = context.Members
.Where(m => m.LastName == "Smith")
.SelectMany(m => m.MemberComments.Select(mc => mc.Comment))
.ToList();
... or ...
var commentsOfMembers = context.MemberComments
.Where(mc => mc.Member.LastName == "Smith")
.Select(mc => mc.Comment)
.ToList();
Or to create a list of members with name "Smith" (we assume there is more than one) along with their comments you can use a projection:
var membersWithComments = context.Members
.Where(m => m.LastName == "Smith")
.Select(m => new
{
Member = m,
Comments = m.MemberComments.Select(mc => mc.Comment)
})
.ToList();
If you want to find all comments of a member with MemberId = 1:
var commentsOfMember = context.MemberComments
.Where(mc => mc.MemberId == 1)
.Select(mc => mc.Comment)
.ToList();
Now you can also filter by the properties in your join table (which would not be possible in a many-to-many relationship), for example: Filter all comments of member 1 which have a 99 in property Something:
var filteredCommentsOfMember = context.MemberComments
.Where(mc => mc.MemberId == 1 && mc.Something == 99)
.Select(mc => mc.Comment)
.ToList();
Because of lazy loading things might become easier. If you have a loaded Member you should be able to get the comments without an explicit query:
var commentsOfMember = member.MemberComments.Select(mc => mc.Comment);
I guess that lazy loading will fetch the comments automatically behind the scenes.
Edit
Just for fun a few examples more how to add entities and relationships and how to delete them in this model:
1) Create one member and two comments of this member:
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) Add a third comment of 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) Create new member and relate it to the existing comment2:
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) Create relationship between existing member2 and comment3:
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) Delete this relationship again:
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) Delete member1 and all its relationships to the comments:
var member1 = context.Members.Where(m => m.FirstName == "Pete")
.SingleOrDefault();
if (member1 != null)
{
context.Members.Remove(member1);
context.SaveChanges();
}
This deletes the relationships in MemberComments too because the one-to-many relationships between Member and MemberComments and between Comment and MemberComments are setup with cascading delete by convention. And this is the case because MemberId and CommentId in MemberComment are detected as foreign key properties for the Member and Comment navigation properties and since the FK properties are of type non-nullable int the relationship is required which finally causes the cascading-delete-setup. Makes sense in this model, I think.
I'll just post the code to do this using the fluent API mapping.
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; }
}
On your DbContext derived class you could do this:
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)
}
}
It has the same effect as the accepted answer, with a different approach, which is no better nor worse.
The code provided by this answer is right, but incomplete, I've tested it. There are missing properties in "UserEmail" class:
public UserTest UserTest { get; set; }
public EmailTest EmailTest { get; set; }
I post the code I've tested if someone is interested.
Regards
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
I want to propose a solution where both flavors of a many-to-many configuration can be achieved.
The "catch" is we need to create a view that targets the Join Table, since EF validates that a schema's table may be mapped at most once per EntitySet.
This answer adds to what's already been said in previous answers and doesn't override any of those approaches, it builds upon them.
The model:
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; }
}
The configuration:
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);
}
}
The context:
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);
}
}
From Saluma's (#Saluma) answer
If you now want to find all comments of members with LastName =
"Smith" for example you can write a query like this:
This still works...
var commentsOfMembers = context.Members
.Where(m => m.LastName == "Smith")
.SelectMany(m => m.MemberComments.Select(mc => mc.Comment))
.ToList();
...but could now also be...
var commentsOfMembers = context.Members
.Where(m => m.LastName == "Smith")
.SelectMany(m => m.Comments)
.ToList();
Or to create a list of members with the name "Smith" (we assume there is
more than one) along with their comments you can use a projection:
This still works...
var membersWithComments = context.Members
.Where(m => m.LastName == "Smith")
.Select(m => new
{
Member = m,
Comments = m.MemberComments.Select(mc => mc.Comment)
})
.ToList();
...but could now also be...
var membersWithComments = context.Members
.Where(m => m.LastName == "Smith")
.Select(m => new
{
Member = m,
m.Comments
})
.ToList();
If you want to remove a comment from a member
var comment = ... // assume comment from member John Smith
var member = ... // assume member John Smith
member.Comments.Remove(comment);
If you want to Include() a member's comments
var member = context.Members
.Where(m => m.FirstName == "John", m.LastName == "Smith")
.Include(m => m.Comments);
This all feels like syntactic sugar, however, it does get you a few perks if you're willing to go through the additional configuration. Either way, you seem to be able to get the best of both approaches.
I've come back here a couple times now, but it seems that EF Core has done a few updates in the past decade, so here's where I'm at currently with setting up many-to-many with custom join entity:
public class MemberModel
{
public int MemberId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public ICollection<CommentModel> Comments { get; set; }
}
public class CommentModel
{
public int CommentId { get; set; }
public string Message { get; set; }
public ICollection<MemberModel> Members { get; set; }
}
public class MemberCommentModel
{
public int Something { get; set; }
public string SomethingElse { get; set; }
public int MembersId { get; set; }
[ForeignKey("MembersId")]
public MemberModel Member { get; set; }
public int CommentsId { get; set; }
[ForeignKey("CommentsId")]
public CommentModel Comment { get; set; }
}
Then in your OnModelCreating:
//Allows access directly from Comments or Members entities to the other
builder.Entity<MemberModel>()
.HasMany(x => x.Comments)
.WithMany(x => x.Members)
.UsingEntity<MemberCommentModel>();
//Defines the actual relationships for the middle table
builder.Entity<MemberCommentModel>()
.HasOne(x => x.Comment)
.WithOne()
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<MemberCommentModel>()
.HasOne(x => x.Member)
.WithOne()
.OnDelete(DeleteBehavior.NoAction);
TLDR; (semi-related to an EF editor bug in EF6/VS2012U5) if you generate the model from DB and you cannot see the attributed m:m table: Delete the two related tables -> Save .edmx -> Generate/add from database -> Save.
For those who came here wondering how to get a many-to-many relationship with attribute columns to show in the EF .edmx file (as it would currently not show and be treated as a set of navigational properties), AND you generated these classes from your database table (or database-first in MS lingo, I believe.)
Delete the 2 tables in question (to take the OP example, Member and Comment) in your .edmx and add them again through 'Generate model from database'. (i.e. do not attempt to let Visual Studio update them - delete, save, add, save)
It will then create a 3rd table in line with what is suggested here.
This is relevant in cases where a pure many-to-many relationship is added at first, and the attributes are designed in the DB later.
This was not immediately clear from this thread/Googling. So just putting it out there as this is link #1 on Google looking for the issue but coming from the DB side first.
One way to solve this error is to put the ForeignKey attribute on top of the property you want as a foreign key and add the navigation property.
Note: In the ForeignKey attribute, between parentheses and double quotes, place the name of the class referred to in this way.

Resources