diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
index 91f0fd81a3..7ea0fb6708 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
@@ -66,7 +66,7 @@
"AuditLogging": "Audit Logging",
"Caching": "Caching",
"Multitenancy": "Multitenancy",
- "DataFiltering": "Data filtering",
+ "DataFiltering": "Data Filtering",
"ConventionOverConfiguration": "Convention Over Configuration",
"ConventionOverConfigurationExplanation": "ABP implements common application conventions by default with minimal or zero configuration.",
"ConventionOverConfigurationExplanationList1": "Auto registers known services to dependency injection.",
@@ -1889,6 +1889,7 @@
"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 abp.io/contact.",
"BiographyContainsUrlValidationMessage": "Biography cannot contain URL.",
"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\").",
- "SEOTitle": "SEO URL"
+ "SEOTitle": "SEO URL",
+ "InvalidYouTubeUrl": "The URL you entered is not a valid YouTube video link. Please make sure it points to a specific video and try again."
}
}
diff --git a/docs/en/Community-Articles/2025-04-20-IDX10204/header.png b/docs/en/Community-Articles/2025-04-20-IDX10204/header.png
index a71c9a66ff..13109a4515 100644
Binary files a/docs/en/Community-Articles/2025-04-20-IDX10204/header.png and b/docs/en/Community-Articles/2025-04-20-IDX10204/header.png differ
diff --git a/docs/en/Community-Articles/2025-04-20-IDX10204/payload.png b/docs/en/Community-Articles/2025-04-20-IDX10204/payload.png
index a83d6e1051..04b927bbb2 100644
Binary files a/docs/en/Community-Articles/2025-04-20-IDX10204/payload.png and b/docs/en/Community-Articles/2025-04-20-IDX10204/payload.png differ
diff --git a/docs/en/Community-Articles/2025-04-25-AI-Comment-Summarization/summary-card.png b/docs/en/Community-Articles/2025-04-25-AI-Comment-Summarization/summary-card.png
index 35151efafb..9a36f3cd35 100644
Binary files a/docs/en/Community-Articles/2025-04-25-AI-Comment-Summarization/summary-card.png and b/docs/en/Community-Articles/2025-04-25-AI-Comment-Summarization/summary-card.png differ
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..6007e5371f
--- /dev/null
+++ b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/POST.md
@@ -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
+
+
+
+
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:
+
+
+
+## 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();
+ //...
+}
+```
+
+
+
+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..72da9bd571
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..3ae3f6650c
Binary files /dev/null and b/docs/en/Community-Articles/2025-05-19-RouteTenantResolveContributor/ui.png differ
diff --git a/docs/en/framework/data/entity-framework-core/index.md b/docs/en/framework/data/entity-framework-core/index.md
index a50edb2d4a..e462662410 100644
--- a/docs/en/framework/data/entity-framework-core/index.md
+++ b/docs/en/framework/data/entity-framework-core/index.md
@@ -142,24 +142,27 @@ Configure(options =>
Add actions for the `ConfigureConventions` and `OnModelCreating` methods of the `DbContext` as shown below:
````csharp
-options.DefaultConventionAction = (dbContext, builder) =>
+Configure(options =>
{
- // This action is called for ConfigureConventions method of all DbContexts.
-};
+ options.ConfigureDefaultConvention((dbContext, builder) =>
+ {
+ // This action is called for ConfigureConventions method of all DbContexts.
+ });
-options.ConfigureConventions((dbContext, builder) =>
-{
- // This action is called for ConfigureConventions method of specific DbContext.
-});
+ options.ConfigureConventions((dbContext, builder) =>
+ {
+ // This action is called for ConfigureConventions method of specific DbContext.
+ });
-options.DefaultOnModelCreatingAction = (dbContext, builder) =>
-{
- // This action is called for OnModelCreating method of all DbContexts.
-};
+ options.ConfigureDefaultOnModelCreating((dbContext, builder) =>
+ {
+ // This action is called for OnModelCreating method of all DbContexts.
+ });
-options.ConfigureOnModelCreating((dbContext, builder) =>
-{
- // This action is called for OnModelCreating method of specific DbContext.
+ options.ConfigureOnModelCreating((dbContext, builder) =>
+ {
+ // This action is called for OnModelCreating method of specific DbContext.
+ });
});
````
@@ -972,4 +975,4 @@ public class MyCustomEfCoreBulkOperationProvider
* [Entities](../../architecture/domain-driven-design/entities.md)
* [Repositories](../../architecture/domain-driven-design/repositories.md)
-* [Video tutorial](https://abp.io/video-courses/essentials/abp-ef-core)
\ No newline at end of file
+* [Video tutorial](https://abp.io/video-courses/essentials/abp-ef-core)
diff --git a/docs/en/modules/language-management.md b/docs/en/modules/language-management.md
index 3bce0d46b0..62327378a4 100644
--- a/docs/en/modules/language-management.md
+++ b/docs/en/modules/language-management.md
@@ -77,6 +77,11 @@ This module follows the [Entity Best Practices & Conventions](../framework/archi
* `Language` (aggregate root): Represents a language in the system.
* `LanguageText` (aggregate root): Represents a language text in the system.
+##### Dynamic Localization
+
+* `LocalizationResourceRecord` (aggregate root): Represents a localization resource in the system.
+* `LocalizationTextRecord` (aggregate root): Represents all texts of a localization resource in the system.
+
#### Repositories
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:
* `ILanguageRepository`
* `ILanguageTextRepository`
+* `ILocalizationResourceRecordRepository`
+* `ILocalizationTextRecordRepository`
#### Domain Services
@@ -121,6 +128,8 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
* **AbpLanguages**
* **AbpLanguageTexts**
+* **AbpLocalizationResources**
+* **AbpLocalizationTexts**
#### MongoDB
@@ -128,6 +137,8 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
* **AbpLanguages**
* **AbpLanguageTexts**
+* **AbpLocalizationResources**
+* **AbpLocalizationTexts**
### Permissions
diff --git a/docs/en/modules/payment-custom-gateway.md b/docs/en/modules/payment-custom-gateway.md
index 40cd073da6..cdf847b528 100644
--- a/docs/en/modules/payment-custom-gateway.md
+++ b/docs/en/modules/payment-custom-gateway.md
@@ -6,7 +6,7 @@ This document explains creating custom a payment gateway that's different than t
## Creating Core Operations
-- Create **MyPaymentGateway.cs** and implement `IPaymentGateway`
+- Create **MyPaymentGateway.cs** in the **Domain** layer of your project and implement `IPaymentGateway`.
```csharp
public class MyPaymentGateway : IPaymentGateway, ITransientDependency
@@ -57,7 +57,7 @@ This document explains creating custom a payment gateway that's different than t
}
```
-- You should configure your gateway in `PaymentOptions`
+- You should also configure `PaymentOptions` for your gateway in the **Domain** layer of your project as shown below.
```csharp
Configure(options =>
@@ -119,7 +119,7 @@ There are 2 types of pages that are supported by default. You can define a pre-p