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
4 changes: 2 additions & 2 deletions docs/samples/multitenancy/header-and-route-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ 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.6" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.7" />
<!-- 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.6" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.7" />
<!-- 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, allow explicitly anonymous endpoints, and disable fallback tenants.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution
# CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution

The project was generated using the [Clean.Architecture.Solution.Template](https://github.com/jasontaylordev/CleanArchitecture) version 10.0.0-preview.

Expand All @@ -17,14 +17,14 @@ 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.6" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.7" />
<!-- 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.6" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.7" />
<!-- Step 2: (End) Add Multitenancy AspNetCore package -->
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RootNamespace>CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution.Application</RootNamespace>
Expand All @@ -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.6" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy" VersionOverride="0.2.7" />
<!-- 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
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<RootNamespace>CleanArchitecture.Extensions.Samples.Multitenancy.HeaderAndRouteResolution.Web</RootNamespace>
Expand All @@ -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.6" />
<PackageReference Include="CleanArchitecture.Extensions.Multitenancy.AspNetCore" VersionOverride="0.2.7" />
<!-- Step 2: (End) Add Multitenancy AspNetCore package -->
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Reflection;
using CleanArchitecture.Extensions.Multitenancy.AspNetCore.Options;
using CleanArchitecture.Extensions.Multitenancy.Configuration;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -46,7 +48,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context)

foreach (var description in context.Results)
{
var template = description.ActionDescriptor?.AttributeRouteInfo?.Template;
var template = GetTemplate(description.ActionDescriptor);
if (!ShouldApplyTemplate(template))
{
continue;
Expand Down Expand Up @@ -89,6 +91,45 @@ private static string NormalizeTemplate(string template)
return template.TrimStart('/');
}

private static string? GetTemplate(ActionDescriptor? actionDescriptor)
{
if (actionDescriptor is null)
{
return null;
}

var template = actionDescriptor.AttributeRouteInfo?.Template;
if (!string.IsNullOrWhiteSpace(template))
{
return template;
}

return TryGetRoutePatternTemplate(actionDescriptor);
}

private static string? TryGetRoutePatternTemplate(ActionDescriptor actionDescriptor)
{
var routePatternProperty = actionDescriptor.GetType().GetProperty("RoutePattern", BindingFlags.Instance | BindingFlags.Public);
if (routePatternProperty is null)
{
return null;
}

var routePattern = routePatternProperty.GetValue(actionDescriptor);
if (routePattern is null)
{
return null;
}

var rawTextProperty = routePattern.GetType().GetProperty("RawText", BindingFlags.Instance | BindingFlags.Public);
if (rawTextProperty?.PropertyType == typeof(string))
{
return rawTextProperty.GetValue(routePattern) as string;
}

return routePattern.ToString();
}

private static bool ContainsRouteParameter(string template, string parameterName)
{
if (string.IsNullOrWhiteSpace(template) || string.IsNullOrWhiteSpace(parameterName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,48 @@ public void OnProvidersExecuting_noops_when_openapi_integration_disabled()
Assert.Equal("api/tenants/TodoItems", description.RelativePath);
Assert.Empty(description.ParameterDescriptions);
}

[Fact]
public void OnProvidersExecuting_uses_route_pattern_when_attribute_template_missing()
{
var options = OptionsFactory.Create(new MultitenancyOptions { RouteParameterName = "tenantId" });
var aspNetCoreOptions = OptionsFactory.Create(new AspNetCoreMultitenancyOptions { EnableOpenApiIntegration = true });
var provider = new TenantRouteApiDescriptionProvider(options, aspNetCoreOptions, new EmptyModelMetadataProvider());

var actionDescriptor = new StubRouteActionDescriptor
{
RoutePattern = new StubRoutePattern("api/tenants/{tenantId}/TodoItems")
};

var description = new ApiDescription
{
ActionDescriptor = actionDescriptor,
RelativePath = "api/tenants/TodoItems"
};

var context = new ApiDescriptionProviderContext(new[] { actionDescriptor });
context.Results.Add(description);

provider.OnProvidersExecuting(context);

Assert.Equal("api/tenants/{tenantId}/TodoItems", description.RelativePath);
var parameter = Assert.Single(description.ParameterDescriptions, item =>
string.Equals(item.Name, "tenantId", StringComparison.OrdinalIgnoreCase));
Assert.Equal(BindingSource.Path, parameter.Source);
}

private sealed class StubRoutePattern
{
public StubRoutePattern(string rawText)
{
RawText = rawText;
}

public string RawText { get; }
}

private sealed class StubRouteActionDescriptor : ActionDescriptor
{
public StubRoutePattern? RoutePattern { get; set; }
}
}
Loading