dotnet-cqrs/docs/core-features/dynamic-queries
2025-12-11 01:18:24 -05:00
..
alter-queryable-services.md this is a mess 2025-12-11 01:18:24 -05:00
filters-and-sorts.md this is a mess 2025-12-11 01:18:24 -05:00
getting-started.md this is a mess 2025-12-11 01:18:24 -05:00
groups-and-aggregates.md this is a mess 2025-12-11 01:18:24 -05:00
interceptors.md this is a mess 2025-12-11 01:18:24 -05:00
queryable-providers.md this is a mess 2025-12-11 01:18:24 -05:00
README.md this is a mess 2025-12-11 01:18:24 -05:00

Dynamic Queries Overview

Dynamic queries provide OData-like filtering, sorting, grouping, and aggregation capabilities for flexible data retrieval.

What are Dynamic Queries?

Dynamic queries enable clients to specify complex filtering, sorting, grouping, and aggregation operations at runtime without requiring server-side code changes for each variation.

Think of it as:

  • OData-style querying without the overhead
  • GraphQL-like flexibility for specific operations
  • SQL-like capabilities via HTTP/gRPC

Characteristics:

  • Client-driven - Clients specify filters, sorts, groups, aggregates
  • Server-controlled - Server provides base queryable and security filters
  • Type-safe - Strongly-typed source and destination types
  • Flexible - No server code changes for new filter combinations
  • Secure - Built-in security filtering and tenant isolation
  • Performant - Translates to efficient SQL queries

Quick Example

Define Dynamic Query

public record ProductDynamicQuery : IDynamicQuery<Product, ProductDto>
{
    // Filters (AND/OR conditions)
    public List<IFilter>? Filters { get; set; }

    // Sorts (multiple sort fields)
    public List<ISort>? Sorts { get; set; }

    // Groups (GROUP BY fields)
    public List<IGroup>? Groups { get; set; }

    // Aggregates (SUM, AVG, COUNT, etc.)
    public List<IAggregate>? Aggregates { get; set; }

    // Paging
    public int? Page { get; set; }
    public int? PageSize { get; set; }
}

Provide Queryable Data Source

public class ProductQueryableProvider : IQueryableProvider<Product>
{
    private readonly ApplicationDbContext _context;

    public ProductQueryableProvider(ApplicationDbContext context)
    {
        _context = context;
    }

    public IQueryable<Product> GetQueryable()
    {
        return _context.Products.AsNoTracking();
    }
}

Register Dynamic Query

builder.Services.AddDynamicQuery<Product, ProductDto>()
    .AddDynamicQueryWithProvider<Product, ProductQueryableProvider>();

// Map endpoints
app.MapSvrntyDynamicQueries();

Execute Dynamic Query

HTTP Request:

curl -X POST http://localhost:5000/api/query/productDynamicQuery \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      { "path": "category", "operator": "Equal", "value": "Electronics" },
      { "path": "price", "operator": "LessThanOrEqual", "value": 1000 }
    ],
    "sorts": [
      { "path": "price", "descending": false }
    ],
    "page": 1,
    "pageSize": 20
  }'

Response:

{
  "data": [
    { "id": 1, "name": "Laptop", "category": "Electronics", "price": 899.99 },
    { "id": 2, "name": "Mouse", "category": "Electronics", "price": 29.99 }
  ],
  "totalCount": 25,
  "page": 1,
  "pageSize": 20
}

How It Works

┌──────────────┐
│HTTP Request  │
│with filters, │
│sorts, etc.   │
└──────┬───────┘
       │
       ▼
┌──────────────────────────┐
│ DynamicQueryHandler      │
│ 1. Get base IQueryable   │
│ 2. Apply security filters│
│ 3. Build filter criteria │
│ 4. Apply sorts/groups    │
│ 5. Execute query         │
│ 6. Return results        │
└──────────────────────────┘
       │
       ▼
┌──────────────────────────┐
│ IQueryExecutionResult    │
│ - Data                   │
│ - TotalCount             │
│ - Aggregates             │
│ - GroupedData            │
└──────────────────────────┘

Filter Operators

Operator Description Example
Equal Exact match price == 100
NotEqual Not equal status != "Inactive"
GreaterThan Greater than price > 100
GreaterThanOrEqual Greater or equal price >= 100
LessThan Less than price < 100
LessThanOrEqual Less or equal price <= 100
Contains String contains name.Contains("Laptop")
StartsWith String starts with name.StartsWith("Pro")
EndsWith String ends with name.EndsWith("Plus")
In Value in list category IN ["Electronics", "Books"]
NotIn Value not in list category NOT IN ["Archived"]

Sort Operations

{
  "sorts": [
    { "path": "price", "descending": false },
    { "path": "name", "descending": false }
  ]
}

Group Operations

{
  "groups": [
    { "path": "category" }
  ],
  "aggregates": [
    { "path": "price", "type": "Average" }
  ]
}

Aggregate Functions

Function Description Example
Count Count of items COUNT(*)
Sum Sum of values SUM(price)
Average Average value AVG(price)
Min Minimum value MIN(price)
Max Maximum value MAX(price)
First First value FIRST(name)
Last Last value LAST(name)

Documentation

Getting Started

First dynamic query:

  • Basic setup
  • Simple filtering
  • First query example

Filters and Sorts

Filtering and sorting:

  • Filter operators
  • Combining filters (AND/OR)
  • Multiple sort fields
  • Pagination

Groups and Aggregates

Grouping and aggregation:

  • GROUP BY operations
  • Aggregate functions
  • Grouped results
  • Multi-level grouping

Queryable Providers

Data source providers:

  • IQueryableProvider implementation
  • EF Core integration
  • Multiple data sources
  • Caching strategies

Alter Queryable Services

Security and filtering:

  • IAlterQueryableService
  • Tenant isolation
  • Security filters
  • User-specific filtering

Interceptors

Advanced customization:

  • IDynamicQueryInterceptorProvider
  • Custom filter operators
  • Query transformation
  • Logging and monitoring

Use Cases

Product Catalog

{
  "filters": [
    { "path": "category", "operator": "Equal", "value": "Electronics" },
    { "path": "inStock", "operator": "Equal", "value": true },
    { "path": "price", "operator": "LessThanOrEqual", "value": 1000 }
  ],
  "sorts": [
    { "path": "price", "descending": false }
  ],
  "page": 1,
  "pageSize": 20
}

Order History

{
  "filters": [
    { "path": "customerId", "operator": "Equal", "value": 123 },
    { "path": "orderDate", "operator": "GreaterThanOrEqual", "value": "2024-01-01" }
  ],
  "sorts": [
    { "path": "orderDate", "descending": true }
  ]
}

Sales Analytics

{
  "groups": [
    { "path": "category" }
  ],
  "aggregates": [
    { "path": "totalAmount", "type": "Sum" },
    { "path": "orderId", "type": "Count" }
  ]
}

Security Considerations

Tenant Isolation

public class TenantFilterService : IAlterQueryableService<Product, ProductDto>
{
    private readonly ITenantContext _tenantContext;

    public IQueryable<Product> AlterQueryable(IQueryable<Product> queryable)
    {
        var tenantId = _tenantContext.TenantId;
        return queryable.Where(p => p.TenantId == tenantId);
    }
}

// Registration
builder.Services.AddAlterQueryable<Product, ProductDto, TenantFilterService>();

User-Specific Filtering

public class UserFilterService : IAlterQueryableService<Order, OrderDto>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public IQueryable<Order> AlterQueryable(IQueryable<Order> queryable)
    {
        var userId = _httpContextAccessor.HttpContext.User.FindFirst("sub")?.Value;

        if (string.IsNullOrEmpty(userId))
            return queryable.Where(o => false); // No results

        return queryable.Where(o => o.UserId == userId);
    }
}

Performance Optimization

Use Projections

Dynamic queries automatically project to DTO types, fetching only needed columns:

// Source entity (in database)
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }  // Not in DTO
    public decimal Price { get; set; }
    public byte[] Image { get; set; }        // Not in DTO
}

// DTO (returned to client)
public record ProductDto
{
    public int Id { get; init; }
    public string Name { get; init; }
    public decimal Price { get; init; }
}

// Query only fetches Id, Name, Price (not Description or Image)

Add Indexes

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>(entity =>
    {
        entity.HasIndex(e => e.Category);
        entity.HasIndex(e => e.Price);
        entity.HasIndex(e => new { e.Category, e.Price });
    });
}

Best Practices

DO

  • Use DTOs for dynamic query results
  • Apply security filters via IAlterQueryableService
  • Use projections to fetch only needed data
  • Add database indexes for filtered/sorted fields
  • Implement pagination for large result sets
  • Validate filter inputs
  • Limit maximum page size

DON'T

  • Don't expose domain entities directly
  • Don't skip security filtering
  • Don't allow unbounded result sets
  • Don't fetch unnecessary columns
  • Don't perform client-side filtering
  • Don't skip validation

What's Next?

See Also