← All articles

Sorting, includes and paging, all typed

The rest of the read pipeline: repeatable sort enums (and the OrderOrThenBy trap), flags-enum includes that keep list payloads lean, and page-size clamps enforced once at the HTTP boundary.
Sorting, includes and paging, all typed

Filters decide which rows come back; this piece covers the rest of the read pipeline — in what order, with which related data, and how many at a time. All three are typed: enums the client sends, translated by registrations you write once. And all three compose with everything from the earlier articles into the same single SQL statement.

Sorting: an enum, repeatable

The client sends ?sortBy= values from an enum you define; a sort builder translates them. From the sample:

e.SortBy((query, sortBy) => sortBy switch
{
    VehicleSortBy.Code => query.OrderOrThenBy(x => x.Code),
    VehicleSortBy.CodeDesc => query.OrderOrThenByDescending(x => x.Code),
    VehicleSortBy.Brand => query.OrderOrThenBy(x => x.Brand!.Title),
    VehicleSortBy.BrandDesc => query.OrderOrThenByDescending(x => x.Brand!.Title),
    _ => query.OrderOrThenBy(x => x.Id)
});

?sortBy= is repeatable, and that's where the one sharp edge lives. The builder runs once per requested value, so each arm must start or continue the ordering. The framework's OrderOrThenBy / OrderOrThenByDescending extensions do exactly that — the hand-rolled is IOrderedQueryable<T> check people write instead compiles fine and throws on the first sorted request.

curl "$URL/vehicles?sortBy=BrandDesc&sortBy=Code&includes=Brand"
# Volvo (TRK-001, TRK-002), Scania (TRK-003), Mercedes-Benz (VAN-001, VAN-002)
# - brand descending, code ascending within a brand

Includes: lean by default, fat on request

Related data is a flags enum the client asks for — so list payloads stay small until somebody actually needs more:

[Flags]
public enum VehicleIncludes
{
    None = 0, Brand = 1, VehicleType = 2, Interventions = 4,
    All = Brand | VehicleType | Interventions
}

e.Includes((query, includes) =>
{
    if (includes?.HasFlag(VehicleIncludes.Brand) == true)
        query = query.Include(x => x.Brand);
    if (includes?.HasFlag(VehicleIncludes.Interventions) == true)
        query = query.Include(x => x.Interventions);
    // ...
    return query;
});
curl "$URL/vehicles"                  # brandTitle: null - nothing joined
curl "$URL/vehicles?includes=Brand"   # brandTitle: "Volvo", ...
curl "$URL/vehicles/1"                # everything - details always applies all includes

That last line is a deliberate default: Details(id) applies all registered includes, on the theory that a detail view wants the whole aggregate.

Two rules save real debugging time. An entity gets one Includes(...) registration — a second one replaces the first, so compose everything in one lambda. And an unexpectedly empty nested collection is a missing or wrongly-gated include, not a mapping bug — never something to "fix" in a filter.

Paging: defaults in options, clamps at the boundary

Two settings do the work: DefaultPageSize fills in when the request omits one, MaxPageSize caps what a request may ask for. The clamp is enforced once at the HTTP boundary — no client escapes it — while direct service-layer calls keep full control for jobs and exports.

curl "$URL/vehicles/search?pageSize=2&page=2"
# count 5, items TRK-003 + VAN-001   (page is 1-based; count = total matching rows)

Note what count is not: it's not the page length. It's the total the pager needs, computed server-side next to the page — which is exactly why the /search endpoint exists.

What to take away

Sorting, includes and paging are the same idea as filters wearing three more hats: a typed vocabulary the client sends, translated once at registration, composed by the pipeline. Typed means discoverable (OpenAPI shows the enums), and typed means the sharp edges — continuing an ordering, replacing an includes registration — surface in one place instead of every endpoint.

One piece left, and it answers the question every filter design eventually faces: AND, OR and the endpoints.

Docs: Regira Entities — sorting, includes, paging options.