diff --git a/docs/samples/multitenancy/header-and-route-resolution.md b/docs/samples/multitenancy/header-and-route-resolution.md
index 065c31f..972e919 100644
--- a/docs/samples/multitenancy/header-and-route-resolution.md
+++ b/docs/samples/multitenancy/header-and-route-resolution.md
@@ -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
-
+
```
- `samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj`:
```xml
-
+
```
-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
@@ -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(options =>
{
- options.RequireTenantByDefault = true;
+ options.RequireTenantByDefault = true;
+ options.AllowAnonymous = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List
{
@@ -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.
diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md
index 4469b91..bfa5b0d 100644
--- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md
+++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/README.md
@@ -17,19 +17,19 @@ Packages:
`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj`:
```xml
-
+
```
`samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj`:
```xml
-
+
```
### 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
@@ -43,6 +43,7 @@ using CleanArchitecture.Extensions.Multitenancy.Configuration;
builder.Services.Configure(options =>
{
options.RequireTenantByDefault = true;
+ options.AllowAnonymous = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List
{
@@ -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.
diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj
index b2df343..5a95057 100644
--- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj
+++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Application/Application.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs
index 5387e37..c457004 100644
--- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs
+++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/DependencyInjection.cs
@@ -29,11 +29,15 @@ public static void AddWebServices(this IHostApplicationBuilder builder)
.AddDbContextCheck();
builder.Services.AddExceptionHandler();
+ // 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(options =>
{
options.RequireTenantByDefault = true;
+ options.AllowAnonymous = true;
options.HeaderNames = new[] { "X-Tenant-ID" };
options.ResolutionOrder = new List
{
diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs
index e7f29f4..e3de17d 100644
--- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs
+++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Infrastructure/WebApplicationExtensions.cs
@@ -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;
@@ -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)
diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs
index 42a683a..d8354b4 100644
--- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs
+++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Program.cs
@@ -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);
@@ -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();
@@ -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();
diff --git a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj
index 9ca7580..8ffa3ac 100644
--- a/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj
+++ b/samples/CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution/src/Web/Web.csproj
@@ -14,7 +14,7 @@
-
+