How-to guide

Inject the current principal

Code that needs the authenticated caller (a junction, an application service, a minimal API handler) takes it from DI. Every AddTrax*Auth call registers the injectable services through AddTraxPrincipalAccessor; no IHttpContextAccessor plumbing is needed.

Inject TraxPrincipal into a gated junction

When the train is gated by [TraxAuthorize], inject TraxPrincipal directly. The authorization check refuses an anonymous caller before the junction is built, so the principal is always there:

using Trax.Api.Auth;
using Trax.Core.Junction;
 
public record SendMessageInput(string Body);
 
public record SentMessage(string SenderId, string SenderName, string Body);
 
public class SendMessageJunction(TraxPrincipal user) : Junction<SendMessageInput, SentMessage>
{
    public override Task<SentMessage> Run(SendMessageInput input) =>
        Task.FromResult(new SentMessage(user.Id, user.DisplayName, input.Body));
}

user.Id is scheme-qualified, such as TraxApiKey:alice. Resolving TraxPrincipal with no authenticated Trax principal throws TraxPrincipalNotAvailableException; if it does, the train is missing its gate.

Junctions the scheduler also runs

A junction that runs from the API and from the scheduler cannot take TraxPrincipal in its constructor: the scheduler path has no HttpContext. Use one of these instead.

A train a machine invokes: inject InvokedRun. Its input is built from the machine's context, which the caller does not write, so it cannot carry the initiator. Its junctions inject InvokedRun instead, whose UserKey is the owner of the instance that queued the run, resolved by Trax from the instance's row. Do not put a token in the context for the train to check: a run's input is kept with the run, and anyone who reads it could use the token.

Carry the initiator in the input. The API side fills it from the principal; the scheduler replays what was persisted with the work item:

using Trax.Core.Junction;
 
public record AddJobInput(string InitiatorUserId, string JobTitle);
 
public record Job(string OwnerId, string Title);
 
public class AddJobJunction : Junction<AddJobInput, Job>
{
    public override Task<Job> Run(AddJobInput input) =>
        Task.FromResult(new Job(input.InitiatorUserId, input.JobTitle));
}

Inject TraxCaller. TraxCaller never throws: Principal is null for an anonymous caller, and IsTrusted says whether the scheduler or a runner is executing:

using Trax.Api.Auth;
using Trax.Core.Junction;
 
public record ReportInput(string Name);
 
public record Report(string Name, string RequestedBy);
 
public class BuildReportJunction(TraxCaller caller) : Junction<ReportInput, Report>
{
    public override Task<Report> Run(ReportInput input) =>
        Task.FromResult(new Report(input.Name, caller.Principal?.Id ?? "system"));
}

When no scheme is registered

If every AddTrax*Auth call is conditional, a host where none ran refuses to start, because a junction needs TraxPrincipal. Register the accessor unconditionally; gated trains then refuse every caller until a scheme is configured:

using Trax.Api.Auth;
 
builder.Services.AddAuthentication(); // UseAuthentication() needs it when no scheme ran
builder.Services.AddTraxPrincipalAccessor();

Supply a principal in tests

A test that builds junctions from a service collection registers a fixed principal. The accessor registers its factory with TryAdd and DI resolves the last registration, so the fixed one wins whichever is registered first:

using Trax.Api.Auth;
 
services.AddScoped(_ => new TraxPrincipal("test-user", "Test User", ["Admin"]));

SDK Reference

AddTraxPrincipalAccessor | TraxPrincipal | TraxCaller