- Add path-specific rules for commands/queries, dynamic queries, validation, and gRPC - Add /add-command, /add-query, /add-dynamic-query scaffolding skills - Add project settings with post-edit formatting, proto validation, and build-gate hooks - Add .editorconfig codifying existing code style conventions - Trim CLAUDE.md from 414 to 130 lines (domain details moved to rules) - Add .harness-version tracking for the shared claude-harness repo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
2.9 KiB
Markdown
98 lines
2.9 KiB
Markdown
---
|
|
paths:
|
|
- "*QueryableProvider*.cs"
|
|
- "*AlterQueryable*.cs"
|
|
- "*DynamicQuery*.cs"
|
|
- "*Interceptor*.cs"
|
|
---
|
|
|
|
# Dynamic Queries — The Default Query Pattern
|
|
|
|
**99% of queries should be dynamic queries.** They provide pagination, filtering, sorting, grouping, and aggregation for free.
|
|
|
|
## The Standard Pattern: IQueryableProviderOverride
|
|
|
|
```csharp
|
|
// File: Features/Orders/OrderQueryableProvider.cs
|
|
public class OrderQueryableProvider : IQueryableProviderOverride<Order>
|
|
{
|
|
private readonly AppDbContext _db;
|
|
public OrderQueryableProvider(AppDbContext db) => _db = db;
|
|
|
|
public Task<IQueryable<Order>> GetQueryableAsync(object query, CancellationToken ct = default)
|
|
=> Task.FromResult(_db.Orders.AsQueryable());
|
|
}
|
|
```
|
|
|
|
Registration — one line:
|
|
```csharp
|
|
builder.Services.AddDynamicQueryWithProvider<Order, OrderQueryableProvider>();
|
|
```
|
|
|
|
This automatically creates endpoints with full filtering, sorting, and pagination support.
|
|
|
|
## Required Dependencies
|
|
|
|
These must be registered before `AddSvrntyCqrs`:
|
|
```csharp
|
|
builder.Services.AddTransient<IAsyncQueryableService, SimpleAsyncQueryableService>();
|
|
builder.Services.AddTransient<IQueryHandlerAsync, QueryHandlerAsync>();
|
|
```
|
|
|
|
## Alter Queryable Services (Security Filters, Tenant Isolation)
|
|
|
|
Use `IAlterQueryableService` to modify the queryable before execution — for security filters, tenant isolation, default ordering, etc.
|
|
|
|
```csharp
|
|
public class OrderTenantFilter : IAlterQueryableService<Order, Order>
|
|
{
|
|
private readonly ITenantContext _tenant;
|
|
public OrderTenantFilter(ITenantContext tenant) => _tenant = tenant;
|
|
|
|
public Task<IQueryable<Order>> AlterQueryableAsync(
|
|
IQueryable<Order> query,
|
|
IDynamicQuery dynamicQuery,
|
|
CancellationToken ct = default)
|
|
{
|
|
return Task.FromResult(query.Where(o => o.TenantId == _tenant.Id));
|
|
}
|
|
}
|
|
```
|
|
|
|
Registration:
|
|
```csharp
|
|
builder.Services.AddAlterQueryable<Order, Order, OrderTenantFilter>();
|
|
```
|
|
|
|
## Interceptors
|
|
|
|
Up to 5 interceptors per query type. These modify the PoweredSoft DynamicQuery criteria at query build time.
|
|
|
|
```csharp
|
|
builder.Services.AddDynamicQueryInterceptor<Order, Order, OrderInterceptor>();
|
|
```
|
|
|
|
## Source to Destination Mapping
|
|
|
|
When the entity type differs from the DTO:
|
|
```csharp
|
|
// Provider returns the source entity queryable
|
|
public class OrderQueryableProvider : IQueryableProviderOverride<Order> { ... }
|
|
|
|
// Registration maps source -> destination
|
|
builder.Services.AddDynamicQueryWithProvider<Order, OrderDto, OrderQueryableProvider>();
|
|
```
|
|
|
|
## Key Interfaces
|
|
|
|
```csharp
|
|
IQueryableProvider<TSource>
|
|
Task<IQueryable<TSource>> GetQueryableAsync(object query, CancellationToken ct)
|
|
|
|
IQueryableProviderOverride<TSource> : IQueryableProvider<TSource>
|
|
// Marker interface — same method, signals override registration
|
|
|
|
IAlterQueryableService<TSource, TDestination>
|
|
Task<IQueryable<TSource>> AlterQueryableAsync(IQueryable<TSource> query, IDynamicQuery dynamicQuery, CancellationToken ct)
|
|
```
|