DynamicWhere.ex
DynamicWhere.exv3.4.0·docs

.ToListAsync<T>(Summary)

Async version of .ToList<T>(Summary). On an EF Core query it counts the groups with EF Core's CountAsync() and reads them with EF Core's ToListAsync(). Since 3.2.0 two more overloads take a CancellationToken, which reaches both.

Guarded, small groups are dropped by default
DwCaps.MinGroupSize ships on, at 5, so a guarded summary removes every group with fewer than five rows — not refused, and nothing in the answer says a group was dropped. That is right for anonymised reporting and surprising for an operational count. Set Caps.MinGroupSize = 1 to switch the floor off, deliberately. An unguarded call is never floored. See k-anonymity.

Signature

public static Task<SummaryResult> ToListAsync<T>(
    this IQueryable<T> query,
    Summary summary,
    bool getQueryString = false)
    where T : class

// 3.2.0
public static Task<SummaryResult> ToListAsync<T>(
    this IQueryable<T> query,
    Summary summary,
    CancellationToken cancellationToken)
    where T : class

public static Task<SummaryResult> ToListAsync<T>(
    this IQueryable<T> query,
    Summary summary,
    bool getQueryString,
    CancellationToken cancellationToken)
    where T : class
ParameterTypeDefaultDescription
summarySummary–Composition object
getQueryStringboolfalseWhen true, captures the generated SQL on SummaryResult.QueryString
cancellationTokenCancellationToken–Cancels the count and the read. The overload without it passes CancellationToken.None

Pipeline

  • Where applied on the typed query.
  • Group applied — produces grouped dynamic intermediate.
  • Having applied — fields must reference aggregate aliases.
  • The grouped query is counted → TotalCount. On an EF Core query this is EF Core's CountAsync(cancellationToken).
  • Order applied on the grouped query.
  • Page applied on the grouped query.
  • Async materialization as List<dynamic>. On an EF Core query this is EF Core's ToListAsync(cancellationToken).

A source whose provider is not EF Core's — rows in memory through AsQueryable(), for one — keeps the reads it had in 3.1: a synchronous Count(), then Dynamic LINQ's ToDynamicListAsync(), which reads on the calling thread. A token that is already canceled still stops it before the count.

Changed in 3.2.0: the count and the read are asynchronous on EF Core
Until 3.2.0 the group count ran synchronously, and the read went through Dynamic LINQ's ToDynamicListAsync(), which had no token to pass on, on every provider. On an EF Core query both now run through EF Core's asynchronous operators, so a canceled token reaches the database. The count and the rows are the same.
Note
Dotted GroupBy fields like Category.Name become flattened aliases in the result (e.g., CategoryName). Order fields in Summary.Orders use the dotted form — the library handles alias mapping internally.

Cancellation

The two overloads that take a CancellationToken are new in 3.2.0. The token reaches the count and the read. On an EF Core query a canceled token stops whichever of the two is running, and the call throws OperationCanceledException. EF Core's TaskCanceledException derives from it. The overload without a token passes CancellationToken.None.

They are overloads, not an optional parameter added to the old signature. The 3.1 signature is unchanged, so code compiled against 3.1 still binds. The guarded handle that ApplyPolicy returns has the same overloads.

ToListAsync(summary, default) does not compile
default fits both bool getQueryString and CancellationToken, so the compiler reports the call as ambiguous (CS0121). Write false, a token, or a named argument.
await db.Products.ToListAsync(summary, default);                   // CS0121: ambiguous
await db.Products.ToListAsync(summary, cancellationToken);         // the token overload
await db.Products.ToListAsync(summary, true, cancellationToken);   // the SQL and a token

Returns

Task<SummaryResult>.

Example

SummaryResult result = await dbContext.Products.ToListAsync(summary);

foreach (var row in result.Data)
{
    Console.WriteLine($"{row.CategoryName}: {row.ProductCount} products");
}

ASP.NET Core endpoint.

app.MapPost("/products/summary", async (Summary summary, AppDbContext db) =>
{
    var result = await db.Products.ToListAsync(summary);
    return Results.Ok(result);
});
{
  "pageNumber": 1,
  "pageSize": 10,
  "pageCount": 1,
  "totalCount": 3,
  "data": [
    { "CategoryName": "Electronics", "ProductCount": 15, "AvgPrice": 349.99, "TotalRevenue": 5249.85 }
  ],
  "queryString": null
}

See also