Trellis.Asp — API Reference
Package: Trellis.Asp (bundles the AOT-friendly Trellis.AspSourceGenerator.dll at analyzers/dotnet/cs/ — installing Trellis.Asp attaches the generator automatically — and contains the ASP.NET actor providers formerly published as Trellis.Asp.Authorization).
Namespaces: Trellis.Asp, Trellis.Asp.Authorization, Trellis.Asp.ModelBinding, Trellis.Asp.Routing, Trellis.Asp.Validation
Purpose: ASP.NET Core integration for mapping Trellis Result/Result<T>/WriteOutcome<T>/Page<T> values to HTTP responses, evaluating HTTP preconditions / ranges / Prefer preferences, hydrating actors from JWT claims, validating scalar value objects in MVC and Minimal APIs, and emitting AOT-friendly JsonConverters for Trellis scalar values.
The single supported response verb is result.ToHttpResponse(...). It returns Microsoft.AspNetCore.Http.IResult and works in both Minimal API and MVC hosts (.NET 7+ executes IResult natively in MVC). For typed ActionResult<T> signatures, chain .AsActionResult<T>(). Configure protocol semantics via the fluent HttpResponseOptionsBuilder<T> (WithETag, WithLastModified, Vary, Created/CreatedAtRoute/CreatedAtAction, EvaluatePreconditions, HonorPrefer, WithRange, WithErrorMapping, …).
See also: trellis-api-cookbook.md — recipes using this package.
Use this file when
- You are wiring ASP.NET Core endpoints/controllers that return Trellis
Result,Result<T>,WriteOutcome<T>, orPage<T>. - You need the exact response-mapping verb, status-code behavior, Problem Details mapping, ETag/range/preference handling, actor-provider setup, scalar value-object binding, or route constraints.
- You are implementing API surface polish: failure response metadata, versioned
Locationheaders, or tests provingError.UnprocessableContentmaps to 422.
Patterns Index
| Goal | Canonical API / action | See |
|---|---|---|
| Enable Trellis Result-to-HTTP mapping | Call builder.Services.AddTrellisAsp() or services.AddTrellis(o => o.UseAsp()) in the composition root. Exception middleware is only a 500 fallback; it does not map Result failures. |
ServiceCollectionExtensions, ServiceDefaults |
| Return a Minimal API result | return result.ToHttpResponse(...) |
HttpResponseExtensions |
| Return an MVC typed action result | Convert first, then adapt: return result.ToHttpResponse(...).AsActionResult<T>() or return await result.ToHttpResponseAsync(...).AsActionResultAsync<T>() |
ActionResultAdapterExtensions |
| Configure 201 Created | .ToHttpResponse(o => o.Created(...)), .CreatedAtRoute(...), or .CreatedAtAction(...) |
HttpResponseOptionsBuilder<TDomain> |
Generate versioned Location headers |
Required when query-string API versioning is enabled. Include the API version in CreatedAtRoute route values: ["api-version"] = ApiVersion. Omitting it produces Location headers that 404 on dereference. |
CreatedAtRoute |
| Map failure codes globally | Configure TrellisAspOptions.ErrorStatusCodeMap through AddTrellisAsp(...) |
TrellisAspOptions |
| Override failure mapping for one endpoint | .WithErrorMapping(...) / .WithErrorMapping<TError>(statusCode) |
HttpResponseOptionsBuilder<TDomain> |
| Document endpoint failure codes | Add ASP.NET response metadata for every spec-listed failure status (422, 409, 403, 404, etc.) in addition to happy-path metadata. |
Code examples |
| Add ETag / conditional GET | .WithETag(...), .WithLastModified(...), .EvaluatePreconditions() |
HttpResponseOptionsBuilder<TDomain>, ETagHelper |
Honor Prefer: return=minimal |
.HonorPrefer() on write responses |
HttpResponseOptionsBuilder<TDomain> |
| Return paginated list responses | Result<Page<T>>.ToHttpResponse(nextUrlBuilder, bodySelector, ...) |
PagedResponse<TResponse> |
| Resolve actors from requests | AddClaimsActorProvider, AddEntraActorProvider, or AddDevelopmentActorProvider |
Trellis.Asp.Authorization |
| Bind scalar value objects from routes/query/body | AddTrellisAsp() plus route constraints / validation middleware as needed |
Trellis.Asp.ModelBinding, Trellis.Asp.Validation |
Endpoint checklist for generated APIs
- Composition root calls
AddTrellisAsp()orUseAsp(). - Every endpoint that returns a Trellis
Resultultimately callsToHttpResponse/AsActionResult. - OpenAPI metadata includes the success code and every failure code listed by the product spec.
201 Createdendpoints include a usableLocationheader. Under query-string API versioning, include["api-version"] = ApiVersioninCreatedAtRouteroute values (or use a literalLocationthat already contains?api-version=...). Forgetting this is a silentLocation-404 bug — tests pass and OpenAPI looks fine, but clients following theLocationheader get 404.[Consumes("application/json")]is not safe at the controller level when the controller has trigger-style POSTs without bodies (e.g.,POST /orders/{id}/submission). ASP.NET Core returns415 Unsupported Media Typefor any request without aContent-Typeheader. Apply[Consumes]per-action on body-bearing endpoints only, or scope it to a route convention.- Integration tests include at least one business-validation failure that asserts
422Problem Details; do not rely on exception middleware to prove Result mapping.
Types
Namespace Trellis.Asp
HttpResponseExtensions
Declaration
public static class HttpResponseExtensions
The single Trellis verb for converting Result / Result<T> / Result<WriteOutcome<T>> / Result<Page<T>> to ASP.NET Core HTTP responses.
| Signature | Returns | Description |
|---|---|---|
public static IResult ToHttpResponse(this Error error, Action<HttpResponseOptionsBuilder>? configure = null) |
IResult |
Maps a standalone Error to a Problem Details response (for endpoints that produce a deterministic error). |
public static IResult ToHttpResponse<T>(this Result<T> result, Action<HttpResponseOptionsBuilder<T>>? configure = null) |
IResult |
Maps Result<T> to 200 OK with the value as body, or 201 Created + Location when Created / CreatedAtRoute / CreatedAtAction is configured. For Result<Unit> (the no-payload result returned by Result.Ok() / Result.Fail(error)), success emits 204 No Content. Failures go through Problem Details. |
public static IResult ToHttpResponse<TDomain, TBody>(this Result<TDomain> result, Func<TDomain, TBody> body, Action<HttpResponseOptionsBuilder<TDomain>>? configure = null) |
IResult |
Same as the Result<T> overload, but projects the response body via body. Selectors in the options builder still run against the domain value. |
public static IResult ToHttpResponse<T>(this Result<WriteOutcome<T>> result, Action<HttpResponseOptionsBuilder<T>>? configure = null) |
IResult |
Maps Result<WriteOutcome<T>> per RFC 9110: Created → 201 + Location, Updated → 200 (or 204 with Prefer: return=minimal), UpdatedNoContent → 204, Accepted → 202 + Retry-After, AcceptedNoContent → 202. |
public static IResult ToHttpResponse<TDomain, TBody>(this Result<WriteOutcome<TDomain>> result, Func<TDomain, TBody> body, Action<HttpResponseOptionsBuilder<TDomain>>? configure = null) |
IResult |
WriteOutcome overload with body projection. |
public static IResult ToHttpResponse<T, TBody>(this Result<Page<T>> result, Func<Cursor, int, string> nextUrlBuilder, Func<T, TBody> body, Action<HttpResponseOptionsBuilder<Page<T>>>? configure = null) |
IResult |
Maps Result<Page<T>> to a paginated JSON envelope (PagedResponse<TBody>) plus an RFC 8288 Link header. nextUrlBuilder(cursor, appliedLimit) builds the absolute URL for next/previous links. |
Each overload also exposes Task<...> and ValueTask<...> async variants named ToHttpResponseAsync with identical signatures.
HttpResponseOptionsBuilder<TDomain>
Declaration
public sealed class HttpResponseOptionsBuilder<TDomain>
Fluent options builder used by every generic ToHttpResponse overload. Selectors run against the TDomain value (not the projected response body). All methods return this for chaining.
| Signature | Returns | Description |
|---|---|---|
WithETag(Func<TDomain, string> selector) |
HttpResponseOptionsBuilder<TDomain> |
Sets a strong ETag (wraps the string in EntityTagValue.Strong). |
WithETag(Func<TDomain, EntityTagValue> selector) |
HttpResponseOptionsBuilder<TDomain> |
Sets a strong or weak ETag from a caller-built EntityTagValue. |
WithLastModified(Func<TDomain, DateTimeOffset> selector) |
HttpResponseOptionsBuilder<TDomain> |
Emits Last-Modified header in RFC 1123 format. |
Vary(params string[] headers) |
HttpResponseOptionsBuilder<TDomain> |
Appends headers to the response Vary header (existing values preserved; duplicates suppressed). |
WithContentLanguage(params string[] languages) |
HttpResponseOptionsBuilder<TDomain> |
Joins values into Content-Language. |
WithContentLocation(Func<TDomain, string> selector) |
HttpResponseOptionsBuilder<TDomain> |
Sets the Content-Location header. |
WithAcceptRanges(string acceptRanges) |
HttpResponseOptionsBuilder<TDomain> |
Sets Accept-Ranges (e.g. "bytes" or "none"). |
Created(string locationLiteral) |
HttpResponseOptionsBuilder<TDomain> |
Returns 201 Created with a literal Location header. |
Created(Func<TDomain, string> selector) |
HttpResponseOptionsBuilder<TDomain> |
Returns 201 Created with a Location derived from the value. |
CreatedAtRoute(string routeName, Func<TDomain, RouteValueDictionary> routeValues) |
HttpResponseOptionsBuilder<TDomain> |
Returns 201 Created with a Location generated via LinkGenerator.GetUriByName (resolved from HttpContext.RequestServices at execute time). AOT-safe. Under query-string API versioning, the route values dictionary MUST include ["api-version"] = ApiVersion — otherwise Location headers omit the version and 404 on dereference. Example: o.CreatedAtRoute("Orders_GetById", o => new RouteValueDictionary { ["id"] = o.Id.Value, ["api-version"] = ApiVersion }). |
[RequiresUnreferencedCode] [RequiresDynamicCode] CreatedAtAction(string actionName, Func<TDomain, RouteValueDictionary> routeValues, string? controllerName = null) |
HttpResponseOptionsBuilder<TDomain> |
MVC equivalent of CreatedAtAction — uses LinkGenerator.GetUriByAction. Not trim/AOT-safe; use CreatedAtRoute for AOT scenarios. |
EvaluatePreconditions() |
HttpResponseOptionsBuilder<TDomain> |
On GET/HEAD, evaluates RFC 9110 conditional headers (If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since) using the configured ETag/LastModified selectors and writes 304 Not Modified or 412 Precondition Failed accordingly. On unsafe methods the precondition must be evaluated before the mutation. |
HonorPrefer() |
HttpResponseOptionsBuilder<TDomain> |
Honors RFC 7240 Prefer: return=minimal / return=representation. Always emits Vary: Prefer; emits Preference-Applied only when honored. |
WithRange(Func<TDomain, ContentRangeHeaderValue> selector) |
HttpResponseOptionsBuilder<TDomain> |
Returns 206 Partial Content with a Content-Range header from the selector (returns 200 when the range covers the whole representation). |
WithRange(long from, long to, long totalLength) |
HttpResponseOptionsBuilder<TDomain> |
Static range variant. Clamps to to totalLength - 1; returns 200 when the range covers the whole resource. |
WithErrorMapping(Func<Error, int> mapper) |
HttpResponseOptionsBuilder<TDomain> |
Per-call mapper for failure responses. Highest precedence. |
WithErrorMapping<TError>(int statusCode) where TError : Error |
HttpResponseOptionsBuilder<TDomain> |
Per-call override for a single error type. Higher precedence than TrellisAspOptions. |
HttpResponseOptionsBuilder
Declaration
public sealed class HttpResponseOptionsBuilder
Non-generic builder used for the value-less Result overload.
| Signature | Returns | Description |
|---|---|---|
Vary(params string[] headers) |
HttpResponseOptionsBuilder |
Appends headers to Vary. |
HonorPrefer() |
HttpResponseOptionsBuilder |
Always emits Vary: Prefer. |
WithErrorMapping(Func<Error, int> mapper) |
HttpResponseOptionsBuilder |
Per-call mapper for failure responses. |
WithErrorMapping<TError>(int statusCode) where TError : Error |
HttpResponseOptionsBuilder |
Per-call override for a single error type. |
ActionResultAdapterExtensions
Declaration
public static class ActionResultAdapterExtensions
MVC adapter that wraps an IResult in an ActionResult<T> so MVC controllers can declare typed return signatures (e.g. Task<ActionResult<TodoResponse>>) for OpenAPI/ApiExplorer inference and [ProducesResponseType<T>] compatibility. Implementation forwards ActionResult.ExecuteResultAsync to IResult.ExecuteAsync(HttpContext) via an internal TrellisActionResult<T> (which also implements IConvertToActionResult).
| Signature | Returns | Description |
|---|---|---|
public static ActionResult<T> AsActionResult<T>(this IResult result) |
ActionResult<T> |
Wraps an IResult in a typed ActionResult<T>. |
public static Task<ActionResult<T>> AsActionResultAsync<T>(this Task<IResult> resultTask) |
Task<ActionResult<T>> |
Async Task overload. |
public static ValueTask<ActionResult<T>> AsActionResultAsync<T>(this ValueTask<IResult> resultTask) |
ValueTask<ActionResult<T>> |
Async ValueTask overload. |
TrellisAspOptions
Declaration
public sealed class TrellisAspOptions
Configuration registered via AddTrellisAsp(...) that maps domain Error types to HTTP status codes.
| Name | Type | Description |
|---|---|---|
SystemDefault |
static TrellisAspOptions (internal) |
Read-only default instance used when DI cannot resolve a configured TrellisAspOptions (e.g. the host did not call AddTrellisAsp). Internal — not callable from user code; hosts that want a different default must register their own. |
| Signature | Returns | Description |
|---|---|---|
public TrellisAspOptions MapError<TError>(int statusCode) where TError : Error |
TrellisAspOptions |
Overrides or adds an error-type-to-status-code mapping. |
internal int GetStatusCode(Error error) |
int |
Walks the error type hierarchy looking for a mapping; falls back to 500. Invoked by the response writer. |
Default mappings (closed-ADT): Error.BadRequest=400, Error.Unauthorized=401, Error.Forbidden=403, Error.NotFound=404, Error.MethodNotAllowed=405, Error.NotAcceptable=406, Error.Conflict=409, Error.Gone=410, Error.PreconditionFailed=412, Error.ContentTooLarge=413, Error.UnsupportedMediaType=415, Error.RangeNotSatisfiable=416, Error.UnprocessableContent=422, Error.PreconditionRequired=428, Error.TooManyRequests=429, Error.InternalServerError=500, Error.Unexpected=500, Error.NotImplemented=501, Error.ServiceUnavailable=503.
RuleViolationProblemDetail
Declaration
public sealed record RuleViolationProblemDetail(string Code, string? Detail, string[] Fields);
AOT-friendly JSON payload used inside Problem Details extensions["rules"] for Error.UnprocessableContent rule violations. Application code should treat this as response shape metadata, not as a domain model.
AggregateRepresentationValidator<T>
Declaration
public sealed class AggregateRepresentationValidator<T> : IRepresentationValidator<T> where T : IAggregate
| Signature | Returns | Description |
|---|---|---|
public EntityTagValue GenerateETag(T value, string? variantKey = null) |
EntityTagValue |
Returns EntityTagValue.Strong(value.ETag) when variantKey is null/empty; otherwise SHA-256 hashes $"{value.ETag}:{variantKey}" and returns the first 16 lowercase hex characters as a strong ETag. |
IRepresentationValidator<in T>
Declaration
public interface IRepresentationValidator<in T>
| Signature | Returns | Description |
|---|---|---|
EntityTagValue GenerateETag(T value, string? variantKey = null) |
EntityTagValue |
Generates a representation-specific validator for a domain value and optional variant key (typically the negotiated content type or language). |
ETagHelper
Declaration
public static class ETagHelper
| Signature | Returns | Description |
|---|---|---|
public static bool IfNoneMatchMatches(IList<EntityTagHeaderValue> ifNoneMatchHeader, string currentETag) |
bool |
Weak-comparison helper for If-None-Match; returns true for * or any matching opaque tag. |
public static bool IfMatchSatisfied(IList<EntityTagHeaderValue> ifMatchHeader, string currentETag) |
bool |
Strong-comparison helper for If-Match; returns true for * or a matching strong tag. |
public static EntityTagValue[]? ParseIfNoneMatch(HttpRequest request) |
EntityTagValue[]? |
null when absent; [] when present but unparseable/empty; wildcard for *; otherwise the parsed strong/weak tags. |
public static DateTimeOffset? ParseIfModifiedSince(HttpRequest request) |
DateTimeOffset? |
Returns the typed If-Modified-Since value. |
public static DateTimeOffset? ParseIfUnmodifiedSince(HttpRequest request) |
DateTimeOffset? |
Returns the typed If-Unmodified-Since value. |
public static EntityTagValue[]? ParseIfMatch(HttpRequest request) |
EntityTagValue[]? |
null when absent; [] when present but empty/only weak; wildcard for *; otherwise strong tags only. |
IfNoneMatchExtensions
Declaration
public static class IfNoneMatchExtensions
Create-if-absent guard for unsafe methods (PUT / POST).
| Signature | Returns | Description |
|---|---|---|
public static Result<T> EnforceIfNoneMatchPrecondition<T>(this Result<T> result, EntityTagValue[]? ifNoneMatchETags) |
Result<T> |
When ifNoneMatchETags contains *, replaces a successful result with Error.PreconditionFailed (PreconditionKind.IfNoneMatch). No-op when the header is absent or the result is already a failure. |
public static Task<Result<T>> EnforceIfNoneMatchPreconditionAsync<T>(this Task<Result<T>> resultTask, EntityTagValue[]? ifNoneMatchETags) |
Task<Result<T>> |
Async Task overload. |
public static ValueTask<Result<T>> EnforceIfNoneMatchPreconditionAsync<T>(this ValueTask<Result<T>> resultTask, EntityTagValue[]? ifNoneMatchETags) |
ValueTask<Result<T>> |
Async ValueTask overload. |
PreferHeader
Declaration
public sealed class PreferHeader
Parses the RFC 7240 Prefer request header. Per RFC 7240 §2 unrecognized or malformed tokens are ignored; duplicate recognized preferences use first-wins behavior.
| Name | Type | Description |
|---|---|---|
ReturnRepresentation |
bool |
true for return=representation. |
ReturnMinimal |
bool |
true for return=minimal. |
RespondAsync |
bool |
true for respond-async. |
Wait |
int? |
Parsed wait=N value; null when absent or unparseable. |
HandlingStrict |
bool |
true for handling=strict. |
HandlingLenient |
bool |
true for handling=lenient. |
HasPreferences |
bool |
true when at least one recognized preference was parsed. Unknown preferences do not set this. |
| Signature | Returns | Description |
|---|---|---|
public static PreferHeader Parse(HttpRequest request) |
PreferHeader |
Parses the header from the request. |
RangeOutcome
Declaration
public abstract record RangeOutcome
{
public sealed record FullRepresentation : RangeOutcome;
public sealed record PartialContent(long From, long To, long CompleteLength) : RangeOutcome;
public sealed record NotSatisfiable(long CompleteLength) : RangeOutcome;
}
Result of evaluating an RFC 9110 Range request.
| Variant | Description |
|---|---|
FullRepresentation |
No Range header, non-bytes unit, multi-range request, malformed range, or non-GET method — serve the full representation (200 OK). |
PartialContent(From, To, CompleteLength) |
Satisfiable single byte range — serve 206 Partial Content with Content-Range. |
NotSatisfiable(CompleteLength) |
from >= completeLength or from > to — serve 416 Range Not Satisfiable with Content-Range: bytes */{CompleteLength}. |
RangeRequestEvaluator
Declaration
public static class RangeRequestEvaluator
| Signature | Returns | Description |
|---|---|---|
public static RangeOutcome Evaluate(HttpRequest request, long completeLength) |
RangeOutcome |
Evaluates the Range header per RFC 9110 §14. Only supports bytes. Non-GET, missing header, multi-range, or malformed → FullRepresentation. Suffix ranges (-N) supported. Throws ArgumentOutOfRangeException for negative completeLength. |
PartialContentHttpResult
Declaration
public sealed class PartialContentHttpResult : IResult
| Name | Type | Description |
|---|---|---|
ContentRangeHeaderValue |
ContentRangeHeaderValue |
The Content-Range header written by the result. |
| Signature | Returns | Description |
|---|---|---|
public PartialContentHttpResult(long rangeStart, long rangeEnd, long? totalLength, IResult inner) |
— | Builds a 206 Partial Content result using unit "items", delegating body serialization to inner. |
public PartialContentHttpResult(ContentRangeHeaderValue contentRangeHeaderValue, IResult inner) |
— | Variant with a caller-built Content-Range. |
public Task ExecuteAsync(HttpContext httpContext) |
Task |
Writes Content-Range, forces status 206, then executes inner. |
PartialContentResult
Declaration
public class PartialContentResult : ObjectResult
MVC ObjectResult companion to PartialContentHttpResult.
| Name | Type | Description |
|---|---|---|
ContentRangeHeaderValue |
ContentRangeHeaderValue |
The Content-Range header written during formatting. |
| Signature | Returns | Description |
|---|---|---|
public PartialContentResult(long rangeStart, long rangeEnd, long? totalLength, object? value) |
— | 206 Partial Content MVC result using unit "items". |
public PartialContentResult(ContentRangeHeaderValue contentRangeHeaderValue, object? value) |
— | Variant with a caller-built Content-Range. |
public override void OnFormatting(ActionContext context) |
void |
Writes the Content-Range response header before object body formatting. |
PagedResponse<TResponse>
Declaration
public sealed record PagedResponse<TResponse>(
IReadOnlyList<TResponse> Items,
PageLink? Next,
PageLink? Previous,
int RequestedLimit,
int AppliedLimit,
int DeliveredCount,
bool WasCapped);
JSON envelope returned by the Result<Page<T>> overload of ToHttpResponse.
PageLink
Declaration
public sealed record PageLink(string Cursor, string Href);
A cursor + the absolute URL the client should follow. Also rendered as <{Href}>; rel="next" / rel="prev" entries in the response Link header.
ServiceCollectionExtensions
Declaration
public static class ServiceCollectionExtensions
The main DI surface for Trellis.Asp (in folder Extensions/).
| Signature | Returns | Description |
|---|---|---|
public static IMvcBuilder AddScalarValueValidation(this IMvcBuilder builder) |
IMvcBuilder |
Configures MVC JSON options + the ScalarValueValidationFilter + a ScalarValueModelBinderProvider. Suppresses MVC validation recursion into Maybe<T>. |
public static IServiceCollection AddScalarValueValidation(this IServiceCollection services) |
IServiceCollection |
Configures both MVC (MvcJsonOptions) and Minimal API (HttpJsonOptions) JSON pipelines for scalar-value/Maybe<T> support. Idempotent. |
public static IApplicationBuilder UseScalarValueValidation(this IApplicationBuilder app) |
IApplicationBuilder |
Adds ScalarValueValidationMiddleware so ValidatingJsonConverter<TValue,TPrimitive> can collect errors per request. |
public static IServiceCollection AddScalarValueValidationForMinimalApi(this IServiceCollection services) |
IServiceCollection |
Configures only the Minimal API JSON pipeline. |
public static RouteHandlerBuilder WithScalarValueValidation(this RouteHandlerBuilder builder) |
RouteHandlerBuilder |
Adds ScalarValueValidationEndpointFilter to the route handler. |
public static IServiceCollection AddTrellisAsp(this IServiceCollection services) |
IServiceCollection |
Registers TrellisAspOptions with default error mappings, then calls AddScalarValueValidation(). |
public static IServiceCollection AddTrellisAsp(this IServiceCollection services, Action<TrellisAspOptions> configure) |
IServiceCollection |
Same as above, with a MapError<TError>(...) callback for overrides. |
Namespace Trellis.Asp.Authorization
The actor-provider DI surface absorbed from the former Trellis.Asp.Authorization package. Domain primitives (Actor, IActorProvider, etc.) live in Trellis.Authorization — see trellis-api-authorization.md.
ServiceCollectionExtensions
Declaration
public static class ServiceCollectionExtensions
| Signature | Returns | Description |
|---|---|---|
public static IServiceCollection AddClaimsActorProvider(this IServiceCollection services, Action<ClaimsActorOptions>? configure = null) |
IServiceCollection |
Adds IHttpContextAccessor, configures ClaimsActorOptions, and registers IActorProvider as a scoped ClaimsActorProvider. |
public static IServiceCollection AddEntraActorProvider(this IServiceCollection services, Action<EntraActorOptions>? configure = null) |
IServiceCollection |
Adds IHttpContextAccessor, configures EntraActorOptions, and registers IActorProvider as a scoped EntraActorProvider. |
public static IServiceCollection AddDevelopmentActorProvider(this IServiceCollection services, Action<DevelopmentActorOptions>? configure = null) |
IServiceCollection |
Adds IHttpContextAccessor + logging, configures DevelopmentActorOptions, and registers IActorProvider as a scoped DevelopmentActorProvider. The provider itself throws outside the Development environment. |
public static IServiceCollection AddCachingActorProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(this IServiceCollection services) where T : class, IActorProvider |
IServiceCollection |
Registers concrete provider T as scoped, then wraps it with CachingActorProvider as the scoped IActorProvider. |
ClaimsActorOptions
Declaration
public class ClaimsActorOptions
| Name | Type | Description |
|---|---|---|
ActorIdClaim |
string |
Claim type used for Actor.Id. Default: "sub". Matched verbatim against Claim.Type; no dotted/JSON-path traversal. |
PermissionsClaim |
string |
Claim type used for permissions. Default: "permissions". Multi-valued JWT claims arrive as repeated Claim instances and are aggregated via FindAll. |
ClaimsActorProvider
Declaration
public class ClaimsActorProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<ClaimsActorOptions> options) : IActorProvider
Hydrates an Actor from the current HttpContext.User using flat JWT/OIDC claims. Subclass and override GetCurrentActorAsync for nested-claim or computed-permission scenarios; EntraActorProvider is a worked example.
| Name | Type | Description |
|---|---|---|
HttpContextAccessor |
IHttpContextAccessor (protected) |
Exposed to derived providers. |
Options |
ClaimsActorOptions (protected) |
Mapped options value. |
| Signature | Returns | Description |
|---|---|---|
public virtual Task<Actor> GetCurrentActorAsync(CancellationToken cancellationToken = default) |
Task<Actor> |
Throws InvalidOperationException when HttpContext is missing, no authenticated identity exists, or the configured ActorIdClaim is missing. Permissions come from FindAll(PermissionsClaim) snapshotted into a FrozenSet<string>; Actor.Create(actorId, permissions) is used so forbidden permissions and attributes default to empty. |
EntraActorOptions
Declaration
public sealed class EntraActorOptions
| Name | Type | Description |
|---|---|---|
IdClaimType |
string |
Claim type used for actor ID. Default: "http://schemas.microsoft.com/identity/claims/objectidentifier". |
MapPermissions |
Func<IEnumerable<Claim>, IReadOnlySet<string>> |
Default returns the values of every roles / ClaimTypes.Role claim (case-insensitive type match). |
MapForbiddenPermissions |
Func<IEnumerable<Claim>, IReadOnlySet<string>> |
Default returns an empty HashSet<string>. |
MapAttributes |
Func<IEnumerable<Claim>, HttpContext, IReadOnlyDictionary<string, string>> |
Default extracts tid, preferred_username, azp, azpacr, acrs, plus ip_address from Connection.RemoteIpAddress and mfa = "true"|"false" from the amr claim. |
EntraActorProvider
Declaration
public sealed class EntraActorProvider : ClaimsActorProvider
| Signature | Returns | Description |
|---|---|---|
public EntraActorProvider(IHttpContextAccessor httpContextAccessor, IOptions<EntraActorOptions> options) |
— | Builds the Entra-specific provider; passes ActorIdClaim = options.Value.IdClaimType and PermissionsClaim = "roles" to the base. |
public override Task<Actor> GetCurrentActorAsync(CancellationToken cancellationToken = default) |
Task<Actor> |
Throws the same missing-context / missing-identity failures as ClaimsActorProvider. When IdClaimType is the long objectidentifier claim, falls back to the short "oid" claim before failing. Wraps any exception from MapPermissions, MapForbiddenPermissions, or MapAttributes in InvalidOperationException. |
DevelopmentActorOptions
Declaration
public sealed class DevelopmentActorOptions
| Name | Type | Description |
|---|---|---|
DefaultActorId |
string |
Default fallback actor ID. Default: "development". |
DefaultPermissions |
IReadOnlySet<string> |
Default fallback permissions when no header is supplied. Default: empty HashSet<string>. |
ThrowOnMalformedHeader |
bool |
When true, malformed X-Test-Actor JSON throws instead of falling back to the default actor. Default: false. |
DevelopmentActorProvider
Declaration
public sealed partial class DevelopmentActorProvider(
IHttpContextAccessor httpContextAccessor,
IHostEnvironment hostEnvironment,
IOptions<DevelopmentActorOptions> options,
ILogger<DevelopmentActorProvider> logger) : IActorProvider
Reads the X-Test-Actor header (JSON: { "Id": ..., "Permissions": [...], "ForbiddenPermissions": [...], "Attributes": {...} }, case-insensitive property matching).
| Signature | Returns | Description |
|---|---|---|
public Task<Actor> GetCurrentActorAsync(CancellationToken cancellationToken = default) |
Task<Actor> |
Throws InvalidOperationException whenever !hostEnvironment.IsDevelopment(), regardless of header presence. In Development, returns Actor.Create(DefaultActorId, DefaultPermissions) when HttpContext is null or the header is missing/empty. Malformed JSON logs a warning and falls back unless ThrowOnMalformedHeader is true. |
CachingActorProvider
Declaration
public sealed class CachingActorProvider : IActorProvider
Decorator that caches the inner provider's resolution task per request scope using LazyInitializer.EnsureInitialized. The shared task uses HttpContext.RequestAborted so expensive work (DB lookups) is canceled with the request, but individual callers' tokens only cancel their own awaits.
| Signature | Returns | Description |
|---|---|---|
public CachingActorProvider(IActorProvider inner, IHttpContextAccessor httpContextAccessor) |
— | inner cannot be null. |
public Task<Actor> GetCurrentActorAsync(CancellationToken cancellationToken = default) |
Task<Actor> |
Returns the cached task; if cancellationToken differs from RequestAborted, applies it via Task.WaitAsync. |
Namespace Trellis.Asp.ModelBinding
ScalarValueModelBinderBase<TResult, TValue, TPrimitive>
Declaration
public abstract class ScalarValueModelBinderBase<TResult, TValue, TPrimitive> : IModelBinder
where TValue : IScalarValue<TValue, TPrimitive>
where TPrimitive : IComparable
| Signature | Returns | Description |
|---|---|---|
protected abstract ModelBindingResult OnMissingValue() |
ModelBindingResult |
Called when no raw value is present in the value provider. |
protected virtual ModelBindingResult? OnEmptyValue() => null |
ModelBindingResult? |
Called when the raw value is an empty string; return null to fall through to normal conversion. |
protected abstract ModelBindingResult OnSuccess(TValue value) |
ModelBindingResult |
Wraps a validated scalar value into the final binding result. |
public Task BindModelAsync(ModelBindingContext bindingContext) |
Task |
Reads the raw value, converts to TPrimitive, calls TValue.TryCreate, and populates ModelState on failure. |
ScalarValueModelBinder<TValue, TPrimitive>
Declaration
public class ScalarValueModelBinder<TValue, TPrimitive>
: ScalarValueModelBinderBase<TValue, TValue, TPrimitive>
| Signature | Returns | Description |
|---|---|---|
protected override ModelBindingResult OnMissingValue() |
ModelBindingResult |
Leaves the binding result unset (default). |
protected override ModelBindingResult OnSuccess(TValue value) |
ModelBindingResult |
Returns ModelBindingResult.Success(value). |
MaybeModelBinder<TValue, TPrimitive>
Declaration
public class MaybeModelBinder<TValue, TPrimitive>
: ScalarValueModelBinderBase<Maybe<TValue>, TValue, TPrimitive>
| Signature | Returns | Description |
|---|---|---|
protected override ModelBindingResult OnMissingValue() |
ModelBindingResult |
Returns ModelBindingResult.Success(Maybe<TValue>.None). |
protected override ModelBindingResult? OnEmptyValue() |
ModelBindingResult? |
Returns ModelBindingResult.Success(Maybe<TValue>.None). |
protected override ModelBindingResult OnSuccess(TValue value) |
ModelBindingResult |
Returns ModelBindingResult.Success(Maybe.From(value)). |
ScalarValueModelBinderProvider
Declaration
public class ScalarValueModelBinderProvider : IModelBinderProvider
| Signature | Returns | Description |
|---|---|---|
public IModelBinder? GetBinder(ModelBinderProviderContext context) |
IModelBinder? |
Returns a MaybeModelBinder<,> for Maybe<TScalar>, a ScalarValueModelBinder<,> for direct scalar values, or null otherwise. Annotated [UnconditionalSuppressMessage] for IL2070/IL2072/IL2075 and IL3050 — model binding is not Native AOT compatible. |
Namespace Trellis.Asp.Routing
TrellisValueObjectRouteConstraint<T>
Declaration
public sealed class TrellisValueObjectRouteConstraint<T> : IRouteConstraint
where T : IParsable<T>
| Signature | Returns | Description |
|---|---|---|
public bool Match(HttpContext?, IRouter?, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) |
bool |
Delegates to T.TryParse(..., CultureInfo.InvariantCulture, out _). Returns false when the route value is missing, null, or fails to parse. |
RouteConstraintRegistrationExtensions
Declaration
public static class RouteConstraintRegistrationExtensions
| Signature | Returns | Description |
|---|---|---|
public static IServiceCollection AddTrellisRouteConstraints(this IServiceCollection services, params Assembly[] assemblies) |
IServiceCollection |
Scans the supplied assemblies (or the calling assembly + the assembly containing IScalarValue<,> from Trellis.Core if none are supplied) for value objects implementing both IScalarValue<TSelf, TPrimitive> and IParsable<TSelf>, then registers a TrellisValueObjectRouteConstraint<T> under the type's simple name. Existing entries in RouteOptions.ConstraintMap are preserved. Reflection-based — not Native AOT compatible. |
public static IServiceCollection AddTrellisRouteConstraint<T>(this IServiceCollection services, string? constraintName = null) where T : IParsable<T> |
IServiceCollection |
Registers a single value-object route constraint without reflection. AOT-safe. |
Once registered, route templates such as "/products/{id:ProductId}" parse and bind the segment via the value object's IParsable<T>.TryParse implementation.
Namespace Trellis.Asp.Validation
ScalarValueJsonConverterBase<TResult, TValue, TPrimitive>
Declaration
public abstract class ScalarValueJsonConverterBase<TResult, TValue, TPrimitive>
: JsonConverter<TResult>
where TValue : class, IScalarValue<TValue, TPrimitive>
where TPrimitive : IComparable
| Name | Type | Description |
|---|---|---|
HandleNull |
bool (override) |
Always true; forces System.Text.Json to call Read(...) for JSON null tokens. |
| Signature | Returns | Description |
|---|---|---|
protected abstract TResult OnNullToken(string fieldName) |
TResult |
Returns the deserialization result for a JSON null token. |
protected abstract TResult WrapSuccess(TValue value) |
TResult |
Wraps a validated scalar value into the final converter result. |
protected abstract TResult OnValidationFailure() |
TResult |
Returns the failure result after a validation error has been collected into ValidationErrorsContext. |
public override TResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) |
TResult |
Reads the primitive JSON value, calls TValue.TryCreate, collects errors into ValidationErrorsContext, and returns the derived-type wrapper. |
protected static string GetDefaultFieldName() |
string |
Returns the camel-cased scalar type name used when no property name is available. |
ValidatingJsonConverter<TValue, TPrimitive>
Declaration
public sealed class ValidatingJsonConverter<TValue, TPrimitive>
: ScalarValueJsonConverterBase<TValue?, TValue, TPrimitive>
| Signature | Returns | Description |
|---|---|---|
protected override TValue? OnNullToken(string fieldName) |
TValue? |
Adds "{TypeName} cannot be null." to ValidationErrorsContext and returns null. |
protected override TValue? WrapSuccess(TValue value) |
TValue? |
Returns the validated scalar value. |
protected override TValue? OnValidationFailure() |
TValue? |
Returns null. |
public override void Write(Utf8JsonWriter writer, TValue? value, JsonSerializerOptions options) |
void |
Writes JSON null for null; otherwise writes the underlying primitive value.Value. |
ValidatingJsonConverterFactory
Declaration
public sealed class ValidatingJsonConverterFactory : JsonConverterFactory
| Signature | Returns | Description |
|---|---|---|
public override bool CanConvert(Type typeToConvert) |
bool |
true when typeToConvert is a scalar value type. |
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) |
JsonConverter? |
Builds a ValidatingJsonConverter<TValue, TPrimitive> for supported scalar value types. Annotated [RequiresDynamicCode] — JsonConverterFactory is not Native AOT compatible. |
MaybeScalarValueJsonConverter<TValue, TPrimitive>
Declaration
public sealed class MaybeScalarValueJsonConverter<TValue, TPrimitive>
: ScalarValueJsonConverterBase<Maybe<TValue>, TValue, TPrimitive>
| Signature | Returns | Description |
|---|---|---|
protected override Maybe<TValue> OnNullToken(string fieldName) |
Maybe<TValue> |
Returns Maybe<TValue>.None; JSON null is valid for optional scalar values. |
protected override Maybe<TValue> WrapSuccess(TValue value) |
Maybe<TValue> |
Returns Maybe.From(value). |
protected override Maybe<TValue> OnValidationFailure() |
Maybe<TValue> |
Returns Maybe<TValue>.None. |
public override void Write(Utf8JsonWriter writer, Maybe<TValue> value, JsonSerializerOptions options) |
void |
Writes JSON null for Maybe.None; otherwise writes the wrapped primitive value.Value.Value. |
MaybeScalarValueJsonConverterFactory
Declaration
public sealed class MaybeScalarValueJsonConverterFactory : JsonConverterFactory
| Signature | Returns | Description |
|---|---|---|
public override bool CanConvert(Type typeToConvert) |
bool |
true when typeToConvert is Maybe<T> and T is a scalar value type. |
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) |
JsonConverter? |
Builds MaybeScalarValueJsonConverter<TValue, TPrimitive> for supported Maybe<TScalar> types. |
ScalarValueValidationFilter
Declaration
public sealed class ScalarValueValidationFilter : IActionFilter, IOrderedFilter
| Name | Type | Description |
|---|---|---|
Order |
int |
Always -2000; runs early in the MVC filter pipeline. |
| Signature | Returns | Description |
|---|---|---|
public void OnActionExecuting(ActionExecutingContext context) |
void |
Short-circuits with a validation problem result for collected JSON validation errors or invalid scalar route/query parameters. |
public void OnActionExecuted(ActionExecutedContext context) |
void |
No-op. |
ScalarValueValidationEndpointFilter
Declaration
public sealed class ScalarValueValidationEndpointFilter : IEndpointFilter
| Signature | Returns | Description |
|---|---|---|
public ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) |
ValueTask<object?> |
For Minimal APIs, returns Results.ValidationProblem(validationError.ToDictionary()) when ValidationErrorsContext contains errors; otherwise invokes next. |
ScalarValueValidationMiddleware
Declaration
public sealed class ScalarValueValidationMiddleware
| Signature | Returns | Description |
|---|---|---|
public ScalarValueValidationMiddleware(RequestDelegate next) |
— | Wraps each request in ValidationErrorsContext.BeginScope(). |
public Task InvokeAsync(HttpContext context) |
Task |
Begins a validation scope, invokes the next middleware, and converts scalar-value BadHttpRequestException binding failures into validation problem responses using endpoint parameter metadata plus route/query raw values. |
ValidationErrorsContext
Declaration
public static class ValidationErrorsContext
| Name | Type | Description |
|---|---|---|
HasErrors |
bool |
true when the current async-local scope contains at least one collected validation error. |
| Signature | Returns | Description |
|---|---|---|
public static IDisposable BeginScope() |
IDisposable |
Starts a new async-local validation collection scope; disposing restores the previous scope and property name. |
public static Error.UnprocessableContent? GetUnprocessableContent() |
Error.UnprocessableContent? |
Returns the aggregated Error.UnprocessableContent for the current scope (with Fields / Rules populated from collected FieldViolations) or null when no errors were collected. |
Behavioral notes
- One verb, every shape.
ToHttpResponseis the only supported response mapper. The internal result types it constructs (TrellisHttpResult<TDomain, TBody>,TrellisWriteOutcomeResult<TDomain, TBody>,TrellisErrorOnlyResult,TrellisEmptyResult) implementIResult, theIStatusCodeHttpResult/IValueHttpResult<T>/IContentTypeHttpResultmetadata interfaces, andIEndpointMetadataProviderso OpenAPI/ApiExplorer surfaces the success status, body type, and the union of error envelopes the writer can emit (200,201,206,304,400,404,412,500). Layer your own[ProducesResponseType]/Produces<T>on top. - Failures use Problem Details. A failure runs through
ResponseFailureWriter(internal). It callsResults.ValidationProblem(...)forError.UnprocessableContentwith field violations (theerrorsdictionary keys are the violationField.Pathwith the leading/trimmed; values areDetail ?? ReasonCode), andResults.Problem(...)for everything else. Companion headers are emitted automatically:AllowforError.MethodNotAllowed,Retry-AfterforError.TooManyRequests/Error.ServiceUnavailable(when a delay is configured), andContent-Range: {Unit} */{CompleteLength}forError.RangeNotSatisfiable. Extensions always carrycodeandkind;Error.InternalServerErroraddsfaultId; rule violations are surfaced underrules. For5xxresponses theDetailis always replaced with"An internal error occurred."so internal diagnostics never leak to clients. - Status code resolution precedence.
WithErrorMapping(Func<Error, int>)(per call) →WithErrorMapping<TError>(int)(per call, walks the type hierarchy) →TrellisAspOptionsresolved fromHttpContext.RequestServices(orTrellisAspOptions.SystemDefaultif none registered) →500 Internal Server Error. - Conditional requests.
EvaluatePreconditions()runs only onGET/HEADand only when at least one ofWithETag/WithLastModifiedis configured. The internalConditionalRequestEvaluatorevaluates RFC 9110 preconditions in this order:If-Match(strong); elseIf-Unmodified-Since; thenIf-None-Match(weak); elseIf-Modified-Sincefor safe methods. FailedIf-Match/If-Unmodified-Since→412; failedIf-None-Match/If-Modified-SinceonGET/HEAD→304. Varyis append-only. Both theHonorPrefer()switch andVary(...)useAppendVaryUnique— they preserve any pre-existingVaryvalues added by other middleware and skip duplicates (case-insensitive).HonorPrefer()semantics onWriteOutcome.Updated.Prefer: return=minimalshort-circuits to204 No Contentand emitsPreference-Applied: return=minimal;return=representationreturns200 OKwith the body and emitsPreference-Applied: return=representation.Vary: Preferis always emitted when honoringPrefer, regardless of which preference was sent.- Range mapping.
WithRangereturns200 OK(full body) when the configured range covers the whole representation; otherwise206 Partial ContentwithContent-Range. The static-range overload clampstotototalLength - 1; the selector overload trusts the providedContentRangeHeaderValue. CreatedAtActionis not AOT-safe. It depends on MVC'sControllerLinkGeneratorExtensions. The builder method, the writer'sResolveActionLocationprivate, and theLocationKind.Actionbranch are annotated[RequiresUnreferencedCode]/[RequiresDynamicCode]. UseCreatedAtRoutewith a named route for trim/AOT scenarios;ResolveActionLocationthrowsNotSupportedExceptionwhenRuntimeFeature.IsDynamicCodeSupportedisfalse.- Pagination. The
Result<Page<T>>overload always emits thePagedResponse<TBody>envelope; the RFC 8288Linkheader is added only whenPage.Nextand/orPage.Previouscursors are present. Failure on the page result short-circuits through the standard error pipeline. - Validation collection scope.
ScalarValueValidationMiddlewareopens aValidationErrorsContextscope per request. BothValidatingJsonConverter<,>andMaybeScalarValueJsonConverter<,>collect errors into this scope;ScalarValueValidationFilter(MVC) andScalarValueValidationEndpointFilter(Minimal API) short-circuit with a validation problem when the scope is non-empty at action/handler entry. - Minimal API scalar binding failures are metadata-driven. When ASP.NET Core throws a 400 while binding route/query parameters,
ScalarValueValidationMiddlewareno longer parsesBadHttpRequestException.Messageto discover a field name or invalid value. It inspectsIParameterBindingMetadata, reads the matching route/query raw value, and re-runs Trellis scalar validation forIScalarValue<,>/Maybe<TScalar>parameters. Non-scalar endpoint binding failures are rethrown to ASP.NET Core. AddTrellisAspis the one-call setup. It registersTrellisAspOptionsand chainsAddScalarValueValidation(), configuring both the MVC and Minimal API JSON pipelines for scalar-value/Maybe<T>deserialization. You still needUseScalarValueValidation()middleware in the request pipeline andWithScalarValueValidation()on each Minimal API endpoint that should short-circuit on validation errors.- Composite value objects in request/response DTOs.
AddTrellisAsp/AddScalarValueValidationonly wires the scalar VO converters. Composite VOs (multi-field[OwnedEntity]types likeShippingAddress,Money) bind throughCompositeValueObjectJsonConverter<T>(inTrellis.Primitives), which is opt-in per type via[JsonConverter(typeof(CompositeValueObjectJsonConverter<MyVo>))]on the value object class itself. Without that attribute, model binding falls back to default construction and silently bypassesTryCreate— the inner-field validation never runs and an invalid payload propagates into the domain layer. See Cookbook Recipe 13 for the full Domain + API JSON + EF pattern.
Code examples
Basic Result<T> → 200 / Problem Details
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Trellis;
using Trellis.Asp;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTrellisAsp();
var app = builder.Build();
app.UseScalarValueValidation();
app.MapGet("/widgets/{id}", (string id) =>
{
Result<Widget> result = WidgetService.Get(id);
return result.ToHttpResponse(opts => opts
.WithETag(w => w.ETag)
.WithLastModified(w => w.UpdatedAt)
.EvaluatePreconditions());
}).WithScalarValueValidation();
app.Run();
WriteOutcome<T> with Prefer / Created
app.MapPost("/widgets", async (CreateWidget cmd, IWidgetWriter writer, CancellationToken ct) =>
{
Result<WriteOutcome<Widget>> result = await writer.CreateAsync(cmd, ct);
return result.ToHttpResponse(
body: w => new WidgetResponse(w.Id, w.Name),
configure: opts => opts
.CreatedAtRoute("widgets.get", w => new RouteValueDictionary { ["id"] = w.Id })
.WithETag(w => w.ETag)
.HonorPrefer());
});
Paginated Result<Page<T>>
app.MapGet("/widgets", async (string? cursor, int? limit, IWidgetReader reader, HttpContext ctx) =>
{
Result<Page<Widget>> page = await reader.ListAsync(cursor, limit ?? 50, ctx.RequestAborted);
return page.ToHttpResponse(
nextUrlBuilder: (c, applied) =>
$"{ctx.Request.Scheme}://{ctx.Request.Host}/widgets?cursor={c.Token}&limit={applied}",
body: w => new WidgetResponse(w.Id, w.Name));
});
MVC controller using AsActionResult<T> for typed signatures
using Microsoft.AspNetCore.Mvc;
using Trellis;
using Trellis.Asp;
[ApiController]
[Route("widgets")]
public sealed class WidgetsController(IWidgetReader reader) : ControllerBase
{
[HttpGet("{id}", Name = "widgets.get")]
[ProducesResponseType<WidgetResponse>(200)]
[ProducesResponseType<ProblemDetails>(404)]
public async Task<ActionResult<WidgetResponse>> Get(string id, CancellationToken ct)
{
Result<Widget> result = await reader.GetAsync(id, ct);
return await result
.ToHttpResponseAsync(w => new WidgetResponse(w.Id, w.Name))
.AsActionResultAsync<WidgetResponse>();
}
}
Per-call error mapping override
return result.ToHttpResponse(opts => opts
.WithErrorMapping<DomainConflict>(StatusCodes.Status409Conflict)
.WithErrorMapping(err => err is OutOfStockError ? 410 : default));
Actor providers
using Microsoft.Extensions.DependencyInjection;
using Trellis.Asp.Authorization;
var services = new ServiceCollection();
services.AddClaimsActorProvider(opts =>
{
opts.ActorIdClaim = "sub";
opts.PermissionsClaim = "permissions";
});
services.AddEntraActorProvider(opts =>
{
opts.MapPermissions = claims => claims
.Where(c => string.Equals(c.Type, "roles", StringComparison.OrdinalIgnoreCase))
.Select(c => c.Value)
.ToHashSet();
});
if (env.IsDevelopment())
{
services.AddDevelopmentActorProvider(opts =>
{
opts.DefaultActorId = "development";
opts.DefaultPermissions = new HashSet<string> { "orders:read", "orders:create" };
});
}
services.AddCachingActorProvider<EntraActorProvider>();
Route constraints for scalar value objects
// AOT-safe — explicit registration
services.AddTrellisRouteConstraint<ProductId>("ProductId");
// Reflection-based — scans the calling assembly
services.AddTrellisRouteConstraints();
app.MapGet("/products/{id:ProductId}", (ProductId id) => Results.Ok(id));
Cross-references
- trellis-api-core.md —
Result,Result<T>,Error,WriteOutcome<T>,Page<T>,RepresentationMetadata,EntityTagValue,Cursor,PreconditionKind,ResourceRef. - trellis-api-authorization.md —
Actor,IActorProvider,IAuthorize,IAuthorizeResource<TResource>, resource loaders. - trellis-api-primitives.md —
IScalarValue<TSelf, TPrimitive>,Maybe<T>. - trellis-api-http.md — Pure HTTP value objects shared between hosts.
- trellis-api-testing-aspnetcore.md —
WebApplicationFactoryExtensions.CreateClientWithActor(writes theX-Test-Actorheader consumed byDevelopmentActorProvider).