Detail Endpoints
Fetch and authorize a single entity in one call.
AuthorizedDetailAsync fetches a single entity, checks authorization, and returns the appropriate HTTP result. It handles 404, 403, and 200 in one call.
Usage
app.MapGet("/api/chains/{id}", async (
string id,
ExampleAppDbContext context,
ISqlOSFgaAuthService authService,
HttpContext http) =>
{
var subjectId = http.GetSubjectId();
return await authService.AuthorizedDetailAsync(
context.Chains.Include(c => c.Locations),
c => c.Id == id,
subjectId, "CHAIN_VIEW",
chain => new ChainDetailDto
{
Id = chain.Id,
ResourceId = chain.ResourceId,
Name = chain.Name,
Description = chain.Description,
LocationCount = chain.Locations.Count,
CreatedAt = chain.CreatedAt
});
});
What it returns
| Condition | HTTP result |
|---|---|
| Entity not found | 404 Not Found |
| Subject lacks permission | 403 { error: "Permission denied" } |
| Access granted | 200 with mapped DTO |
Method signature
Task<IResult> AuthorizedDetailAsync<TEntity, TDto>(
IQueryable<TEntity> query,
Expression<Func<TEntity, bool>> predicate,
string subjectId,
string permissionKey,
Func<TEntity, TDto> selector) where TEntity : class, IHasResourceId;
When to use
Use AuthorizedDetailAsync for any GET /resource/{id} endpoint. It replaces the common pattern of:
var entity = await query.FirstOrDefaultAsync(e => e.Id == id);
if (entity == null) return Results.NotFound();
var access = await authService.CheckAccessAsync(subjectId, "CHAIN_VIEW", entity.ResourceId);
if (!access.Allowed) return Results.Json(new { error = "Permission denied" }, statusCode: 403);
return Results.Ok(MapToDto(entity));