Skip to content

Custom List Endpoints (QueryKit)

Use QueryKit when you need paginated list endpoints with domain-specific joins/aggregations that do not fit the generic CRUD pipeline.

Why QueryKit

  • Reuses a standard request model for page/sort/search.
  • Applies filter composition and sort whitelist.
  • Returns consistent OperationResponse<TData> with pagination meta.
  • Keeps custom business rules in dedicated handlers.

Core building blocks

ComponentNamespaceResponsibility
AggregateQueryRequestGrydCrud.Core.QueryKit.RequestsBase request for custom list endpoints
IQueryFilter<TRequest, TReadModel>GrydCrud.Core.QueryKit.FiltersComposable query filters
QuerySortMap<TRequest, TReadModel>GrydCrud.Core.QueryKit.SortingSort whitelist + expression mapping
IQuerySpecBuilder<TRequest, TReadModel>GrydCrud.Core.QueryKit.SpecShapes query from filters/sort
CustomListQuery<TRequest, TItem>GrydCrud.Cqrs.QueriesMediatR query wrapper
CustomListQueryHandlerBase<...>GrydCrud.Cqrs.HandlersBase paginated custom-list handler
CustomListController<TRequest, TItem>GrydCrud.API.ControllersStandardized HTTP endpoint response/meta

Standard response

Custom list controllers return:

json
{
  "data": [],
  "meta": {
    "page": 1,
    "pageSize": 20,
    "totalCount": 0,
    "totalPages": 0,
    "hasNext": false,
    "hasPrevious": false,
    "sortBy": "createdAt",
    "sortDirection": "desc"
  }
}

Example flow

1) Define request

csharp
public sealed record ListInvoicesRequest : AggregateQueryRequest
{
    public Guid? ClientId { get; init; }
    public DateTime? FromDate { get; init; }
    public DateTime? ToDate { get; init; }
}

2) Define query

csharp
public sealed class ListInvoicesQuery : CustomListQuery<ListInvoicesRequest, InvoiceListItemDto>
{
    public static readonly string[] SortFields = ["referenceMonth", "clientName", "status"];
    public ListInvoicesQuery(ListInvoicesRequest request) : base(request, SortFields) {}
}

3) Filters + sort map

csharp
public sealed class InvoicesByClientFilter : IQueryFilter<ListInvoicesRequest, InvoiceReadModel>
{
    public IQueryable<InvoiceReadModel> Apply(IQueryable<InvoiceReadModel> query, ListInvoicesRequest request) =>
        request.ClientId.HasValue ? query.Where(x => x.ClientId == request.ClientId.Value) : query;
}

public sealed class InvoiceSortMap : QuerySortMap<ListInvoicesRequest, InvoiceReadModel>
{
    public InvoiceSortMap()
    {
        Map("referenceMonth", x => x.ReferenceMonth);
        Map("clientName", x => x.ClientName);
        Map("status", x => x.Status);
        SetDefault("referenceMonth", "desc");
    }
}

4) Handler

csharp
public sealed class ListInvoicesQueryHandler
    : CustomListQueryHandlerBase<ListInvoicesRequest, InvoiceReadModel, InvoiceListItemDto>
{
    // Inject data source + spec builder + normalizer + CrudOptions
}

5) Controller

csharp
[Route("api/v{version:apiVersion}/invoices/custom-list")]
public sealed class InvoicesCustomListController
    : CustomListController<ListInvoicesRequest, InvoiceListItemDto>
{
    protected override CustomListQuery<ListInvoicesRequest, InvoiceListItemDto> CreateQuery(ListInvoicesRequest request) =>
        new ListInvoicesQuery(request);
}

DI registration

csharp
services.AddGrydCrud(options =>
{
    options.DefaultPageSize = 20;
    options.MaxPageSize = 100;
});

services.AddScoped<IQueryFilter<ListInvoicesRequest, InvoiceReadModel>, InvoicesByClientFilter>();
services.AddScoped<IQuerySortApplier<ListInvoicesRequest, InvoiceReadModel>, InvoiceSortMap>();
services.AddScoped<IQuerySpecBuilder<ListInvoicesRequest, InvoiceReadModel>, DefaultQuerySpecBuilder<ListInvoicesRequest, InvoiceReadModel>>();
services.AddCustomListHandler<ListInvoicesRequest, InvoiceListItemDto, ListInvoicesQueryHandler>();

Recommendations

  1. Always whitelist sortBy values.
  2. Keep filters typed (never free-form SQL strings).
  3. Keep heavy joins/projections in read-model sources.
  4. Add integration tests for data + meta contract.

Released under the MIT License.