411 lines
10 KiB
Markdown
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)
|