How-to guide

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>();

SDK Reference

ICurrentPrincipalProvider | Concurrency limits