DynamicWhere.ex
DynamicWhere.exv3.4.0·docs

.ToListAsync<T>(Segment)

Async-only segment operation. Combines every ConditionSet with set operations (Union / Intersect / Except) into one query, then orders, pages and projects it in the database exactly as a Filter is.

Warning
Async-only. .ToListAsync<T>(Segment) is the only entry point for segment queries — there is no synchronous .ToList<T>(Segment) variant. It needs an EF Core provider. On a type with a primary key, only Except needs one that translates a correlated EXISTS.

Signature

public static Task<SegmentResult<T>> ToListAsync<T>(
    this IQueryable<T> query,
    Segment segment)
    where T : class

// 3.2.0
public static Task<SegmentResult<T>> ToListAsync<T>(
    this IQueryable<T> query,
    Segment segment,
    CancellationToken cancellationToken)
    where T : class
ParameterTypeDescription
segmentSegmentComposition object — ConditionSets, Selects, Orders, Page
cancellationTokenCancellationTokenCancels the count and the read. New in 3.2.0; the overload without it passes CancellationToken.None

Pipeline

  • Combine the sets in Sort order, left to right, using each later set's Intersection. The first set's Intersection is ignored.
  • Union and Intersect join the sets' own conditions with OR and AND. Except removes the rows of its set with NOT EXISTS, matched on T's primary key as EF Core maps it — composite, value-converted and inherited keys included.
  • Apply Orders, then Page, then Selects to the combined query, and count it for TotalCount — the same steps, in the same order, as ToListAsync(Filter). Only the requested page is read, and an order field need not be selected.
  • The overload that takes a CancellationToken passes it to that count and that read. A canceled token stops whichever of the two is running, and the call throws OperationCanceledException.

A segment takes no getQueryString, so ToListAsync(segment, default) is not ambiguous: it binds the token overload and passes CancellationToken.None. The 3.1 signature is unchanged, so code compiled against 3.1 still binds.

Which rows belong is decided in the database, not by object reference, so a tracking query, an AsNoTracking() query and a query with Selects all return the same rows. Ordering is the database's: text sorts by its collation, and NULLs fall where the provider puts them.

Validations

  • ConditionSets Sort values must be unique — SetsUniqueSort.
  • Sets at index 1+ must have Intersection specified — RequiredIntersection.
  • Each ConditionSet.ConditionGroup is validated as in .Where<T>.
  • Orders (if provided): each Field must be non-empty and valid on T.
  • Page (if provided): both PageNumber and PageSize must be > 0.

Returns

Task<SegmentResult<T>> — inherits all properties from FilterResult<T>.

Example

var segment = new Segment
{
    ConditionSets = new List<ConditionSet>
    {
        new ConditionSet
        {
            Sort = 1,
            Intersection = null,
            ConditionGroup = new ConditionGroup
            {
                Connector = Connector.And,
                Conditions = new List<Condition>
                {
                    new Condition { Sort = 1, Field = "Category.Name", DataType = DataType.Text, Operator = Operator.Equal, Values = new List<object> { "Electronics" } }
                }
            }
        },
        new ConditionSet
        {
            Sort = 2,
            Intersection = Intersection.Union,
            ConditionGroup = new ConditionGroup
            {
                Connector = Connector.And,
                Conditions = new List<Condition>
                {
                    new Condition { Sort = 1, Field = "Price", DataType = DataType.Number, Operator = Operator.LessThan, Values = new List<object> { 20 } }
                }
            }
        },
        new ConditionSet
        {
            Sort = 3,
            Intersection = Intersection.Except,
            ConditionGroup = new ConditionGroup
            {
                Connector = Connector.And,
                Conditions = new List<Condition>
                {
                    new Condition { Sort = 1, Field = "IsActive", DataType = DataType.Boolean, Operator = Operator.Equal, Values = new List<object> { false } }
                }
            }
        }
    },
    Selects = new List<string> { "Id", "Name", "Price" },
    Orders  = new List<OrderBy> { new OrderBy { Sort = 1, Field = "Name", Direction = Direction.Ascending } },
    Page    = new PageBy { PageNumber = 1, PageSize = 20 }
};

SegmentResult<Product> result = await dbContext.Products.ToListAsync(segment);
{
  "conditionSets": [
    {
      "sort": 1,
      "intersection": null,
      "conditionGroup": {
        "connector": "And",
        "conditions": [
          { "sort": 1, "field": "Category.Name", "dataType": "Text", "operator": "Equal", "values": ["Electronics"] }
        ],
        "subConditionGroups": []
      }
    },
    {
      "sort": 2,
      "intersection": "Union",
      "conditionGroup": {
        "connector": "And",
        "conditions": [
          { "sort": 1, "field": "Price", "dataType": "Number", "operator": "LessThan", "values": ["20"] }
        ],
        "subConditionGroups": []
      }
    },
    {
      "sort": 3,
      "intersection": "Except",
      "conditionGroup": {
        "connector": "And",
        "conditions": [
          { "sort": 1, "field": "IsActive", "dataType": "Boolean", "operator": "Equal", "values": ["false"] }
        ],
        "subConditionGroups": []
      }
    }
  ],
  "selects": ["Id", "Name", "Price"],
  "orders": [
    { "sort": 1, "field": "Name", "direction": "Ascending" }
  ],
  "page": { "pageNumber": 1, "pageSize": 20 }
}

Logic: (Electronics) UNION (Price < 20) EXCEPT (Inactive) → order → paginate.

A type with no primary key compares whole rows
A keyless entity type, or a query EF Core does not map to T, has no key to match on. Its sets are combined with SQL UNION / INTERSECT / EXCEPT, which compare every column: identical rows collapse into one, a column the database cannot compare (PostgreSQL json, SQL Server xml) fails the query even when it is not selected, and the provider must support the operators the sets use.
Changed in 3.1.0
The sets used to be loaded one query each and combined in memory by object reference. With AsNoTracking(), with Selects, and under ApplyPolicy, which is always untracked, Intersect returned nothing, Except removed nothing and Union counted a row once per set. Ordering and paging ran in memory after projection, and every row of every set was read. See breaking changes.

A typed row is a whole Product, not a trimmed object: the members outside Selects are still present, holding their defaults.

{
  "pageNumber": 1,
  "pageSize": 20,
  "pageCount": 2,
  "totalCount": 35,
  "data": [
    {
      "id": 1,
      "name": "Adapter Cable",
      "price": 9.99,
      "isActive": false,
      "createdAt": "0001-01-01T00:00:00",
      "category": null
    }
  ],
  "queryString": null
}

See also