dotnet-cqrs/docs/core-features/dynamic-queries/getting-started.md

9.9 KiB

Getting Started with Dynamic Queries

Create your first dynamic query with filtering and sorting.

Prerequisites

  • Svrnty.CQRS.DynamicQuery package installed
  • Basic understanding of CQRS queries
  • Entity Framework Core (or other IQueryable source)

Installation

Install Packages

dotnet add package Svrnty.CQRS.DynamicQuery
dotnet add package Svrnty.CQRS.DynamicQuery.MinimalApi

Package References

<ItemGroup>
  <PackageReference Include="Svrnty.CQRS.DynamicQuery" Version="1.0.0" />
  <PackageReference Include="Svrnty.CQRS.DynamicQuery.MinimalApi" Version="1.0.0" />
</ItemGroup>

Step 1: Define Your Entity

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Category { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
}

Step 2: Create DTO

public record ProductDto
{
    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string Category { get; init; } = string.Empty;
    public decimal Price { get; init; }
    public int Stock { get; init; }
}

Step 3: Define Dynamic Query

using Svrnty.CQRS.DynamicQuery.Abstractions;

public record ProductDynamicQuery : IDynamicQuery<Product, ProductDto>
{
    public List<IFilter>? Filters { get; set; }
    public List<ISort>? Sorts { get; set; }
    public List<IGroup>? Groups { get; set; }
    public List<IAggregate>? Aggregates { get; set; }
}

That's it! The IDynamicQuery<TSource, TDestination> interface defines the structure. The framework provides the implementation.

Step 4: Implement Queryable Provider

using Svrnty.CQRS.DynamicQuery.Abstractions;

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

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

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

Step 5: Register Services

var builder = WebApplication.CreateBuilder(args);

// Register CQRS discovery
builder.Services.AddSvrntyCQRS();
builder.Services.AddDefaultQueryDiscovery();

// Register dynamic query
builder.Services.AddDynamicQuery<Product, ProductDto>()
    .AddDynamicQueryWithProvider<Product, ProductQueryableProvider>();

// Add HTTP endpoints
var app = builder.Build();

// Map dynamic query endpoints
app.MapSvrntyDynamicQueries();

app.Run();

This creates endpoints:

  • GET /api/query/productDynamicQuery (with query string parameters)
  • POST /api/query/productDynamicQuery (with JSON body)

Step 6: Test Your Dynamic Query

Simple Filter Query

curl -X POST http://localhost:5000/api/query/productDynamicQuery \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      {
        "path": "category",
        "operator": "Equal",
        "value": "Electronics"
      }
    ]
  }'

Response:

{
  "data": [
    {
      "id": 1,
      "name": "Laptop",
      "category": "Electronics",
      "price": 999.99,
      "stock": 50
    },
    {
      "id": 2,
      "name": "Mouse",
      "category": "Electronics",
      "price": 29.99,
      "stock": 200
    }
  ],
  "totalCount": 2
}

Filter with Sorting

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
      }
    ]
  }'

Multiple Filters with Pagination

curl -X POST http://localhost:5000/api/query/productDynamicQuery \
  -H "Content-Type: application/json" \
  -d '{
    "filters": [
      {
        "path": "isActive",
        "operator": "Equal",
        "value": true
      },
      {
        "path": "stock",
        "operator": "GreaterThan",
        "value": 0
      }
    ],
    "sorts": [
      {
        "path": "name",
        "descending": false
      }
    ],
    "page": 1,
    "pageSize": 20
  }'

Common Scenarios

Scenario 1: Search by Name

{
  "filters": [
    {
      "path": "name",
      "operator": "Contains",
      "value": "Laptop"
    }
  ]
}

Scenario 2: Price Range

{
  "filters": [
    {
      "path": "price",
      "operator": "GreaterThanOrEqual",
      "value": 100
    },
    {
      "path": "price",
      "operator": "LessThanOrEqual",
      "value": 500
    }
  ]
}

Scenario 3: Multiple Categories

{
  "filters": [
    {
      "path": "category",
      "operator": "In",
      "value": ["Electronics", "Books", "Toys"]
    }
  ]
}

Scenario 4: Recent Products

{
  "filters": [
    {
      "path": "createdAt",
      "operator": "GreaterThanOrEqual",
      "value": "2024-01-01T00:00:00Z"
    }
  ],
  "sorts": [
    {
      "path": "createdAt",
      "descending": true
    }
  ]
}

Adding Pagination

Built-in Pagination

public record ProductDynamicQuery : IDynamicQuery<Product, ProductDto>
{
    public List<IFilter>? Filters { get; set; }
    public List<ISort>? Sorts { get; set; }
    public List<IGroup>? Groups { get; set; }
    public List<IAggregate>? Aggregates { get; set; }

    // Pagination properties
    public int? Page { get; set; }
    public int? PageSize { get; set; }
}

Request with Pagination

{
  "filters": [
    {
      "path": "category",
      "operator": "Equal",
      "value": "Electronics"
    }
  ],
  "page": 2,
  "pageSize": 10
}

Response with Pagination

{
  "data": [ /* 10 products */ ],
  "totalCount": 45,
  "page": 2,
  "pageSize": 10
}

Client-Side Integration

JavaScript/TypeScript

interface DynamicQueryRequest {
  filters?: Array<{
    path: string;
    operator: string;
    value: any;
  }>;
  sorts?: Array<{
    path: string;
    descending: boolean;
  }>;
  page?: number;
  pageSize?: number;
}

interface DynamicQueryResponse<T> {
  data: T[];
  totalCount: number;
  page?: number;
  pageSize?: number;
}

async function searchProducts(
  category: string,
  maxPrice: number
): Promise<DynamicQueryResponse<ProductDto>> {
  const request: DynamicQueryRequest = {
    filters: [
      { path: "category", operator: "Equal", value: category },
      { path: "price", operator: "LessThanOrEqual", value: maxPrice }
    ],
    sorts: [
      { path: "price", descending: false }
    ],
    page: 1,
    pageSize: 20
  };

  const response = await fetch('/api/query/productDynamicQuery', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(request)
  });

  return await response.json();
}

C# HttpClient

public class ProductApiClient
{
    private readonly HttpClient _httpClient;

    public async Task<DynamicQueryResponse<ProductDto>> SearchProductsAsync(
        string category,
        decimal maxPrice)
    {
        var request = new
        {
            filters = new[]
            {
                new { path = "category", @operator = "Equal", value = category },
                new { path = "price", @operator = "LessThanOrEqual", value = maxPrice }
            },
            sorts = new[]
            {
                new { path = "price", descending = false }
            },
            page = 1,
            pageSize = 20
        };

        var response = await _httpClient.PostAsJsonAsync(
            "/api/query/productDynamicQuery",
            request);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadFromJsonAsync<DynamicQueryResponse<ProductDto>>();
    }
}

public class DynamicQueryResponse<T>
{
    public List<T> Data { get; set; } = new();
    public int TotalCount { get; set; }
    public int? Page { get; set; }
    public int? PageSize { get; set; }
}

Next Steps

Now that you have a basic dynamic query working:

  1. Filters and Sorts - Learn all filter operators and advanced sorting
  2. Groups and Aggregates - Add grouping and aggregation
  3. Queryable Providers - Advanced queryable provider patterns
  4. Alter Queryable Services - Add security filters and tenant isolation
  5. Interceptors - Customize query behavior

Troubleshooting

No Results Returned

Issue: Query returns empty array even though data exists.

Solution: Check your queryable provider is returning data:

public IQueryable<Product> GetQueryable()
{
    var query = _context.Products.AsNoTracking();

    // Debug: Log count
    var count = query.Count();
    _logger.LogInformation("Queryable returned {Count} products", count);

    return query;
}

Filter Not Working

Issue: Filter doesn't seem to apply.

Solution: Ensure property names match exactly (case-insensitive):

{
  "filters": [
    { "path": "category", "operator": "Equal", "value": "Electronics" }
    // ✅ "category" matches Product.Category
    // ❌ "Category" - works (case-insensitive)
    // ❌ "cat" - won't work
  ]
}

Performance Issues

Issue: Query is slow.

Solution: Add database indexes:

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

See Also