Zonit.Extensions.Tenants 10.0.0-preview.10

This is a prerelease version of Zonit.Extensions.Tenants.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Zonit.Extensions.Tenants --version 10.0.0-preview.10
                    
NuGet\Install-Package Zonit.Extensions.Tenants -Version 10.0.0-preview.10
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Zonit.Extensions.Tenants" Version="10.0.0-preview.10" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Zonit.Extensions.Tenants" Version="10.0.0-preview.10" />
                    
Directory.Packages.props
<PackageReference Include="Zonit.Extensions.Tenants" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Zonit.Extensions.Tenants --version 10.0.0-preview.10
                    
#r "nuget: Zonit.Extensions.Tenants, 10.0.0-preview.10"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Zonit.Extensions.Tenants@10.0.0-preview.10
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Zonit.Extensions.Tenants&version=10.0.0-preview.10&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Zonit.Extensions.Tenants&version=10.0.0-preview.10&prerelease
                    
Install as a Cake Tool

Zonit.Extensions.Tenants

Per-domain site identity ("tenant") plus its persisted, strongly-typed settings. Framework-agnostic: this package has no ASP.NET Core dependency — it references only Microsoft.Extensions.DependencyInjection.Abstractions and Zonit.Extensions. The HTTP glue (TenantMiddleware) lives in Zonit.Extensions.Website, is internal, and is installed by app.UseWebsite<TApp>(...).

NuGet Downloads

dotnet add package Zonit.Extensions.Tenants

What you get

  • ITenantSource (you implement it) — Task<Tenant?> GetByDomainAsync(string domain, CancellationToken ct = default). You receive the raw HttpRequest.Host.Host; you own case folding, aliases, wildcards and all cross-request caching. The framework never compares Tenant.Domain against anything.
  • ITenantRepository (scoped) — Tenant? Current, void Initialize(Tenant?), Task<Tenant?> InitializeAsync(string domain, CancellationToken), event Action? OnChange. Per-scope snapshot; the middleware drives it in web hosts, you drive it everywhere else.
  • ITenantProvider (scoped, read) — Tenant? Current, TenantSettings Settings, TSetting GetSetting<TSetting>() where TSetting : ISetting, new(), event Action? OnChange, event Action<SettingHydrationFailure>? OnSettingHydrationFailed.
  • Tenant — a sealed class (not a record): required Guid Id, required string Domain, FrozenDictionary<string,string> Variables, the sentinel Tenant.Solo (Guid.Empty / "*") and bool IsSolo.
  • Setting<TModel> — abstract base for a setting: Key / Name / Description / Value / Templates plus an AOT-safe TModel Hydrate(string json) you implement.
  • Four built-ins — SiteSetting (site), ThemeSetting (theme), MaintenanceSetting (maintenance), SocialMediaSetting (social_media). MaintenanceSetting stores a flag; nothing in this package acts on it.
  • TenantsJsonContext (public) — the source-generated JsonTypeInfos for the four built-in models. Use it on the write side so the blobs you persist match what Hydrate reads.
  • A Roslyn generator (analyzers/dotnet/cs) that gives every Setting<T> you declare a strongly-typed accessor.

Setup

using Zonit.Extensions;            // AddTenantsExtension() — namespace Zonit.Extensions
using Zonit.Extensions.Tenants;    // ITenantProvider, ITenantSource, Tenant

builder.Services.AddScoped<ITenantSource, MyTenantSource>();

// AddWebsite() already calls AddTenantsExtension(); call it directly only in non-web hosts.
builder.Services.AddTenantsExtension();

AddTenantsExtension() TryAdds exactly two scoped services (ITenantRepository, ITenantProvider) and is idempotent. It registers no ITenantSource — a single-site app leaves that seam empty and the middleware seeds Tenant.Solo; a multi-site app registers its own. If a host resolves the seam directly, use GetService<ITenantSource>(), because GetRequiredService throws when none is registered.

Without middleware (console, worker, tests) drive the repository yourself:

using var scope = provider.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<ITenantRepository>();
await repository.InitializeAsync("acme.example.com");   // or repository.Initialize(Tenant.Solo);

Reading settings

@inject ITenantProvider Tenants

<h1>@Tenants.Settings.Site.Title</h1>
<style>:root { --primary: @Tenants.Settings.Theme.PrimaryColor; }</style>

Settings.Site returns the model (SiteSettingsModel). GetSetting<SiteSetting>() returns the setting — add .Value for the equivalent expression. Both resolve to the same instance, cached per scope and invalidated on OnChange. Treat the models as read-only: mutating one changes what every later reader in the scope sees and persists nothing.

The storage contract

Tenant.Variables maps ISetting.Key → JSON of the model. The shape is whatever the setting's Hydrate deserialises with; for the built-ins that is TenantsJsonContext, which uses camelCase and serialises enums as numbers.

var json = JsonSerializer.Serialize(
    new SiteSettingsModel { Title = "Acme", Language = "en-US" },
    TenantsJsonContext.Default.SiteSettingsModel);
// {"title":"Acme","metaDescription":"This is a new website created","language":"en-US"}

Deserialisation is case-sensitive: a PascalCase blob (what a plain JsonSerializer.Serialize(model) produces) matches nothing, hydrates the compile-time defaults, and raises no exception and no event. Malformed JSON is different — it surfaces on ITenantProvider.OnSettingHydrationFailed as SettingHydrationFailure(Guid TenantId, string SettingKey, Exception Exception). This package has no ILogger dependency, so subscribing and forwarding to your logger is the only way to see it.

Your own setting

namespace Acme.Pricing;

public sealed class PricingModel { public string Plan { get; set; } = "free"; }

public sealed class PricingSetting : Setting<PricingModel>
{
    public override string Key         => "acme_pricing";
    public override string Name        => "Pricing";
    public override string Description => "Billing plan shown on the pricing page.";

    public override PricingModel Hydrate(string json)
        => JsonSerializer.Deserialize(json, PricingJsonContext.Default.PricingModel) ?? new();
}

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(PricingModel))]
public sealed partial class PricingJsonContext : JsonSerializerContext;

The generator emits a TenantSettingsExtensions class into Acme.Pricing, so with using Acme.Pricing; you call Tenants.Settings.Pricing() — an extension method, because a partial class cannot span assemblies. The four built-ins are properties because they are compiled inside this package. GetSetting<PricingSetting>().Value works everywhere and needs no using.

The setting type must be public (non-public types are skipped) and Hydrate must use a source-generated JsonSerializerContext — the reflection overloads break trimming and AOT.

Lifetimes

Service Lifetime Notes
ITenantRepository Scoped Per-scope snapshot, no cross-request cache
ITenantProvider Scoped Caches hydrated settings per scope
ITenantSource yours, Scoped recommended Not registered by the package — put your durable cache here

AOT / trimming

The package is IsTrimmable / IsAotCompatible and contains no [UnconditionalSuppressMessage], no [RequiresUnreferencedCode] and no [DynamicallyAccessedMembers]. Hydration is dispatched to each concrete Setting<T>.Hydrate, so AOT-safety of a custom setting is the author's responsibility.

Known limitations

  • Interactive Blazor circuits have no tenant. The middleware only runs for HTTP request scopes, so Current is null in a circuit and settings render their compile-time defaults after prerender. TenantSnapshot (From / ToTenant) exists as the payload for a prerender→circuit bridge, but that bridge is not implemented yet and nothing in the SDK consumes the type.
  • Colliding accessor names break the build. Two settings in one namespace that reduce to the same accessor (FooSetting and Foo) emit duplicate extension methods → CS0111 in generated code. Rename one, or move it to another namespace.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Zonit.Extensions.Tenants:

Package Downloads
Zonit.Extensions.Website

ASP.NET Core and Blazor web extensions providing base components (PageBase, PageEditBase, PageViewBase), navigation services, breadcrumbs management, toast notifications, cookie handling, and data protection utilities for building modern web applications.

Zonit.Extensions.Tenants.Web

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.0-preview.27 96 8/17/2026
10.0.0-preview.26 184 8/14/2026
10.0.0-preview.25 96 8/14/2026
10.0.0-preview.24 101 8/12/2026
10.0.0-preview.23 88 8/11/2026
10.0.0-preview.22 87 8/11/2026
10.0.0-preview.21 88 8/11/2026
10.0.0-preview.20 84 8/10/2026
10.0.0-preview.19 84 8/10/2026
10.0.0-preview.18 87 8/9/2026
10.0.0-preview.17 91 8/9/2026
10.0.0-preview.16 99 8/9/2026
10.0.0-preview.15 88 8/7/2026
10.0.0-preview.14 83 8/6/2026
10.0.0-preview.13 85 8/6/2026
10.0.0-preview.12 90 8/6/2026
10.0.0-preview.11 108 8/4/2026
10.0.0-preview.10 94 8/3/2026
10.0.0-preview.9 92 5/16/2026
1.0.50 561 11/18/2025
Loading failed