List Filtering
Filter EF Core queries by authorization at query time.
BuildFilterAsync returns a filter for Where(...). Lists only rows the subject may see. Use this on list endpoints.
This is a read-side helper. Lifecycle-managed domain rows normally implement ISqlOSResourceEntity; the filter only needs the inherited ResourceId contract to join application rows to accessible FGA resources.
The filter authorizes a subject ID; it does not authenticate the request. Validate a bearer token for the API's exact audience before the endpoint runs, then use the validated token's user ID as the FGA subject.
var filter = await authService
.BuildFilterAsync<Chain>(subjectId, "CHAIN_VIEW");
var chains = await dbContext.Chains
.Where(filter)
.OrderBy(c => c.Name)
.ToListAsync();The subject only sees chains they have access to. The filter translates to a SQL query -- no in-memory filtering.
The subject's principal set (the subject plus its active group memberships) is resolved when the returned task is awaited. Build the filter once per request and compose it into that request's queries. Do not cache the filter in statics or reuse it across requests -- grant and membership changes are not reflected in an already-built filter.
SqlOS does authorization filters only. Pagination, sorting, search, and projection are ordinary EF Core on the same query:
var canView = await authService
.BuildFilterAsync<InventoryItem>(subjectId, "INVENTORY_VIEW");
var items = await db.InventoryItems
.Where(canView)
.Where(i => i.LocationId == locationId)
.Select(i => new InventoryItemDto
{
Id = i.Id,
Name = i.Name,
LocationName = i.Location!.Name
})
.OrderBy(d => d.Name).ThenBy(d => d.Id)
.Skip(page * pageSize).Take(pageSize)
.ToListAsync();Everything composes into one SQL statement: the authorization TVF join, your business predicates, the projection, the ordering, and the paging.
For cursor/keyset pagination over authorized lists, see Paginating authorized lists.
From the retail example app:
using SqlOS.AuthServer.Extensions;
using SqlOS.Extensions;
var api = app.MapGroup("/api");
api.MapGet("/chains", async (
ExampleAppDbContext context,
ISqlOSFgaAuthService authService,
HttpContext http,
string? search, int? page, int? pageSize) =>
{
var subjectId = http.GetSqlOSValidatedToken()?.UserId;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
var canView = await authService
.BuildFilterAsync<Chain>(subjectId, RetailPermissionKeys.ChainView);
var query = context.Chains.Where(canView);
if (!string.IsNullOrWhiteSpace(search))
query = query.Where(c => c.Name.Contains(search));
var chains = await query
.Select(c => new ChainListDto
{
Id = c.Id,
Name = c.Name,
LocationCount = c.Locations.Count
})
.OrderBy(d => d.Name).ThenBy(d => d.Id)
.Skip((page ?? 0) * (pageSize ?? 20)).Take(pageSize ?? 20)
.ToListAsync();
return Results.Ok(chains);
});For single-resource access, use AuthorizedDetailAsync. For mutations, use CheckAccessAsync.