Zonit.Extensions.Organizations
10.0.0-preview.15
See the version list below for details.
dotnet add package Zonit.Extensions.Organizations --version 10.0.0-preview.15
NuGet\Install-Package Zonit.Extensions.Organizations -Version 10.0.0-preview.15
<PackageReference Include="Zonit.Extensions.Organizations" Version="10.0.0-preview.15" />
<PackageVersion Include="Zonit.Extensions.Organizations" Version="10.0.0-preview.15" />
<PackageReference Include="Zonit.Extensions.Organizations" />
paket add Zonit.Extensions.Organizations --version 10.0.0-preview.15
#r "nuget: Zonit.Extensions.Organizations, 10.0.0-preview.15"
#:package Zonit.Extensions.Organizations@10.0.0-preview.15
#addin nuget:?package=Zonit.Extensions.Organizations&version=10.0.0-preview.15&prerelease
#tool nuget:?package=Zonit.Extensions.Organizations&version=10.0.0-preview.15&prerelease
Zonit.Extensions.Organizations
Workspace (organization / tenant) context for Zonit applications: which organization the current user is
working in, and which ones they can switch into. Framework-agnostic — no HttpContext, no middleware, no
Blazor types. The ASP.NET Core middleware and the prerender → circuit bridge ship in
Zonit.Extensions.Website.
dotnet add package Zonit.Extensions.Organizations
What you get
IOrganizationSource— the one contract you implement: load the current workspace, list switchable organizations, perform a switch. Everything else is built on it.IWorkspaceManager— per-scope state machine:Initialize(StateModel),InitializeAsync(ct),SwitchOrganizationAsync(id, ct), plusWorkspace/Organizations/StateandOnChange.IWorkspaceProvider— read surface for UI and domain code: the active organization as theOrganizationvalue object,Organization.Emptywhen none is selected.
AddOrganizationsExtension() registers exactly three scoped services — IWorkspaceManager,
IWorkspaceProvider and a NullOrganizationSource safety net — and nothing else.
Setup
In a Website host, register only your source: AddWebsite() already calls AddOrganizationsExtension()
and UseWebsite<TApp>() installs the middleware.
builder.Services.AddScoped<IOrganizationSource, AcmeOrganizationSource>();
builder.Services.AddWebsite();
Anywhere else (console, worker, tests) wire it yourself and drive the manager once per scope:
services.AddScoped<IOrganizationSource, AcmeOrganizationSource>();
services.AddOrganizationsExtension();
await using var scope = provider.CreateAsyncScope();
var manager = scope.ServiceProvider.GetRequiredService<IWorkspaceManager>();
await manager.InitializeAsync(cancellationToken);
Use
AddScoped, notTryAddScoped. The null source is registered withTryAddScoped; aTryAddScopedof your own after it is a silent no-op and you keep an empty workspace forever.
Implementing the source
public interface IOrganizationSource
{
Task<WorkspaceModel> InitializeAsync(CancellationToken cancellationToken = default);
Task<IReadOnlyCollection<OrganizationModel>?> GetOrganizationsAsync(CancellationToken cancellationToken = default);
Task<WorkspaceModel?> SwitchOrganizationAsync(Guid organizationId, CancellationToken cancellationToken = default);
}
Three things the compiler will not tell you:
InitializeAsyncandGetOrganizationsAsyncare started concurrently on the same scoped instance (Task.WhenAll). InjectIDbContextFactory<T>, not a sharedDbContext— EF Core forbids overlapping operations on one context.nullfromSwitchOrganizationAsyncmeans denied — this method is the authorization check.GetOrganizationsAsyncreturningnullmeans "unknown"; return[]for "member of nothing".OrganizationModel.Idmust be non-empty andNamenon-blank and ≤ 60 graphemes, or the projection to theOrganizationvalue object degrades silently (empty organization / empty or truncated title). It never throws.
In a Website host the whole snapshot — the active workspace and every organization you return — is
serialized into the prerendered HTML, so do not fill invoice fields (Email, TaxIdentification, address)
unless the page needs them.
Reading the workspace
@inject IWorkspaceProvider Workspace
@if (Workspace.Organization.HasValue)
{
<p>@Workspace.Organization.Name — @Workspace.Organization.Id</p>
}
Organization is a readonly struct: test HasValue, never != null. Pages deriving from PageBase /
PageViewBase<T> already expose Workspace as a protected property.
Switching organization
if (!await manager.SwitchOrganizationAsync(organizationId, cancellationToken))
{
// Denied — the previously selected organization is untouched and no OnChange fired.
}
A granted switch replaces the workspace, back-fills the organization list if the scope had none, and
raises OnChange, which IWorkspaceProvider and ExtensionsBase-derived components observe.
Authorization is elsewhere
WorkspaceModel.Permissions / Roles are transported but never read by the framework. Authorization runs
off the Identity produced by Zonit.Extensions.Auth ([RequirePermission], IAuthorizationService,
<AuthorizeView>). Pure tenancy lives here.
Migration to 10.0.0-preview.10
Removed public types — there is no drop-in replacement; the framework only ever needs IOrganizationSource:
| Removed | Notes |
|---|---|
IOrganizationProvider |
GetOrganizationAsync / GetUserOrganizationAsync / GetUserOrganizationsAsync belong in your own data layer |
IOrganizationManager |
single-method (GetAsync) lookup contract, unused by the framework |
IOrganizationEntity |
unused marker; put Guid OrganizationId on your entity and filter with IWorkspaceProvider.Organization |
Zonit.Extensions.Organizations.Entities.Organization |
use the Zonit.Extensions.Organization value object or your own entity |
<ZonitOrganizationsExtension /> |
replaced by <WebsiteHydrator /> (Zonit.Extensions.Website), placed once in App.razor |
Behaviour changes:
IWorkspaceManager.SwitchOrganizationAsyncreturnsTask<bool>instead ofTask. Source-compatible for a plainawait, binary-breaking (recompile), source-breaking for anyone implementing the interface.- A denied switch is now a no-op. It used to clear the workspace, logging the user out of an
organization they did have access to; it now returns
falseand leaves the selection alone. - A switch on a scope that was never initialized is no longer ignored — the source is called, so
SwitchOrganizationAsyncmay arrive without a precedingInitializeAsyncon the same scope. - Malformed source data no longer throws
ArgumentExceptionfromIWorkspaceProvider.Organization:Guid.Empty→Organization.Empty, blank name →Title.Empty, over-long name → truncated at 60 graphemes. ReadIWorkspaceManager.Workspace?.Organizationfor the verbatim values.
Earlier versions also exposed IsPermission(string) / IsRole(string) with hard-coded "Developer" /
"All" bypasses. That is gone; see Zonit.Extensions.Auth.
License
MIT.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Zonit.Extensions (>= 10.0.0-preview.15)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Zonit.Extensions.Organizations:
| 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.Services.Dashboard
Package Description |
|
|
Zonit.Services.Manager
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.0-preview.27 | 46 | 8/17/2026 |
| 10.0.0-preview.26 | 59 | 8/14/2026 |
| 10.0.0-preview.25 | 52 | 8/14/2026 |
| 10.0.0-preview.24 | 49 | 8/12/2026 |
| 10.0.0-preview.23 | 55 | 8/11/2026 |
| 10.0.0-preview.22 | 48 | 8/11/2026 |
| 10.0.0-preview.21 | 54 | 8/11/2026 |
| 10.0.0-preview.20 | 53 | 8/10/2026 |
| 10.0.0-preview.19 | 56 | 8/10/2026 |
| 10.0.0-preview.18 | 55 | 8/9/2026 |
| 10.0.0-preview.17 | 64 | 8/9/2026 |
| 10.0.0-preview.16 | 61 | 8/9/2026 |
| 10.0.0-preview.15 | 59 | 8/7/2026 |
| 10.0.0-preview.14 | 56 | 8/6/2026 |
| 10.0.0-preview.13 | 57 | 8/6/2026 |
| 10.0.0-preview.12 | 46 | 8/6/2026 |
| 10.0.0-preview.11 | 79 | 8/4/2026 |
| 10.0.0-preview.10 | 66 | 8/3/2026 |
| 10.0.0-preview.9 | 71 | 5/16/2026 |
| 0.1.52 | 152 | 1/15/2026 |