LINQ Select IDs from multiple levels - c#

I'd like to get one list of IDs from all nested entities.
Code:
// Entities
class Floor
{
public int Id { get; set; }
public ICollection<Room> Rooms { get; set; } = new List<Room>();
}
class Room
{
public int Id { get; set; }
public ICollection<Chair> Chairs { get; set; } = new List<Chair>();
}
class Chair
{
public int Id { get; set; }
}
// Setup
var floor = new Floor() { Id = 1000 };
var room = new Room() { Id = 100 };
var chair = new Chair() { Id = 10 };
room.Chairs.Add(chair);
floor.Rooms.Add(room);
var floors = new List<Floor>() { floor };
// Select all IDs
var ids = floors.???
Expected result:
{ 10, 100, 1000 }
What I've tried. It selects IDs only from the deepest level, not all of them:
// Select all IDs
var ids = floors
.SelectMany(f => f.Rooms)
.SelectMany(r => r.Chairs)
.Select(ch => ch.Id)
.ToList();

SelectMany is what you need together with Append:
var ids = floors
.SelectMany(f => f.Rooms
.SelectMany(r => r.Chairs
.Select(c => c.Id).Append(r.Id)).Append(f.Id));

Your current code flattens the hierarchy to collection of Chair and selects only their ids.
With pure LINQ you can do via nesting SelectMany and using Append/Prepend:
var ids = floors
.SelectMany(f => f.Rooms
.SelectMany(r => r.Chairs
.Select(ch => ch.Id) // select chairs ids
.Append(r.Id)) // append "current" room id to it's collection of chair ids
.Append(f.Id)) // append "current" floor id to it's collection of rooms and chairs ids
.ToList();

An ugly hack would be to append "fakes" to the child rooms/chairs with the ID of their parent:
var ids = floors
.SelectMany(f => f.Rooms.Append(new Room { Id = f.Id }))
.SelectMany(r => r.Chairs.Append(new Chair { Id = r.Id }))
.Select(ch => ch.Id)
.ToList();

It can be done with a recursive helper function like this:
IEnumerable<int> CollectIds<T>(T x) => x switch {
Floor f => f.Rooms.SelectMany(CollectIds).Prepend(f.Id),
Room r => r.Chairs.SelectMany(CollectIds).Prepend(r.Id),
Chair c => Enumerable.Repeat(c.Id, 1),
_ => throw new NotSupportedException()
};
Usage:
var ids = floors
.SelectMany(CollectIds)
.ToList();
You could consider extracting this function into a common interface to avoid the awkward type switch and the recursion.

Related

Get Collection (Products) of Objects for each item in a list

I have orders collection that contains products collection.I'm passing some product ids as list to the method.I need to return a list of products matching with any of the id in the input list.
Do i need to use a foreach loop like this? Please advice ..
public List < ProductOrderData > ListProductsByOrderId(List < Guid > input) {
List < ProductOrderData > products = new List < ProductOrderData > ();
foreach(var id in input) {
var orders = this.Collection.AsQueryable().SelectMany(order => order.Products.Where(product => product.Id == id));
}
}
Update:
Update2:
Update 3:
I pass product Ids and i need to get list of products from orders that match the product Ids.
I have orders collection that contains products collection.
Okay, so you have something like this:
var ordersWithTheirProducts = ...
.Select(... => new Order
{
// some Order properties:
Id = ...
OrderDate = ...
ClientId = ...
// Products of this Order:
Products = ...
.Select( product => new Product
{
// Several Product properties
Id = product.Id,
Name = product.Name,
Price = product.Price,
...
},
};
I'm passing some product ids as list
IEnumerable<Guid> productIds = ...
I need to return a list of products matching with any of the id in the input list.
So, you want to select all Products that were used in any of your Orders, and if that Product has an Id that is one of the values in ProductIds, then you want it in your result.
First we extract all Products that are used in your Orders, then we'l remove the Duplicates, and finally we keep only those Products that have an Id in your productIds:
var result = Orders.SelectMany(order => order.Products) // extract all Products
.Distinct() // remove duplicate products
.Where(product => productIds.Contains(product.Id))
// keep only those products with an Id that is in productIds
iterating over the input guids will cause a query per each guid. instead issue a single command like this:
var products = collection.AsQueryable()
.Where(o => o.Products.Any(p => input.Contains(p.Id)))
.SelectMany(o => o.Products)
.Where(p => input.Contains(p.Id))
.ToList();
first shortlist the orders with the first where clause. and then unwind the products with SelectMany. because, unwinding the whole collection is a bad idea. unwinding creates objects in server memory and there's a pipeline memory limit of 100mb in mongodb.
test program:
using MongoDB.Entities;
using MongoDB.Entities.Core;
using System;
using System.Linq;
namespace StackOverFlow
{
public class OrderInfo : Entity
{
public Product[] Products { get; set; }
}
public class Product
{
public Guid Id { get; set; }
}
public static class Program
{
private static void Main()
{
new DB("test-db");
var input = new[] { Guid.NewGuid(), Guid.NewGuid() };
var orders = new[] {
new OrderInfo { Products = new[] { new Product { Id = input[0] } } },
new OrderInfo { Products = new[] { new Product { Id = input[1] } } }
};
orders.Save();
var products = DB.Queryable<OrderInfo>() // collection.AsQueryable() for official driver
.Where(o => o.Products.Any(p => input.Contains(p.Id)))
.SelectMany(o => o.Products)
.Where(p => input.Contains(p.Id))
.ToList();
}
}
}
It would be easier to loop over your order collection first and then retrieve your products
public static List<Product> GetProducts(List<int> ids)
{
var products = new List<Product>();
foreach(var order in Orders)
{
products.AddRange(order.Products.Where(p => ids.Any(id => id == p.Id)));
}
return products;
}
Or simply
public static List<Product> GetProducts(List<int> ids)
{
return Orders.
SelectMany(
o => o.Products
.Where(p => ids.Any(id => id == p.Id)))
.ToList();
}

Get groups of elements from name inside of list C#

I have a List of :
public class GT
{
public string ActivityName { get; set; }
public int Seconds { get; set; }
}
And the ActivityName could repeat through the iteration and what I want to do is, regroup all the items that have the same name and calculate the average of seconds of these specifics items. I tried to use Distinct but it didn't group them.
You should use GroupBy instead of Distinct:
List<GT> list = /*...*/;
var query = list.GroupBy(x => x.ActivityName, (k,g) => new { ActivityName = k, AverageTimeInSeconds = g.Average(x => x.Seconds) });
var groups = items.GroupBy(p => p.ActivityName)
.Select(g => new
{
ActivityName = g.Key,
Average = g.Average(t => t.Seconds)
}).ToList();

Linq to get values from 2 list with one unique Id

I have one class to bind for ex.
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
public int Price { get; set; }
}
Now, this class will get values from different tables but they have common ID.
here is the Lists that I am getting in my loop.
var List1 = item.TicketTypes.Where(m => m.PerformanceID == item.Id)
.Select(t => new { t.Name, t.ID});
var List2 = item.Pricies.Where(m => m.PerformanceID == item.Id)
.Select(t => new { t.Price, t.ID });
item is object of for each loop instance.
What I want is,
I want List of MyClass filled with Entity Name and Price as per their ID.
There isn't much informations here for me to test this, but I have this to suggest:
// Make sure to add a constructor to MyClass
var List1 = item.TicketTypes.Where(m => m.PerformanceID == item.Id)
.Select(t => new MyClass(t.Name, t.ID, item.Pricies.FirstOrDefault(p => p.PerformanceID == t.ID).Price));
This should give you the desired result, but like I said I don't have enough informations to test this so make sure you comment the problems so we can fix them together.
In a Linq Statement, if my assumptions are right(cannot test it):
var result= from t1 in item.TicketTypes
join t2 in item.Pricies on t1.ID equals t2.ID
select new MyClass() { Id = t1.ID, Name = t1.Name, Price = t2.Price };
use .ToList() if you need a List
Try this :
var query = List1.Join(List2, l1 => l1.Id, l2 => l2.Id, (l1, l2) => new { ID = l1.Id, Name = l1.Name, Price = l2.Price });
foreach (var obj in query)
{
Console.WriteLine("{0} - {1} - {2}", obj.ID, obj.Name, obj.Price);
}

LINQ to Entities, Where Any In

How to write 'Where Any In' in LINQ to Entity?
Here is my model :
class Chair
{
public int Id { get; set; }
public int TableId { get; set; }
public Table Table { get; set; }
}
class Table
{
public int Id { get; set; }
public ICollection<Chair> Chairs { get; set; }
public ICollection<Category> Categories { get; set; }
public Table()
{
Chairs = new List<Chair>();
Categories = new List<Category>();
}
}
class Category
{
public int Id { get; set; }
public ICollection<Table> Tables { get; set; }
}
I also got a simple list of Category :
List<Category> myCategories = new List<Category>(c,d,e);
I want to get only that Chairs that belongs to Table that got one of the Category from myCategories List. Thats what im trying to do :
var result =
ctx.Chairs.Where(x => x.Table.Categories.Any(y => myCategories.Any(z => z.Id == y.Id))).ToList();
I think its ok but what i get is error :
"Unable to create a constant value of type 'ConsoleApplication1.Category'. Only primitive types or enumeration types are supported in this context"
Try to compare with in-memory categories Ids collection, instead of categories collection.
var myCategoriesIds = myCategories.Select(c => c.Id).ToArray();
var result =
context.Chairs
.Where(
x => x.Table.Categories.Any(
y => myCategoriesIds.Contains(y.Id)))
.ToList();
this is because ctx.Chairs is a collection that is in database, you should retrieve that collection first in order to compare it with in-memory data:
var result = ctx
.Chairs
.AsEnumerable() // retrieve data
.Where(x =>
x.Table.Categories.Any(y =>
myCategories.Any(z => z.Id == y.Id)))
.ToList();
EDIT: that wouldn't be the correct thing to do if you have a lot of entities on database, what you can do is to split it into two queries:
var tables = ctx.Tables
.Where(x =>
x.Categories.Any(y =>
myCategories.Any(z => z.Id == y.Id)));
var result = ctx.Chairs
.Where(x =>
tables.Any(t=> t.Id == x.TableId))
.ToList();
You can select Ids from myCategories and use it last statement.
var CategoryIds = myCategories.Select(ct => ct.Id);
var result = ctx.Chairs.Where(x => x.Table.Categories.Any(y => CategoryIds.Any(z => z == y.Id))).ToList();

Group nested list with linq

I have a nested list of objects. That I need to group by identifierA and Sum its numeric properties, nested list shall group respectively:
public class TypeA
{
public String identifierA{ get; set; }
public Int32 number { get; set; }
public List<TypeB> nestedList { get; set; }
}
public class TypeB
{
public String identifierB { get; set; }
public Int32 otherNumber { get; set; }
}
So I'm expecting something like this:
var List<TypeA> groupedList = (from a in TypeAList
group a by a.identifierA
into groupedData
select new TypeA
{
identifierA = groupedData.Key,
number = groupedData.Sum(g => g.number ),
nestedList = //HOW TO GROUP NESTED PART?
}).ToList();
I think that this will resolve your issue.
List<TypeA> list = TypeAList
.GroupBy(a => a.identifierA)
.Select(
g =>
new TypeA
{
identifierA = g.Key,
number = g.Sum(n => n.number),
nestedList =
g.SelectMany(l => l.nestedList)
.GroupBy(b => b.identifierB)
.Select(
gg =>
new TypeB
{
identifierB = gg.Key,
otherNumber = gg.Sum(b => b.otherNumber)
}).ToList()
}).ToList();
SelectMany takes an IEnumerable<SomethingWithAnIEnumerable> and flattens all the SomethingWithAnIEnumerable's selected IEnumerables into a single IEnumerable:
nestedList = groupedData.SelectMany(pa => pa.nestedList).ToList()
use SelectMany
if you want to group the list into one use
nestedList = groupedData.SelectMany(d=>d.nestedList)
and if you want Sum of that list, use
nestedList = groupedData.SelectMany(d=>d.nestedList).Sum(o=>o.otherNumber)
I think your IdentfierB might be a key of some kind, and your result should reflect Grouped and Summed TypeBs.
List<TypeA> groupedList = TypeAList
.GroupBy(a => a.identifierA)
.Select(g => new TypeA()
{
identierA = g.Key,
number = g.Sum(a => a.number)
nestedList = g.SelectMany(a => a.nestedList)
.GroupBy(b => b.identifierB)
.Select(g2 => new TypeB()
{
identifierB = g2.Key,
otherNumber = g2.Sum(b => b.otherNumber)
}
}

Categories