LINQ extension method for multiple join with multiple GroupBy requirements - c#

As an exercise in learning EF, I have the following 4 tables. Person 1toM, with Orders M2M, with Products via OrderProducts (Gender is an Enum):
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public Gender Gender { get; set; }
public IList<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public int PersonId { get; set; }
public Person Person { get; set; }
public IList<OrderProduct> OrderProducts { get; set; }
}
public class OrderProduct
{
public int Id { get; set; }
public int Qty { get; set; }
public Order Order { get; set; }
public int OrderId { get; set; }
public Product Product { get; set; }
public int ProductId { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public IList<OrderProduct> OrderProducts { get; set; }
}
I'm trying to generate the following with LINQ extension methods:
A list of products and total [=Sum(OrderProducts.Qty * Product.Price)] spent, grouped by product name then grouped by gender.
The result would look something like this:
Female
Ball $655.60
Bat $1,925.40
Glasses $518.31
Etc...
Male
Ball $1,892.30
Bat $3,947.07
Glasses $1,315.71
Etc...
I'm committing myself to LINQ extension methods and hope that I can also develop some best practice here. I can't work out how to now group by ProductName and aggregate the Qty * Price into a total by product:
var prodsalesbygender = context.Orders
.GroupBy(o => new
{
Gender = o.Person.Gender
})
.Select(g => new
{
ProductName = g.Select(o => o.OrderProducts.Select(op => op.Product.Name)),
Qty = g.Select(o => o.OrderProducts.Select(op => op.Qty)),
Price = g.Select(o => o.OrderProducts.Select(op => op.Product.Price))
}
);
I've tried adding .GroupBy(g => g.ProductName) to the end but get the error "The key selector type for the call to the 'GroupBy' method is not comparable in the underlying store provider.".

I think you are almost there..
Try this one:
var prodsalesbygender = context.Orders
.GroupBy(o => new
{
Gender = o.Person.Gender
})
.Select(g => new
{
Gender = g.Key,
Products = g.Select(o => o.OrderProducts
.GroupBy(op => op.Product)
.Select(op => new
{
ProductName = op.Key.Name,
Qty = op.Sum(op2 => op2.Qty),
Price = op.Select(x => x.Product.Price)
.First(),
})
.Select(x => new
{
ProducName = x.ProductName,
Qty = x.Qty,
Price = x.Price,
TotalPrice = x.Qty * x.Price
}))
});
In short, you just need more projection. In my suggested solution, first you group by the gender. The next step is to project the gender and 'list of product' directly (yes, the difficult part to get around is the OrderProducts). Within the Products we group it by product name, then take the total quantity (Qty) and set the Price - assuming the price for the same product is constant. The next step is to set the TotalPrice, the Qty * Price thing.
Ps. I am fully aware that this query had many deficiencies. Perhaps LINQ expert can give a better help on this.
It will result in a class something like:
{
Gender Gender
IEnumerable<{ ProductName, Qty, Price, TotalPrice }> Products
}
Yes, so much for anonymous type..
Nevertheless, i am baffled by the down votes as the question contains the models in question and the attempt OP have provided.

Finally here is my solution, producing a result exactly as required.
var prodsalesbygender = context.OrderProducts
.GroupBy(op => new
{
Gender = op.Order.Person.Gender
})
.Select(g => new
{
Gender = g.Key.Gender,
Products = g.GroupBy(op => op.Product)
.Select(a => new
{
ProductName = a.Key.Name,
Total = a.Sum(op => op.Qty * op.Product.Price)
})
.OrderBy(a => a.ProductName)
});
foreach (var x in prodsalesbygender)
{
Console.WriteLine(x.Gender);
foreach (var a in x.Products)
Console.WriteLine($"\t{a.ProductName} - {a.Total}");
}
My thanks to #Bagus Tesa

Related

How to combine Join with an aggregate function and group by

I am trying to get the average rating of all restaurants and return the names of all resteraunts associated with that id, I was able to write a sql statement to get the average of restaurants along with the names of the restaurants however I want to only return the name of the restaurant once.
Select t.Average, Name from [dbo].[Reviews] as rev
join [dbo].[Resteraunts] as rest
on rest.ResterauntId = rev.ResterauntId
inner join
(
SELECT [ResterauntId],
Avg(Rating) AS Average
FROM [dbo].[Reviews]
GROUP BY [ResterauntId]
)
as t on t.ResterauntId = rest.ResterauntId
resteraunt class
public int ResterauntId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
public virtual Review reviews{ get; set; }
Review class
public int ReviewId { get; set; }
public double Rating { get; set; }
[ForeignKey("ResterauntId")]
Resteraunt Resteraunt { get; set; }
public int ResterauntId { get; set; }
public DateTime DateOfReview { get; set; }
If possible I would like to have the answer converted to linq.
Resteurants.Select(r => new {
Average = r.Reviews.Average(rev => rev.Rating),
r.Name
})
This should give you a set of objects that have Average (the average of all reviews for that restaurant) and the Name of the restaurant.
This assumes that you have correctly setup the relationships so that Restaurant.Reviews only refers to the ones that match by ID.
If you don't have that relationship setup and you need to filter it yourself:
Resteurants.Select(r => new {
Average = Reviews.Where(rev => rev.ResteurantId == r.Id).Average(rev => rev.Rating),
r.Name
})
Firstly your models seems to have more aggregation than required, I have taken the liberty to trim it and remove extra fields, ideally all that you need a Relation ship between two models RestaurantId (Primary Key for Restaurant and Foreign Key (1:N) for Review)
public class Restaurant
{
public int RestaurantId { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public string City { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
}
public class Review
{
public int ReviewId { get; set; }
public double Rating { get; set; }
public int RestaurantId { get; set; }
public DateTime DateOfReview { get; set; }
}
If these are the models, then you just need List<Restaurant> restaurantList, since that internally contains the review collection, then all that you need is:
var result =
restaurantList.Select(x => new {
Name = x.Name,
Average = x.Reviews.Average(y => y.Rating)
}
);
In case collection aggregation is not there and you have separate ReviewList as follows: List<Review> reviewList, then do the following:
var result =
reviewList.GroupBy(x => x.RestaurantId, x => new {x.RestaurantId,x.Rating})
.Join(restaurantList, x => x.Key,y => y.RestaurantId,(x,y) => new {
Name = y.Name,
AvgRating = x.Average(s => s.Rating)
});
Also please note this will only List the Restaurants, which have atleast one review, since we are using InnerJoin, otherwise you need LeftOuterJoin using GroupJoin, for Restaurants with 0 Rating
I see your Restaurant class already has an ICollection<Review> that represents the reviews of the restaurant. This is probably made possible because you use Entity Framework or something similar.
Having such a collection makes the use of a join unnecessary:
var averageRestaurantReview = Restaurants
.Select(restaurant => new
.Where(restaurant => ....) // only if you don't want all restaurants
{
Name = restaurant.Name,
AverageReview = restaurants.Reviews
.Select(review => review.Rating)
.Average(),
});
Entity Framework will do the proper joins for you.
If you really want to use something join-like you'd need Enumerable.GroupJoin
var averageRestaurantReview = Restaurants
.GroupJoin(Reviews, // GroupJoin Restaurants and Reviews
restaurant => restaurant.Id, // From every restaurant take the Id
review => review.RestaurantId, // From every Review take the RestaurantId
.Select( (restaurant, reviews) => new // take the restaurant with all matching reviews
.Where(restaurant => ....) // only if you don't want all restaurants
{ // the rest is as before
Name = restaurant.Name,
AverageReview = reviews
.Select(review => review.Rating)
.Average(),
});

IQueryable<decimal> to decimal

I have a model class which contains a property ProductPrice of type decimal. I am not able to store an IQueryable type to a property decimal. I have even tried to Convert.ToDecimal but it still showing me the error.
Model - Product
public class Product
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal ProductPrice { get; set; }
public int ProductQty { get; set; }
}
Model CartDispay
public class CartDisplay
{
public int ItemId { get; set; }
public String ProductName { get; set; }
public int ProductQty { get; set; }
public decimal ProductPrice { get; set; }
}
Controller
public ActionResult Index()
{
int userId = 3;
var items = _context.Cart.Join(_context.Items, c => c.CartId, i => i.CartId, (c, i) => new { c, i }).Where(c => c.c.UserId == userId).ToList();
foreach(var item in items)
{
CartDisplay display = new CartDisplay();
display.ItemId = item.i.ItemId;
display.ProductName = _context.Product.Where(p => p.ProductId == item.i.ProductId).Select(p => p.ProductName).ToString();
display.ProductPrice = _context.Product.Where(p => p.ProductId == item.i.ProductId).Select(p => p.ProductPrice); ;
display.ProductQty = item.i.ProductQty;
cartView.CartDisplay.Add(display);
}
retu
IQueryable<T> defines a sequence of elements of type T, rather than a single item. It also lets you perform additional querying without bringing the sequence into memory, but that is not important in the context of your question.
Your situation is a lot simpler, though: rather than querying for individual properties, you should query for the whole product by its ID, then take its individual properties, like this:
var prod = _context.Product.SingleOrDefault(p => p.ProductId == item.i.ProductId);
if (prod != null) {
display.ProductName = prod.ProductName
display.ProductPrice = prod.ProductPrice;
display.ProductQty = ...
} else {
// Product with id of item.i.ProductId does not exist
}

Add data to a list insde a list where item in the prerent list

I would like to select a where statement that adds items to a list where only product codes match. I have it so it gets all of the products sold in the sale but I would like there were statement to get only products in this sale.
PS: This is really hard to explain
Model
public class userSales
{
public string Name { get; set; }
public string UserName { get; set; }
public int Sale_Id { get; set; }
public int CostumerID { get; set; }
public string Sale_Date { get; set; }
public string Paid { get; set; }
public Nullable<int> Sale_Cost { get; set; }
public string Discount_Code { get; set; }
public List<SaleProduct> saleProductsList { get; set; }
}
public class SaleProduct
{
public int SaleID { get; set; }
public string ProductCode { get; set; }
public int ProductCount { get; set; }
public string Image_Path { get; set; }
public string Shoot_Date { get; set; }
public string Shoot_Info { get; set; }
}
Linq statement where I'm having trouble:
var test = (from _ClientData in db.ClientDatas
join _salesInfo in db.Sales_Infoes
on _ClientData.CostumerID
equals _salesInfo.CostumerID
where _ClientData.UserName == _userName
select new userSales()
{
CostumerID = _ClientData.CostumerID,
Name = _ClientData.Name,
UserName = _ClientData.UserName,
Sale_Id = _salesInfo.Sale_Id, // This is the item i would like to use in my were statement
Sale_Date = _salesInfo.Sale_Date,
Sale_Cost = _salesInfo.Sale_Cost,
Discount_Code = _salesInfo.Discount_Code,
Paid = _salesInfo.Paid,
// Problem here
saleProductsList = db.SaleProducts.Where()
}).ToList();
Got to this based on the answer:
var reult = db.ClientDatas.Where(a => a.UserName == _userName)
.Join(db.Sales_Infoes,
a => a.CostumerID,
b => b.CostumerID,
(a, b) => new userSales()
{
CostumerID = a.CostumerID,
Discount_Code = b.Discount_Code,
Sale_Cost = b.Sale_Cost,
Sale_Id= b.Sale_Id,
Name = a.Name,
Sale_Date = b.Sale_Date,
UserName = a.UserName,
Paid = b.Paid,
saleProductsList = db.SaleProducts.Where(c => c.SaleID == b.Sale_Id).ToList()
}).ToList();
You're not looking for a where, you're looking for a join. Where filters the results on a single table, join intersects two tables which is actually what you want here.
var result = db.Sales_Infoes.Where(x => x.UserName == _userName)
.Join(db.ClientDatas,
x => x.Sale_Id,
y => y.Sale_id,
(x, y) => new userSales() {
// x is SalesInfo obj y is ClientDatas obj do assignement here
Name = y.Name,
Sale_Date = y.Sale_date
}).ToList();
Just fyi I haven't had a chance to test that but it's the basic idea. You don't need a select like in your statement because the last argument I'm passing into join is the lambda (x, y) => ... in that case x and y are the current row from each table (that we've gotten from applying our where to the user sales table then joining those results into the salesproduct table) so whatever projections you want to do occur there. The other two method args above that are the telling join which fields to compare, it's the 'key selector' lambda expression for each table.

Calculate Sum of column using groupby

I am trying to calculate my customer debt using groupby
I have 2 tables with the structures as you can see here :
public partial class Inovice
{
public Inovice()
{
this.Payments = new HashSet<Payment>();
this.InvoiceDetails = new HashSet<InvoiceDetail>();
}
public PersianCalendar objPersianCalender = new PersianCalendar();
[DisplayName("شناسه")]
public int Id { get; set; }
[DisplayName("شناسه کاربر")]
public int MemberId { get; set; }
public virtual ICollection<InvoiceDetail> InvoiceDetails { get; set; }
}
This table holds the customer invoices ,another tables hold the items that the customers buy.the structure is like this :
public partial class InvoiceDetail
{
[DisplayName("شناسه ")]
public int Id { get; set; }
[DisplayName("مقدار")]
public Nullable<double> Amount { get; set; }
[DisplayName("شناسه فاکتور")]
public int InoviceId { get; set; }
public Nullable<bool> IsReturned { get; set; }
public virtual Inovice Inovice { get; set; }
}
My values:
MemberOrCustomer sumOfAmountvalues
1 12
1 8
2 12
2 11
These two tables has a relations on invoiceid.I need to sum amount(InvoiceDetail) values for each memberId(Invoice) for example :
MemberOrCustomer sumOfAmountvalues
1 20
2 23
I write this code for doing this but it doesn't work :
var result= invoices.GroupBy(i => i.MemberId).Select(group=>new {Amount=group.Sum(Amount)} );
Best regards
Well, you could work with a list of invoicesDetails.
var result = invoicesDetails.GroupBy(id => id.Invoice.MemberId)
.Select(g => new {
MemberOrCustomer = g.Key,
Amount = g.Sum(x => x.Amount)
});
where invoiceDetails might be
invoices.SelectMany(x => x.InvoiceDetails)
so
invoices
.SelectMany(x => x.InvoiceDetails)
.GroupBy(id => id.Invoice.MemberId)
.Select(g => new {
MemberOrCustomer = g.Key,
Amount = g.Sum(x => x.Amount)
});

LINQ - get total count of values from nested list

i have a Class:
public class Company
{
public int id { get; set; }
public string title { get; set; }
public string address { get; set; }
public string city { get; set; }
public string zip { get; set; }
public List<string> phones { get; set; }
public List<string> categories { get; set; }
}
and i have a Generic List which contains that Class:
public List<Company> Companies = new List<Company>();
i want to do two things:
get a distinct list of the categories
get a total count of companies per category
i think i managed to the the first thing:
Companies.SelectMany(c => c.categories).Distinct()
please tell me if you think anything is wrong with that.
i tried the second step as so:
Companies.SelectMany(c => c.categories).Where(c=>c == Category).Count()
but im not sure that is really right.
Correct
You need to flatten the list into (company, category) pairs, then group by category:
from company in Companies
from category in company.Categories
group company by category into g
select new { Category = g.Key, Count = g.Count() }
EDIT: If you want to find out how many companies are in a single given category, you can write
Companies.Count(c => c.Categories.Contains(categoryName))
Companies
.SelectMany(c => c.categories)
.GroupBy(c => c)
.Select(g => new
{
Cateogry = g.Key,
Count = g.Count()
});
If you want it for specific category then your query is correct already.

Categories

Resources