EF Core 3.18 get sum and count from related table - .net-core

I have a web api where I am trying to get sum and count of a related table. Using .net core 3 and EF Core 3.1.8.
This is what I have tried:
_context.Books
.Include(r => r.BookCategories)
.Include(r => r.Resources)
.Include(r => r.Ratings.GroupBy(g => g.Bookid).Select(s => new { SumAllVotes = s.Sum(item => item.Rating) }))
.ToListAsync();
But I just get an error message. (see below).
I find it difficault debugging with EF Core as I dont know where it is going wrong. Have been trying a couple of hours, but whatever I write I get the same error message.
Thought maybe you guys were able to see what was wrong.
What I want
I am trying to get Sum of all Rating inside table Ratings.
Rating contains only 0 or 1. And I am trying to sum ratings on each bookid. I wanted to have it in this class public int SumAllVotes { get; set; }.
Because I list out all Books...and one of the properties will then be SumAllVotes. (And also CountAllVotes, when I have finished this problem).
By the end I will have a SumAllVotes and CountAllVotes and can calculate the percentage of how many have pressed "1".
Error message:
An unhandled exception occurred while processing the request.
InvalidOperationException: Lambda expression used inside Include is
not valid.
Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ProcessInclude(NavigationExpansionExpression
source, Expression expression, bool thenInclude)
What I have tried:
[HttpGet]
public async Task<ActionResult<IEnumerable<Books>>> GetBooks()
{
Guid userid = Guid.Parse(this.User.FindFirst(ClaimTypes.NameIdentifier).Value);
return await _context.Books
.Include(r => r.BookCategories)
.Include(r => r.Resources)
.Include(r => r.Ratings.GroupBy(g => g.Bookid).Select(s => new { SumAllVotes = s.Sum(item => item.Rating) }))
.ToListAsync();
}
Books and Ratings are defined as -
public partial class Books
{
public Books()
{
Bookmarks = new HashSet<Bookmarks>();
Comments = new HashSet<Comments>();
Favourites = new HashSet<Favourites>();
BookCategories = new HashSet<BookCategories>();
Resources = new HashSet<Resources>();
Ratings = new HashSet<Ratings>();
}
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public byte Scaleoffun { get; set; }
public byte Scaleoflearning { get; set; }
public int? Goal { get; set; }
public int? Secondgoal { get; set; }
public int? Thirdgoal { get; set; }
public int? Subjectid { get; set; }
public int? Categoryid { get; set; }
public string Language { get; set; }
public string Estimatedtime { get; set; }
public string Image { get; set; }
public int? File { get; set; }
public int? Ownerid { get; set; }
public DateTime Createdon { get; set; }
public DateTime? Lastmodifiedon { get; set; }
public string Active { get; set; }
public string Url { get; set; }
public Guid Userid { get; set; }
public byte? Grade { get; set; }
[NotMapped]
public int SumAllVotes { get; set; }
[NotMapped]
public int CountAllVotes { get; set; }
public virtual Categories Category { get; set; }
public virtual Curriculum GoalNavigation { get; set; }
public virtual Users Owner { get; set; }
public virtual Curriculum SecondgoalNavigation { get; set; }
public virtual Subjects Subject { get; set; }
public virtual Curriculum ThirdgoalNavigation { get; set; }
public virtual ICollection<Bookmarks> Bookmarks { get; set; }
public virtual ICollection<Comments> Comments { get; set; }
public virtual ICollection<Favourites> Favourites { get; set; }
public virtual ICollection<BookCategories> BookCategories { get; set; }
public virtual ICollection<Resources> Resources { get; set; }
public virtual ICollection<Ratings> Ratings { get; set; }
}
public partial class Ratings
{
public int Id { get; set; }
public int? Bookid { get; set; }
public string Type { get; set; }
public Int16? Rating { get; set; }
public Guid Userid { get; set; }
public string Subject { get; set; }
public DateTime Createdon { get; set; }
public DateTime? Modifiedon { get; set; }
public byte? Active { get; set; }
public virtual Books Book { get; set; }
//public virtual Users User { get; set; }
}
These are some other solutions I have tried, but got the same error message:
.Include(r=> r.Ratings.Sum(i=>i.Rating))
and
.Include(r => new { m = r.Ratings.GroupBy(g => g.Bookid) })

You don't need to group child entities by parent's Id. When you Include one-to-many child entities, they are added to their parent's child list, and hence grouped by their parent's identity, based on the relationship between them. All you need to do is tell EF what values you want from that child list.
Change your query to -
_context.Books
.Include(r => r.BookCategories)
.Include(r => r.Resources)
.Include(r => r.Ratings)
.Select(p => new
{
// set ALL the primitive properties from Books entity
Id = p.Id,
Title = p.Title,
// etc ...
// set the computed properties
CountAllVotes = p.Ratings.Count,
SumAllVotes = p.Ratings.Sum(x => x.Rating)
// set the related entities
BookCategories = p.BookCategories,
Resources = p.Resources
})
.ToListAsync();
AutoMapper has a ProjectTo method that generates the required query and does the projection (the Select part) automatically. You can use that to avoid the hassle of setting all those properties manually.

I suggest you don't use Include with Select. Read article how to make queries with Projection (Select). Note, that Rating.Rating is nullable and you need to handle this. Here is a possible code sample:
var view = await _context.Books
.Where(your condition)
.Select(item => new
{
//Todo: fill other props
SumAllVotes = item.Ratings.Sum(rating => (Int16?) rating.Rating),
CountAllVotes = item.Ratings.Count,
})
.ToListAsync()

Related

Translate LINQ to LAMBDA Entity Framework Core using INCLUDE (NOT JOIN)

How to call multiple entities using Include method (not Join method) in Entity Framework Core? I am trying to translate this LINQ query to EF Core 5 syntax, but I do not know how to call multiple entities and join them together using include method.
var reservations = from reservation in _dbContext.Reservations
join customer in _dbContext.Users on reservation.UserId equals customer.Id
join movie in _dbContext.Movies on reservation.MovieId equals movie.Id
select new
{
Id = reservation.Id,
ReservationTime = reservation.ReservationTime,
CustomerName = customer.Id,
MovieName = movie.Name
};
I tried using multiple include and select method, but do not know how to call multiple entities and join
Here are my models
public class Reservation
{
public int Id { get; set; }
public int Qty { get; set; }
public double Price { get; set; }
public string Phone { get; set; }
public DateTime ReservationTime { get; set; }
public int MovieId { get; set; }
public int UserId { get; set; }
}
public class Movie
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Language { get; set; }
public string Duration { get; set; }
public DateTime PlayingDate { get; set; }
public DateTime PlayingTime { get; set; }
public double TicketPrice { get; set; }
public double Rating { get; set; }
public string Genre { get; set; }
public string TrailorUrl { get; set; }
public string ImageUrl { get; set; }
[NotMapped]
public IFormFile Image { get; set; }
public ICollection<Reservation> Reservations { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string Role { get; set; }
public ICollection<Reservation> Reservations { get; set; }
}
Controller code:
var reservations = _dbContext.Reservations
.Include(r => r.Id)
.Include(c => c.User)
.Select(x => new { x.Id, x.ReservationTime, x.User, x.User.Name });
If add to Reservation navigation properties Movie and User, your query can be simplified. Include cannot be used with Select together, it is ignored by EF translator.
var reservations = _dbContext.Reservations
.Select(r => new
{
Id = r.Id,
ReservationTime = r.ReservationTime,
CustomerName = r.User.Id,
MovieName = r.Movie.Name
});

EF Core 6 Many to many table after scaffold

I've made a dotnet ef scaffold from database and the classes generated were:
public partial class Course
{
public int Id { get; set; }
public string? Description { get; set; }
}
public partial class Student
{
public int Id { get; set; }
public string? Name { get; set; }
}
public partial class StudentCourse
{
public int? IdStudent { get; set; }
public int? IdCourse { get; set; }
public virtual Student? IdStudentNavigation { get; set; }
public virtual Course? IdCourseNavigation { get; set; }
}
I want to get a List of Student where id of Course is X
I've tried _context.Student.Include("StudentCourse").Where(x=>x.Any(....) but Intellisense does not accept "Any" function.
How can i get this ?
Any(...) is a method provided by Enumerable class so you can not use it on a single Student (which is obviously not an Enumerable object).
Your configuration of many-to-many relationship is maybe missing some lines, here is my suggestion:
public partial class Course
{
public int Id { get; set; }
public string? Description { get; set; }
public List<StudentCourse> StudentCourses { get; set; }
}
public partial class Student
{
public int Id { get; set; }
public string? Name { get; set; }
public List<StudentCourse> StudentCourses { get; set; }
}
public partial class StudentCourse
{
public int? IdStudent { get; set; }
public int? IdCourse { get; set; }
public virtual Student? StudentNavigation { get; set; }
public virtual Course? CourseNavigation { get; set; }
}
In Context file:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StudentCourse>()
.HasOne(sc => sc.StudentNavigation)
.WithMany(s => s.StudentCourses)
.HasForeignKey(sc => sc.IdStudent);
modelBuilder.Entity<StudentCourse>()
.HasOne(sc => sc.CourseNavigation)
.WithMany(c => c.StudentCourses)
.HasForeignKey(sc => sc.IdCourse);
}
Finally, your query could be:
IEnumerable<Student> students = await _context.Students
.Include(s => s.StudentCourses)
.Where(s => s.StudentCourses.Any(sc => sc.IdCourse == X)))
I am just taking your code as example but this is not a way you design entity in EF core.
Try following though.
var students
=_context.StudentCourse.Include("IdStudentNavigation").Where(x=>x.IdCourse == 1).Select(x => x.IdStudentNavigation).ToList();
Replace one with your course id.

How to extend Application User to hold a collection of orders?

I'm trying to extend Application User (using Code-First) to hold a collection of orders, but I'm getting errors.
My Order class is
public class Order
{
public Order()
{
OrderDetails = new HashSet<OrderDetails>();
}
public int ID { get; set; }
public DateTime OrderDate { get; set; }
public string UserId { get; set; }
public bool IsDelivered { get; set; }
public bool IsReturned { get; set; }
public virtual ApplicationUser User { get; set; }
public virtual ICollection<OrderDetails> OrderDetails { get; set; }
}
And I'm trying to extend Application user like this
public class ApplicationUser : IdentityUser
{
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
// Add custom user claims here
return userIdentity;
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string CompanyName { get; set; }
public string Profession { get; set; }
public string TaxAuthority { get; set; }
public string TaxNumber { get; set; }
public string Address { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string MobilePhone { get; set; }
public bool NewsLetterSubscribe { get; set; } = false;
public DateTime TimeStamp { get; set; } = DateTime.Now;
public ICollection<Order> Orders { get; set; }
}
And I'm getting the following errors:
QCMS.Models.IdentityUserLogin: : EntityType 'IdentityUserLogin' has no key defined. Define the key for this EntityType.
QCMS.Models.IdentityUserRole: : EntityType 'IdentityUserRole' has no key defined. Define the key for this EntityType.
IdentityUserLogins: EntityType: EntitySet 'IdentityUserLogins' is based on type 'IdentityUserLogin' that has no keys defined.
IdentityUserRoles: EntityType: EntitySet 'IdentityUserRoles' is based on type 'IdentityUserRole' that has no keys defined.
Can you please help me to solve this problem?
UPDATE:
I'm using two db contexts. The one provided for Individual User Account (when the project is first created) and a second one named "qvModel" that is for all other database classes of my project.
public partial class qvModel : DbContext
{
public qvModel()
: base("name=qvModel")
{
}
//APPSETTINGS
public virtual DbSet<AdminLog> AdminLog { get; set; }
public virtual DbSet<WebLog> WebLog { get; set; }
//LANGUAGES
public virtual DbSet<Language> Languages { get; set; }
.
.
.
public virtual DbSet<Order> Orders { get; set; }
public virtual DbSet<OrderDetails> OrderDetails { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Precision attribute for decimals
Precision.ConfigureModelBuilder(modelBuilder);
modelBuilder.Entity<Language>()
.HasMany(e => e.Brochures)
.WithRequired(e => e.Language)
.WillCascadeOnDelete(true);
.
.
.
modelBuilder.Entity<Order>()
.HasMany(c => c.OrderDetails)
.WithRequired(c => c.Order)
.WillCascadeOnDelete(true);
modelBuilder.Entity<ApplicationUser>()
.HasMany(c => c.Orders)
.WithRequired(c => c.User)
.WillCascadeOnDelete(true);
base.OnModelCreating(modelBuilder);
}
}
I found a solution that is very simple.
The solution is to inherit from IdentityDbContext like this
public class qvModel : IdentityDbContext<ApplicationUser>
{
public qvModel()
: base("name=qvModel")
{
}
I was also missing the following line from OnModelCreating
base.OnModelCreating(modelBuilder);
After these changes, my migration is working and I stopped getting the errors I mentioned.

Code-First Entity Framework Multiple Collections with Many to Many

I have another Entity Framework question here. I have a complicated object called Book and that object has a number of collections of type Contributor such as Writer, Letterer, Colorist, etc. Contributors are not necessarily scoped to a particular role though. So the same contributor (with the same ContributorId) could be both a Writer and a Colorist, for example.
public Book {
public ICollection<Contributor> Writers { get; set; }
public ICollection<Contributor> Artists { get; set; }
public ICollection<Contributor> Pencilers { get; set; }
public ICollection<Contributor> Inkers { get; set; }
public ICollection<Contributor> Colorists { get; set; }
public ICollection<Contributor> Letterers { get; set; }
public ICollection<Contributor> CoverArtists { get; set; }
public ICollection<Contributor> OtherContributors { get; set; }
}
public Contributor {
public int ContributorId { get; set; }
public string Name { get; set; }
}
I am having trouble, viewing the examples I have found here and on other sites, determining how I would signify the appropriate model. I would expect a Db Model something like this. What I want to avoid is a model wherein I have a separate table for every Contributor Role, or a separate row in the Contributor table for every instance in which a contributor is associated with a book in any role.
+ Books
--BookId
+ Contributors
--ContributorId
+ BookContributors
--BookId
--ContributorId
--Discriminator
I am such as ADO.NET guy that I am not really finding this too enjoyable, but I am determined to become at least borderline proficient in this important framework.
A Quick Note:
Since opening this question, I got pulled away at work and haven't had the time to thoroughly review the answers and play around with the results. But I didn't want to leave the bounty hanging as I appreciate the answers everyone has provided. So I selected the answer that appeared of the most interest to me starting out. I want to thank everyone though for this.
I have worked on a solution that implements the model you proposed although it works a bit different than what you would expect. Hope this answers your question.
Models
[Table("Book")]
public class Book
{
[Column("BookId")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int BookId { get; set; }
[NotMapped]
public ICollection<Contributor> Writers { get; set; }
[NotMapped]
public ICollection<Contributor> Artists { get; set; }
[NotMapped]
public ICollection<Contributor> Pencilers { get; set; }
[NotMapped]
public ICollection<Contributor> Inkers { get; set; }
[NotMapped]
public ICollection<Contributor> Colorists { get; set; }
[NotMapped]
public ICollection<Contributor> Letterers { get; set; }
[NotMapped]
public ICollection<Contributor> CoverArtists { get; set; }
[NotMapped]
public ICollection<Contributor> OtherContributors { get; set; }
}
[Table("Contributor")]
public class Contributor
{
[Column("ContributorId")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ContributorId { get; set; }
}
// Contributor Type is one of the following options: Writer, Artist, Penciler, etc.
[Table("ContributorType")]
public class ContributorType
{
[Column("ContributorTypeId")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ContributorTypeId { get; set; }
[Column("Name")]
public string Name { get; set; }
}
[Table("BookContributor")]
public class BookContributor
{
[Column("BookContributorId")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int BookContributorId { get; set; }
[Column("BookId")]
public int BookId { get; set; }
[Column("ContributorId")]
public int ContributorId { get; set; }
[Column("RoleId")]
public int RoleId { get; set; }
[ForeignKey("BookId")]
public virtual Book Book { get; set; }
[ForeignKey("ContributorId")]
public virtual Contributor Contributor { get; set; }
[ForeignKey("RoleId")]
public virtual ContributorType Role { get; set; }
}
Database Context
AppDbContext.cs:
public class AppDbContext : DbContext
{
public AppDbContext()
{
Database.SetInitializer<AppDbContext>(new AppDbInitializer());
}
public AppDbContext(string connectionString)
: base(connectionString)
{
Database.SetInitializer<AppDbContext>(new AppDbInitializer());
}
public DbSet<Book> Books { get; set; }
public DbSet<Contributor> Contributors { get; set; }
public DbSet<ContributorType> ContributorTypes { get; set; }
public DbSet<BookContributor> BookContributors { get; set; }
}
AppDbInitializer.cs:
public class AppDbInitializer : DropCreateDatabaseAlways<AppDbContext>
{
protected override void Seed(AppDbContext context)
{
// default contributor types
var contributorTypes = new List<ContributorType>();
contributorTypes.Add(new ContributorType() { Name = "Writer" });
contributorTypes.Add(new ContributorType() { Name = "Artist" });
contributorTypes.Add(new ContributorType() { Name = "Penciler" });
contributorTypes.Add(new ContributorType() { Name = "Inker" });
contributorTypes.Add(new ContributorType() { Name = "Colorist" });
contributorTypes.Add(new ContributorType() { Name = "Letterer" });
contributorTypes.Add(new ContributorType() { Name = "CoverArtist" });
contributorTypes.Add(new ContributorType() { Name = "OtherContributor" });
// adding it to the context
foreach (var type in contributorTypes)
context.ContributorTypes.Add(type);
base.Seed(context);
}
}
Wrapping everything together
Program.cs:
class Program
{
static void Main(string[] args)
{
// enter name of the connection string in App.Config file
var connectionSettings = ConfigurationManager.ConnectionStrings["..."];
using (var dbContext = new AppDbContext(connectionSettings.ConnectionString))
{
// Creating a book
var book = new Book();
dbContext.Books.Add(book);
dbContext.SaveChanges();
// Creating contributor
var contributor = new Contributor();
dbContext.Contributors.Add(contributor);
dbContext.SaveChanges();
// Adding contributor to the book
var bookContributor = new BookContributor()
{
BookId = book.BookId,
ContributorId = contributor.ContributorId,
RoleId = dbContext.ContributorTypes.First(t => t.Name == "Writer").ContributorTypeId
};
dbContext.BookContributors.Add(bookContributor);
dbContext.SaveChanges();
// retrieving a book
var book = dbContext.Books.Where(b => b.BookId == 2).FirstOrDefault();
if (book != null)
{
book.Writers =
from contributor in dbContext.Contributors
join bookContributor in dbContext.BookContributors on contributor.BookId equals bookContributor.BookId
join contributorType in dbContext.ContributorTypes on contributorType.ContributorTypeId equals bookContributor.ContributorTypeId
where
bookContributor.BookId == 2 and
contributorType.Name == "Writer"
select contributor;
// do the same for other types of contributors
}
}
}
}
Create similar collections in the Contributor entity with M:N mapping and use the InverseProperty attribute to declare which collection in Contributor class corresponds with which collection in the Book class.
public class Book
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Contributor> Writers { get; set; }
public virtual ICollection<Contributor> Artists { get; set; }
public virtual ICollection<Contributor> Pencilers { get; set; }
public virtual ICollection<Contributor> Inkers { get; set; }
public virtual ICollection<Contributor> Colorists { get; set; }
public virtual ICollection<Contributor> Letterers { get; set; }
public virtual ICollection<Contributor> CoverArtists { get; set; }
public virtual ICollection<Contributor> OtherContributors { get; set; }
public Book()
{
Writers = new List<Contributor>();
Artists = new List<Contributor>();
Pencilers = new List<Contributor>();
Inkers = new List<Contributor>();
Colorists = new List<Contributor>();
Letterers = new List<Contributor>();
CoverArtists = new List<Contributor>();
OtherContributors = new List<Contributor>();
}
}
public class Contributor
{
public int ContributorId { get; set; }
public string Name { get; set; }
[InverseProperty("Writers")]
public virtual ICollection<Book> WriterOfBooks { get; set; }
[InverseProperty("Artists")]
public virtual ICollection<Book> ArtistOfBooks { get; set; }
[InverseProperty("Pencilers")]
public virtual ICollection<Book> PencilerOfBooks { get; set; }
[InverseProperty("Inkers")]
public virtual ICollection<Book> InkerOfBooks { get; set; }
[InverseProperty("Colorists")]
public virtual ICollection<Book> ColoristOfBooks { get; set; }
[InverseProperty("Letterers")]
public virtual ICollection<Book> LettererOfBooks { get; set; }
[InverseProperty("CoverArtists")]
public virtual ICollection<Book> CoverArtistOfBooks { get; set; }
[InverseProperty("OtherContributors")]
public virtual ICollection<Book> OtherContributorOfBooks { get; set; }
public Contributor()
{
WriterOfBooks = new List<Book>();
ArtistOfBooks = new List<Book>();
PencilerOfBooks = new List<Book>();
InkerOfBooks = new List<Book>();
ColoristOfBooks = new List<Book>();
LettererOfBooks = new List<Book>();
CoverArtistOfBooks = new List<Book>();
OtherContributorOfBooks = new List<Book>();
}
}
The usage is then quite simple:
using (var dc = new MyDbContext())
{
// create sample data
var book1 = new Book() { Name = "Book 1" };
dc.Books.Add(book1);
var contrib1 = new Contributor() { Name = "Contributor 1" };
var contrib2 = new Contributor() { Name = "Contributor 2" };
var contrib3 = new Contributor() { Name = "Contributor 3" };
dc.Contributors.Add(contrib1);
dc.Contributors.Add(contrib2);
dc.Contributors.Add(contrib3);
dc.SaveChanges();
// add relationships
book1.Writers.Add(contrib1);
book1.Artists.Add(contrib1);
book1.Artists.Add(contrib2);
book1.OtherContributors.Add(contrib3);
dc.SaveChanges();
}
// verify that the contributor 1 has both Artist and Writer relations
using (var dc = new MyDbContext())
{
var contrib1 = dc.Contributors.Single(c => c.Name == "Contributor 1");
var hasWriter = contrib1.WriterOfBooks.Count == 1;
var hasArtist = contrib1.ArtistOfBooks.Count == 1;
if (!hasWriter || !hasArtist)
{
throw new Exception("Houston, we have a problem.");
}
}
I'm working on books at test.polarcomputer.com
If you have a book object and this object has writer,publisher,designer ..whoever, you need just 3 object :
1.book object
2.contributor object.
3.integration object
book object has
- bookid
- bookname
contributor object has
- contributorid
- name
- typeofcontributor // 0-writer 1-colorist 2-CoverArtists 3-whoever
integration object has
- bookid
- contributorid
- typeofcontributor // 0-writer 1-colorist 2-CoverArtists 3-whoever
Check this if i understant it truly , i can give you full solution.
The data model you show is OK, but one thing is clear. You can't map this as a pure many-to-many association. That's only possible if the junction table BookContributors only contains BookId and ContributorId.
So you always need an explicit BookContributor class, and getting a collection of one of the contributor types is always going to take this basic shape:
book.BookContributors
.Where(bc => bc.Type == type)
.Select(bc => bc.Contributor)
Clunky, compared to what you have in mind. But no way to get around it, I'm afraid. What's left is a few options in the implementation details.
Option 1: Get all contributors, filter later.
First, let's get the basic model right:
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public virtual ICollection<BookContributor> BookContributors { get; set; }
}
public class Contributor
{
public int ContributorId { get; set; }
public string Name { get; set; }
public virtual ICollection<BookContributor> BookContributors { get; set; }
}
public class BookContributor
{
public int BookId { get; set; }
public virtual Book Book { get; set; }
public int ContributorId { get; set; }
public virtual Contributor Contributor { get; set; }
public string Type { get; set; }
}
And the mapping:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Book>().HasMany(b => b.BookContributors)
.WithRequired(bc => bc.Book)
.HasForeignKey(bc => bc.BookId);
modelBuilder.Entity<Contributor>().HasMany(c => c.BookContributors)
.WithRequired(bc => bc.Contributor)
.HasForeignKey(bc => bc.ContributorId);
modelBuilder.Entity<BookContributor>()
.HasKey(bc => new {bc.BookId, bc.ContributorId, bc.Type});
}
(by the way, here I avoid the term 'Discriminator' because that suggests TPH inheritance, which isn't applicable - yet).
Now you could add properties to Book like these:
[NotMapped]
public IEnumerable<Contributor> Writers
{
get
{
return BookContributors.Where(bc => bc.Type == "writer")
.Select(bc => bc.Contributor);
}
}
The downside of this approach is that you always have to ensure that books are loaded with their BookContributors and their Contributors included, or that lazy loading is possible. And you can't use these properties directly in a LINQ query. Also, it's somewhat hard to get books and only their unique contributors (i.e. distinct).
Option 2: Inheritance - essentially the same
You could make BookContributor an abstract base class having a number of inheritors:
public abstract class BookContributor
{
public int Id { get; set; }
public int BookId { get; set; }
public virtual Book Book { get; set; }
public int ContributorId { get; set; }
public virtual Contributor Contributor { get; set; }
}
public class Artist : BookContributor
{ }
public class Writer : BookContributor
{ }
BookContributor now needs a surrogate key, Id, because EF will now use a field Discriminator, which is hidden, so it can't be configured as part of the primary key.
Now Book could have properties like...
[NotMapped]
public ICollection<Artist> Artists
{
get { return BookContributors.OfType<Artist>().ToList(); }
}
...but these will still have the same downsides as mentioned above. The only possible advantage is that you can use types now (with compile-time checking) in stead of strings (or enum values) to get to the various BookContributor types.
option 3: A different model
Maybe the most promising approach is a slightly different model: books and contributors, where each association between them can have a collection of contributor types. BookContributor now looks like this:
public class BookContributor
{
public int BookId { get; set; }
public virtual Book Book { get; set; }
public int ContributorId { get; set; }
public virtual Contributor Contributor { get; set; }
public virtual ICollection<BookContributorType> BookContributorTypes { get; set; }
}
And a new type:
public class BookContributorType
{
public int ID { get; set; }
public int BookId { get; set; }
public int ContributorId { get; set; }
public string Type { get; set; }
}
Modified mapping:
modelBuilder.Entity<BookContributor>().HasKey(bc => new { bc.BookId, bc.ContributorId });
Additional mapping:
modelBuilder.Entity<BookContributor>().HasMany(bc => bc.BookContributorTypes).WithRequired();
modelBuilder.Entity<BookContributorType>().HasKey(bct => bct.ID);
With this model you can either just get books and their distinct contributors, if you're not interested in the contributor's types...
context.Books.Include(b => b.BookContributors
.Select(bc => bc.Contributor))
...or with the types...
context.Books.Include(b => b.BookContributors
.Select(bc => bc.Contributor))
.Include(b => b.BookContributors
.Select(bc => bc.BookContributorTypes));
...or books with only writers...
context.Books.Select(b => new
{
Book = b,
Writers = b.BookContributors
.Where(bc => bc.BookContributorTypes
.Any(bct => bct.Type == "artist"))
})
Again, the latter query can be wrapped in a property...
[NotMapped]
public ICollection<Artist> Artists
{
get
{
return BookContributors
.Where(bc => bc.BookContributorTypes
.Any(bct => bct.Type == "artist"))
.Select(bc => bc.Contributor).ToList();
}
}
...however, with all aforementioned cautions.
Your model should be like that :
[Table("tblBooks")]
public class BookTbl
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int BookID { get; set; }
public string BookName { get; set; }
}
[Table("tblContributor")]
public class ContributorTbl
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int ContID { get; set; }
public string Contributor { get; set; }
}
[Table("tblIntegration")]
public class IntegrationTbl
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int IntID { get; set; }
public int BookID { get; set; }
[ForeignKey("BookID")]
public BookTbl Book { get; set; }
public int ContID { get; set; }
[ForeignKey("ContID")]
public IntegrationTbl Integration { get; set; }
}

ASP.NET MVC 4 Code First Many to Many Adding to Collection

I am using ASP.NET MVC 4 code first pattern for database layer. I have a many to many relationship between UserProfile and Task. When I try to add a task to the the collection of tasks of a user, it's added but if I try to query it and see if it's there it's not showing up.
My model:
public class UserProfile
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string SirName { get; set; }
public string Position { get; set; }
public string Email { get; set; }
public ICollection<TaskModels> Tasks {get; set; }
public bool? isActive { get; set; }
public UserProfile()
{
Tasks = new HashSet<TaskModels>();
}
}
public class TaskModels
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public ICollection<UserProfile> Employees { get; set; }
public int TimeNeeded { get; set; }
public int TimeWorked { get; set; }
public string Status { get; set; }
public bool isActive { get; set; }
public TaskModels()
{
Employees = new HashSet<UserProfile>();
}
}
public class WorkLogModels
{
public int Id { get; set; }
public UserProfile Author { get; set; }
public DateTime TimeBeganWorking { get; set; }
public int TimeWorkedOn { get; set; }
public TaskModels Task { get; set; }
public string Description { get; set; }
}
public class TimeTrackerDb : DbContext
{
public TimeTrackerDb() : base("DefaultConnection")
{
}
public DbSet<UserProfile> UserProfiles { get; set; }
public DbSet<TaskModels> Tasks { get; set; }
public DbSet<WorkLogModels> WorkLogs { get; set; }
}
I try to check if a UserProfile already exists in a Task's Employees list and it's always empty.
[HttpPost]
public ActionResult Create(WorkLogModels worklogmodels)
{
var tasks = db.Tasks.Where(x => x.Name == worklogmodels.Task.Name).SingleOrDefault();
if (tasks == null)
{
return View(worklogmodels);
}
if (ModelState.IsValid)
{
var user = db.UserProfiles.Where(x => x.UserId == WebSecurity.CurrentUserId).FirstOrDefault();
var task = db.Tasks.Where(x => x.Name == worklogmodels.Task.Name).FirstOrDefault();
WorkLogModels log = new WorkLogModels();
log.Description = worklogmodels.Description;
log.TimeBeganWorking = worklogmodels.TimeBeganWorking;
log.TimeWorkedOn = worklogmodels.TimeWorkedOn;
log.Author = user;
log.Task = task;
db.WorkLogs.Add(log);
if (!db.UserProfiles.Where(x => x.UserId == WebSecurity.CurrentUserId).First().Tasks.Any(x=> x.Name == worklogmodels.Task.Name))
{
db.UserProfiles.Where(x => x.UserId == WebSecurity.CurrentUserId).FirstOrDefault().Tasks.Add(task);
db.Tasks.Where(x => x.Name == worklogmodels.Task.Name).FirstOrDefault().Employees.Add(user);
}
db.SaveChanges();
return RedirectToAction("Index");
}
return View(worklogmodels);
}
I've been fighting with this for two days now.
Any help will be greatly appreciated
EDIT:
I am not sure if I made myself clear. In the Crate action for the WorkLog Controller I am trying to put the current user in the current task's collection and vice versa. It works correctly the first time, but then if I do it again it fails to skip the if statement and tries to add it once again and throws an exception : System.Data.SqlClient.SqlException. It's trying to add the same record to the intermediate table.

Resources