The Shared block carries the typed constants and DTOs every module references — tenant info, the permission registry, claim/action/resource string constants, audit attributes, and the DatabaseOptions consumed by Persistence. It exists so modules can reference one block of shared types instead of either copy-pasting constants or pulling in the heavier Web / Persistence runtime blocks.
What it ships
Multitenancy
AppTenantInfo— extends Finbuckle’sTenantInfo+ implementsIAppTenantInfo. CarriesId,Identifier,Name,ConnectionString,AdminEmail,IsActive,ValidUpto,Issuer,Plan, andQuotaLimits(aDictionary<QuotaResource, long>of per-tenant overrides). Methods:AddValidity(months),SetValidity(DateTime)(forward-only — backdating throws),Activate()/Deactivate()(both throw for the root tenant). New tenants get a 1-month demo validity by default.IAppTenantInfo— contract for tenant info; the kit references the interface where it can, allowing custom subclasses if you ever need them.MultitenancyConstants— well-known values: theRoottenant (Id = "root", name, admin email), thetenantresolution identifier, and the tenant schema name.ITenantInitialPasswordBuffer— singleton interface for buffering the operator-supplied tenant admin password between the create-tenant handler and the background seed step. Implementation lives in the Multitenancy module.
Identity + permissions
PermissionConstants— the central registry. StaticRegister(IEnumerable<FshPermission>)is called by each module duringConfigureServices(duplicates byNameare skipped). Properties:All,Root(whereIsRoot),Admin(everything non-root),Basic(whereIsBasic).FshPermission(Description, Action, Resource, IsBasic = false, IsRoot = false)— immutable record. ComputedNameis the canonical permission string:Permissions.{Resource}.{Action}(e.g.Permissions.Users.Create).PermissionConstants.RequiredPermissionPolicyName— the name of the authorization policy that evaluates.RequirePermission()metadata. The Identity module registers the policy and sets it as bothDefaultPolicyandFallbackPolicy— that double assignment matters (see gotchas).SystemPermissions.All— platform permissions registered byAddHeroPlatformbefore any module runs.RoleConstants— well-known role names (Admin,Basic) plusDefaultRoles/IsDefault(role).ClaimConstants— claim type names (tenant,fullName,permission,image_url,ipAddress,exp, and the impersonation actor claimsact_sub/act_tenant).CustomClaims— legacy alias set covering the same core claim names.ResourceConstants— framework resource names (Tenants,Users,Roles,UserRoles,RoleClaims,AuditTrails,Dashboard,Hangfire). Modules define their own resource strings in their contracts (e.g.Catalog.Products).ActionConstants— action names (View,Search,Create,Update,Delete,Export,Generate,Clean,UpgradeSubscription).
Claims + authorization
ClaimsPrincipalExtensions— extracts user id, tenant, email, etc. from aClaimsPrincipal. Used in middleware and handlers.RequiredPermissionAttribute— implementsIRequiredPermissionMetadata(aHashSet<string> RequiredPermissions). This interface is what the authorization handler keys off — it must exist exactly once, inFSH.Framework.Shared.Identity.Authorization.EndpointExtensions.RequirePermission(permission, params additionalPermissions)— the canonical fluent helper for minimal-API endpoints; it attaches aRequiredPermissionAttributeas endpoint metadata. Permissions are plain strings (modules expose them asconst strings in their contracts).
Persistence shared
DatabaseOptions— defined here, consumed by the Persistence block.Provider(string),ConnectionString(string, validated required),MigrationsAssembly(string).DbProviders.PostgreSQL+DbProviders.MSSQL— provider name constants.
Auditing markers
AuditAttributes—[AuditIgnore](exclude a property from audit diffs/payloads) and[AuditSensitive(hash, redact)](mask or hash the value when serialized). The Auditing module reads these when capturing changes.HttpContextItemKeys— well-knownHttpContext.Itemskeys building-block middleware uses to signal the audit pipeline without depending on it. Currently:QuotaRejected(set by the quota middleware on a 429).
Storage + quota DTOs
FileUploadRequest,PresignedUploadUrl,StoredObjectMetadata— the storage transfer types shared between the Storage block and the Files module.QuotaResource— enum (ApiCalls,StorageBytes,Users,ActiveFeatureFlags). New quota dimensions get added here.
How modules consume Shared
A typical module declares string constants for endpoint wiring plus an FshPermission list for the registry — this is Catalog’s real shape:
public static class CatalogPermissions{ public static class Products { public const string Resource = "Catalog.Products"; public const string View = $"Permissions.{Resource}.View"; public const string Create = $"Permissions.{Resource}.Create"; // ... }
public static IReadOnlyList<FshPermission> All { get; } = [ new("View Products", ActionConstants.View, Products.Resource, IsBasic: true), // ... ];}
// CatalogModule.ConfigureServicesPermissionConstants.Register(CatalogPermissions.All);Endpoints reference the string constants via the fluent helper:
endpoints.MapPost("/products", handler) .RequirePermission(CatalogPermissions.Products.Create);How to extend
Add a new resource
Add a constant to ResourceConstants, define a FshPermission set for the resource in your module’s Contracts.Authorization, register it during module startup. Done.
Add a new well-known claim
Add a name to ClaimConstants (or CustomClaims for kit-specific values). Update TokenService to issue the claim, update ClaimsPrincipalExtensions to read it. The Identity module is the place for both changes.
Add a new quota dimension
Extend QuotaResource with the new value, expose a gauge provider (see Quota), update plan config to set defaults.
Gotchas
PermissionConstants.Registerdedupes byName. Calling it twice with the same permission is a no-op. There is no way to remove a permission — once registered, it stays for the process lifetime.IRequiredPermissionMetadatamust never be duplicated. The authorization handler discovers permissions via this interface; a copy-pasted duplicate in another namespace means endpoint metadata implements the wrong interface and every.RequirePermission()gate silently stops enforcing.- The permission policy must stay both DefaultPolicy AND FallbackPolicy. A group-level
.RequireAuthorization()attaches the default policy, which suppresses the fallback — if the default were only “authenticated”,.RequirePermission()would silently fail open (this was a real bug, fixed in PR #1290). The Identity module assigns theRequiredPermissionpolicy to both; don’t undo that. AppTenantInfo.SetValidityis forward-only. It throws “Subscription cannot be backdated” — for initial/explicit/backdated sets, assignValidUptodirectly.AppTenantInfo.Planis nullable. When null, plan resolution falls back toQuotaOptions:DefaultPlan(freeby default). Set this explicitly on every tenant unless you want the default.AppTenantInfo.QuotaLimitsis a per-tenant override. If a resource is present in the dict, it wins over the plan’s limit. Don’t sprinkle overrides; reserve them for negotiated contracts.- Permissions are strings. No compile-time safety on typos. Reference the module’s constants — never write
"Permissions.X.Y"literals in handlers or endpoints.
Critical files
src/BuildingBlocks/Shared/Multitenancy/AppTenantInfo.cssrc/BuildingBlocks/Shared/Identity/PermissionConstants.cs(also home of theFshPermissionrecord)src/BuildingBlocks/Shared/Identity/SystemPermissions.cssrc/BuildingBlocks/Shared/Identity/Authorization/RequiredPermissionAttribute.cs(andIRequiredPermissionMetadata)src/BuildingBlocks/Shared/Auditing/AuditAttributes.cs
Related
- Core — even more foundational.
- Web —
.RequirePermission()applied to endpoints. - Quota —
QuotaResourceenum lives here, the enforcement runs there. - Identity module — registers
IdentityPermissions.AllagainstPermissionConstants.