Skip to content
62 changes: 57 additions & 5 deletions docs/samples/multitenancy/header-and-route-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ Document a sample that shows deterministic tenant resolution from route first, h
- `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" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.6" />
<!-- 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" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.6" />
<!-- 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.
3. Configure `MultitenancyOptions` for route-first ordering (`Route > Host > Header > Query > Claim`), set header name `X-Tenant-ID`, require tenants by default, allow explicitly anonymous endpoints, and disable fallback tenants.
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs`:
```csharp
// Step 3: (Begin) Multitenancy configuration imports
Expand All @@ -58,7 +58,8 @@ Document a sample that shows deterministic tenant resolution from route first, h
// Step 3: (Begin) Configure multitenancy resolution defaults
builder.Services.Configure<MultitenancyOptions>(options =>
{
options.RequireTenantByDefault = true;
options.RequireTenantByDefault = true;
options.AllowAnonymous = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List<TenantResolutionSource>
{
Expand Down Expand Up @@ -101,8 +102,59 @@ Document a sample that shows deterministic tenant resolution from route first, h
// 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.

- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs`:

```csharp
// Step 5: (Begin) Prefix tenant-bound endpoints with tenant route
var tenantRoutePrefix = "/api/tenants/{tenantId}";

var routeGroup = app
.MapGroup($"{tenantRoutePrefix}/{groupName}")
.WithGroupName(groupName)
.WithTags(groupName);
// Step 5: (End) Prefix tenant-bound endpoints with tenant route
```

6. Decorate tenant-bound endpoints with `RequireTenant`, and mark public endpoints with `AllowAnonymousTenant` to keep resolution optional without enforcement (requires `AllowAnonymous = true` in Step 3).
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs`:
```csharp
// Step 6: (Begin) Tenant enforcement routing helpers
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Routing;
// Step 6: (End) Tenant enforcement routing helpers
```
```csharp
// Step 6: (Begin) Enforce tenant requirements for grouped endpoints
routeGroup.AddTenantEnforcement();
routeGroup.RequireTenant();
// Step 6: (End) Enforce tenant requirements for grouped endpoints
```
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs`:
```csharp
// Step 6: (Begin) Tenant requirement routing helpers
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Routing;
// Step 6: (End) Tenant requirement routing helpers
```
```csharp
// Step 6: (Begin) Allow tenant-less access for public endpoints
app.Map("/", () => Results.Redirect("/api"))
.AddTenantEnforcement()
.AllowAnonymousTenant();
// Step 6: (End) Allow tenant-less access for public endpoints
```
7. Enable `TenantExceptionHandler`/ProblemDetails so unresolved tenants return 400, missing tenants return 404, and suspended tenants return 403.
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs`:
```csharp
// Step 7: (Begin) Register ProblemDetails for exception handling
builder.Services.AddProblemDetails();
// Step 7: (End) Register ProblemDetails for exception handling
```
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs`:
```csharp
// Step 7: (Begin) Enable exception handlers for ProblemDetails responses
app.UseExceptionHandler();
// Step 7: (End) Enable exception handlers for ProblemDetails responses
```
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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ Packages:
`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" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.6" />
<!-- 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" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.6" />
<!-- Step 2: (End) Add Multitenancy AspNetCore package -->
```

### Step 3: Configure multitenancy resolution defaults
Set route-first ordering, require tenants by default, and disable fallback tenants.
Set route-first ordering, require tenants by default, allow explicitly anonymous endpoints, and disable fallback tenants.

`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs`:
```csharp
Expand All @@ -43,6 +43,7 @@ using CleanArchitecture.Extensions.Multitenancy.Configuration;
builder.Services.Configure<MultitenancyOptions>(options =>
{
options.RequireTenantByDefault = true;
options.AllowAnonymous = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List<TenantResolutionSource>
{
Expand Down Expand Up @@ -89,6 +90,68 @@ app.UseAuthorization();
// Step 4: (End) Add multitenancy middleware between routing and auth
```

### Step 5: Add tenant route prefix for endpoint groups
Group tenant-bound APIs under `/api/tenants/{tenantId}/...`. Health and status endpoints remain outside this group.

`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs`:
```csharp
// Step 5: (Begin) Prefix tenant-bound endpoints with tenant route
var tenantRoutePrefix = "/api/tenants/{tenantId}";

var routeGroup = app
.MapGroup($"{tenantRoutePrefix}/{groupName}")
.WithGroupName(groupName)
.WithTags(groupName);
// Step 5: (End) Prefix tenant-bound endpoints with tenant route
```

### Step 6: Enforce tenant requirements and allow public endpoints
Require tenants on grouped endpoints and mark public endpoints as optional (requires `AllowAnonymous = true` in Step 3).

`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs`:
```csharp
// Step 6: (Begin) Tenant enforcement routing helpers
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Routing;
// Step 6: (End) Tenant enforcement routing helpers
```
```csharp
// Step 6: (Begin) Enforce tenant requirements for grouped endpoints
routeGroup.AddTenantEnforcement();
routeGroup.RequireTenant();
// Step 6: (End) Enforce tenant requirements for grouped endpoints
```

`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs`:
```csharp
// Step 6: (Begin) Tenant requirement routing helpers
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Routing;
// Step 6: (End) Tenant requirement routing helpers
```
```csharp
// Step 6: (Begin) Allow tenant-less access for public endpoints
app.Map("/", () => Results.Redirect("/api"))
.AddTenantEnforcement()
.AllowAnonymousTenant();
// Step 6: (End) Allow tenant-less access for public endpoints
```

### Step 7: Enable ProblemDetails exception handling
Activate the registered exception handlers so multitenancy failures map to ProblemDetails responses.

`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs`:
```csharp
// Step 7: (Begin) Register ProblemDetails for exception handling
builder.Services.AddProblemDetails();
// Step 7: (End) Register ProblemDetails for exception handling
```

`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs`:
```csharp
// Step 7: (Begin) Enable exception handlers for ProblemDetails responses
app.UseExceptionHandler();
// Step 7: (End) Enable exception handlers for ProblemDetails responses
```

## Build

Run `dotnet build -tl` to build the solution.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="AutoMapper" />
<!-- Step 2: (Begin) Add Multitenancy core package -->
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.5" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.6" />
<!-- Step 2: (End) Add Multitenancy core package -->
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" />
<PackageReference Include="MediatR" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ public static void AddWebServices(this IHostApplicationBuilder builder)
.AddDbContextCheck<ApplicationDbContext>();

builder.Services.AddExceptionHandler<CustomExceptionHandler>();
// Step 7: (Begin) Register ProblemDetails for exception handling
builder.Services.AddProblemDetails();
// Step 7: (End) Register ProblemDetails for exception handling

// Step 3: (Begin) Configure multitenancy resolution defaults
builder.Services.Configure<MultitenancyOptions>(options =>
{
options.RequireTenantByDefault = true;
options.AllowAnonymous = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List<TenantResolutionSource>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Reflection;
// Step 6: (Begin) Tenant enforcement routing helpers
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Routing;
// Step 6: (End) Tenant enforcement routing helpers

namespace CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution.Web.Infrastructure;

Expand All @@ -8,10 +11,21 @@ private static RouteGroupBuilder MapGroup(this WebApplication app, EndpointGroup
{
var groupName = group.GroupName ?? group.GetType().Name;

return app
.MapGroup($"/api/{groupName}")
// Step 5: (Begin) Prefix tenant-bound endpoints with tenant route
var tenantRoutePrefix = "/api/tenants/{tenantId}";

var routeGroup = app
.MapGroup($"{tenantRoutePrefix}/{groupName}")
.WithGroupName(groupName)
.WithTags(groupName);
// Step 5: (End) Prefix tenant-bound endpoints with tenant route

// Step 6: (Begin) Enforce tenant requirements for grouped endpoints
routeGroup.AddTenantEnforcement();
routeGroup.RequireTenant();
// Step 6: (End) Enforce tenant requirements for grouped endpoints

return routeGroup;
}

public static WebApplication MapEndpoints(this WebApplication app)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Step 4: (Begin) Multitenancy middleware import
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Middleware;
// Step 4: (End) Multitenancy middleware import
// Step 6: (Begin) Tenant requirement routing helpers
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Routing;
// Step 6: (End) Tenant requirement routing helpers
using CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution.Infrastructure.Data;

var builder = WebApplication.CreateBuilder(args);
Expand Down Expand Up @@ -35,7 +38,9 @@
});


app.UseExceptionHandler(options => { });
// Step 7: (Begin) Enable exception handlers for ProblemDetails responses
app.UseExceptionHandler();
// Step 7: (End) Enable exception handlers for ProblemDetails responses

// Step 4: (Begin) Add multitenancy middleware between routing and auth
app.UseRouting();
Expand All @@ -44,7 +49,11 @@
app.UseAuthorization();
// Step 4: (End) Add multitenancy middleware between routing and auth

app.Map("/", () => Results.Redirect("/api"));
// Step 6: (Begin) Allow tenant-less access for public endpoints
app.Map("/", () => Results.Redirect("/api"))
.AddTenantEnforcement()
.AllowAnonymousTenant();
// Step 6: (End) Allow tenant-less access for public endpoints

app.MapEndpoints();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" />
<!-- Step 2: (Begin) Add Multitenancy AspNetCore package -->
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.5" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.6" />
<!-- Step 2: (End) Add Multitenancy AspNetCore package -->
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
Expand Down
Loading