Filtering related tables in EF Core 2.0 - asp.net

I'm working at my first project in .Net Core 2.0. It is a simple blog system. I want to add search functionality based on post title and tags.
My entities:
public class Post
{
public int ID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Body { get; set; }
public int CategoryID { get; set; }
public DateTime ReleaseDate { get; set; }
public string ImageName { get; set; }
public Category Category { get; set; }
public ICollection<PostTag> PostTags { get; } = new List<PostTag>();
}
public class PostTag
{
public int PostID { get; set; }
public int TagID { get; set; }
public Post Post { get; set; }
public Tag Tag { get; set; }
}
public class Tag
{
public int TagID { get; set; }
public string Name { get; set; }
public int Counter { get; set; }
public ICollection<PostTag> PostTags { get; } = new List<PostTag>();
public Tag()
{
Counter = 1;
}
So far I have come up with something like that. I joined the Tag table to be able to view all the tags for each post in IndexView.
public async Task<IActionResult> Index(int? page, string searchString)
{
IQueryable<Post> posts = _context.Posts
.OrderByDescending(post => post.ReleaseDate)
.Include(post => post.Category)
.Include(post => post.PostTags)
.ThenInclude(pt => pt.Tag);
//SEARCH
if (!String.IsNullOrEmpty(searchString))
{
posts = posts.Where(post => post.PostTags.Any(pt => pt.Tag.Name.Contains(searchString)) || post.Title.Contains(searchString));
//POPULARITY INCREESE
var tag = _context.Tags.SingleOrDefault(t => t.Name == searchString);
if (tag != null)
{
tag.Counter += 1;
_context.Update(tag);
_context.SaveChanges();
}
}
int pageSize = 4;
return View("Index", await PaginatedList<Post>.CreateAsync(posts.AsNoTracking(), page ?? 1, pageSize));
}
It's wroking but I would like to know if there is a simpler or better way.
And will .Where function work when i dont include related tables?

First of all the answer to your last question: As long as you didn´t send any request to the database, you don´t need Include() for filtering.
If you want to populate a list of entities, and you want to access Navigation-Properties e.g. while iterating over the list, you need to use Include().
If you want to avoid using Include(), you should select the values you need. This will avoid unexpected behaviour with NavigationProperties or something like this too. I would do something like this:
IQueryable<Post> posts = _context.Posts.OrderByDescending(post => post.ReleaseDate);
//SEARCH
if (!String.IsNullOrEmpty(searchString))
{
posts = posts.Where(post => post.PostTags.Any(pt => pt.Tag.Name.Contains(searchString)) || post.Title.Contains(searchString));
//POPULARITY INCREESE
var tag = _context.Tags.SingleOrDefault(t => t.Name == searchString);
if (tag != null)
{
tag.Counter += 1;
_context.Update(tag);
_context.SaveChanges();
}
}
int pageSize = 4;
return View("Index", await PaginatedList<Post>.CreateAsync(posts.AsNoTracking().Select(e=> new YourDisplayObject { DiplayValue = e.DbValue, DisplayNavProp = e.NavProp }, page ?? 1, pageSize));

Related

Adding and Removing an Object from a List of Objects using HTTP PATCH

I am implementing a PATCH endpoint using ASP.NET Core and EF7 for two models related by a many-to-many relationship and encountered two errors when trying to add or remove an object from a list of objects
I have these Note and Tag model classes:
public class Note
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public string? Title { get; set; }
[Required]
public string Description { get; set; }
public List<Tag>? Tags { get; set; }
public bool Deleted { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ChangedDate { get; set; }
}
public class Tag
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int TagID { get; set; }
[Required]
public string TagName { get; set; }
public List<Note>? Notes { get; set; }
public DateTime CreatedDate { get; set; }
}
They are related by a many-to-many relation using a join table configured with Fluent API. The DbContext is provided below
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<Note> Notes { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Note>()
.HasMany(n => n.Tags)
.WithMany(t => t.Notes)
.UsingEntity(j => j
.ToTable("NoteTag")
.HasData
(
new {NotesID = 1, TagsTagID = 1},
new {NotesID = 2, TagsTagID = 2}
));
modelBuilder.Entity<Tag>().HasData
(
new Tag
{
TagID= 1,
TagName="testTag1",
CreatedDate= DateTime.Now
},
new Tag
{
TagID= 2,
TagName = "testTag2",
CreatedDate= DateTime.Now
}
);
modelBuilder.Entity<Note>().HasData
(
new Note
{
ID= 1,
Title="testTitle1",
Description="testDescription1",
Deleted=false,
CreatedDate= DateTime.Now,
ChangedDate= DateTime.Now,
},
new Note
{
ID = 2,
Title = "testTitle2",
Description = "testDescription2",
Deleted = false,
CreatedDate = DateTime.Now,
ChangedDate = DateTime.Now,
}
);
}
I have a PATCH endpoint to update Note as below
[HttpPatch("{id:int}", Name = "UpdateNote")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult UpdateNote(int id, JsonPatchDocument<Note> patchNoteDTO)
{
if (id == 0 || patchNoteDTO == null)
return BadRequest();
var note = _db.Notes.Include(x=>x.Tags).AsNoTracking().FirstOrDefault(u => u.ID == id);
if (note == null)
return NotFound();
patchNoteDTO.ApplyTo(note, ModelState);
if (!ModelState.IsValid)
return BadRequest(ModelState);
note.ChangedDate = DateTime.Now;
_db.Update(note);
_db.SaveChanges();
return NoContent();
}
Error 1: When I am trying to add a new Tag to the List of Tags using PATCH (Add Patch Doc), it throws an error (Error Message) as it is trying to enter a duplicate record in NoteTag (Join) table even though I'm adding a Tag of ID 2
Error 2: Not exactly an error. When I try to remove an existing Tag from the List using PATCH (Remove Patch Doc), it runs but the corresponding entry in NoteTag(Join) table is not removed (NoteTag). I'm assuming EF should handle this automatically as I cannot access NoteTag table to do any modifications
Any solutions to fix these two errors to get the desired behavior?

How to check if tag exisits before adding the new one to database [ASP MVC]

How I can check if the tag is exists in the database before adding a new one ?? also if the tag exists I don't want to duplicate it just link the tag id with the book id ..
Here is the book class :
public partial class Book
{
[Key]
public int Book_id { get; set; }
[Required]
public string User_ID { get; set; }
public string UrlSlug { get; set; }
[Required]
public string Book_name { get; set; }
public int Edition { get; set; }
public int Category_id { get; set; }
public DateTime Publish_date { get; set; }
public string Author_name { get; set; }
public string Book_Image { get; set; }
public string Download_Link { get; set; }
public string pdf_file { get; set; }
[AllowHtml]
public string Book_Description { get; set; }
public int View_Count { set; get; }
public virtual string TagsListing { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
public virtual Category Category { get; set; }
}
Tag class :
public class Tag
{
public virtual int Id
{ get; set; }
public virtual string Name
{ get; set; }
public virtual string UrlSlug
{ get; set; }
public virtual ICollection<Book> Books { get; set; }
}
My Create (New book controller):
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(Book model)
{
if (ModelState.IsValid)
{
if(model.TagsListing != null)
{
Collection<Tag> TagBooks = new Collection<Tag>();
var s = model.TagsListing.ToString();
string[] newTags = s.Split(',');
foreach (var val in newTags)
{
Tag iTag = new Tag
{
Name = val,
UrlSlug=val,
};
db.Tags.Add(iTag);
TagBooks.Add(iTag);
}
model.Tags = TagBooks;
}
db.Books.Add(model);
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(model);
}
My Create View :
<div class="form-group">
<h4>#Html.LabelFor(model => model.TagsListing, htmlAttributes: new { #class = "control-label col-md-3"})</h4>
<div class="col-md-6">
#Html.EditorFor(model => model.TagsListing, new { htmlAttributes = new { #class = "alert-info form-control",#id= "tags"} })
#Html.ValidationMessageFor(model => model.TagsListing, "", new { #class = "text-danger" })
</div>
</div>
If that possible , please let me know and how to do that .
Thanks
You need to look up the tags from the database based on the posted values:
var tags = db.Tags.Where(m => newTags.Contains(m.Name)).ToList();
Then, you need to manually remove tags from the collection that were removed and add tags that were added:
// Remove deselected tags
model.Tags.Where(t => !tags.Contains(t)).ToList()
.ForEach(t => model.Tags.Remove(t));
// Add newly selected tags
tags.Where(t => !model.Tags.Contains(t)).ToList()
.ForEach(t => model.Tags.Add(t));
EDIT
Since this is a create view, the book has no tags yet. I gave you the code you would need to modify an existing book, since that is the more complicated scenario worthy of a code sample. For a brand new book, you'd simply set the tags directly to the book's property, i.e. model.Tags = tags.
EDIT:
Leaving this since it got the questioner started and was a bit easier for them to read. However, there are much more efficient ways to go about this as other answers show.
Be careful of performance since you're doing a string match. I'm also assuming you just want to be unique on the name. If not, just compare whatever columns you want in the FirstOrDefault statement.
All Tags:
foreach (var val in newTags)
{
var iTag = db.Tags.FirstOrDefault(t => t.Name == val);
if(iTag == null)
{
iTag = new Tag
{
Name = val,
UrlSlug=val,
};
db.Tags.Add(iTag);
}
TagBooks.Add(iTag);
}
model.Tags = TagBooks;
db.Save();

Send info to the view from another table with navigation property?

This is my models
public class Product
{
[Key]
public int SerialNumber { get; set; }
public int PartNumber { get; set; }
public string Description { get; set; }
public virtual ICollection<Reading> Reading { get; set; }
}
public class Reading
{
[Key]
public int Id { get; set; }
public int SerialNumber { get; set; }
public int ReadingValue { get; set; }
public virtual Product Product { get; set; }
}
I can send all products to the view with
return View(db.Products.ToList().Where(product => product.CustomerID == Customer));
And I can get the latest ReadingValue if I know the Product SerialNumber
var LatestReading = db.Readings.OrderByDescending(m => m.Id).Where(s => s.SerialNumber == SerialNumber).Select(m => m.ReadingValue).FirstOrDefault();
How can I send all the products to the view with the latest ReadingValue for each product?
Create a new view model that will hold both the data:
public class FooViewModel
{
public List<Product> Products { get; set; }
public Reading LatestReading { get; set; }
}
Change your view to use the new model with:
#model FooViewModel
Then send them back in your controller:
var model = new FooViewModel();
model.Products = db.Products.ToList().Where(product => product.CustomerID == Customer);
model.LatestReading = db.Readings.OrderByDescending(m => m.Id).Where(s => s.SerialNumber == SerialNumber).Select(m => m.ReadingValue).FirstOrDefault();
return View(model);
Because you have Reading property in Products class, you can get the latest ReadingValue in the view:
foreach(Product product in Model)
{
var latestReadingValue = product.Reading.OrderByDescendin(m => m.Id).FirstOrDefault();
// do what you want here
}
but as hutchonoid points out the better option is creating a ViewModel for it, because having logic in the view is a bad practice, and it doesn't correspond to MVC pattern.

MVC 4 - many to many - get data only gets id of related table

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)

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