mirror of https://github.com/abpframework/abp.git
4 changed files with 270 additions and 0 deletions
@ -0,0 +1,270 @@ |
|||||
|
# Resolving Tenant from Route in ABP Framework |
||||
|
|
||||
|
 |
||||
|
|
||||
|
The ABP Framework provides multi-tenancy support with various ways to resolve tenant information, including: Cookie, Header, Domain, Route, and more. |
||||
|
|
||||
|
This article will demonstrate how to resolve tenant information from the route. |
||||
|
|
||||
|
## Tenant Information in Routes |
||||
|
|
||||
|
In the ABP Framework, tenant information in routes is handled by the `RouteTenantResolveContributor`. |
||||
|
|
||||
|
Let's say your application is hosted at `https://abp.io` and you have a tenant named `acme`. You can add the `{__tenant}` variable to your controller or page routes like this: |
||||
|
|
||||
|
```csharp |
||||
|
[Route("{__tenant}/[Controller]")] |
||||
|
public class MyController : MyProjectNameController |
||||
|
{ |
||||
|
[HttpGet] |
||||
|
public IActionResult Get() |
||||
|
{ |
||||
|
return Ok("Hello My Page"); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
```cshtml |
||||
|
@page "{__tenant?}/mypage" |
||||
|
@model MyPageModel |
||||
|
|
||||
|
<html> |
||||
|
<body> |
||||
|
<h1>My Page</h1> |
||||
|
</body> |
||||
|
</html> |
||||
|
``` |
||||
|
|
||||
|
When you access `https://abp.io/acme/my` or `https://abp.io/acme/mypage`, ABP will automatically resolve the tenant information from the route. |
||||
|
|
||||
|
## Adding __tenant to Global Routes |
||||
|
|
||||
|
While we've shown how to add `{__tenant}` to individual controllers or pages, you might want to add it globally to your entire application. Here's how to implement this: |
||||
|
|
||||
|
```cs |
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Microsoft.AspNetCore.Mvc.ApplicationModels; |
||||
|
|
||||
|
namespace MyCompanyName; |
||||
|
|
||||
|
public class AddTenantRouteToPages : IPageRouteModelConvention, IApplicationModelConvention |
||||
|
{ |
||||
|
public void Apply(PageRouteModel model) |
||||
|
{ |
||||
|
var selectorCount = model.Selectors.Count; |
||||
|
var selectorModels = new List<SelectorModel>(); |
||||
|
for (var i = 0; i < selectorCount; i++) |
||||
|
{ |
||||
|
var selector = model.Selectors[i]; |
||||
|
selectorModels.Add(new SelectorModel |
||||
|
{ |
||||
|
AttributeRouteModel = new AttributeRouteModel |
||||
|
{ |
||||
|
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[a-zA-Z0-9]+$)}", selector.AttributeRouteModel!.Template!.RemovePreFix("/")) |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
foreach (var selectorModel in selectorModels) |
||||
|
{ |
||||
|
model.Selectors.Add(selectorModel); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class AddTenantRouteToControllers :IApplicationModelConvention |
||||
|
{ |
||||
|
public void Apply(ApplicationModel application) |
||||
|
{ |
||||
|
var controllers = application.Controllers; |
||||
|
foreach (var controller in controllers) |
||||
|
{ |
||||
|
var selector = controller.Selectors.FirstOrDefault(); |
||||
|
if (selector == null || selector.AttributeRouteModel == null) |
||||
|
{ |
||||
|
controller.Selectors.Add(new SelectorModel |
||||
|
{ |
||||
|
AttributeRouteModel = new AttributeRouteModel |
||||
|
{ |
||||
|
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", controller.ControllerName) |
||||
|
} |
||||
|
}); |
||||
|
controller.Selectors.Add(new SelectorModel |
||||
|
{ |
||||
|
AttributeRouteModel = new AttributeRouteModel |
||||
|
{ |
||||
|
Template = controller.ControllerName |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var template = selector.AttributeRouteModel?.Template; |
||||
|
template = template.IsNullOrWhiteSpace() ? "{__tenant:regex(^[[a-zA-Z0-9]]+$)}" : AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", template.RemovePreFix("/")); |
||||
|
controller.Selectors.Add(new SelectorModel |
||||
|
{ |
||||
|
AttributeRouteModel = new AttributeRouteModel |
||||
|
{ |
||||
|
Template = template |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Register the services: |
||||
|
|
||||
|
```cs |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
//... |
||||
|
|
||||
|
PostConfigure<RazorPagesOptions>(options => |
||||
|
{ |
||||
|
options.Conventions.Add(new AddTenantRouteToPages()); |
||||
|
}); |
||||
|
|
||||
|
PostConfigure<MvcOptions>(options => |
||||
|
{ |
||||
|
options.Conventions.Add(new AddTenantRouteToControllers()); |
||||
|
}); |
||||
|
|
||||
|
// Configure cookie path to prevent authentication cookie loss |
||||
|
context.Services.ConfigureApplicationCookie(x => |
||||
|
{ |
||||
|
x.Cookie.Path = "/"; |
||||
|
}); |
||||
|
//... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
After implementing this, you'll notice that all controllers in your Swagger UI will have the `{__tenant}` route added: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Handling Navigation Links |
||||
|
|
||||
|
To ensure navigation links automatically include tenant information, we need to add middleware that dynamically adds the tenant to the PathBase: |
||||
|
|
||||
|
```cs |
||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
||||
|
{ |
||||
|
//... |
||||
|
app.Use(async (httpContext, next) => |
||||
|
{ |
||||
|
var tenantMatch = Regex.Match(httpContext.Request.Path, "^/([^/.]+)(?:/.*)?$"); |
||||
|
if (tenantMatch.Groups.Count > 1 && !string.IsNullOrEmpty(tenantMatch.Groups[1].Value)) |
||||
|
{ |
||||
|
var tenantName = tenantMatch.Groups[1].Value; |
||||
|
if (!tenantName.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
var tenantStore = httpContext.RequestServices.GetRequiredService<ITenantStore>(); |
||||
|
var tenantNormalizer = httpContext.RequestServices.GetRequiredService<ITenantNormalizer>(); |
||||
|
var tenantInfo = await tenantStore.FindAsync(tenantNormalizer.NormalizeName(tenantName)!); |
||||
|
if (tenantInfo != null) |
||||
|
{ |
||||
|
if (httpContext.Request.Path.StartsWithSegments(new PathString(tenantName.EnsureStartsWith('/')), out var matchedPath, out var remainingPath)) |
||||
|
{ |
||||
|
var originalPath = httpContext.Request.Path; |
||||
|
var originalPathBase = httpContext.Request.PathBase; |
||||
|
httpContext.Request.Path = remainingPath; |
||||
|
httpContext.Request.PathBase = originalPathBase.Add(matchedPath); |
||||
|
try |
||||
|
{ |
||||
|
await next(httpContext); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
httpContext.Request.Path = originalPath; |
||||
|
httpContext.Request.PathBase = originalPathBase; |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await next(httpContext); |
||||
|
}); |
||||
|
app.UseRouting(); |
||||
|
app.MapAbpStaticAssets(); |
||||
|
//... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
 |
||||
|
|
||||
|
After setting the PathBase, we need to add a custom tenant resolver to extract tenant information from the `PathBase`: |
||||
|
|
||||
|
```cs |
||||
|
public class MyRouteTenantResolveContributor : RouteTenantResolveContributor |
||||
|
{ |
||||
|
public const string ContributorName = "MyRoute"; |
||||
|
|
||||
|
public override string Name => ContributorName; |
||||
|
|
||||
|
protected override Task<string?> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext) |
||||
|
{ |
||||
|
var tenantId = httpContext.GetRouteValue(context.GetAbpAspNetCoreMultiTenancyOptions().TenantKey) ?? httpContext.Request.PathBase.ToString(); |
||||
|
var tenantIdStr = tenantId?.ToString()?.RemovePreFix("/"); |
||||
|
return Task.FromResult(!tenantIdStr.IsNullOrWhiteSpace() ? Convert.ToString(tenantIdStr) : null); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Register the MyRouteTenantResolveContributor with the ABP Framework: |
||||
|
|
||||
|
```cs |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
//... |
||||
|
Configure<AbpTenantResolveOptions>(options => |
||||
|
{ |
||||
|
options.TenantResolvers.Add(new MyRouteTenantResolveContributor()); |
||||
|
}); |
||||
|
//... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Modifying abp.appPath |
||||
|
|
||||
|
```csharp |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
//... |
||||
|
context.Services.AddOptions<AbpThemingOptions>().Configure<IServiceProvider>((options, rootServiceProvider) => |
||||
|
{ |
||||
|
var currentTenant = rootServiceProvider.GetRequiredService<ICurrentTenant>(); |
||||
|
if (!currentTenant.Name.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
options.BaseUrl = currentTenant.Name.EnsureStartsWith('/').EnsureEndsWith('/'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
context.Services.RemoveAll(x => x.ServiceType == typeof(IOptions<AbpThemingOptions>)); |
||||
|
context.Services.Add(ServiceDescriptor.Scoped(typeof(IOptions<>), typeof(OptionsManager<>))); |
||||
|
//... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Browser console output: |
||||
|
|
||||
|
```cs |
||||
|
> https://localhost:44303/acme/ |
||||
|
> abp.appPath |
||||
|
> '/acme/' |
||||
|
``` |
||||
|
|
||||
|
## Summary |
||||
|
|
||||
|
By following these steps, you can implement tenant resolution from routes in the ABP Framework and handle navigation links appropriately. This approach provides a clean and maintainable way to manage multi-tenancy in your application. |
||||
|
|
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [ABP Multi-Tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy) |
||||
|
- [Routing in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing) |
||||
|
- [HTML base tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) |
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 536 KiB |
|
After Width: | Height: | Size: 209 KiB |
Loading…
Reference in new issue