How can I improve this LINQ query? - c#

I fear that I'm doing n+1 query here, how can I improve this?
var inventories = AppContext.Inventories
.GroupBy(i => new { i.LocationId, i.ProductId })
.Select(g => new InventoryAvailableQuantity
{
ProductId = g.Key.ProductId,
LocationId = g.Key.LocationId,
Product = g.FirstOrDefault().Product.Name,
Location = g.FirstOrDefault().Location.Name,
PurchasePrice = AppContext.Inventories.Where(i => i.ProductId == g.Key.ProductId).OrderByDescending(i => i.DateAdded).FirstOrDefault().PurchasePrice,
ResellerPrice = AppContext.Inventories.Where(i => i.ProductId == g.Key.ProductId).OrderByDescending(i => i.DateAdded).FirstOrDefault().ResellerPrice,
RetailPrice = AppContext.Inventories.Where(i => i.ProductId == g.Key.ProductId).OrderByDescending(i => i.DateAdded).FirstOrDefault().RetailPrice
}).ToList();

You can use comprehension instead of method and gain the ability to use "let":
var inventories = from inv in AppContext.Inventories
group inv by new { i.LocationId, i.ProductId } into g
let firstInv = g.FirstOrDefault()
let firstPur = AppContext.Inventories
.Where(i => i.ProductId == g.Key.ProductId)
.OrderByDescending(i => i.DateAdded)
.FirstOrDefault()
select new InventoryAvailableQuantity
{
ProductId = g.Key.ProductId,
LocationId = g.Key.LocationId,
Product = firstInv.Product.Name,
Location = firstInv.Location.Name,
PurchasePrice = firstPur.PurchasePrice,
ResellerPrice = firstPur.ResellerPrice,
RetailPrice = firstPur.RetailPrice
}; // ( select ... { ... }).ToList(); if you will

Fast answer
var inventories = Inventories
.GroupBy(i => new {i.LocationId, i.ProductId})
.Select(g => new
{
g.Key.ProductId,
g.Key.LocationId,
CurrentInventories = g.FirstOrDefault(),
LastInventories = Inventories.Where(i => i.ProductId == g.Key.ProductId).OrderByDescending(i => i.DateAdded).FirstOrDefault()
})
.Select(g => new InventoryAvailableQuantity
{
ProductId = g.ProductId,
LocationId = g.LocationId,
Product = g.CurrentInventories.Product.Name,
Location = g.CurrentInventories.Location.Name,
PurchasePrice = g.LastInventories.PurchasePrice,
ResellerPrice = g.LastInventories.ResellerPrice,
RetailPrice = g.LastInventories.RetailPrice
})
.ToList();
You can take last item after grouping and take what you want.

Related

Does not display data in linq C#

I have Linq which counts the goods, the problem is that the names that I pass, they do not work
ProductName, CompanyName, CustomerName,
Maybe there is a error in Linq?
It produces many anonymous methods that have these fields, but after ToList() everything does not work
public async Task<IEnumerable<SalesReportItem>> GetReportData(DateTime dateStart, DateTime dateEnd)
{
dateStart = new DateTime(2000, 1, 1);
var context = await _contextFactory.CreateDbContextAsync();
var queryable = context.SalesTransactionRecords.Join(
context.Products,
salesTransactionRecords => salesTransactionRecords.ProductId,
products => products.Id,
(salesTransactionRecords, products) =>
new
{
salesTransactionRecords,
products
})
.Join(context.Companies,
combinedEntry => combinedEntry.salesTransactionRecords.CompanyId,
company => company.Id,
(combinedEntry, company) => new
{
combinedEntry,
company
})
.Join(context.VendorCustomers,
combinedEntryAgain => combinedEntryAgain.combinedEntry.salesTransactionRecords.CustomerId,
vendorCustomer => vendorCustomer.Id,
(combinedEntryAgain, vendorCustomer) => new
{
CompanyName = combinedEntryAgain.company.Name,
CustomerName = vendorCustomer.Name,
ProductId = combinedEntryAgain.combinedEntry.products.Id,
ProductName = combinedEntryAgain.combinedEntry.products.Name,
combinedEntryAgain.combinedEntry.salesTransactionRecords.MovementType,
combinedEntryAgain.combinedEntry.salesTransactionRecords.Period,
combinedEntryAgain.combinedEntry.salesTransactionRecords.Quantity,
combinedEntryAgain.combinedEntry.salesTransactionRecords.Amount,
}).Where(x => x.Period >= dateStart && x.Period <= dateEnd)
.GroupBy(combinedEntryAgain => new
{
combinedEntryAgain.ProductId,
combinedEntryAgain.ProductName,
combinedEntryAgain.CompanyName,
combinedEntryAgain.CustomerName,
}
).Select(x => new SalesReportItem
{
ProductId = x.Key.ProductId,
Quantity = x.Sum(a => a.Quantity),
Amount = x.Sum(x => (x.MovementType == TableMovementType.Income ? x.Amount : -(x.Amount)))
});
var items = await queryable.ToListAsync();
return _mapper.Map<IEnumerable<SalesReportItem>>(items);
}
my mistake was that I did not specify the fields in the select, otherwise everything is buzzing, the upper code is working
Select(x => new SalesReportItem
{
ProductId = x.Key.ProductId,
ProductName = x.Key.ProductName,
CompanyName = x.Key.CompanyName,
CustomerName = x.Key.CustomerName,
Quantity = x.Sum(x => (x.MovementType == TableMovementType.Income ? x.Quantity : - x.Quantity)),
Amount = x.Sum(x => (x.MovementType == TableMovementType.Income? x.Amount: - x.Amount))
});
Thanks for the help
Hans Kesting

How do I create groups and subgroup1 and subgroup2 Use linq

How do I create groups and subgroup1 and subgroup2 Use linq.
Example of this picture
I want to create json.
Example of this picture.
I tried to do this but there was a problem.
The items are repeated within one subgroup2.
var list = result
.GroupBy(x => new { x.GroupId, x.GroupName })
.Select(g => new
{
ID = g.Key.GroupId,
Name = g.Key.GroupName,
SubGroup1 = g.GroupBy(x => new { x.SubGroupID1, x.SubGroupName1 })
.Select(cg => new
{
ID = cg.Key.SubGroupID1,
Name = cg.Key.SubGroupName1,
SubGroup2 = g.GroupBy(x => new { x.SubGroupID2, x.SubGroupName2 })
.Select(ii => new
{
ID = ii.Key.SubGroupID2,
Name = ii.Key.SubGroupName2,
item = ii.GroupBy(x => new { x.Stock_Id, x.Stock_Name, x.Prices, x.ScreenNumber })
.Select(oo => new
{
Stock_Id = oo.Key.Stock_Id,
Stock_Name = oo.Key.Stock_Name,
Prices = oo.Key.Prices,
ScreenNumber = oo.Key.ScreenNumber
}).OrderBy(Or => Or.Stock_Id)
.ToList()
}).OrderBy(Or => Or.ID)
.ToList()
}).OrderBy(Or => Or.ID)
.ToList()
}).OrderBy(Or => Or.ID)
.ToList();
Your query could be a lot cleaner if you grouped the groups up front, then project out to your desired results.
var query =
from x in data
group new { x.StockId, x.StockName, x.Prices, x.ScreenNumber }
by new { x.GroupId, x.GroupName, x.SubGroupId1, x.SubGroupName1, x.SubGroupId2, x.SubGroupName2 }
into g
group g
by new { g.Key.GroupId, g.Key.GroupName, g.Key.SubGroupId1, g.Key.SubGroupName1 }
into g2
group g2
by new { g2.Key.GroupId, g2.Key.GroupName }
into g1
select new
{
Id = g1.Key.GroupId,
Name = g1.Key.GroupName,
SubGroup1 = g1.Select(g2 => new
{
Id = g2.Key.SubGroupId1,
Name = g2.Key.SubGroupName1,
SubGroup2 = g2.Select(g => new
{
Id = g.Key.SubGroupId2,
Name = g.Key.SubGroupName2,
Items = g.Select(x => new
{
x.StockId,
x.StockName,
x.Prices,
x.ScreenNumber,
}),
}),
}),
};
The idea is to start off with the most specific grouping first, then one-by-one group the groups by the next layer, and so on.
SubGroup2 = g.GroupBy(x => new { x.SubGroupID2, x.SubGroupName2 })
You are grouping g instead of cg.
I suggest structuring your code a bit, which would help avoiding this kind of mistake.

Optimize linq query by storing value in select

I have problem with linq query. In Select I am getting the same item twice which makes code execution much longer than I can afford. Is there any way to store x.OrderByDescending(z => z.Date).FirstOrDefault() item inside Select query?
Execution time: 180 ms
var groups = dataContext.History
.GroupBy(a => new { a.BankName, a.AccountNo })
.Select(x => new HistoryReportItem
{
AccountNo = x.FirstOrDefault().AccountNo,
BankName = x.FirstOrDefault().BankName,
IsActive = x.FirstOrDefault().IncludeInCheck,
})
.ToList();
Execution time: 1200 ms
var groups = dataContext.History
.GroupBy(a => new { a.BankName, a.AccountNo })
.Select(x => new HistoryReportItem
{
AccountNo = x.FirstOrDefault().AccountNo,
BankName = x.FirstOrDefault().BankName,
IsActive = x.FirstOrDefault().IncludeInCheck,
LastDate = x.OrderByDescending(z => z.Date).FirstOrDefault().Date,
})
.ToList();
Execution time: 2400 ms
var groups = dataContext.History
.GroupBy(a => new { a.BankName, a.AccountNo })
.Select(x => new HistoryReportItem
{
AccountNo = x.FirstOrDefault().AccountNo,
BankName = x.FirstOrDefault().BankName,
IsActive = x.FirstOrDefault().IncludeInCheck,
LastDate = x.OrderByDescending(z => z.Date).FirstOrDefault().Date,
DataItemsCount = x.OrderByDescending(z => z.Date).FirstOrDefault().CountItemsSend
})
.ToList();
You can try doing the select in two steps:
var groups = dataContext.History
.GroupBy(a => new { a.BankName, a.AccountNo })
.Select(x => new
{
first = x.FirstOrDefault();
lastDate = x.OrderByDescending(z => z.Date).FirstOrDefault();
}
.Select(x => new HistoryReportItem
{
AccountNo = x.first.AccountNo,
BankName = x.first.BankName,
IsActive = x.first.IncludeInCheck,
LastDate = x.lastDate.Date,
DataItemsCount = x.lastDate.CountItemsSend
})
.ToList();
If this fails, it might be because the engine can't convert it completely to SQL, and you can try adding an AsEnumerable() between the two Selects.

LINQ: Filter by value in sub list

I have obtained a list like this:
int[] listOfUserIds = new int[]{1,2,5};
var groups = db.some_table.Where(x => x.isOpen == true)
.Select(t => new Models.XModel() {
Id = t.Id,
Name = t.name,
Users = t.Users.Where(x => x.Age > 25).Select(user => new Models.UsersModel()
{
Name = user.Name,
UserId = user.UserId
})
});
Now from this groups list I would like to get the records where there is atleast one user whose UserId is within listOfUserIds.
How to achieve this easily without for loop?
Add 1 more condition to your Where clause:
x.Users.Any(user => listOfUserIds.Contains(user.UserId))
Your code:
var groups = db.some_table.Where(x => x.isOpen == true && x.Users.Any(user => listOfUserIds.Contains(user.UserId)))
.Select(t => new Models.SomeModel() {
Id = t.Id,
Name = t.name,
Users = t.Users.Where(x => x.Age > 25).Select(user => new Models.UsersModel()
{
Name = user.Name,
UserId = user.UserId
})
});
To improve performance, you can build a HashSet<int>:
int[] listOfUserIds = new int[]{1,2,5};
HashSet<int> hash = new HashSet<int>(listOfUserIds);
var groups = db.some_table.Where(x => x.isOpen == true && x.Users.Any(user => hash.Contains(user.UserId)))
.Select(t => new Models.SomeModel() {
Id = t.Id,
Name = t.name,
Users = t.Users.Where(x => x.Age > 25).Select(user => new Models.UsersModel()
{
Name = user.Name,
UserId = user.UserId
})
});
With HashSet<int>, this operation hash.Contains(x.Id) is O(1) instead of O(n) with listOfUserIds

The LINQ expression node type 'ArrayIndex' is not supported in LINQ to Entities

var residenceRep =
ctx.ShiftEmployees
.Include(s => s.UserData.NAME)
.Include(s => s.ResidenceShift.shiftName)
.Join(ctx.calc,
sh => new { sh.empNum, sh.dayDate },
o => new { empNum = o.emp_num, dayDate = o.trans_date },
(sh, o) => new { sh, o })
.Where(s => s.sh.recordId == recordId && s.o.day_flag.Contains("R1"))
.OrderBy(r => r.sh.dayDate)
.Select(r => new
{
dayDate = r.sh.dayDate,
empNum = r.sh.empNum,
empName = r.sh.UserData.NAME,
shiftId = r.sh.shiftId,
shiftName = r.sh.ResidenceShift.shiftName,
recordId,
dayState = r.o.day_desc.Split('[', ']')[1]
}).ToList();
I get an exception :
The LINQ expression node type 'ArrayIndex' is not supported in LINQ to
Entities
How i could find an alternative to Split('[', ']')[1] in this query
You must commit the query and do the split after loading the data:
var residenceRep =
ctx.ShiftEmployees
.Include(s => s.UserData.NAME)
.Include(s => s.ResidenceShift.shiftName)
.Join(ctx.calc,
sh => new { sh.empNum, sh.dayDate },
o => new { empNum = o.emp_num, dayDate = o.trans_date },
(sh, o) => new { sh, o })
.Where(s => s.sh.recordId == recordId && s.o.day_flag.Contains("R1"))
.OrderBy(r => r.sh.dayDate)
.Select(r => new
{
dayDate = r.sh.dayDate,
empNum = r.sh.empNum,
empName = r.sh.UserData.NAME,
shiftId = r.sh.shiftId,
shiftName = r.sh.ResidenceShift.shiftName,
recordId = r.sh.recordId,
dayState = r.o.day_desc,
})
.ToList()//Here we commit the query and load data
.Select(x=> {
var parts = x.dayState.Split('[', ']');
return new {
x.dayDate,
x.empNum,
x.empName,
x.shiftId,
x.shiftName,
x.recordId,
dayState = parts.Length > 1 ?parts[1]:"",
};
})
.ToList();
I had this Issue and the approach that I've chose was that get all element I wanted and save them into a List and then filter the actual data on that list.
I know this is not the best answer but it worked for me.

Categories