Skip to content

IQueryPort Interface

What happens when you query order lists with Repository’s GetById? You’d have to load Aggregates one by one and then filter in memory. When you try to solve read-specialized requirements like list queries, search, and pagination with Repository, inefficiency accumulates. IQueryPort<TEntity, TDto> is the core interface of the CQRS Query side that solves this problem.


After completing this chapter, you will be able to:

  1. Explain the roles of IQueryPort<TEntity, TDto>‘s two type parameters
  2. Choose the appropriate query method among Search, SearchByCursor, and Stream based on the use case
  3. Understand the structure of PagedResult and CursorPagedResult return types
  4. Explain why Query Port returns DTOs instead of domain entities

”Why Is This Needed?” — Limitations of Repository-Based Reads

Section titled “”Why Is This Needed?” — Limitations of Repository-Based Reads”

Repository is optimized for single-item lookups. Imagine handling the request “show me 20 in-stock products sorted by price” with Repository.

// Querying lists with Repository?
var allProducts = await repository.GetAll(); // Load all Aggregates into memory
var filtered = allProducts
.Where(p => p.IsInStock) // Filter in memory
.OrderBy(p => p.Price) // Sort in memory
.Skip(20).Take(20); // Paginate in memory

All Aggregates are loaded, then filtered, sorted, and sliced in memory. Performance degrades sharply as data grows. This is why a read-only interface is needed.

IQueryPort separates the filtering target and return type with two type parameters.

ParameterRoleExample
TEntitySpecification filtering target (domain entity)Product
TDtoRead-only projection returned to clientsProductDto

Different query approaches are suitable depending on data volume and usage patterns. IQueryPort provides all three methods.

MethodReturn TypePurpose
SearchFinT<IO, PagedResult<TDto>>Offset-based pagination
SearchByCursorFinT<IO, CursorPagedResult<TDto>>Keyset-based pagination
StreamIAsyncEnumerable<TDto>Large data streaming

IQueryPort, like IRepository, inherits from IObservablePort. Query-side implementations return RequestCategory => "Query", enabling the Observability pipeline to collect Command and Query metrics separately.

In CQRS, writes and reads take different paths. Check how the two paths mirror each other in the table below.

AspectCommand SideQuery Side
PortIRepositoryIQueryPort<TEntity, TDto>
ReturnEntity (domain model)DTO (read-only projection)
PurposeState change (CUD)Data query (R)
FilteringID-basedSpecification-based

Domain entity with Ulid-based EntityId inheriting AggregateRoot. Used as TEntity for Specification.

A read-only record containing only the fields clients need, not all fields of the domain entity.

public interface IProductQuery : IQueryPort<Product, ProductDto> { }

A Product domain-specific Query Port. Inherits the three query methods by extending IQueryPort<Product, ProductDto>.


ItemDescription
IQueryPortCQRS Query-side port interface
TEntitySpecification filtering target (domain entity)
TDtoRead-only projection for client return
SearchOffset-based pagination (PagedResult)
SearchByCursorKeyset-based pagination (CursorPagedResult)
StreamLarge data streaming (IAsyncEnumerable)

Q1: Why does IQueryPort exist separately from IRepository?

Section titled “Q1: Why does IQueryPort exist separately from IRepository?”

A: Following CQRS principles, Command (write) and Query (read) are separated. Repository handles domain entity persistence, while QueryPort handles read-only projections. This separation allows each to be optimized independently.

A: Returning domain entities directly (1) exposes unnecessary domain logic to clients, (2) causes N+1 problems, and (3) makes read optimization difficult. With DTOs, you can query only needed fields and fetch all required data in a single query through JOINs.

A: LanguageExt’s monad transformer that composes IO effects (side effects) with Fin (success/failure results). It safely represents results of operations with side effects like database queries.


We’ve defined the read-only interface. But should the Order used in Commands and the OrderDto returned in Queries be the same class? In the next chapter, we’ll examine the design criteria for separating Command DTOs and Query DTOs.

-> Chapter 2: DTO Separation