Server-Side Query Filters

Server-side query filters restrict which rows a client can ever access — at the SQL query level. They are applied to every GET request (list, by-id, count, pagedresult, custom routes) before any client-supplied parameters. A record that fails the filter behaves exactly as if it does not exist.

Filter types

TypeMethodWhen evaluated
Static .WithQueryFilter(predicate) Compiled once at startup — constant values only
Scoped .WithScopedQueryFilter(factory) Resolved on every request — can read from HTTP context, JWT, headers

Multiple filters of either type can be chained. All are composed with AND semantics.

Static Query Filters

.WithQueryFilter(predicate) registers a permanent server-side row filter. The predicate must use only EF-translatable operations and cannot reference request-scoped data.

Basic usage — soft-delete / visibility
// Only expose available products — unavailable ones are completely invisible
options.Entity<Product>("products")
    .WithQueryFilter(p => p.IsAvailable);

The effect on all operations for this entity:

RequestBehaviour
GET /api/productsReturns only IsAvailable == true
GET /api/products/10 (IsAvailable = false)404 — record is hidden, not revealed
GET /api/products/countCounts only available products
GET /api/products/pagedresulttotal and items reflect only available products
PUT /api/products/10 (IsAvailable = false)404 — query filter applied before update
PATCH /api/products/10 (IsAvailable = false)404

Chaining multiple static filters

Each .WithQueryFilter() call adds another predicate. All predicates are ANDed — a row must satisfy all of them.

AND chain
// A product must be available AND have stock > 0
options.Entity<Product>("products")
    .WithQueryFilter(p => p.IsAvailable)
    .WithQueryFilter(p => p.Stock > 0);

Compound conditions in a single predicate

Complex static predicates
// Hide cancelled and pending orders
options.Entity<Order>("orders")
    .WithQueryFilter(o => o.Status != OrderStatus.Cancelled
                       && o.Status != OrderStatus.Pending);

// Expose items from tenant 1 OR tenant 2
options.Entity<Report>("reports")
    .WithQueryFilter(r => r.TenantId == 1 || r.TenantId == 2);

Static filter limitations

Scoped Query Filters (per-request)

.WithScopedQueryFilter(factory) registers a dynamic filter resolved on every request via IServiceProvider. Use this when the filter depends on runtime data — the current user ID, tenant ID from a JWT token, or any HTTP context value.

💡
Requires services.AddHttpContextAccessor() if reading from IHttpContextAccessor.
Per-user order isolation
options.Entity<Order>("orders")
    .WithScopedQueryFilter(sp =>
    {
        var http   = sp.GetRequiredService<IHttpContextAccessor>();
        var userId = http.HttpContext?.User.FindFirst("sub")?.Value ?? "";
        return o => o.UserId == userId;
    });
Multi-tenant product isolation
options.Entity<Product>("products")
    .WithScopedQueryFilter(sp =>
    {
        var http     = sp.GetRequiredService<IHttpContextAccessor>();
        var tenantId = int.Parse(http.HttpContext?.User.FindFirst("tenantId")?.Value ?? "0");
        return p => p.TenantId == tenantId;
    });

Combining static and scoped filters

Both types can be freely mixed on the same entity — all are composed with AND:

Static + scoped combination
options.Entity<Product>("products")
    .WithQueryFilter(p => p.IsActive)             // static — always applied
    .WithScopedQueryFilter(sp =>                   // dynamic — per-request
    {
        var http     = sp.GetRequiredService<IHttpContextAccessor>();
        var tenantId = int.Parse(http.HttpContext!.User.FindFirst("tenantId")!.Value);
        return p => p.TenantId == tenantId;
    });

Combining with client-supplied filters

Server-side query filters and client-supplied filter[...] parameters are all composed with AND. The server filter is always applied first:

HTTP
# Server filter: IsAvailable == true
# Client filter: CategoryId == 1
# Result:        available products in category 1
GET /api/products?filter[categoryId]=1
💡
Unit testing: If the controller is created without a service provider (e.g. directly in unit tests), scoped filters are silently skipped — all records are returned as if no scoped filter existed.