Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions docs/samples/index.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# Samples

There are no runnable samples in the repository yet. The current guidance is:

- Use the extension pages for step-by-step setup.
- Review tests under `tests/` for realistic usage patterns.

Planned samples:

- Query caching with explicit invalidation.
- Multitenancy resolution (header + route) with enforcement.
- EF Core shared-database isolation with tenant filters.
There are still no runnable samples committed, but the multitenancy scenarios below capture the step-by-step work needed to add them under `samples/` when ready. Each plan follows the Clean Architecture template (SQLite by default) and the repository rule to keep numbered step comments mirrored in the sample README.

- [Header + route resolution with enforcement](multitenancy/header-and-route-resolution.md)
- [Tenant context propagation into background jobs](multitenancy/background-jobs-context-propagation.md)
- [EF Core database-per-tenant isolation](multitenancy/efcore-database-per-tenant.md)
- [Tenant-isolated Identity and authorization](multitenancy/identity-tenant-isolated-auth.md)
- [Provisioning with region-aware sharding and dedicated databases](multitenancy/provisioning-hybrid-residency.md)
- [Tenant-scoped Redis caching with key prefixing](multitenancy/redis-tenant-caching.md)
31 changes: 31 additions & 0 deletions docs/samples/multitenancy/background-jobs-context-propagation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Scenario: Tenant Context Propagation into Background Jobs

## Goal
Document a sample that captures tenant context in HTTP requests and restores it inside background jobs so multi-tenant data access and logging stay isolated.

## Sample name and location
- Solution: `CleanArchitecture.Extensions.Samples.Multitenancy.BackgroundJobs`
- Path: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.BackgroundJobs`

## Modules used
- Multitenancy core (context, serializer, enforcement behaviors)
- Multitenancy.AspNetCore (HTTP resolution)

## Prerequisites
- Create the base Web API solution using SQLite.
- Keep numbered step comments in code changes with matching entries in the sample README.

## Steps
1. Reference `CleanArchitecture.Extensions.Multitenancy` and `CleanArchitecture.Extensions.Multitenancy.AspNetCore`; enable middleware for header-based resolution and keep enforcement on.
2. Ensure `ITenantContextSerializer` is registered (from the core package) and configure correlation options if needed to include tenant IDs in log scopes.
3. Build an in-memory `IBackgroundJobQueue` abstraction that carries a payload plus serialized `TenantContext`.
4. In controllers/handlers that enqueue work, serialize the current `TenantContext` and attach it to the queued message.
5. Implement a hosted worker that dequeues messages, restores tenant context via `ITenantAccessor`/`ITenantContextSerializer`, runs the work, and clears `ICurrentTenant` afterward.
6. Register MediatR behaviors (`TenantEnforcementBehavior`, `TenantCorrelationBehavior`) so background commands also require a tenant when needed and emit tenant-aware log scopes.
7. Add tests to assert tenant context flows into the worker (log scope contains tenant ID; EF Core context sees the tenant) and that enqueuing without a tenant is rejected when the message type requires it.
8. Document failure handling: how to drop or poison messages when context deserialization fails and how to avoid leaking tenant info in those paths.

## Validation
- Jobs executed by the worker see the same tenant ID as the request that queued them.
- Messages created without tenant context are rejected for tenant-required workflows.
- Tenant context is cleared after each job to avoid cross-tenant leakage.
32 changes: 32 additions & 0 deletions docs/samples/multitenancy/efcore-database-per-tenant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Scenario: EF Core Database-Per-Tenant Isolation

## Goal
Document a sample that uses database-per-tenant isolation with EF Core helpers, per-tenant migrations, and strict SaveChanges enforcement.

## Sample name and location
- Solution: `CleanArchitecture.Extensions.Samples.Multitenancy.EfCoreDatabasePerTenant`
- Path: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.EfCoreDatabasePerTenant`

## Modules used
- Multitenancy core
- Multitenancy.AspNetCore
- Multitenancy.EFCore

## Prerequisites
- Base Web API solution using SQLite with the Clean Architecture template.
- Numbered step comments and matching README entries for every change.

## Steps
1. Add references to `CleanArchitecture.Extensions.Multitenancy`, `CleanArchitecture.Extensions.Multitenancy.AspNetCore`, and `CleanArchitecture.Extensions.Multitenancy.EFCore`; include them in the sample solution.
2. Configure `EfCoreMultitenancyOptions` to `DatabasePerTenant`, set the tenant ID property name, and mark global entities (Identity tables, etc.) as exclusions.
3. Implement `ITenantConnectionResolver` mapping each tenant to a SQLite file under `App_Data/{tenantId}.db`, allowing premium/regional tenants to pick dedicated databases if needed.
4. Replace the default DbContext registration with `ITenantDbContextFactory<ApplicationDbContext>` using the `TenantDbContext` base class plus `TenantSaveChangesInterceptor` and the tenant-aware model customizer.
5. Wire `TenantMigrationRunner` to run migrations per tenant; include a CLI/hosted command that iterates the tenant catalog and migrates active tenants.
6. Ensure the web pipeline resolves tenants before EF Core usage and keep `TenantEnforcementBehavior` enabled so data access cannot occur without tenant context.
7. Add tests verifying per-tenant database files are created, cross-tenant queries are blocked, and global entities bypass filters when configured.
8. Document operational guidance: backup/restore per tenant, handling connection string rotation, and cleaning up databases on tenant deletion.

## Validation
- Each tenant writes to its own SQLite file; queries do not cross boundaries.
- SaveChanges fails when tenant context is missing or mismatched.
- Migration runner reports per-tenant status and supports reruns without data loss.
113 changes: 113 additions & 0 deletions docs/samples/multitenancy/header-and-route-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Scenario: Header + Route Resolution with ASP.NET Core Enforcement

## Goal

Document a sample that shows deterministic tenant resolution from route first, host second, and header fallback, with ProblemDetails responses when a tenant is missing or inactive

## Sample name and location

- Solution: `CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution`
- Path: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution`

## Modules used

- Multitenancy core (resolution pipeline + behaviors)
- Multitenancy.AspNetCore (middleware, attributes, ProblemDetails)

## Prerequisites

- Install the .NET SDK required by the Clean Architecture template.
- Keep numbered step comments in code changes and mirror them in the sample README per repository guidance.

## Steps

1. Install Jason Taylor's Clean Architecture template and create the base Web API-only solution (no extensions yet).
- Install or update the template to the version we align with:
```bash
dotnet new install Clean.Architecture.Solution.Template::10.0.0-preview
```
- From the repo root, create the sample solution under `CleanArchitecture.Extensions/samples` using SQLite:
```bash
cd CleanArchitecture.Extensions/samples
dotnet new ca-sln -cf None -o CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution --database sqlite
```
- Verify the output folder exists and contains the new solution file plus `src/` and `tests/`.
2. Add NuGet package references for the multitenancy extensions (use the latest published versions from NuGet).
- Packages: [CleanArchitecture.Extensions.Multitenancy](https://www.nuget.org/packages/CleanArchitecture.Extensions.Multitenancy) and [CleanArchitecture.Extensions.Multitenancy.AspNetCore](https://www.nuget.org/packages/CleanArchitecture.Extensions.Multitenancy.AspNetCore).
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj`:
```xml
<!-- Step 2: (Begin) Add Multitenancy core package -->
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.5" />
<!-- Step 2: (End) Add Multitenancy core package -->
```
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj`:
```xml
<!-- Step 2: (Begin) Add Multitenancy AspNetCore package -->
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.5" />
<!-- Step 2: (End) Add Multitenancy AspNetCore package -->
```
3. Configure `MultitenancyOptions` for route-first ordering (`Route > Host > Header > Query > Claim`), set header name `X-Tenant-ID`, require tenants by default, and disable fallback tenants.
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs`:
```csharp
// Step 3: (Begin) Multitenancy configuration imports
using CleanArchitecture.Extensions.Multitenancy;
using CleanArchitecture.Extensions.Multitenancy.Configuration;
// Step 3: (End) Multitenancy configuration imports
```
```csharp
// Step 3: (Begin) Configure multitenancy resolution defaults
builder.Services.Configure<MultitenancyOptions>(options =>
{
options.RequireTenantByDefault = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List<TenantResolutionSource>
{
TenantResolutionSource.Route,
TenantResolutionSource.Host,
TenantResolutionSource.Header,
TenantResolutionSource.QueryString,
TenantResolutionSource.Claim
};
options.FallbackTenant = null;
options.FallbackTenantId = null;
});
// Step 3: (End) Configure multitenancy resolution defaults
```
4. Register services with `AddCleanArchitectureMultitenancy` then `AddCleanArchitectureMultitenancyAspNetCore(autoUseMiddleware: false)`; add `UseCleanArchitectureMultitenancy` after routing and before authentication/authorization.
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs`:
```csharp
// Step 4: (Begin) Multitenancy ASP.NET Core registration imports
using CleanArchitecture.Extensions.Multitenancy.AspNetCore;
// Step 4: (End) Multitenancy ASP.NET Core registration imports
```
```csharp
// Step 4: (Begin) Register multitenancy services and ASP.NET Core adapter
builder.Services.AddCleanArchitectureMultitenancy();
builder.Services.AddCleanArchitectureMultitenancyAspNetCore(autoUseMiddleware: false);
// Step 4: (End) Register multitenancy services and ASP.NET Core adapter
```
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs`:
```csharp
// Step 4: (Begin) Multitenancy middleware import
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Middleware;
// Step 4: (End) Multitenancy middleware import
```
```csharp
// Step 4: (Begin) Add multitenancy middleware between routing and auth
app.UseRouting();
app.UseCleanArchitectureMultitenancy();
app.UseAuthentication();
app.UseAuthorization();
// Step 4: (End) Add multitenancy middleware between routing and auth
```
5. Add route conventions that group tenant-bound APIs under `/tenants/{tenantId}/...`; keep health/status endpoints outside the group for anonymous access.
6. Decorate tenant-bound endpoints with `RequireTenant`, and mark public endpoints with `AllowAnonymousTenant` to keep resolution optional without enforcement.
7. Enable `TenantExceptionHandler`/ProblemDetails so unresolved tenants return 400, missing tenants return 404, and suspended tenants return 403.
8. Add integration tests that cover: resolved via route, resolved via host mapping, header fallback when the route is absent, conflict handling when route/header disagree, and enforcement responses when no tenant is provided.
9. Update the sample README with the walkthrough (inputs, expected status codes) and middleware ordering reminders.

## Validation

- Requests with `/tenants/{tenantId}` succeed only when the tenant exists and is active.
- Requests without tenant context return the expected ProblemDetails payloads.
- Tenant context is cleared per request (no AsyncLocal leakage between tests).
33 changes: 33 additions & 0 deletions docs/samples/multitenancy/identity-tenant-isolated-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Scenario: Tenant-Isolated Identity and Authorization

## Goal
Document a sample that ties Identity login and authorization to the current tenant using tenant-aware stores, claims, and policies.

## Sample name and location
- Solution: `CleanArchitecture.Extensions.Samples.Multitenancy.IdentityPerTenant`
- Path: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.IdentityPerTenant`

## Modules used
- Multitenancy core
- Multitenancy.AspNetCore
- Multitenancy.Identity (planned adapter)
- Multitenancy.EFCore for data isolation

## Prerequisites
- Base Web API solution with Identity enabled (template default) and SQLite.
- Numbered step comments and matching README entries for all changes.

## Steps
1. Reference multitenancy core and AspNetCore; add `CleanArchitecture.Extensions.Multitenancy.Identity` when the package is available (use a project reference if built in-repo).
2. Ensure tenant resolution runs before authentication so login attempts already have tenant context (host/route/header providers configured).
3. Update Identity user and role entities/stores to implement `ITenantUser`/`ITenantRole`; scope user queries by tenant ID and namespace roles using the configured prefix pattern.
4. Register `TenantClaimsPrincipalFactory` to inject `tenant_id`, `tenant_name`, and per-tenant roles/permissions into JWTs or cookies.
5. Configure `TenantPolicyProvider` plus authorization handlers (`TenantMembershipHandler`, `TenantPermissionRequirement`) and swap existing policy registrations to tenant-aware versions.
6. Enforce tenant suspension/inactive flags during sign-in (fail fast with the proper error) and ensure issued tokens are invalidated if tenant state changes.
7. Add integration tests covering successful login within a tenant, rejected login for mismatched tenant, role prefixing behavior, and authorization policies that deny cross-tenant access.
8. Document how to seed a tenant admin user via provisioning events or startup seeding, and how to rotate keys without breaking tenant isolation.

## Validation
- Authentication only succeeds when the principal’s tenant matches the resolved tenant context.
- Authorization policies respect tenant-prefixed roles/permissions.
- Token claims include tenant identifiers and are rejected if the tenant is suspended or deleted.
35 changes: 35 additions & 0 deletions docs/samples/multitenancy/provisioning-hybrid-residency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Scenario: Provisioning with Region-Aware Sharding and Dedicated Databases

## Goal
Document a sample that provisions tenants through an orchestrator, chooses shard/database based on region/plan, and runs migrations/seeds accordingly.

## Sample name and location
- Solution: `CleanArchitecture.Extensions.Samples.Multitenancy.ProvisioningHybrid`
- Path: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.ProvisioningHybrid`

## Modules used
- Multitenancy core
- Multitenancy.EFCore
- Multitenancy.Provisioning (planned adapter)
- Multitenancy.Sharding (planned adapter)
- Multitenancy.AspNetCore for API hosting
- Multitenancy.Storage initializer for assets (optional)

## Prerequisites
- Base Web API solution using SQLite for shared mode, with the option to create per-tenant SQLite files for dedicated mode.
- Numbered step comments and matching README entries for every change.

## Steps
1. Reference core, AspNetCore, and EFCore packages; add `Multitenancy.Provisioning` and `Multitenancy.Sharding` once available, and include them in the sample solution.
2. Implement a tenant catalog store and register `ProvisioningOrchestrator` with the Requested -> Validating -> Provisioning -> Active -> Suspended -> Deleted state machine from the blueprint.
3. Configure `ITenantPlanBuilder` to choose isolation mode: default shared database, but premium/regulated tenants get a dedicated database on a region-tagged shard (via `IShardResolver`).
4. Hook `ITenantSchemaManager` to run EF Core migrations/seeds per tenant according to the selected isolation mode; provide a dry-run path for planning without execution.
5. Add API endpoints or CLI commands for create/suspend/delete tenant operations that call provisioning services and publish lifecycle events (`TenantCreated`, `TenantProvisioned`, `TenantSuspended`, `TenantDeleted`).
6. Integrate the storage initializer so tenant-specific folders/containers are created on activation, following the Multitenancy.Storage path conventions.
7. Add operational scripts/tests to simulate cold onboarding (pre-created database) and hybrid flows, verifying events fire in order and rollback works on partial failures.
8. Document monitoring expectations (progress logs per tenant, metrics for provisioning duration) and cleanup steps when deletion or suspension occurs.

## Validation
- Provisioning selects the correct isolation mode based on plan/region and records the choice in tenant metadata.
- Migrations/seeds run per tenant and can resume idempotently after failures.
- Lifecycle events drive downstream actions (Identity seeding, storage setup) without leaking tenant data across boundaries.
33 changes: 33 additions & 0 deletions docs/samples/multitenancy/redis-tenant-caching.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Scenario: Tenant-Scoped Redis Caching with Key Prefixing

## Goal
Document a sample that combines multitenancy caching behavior with Redis key prefixing to guarantee cache isolation per tenant.

## Sample name and location
- Solution: `CleanArchitecture.Extensions.Samples.Multitenancy.RedisCaching`
- Path: `samples/CleanArchitecture.Extensions.Samples.Multitenancy.RedisCaching`

## Modules used
- Multitenancy core
- Multitenancy.Caching
- Multitenancy.Redis (planned adapter)
- Multitenancy.AspNetCore for inbound resolution

## Prerequisites
- Base Web API solution; provide a Redis connection (local container or test instance) via configuration.
- Numbered step comments and matching README entries for every change.

## Steps
1. Reference `CleanArchitecture.Extensions.Multitenancy`, `CleanArchitecture.Extensions.Multitenancy.AspNetCore`, `CleanArchitecture.Extensions.Multitenancy.Caching`, and add `CleanArchitecture.Extensions.Multitenancy.Redis` when available; ensure distributed cache services are registered before the multitenancy adapters.
2. Configure `MultitenancyOptions` to require tenant context for cache-bearing operations and enable `TenantScopedCacheBehavior` in MediatR.
3. Configure `RedisKeyStrategyOptions` with a prefix pattern such as `{tenant}:{resource}:{hash}`, disable global keys, and set default TTLs per resource; wire `TenantRedisConnectionResolver` if different endpoints per tenant are needed.
4. Register Redis distributed cache (StackExchange.Redis) pointing at the dev instance; expose the connection string via configuration so tests can swap to in-memory fakes.
5. Update caching helpers to reject keys that lack tenant context; audit background jobs to ensure `TenantContext` is restored before cache usage.
6. Add integration tests to assert keys include tenant prefixing, cross-tenant cache pollution is prevented, and cache hits/misses are counted per tenant.
7. Document migration steps for changing prefix formats and how to flush a single tenant's keys safely during deletion without issuing a global flush.
8. Note operational guardrails: avoid flush-all commands, monitor keyspace size per tenant, and alert on prefix collisions.

## Validation
- Cache keys are consistently prefixed with the current tenant ID and optional environment/resource segments.
- Requests without tenant context fail fast when cache access is required.
- Cache isolation holds under concurrent cross-tenant load and during key format migrations.
Loading
Loading