PagedListResult: A Contract-Driven Approach to Pagination in .NET
#Backend

PagedListResult: A Contract-Driven Approach to Pagination in .NET

Backend Reporter
5 min read

PagedListResult provides a structured approach to server-side pagination by separating intent, interpretation, and execution layers. This .NET library eliminates repetitive pagination logic while maintaining explicit contracts, testable behavior, and rich metadata for API consumers.

The Challenge of Pagination in Distributed Systems

Pagination represents one of the most fundamental yet consistently problematic patterns in API development. Teams repeatedly implement similar logic across endpoints, often with subtle variations in behavior. The resulting inconsistencies create maintenance burdens and unpredictable client experiences. Common pain points include:

  • Scattered pagination parameters across controller actions
  • Inconsistent metadata formats between endpoints
  • Ambiguous ordering semantics affecting result stability
  • Duplication of query logic across services
  • Tight coupling between data access and presentation concerns

These issues compound in distributed systems where multiple services need to expose paginated data with consistent behavior.

PagedListResult: Layered Architecture for Pagination

PagedListResult addresses these challenges through a deliberately opinionated architecture that separates concerns across three distinct layers. This separation isn't merely organizational—it directly reflects how pagination logic should be decomposed in production systems.

Contract Layer: Defining the Interface

The foundation of PagedListResult is its DataModels project, which establishes stable, serializable contracts between clients and query execution. This layer acts as an architectural boundary, ensuring pagination behavior remains explicit and versionable across services.

Central to this approach is the PagedRequest type, which consolidates all query intent into a single object rather than scattering parameters across method signatures. This design pattern offers several advantages:

  1. Request Evolution: Adding new pagination capabilities doesn't require changing method signatures
  2. Consistency: All endpoints consume pagination through the same contract
  3. Testability: Request models can be unit tested independently of data access
  4. Documentation: The contract itself serves as clear documentation of supported capabilities

The library models different aspects of pagination through specialized types:

  • OrderOptions: Expresses sorting intent without binding to specific LINQ or database constructs
  • SearchOptions: Isolates search behavior from filtering concerns
  • Filter Models: Provides a structured hierarchy for complex filtering conditions
  • PagedResult<T>: Returns both the data subset and complete metadata for client-side controls

This contract-first approach enables the same pagination request to be reused across ASP.NET Core controllers, application services, background jobs, and integration tests without modification.

Interpretation Layer: Translating Intent to Logic

The Common project bridges the gap between high-level requests and executable queries. Its responsibility is converting DataModel contracts into expression trees that can be applied to IQueryable<T> sources without knowledge of the underlying provider.

This layer demonstrates an important architectural principle: interpretation should be separate from execution. By building expression trees rather than imperative queries, the library enables:

  • Provider-level optimization through deferred execution
  • Compatibility with various LINQ providers (Entity Framework, Dapper, custom implementations)
  • Composability of different query concerns

The expression-driven approach allows each pagination aspect (filter, search, order) to be handled independently and then combined deterministically. This separation proves valuable when requirements evolve or when different endpoints need variations of the same query logic.

Execution Layer: Orchestrating the Pipeline

The core PagedListResult project assembles the complete pagination pipeline through extension methods over IQueryable<T>. This design deliberately avoids base repository abstractions or framework-specific dependencies, keeping the implementation lightweight and unobtrusive.

The execution pipeline follows a strict order to ensure predictable behavior:

  1. Request Validation: Enforces page boundaries, size constraints, and property existence
  2. Search Application: Reduces dataset early through text-based expressions
  3. Filter Application: Applies structured conditions with logical operators
  4. Ordering Enforcement: Ensures deterministic ordering for stable pagination
  5. Row Count Evaluation: Calculates total matching records before pagination
  6. Pagination Window: Applies Skip/Take operations to the final dataset
  7. Result Materialization: Captures execution timing and populates the response

This orchestration addresses a critical pagination challenge: maintaining result stability across multiple requests. By enforcing ordering before pagination and evaluating row counts before applying the page window, the library guarantees consistent behavior even as datasets change.

Serialization and API Integration

A distinguishing feature of PagedListResult is its built-in support for structured serialization targeting both JSON and XML consumers. This design reflects the reality that paginated results most often flow across system boundaries as API responses.

The library achieves this through:

  • Deterministic result contracts independent of execution context
  • Explicit metadata representation in serialized output
  • No reliance on custom converters or runtime mutation

This approach eliminates transformation overhead at API boundaries, reducing controller/service boilerplate while ensuring stable contracts for front-end consumers. The JSON and XML representations directly mirror the result structure, making them predictable for client-side pagination components.

Trade-offs and Considerations

PagedListResult's opinionated architecture involves several deliberate trade-offs:

Flexibility vs. Convention

The library prioritizes established patterns over configurability. While this reduces decision fatigue for teams, it may require adaptation for organizations with entrenched pagination patterns. The contract-first approach, however, often proves beneficial in the long run by establishing consistent behavior across services.

Layer Overhead vs. Maintainability

The three-layer architecture introduces additional abstractions compared to simple helper methods. This overhead pays dividends in maintainability, testability, and the ability to evolve pagination capabilities without widespread changes.

Immediate Execution vs. Deferred Optimization

By operating on IQueryable<T>, the library preserves provider-level optimization opportunities. This approach differs from some pagination solutions that materialize intermediate results, potentially missing important optimization opportunities in the underlying data store.

Implementation Patterns and Best Practices

Teams adopting PagedListResult should consider several implementation patterns:

  1. Request Validation: Implement custom validation logic for domain-specific constraints
  2. Property Mapping: Configure how string-based property names map to actual entity properties
  3. Fallback Strategies: Define default ordering when none is specified
  4. Performance Monitoring: Leverage ExecutionDetails for query performance analysis
  5. Contract Versioning: Plan for evolution of pagination request/response models

The library's separation of concerns makes these adaptations possible without modifying core functionality.

Conclusion

PagedListResult represents a thoughtful approach to pagination that addresses common pain points in distributed systems. By separating intent, interpretation, and execution, it provides a foundation for consistent, testable, and maintainable pagination logic across .NET applications.

For teams building serious, data-driven APIs, this repository offers more than just pagination helpers—it provides a contract-driven framework that aligns with modern service-oriented architecture patterns. The explicit contracts, rich metadata, and execution diagnostics make it particularly valuable in environments where pagination behavior must be consistent across multiple services and client applications.

The project demonstrates that pagination, when designed with proper architectural separation, can evolve from repetitive boilerplate into a well-structured system capability that enhances both developer productivity and system reliability.

For more information, visit the PagedListResult GitHub repository.

Comments

Loading comments...