501 lines
9.9 KiB
Markdown
501 lines
9.9 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
dotnet add package Svrnty.CQRS.DynamicQuery
|
|
dotnet add package Svrnty.CQRS.DynamicQuery.MinimalApi
|
|
```
|
|
|
|
### Package References
|
|
|
|
```xml
|
|
<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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```bash
|
|
curl -X POST http://localhost:5000/api/query/productDynamicQuery \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"filters": [
|
|
{
|
|
"path": "category",
|
|
"operator": "Equal",
|
|
"value": "Electronics"
|
|
}
|
|
]
|
|
}'
|
|
```
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```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
|
|
}
|
|
]
|
|
}'
|
|
```
|
|
|
|
### Multiple Filters with Pagination
|
|
|
|
```bash
|
|
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
|
|
|
|
```json
|
|
{
|
|
"filters": [
|
|
{
|
|
"path": "name",
|
|
"operator": "Contains",
|
|
"value": "Laptop"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Scenario 2: Price Range
|
|
|
|
```json
|
|
{
|
|
"filters": [
|
|
{
|
|
"path": "price",
|
|
"operator": "GreaterThanOrEqual",
|
|
"value": 100
|
|
},
|
|
{
|
|
"path": "price",
|
|
"operator": "LessThanOrEqual",
|
|
"value": 500
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Scenario 3: Multiple Categories
|
|
|
|
```json
|
|
{
|
|
"filters": [
|
|
{
|
|
"path": "category",
|
|
"operator": "In",
|
|
"value": ["Electronics", "Books", "Toys"]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Scenario 4: Recent Products
|
|
|
|
```json
|
|
{
|
|
"filters": [
|
|
{
|
|
"path": "createdAt",
|
|
"operator": "GreaterThanOrEqual",
|
|
"value": "2024-01-01T00:00:00Z"
|
|
}
|
|
],
|
|
"sorts": [
|
|
{
|
|
"path": "createdAt",
|
|
"descending": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Adding Pagination
|
|
|
|
### Built-in Pagination
|
|
|
|
```csharp
|
|
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
|
|
|
|
```json
|
|
{
|
|
"filters": [
|
|
{
|
|
"path": "category",
|
|
"operator": "Equal",
|
|
"value": "Electronics"
|
|
}
|
|
],
|
|
"page": 2,
|
|
"pageSize": 10
|
|
}
|
|
```
|
|
|
|
### Response with Pagination
|
|
|
|
```json
|
|
{
|
|
"data": [ /* 10 products */ ],
|
|
"totalCount": 45,
|
|
"page": 2,
|
|
"pageSize": 10
|
|
}
|
|
```
|
|
|
|
## Client-Side Integration
|
|
|
|
### JavaScript/TypeScript
|
|
|
|
```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
|
|
|
|
```csharp
|
|
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:
|
|
|
|
1. **[Filters and Sorts](filters-and-sorts.md)** - Learn all filter operators and advanced sorting
|
|
2. **[Groups and Aggregates](groups-and-aggregates.md)** - Add grouping and aggregation
|
|
3. **[Queryable Providers](queryable-providers.md)** - Advanced queryable provider patterns
|
|
4. **[Alter Queryable Services](alter-queryable-services.md)** - Add security filters and tenant isolation
|
|
5. **[Interceptors](interceptors.md)** - Customize query behavior
|
|
|
|
## Troubleshooting
|
|
|
|
### No Results Returned
|
|
|
|
**Issue:** Query returns empty array even though data exists.
|
|
|
|
**Solution:** Check your queryable provider is returning data:
|
|
|
|
```csharp
|
|
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):
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```csharp
|
|
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);
|
|
});
|
|
}
|
|
```
|
|
|
|
## See Also
|
|
|
|
- [Dynamic Queries Overview](README.md)
|
|
- [Filters and Sorts](filters-and-sorts.md)
|
|
- [Basic Queries](../queries/basic-queries.md)
|
|
- [Query Registration](../queries/query-registration.md)
|