Skip to content

Commit 2224134

Browse files
committed
Merge branch 'dev' into feature/ssr-support
# Conflicts: # npm/ng-packs/package.json # npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts
2 parents b079f5e + 490dd6a commit 2224134

File tree

184 files changed

+2080
-1655
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

184 files changed

+2080
-1655
lines changed

abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"AuditLogging": "Audit Logging",
6767
"Caching": "Caching",
6868
"Multitenancy": "Multitenancy",
69-
"DataFiltering": "Data filtering",
69+
"DataFiltering": "Data Filtering",
7070
"ConventionOverConfiguration": "Convention Over Configuration",
7171
"ConventionOverConfigurationExplanation": "ABP implements common application conventions by default with minimal or zero configuration.",
7272
"ConventionOverConfigurationExplanationList1": "Auto registers known services to dependency injection.",
@@ -1889,6 +1889,7 @@
18891889
"FaqIyzicoPaymentIssuesExplanation8": "ABP website doesn't save or process your credit card. We use payment gateways for this and the entire transaction is handled by payment gateways. We have no authority to interfere with the payment process or fix the payment steps. If you have further questions or need additional support, feel free to contact us at <a href='https://abp.io/contact'>abp.io/contact</a>.",
18901890
"BiographyContainsUrlValidationMessage": "Biography cannot contain URL.",
18911891
"CreatePostSEOTitleInfo": "SEO URL is a clean, readable, keyword-rich URL that helps both users and search engines understand what this post is about. Keep it short with 60 characters. SEO titles over 60 characters will be truncated. Use hyphens (-) to separate words (not underscores). Include target keywords near the start. Lowercase only. No stop words unless needed (e.g: \"and\", \"or\", \"the\").",
1892-
"SEOTitle": "SEO URL"
1892+
"SEOTitle": "SEO URL",
1893+
"InvalidYouTubeUrl": "The URL you entered is not a valid YouTube video link. Please make sure it points to a specific video and try again."
18931894
}
18941895
}
Loading
Loading
Loading
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# Resolving Tenant from Route in ABP Framework
2+
3+
The ABP Framework provides multi-tenancy support with various ways to resolve tenant information, including: Cookie, Header, Domain, Route, and more.
4+
5+
This article will demonstrate how to resolve tenant information from the route.
6+
7+
## Tenant Information in Routes
8+
9+
In the ABP Framework, tenant information in routes is handled by the `RouteTenantResolveContributor`.
10+
11+
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:
12+
13+
```csharp
14+
[Route("{__tenant}/[Controller]")]
15+
public class MyController : MyProjectNameController
16+
{
17+
[HttpGet]
18+
public IActionResult Get()
19+
{
20+
return Ok("Hello My Page");
21+
}
22+
}
23+
```
24+
25+
```cshtml
26+
@page "{__tenant?}/mypage"
27+
@model MyPageModel
28+
29+
<html>
30+
<body>
31+
<h1>My Page</h1>
32+
</body>
33+
</html>
34+
```
35+
36+
When you access `https://abp.io/acme/my` or `https://abp.io/acme/mypage`, ABP will automatically resolve the tenant information from the route.
37+
38+
## Adding __tenant to Global Routes
39+
40+
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:
41+
42+
```cs
43+
using System;
44+
using System.Collections.Generic;
45+
using System.Linq;
46+
using Microsoft.AspNetCore.Mvc.ApplicationModels;
47+
48+
namespace MyCompanyName;
49+
50+
public class AddTenantRouteToPages : IPageRouteModelConvention, IApplicationModelConvention
51+
{
52+
public void Apply(PageRouteModel model)
53+
{
54+
var selectorCount = model.Selectors.Count;
55+
var selectorModels = new List<SelectorModel>();
56+
for (var i = 0; i < selectorCount; i++)
57+
{
58+
var selector = model.Selectors[i];
59+
selectorModels.Add(new SelectorModel
60+
{
61+
AttributeRouteModel = new AttributeRouteModel
62+
{
63+
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[a-zA-Z0-9]+$)}", selector.AttributeRouteModel!.Template!.RemovePreFix("/"))
64+
}
65+
});
66+
}
67+
foreach (var selectorModel in selectorModels)
68+
{
69+
model.Selectors.Add(selectorModel);
70+
}
71+
}
72+
}
73+
74+
public class AddTenantRouteToControllers :IApplicationModelConvention
75+
{
76+
public void Apply(ApplicationModel application)
77+
{
78+
var controllers = application.Controllers;
79+
foreach (var controller in controllers)
80+
{
81+
var selector = controller.Selectors.FirstOrDefault();
82+
if (selector == null || selector.AttributeRouteModel == null)
83+
{
84+
controller.Selectors.Add(new SelectorModel
85+
{
86+
AttributeRouteModel = new AttributeRouteModel
87+
{
88+
Template = AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", controller.ControllerName)
89+
}
90+
});
91+
controller.Selectors.Add(new SelectorModel
92+
{
93+
AttributeRouteModel = new AttributeRouteModel
94+
{
95+
Template = controller.ControllerName
96+
}
97+
});
98+
}
99+
else
100+
{
101+
var template = selector.AttributeRouteModel?.Template;
102+
template = template.IsNullOrWhiteSpace() ? "{__tenant:regex(^[[a-zA-Z0-9]]+$)}" : AttributeRouteModel.CombineTemplates("{__tenant:regex(^[[a-zA-Z0-9]]+$)}", template.RemovePreFix("/"));
103+
controller.Selectors.Add(new SelectorModel
104+
{
105+
AttributeRouteModel = new AttributeRouteModel
106+
{
107+
Template = template
108+
}
109+
});
110+
}
111+
}
112+
}
113+
}
114+
```
115+
116+
Register the services:
117+
118+
```cs
119+
public override void ConfigureServices(ServiceConfigurationContext context)
120+
{
121+
//...
122+
123+
PostConfigure<RazorPagesOptions>(options =>
124+
{
125+
options.Conventions.Add(new AddTenantRouteToPages());
126+
});
127+
128+
PostConfigure<MvcOptions>(options =>
129+
{
130+
options.Conventions.Add(new AddTenantRouteToControllers());
131+
});
132+
133+
// Configure cookie path to prevent authentication cookie loss
134+
context.Services.ConfigureApplicationCookie(x =>
135+
{
136+
x.Cookie.Path = "/";
137+
});
138+
//...
139+
}
140+
```
141+
142+
After implementing this, you'll notice that all controllers in your Swagger UI will have the `{__tenant}` route added:
143+
144+
![Swagger UI](./swagger-ui.png)
145+
146+
## Handling Navigation Links
147+
148+
To ensure navigation links automatically include tenant information, we need to add middleware that dynamically adds the tenant to the PathBase:
149+
150+
```cs
151+
public override void OnApplicationInitialization(ApplicationInitializationContext context)
152+
{
153+
//...
154+
app.Use(async (httpContext, next) =>
155+
{
156+
var tenantMatch = Regex.Match(httpContext.Request.Path, "^/([^/.]+)(?:/.*)?$");
157+
if (tenantMatch.Groups.Count > 1 && !string.IsNullOrEmpty(tenantMatch.Groups[1].Value))
158+
{
159+
var tenantName = tenantMatch.Groups[1].Value;
160+
if (!tenantName.IsNullOrWhiteSpace())
161+
{
162+
var tenantStore = httpContext.RequestServices.GetRequiredService<ITenantStore>();
163+
var tenantNormalizer = httpContext.RequestServices.GetRequiredService<ITenantNormalizer>();
164+
var tenantInfo = await tenantStore.FindAsync(tenantNormalizer.NormalizeName(tenantName)!);
165+
if (tenantInfo != null)
166+
{
167+
if (httpContext.Request.Path.StartsWithSegments(new PathString(tenantName.EnsureStartsWith('/')), out var matchedPath, out var remainingPath))
168+
{
169+
var originalPath = httpContext.Request.Path;
170+
var originalPathBase = httpContext.Request.PathBase;
171+
httpContext.Request.Path = remainingPath;
172+
httpContext.Request.PathBase = originalPathBase.Add(matchedPath);
173+
try
174+
{
175+
await next(httpContext);
176+
}
177+
finally
178+
{
179+
httpContext.Request.Path = originalPath;
180+
httpContext.Request.PathBase = originalPathBase;
181+
}
182+
return;
183+
}
184+
}
185+
}
186+
}
187+
188+
await next(httpContext);
189+
});
190+
app.UseRouting();
191+
app.MapAbpStaticAssets();
192+
//...
193+
}
194+
```
195+
196+
![ui](ui.png)
197+
198+
After setting the PathBase, we need to add a custom tenant resolver to extract tenant information from the `PathBase`:
199+
200+
```cs
201+
public class MyRouteTenantResolveContributor : RouteTenantResolveContributor
202+
{
203+
public const string ContributorName = "MyRoute";
204+
205+
public override string Name => ContributorName;
206+
207+
protected override Task<string?> GetTenantIdOrNameFromHttpContextOrNullAsync(ITenantResolveContext context, HttpContext httpContext)
208+
{
209+
var tenantId = httpContext.GetRouteValue(context.GetAbpAspNetCoreMultiTenancyOptions().TenantKey) ?? httpContext.Request.PathBase.ToString();
210+
var tenantIdStr = tenantId?.ToString()?.RemovePreFix("/");
211+
return Task.FromResult(!tenantIdStr.IsNullOrWhiteSpace() ? Convert.ToString(tenantIdStr) : null);
212+
}
213+
}
214+
```
215+
216+
Register the MyRouteTenantResolveContributor with the ABP Framework:
217+
218+
```cs
219+
public override void ConfigureServices(ServiceConfigurationContext context)
220+
{
221+
//...
222+
Configure<AbpTenantResolveOptions>(options =>
223+
{
224+
options.TenantResolvers.Add(new MyRouteTenantResolveContributor());
225+
});
226+
//...
227+
}
228+
```
229+
230+
### Modifying abp.appPath
231+
232+
```csharp
233+
public override void ConfigureServices(ServiceConfigurationContext context)
234+
{
235+
//...
236+
context.Services.AddOptions<AbpThemingOptions>().Configure<IServiceProvider>((options, rootServiceProvider) =>
237+
{
238+
var currentTenant = rootServiceProvider.GetRequiredService<ICurrentTenant>();
239+
if (!currentTenant.Name.IsNullOrWhiteSpace())
240+
{
241+
options.BaseUrl = currentTenant.Name.EnsureStartsWith('/').EnsureEndsWith('/');
242+
}
243+
});
244+
245+
context.Services.RemoveAll(x => x.ServiceType == typeof(IOptions<AbpThemingOptions>));
246+
context.Services.Add(ServiceDescriptor.Scoped(typeof(IOptions<>), typeof(OptionsManager<>)));
247+
//...
248+
}
249+
```
250+
251+
Browser console output:
252+
253+
```cs
254+
> https://localhost:44303/acme/
255+
> abp.appPath
256+
> '/acme/'
257+
```
258+
259+
## Summary
260+
261+
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.
262+
263+
264+
## References
265+
266+
- [ABP Multi-Tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy)
267+
- [Routing in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing)
268+
- [HTML base tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base)
Loading
Loading
Loading

docs/en/framework/data/entity-framework-core/index.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,24 +142,27 @@ Configure<AbpDbContextOptions>(options =>
142142
Add actions for the `ConfigureConventions` and `OnModelCreating` methods of the `DbContext` as shown below:
143143

144144
````csharp
145-
options.DefaultConventionAction = (dbContext, builder) =>
145+
Configure<AbpDbContextOptions>(options =>
146146
{
147-
// This action is called for ConfigureConventions method of all DbContexts.
148-
};
147+
options.ConfigureDefaultConvention((dbContext, builder) =>
148+
{
149+
// This action is called for ConfigureConventions method of all DbContexts.
150+
});
149151

150-
options.ConfigureConventions<YourDbContext>((dbContext, builder) =>
151-
{
152-
// This action is called for ConfigureConventions method of specific DbContext.
153-
});
152+
options.ConfigureConventions<YourDbContext>((dbContext, builder) =>
153+
{
154+
// This action is called for ConfigureConventions method of specific DbContext.
155+
});
154156

155-
options.DefaultOnModelCreatingAction = (dbContext, builder) =>
156-
{
157-
// This action is called for OnModelCreating method of all DbContexts.
158-
};
157+
options.ConfigureDefaultOnModelCreating((dbContext, builder) =>
158+
{
159+
// This action is called for OnModelCreating method of all DbContexts.
160+
});
159161

160-
options.ConfigureOnModelCreating<YourDbContext>((dbContext, builder) =>
161-
{
162-
// This action is called for OnModelCreating method of specific DbContext.
162+
options.ConfigureOnModelCreating<YourDbContext>((dbContext, builder) =>
163+
{
164+
// This action is called for OnModelCreating method of specific DbContext.
165+
});
163166
});
164167
````
165168

@@ -972,4 +975,4 @@ public class MyCustomEfCoreBulkOperationProvider
972975

973976
* [Entities](../../architecture/domain-driven-design/entities.md)
974977
* [Repositories](../../architecture/domain-driven-design/repositories.md)
975-
* [Video tutorial](https://abp.io/video-courses/essentials/abp-ef-core)
978+
* [Video tutorial](https://abp.io/video-courses/essentials/abp-ef-core)

docs/en/modules/language-management.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ This module follows the [Entity Best Practices & Conventions](../framework/archi
7777
* `Language` (aggregate root): Represents a language in the system.
7878
* `LanguageText` (aggregate root): Represents a language text in the system.
7979

80+
##### Dynamic Localization
81+
82+
* `LocalizationResourceRecord` (aggregate root): Represents a localization resource in the system.
83+
* `LocalizationTextRecord` (aggregate root): Represents all texts of a localization resource in the system.
84+
8085
#### Repositories
8186

8287
This module follows the [Repository Best Practices & Conventions](../framework/architecture/best-practices/repositories.md) guide.
@@ -85,6 +90,8 @@ Following custom repositories are defined for this module:
8590

8691
* `ILanguageRepository`
8792
* `ILanguageTextRepository`
93+
* `ILocalizationResourceRecordRepository`
94+
* `ILocalizationTextRecordRepository`
8895

8996
#### Domain Services
9097

@@ -121,13 +128,17 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
121128

122129
* **AbpLanguages**
123130
* **AbpLanguageTexts**
131+
* **AbpLocalizationResources**
132+
* **AbpLocalizationTexts**
124133

125134
#### MongoDB
126135

127136
##### Collections
128137

129138
* **AbpLanguages**
130139
* **AbpLanguageTexts**
140+
* **AbpLocalizationResources**
141+
* **AbpLocalizationTexts**
131142

132143
### Permissions
133144

0 commit comments

Comments
 (0)