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 @@ - +