Tharga.Team
3.10.9
dotnet add package Tharga.Team --version 3.10.9
NuGet\Install-Package Tharga.Team -Version 3.10.9
<PackageReference Include="Tharga.Team" Version="3.10.9" />
<PackageVersion Include="Tharga.Team" Version="3.10.9" />
<PackageReference Include="Tharga.Team" />
paket add Tharga.Team --version 3.10.9
#r "nuget: Tharga.Team, 3.10.9"
#:package Tharga.Team@3.10.9
#addin nuget:?package=Tharga.Team&version=3.10.9
#tool nuget:?package=Tharga.Team&version=3.10.9
Tharga Team
Domain models, service abstractions, and authorization primitives for multi-tenant Blazor applications. This package has no server-side dependencies and works with both Blazor Server and Blazor WebAssembly.
What's included
Team and user models
ITeam/ITeam<TMember>- Team aggregate with members.ITeamMember- Team member withAccessLevel, invitation state, tenant roles, and scope overrides.IUser- User identity.Invitation,InviteUserModel,MembershipState.
Service interfaces
ITeamService- the storage contract you implement, not the one you inject. Team CRUD, member management, invitations. Deliberately unchecked, because framework code reads through it while building the very claims that would authorize the read — so it is marked[EditorBrowsable(Never)]and a component, controller or MCP provider must inject a gated facet (ITeamManagementServiceand its siblings) instead. IncludesGetMembersAsync(teamKey)returningIAsyncEnumerable<ITeamMember>for consumers that need to enumerate members without knowing the per-consumerTMembertype.ITeamManagementService- Scope-enforced mutations (create, rename, delete, invite, etc.).IUserService- Current user resolution.IApiKeyAdministrationService/IApiKeyManagementService- API key management.IApiKeyLifecycleHandler- Opt-in hook receiving an API key's private token on create/recycle (and a tokenless delete signal) viaApiKeyLifecycleContext/ApiKeyLifecycleReason. Registered throughThargaTeamOptions.AddApiKeyLifecycleHandler<T>().
Authorization
AccessLevelenum - Owner, Administrator, User, Viewer, Custom.Customgrants no inherited base scopes (effective scopes = roles ∪ scope overrides only) for least-privilege keys/members.Tagrecord - System-set key-value tag on an API key (a list, so a key may repeat). Set at creation only; surfaced as atag.{Key}claim on the authenticated principal.TeamClaimTypes- Claim type constants (TeamKey,AccessLevel,Scope,TagPrefix).IScopeRegistry/ScopeRegistry- Register and resolve scopes per access level.ITenantRoleRegistry/TenantRoleRegistry- Register code roles (global, fixed at deploy time) with associated scopes.ITenantRoleService/TenantRoleService- Team-aware role resolution: merges code roles with a team's runtime-defined custom roles and unions their scopes for a member of a given team. Registered byAddThargaDynamicTenantRoles()(wired wheno.EnableDynamicRoles = true); custom roles are created/edited per team viaITeamService.SetTeamCustomRolesAsync, with scopes constrained to app-registered scopes. The scope required to manage custom roles is configurable viaAddThargaDynamicTenantRoles(o => o.ManageScope = "…")(defaultteam:manage).ITenantRoleVisibilityProvider- Optional per-team hook that decides whether a tenant role is offered in the role editor. Default (AllRolesVisibleTenantRoleVisibilityProvider) shows every role; register your own to hide feature-gated roles from teams where the feature is disabled. Hiding a role never prunes existing assignments and does not affect scope resolution.RequireAccessLevelAttribute/RequireScopeAttribute- Declarative authorization on service methods.TeamScopes/ApiKeyScopes/AuditScopes- Built-in scope constants (audit:readgates the audit log).SystemTeamScopes- Cross-team system scopes:teams:read(enumerate any team),teams:delete(delete any team),teams:assign-owner(give an ownerless team an owner, chosen from its existing members; refused when the team already has one).ISystemScopeRegistry/ISystemRoleRegistry- Global (system) scopes for system API keys, and a mapping of app/global roles (e.g.Developer) to those scopes for privileged users. Configured viao.ConfigureSystemScopes/o.ConfigureSystemRoles.
Base classes
TeamServiceBase- Implement your own team service backend.UserServiceBase- Implement your own user service backend.
What you must override, and what happens if you do not
UserServiceBase leaves persistence to you. Several members are virtual with a do-nothing default,
so forgetting one produces a write that reports success and discards the data — no error, no log, and a
feature that looks configured and is not.
| Member | If you do not override it |
|---|---|
SetUserNameAsync |
Renaming a user reports success and changes nothing |
SeedUserNameAsync |
An invited user's name is discarded when they accept |
SetUserIconReferenceAsync (protected) |
An uploaded icon is stored, its reference discarded, and the blob orphaned |
SetUserDirectoryIdAsync |
The directory link is never persisted, so verification falls back to matching by email |
You are told at startup. AddThargaTeamBlazor reports every un-overridden member in one error,
naming the type and what each silently loses. Set o.Blazor.ThrowOnIncompleteUserService = true to make
it fatal instead. Members whose feature is unreachable — the icon one with no IIconStore registered —
are not reported, so the message stays about real mistakes.
Deriving from
UserServiceRepositoryBase(in Tharga.Team.MongoDB) implements all of these. The gaps only apply to a service extendingUserServiceBasedirectly.
One of them cannot be caught by an interface check. SetUserIconReferenceAsync is protected, so it
does not appear in an interface map — a test asserting "my service implements IUserService" cannot see
it. The startup check reflects over the concrete type and walks the base chain instead, so an override on
your own intermediate base counts.
The user cache
UserServiceBase caches resolved users. Overriding a persistence member replaces the path that
invalidated it, so the toolkit invalidates through a decorator instead — you do not need to call
InvalidateUserCache yourself.
If you see a change that survives every page reload and corrects only on process restart, that is a stale-cache read. Nothing else looks like that; a write that never landed looks identical on screen and has the opposite fix.
Caching and multi-instance deployments
The claims path runs on every authenticating request and performs three lookups: the caller, their
membership in the selected team, and that team's custom roles. All three go through ITeamCache.
The built-in InMemoryTeamCache is registered for you. It holds entries in this process with no expiry,
dropped when a write invalidates them.
If you run more than one instance, replace it
InMemoryTeamCache is correct for a single instance only. A change made through one instance never
reaches the others, so until that instance restarts:
| Changed on instance A | Instance B keeps |
|---|---|
| Member access level, tenant roles, scope overrides | issuing the old claims |
| Member suspended | granting them their full team scopes |
| User disabled | their session alive |
| Team custom roles | the old role-to-scope mapping |
Periodic claim revalidation does not correct this — it recomputes through the same cache, reads the same stale entry, and concludes nothing changed.
Register your own implementation over any store every instance can see:
builder.Services.AddSingleton<ITeamCache, RedisTeamCache>(); // before AddThargaTeam
builder.AddThargaTeam(o => { ... });
The toolkit registers its built-in with TryAdd, so yours wins.
Then forward it from your own service's constructor. Since 3.10.8 the toolkit fails at startup if you register a custom cache your services never received, naming the types — so this step can no longer be missed silently:
public class TeamService : TeamServiceRepositoryBase<TeamEntity, TeamMember>
{
public TeamService(IUserService userService, ITeamRepository<TeamEntity, TeamMember> repository,
IMongoDbServiceFactory factory, IIconStore iconStore = null, ITeamCache cache = null)
: base(userService, repository, factory, iconStore, cache) { } // <- cache
}
A service that does not forward it falls back to the process-local cache, and the table above would apply again — which is why registering a custom cache without forwarding it is a startup failure rather than a warning. The check compares the cache each service actually holds against the registered one, so it cannot misfire; a host that has registered nothing custom never sees it.
Writing an adapter
Foundand the value are separate. Bothnullusers andnullmemberships are cached deliberately — a non-member is remembered as not being one. ReturnCachedValue<T>.Missfor "no entry", not a null value, or every non-member request goes back to the store.- You serialize your own types.
IUserandITeamMemberare interfaces your entities implement, which is precisely why this is your adapter and not something the toolkit can ship. - Returning
Missfrom every read is valid and simply disables caching. Prefer reporting a miss over throwing: an uncached read is slow, a throwing one breaks sign-in. - The two by-user removals need an index.
RemoveUserByKeyAsyncandRemoveMembersForUserAsyncare not keyed the way their entries are, so expect a companion index from a user key to that user's identity and teams.
What is deliberately not cached
The team document. It carries the member roster, and the paths that suspend a member, remove one, assign an owner or transfer ownership read it precisely because they need current state to decide access — a cache there would sit in front of an authorization check. The consent-teams query is also uncached.
Related packages
| Package | Description |
|---|---|
| Tharga.Team.Blazor | Team management Blazor UI components, authentication |
| Tharga.Team.MongoDB | MongoDB persistence for teams and users |
| Tharga.Team.Service | Server-side API key auth, Swagger, audit logging |
| Tharga.Blazor | Generic Blazor UI components (buttons, breadcrumbs, etc.) |
| 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.AspNetCore.Components.Authorization (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
- Tharga.Toolkit (>= 1.16.0)
NuGet packages (5)
Showing the top 5 NuGet packages that depend on Tharga.Team:
| Package | Downloads |
|---|---|
|
Tharga.Team.Service
Server-side API-key authentication, authorization enforcement, controller registration, OpenAPI/Swagger setup, and audit logging for ASP.NET Core projects. |
|
|
Tharga.Team.MongoDB
MontoDB Team features for Tharga Blazor. |
|
|
Tharga.Team.Blazor
Team management Blazor components for multi-tenant applications. Works with both Blazor Server and WebAssembly. |
|
|
Tharga.Team.Entra
Microsoft Entra ID user-directory provider for Tharga Team: verify users against Entra, list directory-only users, and delete users from the directory via Microsoft Graph. |
|
|
Tharga.Team.Images
Image processing for Tharga Team icons — automatic downscaling of uploaded icons via ImageSharp. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.10.9 | 0 | 8/9/2026 |
| 3.10.8 | 0 | 8/9/2026 |
| 3.10.7 | 53 | 8/7/2026 |
| 3.10.6 | 55 | 8/7/2026 |
| 3.10.5 | 152 | 8/4/2026 |
| 3.10.4 | 160 | 8/3/2026 |
| 3.10.3 | 164 | 8/2/2026 |
| 3.10.2 | 154 | 8/2/2026 |
| 3.10.1 | 154 | 8/2/2026 |
| 3.10.0 | 155 | 8/1/2026 |
| 3.9.0 | 152 | 8/1/2026 |
| 3.8.3 | 163 | 8/1/2026 |
| 3.8.2 | 149 | 8/1/2026 |
| 3.8.1 | 162 | 7/31/2026 |
| 3.8.0 | 158 | 7/31/2026 |
| 3.7.0 | 178 | 7/30/2026 |
| 3.6.1 | 153 | 7/27/2026 |
| 3.6.0 | 158 | 7/27/2026 |
| 3.5.4 | 166 | 7/27/2026 |
| 3.5.3 | 157 | 7/27/2026 |