IQueryPort Interface
Overview
Section titled “Overview”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.
Learning Objectives
Section titled “Learning Objectives”After completing this chapter, you will be able to:
- Explain the roles of IQueryPort<TEntity, TDto>‘s two type parameters
- Choose the appropriate query method among Search, SearchByCursor, and Stream based on the use case
- Understand the structure of PagedResult
and CursorPagedResult return types - Explain why Query Port returns DTOs instead of domain entities
Core Concepts
Section titled “Core Concepts””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 memoryvar filtered = allProducts .Where(p => p.IsInStock) // Filter in memory .OrderBy(p => p.Price) // Sort in memory .Skip(20).Take(20); // Paginate in memoryAll 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.
Type Parameters
Section titled “Type Parameters”IQueryPort separates the filtering target and return type with two type parameters.
| Parameter | Role | Example |
|---|---|---|
TEntity | Specification filtering target (domain entity) | Product |
TDto | Read-only projection returned to clients | ProductDto |
Three Query Methods
Section titled “Three Query Methods”Different query approaches are suitable depending on data volume and usage patterns. IQueryPort provides all three methods.
| Method | Return Type | Purpose |
|---|---|---|
Search | FinT<IO, PagedResult<TDto>> | Offset-based pagination |
SearchByCursor | FinT<IO, CursorPagedResult<TDto>> | Keyset-based pagination |
Stream | IAsyncEnumerable<TDto> | Large data streaming |
IObservablePort
Section titled “IObservablePort”IQueryPort, like IRepository, inherits from IObservablePort. Query-side implementations return RequestCategory => "Query", enabling the Observability pipeline to collect Command and Query metrics separately.
Command Side vs Query Side
Section titled “Command Side vs Query Side”In CQRS, writes and reads take different paths. Check how the two paths mirror each other in the table below.
| Aspect | Command Side | Query Side |
|---|---|---|
| Port | IRepository | IQueryPort<TEntity, TDto> |
| Return | Entity (domain model) | DTO (read-only projection) |
| Purpose | State change (CUD) | Data query (R) |
| Filtering | ID-based | Specification-based |
Project Description
Section titled “Project Description”ProductId / Product
Section titled “ProductId / Product”Domain entity with Ulid-based EntityId inheriting AggregateRoot. Used as TEntity for Specification
ProductDto
Section titled “ProductDto”A read-only record containing only the fields clients need, not all fields of the domain entity.
IProductQuery
Section titled “IProductQuery”public interface IProductQuery : IQueryPort<Product, ProductDto> { }A Product domain-specific Query Port. Inherits the three query methods by extending IQueryPort<Product, ProductDto>.
Summary at a Glance
Section titled “Summary at a Glance”| Item | Description |
|---|---|
| IQueryPort | CQRS Query-side port interface |
| TEntity | Specification filtering target (domain entity) |
| TDto | Read-only projection for client return |
| Search | Offset-based pagination (PagedResult) |
| SearchByCursor | Keyset-based pagination (CursorPagedResult) |
| Stream | Large 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.
Q2: Why return DTOs?
Section titled “Q2: Why return DTOs?”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.
Q3: What is FinT<IO, T>?
Section titled “Q3: What is FinT<IO, T>?”A: LanguageExt’s monad transformer that composes IO effects (side effects) with Fin
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.