Skip to main content

Design choices and where to see them

Each choice that the Tenancy pages describe, with three pointers: the code, something to try on the running sample, and a test that fails when the choice is undone.

  • Try it names one of these:

    • a preset of the UI's Try it page, by its id in DevAttempts.cs;
    • a UI page;
    • a request in the host's .http file, marked (.http) and listed under the person who sends it;
    • a command.

    Try it explains how to start the sample.

  • Test names a test class under Tests/. The sample's tests are in Tests/Examples.Tenancy.Tests. A test that needs the host with a database takes it from the fixture SampleHosts, and needs Docker.

Access rules​

The choiceWhere to see it
Every request declares who may send it, with one attribute. A Tenancy manager checks again, with rules that only the manager can read.Code: AccessRequirement.​cs, RequiresPermissionAttribute.​cs, AtUnitAttribute.​cs, MakeGrant.​cs, SeatGrants.​cs
Try it: Presets grant-outside-your-region (the request's requirement refuses) and give-a-role-that-manages-access (the manager refuses)
Test: AccessDeclarationTests, RequestPipelineTests, CurrentUserRequirementTests
The owner holds permissions by owning the project, with no role. A crew role gives at most what a crew can give.Code: Project.​cs, ProjectPermissions.​cs, ProjectMembership.​cs
Try it: Presets owner-change-from-the-crew, remove-the-owner, give-the-owner-a-role
Test: OwnerScenarios, CrewRoleScenarios, ProjectMembershipTests
The permission that closes a project also reopens it.Code: ReopenProject.​cs
Try it: The Reopen button. Leo closes and reopens Pier 7 (.​http).
Test: ClosingAndReopeningScenarios
A seat that manages a crew can give any project role in use to anyone on the crew, itself included. The seat does not need to hold the role's permissions.Code: GiveCrewRole.​cs
Try it: Leo gives Vic Surveyor (.​http). Preset crew-role-without-crew-management.
Test: CrewRoleScenarios
A seat that manages grants at a unit can give an organization role there. For a role that manages access, the giver must also hold the role's access-managing permissions at that unit, for at least as long. Nobody can give such a role to themselves.Code: TenancyGate.​cs, SampleCatalogue.​cs
Try it: Presets give-a-role-that-manages-access, appoint-yourself, appoint-yourself-holding-its-permission
Test: PeopleOfficeScenarios, SeatManagerTests
The same rule applies to changing a seat's status and to moving a unit.Code: SeatManager.​cs, OrganizationManager.​cs
Try it: Not in the seed data
Test: SeatManagerTests, OrganizationManagerTests, ContainmentAndLastAdminScenarios
SeatManager.​OverviewAsync, which reads another seat's overview, requires no permission of its own. The request declares who may send it, and the read rules decide which rows come back.Code: SeatManager.​cs, SeatGrants.​cs
Try it: Maud's and Leo's seatGrants at /admin/graphql (.​http)
Test: DirectoryTests, ReadRulesTests, AdministrationSchemaScenarios
Containment is a catalogue setting. It is on by default, and on in the sample. The managers and the database both apply it.Code: ApplicationCatalogue.​cs, SampleCatalogue.​cs
Try it: Every preset that returns tenancy.​grant-exceeds-own or tenancy.​self-appointment
Test: ContainmentTests, StartUpCheckTests, ApplicationRuleScenarios, TenancyChoicesExportTests (the export with containment off)
The database functions get the list of access-managing permissions from the host's catalogue.Code: TenancyRowAccessContribution.​cs, SampleCatalogue.​cs, Examples/Tenancy/supabase/migrations/*_access.​tenants.​ddd.​sql
Try it: The host starts only when the database and the catalogue agree.
Test: ContributionTests, StartupTests, SampleOnPostgresTests
An administrators' starter role may list its permissions, so that administrators run access and do none of the work.Code: TenancyCatalogue.​cs, SampleCatalogue.​cs
Try it: Preset rename-as-access-admin, as Maud
Test: CatalogueTests, AccessAdminScenarios
Without a declared administrators' starter role, Tenancy adds its own, with every live permission, for every shape. With declared ones, there is one per shape.Code: TenancyStarterRoles.​cs, TenancyCatalogue.​cs
Try it: Not in the sample. The publishing house in the package check declares none (Tenants.​cs).
Test: CatalogueTests, TenantManagerTests, ProvisioningTests, and build/verify-package-consumption.sh
When the host syncs starter roles with SyncStarterRoles(), the roles copied from a starter role follow its changes. What the tenant changed itself stays.Code: RoleAggregate.​cs, RoleManager.​cs, EfStarterRoleSync.​cs, Program.​cs
Try it: Use the CLI's stack, because the AppHost's database is new on each run. Run the host, add a permission to a starter role, build the exporter, run supabase migration up, and run the host again. The history shows tenancy.​role-followed-its-starter-role.
Test: RoleFollowsItsStarterRoleTests, FollowStarterRolesTests, StarterRoleSyncTests, StarterRoleSyncScenarios
A crew member can hold several dated roles. These are the tenant's project roles, made from starter roles when the tenant is provisioned.Code: CrewMember.​cs, ProjectRole.​cs, ProjectMembership.​cs, ProjectsInfrastructure.​cs
Try it: The Crew roles page. Presets organization-role-on-a-crew, crew-role-held-twice.
Test: CrewMembershipScenarios, ProjectRoleScenarios, ProjectCrewTests
When a seat gives up its own place on a crew, the crew manager saves the change as the module's work, for that request and project only. The database checks each row as it is after the change, when the seat no longer has the place.Code: TakeCrewRole.​cs, MemberManager.​cs
Try it: Leo gives Vic Crew lead on Pier 7 in the UI. Vic then takes the role from himself.
Test: CrewRoleScenarios, AccessHoldScenarios

Structure​

The choiceWhere to see it
Each module has one project per layer, and ports to its storage. The module's entry point is in the API project, the only project that the host references.Code: Modules, ProjectsModule.​cs, IProjectStore.​cs
Try it: dotnet run --project Examples/Tenancy/Examples.​Tenancy.​AppHost
Test: LayerReferenceTests
A module declares its permissions once, in a [Permissions] class. Generators give the host and the exporter the permissions of every module.Code: ProjectPermissions.​cs, InspectionPermissions.​cs, Program.​cs, SampleCatalogue.​cs, TenancyPermissionsGenerator.​cs
Try it: Build with -p:EmitCompilerGeneratedFiles=true, and look under obj for DeclaredPermissions.​g.​cs, TenancyPermissionsOfModules.​g.​cs and DTS.​DDD.​RowAccessContributionsOfPackages.​g.​cs.
Test: ModulePermissionsTests, TenancyPermissionsGeneratorTests, PackageContributionsGeneratorTests, StartupTests
No project writes out Tenancy's nine type arguments. Code takes the managers and the access manager as classes the generator writes, such as SeatManager and TenancyAccessManager.Code: TemplateFacades.​cs, AssemblyInfo.​cs, PermissionOfMineType.​cs
Try it: Build with -p:EmitCompilerGeneratedFiles=true, and find SeatManager.​TemplateFacade.​g.​cs and the other managers' files in the obj folder of the Tenants domain project.
Test: TemplateFacadeTests, SourceTreeTests
Code that sees the Tenants classes never writes out Tenancy's id types. This holds for starting system work, reading the caller, registering, and selecting the tenant.Code: TenancyAccessManager.​cs, ITenantSelection.​cs, DemoSeeder.​cs, TenantHeader.​cs, TenantsInfrastructure.​cs, ProjectsInfrastructure.​cs
Try it: Nothing to run. The seeding works this way at every start.
Test: SourceTreeTests, TenancyClosedOverTheIdsTests, ClosedOverTheIdsTests
A class to which the application adds nothing is not hand-written. The generator writes the organization and role classes. The application declares the tenant, unit, seat and invitation classes.Code: Module.​cs, GenerateTenancyClassesAttribute.​cs, TemplateDefaults.​cs
Try it: Build with -p:EmitCompilerGeneratedFiles=true, and find Organization.​TemplateDefault.​*.​g.​cs and Role.​TemplateDefault.​*.​g.​cs.
Test: SourceTreeTests, MigrationTests, TemplateDefaultsTests, ProvisioningTests
Each feature is one command or query. It is sent through the mediator, and a behavior generated from the module's request interface checks it.Code: CloseProject.​cs, IProjectsRequest.​cs, MemberAccessCheck.​cs
Try it: Any route. Preset close-as-observer.
Test: AccessDeclarationTests, RequestPipelineTests
Folders are by feature first and by kind second. Each feature folder has Commands and Queries. Each feature folder of the API project has Rest and GraphQL.Code: AllCrewMembers.​cs, CrewEndpoints.​cs, CrewMutations.​cs
Try it: Juno reads Pier 7's crew (.​http).
Test: FeatureFolderTests, AllCrewMembersScenarios
Folders are deep. In the sample, each namespace matches its folder.Code: Aggregates/Projects, Aggregates
Try it: Folders inside the layers shows the tree.
Test: SourceTreeTests, StoredNameTests
The sample's namespace, Examples.​Tenancy, is outside the toolkit's namespace.Code: Directory.​Build.​props
Try it: The command above
Test: SourceTreeTests, LayerReferenceTests
Types that modules share and no module owns, such as DateRange, have their own project.Code: DateRange.​cs, ProjectType.​cs, InspectionType.​cs
Try it: Preset inspection-outside-the-planned-range. planned { from until } in schema.​graphql.
Test: DateRangeTests, PlannedRangeScenarios, GraphQLMutationScenarios, GraphQLSchemaTests
A program without routes may reference a module's infrastructure project. The exporter does this.Code: Examples.​Tenancy.​Exporter
Try it: dotnet build Examples/Tenancy/Examples.​Tenancy.​Exporter writes Examples/Tenancy/supabase/migrations.
Test: PostgresCompositionTests
A module's migrations are beside its context. One factory, [SupabaseMigrations], serves dotnet ef, the export and the start-up check.Code: Migrations, TenantsContext.​cs, Program.​cs, PostgresPools.​cs
Try it: Run dotnet ef migrations add, then build the exporter. The context's remarks list the steps.
Test: MigrationTests, PostgresCompositionTests
A read takes its own context for its one query. Every context comes from a pool.Code: PooledContexts.​cs, PostgresPools.​cs
Try it: Run all presets on the Try it page.
Test: PooledContextScenarios, PooledContextSaveTests
Tests run on Supabase's images, from one fixture, on a database that is migrated and seeded once per run. No test moves a clock.Code: SampleHosts.​cs, SampleOnPostgres.​cs, SampleSupabaseStack.​cs
Try it: DDDTOOLKIT_REQUIRE_CONTAINERS=1 dotnet test Tests/Examples.​Tenancy.​Tests --filter "Sample=Tenancy.​Supabase"
Test: ContainerTraitTests, SeededOnceTests, CrewMembershipOverTimeScenarios
A project's longest build path is at most 200 characters below the repository root. Visual Studio refuses paths over 259 characters. The command line accepts them.Code: SourceTreeTests.​cs
Try it: Nothing to run
Test: SourceTreeTests

Data and names​

The choiceWhere to see it
No module reads Tenancy's tables. A module maps the read model, which is a set of Tenancy's database functions.Code: ProjectsContext.​cs, TenancyModelBuilderExtensions.​cs
Try it: Nothing to see in the UI. A project list calls the functions inside its SQL statement.
Test: ModuleModelTests, ReadFunctionTests, MigrationTests, AccessStatementTests
Permission sets decide which buttons a screen shows. A command checks the permission again when it runs.Code: PermissionsOnProjects.​cs, ProjectAbilities.​cs, ITenancyQueries.​cs
Try it: Rhea's two /access requests (.​http). The actions on a project page.
Test: PermissionSetScenarios, PermissionSetQuestionTests
A unit's kind belongs to the application. It is an enum on the application's unit class, set in the callback of AddUnitAsync.Code: OrganizationUnit.​cs, AddOrganizationUnit.​cs, OrganizationUnitType.​cs
Try it: GET /tenancy/units. organizationUnits { name kind }.
Test: FlatAndHierarchicalScenarios, GraphQLMutationScenarios, OrganizationManagerTests, DirectoryTests, AccessStatementTests, MigrationTests
A seat's name belongs to the application, with the application's rule and command. Tenancy stores no name.Code: Seat.​cs, SeatType.​cs, RenameSeat.​cs, AcceptInvitation.​cs, DemoSeeder.​cs
Try it: Juno accepts with her name and renames her seat (.​http). seatRename.
Test: SeatNameScenarios, InvitationScenarios, SeatManagerTests, DirectoryTests, MigrationTests
The read model has no names. Names come from the sample's directory routes, by id.Code: ReadModel, DirectoryEndpoints.​cs, DirectoryNames.​cs
Try it: The directory calls on the Try it page
Test: StrictAnswersTests, DirectoryScenarios, DirectoryNamesTests, ReadModelTests
Access is checked live, and a name is read from the module that owns it. No module copies another module's names from events.Code: DirectoryQueries.​cs, ReferencedSeat.​cs
Try it: "A project with names" (.​http)
Test: GraphQLLookupScenarios, DirectoryLookupScenarios
The sample keeps Entity Framework's default names. Snake case is a choice each host makes.Code: TenancyTableNames.​cs
Try it: Naming tables and columns. Examples/Tenancy/supabase/migrations.
Test: TenancyNamingTests, MigrationTests
When the database refuses a write, the response carries the refusal's code.Code: ProjectsContext.​cs, TenancyMapping.​cs
Try it: Not by hand. It needs two commands at the same moment.
Test: DatabaseRefusalScenarios, IndexRefusalTests

Database security​

The choiceWhere to see it
A seat reads only its own rights. The database maintains them.Code: TenancySql.​cs, TenantsInfrastructure.​cs
Try it: Start the sample. It runs no other way.
Test: RightsVisibilityTests, DatabaseKeepsRightsTests, SampleOnPostgresTests
A seat with tenancy.​grants.​manage, tenancy.​seats.​manage or tenancy.​units.​manage reads another seat's grants at the permission's unit and below. A seat with tenancy.​roles.​manage for the whole tenant reads all grants (Who reads which grants).Code: TenancySql.​cs, TenancyQueries.​cs
Try it: Not in the seed data. Rhea is refused by the request's requirement.
Test: RightsVisibilityTests, CrossSeatQuestionTests, AdministrationSchemaScenarios
A read across tenants goes through a database function. It never bypasses the policies.Code: TenancySql.​cs, TenancySystemReads.​cs
Try it: Not in the sample, which has no stored files or channels
Test: SystemReadFunctionTests, TenantArgumentFunctionTests
A token's role reaches the database only as a mapped role. A mapped role is shut out of every tenant, except an operator's role, which may only read.Code: SampleStorage.​cs, TenancyPostgresChecks.​cs
Try it: Orla's requests (.​http)
Test: TokenRoleTests, OperatorPolicyTests, TokenRolePostgresTests
The login role owns nothing. Every table forces its policies. Privileges follow the policies. Rows are only ever added to the event log.Code: Examples/Tenancy/supabase/migrations/*_login_role.​tenancy_api.​ddd.​sql, Examples.​Tenancy.​Exporter.​csproj, Program.​cs
Try it: On the CLI's stack
Test: SampleOnPostgresTests, SampleWithoutDatabaseTests, LoginRoleFileTests, LoginThatOwnsNothingTests, ForcedRowLevelSecurityTests, EventLogGuardTests
On Postgres, the unique index on a tenant's root unit is required.Code: TenancyPostgresChecks.​cs, TenantsContext.​cs
Try it: The host starts only when the check passes.
Test: RootIndexCheckTests
In the database, a project's unit, owner, crew, name, plan and state change only with the permission of the matching command. A trigger, the Membership lock and column rules enforce this.Code: UnitChangesWithItsPermissions.​cs, NameAndPlanChangeWithTheEditPermission.​cs, StateChangesWithTheClosePermission.​cs, SampleCatalogue.​cs, SeatsChangeTheProjectsTheyWorkOn.​cs, Program.​cs
Try it: Not through the application (Rules checked only in C#)
Test: SampleOnPostgresTests, MovingScenarios, OwnerScenarios
Who reads other seats, and who reads invitations, are defaults. A read rule on the application's class replaces a default. The sample keeps both defaults.Code: TenancySql.​cs, ContributedDefault.​cs, TenancyRowAccess.​cs
Try it: Not in the sample (Default read policies for seats and invitations)
Test: ReadRulesTests, RowAccessContributionTests, TenancyReadRuleGeneratorTests, TenancyChoicesExportTests (the export with a read rule on the seat class)
Tenancy protects its own seat columns. The application protects its own columns. In the sample, a column rule protects the name.Code: TenancySql.​cs, NameChangesByTheSeatOrWithTheSeatsPermission.​cs, RenameSeat.​cs
Try it: Not through the application (Who may change a seat's columns)
Test: SeatColumnsTests, TenancyTriggerTests, SampleOnPostgresTests
A row rule of one module checks another module's projects by id, which is all Projects' contracts publish of them. The rule names no SQL function.Code: ProjectId.​cs, SeatsSeeTheInspectionsOfProjectsTheySee.​cs, SeatsRecordWhereTheyMay.​cs, ProjectMembership.​cs
Try it: The newest Examples/Tenancy/supabase/migrations/*_access.​inspections.​ddd.​sql calls "projects".​projects_where_i_hold (A resource's access, asked by its id).
Test: ProjectRowRulesTests, InspectionRowRulesTests, SampleOnPostgresTests
One call, UseDDDToolkit, sets up every context. A tenant-scoped context without the save check is refused at its first save.Code: ProjectsInfrastructure.​cs, SampleStorage.​cs, DependencyInjection.​cs
Try it: The host's log lists each context once, with what it was given.
Test: StartupTests, UseDDDToolkitTests, OneCallTests, RequiredSaveCheckTests
Connections are budgeted per purpose. One budget is for requests, one for background work.Code: PostgresPools.​cs, ContextsByPurpose.​cs
Try it: Sample:Pools:Requests, Sample:Pools:Background
Test: SampleOnPostgresTests
The application's clock and the database's clock must agree. The policies allow no difference.Code: Tenancy's SQL functions states the rule. The packages contain no code for it.
Try it: Nothing to try
Test: DatabaseClock, the clock that the package's Postgres tests run with
Besides the dev login, the host accepts both kinds of Supabase Auth token. It checks them with the JWT secret or with a key that Auth publishes.Code: SupabaseTokenHandler.​cs, SampleAuthentication.​cs, SupabaseLoginClient.​cs
Try it: The password form on the login page, as rhea@example.​test
Test: SampleOnSupabaseTests, PublishedKeyTokenTests, SupabaseTokenHandlerTests
The sample is a Supabase project that the Supabase CLI can read.Code: Examples/Tenancy/supabase/config.​toml, Examples/Tenancy/supabase/migrations
Try it: On the CLI's stack
Test: SampleOnTheCliStackTests, skipped where the CLI's stack is not running

REST​

The choiceWhere to see it
The seat check is a host authorization policy. It runs before a route reads its arguments.Code: SamplePolicies.​cs, SeatRequirementHandler.​cs
Try it: Preset other-tenants-slug. Rhea's body that cannot be bound, sent to meadow (.​http).
Test: SeatPolicyScenarios
Enum values travel as names in lower snake case. The host sets this once.Code: Program.​cs, SampleGraphQL.​cs
Try it: Any response, for example "state": "open"
Test: EnumSpellingScenarios
A change may name the version that the client read. A stale version returns 409. The version is compared only after the access check.Code: MemberAccessCheck.​cs, ProjectVersions.​cs, EfProjectStore.​cs
Try it: Leo renames Pier 7 with If-Match, then with the old version (.​http).
Test: VersionScenarios, AccessHoldScenarios
A handler uses nothing from its access check. The sample leaves out the expert hold.Code: ChangeProjectName.​cs, MemberHoldInterceptor.​cs
Try it: Not by hand. The tests play each race.
Test: AccessHoldScenarios, RequestPipelineTests, HoldsCheckedAtSaveTests
Lists page by cursor, and return a count when asked. Filters only narrow a list. A cursor from another list is refused.Code: VisibleProjects.​cs, EfProjectReads.​cs, ListCursors.​cs
Try it: Rhea's GET /projects?size=1&count=true, and the next page (.​http)
Test: ProjectListScenarios, InspectionListScenarios, ListCursorsTests, AccessStatementTests
A browser on another origin is let in only when the host lists that origin.Code: BrowserCors.​cs
Try it: Sample:Cors:Origins. The fetch example on the samples' page.
Test: CorsTests

GraphQL​

The choiceWhere to see it
REST routes and GraphQL fields send the same commands and queries.Code: CrewMutations.​cs, CrewEndpoints.​cs
Try it: The GraphQL part of the .​http file
Test: FeatureFolderTests, GraphQLMutationScenarios
A Tenants command returns the aggregate its manager made or changed. The mutation and the route return it without reading again.Code: SuspendTenantSeat.​cs, SeatsMutations.​cs, SeatsEndpoints.​cs, AcceptInvitation.​cs
Try it: Juno accepts and renames her seat (.​http). The rename answers her seat.
Test: GraphQLMutationScenarios, SeatNameScenarios, InvitationScenarios
Each module has a source schema, and the host composes them in the process. A module refers to another module's entity by id.Code: ProjectsGraphQL.​cs, ReferencedSeat.​cs, schema.​graphql
Try it: "A project with names" (.​http)
Test: GraphQLSchemaTests, GraphQLLookupScenarios
A lookup is in the composed schema only when a client needs it.Code: DirectoryQueries.​cs, OverviewQueries.​cs
Try it: schema.​graphql has project(id:), and no lookup for a seat, unit or role.
Test: GraphQLSchemaTests, DirectoryLookupScenarios
A reference that its owner does not resolve returns its key, with every other field null and no error.Code: EntityFieldsNullableInterceptor.​cs, SeatType.​cs
Try it: Not in the seed data
Test: GraphQLLookupScenarios, EntityNullabilityTests
An operation is a static method marked [Query] or [Mutation].Code: LifecycleMutations.​cs, Module.​cs
Try it: The rename (.​http)
Test: OperationDeclarationTests
Routes and fields both use HotChocolate's paging. A page starts from one end, and its size stays within the list's range.Code: OverviewPagedQueries.​cs, EfInspectionReads.​cs, PageSizes.​cs
Try it: projects(first: 2), and the refused projects(first: 2, last: 2) (.​http)
Test: GraphQLProjectScenarios, InspectionListScenarios, ProjectListScenarios, PageSizesTests, IdsAsPagingKeysTests
A type is the application's record. Any further read is a resolver behind a data loader.Code: ProjectType.​cs, OverviewDataLoaders.​cs, AccessHistoryEntryType.​cs
Try it: "A project with names". The crews are one read.
Test: GraphQLDeclarationTests, GraphQLProjectScenarios
A field may require a permission. The query enforces the same rule where it reads the data.Code: CrewOverviews.​cs, RoleListing.​cs, CrewMemberType.​cs, CrewFieldPermissions.​cs, PermissionAuthorizationHandler.​cs
Try it: Juno's crew, and Leo's { roles { name permissions } } (.​http)
Test: CrewRoleScenarios, RolePermissionsScenarios, GraphQLProjectScenarios, PermissionAuthorizationTests
The gateway resolves relations across modules. A batch of projects costs the other module one SQL statement.Code: ProjectType.​cs, RecordingDataLoaders.​cs, IProjectGate.​cs
Try it: Juno's projects with their inspections (.​http)
Test: InspectionsOfProjectsScenarios, InspectionsSchemaTests
The administration gateway, /admin/graphql, adds another person's roles.Code: SeatsAdminQueries.​cs, TenantsGraphQL.​cs, SampleGateways.​cs, Program.​cs, admin.​graphql
Try it: Maud's and Leo's seatGrants (.​http)
Test: AdministrationSchemaScenarios, GraphQLSchemaTests, OneSchemaPerClassTests
A gateway requires what the routes require. A tool reads the schema with a key.Code: Program.​cs, SampleGateways.​cs
Try it: "The schema, as a tool reads it" (.​http)
Test: GraphQLSchemaKeyTests, GraphQLSeatGateScenarios, SchemaKeyTests
A project is a node. Mutations return typed errors. The seat is checked before every root field.Code: SampleGraphQL.​cs, SeatGate.​cs
Try it: The rename with expectedVersion. Rhea's query in meadow (.​http).
Test: GraphQLMutationScenarios, GraphQLSeatGateScenarios
A staff field requires an operator. It is in its module's Operators feature.Code: SeatGate.​cs, OperatorsQueries.​cs, TenantProjectType.​cs
Try it: Orla's four requests (.​http)
Test: GraphQLSeatGateScenarios, OperatorFieldScenarios, TenantProjectsFieldScenarios, TenantProjectInspectionsFieldScenarios
A page fits within the cost limit of a request. The gateway limits the depth and the number of fields. A user's SQL statement times out.Code: OverviewPagedQueries.​cs, InspectionsConnection.​cs, SampleGraphQL.​cs, SampleStorage.​cs
Try it: The refused request that goes round three times (.​http)
Test: GraphQLProjectScenarios, GraphQLDeclarationTests, SampleOnPostgresTests

Operators and history​

The choiceWhere to see it
An operator has no seat. An operator reads across tenants, through a database role of its own. An operator changes a tenant only through a command marked [Operator], which runs as operator work in the tenant that the command names.Code: TenantManager.​cs, OperatorAttribute.​cs, AllTenants.​cs, RenameTenantProject.​cs, OperatorRequirement.​cs, OperatorsSeeEveryProject.​cs, OperatorsQueries.​cs
Try it: Orla's requests (.​http). The .​http file has no operator's command. The UI has no operator page.
Test: OperatorScenarios, OperatorFieldScenarios, OperatorTests, OperatorPolicyTests
Who acted is reported in one form everywhere. It is kept apart from the toolkit's current user.Code: TenancyActor.​cs, TenancyAttribution.​cs, ChangedBy.​cs
Try it: "Last changed by" on a project's page
Test: WhoChangedScenarios, ActorTests, AttributionTests, AttributionTriggerTests
The access history is the event log. Rows are only ever added to it. Reading it needs a permission of its own.Code: TenancyEventLogExtensions.​cs, AccessHistory.​cs, HistoryEndpoints.​cs, HistoryPagedQueries.​cs
Try it: The Access history page. Presets history-with-the-permission, history-without-the-permission.
Test: AccessHistoryScenarios, AccessHistoryFieldScenarios, AccessHistoryTests, AccessHistoryPolicyTests

Invitations​

The choiceWhere to see it
Accepting creates the seat, its placement and its grant in one save. Issuing creates only the invitation.Code: InvitationAggregate.​cs, InvitationManager.​cs, InvitePerson.​cs, InvitationsMutations.​cs
Try it: The Invitations pages. Tove's requests (.​http).
Test: InvitationScenarios, InvitationFieldScenarios, InvitationTests, InvitationStoreTests
An invitation never gives more than its issuer could give at the moment it is accepted.Code: InvitationManager.​cs
Try it: Preset invite-without-seat-management, which shows the issuing half
Test: InvitationTests, InvitationPolicyTests
Accepting needs the token and the caller's verified identity. Nobody is looked up by address. A verified address only narrows who may accept.Code: AcceptInvitation.​cs
Try it: Preset accept-an-invitation-nobody-sent
Test: InvitationScenarios, InvitationTests
A token is shown once. The database keeps only its digest, which no caller's role can read.Code: BearerTokens.​cs, TenancyInvitationDigest.​cs
Try it: The Invitations page shows the token once.
Test: BearerTokensTests, InvitationPolicyTests
An invitation offers one role to one address. The invitation clears the address once it is accepted or cancelled.Code: InvitationAggregate.​cs
Try it: The form on the Invitations page
Test: InvitationTests, ModelTests
An invitation is open, accepted or cancelled. Whether it has lapsed is computed from the clock.Code: InvitationState.​cs
Try it: Tove revokes her second invitation (.​http).
Test: InvitationOverTimeScenarios, InvitationTests
Listing and cancelling an invitation need tenancy.​seats.​manage at the invitation's unit.Code: OpenInvitations.​cs, CancelInvitation.​cs
Try it: The Invitations page
Test: InvitationTests, InvitationPolicyTests
When two acceptances run at the same time, the second one loses the race.Code: InvitationManager.​cs
Try it: Not in the sample
Test: InvitationStoreTests
What an invitation offers cannot change after it is issued. On Postgres, a trigger enforces this.Code: TenancyMapping.​cs, TenancySql.​cs
Try it: Nothing to try
Test: InvitationStoreTests, InvitationPolicyTests
A host may leave invitations out.Code: TenantsContext.​cs, Invitation.​cs
Try it: A host without invitations has neither of the two invitation pages.
Test: ModelTests, TemplateWiringTests
The invited account comes through a port. An existing account gets no mail, and the inviter cannot tell the difference. The invitation keeps the account it created.Code: IIdentityAccounts.​cs, SupabaseIdentityAccounts.​cs, SampleIdentityAccounts.​cs, Invitation.​cs, InvitePerson.​cs, CancelInvitation.​cs
Try it: Tove invites Juno, and invites Wren twice (.​http). The mails are in mail.
Test: InvitationScenarios, InvitationWithSupabaseAuthTests, InvitedByMailTests, SupabaseIdentityAccountsOnAuthTests
The mailed link leads to the accept page, with the token after the #. Auth receives the token, so the invitation depends on the sign-in with the invited address.Code: InvitationPage.​cs, AcceptInvitation.​razor, AuthLink.​cs, LinkSignIn.​cs
Try it: Tove invites a new address. The mail is in mail.
Test: InvitedByMailTests, AuthLinkTests, LinkSignInTests, TenancyOnSupabase

Languages​

The choiceWhere to see it
The package's Dutch texts never use "je" or "u". An application that wants one of them puts its own resource file in front.Code: TenancyFailures.​nl.​resx, ProjectFailures.​nl.​resx
Try it: The en/nl switch. Rhea's Accept-Language: nl request (.​http).
Test: RefusalTableTests, TranslationTests
A request is answered in the request's language. Code outside a request names the language of its reader.Code: CultureScope.​cs, RequestLanguages.​cs, RefusalProblems.​cs
Try it: The same switch
Test: CultureScopeTests, LanguageScenarios
A refusal keeps the input it refers to as an argument, Field. The sample's own refusal codes do not carry Field yet.Code: TenancyRefusals.​cs, TenancyNames.​cs
Try it: Rhea asks about an unknown permission (.​http). Field is permissions.
Test: RefusalTableTests
A role copied from a starter role gets its name once, in the language passed at provisioning. After that, the name is the tenant's to change.Code: IStarterRoleTexts.​cs
Try it: Not in the sample, which passes no language
Test: StarterRoleTextsTests, ProvisioningHooksTests
The package's own administrators' starter role has a name in English and in Dutch.Code: TenancyStarterRoleTexts.​nl.​resx, TenantManager.​cs
Try it: Not in the sample, which declares its own starter roles
Test: StarterRoleTextsTests