C# Linq Join Issue - c#

I am trying to join two object list and update the property(Revenue) in the first object (ProductReport) to be the same the one of the properties(Revenue) in the joined object (Transaction). The code is as blow:
using System.Collections.Generic;
using System.Linq;
namespace TestConsole
{
public class ProductReport
{
public string productName { get; set; }
public int PrdouctID { get; set; }
public double Revenue { get; set; }
}
public class Transaction
{
public double Revenue { get; set; }
public int PrdouctID { get; set; }
}
public class Program
{
static void Main(string[] args)
{
List<ProductReport> products = new List<ProductReport>();
ProductReport p = new ProductReport();
p.productName = "Sport Shoe";
p.PrdouctID = 1;
products.Add(p);
List<Transaction> transactions = new List<Transaction>();
Transaction t1 = new Transaction();
t1.PrdouctID = 1;
t1.Revenue = 100.0;
Transaction t2 = new Transaction();
t2.PrdouctID = 1;
t2.Revenue = 200.00;
transactions.Add(t1);
transactions.Add(t2);
var results = (from product in products
join transaction in transactions on product.PrdouctID equals transaction.PrdouctID into temp
from tran in temp.DefaultIfEmpty()
select new { product, tran }
).ToList();
results.ForEach(x => x.product.Revenue = x.tran.Revenue);
}
}
}
The code joins 2 object lists based on ProductID. I am expecting to see two ProductReport items in the results object with 100.00 and 200.00 revenue respectively. However, the foreach function somehow overwrite the revenue property in the first ProductReport. In the end, both ProductReport have 200.00 as the revenue. Can anyone help me with that and how to easily get the result i want
? Thanks.

You'll just need to change your join around
var results = (from p in products
join t in transactions on p.ProductID equals t.ProductId
select new ProductReport{
ProductName = p.productName,
ProductId = p.ProductID,
Revenue = t.Revenue
}).ToList();
You'll then have a list which has 2 entries for ProductId = 1 (Sport Shoe)
ProductName = "Sport Shoe"
ProductId = 1
Revenue = 200.00
ProductName = "Sport Shoe"
ProductId = 1
Revenue = 100.00

The foreach function somehow overwrite the revenue property in the first ProductReport. In the end, both ProductReport have 200.00 as the revenue.
In the following line x.product refers to the same instance.
results.ForEach(x => x.product.Revenue = x.tran.Revenue);
You have only one ProductReport object, and it can have only one value for its Revenue property. The Revenue property of this single instance cannot simultaneously be both 100.00 and 200.00. The first time you write to it, you set it to 100.00, the second time you write to it, you overwrite it to 200.00.
I am expecting to see two ProductReport items in the results object.
Your code creates only one ProductReport item. If you want to see two (or more) of them, then you need to create two (or more) objects. The following code leverages Select to do that for you. Whereas your ForEach mutates the original object, Select and the rest of the LINQ create a new object and do not mutate the original one.
var results =
from product in products
join transaction in transactions on product.PrdouctID equals transaction.PrdouctID
select new
{
product, transaction
};
var updated = results.Select(x => {
x.product.Revenue = x.transaction.Revenue;
return x.product;
});
And here it is as a Fiddle with this output:
Sport Shoe 100
Sport Shoe 200

Related

Select count from joined tables

I have two tables, CaseProductLinks and Products as shown here:
I am trying to get the following information using LINQ:
Here's what I would do in SQL:
SELECT
p.ProductID, p.ProductName,
COUNT(c.CaseID) AS Frequency
FROM
CaseProductLinks c
JOIN
Products p ON c.ProductID = p.ProductID
GROUP BY
p.ProductID
Here's what I have in C# so far which throws a "System.InvalidOperationException":
var objs = from p in _db.CaseProductLinks
join c in _db.Cases on p.ProductId equals c.ProductId into g
select new S_Link
{
ProductID = p.ProductId,
ProductName = p.Product,
Frequency = g.Count() //Not sure what to do here
};
If you've set your navigation up correctly (i.e. a Product has an ICollection<CaseProductLink> CaseProductLinks) you can simply do:
var r = _db.Products.Select(p =>
new
{
p.ProductId,
p.ProductName,
Frequency = p.CaseProductLinks.Count()
}
);
Here's what I would do in SQL:
If you're quite used to SQL it can be a step to pull yourself away from thinking in those ways and into the ways that EF is designed to abstract over them. One of the big plus points of EF is in telling it how your database tables/entities relate to each other, and then it will form the joins. It's not some dumb device that has to be pummeled into making every join and group that it does; if it knew there was 1 Product with Many CaseProductLink then it can write the join/group that counts the number of relates CPL per P simply by accessing the collection on Product with an operation that aggregates (Count)
If you don't have this nav set up, then I really would recommend to do so, as it's a big chunk of the beauty of EF that makes C# side code nice to work with
I Test Below Code And Work Fine.Please check it
public class Productlinks
{
public int CaseId { get; set; }
public int ProductId { get; set; }
}
public class Products
{
public int ProductId { get; set; }
public string ProductName { get; set; }
}
static void Main(string[] args)
{
var products = new List<Products>()
{
new Products(){ ProductId=1, ProductName="A"},
new Products(){ ProductId=2, ProductName="B"},
new Products(){ ProductId=3, ProductName="C"},
};
var links = new List<Productlinks>()
{
new Productlinks(){ CaseId=1, ProductId=1 },
new Productlinks(){ CaseId=3, ProductId=2 },
new Productlinks(){ CaseId=3, ProductId=2 },
new Productlinks(){ CaseId=4, ProductId=3 },
};
var objs = from p in products
join c in links on p.ProductId equals c.ProductId into g
select new
{
ProductID = p.ProductId,
ProductName = p.ProductName,
Frequency = g.Count()
};
}

ef-core nested table aggregate functions give "NavigationExpandingExpressionVisitor failed"

I have a nested data model and I'd like to get aggregated data from it grouped by a top level property.
My models for example:
public class Scan {
public long Id {get; set;}
public int ProjectId { get; set; }
public int ScanUserId { get; set; }
public ICollection<ScanItem>? Items { get; set; }
}
public class ScanItem
{
public long Id { get; set; }
public long InventoryScanId { get; set; }
public double Qty { get; set; }
}
I'd like to get all Scans grouped by Scan.ScanUserId and then then get the sum of ScanItems.Qty for example per user. My query looks ike this and EF gives the following error:
Processing of the LINQ expression 'AsQueryable((Unhandled
parameter: x).Items)' by 'NavigationExpandingExpressionVisitor' failed
from scan in Scans
.Include(x=>x.ScanUser)
.Include(x=>x.Items)
group scan by new { scan.ScanUser.Name, scan.ScanUser.Id } into g
select new
{
UserId = g.Key.Id,
Name = g.Key.Name,
LastSyncTime = g.Max(x => x.ScanDate),
ScanItems = g.Sum(x=>x.Items.Sum(i=>i.Qty))
}
How can I run aggregate functions on the properties of the nested table without evaluating it on the client side?
EF Core still can't translate nested aggregates on GroupBy result (grouping).
You have to pre calculate the nested aggregates in advance by utilizing the element selector of GroupBy (or element in query syntax group element by key):
from scan in Scans
group new { scan.ScanDate, Qty = scan.Items.Sum(i => i.Qty) } // <--
by new { scan.ScanUser.Name, scan.ScanUser.Id } into g
select new
{
UserId = g.Key.Id,
Name = g.Key.Name,
LastSyncTime = g.Max(x => x.ScanDate),
ScanItems = g.Sum(x => x.Qty) // <--
}
**Update: ** for SqlServer the above LINQ query translates to SQL query like this:
SELECT [s1].[Id] AS [UserId], [s1].[Name], MAX([s0].[ScanDate]) AS [LastSyncTime], SUM((
SELECT SUM([s].[Qty])
FROM [ScanItem] AS [s]
WHERE [s0].[Id] = [s].[InventoryScanId])) AS [ScanItems]
FROM [Scan] AS [s0]
INNER JOIN [ScanUser] AS [s1] ON [s0].[ScanUserId] = [s1].[Id]
GROUP BY [s1].[Name], [s1].[Id]
which as mentioned in the comment generates SQL execution exception "Cannot perform an aggregate function on an expression containing an aggregate or a subquery.".
So you really need another approach - use left join to flatten the result set before grouping, and then perform the grouping / aggregates on that set:
from scan in Scans
from item in scan.Items.DefaultIfEmpty() // <-- left outer join
group new { scan, item } by new { scan.ScanUser.Name, scan.ScanUser.Id } into g
select new
{
UserId = g.Key.Id,
Name = g.Key.Name,
LastSyncTime = g.Max(x => x.scan.ScanDate),
ScanItems = g.Sum(x => (double?)x.item.Qty) ?? 0
};
which now translates to hopefully valid SqlServer SQL query:
SELECT [s1].[Id] AS [UserId], [s1].[Name], MAX([s].[ScanDate]) AS [LastSyncTime], COALESCE(SUM([s0].[Qty]), 0.0E0) AS [ScanItems]
FROM [Scan] AS [s]
LEFT JOIN [ScanItem] AS [s0] ON [s].[Id] = [s0].[InventoryScanId]
INNER JOIN [ScanUser] AS [s1] ON [s].[ScanUserId] = [s1].[Id]
GROUP BY [s1].[Name], [s1].[Id]

Building a new list of Object with combination of other lists of Objects

I have two lists of Client Object:
public class Client
{
public int ClientID { get; set; }
public string Name { get; set; }
public string DCCode { get; set; }
public string CountryName { get; set; }
}
my list A hold 4 items ClientID , and name fields are populated,
My List B Hold the same 4 items but there is no name and hold the ClientID DCCode, and CountryName
i need to either Update list A DCCode and Countryname with corresponding values in list B
or create List C which it hold 4 items with complete value of list A and B together
Like :
List C L: item 1 : ClientID : 1, Name: XXYY, DCCode :4, CountryName: UK
I can do this using for loop, but i have been trying to use LINQ
i have tried the following codes but i could not find the correct way to get the result i want
Solution 1
Clients1.Where(i => Clients2.Any(a=> i.CLinetID == a.CLinetID))
Solution 2:
Clients1.Concat(Clients1).Concat(Clients2).ToList();
Any help would be welcomed
As you have the ClientID field populated in both lists join them by that property and project a new object populated with all fields:
var result = from c1 in Clients1
join c2 in Clients2 on c1.ClientID equals c2.ClientID
select new Client { ClientID = c1.ClientID, Name = c1.Name, DCCode = c2.DCCode, CountryName = c2.CountryName };
This will create the third list. You can also update the items of Clients1 likewise:
foreach (var c1 in Clients1)
{
var c2 = Clients2.FirstOrDefault(i => i.ClientID == c1.ClientID);
if(c2 != null)
{
c1.DCCode = c2.DCCode;
c1.CountryName = c2.CountryName;
}
}
For updating entities in first list you can create dictionary from second list - that will allow you to quickly find corresponding entity with O(1):
var clientsByID = listB.ToDictionary(c => c.ClientID);
foreach(var clientA in listA)
{
var clientB = clientsByID[clientA.ClientID];
clientA.DCCode = clientB.DCCode;
clientA.CountryName = clientB.CountryName;
}
You can also join two lists on ClientID property and produce new list from results (if enumerable is good for you, then I would go with query syntax instead):
var listC = listA.Join(listB,
a => a.ClientID,
b => b.ClientID,
(a,b) => new Client {
ClientID = a.ClientID,
Name = a.Name,
DCCode = b.DCCode,
CountryName = b.CountryName
}).ToList();

Select element in linq and change properties

Lets say I have two lists objects with this classes
class Sku
{
public string skuId { get; set; }
public int qty { get; set; }
public string city { get; set; }
}
class SkuWithCity
{
public string skuId { get; set; }
public string city { get; set; }
}
And I have two lists with objects:
List<Sku> skuList = new List<Sku>();
List<SkuWithCity> skuListWithCity = List<SkuWithCity>();
Imagine that in the first list(skuList), the property "City" of each object is null.
What I want to do is, using linq, select the sku objects that have the same skuId and add the city. Somethis like:
var result = from skuElements in skuList
from skuWCity in skuListWithCity
where skuElements.sku == skuWCity.sku
select skuElements
{
skuElements.city = skuWCity.city,
};
And get the whole object, not just the city
int order to get:
|Object | Qty | City
|---- |----|
|AAA | 2 | Panama|
|BBB | 5 | Rio De Janeiro|
is this even possible, get the whole object and modify one or many properties?
UPDATE: In real life the object that I'm trying to copy has a lot of members, that is why I'm trying to "copy" de object of list A and just modify some attributes using the match object of list B.
If you just want to update the existing objects instead of projecting to a new set of objects then first use a join to get the matching items.
var result = from skuElement in skuList
join skuWCity in skuListWithCity
on skuElements.skuId == skuWCity.skuId
select skuElements
{
skuElement,
skuWCity
};
Then iterate them and do the update.
foreach(var x in result)
{
x.skuElement.City = x.skuWCity.City;
}
Note this does not handle the case where more than one item in either list has more than one match in the other. I'm assuming that the lists have a one-to-one match already.
Alternatively you could just use a dictionary.
var cities = skuListWithCity.ToDictionary(s => s.skuId, s => s.City);
foreach(var s in skuList)
{
s.City = cities[s.skuId];
}
Note that this fails if there are duplicate skuId in skuListWithCity or if a skuId in skuList is not in skuListWithCity
You could use a join and then make a projection of the result set as below:
var result = from skuElements in skuList
join skuWCity in skuListWithCity
on skuElements.skuId equals skuWCity.skuId
select new Sku
{
skuId = skuElements.skuId,
qty = skuElements.qty,
city = skuWCity.city,
};

Can you add a Where() to an IQueryable when the field is not in the selected output

Suppose I have a 2 table join in a function that returns an IQueryable, but the output is a named type that is neither of the two tables:
var qry = from p in Persons
join h in Hobbies on p.PersonId equals h.PersonId
select new OutputType
{
Name = p.FirstName,
Hobby = h.HobbyName
}
return qry
Let's say now I wanted to take this returned query and do something like:
var newQuery = qry.Where( p=>p.Age > 18 )
As you can see this is a problem because the IQueryable is of type OutputType, so I can't add a where to a person's age unless I were to add the Age to OutputType.
Is there anyway of 'breaking into' the IQueryable expression tree and adding a lambda somehow that will query on the source collection specified in it and add a Where clause to it? Or do I have do I have to add a Where field to the OutputType even though I'm uninterested in ultimately projecting it?
It is easier to narrow your view later than to try to backtrack. Here is a stripped down example of how I like to layer methods for reuse so that they spit out nice sql.
private IQueryable<Part> GetParts_Base()
{
//Proprietary. Replace with your own.
var context = ContextManager.GetDbContext();
var query = from c in context.Component
where c.Active
//kind of pointless to select into a new object without a join, but w/e
select new Part()
{
PartNumber = c.ComponentNumber,
Description = c.ComponentDescription,
Cost = c.ComponentCost,
Price = c.ComponentPrice
};
return query;
}
//Exclude cost from this view
public IEnumerable<Part_PublicView> GetParts_PublicView(decimal maxPrice)
{
var query = GetParts_Base();
var results = from p in query
where p.Cost < maxPrice
select new Part_PublicView()
{
PartNumber = p.PartNumber,
Description = p.Description,
Price = p.Price
};
return results;
}
public class Part_PublicView
{
public string PartNumber { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
private class Part : Part_PublicView
{
public decimal Cost { get; set; }
}
Linq-to-entity does not penalize you for selecting the extra column early on. As you can see, the sql includes the Cost column in the constraint but not in the select.
SELECT
1 AS [C1],
[Extent1].[ComponentNumber] AS [ComponentNumber],
[Extent1].[ComponentDescription] AS [ComponentDescription],
[Extent1].[ComponentPrice] AS [ComponentPrice]
FROM [dbo].[Component] AS [Extent1]
WHERE [Extent1].[ComponentCost] < #p__linq__0

Categories

Resources