Skip to content

EntityFramework Core Extensions

RzR edited this page May 1, 2026 · 1 revision

EntityFramework Core Extensions

PagedListResult (assembly RzR.ResultMessage.Pagination.EntityFrameworkCore) is the package most consumers actually install. It exposes four extension methods on IQueryable<T> through the static PaginatedQueryExtensions class, plus SOAP/XML adapters for the result envelope.

This page documents each method, its overloads, what part of the request it honors, and when to use which.

Method overview

Method Filter handling Honours Order/Search/Fields/PredefinedRecord Async
GetPaged(page, pageSize) None No (no request object) No
GetPaged(request, defaultPrimaryKey?) None Yes No
GetPagedAsync(page, pageSize, ct) None No Yes
GetPagedAsync(request, defaultPrimaryKey?, ct) None Yes Yes
GetPagedWithMainFiltersAsync(request, defaultPrimaryKey?, ct) Top-level filters AND-combined; Dependencies ignored Yes Yes
GetPagedWithFiltersAsync(request, defaultPrimaryKey?, filterLink?, ct) Top-level filters combined by filterLink (default And); each filter's Dependencies honored Yes Yes

All request-bound overloads are generic on TPageRequest : PagedRequest (or PageRequestWithFilters), so you can derive your own request shape and the library will still bind it correctly.

GetPaged(page, pageSize) and async variant

Use these when you do not need search/order/filters and only want to slice an IQueryable<T>:

PagedResult<Product> result = await _db.Products
    .OrderBy(p => p.Id)
    .GetPagedAsync(page: 1, pageSize: 25, cancellationToken);

Both page and pageSize are validated to be positive (ArgumentException otherwise). The execution time is captured via TimeWatchHelper and stored in result.ExecutionDetails.

GetPaged(request) and GetPagedAsync(request)

Use these when the client sends a PagedRequest (or PageRequestWithFilters) but you do not need its Filters collection. Pipeline:

  1. Search — apply request.Search via FilterSourceByText if the search definition is non-null.
  2. Total count — query.Count() (or CountAsync in the async overload).
  3. Order — apply request.Order.OrderByProperty if present, otherwise the [PaginationDefaultOrderProperty] fallback if OrderByDefaultProperty == true, otherwise OrderBy(x => 0).
  4. Page math — compute PageCount = Math.Ceiling(RowCount / PageSize) and skip = (Page - 1) * PageSize.
  5. Predefined records — only on page 1 (skip <= 0). When defaultPrimaryKey is provided and the request has any PredefinedRecord.PredefinedRecords, the library:
    • resolves the property to match against (PredefinedFieldName if present, otherwise via the attribute / EF model / explicit fallback);
    • prepends the matched rows to the response;
    • takes pageSize - matchedCount from the rest of the query.
  6. Slice — Skip(skip).Take(pageSize) (or async equivalent).
  7. Fields projection — when request.Fields is non-empty, the response items are flattened to the listed property names through JsonSerializer (handy when shaping stored-procedure outputs).
  8. Result — set IsSuccess = true and write ExecutionDetails.

Important: Filters from PageRequestWithFilters are not applied by these overloads. Use one of the two filter-aware variants below.

return await _db.Products
    .AsNoTracking()
    .GetPagedAsync(
        request,
        new DefaultPrimaryKeyDefinition("Id"), // optional
        cancellationToken);

GetPagedWithMainFiltersAsync(request)

The simplest filter pipeline. The request must derive from PageRequestWithFilters. The method:

  1. Validates the request via IValidatableObject.Validate(...) and throws on failure (ApplicationException with the first error).
  2. Calls query.AsSimpleFilterable(request.Filters) to AND-combine the top-level filters (each filter's Dependencies is ignored).
  3. Delegates to GetPagedAsync(request, defaultPrimaryKey, ct) to apply search/order/predefined/paging.
  4. Updates ExecutionDetails.ExecutionTimeMs with the cumulative time.

Use this when the client filters are simple AND-combined predicates (the most common case for back-office grids).

return await _db.Products
    .AsNoTracking()
    .GetPagedWithMainFiltersAsync(request, cancellationToken: ct);

The translated SQL will look like:

WHERE (status = 'active') AND (price > 100) AND (createdOn >= @from)

GetPagedWithFiltersAsync(request, filterLink?)

The full filter pipeline. The signature adds filterLink (FilterConditionType.And or FilterConditionType.Or, default And) and honors each filter's Dependencies collection.

return await _db.Products
    .AsNoTracking()
    .GetPagedWithFiltersAsync(
        request,
        defaultPrimaryKey: new DefaultPrimaryKeyDefinition("Id"),
        filterLink: FilterConditionType.Or,
        cancellationToken: ct);

The expression looks like (pseudo-SQL):

WHERE
   ((authorId IN (2)) OR (authorId = 3)) -- one filter + its dependency
   AND                    -- top-level link from filterLink
   (createdOn BETWEEN @start AND @end)

The combination is built bottom-up:

  1. For each top-level filter, the per-condition visitor builds a Predicate<T>.
  2. For each Dependence of that filter, a second predicate is built and combined with the parent predicate via AndAlso / OrElse according to ParentFilterLinkType.
  3. The resulting per-filter predicates are combined together via filterLink.

Validation rules from DataFilterValue.Validate(...) are enforced first; an invalid request throws ApplicationException carrying the first error. The Web layer wraps this into a 400 ProblemDetails response.

SOAP/XML adapters

The package ships two ToSoapXmlPagedResult(...) extensions that flatten a PagedResult<T> (or IPagedResult<T>) into a SoapXmlPagedResult carrying string messages and execution details suitable for XmlSerializer. These are used by BaseApiPagedResultController.PagedXmlResult<T> to produce a text/xml response when an XML payload is required.

SoapXmlPagedResult xml = result.ToSoapXmlPagedResult();
string serialized = ObjectExtensions.SerializeToString(xml);

The serialization is symmetrical: a paged result that round-trips through SOAP/XML retains all paging metadata, the response items, the success flag and the messages.

Cancellation

Every async method accepts CancellationToken as the last parameter (defaults to CancellationToken.None). The token is forwarded to ToListAsync / CountAsync. If the token fires after the count but before the slice, the partial work is discarded and OperationCanceledException is propagated as expected.

Performance notes

  • The library issues two database trips for any non-empty request: one for Count() and one for Skip(skip).Take(pageSize). With predefined records the slice can be split in two queries, capped at pageSize - matchedCount rows for the main query.
  • All reflection lookups are cached statically.
  • The default-primary-key probe walks DbContext.Model once per query type; if you need to bypass EF Core entirely (DTO projections without an entity counterpart), set DefaultPrimaryKeyDefinition.DefaultPrimaryKey explicitly.
  • GetPaged* returns PagedResult<TSource>, not IQueryable<TSource>. Materialization happens inside the extension; do not call ToListAsync() again on the result.

Cookbook references

For end-to-end controller and minimal-API examples that combine these extensions with the Web integration, see Recipes.

Clone this wiki locally