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.