dotnet-cqrs/.claude/rules/dynamic-query.md
Mathias Beaulieu-Duncan a4525bad6a Add Claude Code harness: rules, skills, hooks, and editorconfig
- 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>
2026-03-12 03:30:27 -04:00

2.9 KiB

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

// 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:

builder.Services.AddDynamicQueryWithProvider<Order, OrderQueryableProvider>();

This automatically creates endpoints with full filtering, sorting, and pagination support.

Required Dependencies

These must be registered before AddSvrntyCqrs:

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.

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:

builder.Services.AddAlterQueryable<Order, Order, OrderTenantFilter>();

Interceptors

Up to 5 interceptors per query type. These modify the PoweredSoft DynamicQuery criteria at query build time.

builder.Services.AddDynamicQueryInterceptor<Order, Order, OrderInterceptor>();

Source to Destination Mapping

When the entity type differs from the DTO:

// Provider returns the source entity queryable
public class OrderQueryableProvider : IQueryableProviderOverride<Order> { ... }

// Registration maps source -> destination
builder.Services.AddDynamicQueryWithProvider<Order, OrderDto, OrderQueryableProvider>();

Key Interfaces

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)