# 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 { // Filters (AND/OR conditions) public List? Filters { get; set; } // Sorts (multiple sort fields) public List? Sorts { get; set; } // Groups (GROUP BY fields) public List? Groups { get; set; } // Aggregates (SUM, AVG, COUNT, etc.) public List? Aggregates { get; set; } // Paging public int? Page { get; set; } public int? PageSize { get; set; } } ``` ### Provide Queryable Data Source ```csharp public class ProductQueryableProvider : IQueryableProvider { private readonly ApplicationDbContext _context; public ProductQueryableProvider(ApplicationDbContext context) { _context = context; } public IQueryable GetQueryable() { return _context.Products.AsNoTracking(); } } ``` ### Register Dynamic Query ```csharp builder.Services.AddDynamicQuery() .AddDynamicQueryWithProvider(); // 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 { private readonly ITenantContext _tenantContext; public IQueryable AlterQueryable(IQueryable queryable) { var tenantId = _tenantContext.TenantId; return queryable.Where(p => p.TenantId == tenantId); } } // Registration builder.Services.AddAlterQueryable(); ``` ### User-Specific Filtering ```csharp public class UserFilterService : IAlterQueryableService { private readonly IHttpContextAccessor _httpContextAccessor; public IQueryable AlterQueryable(IQueryable 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(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)