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
.httpfile, marked (.http) and listed under the person who sends it; - a command.
Try it explains how to start the sample.
- a preset of the UI's Try it page, by its id in
-
Test names a test class under
Tests/. The sample's tests are inTests/Examples.Tenancy.Tests. A test that needs the host with a database takes it from the fixtureSampleHosts, and needs Docker.
Access rules
| The choice | Where 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.csTry 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.csTry it: Presets owner-change-from-the-crew, remove-the-owner, give-the-owner-a-roleTest: OwnerScenarios, CrewRoleScenarios, ProjectMembershipTests |
| The permission that closes a project also reopens it. | Code: ReopenProject.csTry 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.csTry 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.csTry it: Presets give-a-role-that-manages-access, appoint-yourself, appoint-yourself-holding-its-permissionTest: PeopleOfficeScenarios, SeatManagerTests |
| The same rule applies to changing a seat's status and to moving a unit. | Code: SeatManager.cs, OrganizationManager.csTry 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.csTry 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.csTry it: Every preset that returns tenancy.grant-exceeds-own or tenancy.self-appointmentTest: 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.sqlTry 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.csTry it: Preset rename-as-access-admin, as MaudTest: 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.csTry 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.csTry 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.csTry 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.csTry it: Leo gives Vic Crew lead on Pier 7 in the UI. Vic then takes the role from himself. Test: CrewRoleScenarios, AccessHoldScenarios |
Structure
| The choice | Where 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.csTry it: dotnet run --project Examples/Tenancy/Examples.Tenancy.AppHostTest: 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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, AggregatesTry 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.propsTry 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.csTry 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.ExporterTry 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.csTry 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.csTry 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.csTry 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.csTry it: Nothing to run Test: SourceTreeTests |
Data and names
| The choice | Where 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry it: Not by hand. It needs two commands at the same moment. Test: DatabaseRefusalScenarios, IndexRefusalTests |
Database security
| The choice | Where to see it |
|---|---|
| A seat reads only its own rights. The database maintains them. | Code: TenancySql.cs, TenantsInfrastructure.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry it: Sample:Pools:Requests, Sample:Pools:BackgroundTest: 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.csTry it: The password form on the login page, as rhea@example.testTest: SampleOnSupabaseTests, PublishedKeyTokenTests, SupabaseTokenHandlerTests |
| The sample is a Supabase project that the Supabase CLI can read. | Code: Examples/Tenancy/supabase/config.toml, Examples/Tenancy/supabase/migrationsTry it: On the CLI's stack Test: SampleOnTheCliStackTests, skipped where the CLI's stack is not running |
REST
| The choice | Where to see it |
|---|---|
| The seat check is a host authorization policy. It runs before a route reads its arguments. | Code: SamplePolicies.cs, SeatRequirementHandler.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.csTry it: Sample:Cors:Origins. The fetch example on the samples' page.Test: CorsTests |
GraphQL
| The choice | Where to see it |
|---|---|
| REST routes and GraphQL fields send the same commands and queries. | Code: CrewMutations.cs, CrewEndpoints.csTry it: The GraphQL part of the .http fileTest: 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.csTry 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.graphqlTry 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.csTry 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.csTry it: Not in the seed data Test: GraphQLLookupScenarios, EntityNullabilityTests |
An operation is a static method marked [Query] or [Mutation]. | Code: LifecycleMutations.cs, Module.csTry 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.csTry 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.csTry 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.csTry 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.csTry 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.graphqlTry 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.csTry 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.csTry 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.csTry 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.csTry it: The refused request that goes round three times ( .http)Test: GraphQLProjectScenarios, GraphQLDeclarationTests, SampleOnPostgresTests |
Operators and history
| The choice | Where 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.csTry 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.csTry 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.csTry it: The Access history page. Presets history-with-the-permission, history-without-the-permission.Test: AccessHistoryScenarios, AccessHistoryFieldScenarios, AccessHistoryTests, AccessHistoryPolicyTests |
Invitations
| The choice | Where 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.csTry 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.csTry it: Preset invite-without-seat-management, which shows the issuing halfTest: 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.csTry it: Preset accept-an-invitation-nobody-sentTest: InvitationScenarios, InvitationTests |
| A token is shown once. The database keeps only its digest, which no caller's role can read. | Code: BearerTokens.cs, TenancyInvitationDigest.csTry 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.csTry 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.csTry 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.csTry it: The Invitations page Test: InvitationTests, InvitationPolicyTests |
| When two acceptances run at the same time, the second one loses the race. | Code: InvitationManager.csTry 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.csTry it: Nothing to try Test: InvitationStoreTests, InvitationPolicyTests |
| A host may leave invitations out. | Code: TenantsContext.cs, Invitation.csTry 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.csTry 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.csTry it: Tove invites a new address. The mail is in mail.Test: InvitedByMailTests, AuthLinkTests, LinkSignInTests, TenancyOnSupabase |
Languages
| The choice | Where 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.resxTry 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.csTry 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.csTry 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.csTry 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.csTry it: Not in the sample, which declares its own starter roles Test: StarterRoleTextsTests |