Skip to content

EntitySet aggregation does not work as expected for object collections #1447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
gathogojr opened this issue Mar 24, 2025 · 0 comments
Open
Labels
bug Something isn't working P2

Comments

@gathogojr
Copy link
Contributor

gathogojr commented Mar 24, 2025

Assemblies affected

  • Microsoft.AspNetCore.OData 9.x - 9.2.1 used in investigation
  • Microsoft.AspNetCore.OData 8.x - 8.2.7 used in investigation
  • Microsoft.AspNetCore.OData 7.x - 7.7.8 used in investigation

Describe the bug

EntitySet aggregation DOES NOT work as expected for object collections but works as expected for Ef Core database provider (InMemory and SqlServer).

  • Visit here to familiarize with entity set aggregation implementation
  • Visit here to find a test for entityset aggregation

Reproduce steps

To demonstrate how the issue can be repro'ed, let's define a simple OData service that works with both Ef Core InMemory database provider and object collections as data sources

Required packages:

Data models

namespace EntitySetAggregation.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public List<Order> Orders { get; set; }
    }

    public class Order
    {
        public int Id { get; set; }
        public decimal Amount { get; set; }
    }
}

Database context and collection objects initialization

namespace EntitySetAggregation.Data
{
    // DbContext - Ef Core
    public class EntitySetAggregationDbContext : DbContext
    {
        public EntitySetAggregationDbContext(DbContextOptions<EntitySetAggregationDbContext> options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }

        public DbSet<Order> Orders { get; set; }
    }

    // Collection objects data source
    internal static class DataSource
    {
        private static readonly List<Customer> customers;
        private static readonly List<Order> orders;

        static DataSource()
        {
            (customers, orders) = Generate();
        }

        public static List<Customer> Customers => customers;

        public static List<Order> Orders => orders;

        public static (List<Customer> Customers, List<Order> Orders) Generate()
        {
            var orders = new List<Order>
            {
                new Order { Id = 1, Amount = 130 },
                new Order { Id = 2, Amount = 190 },
                new Order { Id = 3, Amount = 170 },
            };

            var customers = new List<Customer>
            {
                new Customer { Id = 1, Name = "Sue", Orders = new List<Order> { orders[0], orders[2] } },
                new Customer { Id = 2, Name = "Joe", Orders = new List<Order> { orders[1] } },
            };

            return (customers, orders);
        }
    }

    // Helper class
    internal static class EntitySetAggregationDbContextInitializer
    {
        public static void SeedDatabase(this EntitySetAggregationDbContext context)
        {
            context.Database.EnsureCreated();

            if (!context.Customers.Any())
            {
                var (customers, orders) = DataSource.Generate();

                context.Customers.AddRange(customers);
                context.Orders.AddRange(orders);
                context.SaveChanges();
            }
        }
    }
}

Service configuration and Edm model definition

var builder = WebApplication.CreateBuilder(args);

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Customer>("Customers");
modelBuilder.EntitySet<Order>("Orders");

builder.Services.AddDbContext<EntitySetAggregationDbContext>(
    options => options.UseInMemoryDatabase("EntitySetAggregationDb"));
builder.Services.AddControllers().AddOData(
    options =>
    {
        var model = modelBuilder.GetEdmModel();

        options.EnableQueryFeatures();
        options.AddRouteComponents("efcore", model);
        options.AddRouteComponents("collection", model);
    });

var app = builder.Build();

app.UseRouting();
app.MapControllers();

app.Run();

Controllers

namespace EntitySetAggregation.Controllers
{
    public class CollectionCustomersController : ODataController
    {
        [EnableQuery]
        [HttpGet("collection/Customers")]
        public IQueryable<Customer> Get()
        {
            return DataSource.Customers.AsQueryable();
        }
    }

    public class EfCoreCustomersController : ODataController
    {
        private readonly EntitySetAggregationDbContext context;

        public EfCoreCustomersController(EntitySetAggregationDbContext context)
        {
            this.context = context;
            this.context.SeedDatabase();
        }

        [EnableQuery]
        [HttpGet("efcore/Customers")]
        public IQueryable<Customer> Get()
        {
            return this.context.Customers;
        }
    }
}

Request/Response

Ef Core request:

http://localhost:5138/efcore/Customers?$apply=groupby((Name),aggregate(Orders(Amount with sum as OrdersTotal)))

Ef Core response:

{
  "@odata.context": "http://localhost:5138/efcore/$metadata#Customers(Name,Orders)",
  "value": [
    {
      "@odata.id": null,
      "Name": "Sue",
      "Orders": [
        {
          "@odata.id": null,
          "OrdersTotal": 300
        }
      ]
    },
    {
      "@odata.id": null,
      "Name": "Joe",
      "Orders": [
        {
          "@odata.id": null,
          "OrdersTotal": 190
        }
      ]
    }
  ]
}

Collection objects request:

http://localhost:5138/collection/Customers?$apply=groupby((Name),aggregate(Orders(Amount with sum as OrdersTotal)))

Collection object response:

{
  "@odata.context": "http://localhost:5138/collection/$metadata#Customers(Name,Orders)",
  "value": [
    {
      "@odata.id": null,
      "Name": "Sue",
      "Orders": [
        {
          "@odata.id": null,
          "OrdersTotal": 130
        },
        {
          "@odata.id": null,
          "OrdersTotal": 170
        }
      ]
    },
    {
      "@odata.id": null,
      "Name": "Joe",
      "Orders": [
        {
          "@odata.id": null,
          "OrdersTotal": 190
        }
      ]
    }
  ]
}

What you'll above from the above responses is that the OrdersTotal for customer Sue is the aggregated total of 300 in the case of Ef Core - expected behaviour, but in the case of collections, aggregation didn't work as expected so we end up with a collection of two objects in the Orders collection each with the original Amount.
We should investigate why it doesn't work for object collections.

Expected behavior

Result when object collections are used as a data source should mirror the result when Ef Core database provider is used.

Additional context

Implementing GetHashCode and Equals on Customer and Order models didn't resolve the issue.

@gathogojr gathogojr added the bug Something isn't working label Mar 24, 2025
@habbes habbes added P3 P2 and removed P3 labels Mar 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working P2
Projects
None yet
Development

No branches or pull requests

2 participants