Write a principal provider
PerPrincipalMaxConcurrentRun gives each caller its own budget of concurrent direct runs. The
mediator learns who the caller is from ICurrentPrincipalProvider. A GraphQL host gets one that
reads the authenticated user from AddTraxApi; any other host (a worker, a gRPC service, a console
host) writes its own, or the limit holds nothing.
1. Pick the id
Use something the caller cannot choose: the subject claim of an authenticated user, a tenant id
your own authentication set. An id taken from a header or other client input lets a caller spread
its runs across as many ids as it likes, each starting with a full budget. Never return a
per-request or random value. Return null for work with no caller, such as a background job; it is
not limited per principal.
2. Write the provider
The concurrency limiter is a singleton and resolves the provider once, so the provider is a
singleton too and must be thread-safe. Read the ambient state (the current request, an
AsyncLocal) on every call rather than capturing it in the constructor. Keep it cheap: it runs on
every direct run while the limit is set.
using Trax.Mediator.Services.Principal;
public sealed class TenantPrincipalProvider : ICurrentPrincipalProvider
{
public string? GetCurrentPrincipalId() => TenantScope.CurrentTenantId;
}3. Register it after AddMediator
AddMediator registers a default that returns null. Register yours after it, so yours is the one
resolved:
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Extensions;
using Trax.Mediator.Extensions;
using Trax.Mediator.Services.Principal;
services.AddTrax(trax => trax
.AddEffects(effects => effects.UsePostgres(connectionString))
.AddMediator(mediator => mediator
.ScanAssemblies(typeof(TenantPrincipalProvider).Assembly)
.PerPrincipalMaxConcurrentRun(5)));
services.AddSingleton<ICurrentPrincipalProvider, TenantPrincipalProvider>();