Core Concepts
The library uses standard .NET conventions, and most internal details are abstracted away from app code. However, there are a few important specifics to be aware of. The items below make up the foundation of the library.
ITenantInfo and TenantInfo
A TenantInfo instance contains information about a tenant, often the current tenant for an app. The type must implement
ITenantInfo, which defines Id and Identifier. The library provides TenantInfo, a basic implementation that also
includes Name.
When calling AddMultiTenant<TTenantInfo>, the type parameter defines the ITenantInfo implementation used throughout
the library and app.
Idis a unique id for a tenant in your app and should never change.Identifieris the value used to resolve a tenant and should use a syntax appropriate for your app (for example, URL-safe characters when it is part of a web address). UnlikeId,Identifiercan be changed if necessary by updating the tenant with a new instance (see the tenant info contract below).
The library provides TenantInfo as a base implementation. Your app can define a custom class that implements
ITenantInfo or inherits from TenantInfo, adding properties as needed. Keep these
classes lightweight since they are often queried. Keep heavier associated data in an external area that can be pulled in
when needed via the tenant Id.
Previous versions of
TenantInfoincluded a connection string property. If needed simply add it to your customTenantInfoderived class.
MultiTenantContext<TTenantInfo>
The MultiTenantContext<TTenantInfo> contains information about the current tenant.
- Implements
IMultiTenantContextandIMultiTenantContext<TTenantInfo>and can be accessed throughIMultiTenantContextAccessorfrom dependency injection. - Includes
TenantInfo,StrategyInfo, andStoreInfoproperties with details on the current tenant, how it was determined, and from where its information was retrieved. - The
IsResolvedproperty indicates whether a tenant was successfully resolved for the current context. - Can be obtained in ASP.NET Core by calling the
GetMultiTenantContext<TTenantInfo>()method on the current request'sHttpContextobject. See ASP.NET Core Integration for details. - The
HttpContextextension methodSetTenantInfocan be used to manually set the current tenant, but normally the middleware handles this. - A custom implementation can be defined for advanced use cases.
In the original v10 release
TenantInfowas a record andITenantInfowas removed to enforce immutability. This was reverted in 10.0.2 in favor of the interface contract described below, which the library enforces on its side and proves by test for both mutable and immutable implementations.
Tenant info contract
ITenantInfo is the only contract the library depends on. It is read-only from the library's point of view: the
library never writes to a tenant info instance after it has been constructed and never forces a particular shape onto
your type. All of the following are supported and covered by the test suite:
// plain mutable properties
public class AppTenantInfo : ITenantInfo
{
public string Id { get; set; } = string.Empty;
public string Identifier { get; set; } = string.Empty;
public string? Name { get; set; }
}
// init-only properties (the shape of the built-in TenantInfo)
public class AppTenantInfo : TenantInfo
{
public string? ConnectionString { get; init; }
}
// constructor-only, fully immutable, not derived from TenantInfo and not a record
public sealed class AppTenantInfo(string id, string identifier, string? name = null) : ITenantInfo
{
public string Id { get; } = id;
public string Identifier { get; } = identifier;
public string? Name { get; } = name;
}
In return the library asks for two things:
Idnever changes. It is the tenant's identity; tenant equality isIdequality.Identifiermay change, but only through the store. Create a new instance carrying the newIdentifierand pass it to the store'sUpdateAsync; the store re-keys its lookups and hands out the new instance from then on. Never changeIdentifierin place on an instance the library already holds (a store's map, a cache entry, or the current multi-tenant context), because those are keyed by the value captured when the tenant was added or updated and cannot notice the change. WhetherIdentifiermatching is case-sensitive depends on the store:InMemoryStoreandConfigurationStoreare case-insensitive by default, while the EF Core, distributed cache, and HTTP remote stores follow their database, cache, or remote server. See MultiTenant Stores for each store.
Mutating other properties of your own type is your business, but keep in mind the same instance may be shared by caches and by the current request.
The library enforces its side of the contract:
IMultiTenantContextandMultiTenantContext<TTenantInfo>are init-only; the current tenant of a request is changed only by assigning a new context (for example throughHttpContext.SetTenantInfo), never by mutating the current instance.- Stores that must construct tenant instances themselves (
ConfigurationStoreandEchoStore) accept a factory for constructor-only types and throw a clearMultiTenantExceptionwhen a type cannot be constructed. See MultiTenant Stores for details.
MultiTenant Strategies
Responsible for determining and returning a tenant identifier string for the current request.
- Several strategies are provided based on host, route, etc. See MultiTenant Strategies for more information.
- Custom strategies implementing
IMultiTenantStrategycan be used as well.
MultiTenant Stores
Responsible for returning a TenantInfo object based on a tenant string identifier (which is usually provided by a
strategy).
- Has methods for adding, removing, updating, and retrieving
TenantInfoobjects. - Several implementations are provided, including in-memory, configuration, EF Core, distributed cache, HTTP remote, and echo stores. See MultiTenant Stores for more information.
- Custom stores implementing
IMultiTenantStorecan be used as well.
MultiTenantException
Exception type thrown when a serious problem occurs within MultiTenant.
- Usually wraps an underlying exception.