9.9 KiB
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:
- Filters and Sorts - Learn all filter operators and advanced sorting
- Groups and Aggregates - Add grouping and aggregation
- Queryable Providers - Advanced queryable provider patterns
- Alter Queryable Services - Add security filters and tenant isolation
- 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);
});
}