Skip to content
Tracon

AmbientTenantScope

Namespace Tracon · Assembly Tracon.Abstractions.dll

The ambient passing mechanism for running an operation executed in the background (outside an HTTP request) as a specific tenant.

public static class AmbientTenantScope
Inheritance
object ← AmbientTenantScope
Inherited Members
object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()

The ITenantContext implementations (the single-tenant default, the HTTP-based multi-tenant resolver) are singletons and read the tenant either from a fixed default or from HttpContext. Which tenant a scheduled job runs for is neither fixed nor lives in an HTTP context — it is carried inside JobRecord.TenantId. This class provides a pass-through using the same Threading.AsyncLocal pattern IHttpContextAccessor uses: while the value is set, both ITenantContext implementations check it before their own default resolution.

The current ambient tenant if set right now; null otherwise.

public static string? Current { get; }

string?

Sets the ambient tenant for the duration of the scope.

public static IDisposable Begin(string tenantId)

tenantId string

The tenant identifier to use for the duration of the scope.

IDisposable

An object that restores the previous value when IDisposable.Dispose is called. Nested use is safe.

The value is stored in the form AmbientTenantScope.Normalize returns, so AmbientTenantScope.Current is always canonical whatever letter case the caller wrote.

ArgumentException

tenantId is empty or whitespace only.

Returns the canonical form a tenant identifier is compared and stored in.

public static string Normalize(string tenantId)

tenantId string

The tenant identifier as written by the caller.

string

The canonical (invariant lower-case) form.

The tenant identifier is the product’s primary isolation key, and it is matched case-insensitively. Every layer that compares or persists one applies this first: the tenant context, the allow-list check, the endpoint filter, and every store.

Normalizing the value (rather than comparing case-insensitively at query time) is deliberate, and it is the same choice TenantProviderBinding.NormalizeProviderName makes: a bare = predicate then behaves identically on PostgreSQL, SQLite and SQL Server whatever their collation, the primary key keeps rejecting duplicates on all three, and the index stays usable. Without it the authorization layer and the storage layer disagree about whether acme and Acme are one tenant — on SQL Server’s case-insensitive default collation they are one, on PostgreSQL and SQLite they are two.

A consumer that implements ITenantContext or a store itself must apply this to the value it returns, writes and queries with. A store that skips it is case-sensitive while the rest of the system is not.

ArgumentException

tenantId is null, empty or whitespace only.

Returns the canonical form of an optional tenant identifier.

public static string? NormalizeOrNull(string? tenantId)

tenantId string?

The tenant identifier, or null.

string?

The canonical form; null when tenantId is null.

A null tenant means “every tenant” in the query types that accept one, so it passes through unchanged.

ArgumentException

tenantId is empty or whitespace only.