| .. | ||
| alter-queryable-services.md | ||
| filters-and-sorts.md | ||
| getting-started.md | ||
| groups-and-aggregates.md | ||
| interceptors.md | ||
| queryable-providers.md | ||
| README.md | ||
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?
- Getting Started - Create your first dynamic query
- Filters and Sorts - Master filtering and sorting
- Groups and Aggregates - Learn grouping and aggregation
- Queryable Providers - Implement data source providers
- Alter Queryable Services - Add security filters
- Interceptors - Advanced customization