c# - Linking two lists efficiently - c#

I have two lists:
Products
A list of Product and Warehouse combination containing
prices/quantities etc.
Two seperate results of sql queries.
The second list has a 'ProductId'.
What I'm currently doing is:
foreach(var product in Products)
var productWarehouses = Warehouses.Where(x=> x.ProductId == productId).ToList();
product.Warehouses = productWarehouses;
Thing is, this takes very, very long.
Note: I've tried splitting Products into chunks of lists and using Parallel.Foreach() and Tasks to take the time down - but still takes very long.

Use a Join rather than doing a linear search through the entirety of one collection for each item in the other collection:
var query = from product in Products
join warehouse in Warehouses
on product.productId equals warehouse.ProductId
into warehouses
select new
{
product,
warehouses,
};

Instead of doing this in c#, I would prefer to do it in SQL stored procedure. Get all details from one sql and then iterate though result to create Product list.

Create a Dictionary<int, Product> from your Products where Product.ProductID is used as key:
var pd = Products.ToDictionary(p => p.ProductID, p => p);
then you can iterate over Warehouses and lookup appropriate products fast:
foreach (var wh in Warehouses)
{
pd[wh.ProductId].Warehouses.Add(wh); //supposed Product.Warehouses lists have already been created, if not, add checks and create them as needed.
}

Related

Linq and RESTful services: how to best merge data from multiple tables in a resultset

I'm experimenting with pulling data from multiple datasets using RESTful services. I'm hooking up to the Cloud version of Northwind, and attempting to use Linq to get the equivalent of this:
SELECT TOP 20 p.ProductName, p.ProductID, s.SupplierID, s.CompanyName AS Supplier,
s.ContactName, s.ContactTitle, s.Phone
FROM Products p
JOIN Suppliers s on p.SupplierID = s.SupplierID
ORDER BY ProductName
So, I define a class to hold my data:
public class ProductSuppliers
{
public string ProductName;
public int ProductID;
public string SupplierName;
public string ContactName;
public string ContactPosition;
public string ContactPhone;
}
And hook into the Northwind service:
NorthwindEntities dc = new NorthwindEntities (new
Uri("http://services.odata.org/Northwind/Northwind.svc/"));
After trying to set up a join, not being able to get it to work, and wandering around in the back corridors of MSDN for a while, I find that Linq joins aren't supported by the OData spec. Which seems obvious once you think about it, given the limitations of URI syntax.
Of course, the usual thing to do is stored procs and views on the server side anyway, handling any sort of joins there. However, I wanted to work out some sort of solution for a situation like this one, where you don't have the capability of creating stored procs or views.
My naive solution has all the elegance of medieval battlefield surgery, and it has to scale horribly. I pulled the two tables as two separate List objects, then iterated one, used Find to locate the matching ID in the other, and Added a combined record into my Product. Here's the code:
public List<ProductSuppliers> GetProductSuppliers()
{
var result = new List<ProductSuppliers>();
ProductSuppliers ps;
var prods =
(
from p in dc.Products
orderby p.ProductName
select p
).ToList();
var sups =
(
from s in dc.Suppliers
select s
).ToList();
foreach (var p in prods)
{
int cIndex = sups.IndexOf(sups.Find(x => x.SupplierID == p.SupplierID));
ps = new ProductSuppliers()
{
ProductName = p.ProductName,
ProductID = p.ProductID,
SupplierName = sups[cIndex].CompanyName,
ContactName = sups[cIndex].ContactName,
ContactPosition = sups[cIndex].ContactTitle,
ContactPhone = sups[cIndex].Phone
};
result.Add(ps);
}
return result;
}
There has to be something better than this, doesn't there? Is there something obvious I'm missing?
[Edit] I've looked at the link someone gave me on the Expand method, and that works...sort of. Here's the code change:
var sups =
(
from s in dc.Suppliers.Expand("Products")
select s
).ToList();
This gives me a list of Suppliers with Products for each in a sublist (dc.Suppliers[0].Products[0], etc.). While I could get what I want from there, I'd still have to iterate the entire list to invert the values (wouldn't I?), so it doesn't look like a more scaleable solution. Also, I can't apply Expand to the Products table to include Suppliers (Changing the from clause in prods to from p in dc.Products.Expand("Suppliers") results in a helpful "An Error occurred while processing this request."). So, it doesn't look like I can expand products to include lookup values from Suppliers, since it looks like expanding is expanding parents to include children, not looking up parent values in a list of children. Is there a way to use Expand (or is there some other mechanism besides client-side manipulation of the two tables) to include lookup values from a foreign key table?
The best you can do is described in this SO answer to a similar question. Not what you expected either, since you're required to make multiple roundtrips to the service.
If you don't control the server-side of things (or you don't want to use SPs/views/joins there) you are forced to use one of these mechanisms.
Anyway, at the very least you can improve the products-suppliers matching in your code to this:
var results = from p in prods
join s in sups on s.SupplierId equals p.SupplierId
select new ProductSuppliers()
{
ProductName = p.ProductName,
ProductID = p.ProductID,
SupplierName = s.CompanyName,
ContactName = s.ContactName,
ContactPosition = s.ContactTitle,
ContactPhone = s.Phone
};
You still need to retrieve all records and join in-memory, though.

How do I select an object by a sub-property

i've got a List of objects, lets call them Product, which each of them contains a bunch of properties and also a List of Version (which are also objects).
Version also has a bunch of properties and does contain a List of Customer (which again are objects).
Customer again has properties, one of them is its ID (=Guid).
What i try to do is to make a List of Product, selected by a certain ID of its Product.VersionList.Version.ID.
I would prefere a join query, but every efficient way is welcome. I tried so far this, but because i have only a single ID to compare with, i don't know how to construct the join.
lp = List<Entity.Product>;
g = GetGuid();
var query = from product in Entity.ProductCollection
join g in g
on product.Version.Where(x => x.id == g)
select product;
lp.AddRange(query);
I'm guessing you mean:
var query = from product in Entity.ProductCollection
where product.Version.Any(x => x.id == g)
select product;
i.e. select all the products that have a version where the id matches the guid you were thinking of.
Note that joining to the versions would cause product duplication if any product has multiple matching versions.
Try this .... May be you wants more deep digging on it..
var query = from Product product in pc
from varsion in product.Version
let v= varsion as Entity.Version
where v.id == g
select product;
var query = Entity.ProductCollection.Where(p => p.Version.Any(v => v.Id == g));
You can use Any rather than having to do a self join.

Splitting Linq List by grouping

for reporting purposes i wanna split a list of purchase orders into multiple lists. One list for each purchase address. I know it's possible to group the list by purchase address, but my question is how to split the list by this purchase address into multiple lists and use these multiple list to create individual reporting files.
code:
(from o in orders
group o by new {field1, field2, field3, field4} into og
orderby og.Key.field1
select new ViewClass
{
purchaseAddress = og.Key.field1,
product = og.key.field2,
count = og.count
}).ToList()
question: how to split above list into multiple lists for each purchaseAddress?
There's a built-in function that I think does what you want. If I assume that your code is assigned to a variable called query then you can write this:
ILookup<string, ViewClass> queryLists = query.ToLookup(x => x.purchaseAddress);
This essentially creates a list of lists that you access like a dictionary, except that each value is a list of zero or more values. Something like:
IEnumerable<ViewClass> someList = queryLists["Some Address"];
Just turn each group into a List.
select new ViewClass
{
purchaseAddress = og.Key.field1,
product = og.key.field2,
count = og.count,
List = og.ToList()
}).ToList();
Oh, your grouping is one way for entities and another way for pages... just regroup.
List<ViewClass> classes = (
from o in orders
group o by new {field1, field2, field3, field4} into og
orderby og.Key.field1
select new ViewClass
{
purchaseAddress = og.Key.field1,
product = og.key.field2,
count = og.count
}).ToList();
List<List<ViewClass>> regrouped = (
from c in classes
group c by c.purchaseAddress into g
select g.ToList()
).ToList();
Another simple built-in function that you can use is the GroupBy function. It does a similar job as the ToLookup but it means that your new list is IQuerable, not like a dictionary and a few other things (see this article for a good breakdown)
var newList = orders.GroupBy(x => x.field1);
This will return a list of lists grouped by the field(s) you specify.

Whats the 'modern' way to find common items in two Lists<T> of objects?

I have two Generic Lists containing different types, for the sake of example, lets call them Products and Employees. I'm trying to find Products that are based at the same location as Employees, i.e. where product.SiteId == emp.SiteId
List<Product> lstProds;
List<Employees> lstEmps;
My (old skool) brain is telling me to use a forEach loop to find the matches but I suspect there is a ('better'/terser/faster?) way to do it using Linq. Can anyone illuminate me? All the examples I've found online deal with Lists of primitives (strings/ints) and are not especially helpful.
I would say:
var products = from product in lstProds
join employee in lstEmps on product.SiteId equals employee.SiteId
select product;
However, if there are multiple employees with the same site ID, you'll get the products multiple times. You could use Distinct to fix this, or build a set of site IDs:
var siteIds = new HashSet<int>(lstEmps.Select(emp => emp.SiteId));
var products = lstProds.Where(product => siteIds.Contains(product.SiteId));
That's assuming SiteId is an int - if it's an anonymous type or something similar, you may want an extra extension method:
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
{
return new HashSet<T>(source);
}
Then:
var siteIds = lstEmps.Select(emp => emp.SiteId).ToHashSet();
var products = lstProds.Where(product => siteIds.Contains(product.SiteId));
Alternatively, if you have few employees, this will work but is relatively slow:
var products = lstProds.Where(p => lstEmps.Any(emp => p.SiteId == emp.SiteId));
Add a ToList call to any of these approaches to get a List<Product> instead of an IEnumerable<Product>.

Combine two items in a list

I need a way to reduce a list, or calculate a "Total." I have a class, lets call it Prod. Prod contains 4 values. One is the name of the product, the id, a serial number, and a quantity. Basically I have one product but 2 different serial numbers. So when I get my data back from my query I have 2 items which I want to treat as a single item. How can I go about using LINQ or something else (I cannot foreach over them. There are many more class members and that would take a while plus look terrible). I want to be able to take the 2 instances and combine their serial numbers (not add just Serail1 - Serial 2) and also calculate the quantities together.
I think what you want is the Linq grouping function (see GroupBy - Simple 3). This should give you a list of serial numbers and their quantity count:
public void Linq42()
{
List<Prod> products = GetProductList();
var serialCombined =
from p in products
group p by p.SerialNumber into g
select new { SerialNumber = g.Key, Total = g.Count() };
}
Use the join operator and place them in a Tuple. You can then call more LINQ on the tuples or iterate over them.
var combinedProducts =
from product1 in products1
join product2 in products2 on product1.serial equals product2.serial
select Tuple.Create(product1, product2);
// Use LINQ to calculate a total
combinedProducts.Sum(combined => combined.Item1.count * combined.Item2.price);
// Or use foreach to iterate over the tuples
foreach (var combined in combinedProducts) {
Console.WriteLine("{0} and {1}", combined.Item1.name, combined.Item2.name);
}

Categories

Resources