I have 2 objects (Order and Product) and a third (OrderDetail) that will be used as "navigation" between Products and Orders.
I'm trying to build a view that will show the less-sold products. For that I am "querying" the object OrderDetail and saving the result in a view model to later on be used in the view.
Model:
public class Product
{
public int ProductID { get; set; }
public string CodProduct { get; set; }
public string Nome { get; set; }
(...)
public ICollection<OrderDetail> OrderDetails { get; set; }
}
public class Order
{
public int OrderID { get; set; }
(...)
[BindNever]
public ICollection<OrderDetail> OrderDetails { get; set; }
}
public class OrderDetail
{
public int OrderDetailId { get; set; }
public int OrderId { get; set; }
public int ProductID { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public virtual Product Product { get; set; }
public virtual Order Order { get; set; }
}
ViewModel:
public class ProductSoldViewModel
{
//Data from the Product
public string ProductCode { get; set; }
public string ProductName { get; set; }
//Data from the OrderDetail
public int Qty { get; set; }
}
Controller:
public IActionResult LSProducts()
{
List<ProductSoldViewModel> model = new List<ProductSoldViewModel>();
model = _context.OrderDetail
.GroupBy(od => od.ProductID)
.Select(o => new ProductSoldViewModel
{
ProductCode = o.Select(s => s.Product.CodProduct).FirstOrDefault(),
ProductName = o.Select(s => s.Product.Nome).FirstOrDefault(),
Qty = o.Sum(s => s.Quantity)
})
.OrderBy(od => od.Qty)
.ToList();
return View(model);
}
With this code, I get only the less-sold products that are present in the orders. But I need to get all the products, even those that have never been sold.
Can you give me some advice about how can I do that?
You should query the product table if you need to get all products:
public IActionResult LSProducts()
{
List<ProductSoldViewModel> model = new List<ProductSoldViewModel>();
model = _context.Product
.Include(a => a.OrderDetails)
.Select(o => new ProductSoldViewModel
{
ProductCode = o.CodProduct,
ProductName = o.Nome,
Qty = o.OrderDetails.Sum(s => s.Qty)
})
.OrderBy(od => od.Qty)
.ToList();
return View(model);
}
To avoid a null exception you may want to add the following constructor to your model:
public class Product
{
public Product()
{
OrderDetails = new HashSet<OrderDetail>();
}
(...)
public ICollection<OrderDetail> OrderDetails { get; set; }
}
Related
My scenario: Users will be able to create lists and add items to these lists. What I want to do is to find the items in the lists created by the users at most.
Item Entity
public class Item:BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
public decimal DiscountedPrice{ get; set; }
public virtual ICollection<ItemList> ItemLists { get; set; }
}
Item List Entity
public class ItemList:BaseEntity
{
public string Name { get; set; }
public string Description { get; set; }
public int UserId { get; set; }
public ICollection<Item> Items { get; set; }
[ForeignKey("UserId")]
public virtual User User { get; set; }
}
User Entity
public class User:BaseEntity
{
public string Name { get; set; }
public string Surname { get; set; }
public string Gsm { get; set; }
public string Email { get; set; }
public virtual ICollection<ItemList> ItemLists{ get; set; }
}
my DTO
public class TopItemsForUsers
{
[BsonRepresentation(BsonType.ObjectId)]
[BsonId]
public string ItemId { get; set; }
public string UserId { get; set; }
public int Quantity { get; set; }
}
My Item repository
var query = _context.Items.Include(l => l.ItemLists)
.GroupBy(g => g.ItemLists)
.Select(z => new TopItemsInLists { ItemId = z.Key.ToString(), Quantity = z.Count() })
.OrderByDescending(z => z.Quantity)
.Take(10);
I want to get products that are very present in users' lists
Where am I doing wrong? If anyone has any other suggestions
Try this query. I hope I understand question correctly.
var query =
from u in _context.Users
from il in u.ItemLists
from i in il.Items
group i by new { UserId = u.Id, ItemId = i.Id } into g
select new TopItemsInLists
{
UserId = g.Key.UserId.ToString(),
ItemId = g.Key.ItemId.ToString(),
Quantity = g.Count()
};
query = query
.OrderByDescending(z => z.Quantity)
.Take(10);
The database schema created has the following relations
The models used for generating the schema above are
public class Option
{
public int OptionId { get; set; }
public string OptionName { get; set; }
}
public class Value
{
public int ValueId { get; set; }
public string OptionValue { get; set; }
}
public class Sku
{
public int SkuId { get; set; }
public int ProductId { get; set; }
public decimal Price { get; set; }
[ForeignKey("ProductId")]
public Product Product { get; set; }
}
public class ProductVariant
{
public int Id { get; set; }
public int ProductId { get; set; }
public int OptionId { get; set; }
public int ValueId { get; set; }
public int SkuId { get; set; }
[ForeignKey("ProductId")]
public Product Product { get; set; }
[ForeignKey("OptionId")]
public Option Option { get; set; }
[ForeignKey("ValueId")]
public Value Value { get; set; }
[ForeignKey("SkuId")]
public Sku Sku { get; set; }
}
while the product class is
public class Product
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public IEnumerable<ProductVariant> ProductVariants { get; set; }
}
How can i load realated entities with this layout?
I tried the following but Options, Values and Skus are not accesible as navigation properties
var products = context.Products
.Include(x => x.ProductVariants)
.Include(x => x.Options)
.Include(x => x.Values)
.Include(x => x.Skus)
What changes should i make?
You lack navigation property in your product class:
public IEnumerable<Sku> Skus { get; set; }
And you need to use .ThenInclude instead of .Include when you are getting nested entities. It would be:
var products = context.Products
.Include(x => x.Skus)
.Include(x => x.ProductVariants)
.ThenInclude(ProductVariants => ProductVariants.Options)
.Include(x => x.ProductVariants)
.ThenInclude(ProductVariants => ProductVariants.Values)
I have strange problem. I have to pass the following Xunit test:
Fact]
public void GetOrder_ValidId_ReturnsCompleteOrder()
{
var service = new DataService();
var order = service.GetOrder(10248);
Assert.Equal(3, order.OrderDetails.Count);
Assert.Equal("Queso Cabrales", order.OrderDetails.First().Product.Name);
Assert.Equal("Dairy Products", order.OrderDetails.First().Product.Category.Name);
}
I am able to pass the orderdetails list to the test. However the "orderdetail" objects that i pass have a product object with null.
My classes are:
public class Order
{
[Key] public int Id { get; set; }
public DateTime Date { get; set; }
public DateTime Required { get; set; }
[Required] public virtual ICollection<OrderDetail> OrderDetails { get; set; }
public string ShipName { get; set; }
public string ShipCity { get; set; }
public Order()
{
this.OrderDetails = new List<OrderDetail>();
}
public override string ToString()
{
string ret =
$"Id = {Id}, DateTime = {Date}, Required = {Required}, shipName= {ShipName}, Shipcity = {ShipCity}";
return ret;
}
}
public class OrderDetail
{
public Order Order { get; set; }
[ForeignKey("orders")]
public int Orderid { get; set; }
[ForeignKey("products")]
public int productid { get; set; }
public int UnitPrice { get; set; }
public int Quantity { get; set; }
public int Discount { get; set; }
[Required] public virtual Product Product { get; set; }
public OrderDetail()
{
Product = Product;
}
public override string ToString()
{
return
$"OrderId = {Orderid}, Productid = {productid}";
}
}
public class Product
{
[ForeignKey("orderdetails")] public int Id { get; set; }
public string Name { get; set; }
public float UnitPrice { get; set; }
public string QuantityPerUnit { get; set; }
public int UnitsInStock { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
public virtual ICollection<OrderDetail> OrderDetails { get; set; }
public override string ToString()
{
return
$"Id = {Id}, Name = {Name}, UnitPrice = {UnitPrice}, QuantityPerUnit = {QuantityPerUnit}, UnitsInStock = {UnitsInStock}, CategoryId = {CategoryId}";
}
}
I have the tried the following two solution:
public Order GetOrder(int id)
{
using var ctx = new NorthWindContext();
var query = ctx.Orders.AsQueryable().Where(o => o.Id == 10248).FirstOrDefault();
ctx.SaveChanges();
//var query2 = ctx.Orders.Include("orderdetails").Where()
return query;
}
and
var query2 = ctx.Orders.Where(o => o.Id == 10248)
.Select(a => new Order
{
Id = a.Id,
OrderDetails = a.OrderDetails
}).FirstOrDefault();
I have tried to reconfigure the mapping but didn't do it.
If i do the same query and use Console.Writeline in a foreach loop i can conclude that every orderdetail has a "product"...
I keep on the getting the error: "Object not set to an instance of an object" when passing to xUnit test.
Ivans response did the job.
First of all i removed using System.Data.Entity;
Then i changed the order class to the following:
public class Order
{
[Key] public int Id { get; set; }
public DateTime Date { get; set; }
public DateTime Required { get; set; }
[Required] public virtual List<OrderDetail> OrderDetails { get; set; }
public string ShipName { get; set; }
public string ShipCity { get; set; }
public Order()
{
this.OrderDetails = new List<OrderDetail>();
}
public override string ToString()
{
string ret =
$"Id = {Id}, DateTime = {Date}, Required = {Required}, shipName= {ShipName}, Shipcity = {ShipCity}";
return ret;
}
}
Then i used the following query just like Ivans:
var query3 = ctx.Orders
.Include(o => o.OrderDetails)
.ThenInclude(d => d.Product)
.ThenInclude(d => d.Category)
.AsSingleQuery()
.FirstOrDefault(o => o.Id == 10248);
How would I create the following query using Linq?
SELECT product.name, product.code, category.Name FROM product
INNER JOIN productCategories ON product.ID = productCategories.productID
INNER JOIN category ON productCategories.categoryID = category.ID
WHERE productCategories.ID = idToFind
Product & Category Classes:
public class Product
{
public Product()
{
this.Categories = new HashSet<Category>();
}
public int ID { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public Category()
{
this.Products = new HashSet<Product>();
this.Children = new HashSet<Category>();
}
public int ID { get; set; }
[StringLength(150)]
public string Name { get; set; }
public int? ParentID { get; set; }
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
I have tried a few different things and can get the results if I only need columns from one table, but can not get details from both tables i.e. category name & product name.
Edit:
I have now added a JunctionClass
public class CategoryProduct
{
public int CategoryID { get; set; }
public Category Category { get; set; }
public int ProductID { get; set; }
public Product Product { get; set; }
}
and tried :
var results = _context.Product.Include(e => e.categoryProducts).ThenInclude(e => e.Category).Where(c=>c.categoryProducts.Category.ID==169).ToList();
But I still cant make it work.
Getting the error:
'ICollection<CategoryProduct>' does not contain a definition for 'Category' and no accessible extension method 'Category' accepting a first argument of type 'ICollection<CategoryProduct>' could be found
In EF core, you need a junction table to map many-to-many relationships.
public class ProductCategory
{
public int Id { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
public class Product
{
...
public virtual ICollection<ProductCategory> ProductCategories { get; set; }
}
public class Category
{
...
public virtual ICollection<ProductCategory> ProductCategories { get; set; }
}
// DbContext
public DbSet<ProductCategory> ProductCategories { get; set; }
public override OnModelCreating(ModelBuilder builder)
{
builder.Entity<ProductCategory>()
.HasOne(pc => pc.Product)
.WithMany(p => p.ProductCategories);
builder.Entity<ProductCategory>()
.HasOne(pc => pc.Category)
.WithMany(c => c.ProductCategories);
}
// Query
var result = await dbContext.ProductCategories
.Select(pc => new {
ProductName = pc.Product.Name,
ProductCode = pc.Product.Code,
CategoryName = pc.Category.Name
})
.SingleOrDefaultAsync(pc => pc.Id == idToFind)
//Mock SomeData
List<Product> products = new List<Product>();
List<Category> category = new List<Category>();
//Linq
var result = products.SelectMany(product => product.Categories.SelectMany(productCategory => category.Where(category => category.ID == productCategory.ID).Select(category => new { category.Name, ProductName = product.Name, product.Code })));
Try this
var idToFind = 3;
var o = (from p in _products
from c in p.Categories
where c.ID == idToFind
select new {ProductName = p.Name, ProductCode = p.Code, CategoryName = c.Name}).ToList();
Is there any way I can avoid using Include and ThenInclude in EF Core ?
I have these models and dtos :
For Book:
public partial class Book
{
public Book()
{
BookAuthors = new HashSet<BookAuthor>();
BookCategories = new HashSet<BookCategory>();
Reviews = new HashSet<Review>();
}
public int BookId { get; set; }
public string Title { get; set; }
...
public string ImageUrl { get; set; }
public ICollection<BookAuthor> BookAuthors { get; set; }
public ICollection<BookCategory> BookCategories { get; set; }
public ICollection<Review> Reviews { get; set; }
}
public class BookDto
{
public int BookId { get; set; }
public string Title { get; set; }
...
public string ImageUrl { get; set; }
public IList<AuthorDto> Authors { get; set; }
public IList<CategoryDto> Categories { get; set; }
public IList<ReviewDto> Reviews { get; set; }
}
For Author :
public partial class Author
{
public Author()
{
BookAuthors = new HashSet<BookAuthor>();
}
public int AuthorId { get; set; }
public string AuthorName { get; set; }
...
public ICollection<BookAuthor> BookAuthors { get; set; }
}
public class AuthorDto
{
public int AuthorId { get; set; }
public string AuthorName { get; set; }
...
public IList<BookDto> Books { get; set; }
}
For Category:
public partial class Category
{
public Category()
{
BookCategories = new HashSet<BookCategory>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public ICollection<BookCategory> BookCategories { get; set; }
}
public class CategoryDto
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public IList<BookDto> Books { get; set; }
}
And Review :
public partial class Review
{
public int ReviewId { get; set; }
public int BookId { get; set; }
public int UserId { get; set; }
public DateTime? Date { get; set; }
public string Comments { get; set; }
public decimal? Rating { get; set; }
public Book Book { get; set; }
public User User { get; set; }
}
public class ReviewDto
{
public int ReviewId { get; set; }
public int BookId { get; set; }
public int UserId { get; set; }
public DateTime? Date { get; set; }
public string Comments { get; set; }
public decimal? Rating { get; set; }
public Book Book { get; set; }
public User User { get; set; }
}
I have this :
public IEnumerable<Book> GetAll()
{
var books = _context.Book
.Include(e => e.BookAuthors)
.ThenInclude(a => a.Author)
.Include(c => c.BookCategories)
.ThenInclude(categ => categ.Category)
.Include(r => r.Reviews)
.AsNoTracking()
.ToList();
return books;
}
And then in Author :
public IEnumerable<Author> GetAll()
{
var authors = _context.Author
.Include(e => e.BookAuthors)
.ThenInclude(b => b.Book)
.ToList();
return authors;
}
public Author GetById(int id)
{
return _context.Author.Include("BookAuthors.Book").SingleOrDefault(x =>
x.AuthorId == id);
}
Between Books and Authors, Books and Categories I have many to many relationship, between Review and Books one to many relationship.
I need this because on the list with books I display the name of the author as well, on an author detail page I display his books and so on. I'm using AutoMapper and DTOs as well.
The same for Categories, Reviews..my json with the returned data becomes very big and it takes a lot of time to load the data into the page, because it has this nested structure. What would be the best solution to do this ?
There's a way to do Eager loading. I tried by GroupJoin(expression).SelectMany(...).
This will allow you to load till one level avoiding circular rerefence. I'll show you how I archived it, but with your models.
You have:
var books = _context.Book
.Include(e => e.BookAuthors)
.ThenInclude(a => a.Author)
.Include(c => c.BookCategories)
.ThenInclude(categ => categ.Category)
.Include(r => r.Reviews)
.AsNoTracking()
.ToList();
return books;
By the way, you dont put BookAuthors model. So, I'll assume it's structure:
var books = _context.Authors
.GroupJoin(_context.Book,
t1 => new { IdAuthor = (Guid)t1.Id }, //where t1 = Authors, you should have IdAuthor in Book.
a => new { IdAuthor = (Guid)a.IdAuthor }, //where a = Book
(t1, a_join) => new { t1, a_join })
.SelectMany(t1 => t1.a_join.DefaultIfEmpty(), (t1, a) => new { t1, a }) //.DefaultIfEmpty() = LEFT JOIN
.Select(x => new AuthorBooksDTO
{
IdAutor = t1.t1.Id //please navegate t1 till VS shoows you which model is
Books = t1.t1.a_join.toList() //navegate t1. a_join will be the list of books.
....
})
.ToList();
For sure, it takes more time to build but performance improve incredibly.
Let us know if it works for you.