|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,268 @@ |
|||
# 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: 275 KiB |
|
After Width: | Height: | Size: 112 KiB |
@ -1,3 +1,52 @@ |
|||
# Configuration |
|||
|
|||
ASP.NET Core has an flexible and extensible key-value based configuration system. In fact, the configuration system is a part of Microsoft.Extensions libraries and it is independent from ASP.NET Core. That means it can be used in any type of application. See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) to learn the configuration infrastructure. ABP is 100% compatible with the configuration system. |
|||
ASP.NET Core has an flexible and extensible key-value based configuration system. The configuration system is a part of Microsoft.Extensions libraries and it is independent from ASP.NET Core. That means it can be used in any type of application. |
|||
|
|||
See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) to learn the configuration infrastructure. ABP is 100% compatible with the configuration system. |
|||
|
|||
## Getting the Configuration |
|||
|
|||
You may need to get the `IConfiguration` service in various places in your codebase. The following section shows two common ways. |
|||
|
|||
### In Module Classes |
|||
|
|||
You typically need to get configuration while initializing your application. You can get the `IConfiguration` service using the `ServiceConfigurationContext.Configuration` property inside your [module class](../architecture/modularity/basics.md) as the following example: |
|||
|
|||
````csharp |
|||
public class MyAppModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var connectionString = context.Configuration["ConnectionStrings:Default"]; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`context.Configuration` is a shortcut property for the `context.Services.GetConfiguration()` method. In general, prefer using `context.Configuration` for simplicity and readability when working within module classes. Use `context.Services.GetConfiguration()` in other contexts where you have an `IServiceCollection` object but do not have access to the `context.Configuration` property. (`IServiceCollection.GetConfiguration` is an extension method that can be used whenever you have an `IServiceCollection` object). |
|||
|
|||
### In Your Services |
|||
|
|||
You can directly [inject](dependency-injection.md) the `IConfiguration` service into your services: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IConfiguration _configuration; |
|||
|
|||
public MyService(IConfiguration configuration) |
|||
{ |
|||
_configuration = configuration; |
|||
} |
|||
|
|||
public string? GetConnectionString() |
|||
{ |
|||
return _configuration["ConnectionStrings:Default"]; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
## See Also |
|||
|
|||
* [Microsoft's Configuration Documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) |
|||
* [The Options Pattern](options.md) |
|||
|
|||
|
|||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.5 MiB |
@ -0,0 +1,23 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.AspNetCore.Components.Web.Configuration; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Components.MauiBlazor; |
|||
|
|||
[Dependency(ReplaceServices = true)] |
|||
public class MauiCurrentApplicationConfigurationCacheResetService : |
|||
ICurrentApplicationConfigurationCacheResetService, |
|||
ITransientDependency |
|||
{ |
|||
private readonly MauiBlazorCachedApplicationConfigurationClient _mauiBlazorCachedApplicationConfigurationClient; |
|||
|
|||
public MauiCurrentApplicationConfigurationCacheResetService(MauiBlazorCachedApplicationConfigurationClient mauiBlazorCachedApplicationConfigurationClient) |
|||
{ |
|||
_mauiBlazorCachedApplicationConfigurationClient = mauiBlazorCachedApplicationConfigurationClient; |
|||
} |
|||
|
|||
public async Task ResetAsync() |
|||
{ |
|||
await _mauiBlazorCachedApplicationConfigurationClient.InitializeAsync(); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
@typeparam TEntity |
|||
@typeparam TResourceType |
|||
@using Volo.Abp.BlazoriseUI |
|||
@using Volo.Abp.Localization |
|||
@inherits ExtensionPropertyComponentBase<TEntity, TResourceType> |
|||
|
|||
@if (PropertyInfo != null && Entity != null) |
|||
{ |
|||
<Validation Validator="@Validate" MessageLocalizer="@LH.Localize"> |
|||
<Field> |
|||
<FieldLabel>@PropertyInfo.GetLocalizedDisplayName(StringLocalizerFactory)</FieldLabel> |
|||
<MemoEdit @bind-Text="@Value" AutoSize Disabled="IsReadonlyField"> |
|||
<Feedback> |
|||
<ValidationError/> |
|||
</Feedback> |
|||
</MemoEdit> |
|||
</Field> |
|||
</Validation> |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace Volo.Abp.BlazoriseUI.Components.ObjectExtending; |
|||
public partial class TextAreaExtensionProperty<TEntity, TResourceType> |
|||
where TEntity : IHasExtraProperties |
|||
{ |
|||
protected string? Value { |
|||
get { |
|||
return PropertyInfo.GetTextInputValueOrNull(Entity.GetProperty(PropertyInfo.Name)); |
|||
} |
|||
set { |
|||
Entity.SetProperty(PropertyInfo.Name, value, validate: false); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
#ProfileManagementWrapper .tab-content { |
|||
padding-top: 0 !important; |
|||
} |
|||
|
After Width: | Height: | Size: 3.1 KiB |