Skip to main content

Query Methods

This page covers the trade-offs between specific domain query methods and generic paginated queries, and how to decide which approach to use.

The Spectrum

Query methods on your custom repository fall on a spectrum from highly specific to fully generic:

Specific ←————————————————————————————→ Generic

FindByCodeAsync(string) FindProductsPagedAsync(PageQuery<Product>)
FindByNameAsync(string) FindAllAsync(IQuery)
FindByCategoryAsync(string) QueryPageAsync(PageQuery<Product>)
CodeExistsAsync(string) ExistsAsync(IQueryFilter)

Named methods that encapsulate a single domain query:

public interface IProductRepository : IRepository<Product, Guid> {
Task<Product?> FindByCodeAsync(string code, CancellationToken ct = default);
Task<IReadOnlyList<Product>> FindByNameAsync(string name, CancellationToken ct = default);
Task<IReadOnlyList<Product>> FindByCategoryAsync(string category, CancellationToken ct = default);
Task<bool> CodeExistsAsync(string code, CancellationToken ct = default);
Task<long> CountByCategoryAsync(string category, CancellationToken ct = default);
}

Advantages

BenefitExplanation
Clear intentThe method name communicates the domain meaning
Encapsulated logicQuery logic is tested once, in one place
SafeNo risk of runtime NotSupportedException from arbitrary expressions
VersionedAdding a new query is a new method — no breaking changes
DiscoverableIDE autocomplete shows all available queries

Disadvantages

DrawbackMitigation
More methods to writeEach method is small and focused
New query = new methodThis is a feature, not a bug — it forces deliberate design

When to Use

  • The query has a clear domain meaning (FindByCode, FindActiveOrders)
  • The query is used in multiple places
  • You want to guarantee the query works (tested, versioned)
  • The query involves complex logic that should not be duplicated

Generic Page Queries

Exposing a method that accepts a PageQuery<TEntity> and delegates to the protected QueryPageAsync:

public interface IProductRepository : IRepository<Product, Guid> {
Task<PageQueryResult<Product>> FindProductsPagedAsync(
PageQuery<Product> request,
CancellationToken ct = default);
}

public class ProductRepository : Repository<Product, Guid>, IProductRepository {
public async Task<PageQueryResult<Product>> FindProductsPagedAsync(
PageQuery<Product> request,
CancellationToken ct = default) {
return await QueryPageAsync(request, ct);
}
}

Advantages

BenefitExplanation
FlexibleOne method covers many query combinations
Less codeNo need to write a method for every query variation
ComposableConsumers can compose filters and sorts at runtime

Disadvantages

DrawbackExplanation
Risk accepted by domain ownerConsumers can compose arbitrary filters that may not be supported by the underlying engine
Less discoverableConsumers must know how to construct PageQuery<TEntity> and IQueryFilter
Runtime errorsUnsupported filter types may throw NotSupportedException at runtime
Leaky abstractionConsumers depend on Kista's query types, not just your domain interface

When to Use

  • You have many query variations that would require too many specific methods
  • You are building a search API where filters are dynamic
  • You accept the risk and document which filter types are supported
  • The consumer is internal (same team) and understands the query model

Hybrid Approach

Combine specific methods for common queries with a generic method for ad-hoc queries:

public interface IProductRepository : IRepository<Product, Guid> {
// Common, safe queries
Task<Product?> FindByCodeAsync(string code, CancellationToken ct = default);
Task<IReadOnlyList<Product>> FindActiveProductsAsync(CancellationToken ct = default);

// Generic paginated query — risk documented
Task<PageQueryResult<Product>> FindProductsPagedAsync(
PageQuery<Product> request,
CancellationToken ct = default);
}

Paginated Specific Queries

You can also combine specificity with pagination:

public interface IProductRepository : IRepository<Product, Guid> {
Task<PageQueryResult<Product>> FindByCategoryPagedAsync(
string category,
PageRequest request,
CancellationToken ct = default);

Task<PageQueryResult<Product>> FindByNamePagedAsync(
string name,
PageRequest request,
CancellationToken ct = default);
}

Implementation uses the protected QueryPageAsync:

public async Task<PageQueryResult<Product>> FindByCategoryPagedAsync(
string category,
PageRequest request,
CancellationToken ct = default) {

var pageQuery = new PageQuery<Product>(request.Page, request.Size)
.Where(p => p.Category == category);

return await QueryPageAsync(pageQuery, ct);
}

Decision Guide

ScenarioRecommended Approach
Single-entity look-up by natural keySpecific method (FindByCodeAsync)
Simple list querySpecific method (FindActiveProductsAsync)
Existence checkSpecific method (CodeExistsAsync)
Count by criteriaSpecific method (CountByCategoryAsync)
Paginated list with fixed filterPaginated specific method (FindByCategoryPagedAsync)
Search API with dynamic filtersGeneric method (FindProductsPagedAsync)
Admin dashboard with arbitrary filtersGeneric method, document supported filter types