Entity Framework Core 3.1 中使用带除法的键进行分组时存在错误?

Bug in Entity Framework Core 3.1 when grouping using a key with a division?

我正在尝试使用 linq 和 entity framework 3.1 对一组数据进行分组。这是我要完成的查询:

var query = dbset
                .Join(vehicles,
                    i => i.VehicleId,
                    v => v.Id,
                    (i, v) => new{i, v})
                .Where(o=>o.v.EnterpriseId == enterpriseId && o.i.ReportDate>=startDate && o.i.ReportDate<=endDate);

dateDataQuery = query.GroupBy(o =>
                    new
                    {
                        Week=(int)(o.i.ReportDate.DayOfYear / 7),
                        o.i.ReportDate.Year
                    })
                    .OrderBy(o=> o.Key.Year).ThenBy(o=>o.Key.Week)
                    .Select(g => new TransferDateData()
                    {
                        Week = g.Key.Week,
                        Year = g.Key.Year,
                        Value = g.Sum(o=>o.i.Cost),
                    });

如您所见,我按周和年分组。问题是 DateTime 没有 Week 属性 来获取一年中可以转换为数据库过程的星期。正如我们可以用 .Month 或 .Day 做的那样。所以,必须计算周。

此查询输出相同周和年的不同结果。例如,我可以有几个结果,如:

{ Week = 20, Year=2020, Value = 20}, {Week=20, Year=2020, Value=30} ...

如您所见,它们本应分组,但实际上没有。如果我在分组中使用一周的硬编码数字,则分组有效并且所有值都被聚合并且只返回一个结果。

所以,我猜除法和浮点数结果有问题,尽管 Week 是一个整数。

以下是我使用的两个模型及其属性:

public class GenericDateData<T>
{
    public T Value { get; set; }
    public int Month { get; set; }
    public int Year { get; set; }
    public int Day { get; set; }
    public int Week { get; set; }
}

public class TransferDateData : GenericDateData<float>
{
    public float TotalKm { get; set; }
}

你能猜到发生了什么吗?对我来说似乎是个错误。

您的查询确实有效。正如@PanagiotisKanavos 在评论中提到的,它 不准确 ,但它 确实将 转换为有效的 SQL:

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

namespace IssueConsoleTemplate
{
    public class Incident
    {
        public int Id { get; set; }
        public int VehicleId { get; set; }
        public DateTime ReportDate { get; set; }
        public float Cost { get; set; }
    }

    public class Vehicle
    {
        public int Id { get; set; }
        public int EnterpriseId { get; set; }
    }

    public class Enterprise
    {
        public int Id { get; set; }
    }
    
    public class GenericDateData<T>
    {
        public T Value { get; set; }
        public int Month { get; set; }
        public int Year { get; set; }
        public int Day { get; set; }
        public int Week { get; set; }
    }

    public class TransferDateData : GenericDateData<float>
    {
        public float TotalKm { get; set; }
    }
    
    public class Context : DbContext
    {
        public DbSet<Incident> Incidents { get; set; }
        public DbSet<Vehicle> Vehicles { get; set; }
        public DbSet<Enterprise> Enterprises { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(
                    @"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63118907")
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Incident>().HasData(
                new Incident {Id = 1, VehicleId = 1, ReportDate = new DateTime(2020, 1, 1), Cost = 42.00f},
                new Incident {Id = 2, VehicleId = 2, ReportDate = new DateTime(2020, 2, 1), Cost = 21.00f});

            modelBuilder.Entity<Vehicle>().HasData(
                new Vehicle {Id = 1, EnterpriseId = 1},
                new Vehicle {Id = 2, EnterpriseId = 1});
            
            modelBuilder.Entity<Enterprise>().HasData(
                new Enterprise {Id = 1});
        }
    }

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

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

            var enterpriseId = 1;
            var startDate = new DateTime(2020, 1, 1);
            var endDate = new DateTime(2021, 12, 31);

            var costPerWeek = context.Incidents
                .Join(
                    context.Vehicles,
                    i => i.VehicleId,
                    v => v.Id,
                    (i, v) => new {i, v})
                .Where(
                    o => o.v.EnterpriseId == enterpriseId &&
                         o.i.ReportDate >= startDate &&
                         o.i.ReportDate <= endDate)
                .GroupBy(
                    o =>
                        new
                        {
                            Week = (int) (o.i.ReportDate.DayOfYear / 7),
                            o.i.ReportDate.Year
                        })
                .OrderBy(o => o.Key.Year)
                .ThenBy(o => o.Key.Week)
                .Select(
                    g => new TransferDateData()
                    {
                        Week = g.Key.Week,
                        Year = g.Key.Year,
                        Value = g.Sum(o => o.i.Cost),
                    })
                .ToList();

            Debug.Assert(costPerWeek.Count == 2);
            Debug.Assert(costPerWeek[0].Week == 0);
            Debug.Assert(costPerWeek[0].Value == 42.00f);
            Debug.Assert(costPerWeek[1].Week == 4);
            Debug.Assert(costPerWeek[1].Value == 21.00f);
        }
    }
}

查询被翻译成以下 SQL:

SELECT DATEPART(dayofyear, [i].[ReportDate]) / 7 AS [Week], DATEPART(year, [i].[ReportDate]) AS [Year], CAST(SUM([i].[Cost]) AS real) AS [Value]
FROM [Incidents] AS [i]
INNER JOIN [Vehicles] AS [v] ON [i].[VehicleId] = [v].[Id]
WHERE (([v].[EnterpriseId] = @__enterpriseId_0) AND ([i].[ReportDate] >= @__startDate_1)) AND ([i].[ReportDate] <= @__endDate_2)
GROUP BY DATEPART(dayofyear, [i].[ReportDate]) / 7, DATEPART(year, [i].[ReportDate])
ORDER BY DATEPART(year, [i].[ReportDate]), DATEPART(dayofyear, [i].[ReportDate]) / 7

它目前从 0 开始计算周数,尽管如此,日期可能会被一些人关闭(取决于日历周在该应用程序所使用的文化中的计算方式)天。


更简单、更准确的解决方案可能是使用 DATEPART(week, @date) T-SQL 函数。不幸的是,目前没有 .NET Core 方法可以转换为这个 SQL 函数。

但是您可以定义一个 UDF(用户定义的函数),将调用重定向到 DATEPART

下面的示例不仅定义并使用了 UDF,还展示了如何使用导航属性而不是显式连接:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace IssueConsoleTemplate
{
    public class Incident
    {
        public int Id { get; set; }
        public int VehicleId { get; set; }
        public DateTime ReportDate { get; set; }
        public float Cost { get; set; }
        
        public Vehicle Vehicle { get; set; }
        public Enterprise Enterprise { get; set; }
    }

    public class Vehicle
    {
        public int Id { get; set; }
        public int EnterpriseId { get; set; }
        
        public virtual ICollection<Incident> Incidents { get; set; } = new HashSet<Incident>();
    }

    public class Enterprise
    {
        public int Id { get; set; }
    }
    
    public class GenericDateData<T>
    {
        public T Value { get; set; }
        public int Month { get; set; }
        public int Year { get; set; }
        public int Day { get; set; }
        public int Week { get; set; }
    }

    public class TransferDateData : GenericDateData<float>
    {
        public float TotalKm { get; set; }
    }
    
    public class Context : DbContext
    {
        public DbSet<Incident> Incidents { get; set; }
        public DbSet<Vehicle> Vehicles { get; set; }
        public DbSet<Enterprise> Enterprises { get; set; }

        // Define your UDF:
        [DbFunction("GetCalendarWeek")]
        public static int GetCalendarWeek(DateTime date)
            => throw new InvalidOperationException("Should never be called but only translated to SQL.");

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(
                    @"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63118907_01")
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Incident>().HasData(
                new Incident {Id = 1, VehicleId = 1, ReportDate = new DateTime(2020, 1, 1), Cost = 42.00f},
                new Incident {Id = 2, VehicleId = 2, ReportDate = new DateTime(2020, 2, 1), Cost = 21.00f});

            modelBuilder.Entity<Vehicle>().HasData(
                new Vehicle {Id = 1, EnterpriseId = 1},
                new Vehicle {Id = 2, EnterpriseId = 1});
            
            modelBuilder.Entity<Enterprise>().HasData(
                new Enterprise {Id = 1});
        }
    }

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

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

            // Since this example recreates the database on each run,
            // lets create our UDF here as well:
            context.Database.ExecuteSqlRaw(@"
CREATE FUNCTION [GetCalendarWeek] (@date DATETIME)
RETURNS INT AS
BEGIN
    RETURN DATEPART(week, @date);
END");

            var enterpriseId = 1;
            var startDate = new DateTime(2020, 1, 1);
            var endDate = new DateTime(2021, 12, 31);

            var costPerWeek = context.Incidents
                .Include(i => i.Vehicle)
                .Include(i => i.Enterprise)
                .Where(
                    i => i.Vehicle.EnterpriseId == enterpriseId &&
                         i.ReportDate >= startDate &&
                         i.ReportDate <= endDate)
                .GroupBy(
                    i =>
                        new
                        {
                            // Let's call our UDF here:
                            Week = Context.GetCalendarWeek(i.ReportDate),
                            i.ReportDate.Year
                        })
                .OrderBy(o => o.Key.Year)
                .ThenBy(o => o.Key.Week)
                .Select(
                    g => new TransferDateData()
                    {
                        Week = g.Key.Week,
                        Year = g.Key.Year,
                        Value = g.Sum(i => i.Cost),
                    })
                .ToList();

            Debug.Assert(costPerWeek.Count == 2);
            Debug.Assert(costPerWeek[0].Week == 1);
            Debug.Assert(costPerWeek[0].Value == 42.00f);
            Debug.Assert(costPerWeek[1].Week == 5);
            Debug.Assert(costPerWeek[1].Value == 21.00f);
        }
    }
}

周数现在是正确的(取决于您的文化)。

生成的SQL现在比较简单:

CREATE FUNCTION [GetCalendarWeek] (@date DATETIME)
RETURNS INT AS
BEGIN
  RETURN DATEPART(week, @date);
END

SELECT [dbo].[GetCalendarWeek]([i].[ReportDate]) AS [Week], DATEPART(year, [i].[ReportDate]) AS [Year], CAST(SUM([i].[Cost]) AS real) AS [Value]
FROM [Incidents] AS [i]
INNER JOIN [Vehicles] AS [v] ON [i].[VehicleId] = [v].[Id]