Authorization primitives for .NET — ISecurityContext, IAuthorizationPolicy, [Policy], [RequirePolicy], AnonymousSecurityContext, and an AuthorizerFor<TRequest> dispatcher emitted by the bundled source generator.
Used by:
- AI.Sentinel — tool-call authorization for
IChatClient-based agents ZeroAlloc.Mediator.Authorizationv5 — request-handler authorization
dotnet add package ZeroAlloc.AuthorizationTargets net8.0, net9.0, net10.0. The package bundles a Roslyn source generator — no separate *.Generator install.
public interface ISecurityContext
{
string Id { get; }
IReadOnlySet<string> Roles { get; }
IReadOnlyDictionary<string, string> Claims { get; }
}
public interface IAuthorizationPolicy
{
ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
ISecurityContext ctx, CancellationToken ct = default);
}[Policy("AdminOnly")]
public sealed class AdminOnlyPolicy : IAuthorizationPolicy
{
public ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
ISecurityContext ctx, CancellationToken ct = default)
=> new(ctx.Roles.Contains("Admin")
? UnitResult<AuthorizationFailure>.Success()
: new AuthorizationFailure(AuthorizationFailure.DefaultDenyCode, "Admin role required"));
}Bind it on a request type — [RequirePolicy] is class/struct-level only, and stacks:
[RequirePolicy("AdminOnly")]
public sealed record DeleteUserCommand(string UserId);
[RequirePolicy("ActiveTenant")]
[RequirePolicy("AdminOnly")]
public sealed record PurgeTenantCommand(string TenantId);The bundled generator emits one AuthorizerFor<TRequest> subclass per [RequirePolicy]-decorated type plus an AddZeroAllocAuthorization() extension on IServiceCollection. Hosts call the extension at startup and resolve AuthorizerFor<T> per request:
using ZeroAlloc.Authorization.Generated;
builder.Services.AddZeroAllocAuthorization();var authorizer = sp.GetService<AuthorizerFor<DeleteUserCommand>>();
if (authorizer is not null)
{
var result = await authorizer.EvaluateAsync(securityContext, ct);
if (result.IsFailure)
return Forbid(result.Error); // host maps Code / Reason to its outcome shape
}Hosts define their own subinterface for richer payloads. AI.Sentinel adds IToolCallSecurityContext : ISecurityContext with ToolName + Args. ZeroAlloc.Mediator.Authorization v5 adds IRequestSecurityContext<TRequest>. Inside the policy body, downcast:
public ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
ISecurityContext ctx, CancellationToken ct = default)
=> new(ctx is IToolCallSecurityContext tc && tc.ToolName == "delete_database"
? new AuthorizationFailure("tool.destructive", "destructive tool blocked")
: UnitResult<AuthorizationFailure>.Success());For checks that need to await something (tenant lookup, external claims validation), mark EvaluateAsync as async:
[Policy("ActiveTenant")]
public sealed class ActiveTenantPolicy(ITenantService tenants) : IAuthorizationPolicy
{
public async ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
ISecurityContext ctx, CancellationToken ct = default)
{
var active = await tenants.IsActiveAsync(ctx.Id, ct).ConfigureAwait(false);
return active
? UnitResult<AuthorizationFailure>.Success()
: new AuthorizationFailure("tenant.inactive", "tenant is suspended");
}
}The bundled generator emits five compile-time diagnostics:
| ID | Fires when |
|---|---|
ZAUTH001 |
[RequirePolicy("X")] references a policy name with no matching [Policy("X")]. |
ZAUTH002 |
Two [Policy("X")] declarations share the same name. |
ZAUTH003 |
[Policy] class doesn't implement IAuthorizationPolicy. |
ZAUTH004 |
[Policy] class is abstract or static. |
ZAUTH005 |
[RequirePolicy] placed on a non-class/non-struct target. |
BenchmarkDotNet (BDN ShortRun, .NET 10 release build, x64) — happy path on a simple role-check policy:
| Method | Mean | Allocated |
|---|---|---|
EvaluateAsync |
~99 ns | 0 B |
Source: benchmarks/ZeroAlloc.Authorization.Benchmarks/PolicyEvaluationBenchmarks.cs.
The contract is enforced as zero-allocation by:
<IsAotCompatible>true</IsAotCompatible>on the main library (build-time IL2026/IL3050 analyzers fire on any reflection regression).- The
samples/ZeroAlloc.Authorization.AotSmoke/console app, exercised on each CI run withPublishAot=true. - The benchmark project above.
MIT.