简体   繁体   中英

EF Core Grouping Multiple Joined Tables

I am getting an error at runtime when trying to group on 3 tables that have been joined together. I know i can do this in sql but i'm trying to keep to EF Core if at all possible. EF Core complains that the linq expression could not be translated or that i would need to switch to client side evaluation. The goal is to have this simple join and grouping run in the sql server.

from line in _context.OrderLines
    join item in _context.Items on line.ItemId equals item.ItemId
    join supplier in _context.Suppliers on item.SupplierId equals supplier.SupplierId
group line by supplier.Name

The grouping above is a valid Query Expression at compile time and will fail without the subsequent select but to elaborate on my end usage: I want to be able to sum my line quantities by the supplier. This should be a simple aggregation after the grouping.

from line in _context.OrderLines
    join item in _context.Items on line.ItemId equals item.ItemId
    join supplier in _context.Suppliers on item.SupplierId equals supplier.SupplierId
group line by supplier.Name into lineGrouping
select new { Name = lineGrouping.Key, Qty = lineGrouping.Sum(x => x.Qty) }

The execution results in the following error:

System.InvalidOperationException: Client side GroupBy is not supported. at Microsoft.EntityFrameworkCore.Query.RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.VisitExtension(Expression extensionExpression)

Take a look at the official EF Core docs for general information about Complex Query Operators: GroupBy .

Since no database structure can represent an IGrouping, GroupBy operators have no translation in most cases. When an aggregate operator is applied to each group, which returns a scalar, it can be translated to SQL GROUP BY in relational databases. The SQL GROUP BY is restrictive too. It requires you to group only by scalar values. The projection can only contain grouping key columns or any aggregate applied over a column. EF Core identifies this pattern and translates it to the server [...]

Now, for your concrete example, it works without issues. Here is a sample console project to demonstrate that:

using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace IssueConsoleTemplate
{
    public class OrderLine
    {
        [Key]
        public int OrderLineId { get; set; }
        public int ItemId { get; set; }
        public int Qty { get; set; }
    }

    public class Supplier
    {
        [Key]
        public int SupplierId { get; set; }
        public string Name { get; set; }
    }

    public class Item
    {
        [Key]
        public int ItemId { get; set; }
        public int SupplierId { get; set; }
    }
    
    public class Context : DbContext
    {
        public DbSet<OrderLine> OrderLines { get; set; }
        public DbSet<Supplier> Suppliers { get; set; }
        public DbSet<Item> Items { get; set; }
        
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63040380")
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<OrderLine>()
                .HasData(
                    new OrderLine {OrderLineId = 1, ItemId = 1, Qty = 100},
                    new OrderLine {OrderLineId = 2, ItemId = 2, Qty = 42},
                    new OrderLine {OrderLineId = 3, ItemId = 3, Qty = 21});

            builder.Entity<Supplier>()
                .HasData(
                    new Supplier {SupplierId = 1, Name = "Supplier A"},
                    new Supplier {SupplierId = 2, Name = "Supplier B"});
            
            builder.Entity<Item>()
                .HasData(
                    new Item {ItemId = 1, SupplierId = 1},
                    new Item {ItemId = 2, SupplierId = 1},
                    new Item {ItemId = 3, SupplierId = 2});
        }
    }

    internal static class Program
    {
        private static void Main()
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var supplierNameAndQuantity = (from line in context.OrderLines
                join item in context.Items on line.ItemId equals item.ItemId
                join supplier in context.Suppliers on item.SupplierId equals supplier.SupplierId
                group line by supplier.Name into lineGrouping
                select new {Name = lineGrouping.Key, Qty = lineGrouping.Sum(x => x.Qty)})
                .ToList();

            Debug.Assert(supplierNameAndQuantity.Count == 2);
            Debug.Assert(supplierNameAndQuantity[0].Name == "Supplier A");
            Debug.Assert(supplierNameAndQuantity[0].Qty == 142);
            Debug.Assert(supplierNameAndQuantity[1].Name == "Supplier B");
            Debug.Assert(supplierNameAndQuantity[1].Qty == 21);
        }
    }
}

The query gets translated to the following SQL, which is correct:

SELECT [s].[Name], SUM([o].[Qty]) AS [Qty]
FROM [OrderLines] AS [o]
INNER JOIN [Items] AS [i] ON [o].[ItemId] = [i].[ItemId]
INNER JOIN [Suppliers] AS [s] ON [i].[SupplierId] = [s].[SupplierId]
GROUP BY [s].[Name]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM