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.63k stars 3.15k forks source link

Implement top-level (queryable) versions of custom support aggregate operators #28264

Open roji opened 2 years ago

roji commented 2 years ago

2981, #28104 and #13278 introduced various aggregate operators, but only IEnumerable versions. This means that they can be used with GroupBy:

_ = context.Products
    .GroupBy(od => od.ProductID)
    .Select(g => new
    {
        ProductID = g.Key,
        StandardDeviation = EF.Functions.StandardDeviationSample(g.Select(p => p.UnitPrice)),
    });

... but cannot be used at the query top-level - without GroupBy - where IQueryable versions would be required. The team discussed this and decided to punt the top-level operators out of 7.0, and revisit this based on user feedback.

Workaround

In the meantime, users can work around this gap by grouping over a constant:

var foo = ctx.Products
    .GroupBy(b => 1)
    .Select(g => EF.Functions.StandardDeviationSample(g.Select(p => p.UnitPrice))
    .FirstOrDefault();

Design discussion

See original discussion in https://github.com/dotnet/efcore/issues/28104#issuecomment-1142427678 on how to represent the top-level operators. Options include:

Extension over IQueryable

_ = context.Products.StandardDeviationSample(p => p.UnitPrice); // non-built-in
_ = context.Products.StringJoin(p => p.Name, ", "); // built-in

EF.Functions

_ = EF.Functions.StandardDeviationSample(context.Products.Select(p => p.UnitPrice)); // non-built-in
_ = EF.Functions.StringJoin(context.Products.Select(p => p.Name), ", "); // built-in

Context.Query

We have an "unrelated" feature to generally allow expressing queries within a lambda (ISSUE NEEDED):

context.Query<T>(() => EF.Functions.StandardDeviationSample(context.Products.Select(p => p.UnitPrice))) // non-built-in
context.Query<T>(() => string.Join(", ", context.Products.Select(p => p.Name)) // built-in

Note: Infra for top-level aggregates was done in #28102 Note: See https://github.com/npgsql/efcore.pg/issues/727 for the Npgsql epic covering PG aggregate operators - we'd have to do the same for those.

roji commented 2 years ago

One additional idea from @divega: define a new “SelectOne” operator that would be essentially a shortcut for GroupBy(constant).Select().

roji commented 2 years ago

Note: remember we have FromExpression for mapping TVFs which is somewhat related, docs (again thanks @divega).

roji commented 1 year ago

Note #30538 which is a partial dup of this.