Appearance
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 paginationmeta. - Keeps custom business rules in dedicated handlers.
Core building blocks
| Component | Namespace | Responsibility |
|---|---|---|
AggregateQueryRequest | GrydCrud.Core.QueryKit.Requests | Base request for custom list endpoints |
IQueryFilter<TRequest, TReadModel> | GrydCrud.Core.QueryKit.Filters | Composable query filters |
QuerySortMap<TRequest, TReadModel> | GrydCrud.Core.QueryKit.Sorting | Sort whitelist + expression mapping |
IQuerySpecBuilder<TRequest, TReadModel> | GrydCrud.Core.QueryKit.Spec | Shapes query from filters/sort |
CustomListQuery<TRequest, TItem> | GrydCrud.Cqrs.Queries | MediatR query wrapper |
CustomListQueryHandlerBase<...> | GrydCrud.Cqrs.Handlers | Base paginated custom-list handler |
CustomListController<TRequest, TItem> | GrydCrud.API.Controllers | Standardized 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
- Always whitelist
sortByvalues. - Keep filters typed (never free-form SQL strings).
- Keep heavy joins/projections in read-model sources.
- Add integration tests for
data + metacontract.