Skip to content

Expression Builders Common

RzR edited this page May 1, 2026 · 1 revision

Expression Builders (Common)

PagedListResult.Common (assembly RzR.ResultMessage.Pagination.Core) translates the DTOs from PagedListResult.DataModels into IQueryable<T> operations. It has no EF Core dependency. You can use it directly against LINQ-to-Objects, against any IQueryable<T> provider, or as the building block of your own custom adapter.

This page describes what the builders do and the public surface you can call. The EF Core integration (EntityFramework Core Extensions) calls these helpers internally; you almost never need to call them yourself.

Folder layout

Folder Contents
Attributes/ PaginationDefaultOrderPropertyAttribute, PaginationDefaultTopRecordPrimaryKeyAttribute.
Extensions/Filters/ The four families of expression builders.
Extensions/Filters/PropertyFilterQuery/ Per-condition expression visitors.
Internal/ Reflection caches and helper utilities.
Helpers/ PredefinedRecordsHelper, TimeWatchHelper.

Search builder

FilterSourceByTextExpression.FilterSourceByText(...) is invoked by GetPaged* to apply the Search block:

query = query.FilterSourceByText(
    request.Search.SearchInAllTextFields,
    request.Search.SearchInAllFields,
    request.Search.Search,
    request.Search.CustomSearchTextProperties,
    typeof(TSource));

Resolution order for the candidate property set:

  1. SearchInAllTextFields == true -> every string property on the entity (default).
  2. SearchInAllFields == true -> every property on the entity (the builder will use ToString() and Contains for non-string types).
  3. Otherwise -> only the names listed in CustomSearchTextProperties.

The matching is Contains, case-insensitive. Empty / whitespace search terms are no-ops.

Order builder

FilterSourceOrderExtensions exposes:

  • OrderByWithDirection(string property, OrderDirection direction, ...) — orders by an explicit property name. The string is matched against the public properties of TSource and the direction is applied through the standard LINQ chain.
  • OrderByWithDirectionByDefaultProperty(OrderDirection direction, ...) — used when Order.OrderByDefaultProperty is true and no explicit property was provided. It looks for the first property decorated with [PaginationDefaultOrderProperty] and orders by it.

If none of the above resolves to an actual property, the EF Core extensions fall back to query.OrderBy(x => 0) so that Skip/Take remain deterministic.

Marking a default sort property

public sealed class Product
{
    public Guid Id { get; set; }

    [PaginationDefaultOrderProperty]
    public string Name { get; set; }

    public decimal Price { get; set; }
}

A request that arrives with Order.OrderByDefaultProperty = true and no explicit OrderByProperty will be ordered by Name.

Filter builder

The filter machinery lives in FilterSourceQueryExtensions and the per-condition visitors under Extensions/Filters/PropertyFilterQuery/.

Two public entry points are used by the EF Core layer:

  • AsSimpleFilterable(IEnumerable<DataFilter> filters) — composes the filters with a top-level AND, ignoring Dependencies. Used by GetPagedWithMainFiltersAsync.
  • AsFilterable(IEnumerable<DataFilter> filters, FilterConditionType filterLink) — composes the filters with the supplied top-level link (And/Or) and honors each filter's Dependencies collection. Used by GetPagedWithFiltersAsync.

The per-condition visitors translate FilterType values into Expression<Func<T, bool>> predicates, with appropriate handling of:

  • nullable types,
  • string comparisons (case-sensitive vs. case-insensitive variants),
  • numeric, DateTime, Guid and Enum parsing from the string-encoded Values collection,
  • Between, which needs both Values[0] and CompareValue,
  • IsIn/IsNotIn, which build a typed HashSet<T> and emit a Contains call on it,
  • IsNull/IsNotNull, which emit a value-less predicate.

Predefined records ("pinned rows")

PredefinedRecordsHelper.BuildPredefinedFilter(query, defaultPrimaryKey, predefinedRecord) resolves the property name and the typed identifier collection used to pin a few rows on top of the result. It honours, in order:

  1. PredefinedRecord.PredefinedFieldName if set.
  2. The property decorated with [PaginationDefaultTopRecordPrimaryKey] if DefaultPrimaryKeyDefinition.FindByAttribute == true.
  3. The entity's primary key resolved through DbContext.Model if FindByEntity == true.
  4. The explicit DefaultPrimaryKeyDefinition.DefaultPrimaryKey value.

FilterPredefinedTopRecordExtensions exposes the two shaping methods:

  • GetInTopPredefinedRecords(values, propertyNames) — selects the rows whose key is in the set.
  • GetNotInTopPredefinedRecords(values, propertyNames) — selects everything else, suitable for the standard paging slice.

The EF Core layer combines both into a single result so that the predefined rows occupy the first slots of page 1 and never bleed into pages 2+.

Marking a default top-record key

public sealed class Product
{
    [PaginationDefaultTopRecordPrimaryKey]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

When the request omits PredefinedFieldName, this attribute tells the builder to match identifiers against Id.

Reflection caches

ReflectionTypeStorage and the helpers under Internal/ cache PropertyInfo lookups by (type, property name). The cost of building each predicate is therefore dominated by the expression construction itself (a constant overhead), not by reflection. The caches are static and thread-safe.

When to use this package directly

  • You want pagination over an in-memory collection (IEnumerable<T> is converted to IQueryable<T> via AsQueryable()).
  • You want to call the filter builder against a non-EF Core provider (Cosmos, Marten, NHibernate, custom LINQ provider).
  • You want to compose predicates and apply them yourself instead of going through GetPaged*.

For everything else, prefer the higher-level PagedListResult package.

Clone this wiki locally