LINQ lambda - OrderByDescending is adding undesired select - c#

I'm working with a pretty wild lambda query. Here is my initial LINQ lambda statement (not being sorted/ordered by):
var query = orders.Join(customers, o => o.CustomerID, c => c.ID, (o, c) => new { o, c })
.Join(ordersections, o => o.o.ID, os => os.OrderID, (o, os) => new { o.o, o.c, os })
.Join(tickets, o => o.os.ID, t => t.OrderSectionID, (o, t) => new { o.o, o.c, o.os, t })
.Join(events, o => o.t.EventID, e => e.id, (o, e) => new { o.o, o.c, o.os, o.t, e })
.Join(clients, o => o.e.ClientID, cl => cl.id, (o, cl) => new { o.o, o.c, o.os, o.t, o.e, cl })
.Join(venues, o => o.e.VenueID, v => v.VenueID, (o, v) => new ModelsCs.LINQ.CustomerSearchResult { order = o.o, customer = o.c, orderSection = o.os, ticket = o.t, evt = o.e, client = o.cl, venue = v })
.AsExpandable()
.Where(predicate) // from PredicateBuilder
.GroupBy(x => new
{
// variables to group by
})
.Select(s => new CustomerSearchResult
{
// Selecting the variables, all good and fun!
});
The SQL that is generated is as follows:
SELECT <correct variables to select>
FROM [dbo].[Order] AS [t0]
INNER JOIN [dbo].[Customer] AS [t1] ON [t0].[Customer] = ([t1].[Customer])
INNER JOIN [dbo].[OrderSection] AS [t2] ON [t0].[Order] = [t2].[Order]
INNER JOIN [dbo].[Ticket] AS [t3] ON [t2].[OrderSection] = [t3].[OrderSection]
INNER JOIN [dbo].[Event] AS [t4] ON [t3].[Event] = [t4].[Event]
INNER JOIN [dbo].[Client] AS [t5] ON [t4].[Client] = ([t5].[Client])
INNER JOIN [dbo].[Venue] AS [t6] ON [t4].[Venue] = ([t6].[Venue])
WHERE ([t5].[Brand] = #p0)
AND ([t0].[Brand] = #p1)
AND ([t4].[EventStart] >= #p2)
AND ([t0].[OrderDateTime] >= #p3)
AND ([t1].[email] LIKE #p4)
GROUP BY <correct group by variables>
Beautiful! But I need to order the results, so I also want this at the end:
...
ORDER BY SortingVariable1 desc
(^^^^ THIS IS WHAT I'M TRYING TO DO)
Here is what I have already tried:
So I tried adding this to my LINQ lambda statement:
.OrderByDescending(x => x.SortingVariable1)
But this is now the SQL code that is generated:
SELECT <correct variables to select>
FROM (
SELECT <correct GROUP BY variables>
FROM [dbo].[Order] AS [t0]
INNER JOIN [dbo].[Customer] AS [t1] ON [t0].[Customer] = ([t1].[Customer])
INNER JOIN [dbo].[OrderSection] AS [t2] ON [t0].[Order] = [t2].[Order]
INNER JOIN [dbo].[Ticket] AS [t3] ON [t2].[OrderSection] = [t3].[OrderSection]
INNER JOIN [dbo].[Event] AS [t4] ON [t3].[Event] = [t4].[Event]
INNER JOIN [dbo].[Client] AS [t5] ON [t4].[Client] = ([t5].[Client])
INNER JOIN [dbo].[Venue] AS [t6] ON [t4].[Venue] = ([t6].[Venue])
WHERE ([t5].[Brand] = #p0)
AND ([t0].[Brand] = #p1)
AND ([t4].[EventStart] >= #p2)
AND ([t0].[OrderDateTime] >= #p3)
AND ([t1].[email] LIKE #p4)
GROUP BY <correct group by variables>
) AS [t7]
ORDER BY [t7].[SortingVariable1] DESC
No matter where in my lambda statement I put that .OrderByDescending, it doesn't work correctly.
My question: Does anyone know how I can alter my LINQ Lambda statement to correctly add an ORDER BY SortingVariable1 DESC to the end of the generated SQL statement?

The outer SELECT by itself is not a problem, because it does not come with an additional overhead of descernable magnitude. The addition of nesting allows SQL generator do sorting on any of the returned fields, even the calculated ones, without including the computation twice.
This behavior is due to a limitation of SQL illustrated by the example below:
SELECT A+B as A_plus_B
FROM MyTable
ORDER BY A_plus_B -- <=== This does not work
The query above must be re-written either with the computation repeated twice, i.e.
SELECT A+B as A_plus_B
FROM MyTable
ORDER BY A+B -- <=== Computation is repeated
or with a nested query or a CTE:
SELECT A_plusB FROM (
SELECT A+B as A_plus_B
FROM MyTable
)
ORDER BY A_plus_B -- <=== This works
LINQ's SQL generator takes the second approach, producing the statement that you see.

It is correctly adding the Order By. It is in the nature of auto-generated code that it is often not going to be as pretty as human generated code. It'll often be more verbose in what it writes, simply because generating such code is often easier.
If you want to have exactly a certain set of SQL code, you'll need to write it by hand. If you want to let it be automatically generated for you then you'll have to be satisfied with less pretty but perfectly correct and equally functional code.

Related

How to better write this EF core 6 Group by Query

I am looking at the queries generated by EF core 6 and I am not that happy about it as it generates lo much blocking overhead and in efficient TSQL and I am hoping that I am the problem...
My EF Query:
var query = db.PurchaseOrders
.Include(i => i.Items)
.Where(w=>w.Items.Any(a=>a.InventoryItemId==item.Id))
.GroupBy(g=>g.SupplierId)
.Select(s => new CurrentInventoryItemSupplier{ SupplierId=s.Key
, LastOrder= s.Max(m=>m.OrderDate)
, FirstOrder= s.Min(m=>m.OrderDate)
, Orders= s.Count()
}
).ToList();
Generates this:
SELECT [p].[SupplierId], (
SELECT MAX([p1].[OrderDate])
FROM [PurchaseOrders] AS [p1]
WHERE EXISTS (
SELECT 1
FROM [PurchasedItem] AS [p2]
WHERE ([p1].[Id] = [p2].[PurchaseOrderId]) AND ([p2].[InventoryItemId] = #__item_Id_0)) AND ([p].[SupplierId] = [p1].[SupplierId])) AS [LastOrder], (
SELECT MIN([p4].[OrderDate])
FROM [PurchaseOrders] AS [p4]
WHERE EXISTS (
SELECT 1
FROM [PurchasedItem] AS [p5]
WHERE ([p4].[Id] = [p5].[PurchaseOrderId]) AND ([p5].[InventoryItemId] = #__item_Id_0)) AND ([p].[SupplierId] = [p4].[SupplierId])) AS [FirstOrder], (
SELECT COUNT(*)
FROM [PurchaseOrders] AS [p7]
WHERE EXISTS (
SELECT 1
FROM [PurchasedItem] AS [p8]
WHERE ([p7].[Id] = [p8].[PurchaseOrderId]) AND ([p8].[InventoryItemId] = #__item_Id_0)) AND ([p].[SupplierId] = [p7].[SupplierId])) AS [Orders]
FROM [PurchaseOrders] AS [p]
WHERE EXISTS (
SELECT 1
FROM [PurchasedItem] AS [p0]
WHERE ([p].[Id] = [p0].[PurchaseOrderId]) AND ([p0].[InventoryItemId] = #__item_Id_0))
GROUP BY [p].[SupplierId]
(plan: https://www.brentozar.com/pastetheplan/?id=rkOloR7S9)
here ideally I would expect
select
P.[SupplierId], MAX([p].[OrderDate]) as LastOrder, MIN([p].[OrderDate]) as FirtOrder, COUNT(P.Id) as Orders
FROM [PurchaseOrders] AS [p]
join [PurchasedItem] AS [p2] on [P2].[PurchaseOrderId]=[P].ID
where [p2].InventoryItemId= #__item_Id_0
group by P.[SupplierId]
(https://www.brentozar.com/pastetheplan/?id=rkOloR7S9)
Is there a way to improve the generated TSQL or make a parametrized Function and call the function from EF core?
There is no way this will survive production data volumes
ok, I figured it out, I had to use the Join method and not the Include
This:
var query = db.PurchaseOrders
.Join(db.PurchaseOrderItems, po => po.Id, pi => pi.PurchaseOrderId
, (po, pi) => new { SupplierId = po.SupplierId, OrderDate = po.OrderDate, pi.InventoryItemId })
.Where(w => w.InventoryItemId == item.Id)
.GroupBy(g => g.SupplierId)
.Select(s => new CurrentInventoryItemSupplier
{
SupplierId = s.Key,
LastOrder = s.Max(m => m.OrderDate),
FirstOrder = s.Min(m => m.OrderDate),
Orders = s.Count(),
});
Generates
SELECT [p].[SupplierId], MAX([p].[OrderDate]) AS [LastOrder], MIN([p].[OrderDate]) AS [FirstOrder], COUNT(*) AS [Orders]
FROM [PurchaseOrders] AS [p]
INNER JOIN [PurchaseOrderItems] AS [p0] ON [p].[Id] = [p0].[PurchaseOrderId]
WHERE [p0].[InventoryItemId] = #__item_Id_0
GROUP BY [p].[SupplierId]
when you loop over it, nothing wrong with it :-)

Linq Query With Multiple Joins Not Giving Correct Results

I have a Linq query which is being used to replace a database function. This is the first one with multiple joins and I can't seem to figure out why it returns 0 results.
If you can see any difference which could result in the incorrect return it would be greatly appreciated......I've been trying to solve it longer than I should have.
Linq Query
context.StorageAreaRacks
.Join(context.StorageAreas, sar => sar.StorageAreaId, sa => sa.Id, (sar, sa) => new { sar, sa })
.Join(context.StorageAreaTypes, xsar => xsar.sar.StorageAreaId, sat => sat.Id, (xsar, sat) => new { xsar, sat })
.Join(context.Racks, xxsar => xxsar.xsar.sar.RackId, r => r.Id, (xxsar, r) => new { xxsar, r })
.Where(x => x.xxsar.sat.IsManual == false)
.Where(x => x.r.IsEnabled == true)
.Where(x => x.r.IsVirtual == false)
.Select(x => new { x.xxsar.sat.Id, x.xxsar.sat.Name })
.Distinct()
.ToList();
This is the query which is generated by the LINQ query
SELECT
[Distinct1].[C1] AS [C1],
[Distinct1].[Id] AS [Id],
[Distinct1].[Name] AS [Name]
FROM ( SELECT DISTINCT
[Extent2].[Id] AS [Id],
[Extent2].[Name] AS [Name],
1 AS [C1]
FROM [dbo].[StorageAreaRacks] AS [Extent1]
INNER JOIN [dbo].[StorageAreaTypes] AS [Extent2] ON [Extent1].[StorageAreaId] = [Extent2].[Id]
INNER JOIN [dbo].[Racks] AS [Extent3] ON [Extent1].[RackId] = [Extent3].[Id]
WHERE (0 = [Extent2].[IsManual]) AND (1 = [Extent3].[IsEnabled]) AND (0 = [Extent3].[IsVirtual])
) AS [Distinct1]
Sql Query which produces required results
SELECT DISTINCT sat.Name, sat.Id
FROM StorageAreaRacks sar
JOIN StorageAreas sa on sa.id = sar.StorageAreaId
JOIN StorageAreaTypes sat on sat.id = sa.StorageAreaTypeId
JOIN Racks r on r.id = sar.RackId
WHERE sat.IsManual = 0
AND r.IsEnabled = 1
AND r.IsVirtual = 0
Using joins with LINQ method syntax is hard to read and error prone.
Using joins with LINQ query syntax is better, but still error prone (you can join by the wrong key as you did) and does not give you information about join cardinality.
The best for LINQ to Entities queries is to use navigation properties (as Gert Arnold suggested in the comments and not only - see Don’t use Linq’s Join. Navigate!) because they have none of the aforementioned drawbacks.
The whole query should be something like this:
var query = context.StorageAreaRacks
.Where(sar => !sar.StorageArea.StorageAreaType.IsManual
&& sar.Rack.IsEnabled && !sar.Rack.IsVirtual)
.Select(sar => new
{
sar.StorageArea.StorageAreaType.Id,
sar.StorageArea.StorageAreaType.Name,
})
.Distinct();
or
var query = (
from sar in context.StorageAreaRacks
let sat = sar.StorageArea.StorageAreaType
let r = sar.Rack
where !sat.IsManual && r.IsEnabled && !r.IsVirtual
select new { sat.Id, sat.Name })
.Distinct();
Simple, readable and almost no place for mistakes. Navigation properties are one of the most beautiful features of EF, don't miss them.
Your LINQ doesn't translate the SQL properly; it Joins the StorageAreaTypes on the StorageAreaRack.StorageAreaId instead of on the StorageAreas.StorageAreaTypeId, which is why EF drops the StorageAreas Join - it has no effect on the outcome.
I think it is clearer if you elevate the members of each join to flatten the anonymous objects and name them based on their members (that are the join tables). Also, no reason to separate the Where clauses, LINQ can use && as well as SQL using AND. Also, if you have boolean values, don't compare them to true or false. Also there is no reason to pass range variables through that aren't used later.
Putting it all together:
var ans = context.StorageAreaRacks
.Join(context.StorageAreas, sar => sar.StorageAreaId, sa => sa.Id, (sar, sa) => new { sar, sa })
.Join(context.StorageAreaTypes, sarsa => sarsa.sa.StorageAreaTypeId, sat => sat.Id, (sarsa, sat) => new { sarsa.sar, sat })
.Join(context.Racks, sarsat => sarsat.sar.RackId, r => r.Id, (sarsat, r) => new { sarsat.sat, r })
.Where(satr => !satr.sat.IsManual && satr.r.IsEnabled && !satr.r.IsVirtual)
.Select(satr => new { satr.sat.Id, satr.sat.Name })
.Distinct()
.ToList();
However, I think when multiple joins are involved and when translating SQL, LINQ comprehension syntax can be easier to understand:
var ans = (from sar in context.StorageAreaRacks
join sa in context.StorageAreas on sar.StorageAreaId equals sa.Id
join sat in context.StorageAreaTypes on sa.StorageAreaTypeId equals sat.Id
join r in context.Racks on sar.RackId equals r.Id
where !sat.IsManual && r.IsEnabled && !r.IsVirtual
select new {
sat.Name,
sat.Id
}).Distinct().ToList();
You are missing a Where for your rack ID != null in your LINQ statement, and a Distinct().

Returning (blanks) in many to many Linq query

A follow up to this question: Changing a linq query to filter on many-many
I have the following Linq query
public static List<string> selectedLocations = new List<string>();
// I then populate selectedLocations with a number of difference strings, each
// corresponding to a valid Location
viewModel.people = (from c in db.People
select c)
.OrderBy(x => x.Name)
.ToList();
// Here I'm basically filtering my dataset to include Locations from
// my array of selectedLocations
viewModel.people = from c in viewModel.people
where (
from a in selectedLocations
where c.Locations.Any(o => o.Name == a)
select a
).Any()
select c;
How can I modify the query so that it also returns people that have NO location set at all?
You can do filtering on database side:
viewModel.people =
(from p in db.People
where !p.Locations.Any() ||
p.Locations.Any(l => selectedLocations.Contains(l.Name))
orderby p.Name
select p).ToList();
Or lambda syntax:
viewModel.people =
db.People.Where(p => !p.Locations.Any() ||
p.Locations.Any(l => selectedLocations.Contains(l.Name)))
.OrderBy(p => p.Name)
.ToList();
EF will generate two EXISTS subqueries in this case. Something like:
SELECT [Extent1].[Name]
[Extent1].[Id]
-- other fields from People table
FROM [dbo].[People] AS [Extent1]
WHERE (NOT EXISTS (SELECT 1 AS [C1]
FROM [dbo].[PeopleLocations] AS [Extent2]
WHERE [Extent2].[PersonId] = [Extent1].[Id])
OR EXISTS (SELECT 1 AS [C1]
FROM [dbo].[PeopleLocations] AS [Extent3]
WHERE [Extent3].[PersonId] = [Extent1].[Id])
AND [Extent3].[Name] IN ('location1', 'location2')))
ORDER BY [Extent1].[Name] ASC

Multiple Left Outer Join with lambda expressions

I have an SQL Query to do with Lambda Expressions like this, generally with more joins than in this example.
select Table2.a,
Table2.b,
Table2.c,
Table2.d
from Table1
LEFT OUTER JOIN Table2
ON Table2.a = Table1.a and
Table2.b = Table1.b and
Table2.c = Table1.c
LEFT OUTER JOIN Table3
ON Table3.b = Table1.b AND
Table3.c = Table1.c AND
Table3.d = Table1.d
where ( Table1.a = ValueA )
order by Table3.f
I'm doing this with Join() Lambda Expression, but i see in SQL Server profiler that this generate an INNER JOIN and i need a LEFT OUTER JOIN.
This is how i'm doing it with Join()
var RS = DBContext.Table1.Join(DBContext.Table2,
Table1 => new {Table1.a, Table1.b, Table1.c},
Table2 => new {Table1.a, Table1.b, Table1.c},
(Table1, Table2) => new {Table1})
.Join(DBContext.Table3,
LastJoin => new {LastJoin.Table1.b, LastJoin.Table1.c, LastJoin.Table1.d},
Table3 => new {Table3.b, Table3.c, Table3.d},
(LastJoin,Table3) => new {LastJoin.Table1, Table3})
.Where (LastTable => LastTable.Table1.a == ValueA)
.OrderBy(LastTable => LastTable.Table3.f)
.Select (LastTable => new {LastTable.Table1, LastTable.Table3});
I have been reading that it can be done with DefaultIfEmpty() or GroupJoin() but i haven't find any complex example with more than one LEFT OUTER JOIN.
Why don't you try using linq query, it is also much easier to write and understand both as compared to lambda expressions. I have on such implementation like:
var products =
from p in this.Products
from cat in this.ProductCategoryProducts
.Where(c => c.ProductID == p.ProductID).DefaultIfEmpty()
from pc in this.ProductCategories
.Where(pc => ac.ProductCategoryID == cat.ProductCategoryID).DefaultIfEmpty()
where p.ProductID == productID
select new
{
ProductID = p.ProductID,
Heading = p.Heading,
Category = pc.ProductCategory
};
return products ;

Entity Framework Query for inner join

What would be the query for:
select s.* from Service s
inner join ServiceAssignment sa on sa.ServiceId = s.Id
where sa.LocationId = 1
in entity framework?
This is what I wrote:
var serv = (from s in db.Services
join sl in Location on s.id equals sl.id
where sl.id = s.id
select s).ToList();
but it's wrong. Can some one guide me to the path?
from s in db.Services
join sa in db.ServiceAssignments on s.Id equals sa.ServiceId
where sa.LocationId == 1
select s
Where db is your DbContext. Generated query will look like (sample for EF6):
SELECT [Extent1].[Id] AS [Id]
-- other fields from Services table
FROM [dbo].[Services] AS [Extent1]
INNER JOIN [dbo].[ServiceAssignments] AS [Extent2]
ON [Extent1].[Id] = [Extent2].[ServiceId]
WHERE [Extent2].[LocationId] = 1
In case anyone's interested in the Method syntax, if you have a navigation property, it's way easy:
db.Services.Where(s=>s.ServiceAssignment.LocationId == 1);
If you don't, unless there's some Join() override I'm unaware of, I think it looks pretty gnarly (and I'm a Method syntax purist):
db.Services.Join(db.ServiceAssignments,
s => s.Id,
sa => sa.ServiceId,
(s, sa) => new {service = s, asgnmt = sa})
.Where(ssa => ssa.asgnmt.LocationId == 1)
.Select(ssa => ssa.service);
You could use a navigation property if its available. It produces an inner join in the SQL.
from s in db.Services
where s.ServiceAssignment.LocationId == 1
select s

Categories