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.
Related
I want to create a query for EFCore 5 using projection from Automapper 10.1.1 that will conditionally include associations.
If I were to write this in a select statement, I would do the following:
_dbContext.Employees
.Select(x => new EmployeeDto()
{
Id = x.Id,
Contract = includeContract ? new ContractDto()
{
Id = x.Contract.Id
} : null
})
.FirstAsync();
I attempted it like so:
Models
//Source
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public Contract ActiveContract { get; set; }
}
public class Contract
{
public int Id { get; set; }
public DateTimeOffset StartDate { get; set; }
public DateTimeOffset EndDate { get; set; }
public Employee Employee { get; set; }
public int EmployeeId { get; set; }
}
//Destination
public class EmployeeDto
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public ContractDto ActiveContract { get; set; }
}
public class ContractDto
{
public int Id { get; set; }
public DateTimeOffset StartDate { get; set; }
public DateTimeOffset EndDate { get; set; }
}
AutoMapper Profile
public class EmployeeProfile : Profile
{
public EmployeeProfile()
{
bool includeContract = false;
CreateMap<Employee, EmployeeDto>()
.ForMember(x => x.ActiveContract, opt => opt.MapFrom(x => includeContract ? x.ActiveContract : null));
}
}
Instead of null, I've tried default(Contract) and (Contract)null these produce the same result.
Also I have a mapping for Contracts, just a simple <Contract, ContractDto>.
Query
bool includeContract = someCondition;
var result = await _dbContext.Employees
.ProjectTo<EmployeeDto>(_mapper.ConfigurationProvider, new { includeContract })
.FirstAsync(x => x.id == id);
My expected result is, I will be returned a EmployeeDto with the Contract being null, unless the includeContract is set to true.
However if the includeContract is false, the following error is thrown:
System.InvalidOperationException: Expression 'NULL' in SQL tree does not have type mapping assigned
the query expression produced:
Compiling query expression:
'DbSet<Employee>()
.Select(dtoEmployee => new EmployeeDto{
Age = dtoEmployee.Age,
Id = dtoEmployee.Id,
Name = dtoEmployee.Name,
ActiveContract = null == null ? null : new ContractDto{
Id = null.Id,
StartDate = null.StartDate,
EndDate = null.EndDate
}
}
)
.First(x => x.Id == __id_0)'
I know that is achievable by explicitly defining the expression within a ConvertUsing, but would like to avoid writing out my whole DTOs if possible.
Referring to the documentation linked via #Lucian in their comment.
The solution was to the adjust the AutoMapper config to the following:
CreateMap<Employee, EmployeeDto>()
.ForMember(x => x.ActiveContract, opt => opt.ExplicitExpansion())
And to adjust the query to the following:
var result = await _dbContext.Employees
.ProjectTo<EmployeeDto>(_mapper.ConfigurationProvider,
null,
dest => dest.ActiveContract
)
.FirstAsync(x => x.id == id);
I have
class User {
...
...
ICollection<Transaction> transactionsUserMade;
}
and
class Transaction {
int ID;
int userThatSentMoneyID;
int userToWhomHeSentMoneyID;
}
I'm trying to make profile page where user can see all transactions he made and to whom. I managed to relate users and transaction but I'm getting integer values, as i should by using
await _context.Users
.Include(u => u.transactionsUserMade)
.AsNoTracking()
.FirstOrDefaultAsync(u => u.ID == userId);
How can i turn those ID's to actual objects of Users so i could get their usernames and display them on Razor Page.
Found one solution. I tweaked Transaction class by adding User userThatRecievedMoney property. And after getting transactions from specific user i manually set that property.
foreach(var transaction in _user.transactionsUserMade)
{
transaction.userThatRecievedMoney = _context.Users
.Where(u => u.ID == transaction.userToWhomHeSentMoneyID).FirstOrDefault();
}
You can use Navigation Property to help you with that as long as you can modify those entity models User and Transaction.
public class UserEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<TransactionEntity> TransactionsAsSender { get; set; }
public List<TransactionEntity> TransactionsAsRecipient { get; set; }
}
public class TransactionEntity
{
public int Id { get; set; }
public double Amount { get; set; }
public string Note { get; set; }
// Foreign key to UserEntity
public int SenderId { get; set; }
public UserEntity Sender { get; set; }
// Foreign key to UserEntity
public int RecipientId { get; set; }
public UserEntity Recipient { get; set; }
}
Then you need to setup their relationships.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<UserEntity>(b => {
b.HasKey(x => x.Id);
b.Property(x => x.Name).IsRequired();
b.Property(x => x.Email).IsRequired();
b.ToTable("User");
});
builder.Entity<TransactionEntity>(b => {
b.HasKey(x => x.Id);
b.Property(x => x.Amount).IsRequired();
// Configure relationships
b.HasOne(x => x.Sender)
.WithMany(u => u.TransactionsAsSender)
.HasForeignKey(x => x.SenderId);
b.HasOne(x => x.Recipient)
.WithMany(u => u.TransactionsAsRecipient)
.HasForeignKey(x => x.RecipientId);
b.ToTable("Transaction");
});
}
public DbSet<UserEntity> Users { get; set; }
public DbSet<TransactionEntity> Transactions { get; set; }
}
After their relationships are setup, you can easily query the related data via navigation properties.
For example, let's say you have view model called UserProfileViewModel and UserProfileTransactionViewModel to contain the information it needs for display purpose.
public class UserProfileViewModel
{
public int UserId { get; set; }
public string UserName { get; set; }
public string UserEmail { get; set; }
public IEnumerable<UserProfileTransactionViewModel> TransactionsAsSender { get; set; }
public IEnumerable<UserProfileTransactionViewModel> TransactionsAsRecipient { get; set }
}
public class UserProfileTransactionViewModel
{
public int TransactionId { get; set; }
public string Sender { get; set; }
public string Recipient { get; set; }
public string Amount { get; set; }
}
In the controller,
var user = _dbContext.Users
.AsNoTracking()
.Include(x => x.TransactionsAsSender)
.Include(x => x.TransactionsAsRecipient)
.SingleOrDefault(x => x.Id == userId);
if (user == null)
{
return NotFound();
}
var vm = new UserProfileViewModel
{
UserId = user.Id,
UserName = user.Name,
UserEmail = user.Email,
TransactionsAsSender = user.TransactionsAsSender
.Select(x => new UserProfileTransactionViewModel
{
TransactionId = x.Id,
Sender = x.Sender.Name,
Recipient = x.Recipient.Name,
Amount = x.Amount.ToString("c")
}),
TransactionsAsRecipient = user.TransactionsAsRecipient
.Select(x => new UserProfileTransactionViewModel
{
TransactionId = x.Id,
Sender = x.Sender.Name,
Recipient = x.Recipient.Name,
Amount = x.Amount.ToString("c")
})
};
return View(vm);
You could even have just a list of all transactions off UserProfileViewModel. You can combine TransactionsAsSender and TransactionsAsRecipient from UserEntity to fill the list.
Disclaim:
I wrote everything by hand and with my imagination :p
I'm having some problems trying to use the many-to-many relationship in EF Core 2.0. Here is me code:
Here are my entities:
User
public class User : IdentityUser
{
private User()
{
}
public String Name { get; private set; }
public ICollection<UserCourse> UserCourses { get; private set; }
public static User Create(string name, string username, string email)
{
var instance = new User
{
Id = Guid.NewGuid().ToString(),
};
instance.Update(name, username, email);
return instance;
}
public void Update(string name, string username, string email)
{
Name = name;
UserName = username;
Email = email;
}
public void Update(UserCreatingModel model)
{
this.UserName = model.Username;
this.Name = model.Name;
this.Email = model.Email;
}
public void Update(UserCourse userCourse)
{
if (UserCourses == null)
{
UserCourses = new List<UserCourse>() {userCourse};
}
else
{
UserCourses.Add(userCourse);
}
}
}
Course entity
public class Course
{
private Course() { }
public Guid Id { get; private set; }
public string Name { get; private set; }
public int Year { get; private set; }
public int Semester { get; private set; }
public List<Lesson> Lessons { get; private set; }
public ICollection<UserCourse> UserCourses { get; private set; }
public static Course Create(string name, int year, int semester, List<Lesson> lessons, List<User> professors)
{
var instance = new Course { Id = Guid.NewGuid() };
instance.Update(name, year, semester, lessons);
return instance;
}
public static Course Create(string name, int year, int semester)
{
var instance = new Course { Id = Guid.NewGuid() };
instance.Update(name, year, semester);
return instance;
}
public void Update(string name, int year, int semester, List<Lesson> lessons)
{
Name = name;
Year = year;
Semester = semester;
Lessons = lessons;
}
public void Update(string name, int year, int semester)
{
Name = name;
Year = year;
Semester = semester;
}
public void Update(UserCourse userCourse)
{
if (UserCourses == null)
{
UserCourses = new List<UserCourse>(){userCourse};
}
else
{
UserCourses.Add(userCourse);
}
}
public void Update(List<Lesson> lessons)
{
this.Lessons = lessons;
}
}
Join entity
public class UserCourse
{
private UserCourse() { }
public string UserId { get; private set; }
public User User { get; private set; }
public Guid CourseId { get; private set; }
public Course Course { get; private set; }
public static UserCourse CreateUserCourse(string userId, User user, Guid coursId, Course course)
{
var instance = new UserCourse
{
UserId = userId,
User = user,
CourseId = coursId,
Course = course
};
return instance;
}
}
This is my database context
public sealed class DatabaseContext : IdentityDbContext<User>, IDatabaseContext
{
public static readonly LoggerFactory MyLoggerFactory
= new LoggerFactory(new[] {new ConsoleLoggerProvider((_, __) => true, true)});
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
{
Database.EnsureCreated();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseLoggerFactory(MyLoggerFactory) // Warning: Do not create a new ILoggerFactory instance each time
.EnableSensitiveDataLogging();
public new DbSet<User> Users { get; set; }
public DbSet<Course> Courses { get; set; }
public DbSet<Lesson> Lessons { get; set; }
public DbSet<UserCourse> UserCourses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Lesson>()
.HasOne(p => p.Course)
.WithMany(b => b.Lessons)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UserCourse>()
.HasKey(uc => new {uc.UserId, uc.CourseId});
modelBuilder.Entity<UserCourse>()
.HasOne(uc => uc.User)
.WithMany(u => u.UserCourses)
.HasForeignKey(uc => uc.UserId);
modelBuilder.Entity<UserCourse>()
.HasOne(uc => uc.Course)
.WithMany(c => c.UserCourses)
.HasForeignKey(uc => uc.CourseId);
}
}
This is the repository where I try to add the 2 existing entities user and course to the joining table with the method AddCoursToProfessor
public class CoursesRepository : ACrudRepository<Course, Guid>, ICoursesRepository
{
public CoursesRepository(IDatabaseContext databaseContext) : base(databaseContext)
{
}
public override IReadOnlyList<Course> GetAll() => _databaseContext.Courses.Include(c => c.Lessons).Include(p => p.UserCourses).ToList();
public override Course GetById(Guid id)
=> _databaseContext.Courses.Include(c => c.Lessons).AsNoTracking().FirstOrDefault(c => c.Id.Equals(id));
public void AddCoursToProfessor(string profId, Guid coursId)
{
var professor = _databaseContext.Users.Include(u => u.UserCourses).FirstOrDefault(u => u.Id.Equals(profId));
var course = GetById(coursId);
var profCourse = UserCourse.CreateUserCourse(profId, professor, coursId, course);
professor.Update(profCourse);
_databaseContext.Users.Update(professor);
_databaseContext.SaveChanges();
}
}
The problem seems to be that when trying to add in the joining table, EF tries to add the course entity into it's table again and I get primary key violation error. I tried different approaches and none of them seem to work. I tried adding directly in UserCourse table but that would try to add both entities into their own tables, I tried deleting the entities before adding them to the join table, that didn't work either. I ran out of ideas, if someone has other ideas, or dealt with similar situations that would be much of help.
I forgot to mention that if I try to add a course by it self or an user, that would work, they both would be added to their tables, so I don't think that the problem is with the DB but with the configuration of the many-to-many relatonship
Ok... so I recently solved the problem. The problem was generated by the GetCourseById method because I was getting the entity AsNoTracking. The entity was not under the EF scope so it tried to create it.
As far as I know here's how I implement a MTM model.
In this instance, there are 3 parameters to take note.
CurrencyPairId is not unique. its a 2-unique parameter.
CurrencyId and IsMain is unique.
This is a trading exchange-styled composite key. i.e. EURUSD
EUR => Main Pair, USD => Counter Pair.
The class
/// <summary>
/// Partial currency pair.
/// </summary>
public class PartialCurrencyPair
{
public long CurrencyId { get; set; }
public long CurrencyPairId { get; set; }
public bool IsMain { get; set; } = false;
public CurrencyPair CurrencyPair { get; set; }
public Currency Currency { get; set; }
}
Currency
public class Currency : BaseEntityModel
{
[Key]
public long Id { get; set; }
public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }
}
CurrencyPair
public class CurrencyPair : BaseEntityModel
{
[Key]
public long Id { get; set; }
// =========== RELATIONS ============ //
public ICollection<PartialCurrencyPair> PartialCurrencyPairs { get; set; }
}
Some of the mappings
in Currency;
entity.HasMany(c => c.PartialCurrencyPairs).WithOne(pcp => pcp.Currency).HasForeignKey(pcp => pcp.CurrencyId).OnDelete(DeleteBehavior.Restrict);
in CurrencyPair
entity.HasMany(cp => cp.PartialCurrencyPairs).WithOne(pcp => pcp.CurrencyPair).HasForeignKey(pcp => pcp.CurrencyPairId).OnDelete(DeleteBehavior.Restrict);
PartialCurrencyPair
builder.Entity<PartialCurrencyPair>(entity =>
{
entity.HasKey(pcp => new { pcp.CurrencyPairId, pcp.IsMain }).HasName("PartialCurrencyPair_CK_CurrencyPairId_IsMain");
});
Judging by your Course.cs, there's no Collection for UserCourse and why is it static?
Update, 2021 APR 6
EF Core has official documentation for the current practise for implementing MTM.
https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#many-to-many
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);
Adding new data to the database is working with the many to many relationship.
Now I'm trying to get the project with all categories (category includes id and name). When I get all the project from my database then the associated categories are filled in, but only the id's.
CLASSES
public class Project
{
public Project() {
Categories = new HashSet<Category>();
}
public int ProjectID { get; set; }
[Display(Name = "Titel")]
public int CompanyID { get; set; }
public virtual Company Company { get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<Project> Projects { get; set; }
}
CONTEXT
/*************ProjectS**************/
modelBuilder.Entity<Project>().HasKey(t => t.ProjectID);
modelBuilder.Entity<Project>().ToTable("Project", "freelauncher");
modelBuilder.Entity<Project>().Property(t => t.ProjectID).HasColumnName("project_id").HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Project>().HasMany(p => p.Categories)
.WithMany(cat => cat.Projects)
.Map(pc =>
{
pc.ToTable("category_has_project");
pc.MapLeftKey("project_id");
pc.MapRightKey("category_id");
}
);
/*************CATEGORY**************/
modelBuilder.Entity<Category>().HasKey(t => t.CategoryID);
modelBuilder.Entity<Category>().ToTable("category", "freelauncher");
modelBuilder.Entity<Category>().Property(t => t.CategoryID).HasColumnName("category_id");
modelBuilder.Entity<Category>().Property(t => t.CategoryName).HasColumnName("category_name");
CONTROLLER
public ActionResult Projects()
{
IEnumerable<Project> projects = Adapter.ProjectRepository.Get();
return View();
}
REPOSITORY
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
What am I doing wrong to get the CategoryName in my projects?
The code works perfectly, the Category Names were missing from the db. (see comment above)