diff --git a/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/POST.md b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/POST.md new file mode 100644 index 0000000000..edde1a730a --- /dev/null +++ b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/POST.md @@ -0,0 +1,270 @@ +# Resolving Tenant from Route in ABP Framework + +![cover](cover.png) + +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 + + + +

My Page

+ + +``` + +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(); + 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(options => + { + options.Conventions.Add(new AddTenantRouteToPages()); + }); + + PostConfigure(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: + +![Swagger UI](./swagger-ui.png) + +## 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(); + var tenantNormalizer = httpContext.RequestServices.GetRequiredService(); + 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(); + //... +} +``` + +![ui](ui.png) + +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 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(options => + { + options.TenantResolvers.Add(new MyRouteTenantResolveContributor()); + }); + //... +} +``` + +### Modifying abp.appPath + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + //... + context.Services.AddOptions().Configure((options, rootServiceProvider) => + { + var currentTenant = rootServiceProvider.GetRequiredService(); + if (!currentTenant.Name.IsNullOrWhiteSpace()) + { + options.BaseUrl = currentTenant.Name.EnsureStartsWith('/').EnsureEndsWith('/'); + } + }); + + context.Services.RemoveAll(x => x.ServiceType == typeof(IOptions)); + 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) diff --git a/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/cover.png b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/cover.png new file mode 100644 index 0000000000..ebd7978ac8 Binary files /dev/null and b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/cover.png differ diff --git a/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/swagger-ui.png b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/swagger-ui.png new file mode 100644 index 0000000000..6762c19e94 Binary files /dev/null and b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/swagger-ui.png differ diff --git a/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/ui.png b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/ui.png new file mode 100644 index 0000000000..58ba3578eb Binary files /dev/null and b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/ui.png differ