Hi,
We’re using the subdomain tenant resolver and need to configure a single “master” entry point while still allowing tenant-specific subdomains. Below is our current behavior, edge case, and questions in one:
Desired routing behavior
- Host Dashboard & tenant registration:
– Entry point:
app.mydomain.io - Tenant access:
– Existing tenants via
tenant1.mydomain.io,tenant2.mydomain.io, etc.
- Host Dashboard & tenant registration:
– Entry point:
Current issue
- Any unknown subdomain (e.g.
dkfk.mydomain.io) also falls back to the Host Dashboard, even when the subdomain doesn’t match an existing tenant.
- Any unknown subdomain (e.g.
Edge case: tenant named “app”
- If a tenant chooses “app” as their short name,
app.mydomain.iowould resolve to that tenant instead of the Host Dashboard.
- If a tenant chooses “app” as their short name,
Questions
Is there built-in ASP.NET Zero support for reserving certain subdomains (e.g. “app”, “www”, “admin”) so they always route to the host context?
If not, can you share a recommended approach—configuration settings or example code (e.g. a custom
ITenantResolveContributor)—to:- Force only
app.mydomain.ioto resolve to the Host Dashboard, and - Prevent tenants from registering the reserved name “app.”
- Force only
Thanks in advance for any guidance or sample snippets!
Best regards Fabian
1 Answer(s)
-
0
Hi Fabian,
That's an excellent question and a very common requirement for customizing multi-tenant applications.
While ASP.NET Zero doesn't have a simple, single configuration setting for reserving subdomains, its framework is perfectly designed to handle this with a custom, code-based solution. The approach involves two main parts: a custom tenant resolver for routing and a custom tenant manager for validation.
Here is the recommended approach with code examples to achieve your desired behavior.
Forcing
app.mydomain.comto the Host DashboardTo control how subdomains are resolved, you'll replace the default tenant resolver with a custom one. This class will contain your specific logic: always treat "app" as the host, and for everything else, try to find a tenant.
Create the Custom Tenant Resolver Class
In your
*.Coreproject, create a new class. A good location would be in aMultiTenancyfolder.ReservedSubdomainTenantResolveContributor.csnamespace YourProjectName.MultiTenancy; public class ReservedSubdomainTenantResolveContributor : ITenantResolveContributor, ITransientDependency { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IConfigurationRoot _appConfiguration; private readonly ITenantStore _tenantStore; public ReservedSubdomainTenantResolveContributor( IHttpContextAccessor httpContextAccessor, IAppConfigurationAccessor configurationAccessor, ITenantStore tenantStore) { _httpContextAccessor = httpContextAccessor; _appConfiguration = configurationAccessor.Configuration; _tenantStore = tenantStore; } public int? ResolveTenantId() { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) { // Can't resolve tenant in a non-HTTP context. return null; } var siteRootAddress = _appConfiguration["App:ServerRootAddress"]; if (string.IsNullOrEmpty(siteRootAddress)) { return null; } var hostName = httpContext.Request.Host.Host.RemovePreFix("www."); var siteHost = new Uri(siteRootAddress).Host.RemovePreFix("www."); if (hostName.Equals(siteHost, StringComparison.OrdinalIgnoreCase)) { return null; } var subdomain = hostName.Replace($".{siteHost}", "", StringComparison.OrdinalIgnoreCase); if (subdomain.Equals("app", StringComparison.OrdinalIgnoreCase)) { return null; } var tenant = _tenantStore.Find(subdomain); if (tenant != null) { return tenant.Id; } return null; } }Register the Custom Resolver
Now, you need to tell the application to use your new class. In your
*CoreModule.csfile, modify thePreInitializemethod to clear the default resolvers and add your own.YourProjectNameCoreModule.csusing YourProjectName.MultiTenancy; public class YourProjectNameCoreModule : AbpModule { public override void PreInitialize() { // ... other configurations // It's crucial to Clear() existing resolvers to prevent conflicts and ensure only your logic runs. Configuration.MultiTenancy.Resolvers.Clear(); Configuration.MultiTenancy.Resolvers.Add<ReservedSubdomainTenantResolveContributor>(); // ... other configurations } // ... }
To prevent tenants from registering names like "app", "www", etc., you'll extend the
TenantManagerand add your validation logic.Create a Custom Tenant Manager
In the same
*.Core/MultiTenancyfolder, create the following class.CustomTenantManager.csnamespace YourProjectName.MultiTenancy; public class CustomTenantManager : TenantManager { private readonly List<string> _reservedTenancyNames = new List<string> { "app", "www", "admin", "host", "support", "mail" }; //.. Constructor public override async Task CreateAsync(Tenant tenant) { if (_reservedTenancyNames.Any(name => name.Equals(tenant.TenancyName, System.StringComparison.InvariantCultureIgnoreCase))) { throw new UserFriendlyException("The tenancy name '" + tenant.TenancyName + "' is reserved and cannot be used."); } await base.CreateAsync(tenant); } }Register the Custom Tenant Manager
Finally, replace the default
TenantManagerwith your custom one using the dependency injection system. Add the following line to thePreInitializemethod in your*CoreModule.cs.YourProjectNameCoreModule.cspublic override void PreInitialize() { // ... Configuration.ReplaceService<ITenantManager, CustomTenantManager>(Abp.Dependency.DependencyLifeStyle.Transient); // ... }By implementing this two-part solution, you gain full control over your application's subdomain routing and tenant registration logic.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)