dotnet-cqrs/docs/core-features/dynamic-queries/README.md

411 lines
10 KiB
Markdown

# 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
```csharp
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
```csharp
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
```csharp
builder.Services.AddDynamicQuery<Product, ProductDto>()
.AddDynamicQueryWithProvider<Product, ProductQueryableProvider>();
// Map endpoints
app.MapSvrntyDynamicQueries();
```
### Execute Dynamic Query
**HTTP Request:**
```bash
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:**
```json
{
"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
```json
{
"sorts": [
{ "path": "price", "descending": false },
{ "path": "name", "descending": false }
]
}
```
## Group Operations
```json
{
"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](getting-started.md)
First dynamic query:
- Basic setup
- Simple filtering
- First query example
### [Filters and Sorts](filters-and-sorts.md)
Filtering and sorting:
- Filter operators
- Combining filters (AND/OR)
- Multiple sort fields
- Pagination
### [Groups and Aggregates](groups-and-aggregates.md)
Grouping and aggregation:
- GROUP BY operations
- Aggregate functions
- Grouped results
- Multi-level grouping
### [Queryable Providers](queryable-providers.md)
Data source providers:
- IQueryableProvider implementation
- EF Core integration
- Multiple data sources
- Caching strategies
### [Alter Queryable Services](alter-queryable-services.md)
Security and filtering:
- IAlterQueryableService
- Tenant isolation
- Security filters
- User-specific filtering
### [Interceptors](interceptors.md)
Advanced customization:
- IDynamicQueryInterceptorProvider
- Custom filter operators
- Query transformation
- Logging and monitoring
## Use Cases
### Product Catalog
```json
{
"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
```json
{
"filters": [
{ "path": "customerId", "operator": "Equal", "value": 123 },
{ "path": "orderDate", "operator": "GreaterThanOrEqual", "value": "2024-01-01" }
],
"sorts": [
{ "path": "orderDate", "descending": true }
]
}
```
### Sales Analytics
```json
{
"groups": [
{ "path": "category" }
],
"aggregates": [
{ "path": "totalAmount", "type": "Sum" },
{ "path": "orderId", "type": "Count" }
]
}
```
## Security Considerations
### Tenant Isolation
```csharp
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
```csharp
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:
```csharp
// 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
```csharp
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](getting-started.md)** - Create your first dynamic query
- **[Filters and Sorts](filters-and-sorts.md)** - Master filtering and sorting
- **[Groups and Aggregates](groups-and-aggregates.md)** - Learn grouping and aggregation
- **[Queryable Providers](queryable-providers.md)** - Implement data source providers
- **[Alter Queryable Services](alter-queryable-services.md)** - Add security filters
- **[Interceptors](interceptors.md)** - Advanced customization
## See Also
- [Basic Queries](../queries/README.md)
- [Query Authorization](../queries/query-authorization.md)
- [Best Practices: Query Design](../../best-practices/query-design.md)
- [PoweredSoft.DynamicQuery Documentation](https://github.com/PoweredSoft/DynamicQuery)