Group by and Select with Models - c#

I have a list of an object defined by a model, and I want to group by a specific field. When grouped, some fields will need to be summed together. I want to group by ItemName, and the Amount will be all summed together.
My model is:
public class Item{
public string ItemName { get; set; }
public string ItemColor { get; set; }
public string SomethingElse { get; set; }
public decimal Amount { get; set; }
}
My current attempt is:
List<Item> fullListOfItems = GetItemsFromDatabase();
List<Item> groupedList = fullListOfItems
.GroupBy( l => l.ItemName )
.Select(i =>
new Item()
{
ItemName = i.Key.ItemName,
ItemColor = i.Key.ItemColor,
SomethingElse = i.Key.SomethingElse,
Amount = i.Sum(k=>k.Amount)
}
);
I'm getting an error after each key statement, saying that the grouping model does not contain a defintion for ItemName.
Any help would be appreciated. Thanks.

your code should look like this
List<Item> groupedList = fullListOfItems
.GroupBy(l => l.ItemName)
.Select(i =>
new Item()
{
ItemName = i.Key,
ItemColor = i.First().ItemColor,
SomethingElse = i.First().SomethingElse,
Amount = i.Sum(k => k.Amount)
}
).ToList();
instead of i.Key.ItemColor you have a List of Items and you have to pick one value (e.g. First()) - Key is the value of ItemName of each group

Related

Linq method - Return all Recipes that contain all Tags selected

This query gets all the rows from the join table, TagRecipes (many-to-many), where the TagId is found in a list, tagIdList, and lastly just returns the Recipe. How can I make it so it only returns Recipes that have all the tags in the list, tagIdList?
Basically, it's filtering by 'or', but I want it to filter by 'and'. The recipe must contain ALL the tags, not just some.
var allRecipes = await _context.TagRecipes
.Where(tr => tagIdList.Contains(tr.TagId))
.Select(i => i.Recipe).Distinct()
.ToListAsync();
e.g, tagIdList = {17, 20 ,21 }
TagRecipes
So, it should only return Recipe with RecipeId = 2, even though RecipeID 4 contains TagId 17
Classes
public class Recipe
{
public int Id { get; set; }
public string Name { get; set; }
public string Ingredients { get; set; }
public string Instructions { get; set; }
public string ImageURL { get; set; }
public string Description { get; set; }
public ICollection<TagRecipe> TagRecipes { get; set; }
public virtual ICollection<StarRating> StarRatings { get; set; }
public ICollection<Binder> Binders { get; set; }
}
public class TagRecipe
{
public int TagId { get; set; }
public int RecipeId { get; set; }
public Tag Tag { get; set; }
public Recipe Recipe { get; set; }
}
Thank you
group TagRecipes by RecipId, so each RecipId (Key) has its own tagIds.
loop on each group to check if it has all the tags in the provided tagIdList, and if it has them all, store this RecipId (Key), in my case i created list of int RecipIds.
get all the Recipes in the RecipIds list.
I hope this could be helpful
List<int> RecipIds = new List<int>();
int count = 0;
var RecipGroup = _context.TagRecipes.GroupBy(tr => tr.RecipeId);
foreach (var group in RecipGroup)
{
count = 0;
foreach (var tr in group)
{
if (tagIdList.Contains(tr.TagId))
{
count += 1;
}
}
if (tagIdList.Length == count)
{
RecipIds.Add(group.Key);
}
}
var allRecipes = _context.Recipes.Where(r => RecipIds.Contains(r.Id)).ToList();
I believe the following solution using Linqkit will be the simplest way to solve this, and without returning duplicates.
var tagIdList = new List<int> {1, 2};
var predicate = tagIdList.Aggregate(PredicateBuilder.New<Recipe>(), (pred, currentTagId) =>
pred.And(recipe => recipe.TagRecipes.Any(x => x.TagId == currentTagId)));
var result = _context.Recipes.Where(predicate).ToList();
Generates this SQL:
SELECT "r"."Id", "r"."Name"
FROM "Recipes" AS "r"
WHERE EXISTS (
SELECT 1
FROM "TagRecipes" AS "t"
WHERE ("r"."Id" = "t"."RecipeId") AND ("t"."TagId" = #__currentTagId_0)) AND EXISTS (
SELECT 1
FROM "TagRecipes" AS "t0"
WHERE ("r"."Id" = "t0"."RecipeId") AND ("t0"."TagId" = #__currentTagId_1))
Code is tested and verified using an asp.net Core 5 app.
Basically, it's filtering by 'or', but I want it to filter by 'and'.
The recipe must contain ALL the tags, not just some.
So, it should only return Recipe with RecipeId = 2, even though RecipeID 4 contains
TagId 17
Since you want to filter the data by and, after filtering the data based on the tag, if group the result based on the RecipeId property, the item count in the group should be the equally with the tag list count. So, you could use the following query statement:
var tagIdList = new List<int>() { 17, 20, 21 };
var allRecipes = _context.TagRecipes.Include(tr => tr.Recipe)
.Where(tr => tagIdList.Contains(tr.TagId))
.ToList()
.GroupBy(tr=> tr.RecipeId)
.Where(c => c.Count() == tagIdList.Count)
.Select(i => new { RecipeId = i.Key, Count = i.Count(), Recipe = i.FirstOrDefault().Recipe })
.ToList();
The result as below:

LINQ extension method for multiple join with multiple GroupBy requirements

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

Array inside list object, how to handle?

I have these two classes:
public class Order
{
public int ID { get; set; }
public int Output { get; set; }
public int Wharf { get; set; }
public int PartOf { get; set; }
public int[] Product { get; set; }
public int[] Quantity { get; set; }
public int[] Storage { get; set; }
public override bool Equals(Order obj)
{
// If parameter is null return false.
if (obj == null)
{
return false;
}
// Return true if the fields match:
return (ID == obj.ID);
}
}
public class RawOrderData
{
public int ID { get; set; }
public int Output { get; set; }
public int Wharf { get; set; }
public int PartOfID { get; set; }
public int ProductID { get; set; }
public int Quantity { get; set; }
}
Every order in the system is in the form as class Order, the array is used when there are more than one product in the order.
RawOrderData is created from a JSON string where every product in the order have its own object. I want to create a List<Order> where every order gets its own object in the list so there not are several orders with same order id when order contains more than one product.
// raw data is here the JSON string
rawdatalist = serializer.Deserialize<List<RawOrderData>> (rawdata);
// Convert raw objects to List<Order>, list of orders
List<Order> orders = new List<Order> ();
orders = ConvertRawOrderToList (rawdatalist);
private List<Order> ConvertRawOrderToList(List<RawOrderData> datalist)
{
List<Order> orders = new List<Order> ();
foreach (RawOrderData dataobj in datalist)
{
// Check if order exists in list
if (orders.Contains(new Order () {ID = dataobj.ID}))
{
// Order exists, add more products
// CODE HERE?
} else {
// order not existing, add new order to list
short storage = GetStorageArea(dataobj.ProductID);
orders.Add (new Order () {ID = dataobj.ID, Output = dataobj.Output, Wharf = dataobj.Wharf, PartOf = dataobj.PartOfID, Product = dataobj.ProductID, Quantity = dataobj.Quantity});
}
}
return orders;
}
Do I think correct with the ConvertRawOrderToList method? The problem is I don't know what to write in // CODE HERE?. When there is array inside the list-object I'm confused.
I'm also wondering how to access all values in the List<Order> orders.
The information to Storage[] is created from another method that have product ID as input.
It sounds like you have a "flattened" collection of objects that you want to group into Orders. If that's the case, a basic Linq projection would be simplest:
var orders = datalist.GroupBy(o => o.ID)
.Select(g => new Order {
ID = g.Key,
Output = g.First().Output,
Wharf = g.First().Wharf,
PartOf = g.First().PartOf,
Product = g.Select(o => o.Product).ToArray(),
Quantity = g.Select(o => o.Product).ToArray(),
})
.ToList();
Then you don't need to worry about overriding Equals (at least not for this purpose).
Where would I add the method for adding Storage also?
Since your GetStorageArea function takes a single ProductID you need to pass the product IDs to that function:
var orders = datalist.GroupBy(o => o.ID)
.Select(g => new Order {
ID = g.Key,
Output = g.First().Output,
Wharf = g.First().Wharf,
PartOf = g.First().PartOf,
Product = g.Select(o => o.Product).ToArray(),
Quantity = g.Select(o => o.Product).ToArray(),
Storage = g.Select(o => GetStorageArea(o.Product)).ToArray()
})
.ToList();

LINQ: how to aggregate list within object after grouping properties

I have an object ChartObject that contains a list. Then I have a list of ChartObjects that I used LINQ to remove duplicate attributes. But I don't know how to combine each"itemList" property of the ChartObjects.
public class ChartObject
{
public string Type { get; set; }
public int Year { get; set; }
public long Cost { get; set; }
public List<Items> itemList { get; set; }
}
List<ChartObject> coList = GetItems();
result = coList.GroupBy(i => new { i.Type, i.Year }).Select(g => new ChartObject
{
Type = g.Key.Type,
Year = g.Key.Year,
Cost = g.Sum(i => i.Cost),
itemList = g.Select(i => i.itemList) //Does not work
}).ToList();
I'm assuming it's not as simple as just casting the itemList as the compiler says and there must be some sort of aggregation to be done?
Assuming you just want to combine all of the items in the group, you're looking for SelectMany, and you'll need to call ToList to get a List<Items>:
g.SelectMany(i => i.itemList).ToList();

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