I believe your statement SELECT ... FROM Orders, OrderItems... WHERE OrderItems.OrderId = Orders.Id
while looking like a CROSS JOIN
, ends up being optimised into an INNER JOIN
.
So, assuming you've got your model set up with navigation properties you might be better off using .Include(). Apart from that I think you are pretty much there:
var query = _context.Set<OrderItem>().Include(o => o.Order)
.Where(rec => rec.Order.OrderStatusId == Convert.ToInt32(orderStatusId))
.Where(rec => rec.Order.OrderDate >= Convert.ToDateTime(startDate) && rec.Order.OrderDate <= Convert.ToDateTime(endDate))
.GroupBy(g => new { g.ProductName, g.ProductId })
.Select(grp => new OrderDto
{
ProductName = grp.Key.ProductName,
ProductId = grp.Key.ProductId,
Quantity = grp.Sum(t => t.Units)
})
.OrderBy(x => x.Quantity)
.Take(Convert.ToInt32(top));
This produces the following output:
SELECT TOP(@__p_3) [o].[ProductName], [o].[ProductId], SUM([o].[Units]) AS [Quantity]
FROM [OrderItems] AS [o]
INNER JOIN [Orders] AS [o0] ON [o].[OrderId] = [o0].[Id]
WHERE ([o0].[OrderStatusId] = @__ToInt32_0) AND (([o0].[OrderDate] >= @__ToDateTime_1) AND ([o0].[OrderDate] <= @__ToDateTime_2))
GROUP BY [o].[ProductName], [o].[ProductId]
ORDER BY SUM([o].[Units])
Suppose you can't add the navigation property to your OrderItem
model, then your code seems pretty much there:
var query2 = (from sta in _context.Set<OrderItem>()
from rec in _context.Set<Order>()
where sta.OrderId == rec.Id && rec.OrderStatusId == Convert.ToInt32(orderStatusId)
&& rec.OrderDate >= Convert.ToDateTime(startDate) && rec.OrderDate <= Convert.ToDateTime(endDate)
group sta by new
{
sta.ProductName,
sta.ProductId
} into grp
select new OrderDto()
{
ProductName = grp.Key.ProductName,
ProductId = grp.Key.ProductId,
Quantity = grp.Max(t => t.Units),
}
)
.OrderBy(x => x.Quantity)
.Take(Convert.ToInt32(top));
This produces the following SQL:
SELECT TOP(@__p_3) [o].[ProductName], [o].[ProductId], MAX([o].[Units]) AS [Quantity]
FROM [OrderItems] AS [o]
CROSS JOIN [Orders] AS [o0]
WHERE ((([o].[OrderId] = [o0].[Id]) AND ([o0].[OrderStatusId] = @__ToInt32_0)) AND ([o0].[OrderDate] >= @__ToDateTime_1)) AND ([o0].[OrderDate] <= @__ToDateTime_2)
GROUP BY [o].[ProductName], [o].[ProductId]
ORDER BY MAX([o].[Units])
Here's my full test bench for reference
using Microsoft.EntityFrameworkCore
using Microsoft.EntityFrameworkCore.Query.SqlExpressions
using Microsoft.EntityFrameworkCore.Query
#region EF Core 3.1 .ToSql() helper method courtesy of https://stackoverflow.com/a/51583047/12339804
public static class IQueryableExtensions
{
public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
{
var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
var relationalCommandCache = enumerator.Private("_relationalCommandCache");
var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression");
var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");
var sqlGenerator = factory.Create();
var command = sqlGenerator.GetCommand(selectExpression);
string sql = command.CommandText;
return sql;
}
private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
private static T Private<T>(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
}
#endregion
public class OrderItem
{
public int Id {get;set;}
public int OrderId {get;set;}
public int ProductName {get;set;}
public int ProductId {get;set;}
public int Units {get;set;}
public Order Order {get;set;} // added navigation property for .Include() to pick up on
}
public class Order {
public int Id {get;set;}
public int OrderStatusId {get;set;}
public DateTime OrderDate {get;set;}
}
public class OrderDto
{
public int ProductName { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
}
class Dbc : DbContext
{
public DbSet<Order> Orders {get;set;}
public DbSet<OrderItem> OrderItems {get;set;}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=.\SQLEXPRESS;Database=Test;Trusted_Connection=True");
base.OnConfiguring(optionsBuilder);
}
}
void Main()
{
var _context = new Dbc();
var orderStatusId = "2";
var top = "10";
var startDate = DateTime.Parse("2019-12-16 16:38:27");
var endDate = DateTime.Parse("2019-12-18 16:38:27");
var query = _context.Set<OrderItem>().Include(o => o.Order)
.Where(rec => rec.Order.OrderStatusId == Convert.ToInt32(orderStatusId))
.Where(rec => rec.Order.OrderDate >= Convert.ToDateTime(startDate) && rec.Order.OrderDate <= Convert.ToDateTime(endDate))
.GroupBy(g => new { g.ProductName, g.ProductId })
.Select(grp => new OrderDto
{
ProductName = grp.Key.ProductName,
ProductId = grp.Key.ProductId,
Quantity = grp.Sum(t => t.Units)
})
.OrderBy(x => x.Quantity)
.Take(Convert.ToInt32(top));
query.ToSql().Dump();
//alternatively, trying to force a cross join syntax with extra WHERE condition. This way you don't need `public Order Order {get;set;}` navigation property on `OrderItem`
var query2 = (from sta in _context.Set<OrderItem>()
from rec in _context.Set<Order>()
where sta.OrderId == rec.Id && rec.OrderStatusId == Convert.ToInt32(orderStatusId)
&& rec.OrderDate >= Convert.ToDateTime(startDate) && rec.OrderDate <= Convert.ToDateTime(endDate)
group sta by new
{
sta.ProductName,
sta.ProductId
} into grp
select new OrderDto()
{
ProductName = grp.Key.ProductName,
ProductId = grp.Key.ProductId,
Quantity = grp.Max(t => t.Units),
}
)
.OrderBy(x => x.Quantity)
.Take(Convert.ToInt32(top));
query2.ToSql().Dump();
}