dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.19k forks source link

SQL Error: Cannot perform an aggregate function on an expression containing an aggregate or a subquery. #34246

Closed RezaRahmatzadeh closed 3 months ago

RezaRahmatzadeh commented 3 months ago

When I ran this query against my database, I encountered an error due to the aggregations in the final Select() projection. I need to return an IQueryable<>, and I want to avoid using AsEnumerable() to switch to in-memory processing, even though placing AsEnumerable() before the last Select() solves the problem.

I'm looking for a way to run the entire query on the database side. It's worth mentioning that this query worked properly in a .NET Framework project. After migrating to the latest .NET version, I encountered this error. Based on my research, the issue arises because older EF versions automatically switched to in-memory processing, which no longer happens in the latest version. This change is beneficial as it makes developers more aware of what's happening under the hood.

Here is the query. Could you help me figure out how to run the entire query on the database side without encountering SQL errors?

var avgGrid = dbContext.TableName.Select(z => new DTO()
{
    IncidentDate = z.IncidentDate,
    ReportReceivedDate = z.ReportReceived,
    InitiatedDate = z.InitiatedDate,
    FinalizedDate = z.FinalizedDate,
    FirstReserveDate = dbContext.Reserves.Where(x => x.ClaimId == z.ClaimId && x.ReserveType == "XYZ").OrderBy(x => x.TransactionDate).FirstOrDefault().TransactionDate,
    FirstPaymentDate = dbContext.Reserves.Where(x => x.ClaimId == z.ClaimId && (x.ReserveType == "ABC" || x.ReserveType == "EFG")).OrderBy(x => x.TransactionDate).FirstOrDefault().TransactionDate,
    IncidentToReceivedDays = 0,
    ReceivedToInitiatedDays = 0,
    InitiatedToFirstReserveDays = 0,
    InitiatedToFirstPaymentDays = 0,
    InitiatedToFinalizedDays = 0,
    ClaimIdentifier = z.ClaimIdentifier,
    Category = z.UnderwritingPeriod == null ? "" :
                    z.UnderwritingPeriod.PolicyNumber1 != null ? "Type1" :
                    z.UnderwritingPeriod.PolicyNumber2 != null ? "Type2" :
                    z.UnderwritingPeriod.PolicyNumber3 != null ? "Type3" : "",
    PolicyIdentifier = z.UnderwritingPeriod == null ? "" : z.UnderwritingPeriod.Policy.PolicyIdentifier,

}).Select(z => new ClaimsReactionsReportDto()
{
    IncidentDate = z.IncidentDate,
    ReportReceivedDate = z.ReportReceivedDate,
    InitiatedDate = z.InitiatedDate,
    FinalizedDate = z.FinalizedDate,
    FirstReserveDate = z.FirstReserveDate,
    FirstPaymentDate = z.FirstPaymentDate,
    IncidentToReceivedDays = (z.IncidentDate.HasValue && z.ReportReceivedDate.HasValue) ? EF.Functions.DateDiffDay(z.IncidentDate, z.ReportReceivedDate).Value : (int?)null,
    ReceivedToInitiatedDays = (z.ReportReceivedDate.HasValue && z.InitiatedDate.HasValue) ? EF.Functions.DateDiffDay(z.ReportReceivedDate, z.InitiatedDate).Value : (int?)null,
    InitiatedToFirstReserveDays = (z.InitiatedDate.HasValue && z.FirstReserveDate.HasValue) ? EF.Functions.DateDiffDay(z.InitiatedDate, z.FirstReserveDate).Value : (int?)null,
    InitiatedToFirstPaymentDays = (z.InitiatedDate.HasValue && z.FirstPaymentDate.HasValue) ? EF.Functions.DateDiffDay(z.InitiatedDate, z.FirstPaymentDate).Value : (int?)null,
    InitiatedToFinalizedDays = (z.InitiatedDate.HasValue && z.FinalizedDate.HasValue) ? EF.Functions.DateDiffDay(z.InitiatedDate, z.FinalizedDate).Value : (int?)null,
    ClaimIdentifier = z.ClaimIdentifier,
    Category = z.Category,
    PolicyIdentifier = z.PolicyIdentifier,
}).GroupBy(y => new { y.IncidentDate.Value.Year, y.IncidentDate.Value.Month })
.Select(e => new ClaimsReactionsAVGReportDto()
{
    IncidentToReceivedDaysAVG = e.Average(x => x.IncidentToReceivedDays),
    ReceivedToInitiatedDaysAVG = e.Average(x => x.ReceivedToInitiatedDays),
    InitiatedToFirstReserveDaysAVG = e.Average(x => x.InitiatedToFirstReserveDays),
    InitiatedToFirstPaymentDaysAVG = e.Average(x => x.InitiatedToFirstPaymentDays),
    InitiatedToFinalizedDaysAVG = e.Average(x => x.InitiatedToFinalizedDays),
    Year = e.Key.Year,
    Month = e.Key.Month,
    DateString = EF.Functions.DateTimeFromParts(e.Key.Year, e.Key.Month, 3, 1, 1, 1, 0),
});
var test = avgGrid.ToList();//error here, this is just for test and In real world scenario it will return up to the UI Grid as Queryable object
roji commented 3 months ago

The above is a snippet - can you please post a minimal, runnable console app that shows the error happening (this should always be done when submitting an issue)? Please make the effort of narrowing down your query to the minimum that reproduces the error, removing unneeded parts.

RezaRahmatzadeh commented 3 months ago

The above is a snippet - can you please post a minimal, runnable console app that shows the error happening (this should always be done when submitting an issue)? Please make the effort of narrowing down your query to the minimum that reproduces the error, removing unneeded parts.

Sure, here is a sample that simulates that exact error using a LocalDB SQL server and some sample tables:

https://github.com/RezaRahmatzadeh/EFCore.CannotAggregationInSubQuery.SQL.Error.Sample

and also here is the code:

public class TableName
{
    public int Id { get; set; }
    public DateTime? IncidentDate { get; set; }
    public DateTime? ReportReceived { get; set; }
    public DateTime? InitiatedDate { get; set; }
    public DateTime? FinalizedDate { get; set; }
    public string ClaimId { get; set; }
    public string ClaimIdentifier { get; set; }
    public UnderwritingPeriod UnderwritingPeriod { get; set; }
}

public class Reserve
{
    public int Id { get; set; }
    public string ClaimId { get; set; }
    public string ReserveType { get; set; }
    public DateTime TransactionDate { get; set; }
}

public class UnderwritingPeriod
{
    public int Id { get; set; }
    public string PolicyNumber1 { get; set; }
    public string PolicyNumber2 { get; set; }
    public string PolicyNumber3 { get; set; }
    public Policy Policy { get; set; }
}

public class Policy
{
    public int Id { get; set; }
    public string PolicyIdentifier { get; set; }
}

public class ClaimsReactionsReportDto
{
    public DateTime? IncidentDate { get; set; }
    public DateTime? ReportReceivedDate { get; set; }
    public DateTime? InitiatedDate { get; set; }
    public DateTime? FinalizedDate { get; set; }
    public DateTime? FirstReserveDate { get; set; }
    public DateTime? FirstPaymentDate { get; set; }
    public int? IncidentToReceivedDays { get; set; }
    public int? ReceivedToInitiatedDays { get; set; }
    public int? InitiatedToFirstReserveDays { get; set; }
    public int? InitiatedToFirstPaymentDays { get; set; }
    public int? InitiatedToFinalizedDays { get; set; }
    public string ClaimIdentifier { get; set; }
    public string Category { get; set; }
    public string PolicyIdentifier { get; set; }
}

public class ClaimsReactionsAVGReportDto
{
    public double? IncidentToReceivedDaysAVG { get; set; }
    public double? ReceivedToInitiatedDaysAVG { get; set; }
    public double? InitiatedToFirstReserveDaysAVG { get; set; }
    public double? InitiatedToFirstPaymentDaysAVG { get; set; }
    public double? InitiatedToFinalizedDaysAVG { get; set; }
    public int Year { get; set; }
    public int Month { get; set; }
    public DateTime DateString { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<TableName> TableName { get; set; }
    public DbSet<Reserve> Reserves { get; set; }
    public DbSet<UnderwritingPeriod> UnderwritingPeriods { get; set; }
    public DbSet<Policy> Policies { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Add any additional model configurations here
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Starting demo application...");

        // Setup DbContext
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ClaimsDemoDb;Trusted_Connection=True;")
            .Options;

        using (var context = new AppDbContext(options))
        {
            // Ensure database is created
            Console.WriteLine("Creating database...");
            context.Database.EnsureCreated();

            // Seed data
            Console.WriteLine("Seeding data...");
            SeedData(context);

            // Run the problematic query
            Console.WriteLine("Running query...");
            RunProblematicQuery(context);
        }

        Console.WriteLine("Demo completed. Press any key to exit.");
        Console.ReadKey();
    }

    private static void SeedData(AppDbContext context)
    {
        if (!context.TableName.Any())
        {
            var policy = new Policy { PolicyIdentifier = "POL001" };
            context.Policies.Add(policy);

            var underwritingPeriod = new UnderwritingPeriod
            {
                PolicyNumber1 = "PN001",
                Policy = policy,
                PolicyNumber2 = "PN001",
                PolicyNumber3 = "PN003"
            };
            context.UnderwritingPeriods.Add(underwritingPeriod);

            var claim = new TableName
            {
                IncidentDate = DateTime.Now.AddDays(-30),
                ReportReceived = DateTime.Now.AddDays(-28),
                InitiatedDate = DateTime.Now.AddDays(-27),
                FinalizedDate = DateTime.Now.AddDays(-5),
                ClaimId = "CL001",
                ClaimIdentifier = "CI001",
                UnderwritingPeriod = underwritingPeriod
            };
            context.TableName.Add(claim);

            var reserve1 = new Reserve
            {
                ClaimId = "CL001",
                ReserveType = "XYZ",
                TransactionDate = DateTime.Now.AddDays(-26)
            };
            var reserve2 = new Reserve
            {
                ClaimId = "CL001",
                ReserveType = "ABC",
                TransactionDate = DateTime.Now.AddDays(-25)
            };
            context.Reserves.AddRange(reserve1, reserve2);

            context.SaveChanges();
        }
    }

    private static void RunProblematicQuery(AppDbContext dbContext)
    {
        try
        {
            var avgGrid = dbContext.TableName.Select(z => new
            {
                IncidentDate = z.IncidentDate,
                ReportReceivedDate = z.ReportReceived,
                InitiatedDate = z.InitiatedDate,
                FinalizedDate = z.FinalizedDate,
                FirstReserveDate = dbContext.Reserves.Where(x => x.ClaimId == z.ClaimId && x.ReserveType == "XYZ").OrderBy(x => x.TransactionDate).FirstOrDefault().TransactionDate,
                FirstPaymentDate = dbContext.Reserves.Where(x => x.ClaimId == z.ClaimId && (x.ReserveType == "ABC" || x.ReserveType == "EFG")).OrderBy(x => x.TransactionDate).FirstOrDefault().TransactionDate,
                ClaimIdentifier = z.ClaimIdentifier,
                Category = z.UnderwritingPeriod == null ? "" :
                            z.UnderwritingPeriod.PolicyNumber1 != null ? "Type1" :
                            z.UnderwritingPeriod.PolicyNumber2 != null ? "Type2" :
                            z.UnderwritingPeriod.PolicyNumber3 != null ? "Type3" : "",
                PolicyIdentifier = z.UnderwritingPeriod == null ? "" : z.UnderwritingPeriod.Policy.PolicyIdentifier,
            }).Select(z => new ClaimsReactionsReportDto
            {
                IncidentDate = z.IncidentDate,
                ReportReceivedDate = z.ReportReceivedDate,
                InitiatedDate = z.InitiatedDate,
                FinalizedDate = z.FinalizedDate,
                FirstReserveDate = z.FirstReserveDate,
                FirstPaymentDate = z.FirstPaymentDate,
                IncidentToReceivedDays = (z.IncidentDate.HasValue && z.ReportReceivedDate.HasValue) ? EF.Functions.DateDiffDay(z.IncidentDate, z.ReportReceivedDate).Value : (int?)null,
                ReceivedToInitiatedDays = (z.ReportReceivedDate.HasValue && z.InitiatedDate.HasValue) ? EF.Functions.DateDiffDay(z.ReportReceivedDate, z.InitiatedDate).Value : (int?)null,
                InitiatedToFirstReserveDays = (z.InitiatedDate.HasValue) ? EF.Functions.DateDiffDay(z.InitiatedDate, z.FirstReserveDate).Value : (int?)null,
                InitiatedToFirstPaymentDays = (z.InitiatedDate.HasValue) ? EF.Functions.DateDiffDay(z.InitiatedDate, z.FirstPaymentDate).Value : (int?)null,
                InitiatedToFinalizedDays = (z.InitiatedDate.HasValue && z.FinalizedDate.HasValue) ? EF.Functions.DateDiffDay(z.InitiatedDate, z.FinalizedDate).Value : (int?)null,
                ClaimIdentifier = z.ClaimIdentifier,
                Category = z.Category,
                PolicyIdentifier = z.PolicyIdentifier,
            }).GroupBy(y => new { y.IncidentDate.Value.Year, y.IncidentDate.Value.Month })
            .Select(e => new ClaimsReactionsAVGReportDto
            {
                IncidentToReceivedDaysAVG = e.Average(x => x.IncidentToReceivedDays),
                ReceivedToInitiatedDaysAVG = e.Average(x => x.ReceivedToInitiatedDays),
                InitiatedToFirstReserveDaysAVG = e.Average(x => x.InitiatedToFirstReserveDays),
                InitiatedToFirstPaymentDaysAVG = e.Average(x => x.InitiatedToFirstPaymentDays),
                InitiatedToFinalizedDaysAVG = e.Average(x => x.InitiatedToFinalizedDays),
                Year = e.Key.Year,
                Month = e.Key.Month,
                DateString = EF.Functions.DateTimeFromParts(e.Key.Year, e.Key.Month, 3, 1, 1, 1, 0),
            });

            var test = avgGrid.ToList(); // This line will throw the error
            Console.WriteLine("Query executed successfully");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error occurred while executing the query:");
            Console.WriteLine(ex.Message);
        }
    }
}
roji commented 3 months ago

Duplicate of #34256

roji commented 3 months ago

Am going to use #34256 to track working around the SQL Server limitation in EF.