diff --git a/docs/en/Community-Articles/2026-07-17-WAD-RECAP/post.md b/docs/en/Community-Articles/2026-07-17-WAD-RECAP/post.md
index e3cfad2361..69e5fe24db 100644
--- a/docs/en/Community-Articles/2026-07-17-WAD-RECAP/post.md
+++ b/docs/en/Community-Articles/2026-07-17-WAD-RECAP/post.md
@@ -1,24 +1,24 @@
-WeAreDevelopers World Congress 2026 has come to an end, and we'd like to thank everyone who stopped by the ABP booth in Berlin!
+WeAreDevelopers World Congress 2026 has come to an end, and we'd like to thank everyone who stopped by the ABP booth in Berlin!
-We had the opportunity to meet developers, architects, engineering leaders, and technology enthusiasts from around the world. It was a pleasure connecting with so many members of the developer community, hearing about the projects you're building, and discussing the challenges and opportunities shaping modern software development.
+We had the opportunity to meet developers, architects, engineering leaders, and technology enthusiasts from around the world. It was a pleasure connecting with so many members of the developer community, hearing about the projects you're building, and discussing the challenges and opportunities shaping modern software development.


-## **Great Conversations and Product Demos**
+## **Great Conversations and Product Demos**
-Throughout the event, our team showcased the latest developments across the ABP ecosystem, including ABP Framework, ABP Studio, and our AI-powered development capabilities.
+Throughout the event, our team showcased the latest developments across the ABP ecosystem, including ABP Framework, ABP Studio, and our AI-powered development capabilities.
-We had countless conversations about modular application development, clean architecture, microservices, AI-assisted development, and how teams can build enterprise applications faster while maintaining long-term quality and maintainability.
+We had countless conversations about modular application development, clean architecture, microservices, AI-assisted development, and how teams can build enterprise applications faster while maintaining long-term quality and maintainability.
-Thank you to everyone who shared feedback, asked questions, and explored how ABP can support your development journey.
+Thank you to everyone who shared feedback, asked questions, and explored how ABP can support your development journey.


-## **Sharing Our Experience on Stage**
+## **Sharing Our Experience on Stage**
In addition to connecting with attendees at our booth, we were proud to see our Co-founder, **Halil İbrahim Kalkan**, speak at WeAreDevelopers World Congress 2026.
@@ -28,19 +28,19 @@ It was a great opportunity to share the engineering practices and ideas behind A

-## **More Than Just a Conference**
+## **More Than Just a Conference**
-WeAreDevelopers World Congress wasn't only about technical sessions. The event also featured interactive experiences, including a lively arcade gaming area, creating plenty of opportunities for attendees to relax, connect, and enjoy the conference between talks.
+WeAreDevelopers World Congress wasn't only about technical sessions. The event also featured interactive experiences, including a lively arcade gaming area, creating plenty of opportunities for attendees to relax, connect, and enjoy the conference between talks.
-This is the approach I'd recommend. It keeps the ABP story focused while giving you a natural place to include photos or videos of the arcade area.
+This is the approach I'd recommend. It keeps the ABP story focused while giving you a natural place to include photos or videos of the arcade area.
[](https://youtu.be/K2WzoMfO76k)

-## **Meeting The Developer Community**
+## **Meeting The Developer Community**
-One of the best parts of WeAreDevelopers World Congress is bringing together developers, architects, engineering leaders, and technology experts from around the world. The conference featured inspiring keynotes and technical sessions covering AI, software architecture, cloud, developer productivity, and many other topics that are shaping the future of software development.
+One of the best parts of WeAreDevelopers World Congress is bringing together developers, architects, engineering leaders, and technology experts from around the world. The conference featured inspiring keynotes and technical sessions covering AI, software architecture, cloud, developer productivity, and many other topics that are shaping the future of software development.

@@ -48,13 +48,13 @@ It was a great opportunity to share the engineering practices and ideas behind A

-## **Until Next Time**
+## **Until Next Time**
-A big thank you to the WeAreDevelopers team for organizing another fantastic event and to everyone who visited us at Hall A, Booth A-41.
+A big thank you to the WeAreDevelopers team for organizing another fantastic event and to everyone who visited us at Hall A, Booth A-41.
-If we didn't get the chance to meet in Berlin, you can always explore ABP online, join our community, or reach out to us with your questions and feedback.
+If we didn't get the chance to meet in Berlin, you can always explore ABP online, join our community, or reach out to us with your questions and feedback.
-We appreciate everyone who made WeAreDevelopers World Congress 2026 such a memorable experience, and we look forward to seeing you again at future events!
+We appreciate everyone who made WeAreDevelopers World Congress 2026 such a memorable experience, and we look forward to seeing you again at future events!

diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json
index 32c47a832e..118e4bb264 100644
--- a/docs/en/docs-nav.json
+++ b/docs/en/docs-nav.json
@@ -1111,6 +1111,10 @@
"text": "Dynamic C# API Clients",
"path": "framework/api-development/dynamic-csharp-clients.md"
},
+ {
+ "text": "IdentityModel Clients",
+ "path": "framework/api-development/identitymodel-clients.md"
+ },
{
"text": "Integration Services",
"path": "framework/api-development/integration-services.md"
@@ -1354,6 +1358,10 @@
"text": "Auth",
"path": "framework/ui/mvc-razor-pages/javascript-api/auth.md"
},
+ {
+ "text": "Clock",
+ "path": "framework/ui/mvc-razor-pages/javascript-api/clock.md"
+ },
{
"text": "Current User",
"path": "framework/ui/mvc-razor-pages/javascript-api/current-user.md"
@@ -1657,6 +1665,10 @@
"text": "Localization",
"path": "framework/ui/angular/localization.md"
},
+ {
+ "text": "Document Title Strategy",
+ "path": "framework/ui/angular/title-strategy.md"
+ },
{
"text": "Hybrid Localization",
"path": "framework/ui/angular/hybrid-localization.md"
@@ -1876,6 +1888,14 @@
"text": "Card",
"path": "framework/ui/angular/card-component.md"
},
+ {
+ "text": "Tree",
+ "path": "framework/ui/angular/tree-component.md"
+ },
+ {
+ "text": "Lookup Search",
+ "path": "framework/ui/angular/lookup-search-component.md"
+ },
{
"text": "Dynamic Forms",
"path": "framework/ui/angular/dynamic-form-module.md"
@@ -1884,6 +1904,10 @@
"text": "Password Complexity Indicator",
"path": "framework/ui/angular/password-complexity-indicator-component.md"
},
+ {
+ "text": "Commercial UI Components (Pro)",
+ "path": "framework/ui/angular/commercial-ui.md"
+ },
{
"text": "Lookup Components(Pro)",
"path": "framework/ui/angular/lookup-components.md"
@@ -2080,6 +2104,10 @@
"text": "MongoDB",
"path": "framework/data/mongodb"
},
+ {
+ "text": "In-Memory Database",
+ "path": "framework/data/memorydb"
+ },
{
"text": "Dapper",
"path": "framework/data/dapper"
@@ -2615,6 +2643,10 @@
"text": "Background Jobs",
"path": "modules/background-jobs.md"
},
+ {
+ "text": "Blogging",
+ "path": "modules/blogging.md"
+ },
{
"text": "Chat (Pro)",
"path": "modules/chat.md"
@@ -2691,7 +2723,7 @@
},
{
"text": "URL Forwarding System",
- "path": "modules/cms-kit-pro/URL-forwarding.md"
+ "path": "modules/cms-kit-pro/url-forwarding.md"
},
{
"text": "Poll System",
@@ -2733,7 +2765,19 @@
},
{
"text": "Identity",
- "path": "modules/identity.md"
+ "isLazyExpandable": true,
+ "path": "modules/identity.md",
+ "items": [
+ {
+ "text": "Overview",
+ "path": "modules/identity.md",
+ "isIndex": true
+ },
+ {
+ "text": "User Lookup and Synchronization",
+ "path": "modules/identity/user-synchronization.md"
+ }
+ ]
},
{
"text": "Identity (Pro)",
diff --git a/docs/en/framework/api-development/auto-controllers.md b/docs/en/framework/api-development/auto-controllers.md
index 9591e544ed..7b08e0d2b4 100644
--- a/docs/en/framework/api-development/auto-controllers.md
+++ b/docs/en/framework/api-development/auto-controllers.md
@@ -95,6 +95,16 @@ Then the route for getting a book will be '**/api/volosoft/book-store/book/{id}*
* Normalization can be customized by setting the `UrlActionNameNormalizer` option. It's an action delegate that is called for every method.
* If there is another parameter with 'Id' postfix, then it's also added to the route as the final route segment (like '/phoneId').
+When the `UrlControllerNameNormalizer` option is not set, the final controller name also removes suffixes configured in `AbpConventionalControllerOptions.IgnoredUrlSuffixesInControllerNames` (a custom normalizer replaces this ignored-suffix step, so the ignored suffixes are not applied). The default list contains `Integration`, so `PaymentIntegrationService` uses `payment` as its controller route name. You can replace the list when another suffix convention is required:
+
+```csharp
+Configure(options =>
+{
+ options.IgnoredUrlSuffixesInControllerNames =
+ ["Integration", "Endpoint"];
+});
+```
+
#### Customizing the Route Calculation
`IConventionalRouteBuilder` is used to build the route. It is implemented by the `ConventionalRouteBuilder` by default and works as explained above. You can replace/override this service to customize the route calculation strategy.
@@ -225,4 +235,4 @@ services.Configure(options =>
````
## See Also
-* [Video tutorial](https://abp.io/video-courses/essentials/auto-api-controllers)
\ No newline at end of file
+* [Video tutorial](https://abp.io/video-courses/essentials/auto-api-controllers)
diff --git a/docs/en/framework/api-development/dynamic-csharp-clients.md b/docs/en/framework/api-development/dynamic-csharp-clients.md
index d560b536eb..bfacc26886 100644
--- a/docs/en/framework/api-development/dynamic-csharp-clients.md
+++ b/docs/en/framework/api-development/dynamic-csharp-clients.md
@@ -209,6 +209,46 @@ Using `asDefaultServices: false` may only be needed if your application has alre
> If you disable `asDefaultServices`, you can only use `IHttpClientProxy` interface to use the client proxies. See the *IHttpClientProxy Interface* section above.
+### Before Sending a Proxy Request
+
+`AbpHttpClientOptions.AddPreSendAction` registers an action for a named remote service. It receives the proxy configuration, the current request context and the `HttpClient`, and runs immediately before each proxy request is sent.
+
+````csharp
+Configure(options =>
+{
+ options.AddPreSendAction(
+ "BookStore",
+ (_, requestContext, httpClient) =>
+ {
+ if (requestContext.Action.Name == "GetReportAsync")
+ {
+ httpClient.Timeout = TimeSpan.FromMinutes(2);
+ }
+ }
+ );
+});
+````
+
+### Custom Parameter Converters
+
+Dynamic proxies normally use the built-in conversion rules for query-string, form-data and path values. Implement `IObjectToQueryString`, `IObjectToFormData` or `IObjectToPath` when a type requires custom serialization, register the implementation in dependency injection, and map the value type to the converter:
+
+````csharp
+context.Services.AddTransient();
+context.Services.AddTransient();
+context.Services.AddTransient();
+
+Configure(options =>
+{
+ options.QueryStringConverts[typeof(MyFilter)] =
+ typeof(MyFilterToQueryString);
+ options.FormDataConverts[typeof(MyUploadMetadata)] =
+ typeof(MyUploadMetadataToFormData);
+ options.PathConverts[typeof(MyStrongId)] =
+ typeof(MyStrongIdToPath);
+});
+````
+
### Retry/Failure Logic & Polly Integration
If you want to add retry logic for the failing remote HTTP calls for the client proxies, you can configure the `AbpHttpClientBuilderOptions` in the `PreConfigureServices` method of your module class.
diff --git a/docs/en/framework/api-development/identitymodel-clients.md b/docs/en/framework/api-development/identitymodel-clients.md
new file mode 100644
index 0000000000..754c4816bc
--- /dev/null
+++ b/docs/en/framework/api-development/identitymodel-clients.md
@@ -0,0 +1,101 @@
+```json
+//[doc-seo]
+{
+ "Description": "Configure ABP IdentityModel clients for server-to-server access tokens, tenant-aware client selection, and request customization."
+}
+```
+
+# IdentityModel Clients
+
+The `Volo.Abp.IdentityModel` package obtains access tokens for server-to-server HTTP calls. `AbpIdentityModelModule` binds the `IdentityClients` configuration section to `AbpIdentityClientOptions`.
+
+## Installation
+
+Install the `Volo.Abp.IdentityModel` NuGet package in the project that obtains the tokens:
+
+````shell
+abp add-package Volo.Abp.IdentityModel
+````
+
+The command adds the package and the `AbpIdentityModelModule` dependency to the module class.
+
+## Configure Identity Clients
+
+Define a `Default` client and any named clients in the application configuration:
+
+````json
+{
+ "IdentityClients": {
+ "Default": {
+ "GrantType": "client_credentials",
+ "ClientId": "MyProject_Backend",
+ "ClientSecret": "your-client-secret",
+ "Authority": "https://localhost:44301/",
+ "Scope": "MyProject"
+ },
+ "Reporting": {
+ "GrantType": "client_credentials",
+ "ClientId": "MyProject_Reporting",
+ "ClientSecret": "your-reporting-client-secret",
+ "Authority": "https://localhost:44301/",
+ "Scope": "Reporting"
+ }
+ }
+}
+````
+
+When a client name is requested for the current tenant, ABP selects the first available configuration in this order:
+
+1. `.`
+2. `.`
+3. ``
+4. `Default`
+
+If no client name is supplied, ABP uses `Default` as the client name. Tenant-specific entries let a tenant use different credentials without changing the consuming service. For example, `Reporting.8e6fcd0a-75ab-4d94-90f4-9a2503d0e70c` overrides the `Reporting` client for that tenant ID.
+
+Pass the client name to `TryAuthenticateAsync` when you authenticate an `HttpClient` directly:
+
+````csharp
+var client = _httpClientFactory.CreateClient();
+
+if (!await _authenticationService.TryAuthenticateAsync(client, "Reporting"))
+{
+ throw new InvalidOperationException(
+ "The Reporting identity client is not configured."
+ );
+}
+
+var response = await client.GetAsync("https://reporting.example.com/api/reports");
+````
+
+Install `Volo.Abp.Http.Client.IdentityModel` when dynamic HTTP client proxies should obtain tokens automatically. Its `AbpHttpClientIdentityModelModule` integration uses the remote service's `IdentityClient` value when configured, then the remote service name, and finally the `Default` identity client fallback described above:
+
+````json
+{
+ "RemoteServices": {
+ "Reporting": {
+ "BaseUrl": "https://reporting.example.com/",
+ "IdentityClient": "Reporting"
+ }
+ }
+}
+````
+
+## Customize Discovery and Token Requests
+
+Use `IdentityModelHttpRequestMessageOptions.ConfigureHttpRequestMessage` to add headers or otherwise customize the request messages:
+
+````csharp
+Configure(options =>
+{
+ options.ConfigureHttpRequestMessage = request =>
+ {
+ request.Headers.TryAddWithoutValidation(
+ "X-Internal-Client",
+ "MyProject"
+ );
+ };
+});
+````
+
+The callback runs for discovery, client-credentials, password and device-authorization request messages created by the default authentication service.
diff --git a/docs/en/framework/api-development/swagger.md b/docs/en/framework/api-development/swagger.md
index 03808d7869..f07f049555 100644
--- a/docs/en/framework/api-development/swagger.md
+++ b/docs/en/framework/api-development/swagger.md
@@ -110,6 +110,21 @@ services.AddAbpSwaggerGen(
)
```
+### Enum and Schema ID Helpers
+
+ABP provides two additional `SwaggerGenOptions` helpers:
+
+* `UserFriendlyEnums()` changes enum schemas from numeric values to string enum names, making generated contracts easier for clients to consume.
+* `CustomAbpSchemaIds()` uses full type names and includes generic argument names to avoid schema ID collisions.
+
+```csharp
+services.AddAbpSwaggerGen(options =>
+{
+ options.UserFriendlyEnums();
+ options.CustomAbpSchemaIds();
+});
+```
+
## Using Swagger with OAUTH
For non MVC/Tiered applications, we need to configure Swagger with OAUTH to handle authorization.
diff --git a/docs/en/framework/architecture/domain-driven-design/repositories.md b/docs/en/framework/architecture/domain-driven-design/repositories.md
index eed0529112..ea74f0976a 100644
--- a/docs/en/framework/architecture/domain-driven-design/repositories.md
+++ b/docs/en/framework/architecture/domain-driven-design/repositories.md
@@ -249,7 +249,7 @@ public virtual async Task> GetListAsync()
ABP uses dynamic proxying to make these attributes work. There are some rules here:
-* If you are **not injecting** the service over an interface (like `IPersonAppService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](../../../dynamic-proxying-interceptors.md) system can not work.
+* If you are **not injecting** the service over an interface (like `IPersonAppService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](../../infrastructure/interceptors.md) system can not work.
* Only `async` methods (methods returning a `Task` or `Task`) are intercepted.
> Change tracking behavior doesn't affect tracking entity objects returned from `InsertAsync` and `UpdateAsync` methods. The objects returned from these methods are always tracked (if the underlying provider has the change tracking feature) and any change you make to these objects are saved into the database.
@@ -309,9 +309,10 @@ Methods:
- `GetListAsync()`
- `GetQueryableAsync()`
-- `WithDetails()` 1 overload
- `WithDetailsAsync()` 1 overload
+The synchronous `WithDetails()` overloads are obsolete. Use `WithDetailsAsync()` for new code.
+
Whereas the `IReadOnlyBasicRepository` provides the following methods:
- `GetCountAsync()`
diff --git a/docs/en/framework/architecture/domain-driven-design/unit-of-work.md b/docs/en/framework/architecture/domain-driven-design/unit-of-work.md
index 0f50be8f66..613620ab13 100644
--- a/docs/en/framework/architecture/domain-driven-design/unit-of-work.md
+++ b/docs/en/framework/architecture/domain-driven-design/unit-of-work.md
@@ -59,7 +59,7 @@ Configure(options =>
### Option Properties
* `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). A global point to configure the transaction behavior. Default value is `Auto` and work as explained in the "*Database Transaction Behavior*" section above. You can enable (even for HTTP GET requests) or disable transactions with this option.
-* `TimeOut` (`int?`): Used to set the timeout value for UOWs. **Default value is `null`** and uses to the default of the underlying database provider.
+* `Timeout` (`int?`): Used to set the timeout value for UOWs. **Default value is `null`** and uses to the default of the underlying database provider.
* `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional.
## Controlling the Unit Of Work
@@ -93,7 +93,7 @@ Then `MyService` (and any class derived from it) methods will be UOW.
However, there are **some rules should be followed** in order to make it working;
-* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](../../../dynamic-proxying-interceptors.md) system can not work).
+* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](../../infrastructure/interceptors.md) system can not work).
* Only `async` methods (methods returning a `Task` or `Task`) are intercepted. So, sync methods can not start a UOW.
> Notice that if `FooAsync` is called inside a UOW scope, then it already participates to the UOW without needing to the `IUnitOfWorkEnabled` or any other configuration.
@@ -156,13 +156,13 @@ namespace AbpDemo
Again, the **same rules** are valid here:
-* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](../../../dynamic-proxying-interceptors.md) system can not work).
+* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual` (otherwise, [dynamic proxy / interception](../../infrastructure/interceptors.md) system can not work).
* Only `async` methods (methods returning a `Task` or `Task`) are intercepted. So, sync methods can not start a UOW.
#### UnitOfWorkAttribute Properties
* `IsTransactional` (`bool?`): Used to set whether the UOW should be transactional or not. **Default value is `null`**. if you leave it `null`, it is determined automatically based on the conventions and the configuration.
-* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value.
+* `Timeout` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value.
* `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value.
* `IsDisabled` (`bool`): Used to disable the UOW for the current method/class.
@@ -234,7 +234,7 @@ namespace AbpDemo
* `requiresNew` (`bool`): Set `true` to ignore the surrounding unit of work and start a new UOW with the provided options. **Default value is `false`. If it is `false` and there is a surrounding UOW, `Begin` method doesn't actually begin a new UOW, but silently participates to the existing UOW.**
* `isTransactional` (`bool`). Default value is `false`.
* `isolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value.
-* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value.
+* `timeout` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value.
### The Current Unit Of Work
diff --git a/docs/en/framework/architecture/modularity/basics.md b/docs/en/framework/architecture/modularity/basics.md
index 727c40f517..c6ee6f567c 100644
--- a/docs/en/framework/architecture/modularity/basics.md
+++ b/docs/en/framework/architecture/modularity/basics.md
@@ -145,6 +145,43 @@ You can also perform startup logic if your module requires it
> These methods have asynchronous versions too, and if you want to make asynchronous calls inside these methods, override the asynchronous versions instead of the synchronous ones.
+#### Custom Module Lifecycle Contributors
+
+`IModuleLifecycleContributor` is an advanced extension point for adding an application-wide initialization or shutdown phase. A contributor is invoked for every loaded module. Initialization follows module dependency order, while shutdown processes modules in reverse order.
+
+Derive from `ModuleLifecycleContributorBase` and override only the phases you need. Each phase has a synchronous and an asynchronous method; the application calls one of them depending on whether it is initialized synchronously or asynchronously, so override both to cover the two startup paths:
+
+````csharp
+public class MyModuleLifecycleContributor : ModuleLifecycleContributorBase
+{
+ public override Task InitializeAsync(
+ ApplicationInitializationContext context,
+ IAbpModule module)
+ {
+ // Run initialization logic for the current module.
+ return Task.CompletedTask;
+ }
+
+ public override void Initialize(
+ ApplicationInitializationContext context,
+ IAbpModule module)
+ {
+ AsyncHelper.RunSync(() => InitializeAsync(context, module));
+ }
+}
+````
+
+Add the contributor type to `AbpModuleLifecycleOptions.Contributors`:
+
+````csharp
+Configure(options =>
+{
+ options.Contributors.Add();
+});
+````
+
+Contributor order is the order of the `Contributors` list. The four built-in contributors run the pre-initialization, initialization, post-initialization and shutdown callbacks.
+
### Application Shutdown
Lastly, you can override ``OnApplicationShutdown`` method if you want to execute some code while application is being shutdown.
diff --git a/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md b/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md
index 076238b444..d99ea4de8e 100644
--- a/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md
+++ b/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md
@@ -273,6 +273,33 @@ property =>
Use `property.UI.OnCreateForm` and `property.UI.OnEditForm` to control forms too. If a property is required, but not added to the create form, you definitely get a validation exception, so use this option carefully. But a required property may not be in the edit form if that's your requirement.
+### Conditional Availability
+
+An extension property can carry global-feature, tenant-feature and permission policies. Policy-aware object-extension consumers use this metadata to decide whether the property is available for the current application and user.
+
+The following example requires either of two permissions:
+
+````csharp
+property =>
+{
+ property.Policy.Permissions.PermissionNames =
+ [
+ "MyProject.Users.Manage",
+ "MyProject.Users.ManageExtendedProfile"
+ ];
+}
+````
+
+The available policy groups are:
+
+* `Policy.GlobalFeatures.Features` for application-wide global features.
+* `Policy.Features.Features` for the current tenant's features.
+* `Policy.Permissions.PermissionNames` for the current principal's permissions.
+
+`RequiresAll` is `false` by default for each group, so any configured name in that group is sufficient. Set the corresponding `RequiresAll` property to `true` to require every name. When more than one group is configured, every configured group must pass. An empty group imposes no restriction.
+
+These policies do not replace the `UI` and `Api` availability options. They add current feature and permission checks to consumers that evaluate extension-property policies.
+
### UI Order
When you define a property, it appears on the data table, create and edit forms on the related UI page. However, you can control its order. Example:
diff --git a/docs/en/framework/data/entity-framework-core/index.md b/docs/en/framework/data/entity-framework-core/index.md
index 418c040565..25b3e00b6b 100644
--- a/docs/en/framework/data/entity-framework-core/index.md
+++ b/docs/en/framework/data/entity-framework-core/index.md
@@ -651,7 +651,7 @@ In addition to the read-only repositories, ABP allows to manually control the ch
## Access to the EF Core API
-In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example:
+In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContextAsync()` or `GetDbSetAsync()` extension methods. Example:
````csharp
public async Task TestAsync()
diff --git a/docs/en/framework/data/memorydb/index.md b/docs/en/framework/data/memorydb/index.md
new file mode 100644
index 0000000000..247d2b1e8f
--- /dev/null
+++ b/docs/en/framework/data/memorydb/index.md
@@ -0,0 +1,77 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to use ABP's in-memory database provider, register a MemoryDb context and repositories, and customize entity serialization."
+}
+```
+
+# In-Memory Database Provider
+
+The `Volo.Abp.MemoryDb` package implements ABP repositories with an in-process database. It is useful for tests and other non-durable scenarios. Data is kept in the application process and is lost when the process stops.
+
+## Installation
+
+Install the `Volo.Abp.MemoryDb` NuGet package in the data-access project:
+
+````shell
+abp add-package Volo.Abp.MemoryDb
+````
+
+The command adds the package and the `AbpMemoryDbModule` dependency to the module class. You can also configure the dependency manually as shown in the next section.
+
+## Configure the Module
+
+Add `AbpMemoryDbModule` as a dependency of your module:
+
+````csharp
+[DependsOn(typeof(AbpMemoryDbModule))]
+public class MyDataModule : AbpModule
+{
+}
+````
+
+## Create a MemoryDb Context
+
+Derive a class from `MemoryDbContext` and return the entity types managed by the context:
+
+````csharp
+public class MyMemoryDbContext : MemoryDbContext
+{
+ private static readonly Type[] EntityTypes =
+ [
+ typeof(Book),
+ typeof(Author)
+ ];
+
+ public override IReadOnlyList GetEntityTypes()
+ {
+ return EntityTypes;
+ }
+}
+````
+
+Register the context in the `ConfigureServices` method of your module:
+
+````csharp
+context.Services.AddMemoryDbContext(options =>
+{
+ options.AddDefaultRepositories();
+});
+````
+
+`AddDefaultRepositories()` registers default repositories for the aggregate roots returned by the context. Pass `includeAllEntities: true` when default repositories are also needed for other entity types.
+
+MemoryDb repositories use ABP's unit-of-work-aware database provider. Repository operations require an active [unit of work](../../architecture/domain-driven-design/unit-of-work.md).
+
+## JSON Serialization
+
+MemoryDb stores serialized entity values. Configure `Utf8JsonMemoryDbSerializerOptions` to customize the underlying `System.Text.Json` options:
+
+````csharp
+Configure(options =>
+{
+ options.JsonSerializerOptions.Converters.Add(
+ new MyEntityJsonConverter()
+ );
+});
+````
diff --git a/docs/en/framework/data/mongodb/index.md b/docs/en/framework/data/mongodb/index.md
index b609e22963..5349decde4 100644
--- a/docs/en/framework/data/mongodb/index.md
+++ b/docs/en/framework/data/mongodb/index.md
@@ -352,6 +352,31 @@ services:
### Advanced Topics
+#### MongoDB DateTime Serialization
+
+ABP applies a clock-aware MongoDB serializer to writable `DateTime` and nullable `DateTime` properties in ABP entity mappings by default. It uses the configured [clock](../../infrastructure/timing.md) kind when serializing these properties. Disable this handling when the application configures its own serialization for the mapped properties:
+
+```csharp
+Configure(options =>
+{
+ options.UseAbpClockHandleDateTime = false;
+});
+```
+
+#### Configuring MongoClientSettings
+
+`AbpMongoDbContextOptions.MongoClientSettingsConfigurer` runs before ABP creates a `MongoClient`. Use it for driver settings that are not part of the connection string, such as timeouts or TLS configuration:
+
+```csharp
+Configure(options =>
+{
+ options.MongoClientSettingsConfigurer = settings =>
+ {
+ settings.ConnectTimeout = TimeSpan.FromSeconds(10);
+ };
+});
+```
+
### Controlling the Multi-Tenancy
If your solution is [multi-tenant](../../architecture/multi-tenancy), tenants may have **separate databases**, you have **multiple** `DbContext` classes in your solution and some of your `DbContext` classes should be usable **only from the host side**, it is suggested to add `[IgnoreMultiTenancy]` attribute on your `DbContext` class. In this case, ABP guarantees that the related `DbContext` always uses the host [connection string](../../fundamentals/connection-strings.md), even if you are in a tenant context.
diff --git a/docs/en/framework/fundamentals/application-startup.md b/docs/en/framework/fundamentals/application-startup.md
index 4c323c1c52..39a849b7bc 100644
--- a/docs/en/framework/fundamentals/application-startup.md
+++ b/docs/en/framework/fundamentals/application-startup.md
@@ -213,6 +213,11 @@ We've passed a lambda method to configure the `ApplicationName` option. Here's a
* `ApplicationName`: A human-readable name for the application. It is a unique value for an application.
* `Configuration`: Can be used to setup the [application configuration](./configuration.md) when it is not provided by the hosting system. It is not needed for ASP.NET Core and other .NET hosted applications. However, if you've used `AbpApplicationFactory` with an internal service provider, you can use this option to configure how the application configuration is built.
+ * `FileName` (default: `appsettings`), `Optional` (default: `true`) and `ReloadOnChange` (default: `true`) configure the JSON files.
+ * The builder loads `.json` first and then the optional `.secrets.json` file. When `EnvironmentName` is set, it loads `..json` after both files.
+ * `EnvironmentName` adds the corresponding environment-specific JSON file. In the `Development` environment, user secrets are added from `UserSecretsId` when it is set; otherwise from `UserSecretsAssembly`.
+ * `BasePath` changes the configuration file base path. The current directory is used by default.
+ * `EnvironmentVariablesPrefix` filters environment variables, and `CommandLineArgs` adds command-line configuration after environment variables.
* `Environment`: Environment name for the application.
* `PlugInSources`: A list of plugin sources. See the [Plug-In Modules documentation](../architecture/modularity/plugin-modules.md) to learn how to work with plugins.
* `Services`: The `IServiceCollection` object that can be used to register service dependencies. You generally don't need that, because you configure your services in your [module class](../architecture/modularity/basics.md). However, it can be used while writing extension methods for the `AbpApplicationCreationOptions` class.
diff --git a/docs/en/framework/fundamentals/caching.md b/docs/en/framework/fundamentals/caching.md
index f52cd9e07d..8f7b85e37e 100644
--- a/docs/en/framework/fundamentals/caching.md
+++ b/docs/en/framework/fundamentals/caching.md
@@ -214,6 +214,58 @@ public class BookService : ITransientDependency
}
````
+## Hybrid Cache
+
+ABP registers Microsoft's `HybridCache` together with typed ABP wrappers when the `Volo.Abp.Caching` module is used. Hybrid caching keeps a local in-process cache and can use the configured `IDistributedCache` as a secondary cache.
+
+Use `IHybridCache` for string keys or `IHybridCache` for another key type:
+
+````csharp
+using Volo.Abp.Caching.Hybrid;
+using Volo.Abp.DependencyInjection;
+
+public class BookCacheItem
+{
+ public string Name { get; set; } = string.Empty;
+}
+
+public class BookService : ITransientDependency
+{
+ private readonly IHybridCache _cache;
+
+ public BookService(IHybridCache cache)
+ {
+ _cache = cache;
+ }
+
+ public Task GetAsync(Guid bookId)
+ {
+ return _cache.GetOrCreateAsync(
+ bookId,
+ () => LoadBookAsync(bookId)
+ );
+ }
+
+ private Task LoadBookAsync(Guid bookId)
+ {
+ // Load the item from its source.
+ throw new NotImplementedException();
+ }
+}
+````
+
+The typed wrapper uses the same cache-name and tenant-aware key normalization conventions as ABP's distributed cache. Use `CacheName` on the cache item type to set its cache name and `IgnoreMultiTenancy` to share entries between tenants. A custom key type is converted with its `ToString()` method.
+
+The main operations are `GetOrCreateAsync`, `SetAsync`, `RemoveAsync` and `RemoveManyAsync`. Each operation has a nullable `hideErrors` argument. When it is `null`, `AbpHybridCacheOptions.HideErrors` is used; its default is `true`. Hidden errors are logged and sent to the exception notification system. `GetOrCreateAsync` can return `null` when a cache error is hidden.
+
+### Hybrid Cache and Unit of Work
+
+The hybrid-cache methods have a `considerUow` argument that defaults to `false`. When it is `true` and a unit of work is active, cache changes are visible inside that unit of work and are applied to the real cache only after the unit of work completes successfully. A rolled-back unit of work does not apply those changes.
+
+### Hybrid Cache Entry Options
+
+Pass `HybridCacheEntryOptions` to an individual `SetAsync` call when it needs a custom expiration. `AbpHybridCacheOptions.GlobalHybridCacheEntryOptions` is used by `SetAsync` when no per-call options are supplied, and `ConfigureCache()` can set the corresponding default for a cache item type.
+
## Configuration
### AbpDistributedCacheOptions
@@ -233,7 +285,7 @@ Configure(options =>
* `HideErrors` (`bool`, default: `true`): Enables or disables hiding errors when reading from or writing to the cache server. In the **development** environment, this option is **disabled** to help developers detect and fix any cache server issues.
-* `KeyPrefix` (`string`, default: `null`): If your cache server is shared by multiple applications, you can set a prefix for the cache keys for your application. In this case, different applications can not overwrite each other's cache items.
+* `KeyPrefix` (`string`, default: an empty string): If your cache server is shared by multiple applications, you can set a prefix for the cache keys for your application. In this case, different applications can not overwrite each other's cache items.
* `GlobalCacheEntryOptions` (`DistributedCacheEntryOptions`): Used to set default distributed cache options (like `AbsoluteExpiration` and `SlidingExpiration`) used when you don't specify the options while saving cache items. The default value uses the `SlidingExpiration` as 20 minutes.
## Error Handling
diff --git a/docs/en/framework/fundamentals/dependency-injection.md b/docs/en/framework/fundamentals/dependency-injection.md
index 2e2e3e377a..9ceedf0ebf 100644
--- a/docs/en/framework/fundamentals/dependency-injection.md
+++ b/docs/en/framework/fundamentals/dependency-injection.md
@@ -547,7 +547,7 @@ public class AppModule : AbpModule
This example simply checks if the service class has `MyLogAttribute` attribute and adds `MyLogInterceptor` to the interceptor list if so.
-> Notice that `OnRegistered` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](../../dynamic-proxying-interceptors.md) of dynamic proxying / interceptors.
+> Notice that `OnRegistered` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](../infrastructure/interceptors.md) of dynamic proxying / interceptors.
### IServiceCollection.OnActivated Event
diff --git a/docs/en/framework/fundamentals/exception-handling.md b/docs/en/framework/fundamentals/exception-handling.md
index 5ccfcf124a..f9e659f29b 100644
--- a/docs/en/framework/fundamentals/exception-handling.md
+++ b/docs/en/framework/fundamentals/exception-handling.md
@@ -344,6 +344,20 @@ Here, a list of the options you can configure:
* `SendExceptionsDetailsToClients` (default: `false`): You can enable or disable sending exception details to the client.
* `SendStackTraceToClients` (default: `true`): You can enable or disable sending the stack trace of exception to the client. If you want to send the stack trace to the client, you must set both `SendStackTraceToClients` and `SendExceptionsDetailsToClients` options to `true` otherwise, the stack trace will not be sent to the client.
+* `SendExceptionDataToClientTypes`: Exception types whose `Data` dictionary is copied to the remote error response. The default list contains `IBusinessException`, so business exception data is sent to clients. Derived and implementing types are matched.
+* `ExcludeExceptionFromLoggerSelectors`: Predicates that suppress matching exceptions from the ABP exception log. Add a selector when an expected exception should still produce an error response but should not be logged by the exception pipeline.
+
+Example:
+
+````csharp
+Configure(options =>
+{
+ options.SendExceptionDataToClientTypes.Add(typeof(MyClientVisibleException));
+ options.ExcludeExceptionFromLoggerSelectors.Add(
+ exception => exception is MyExpectedException
+ );
+});
+````
## See Also
diff --git a/docs/en/framework/fundamentals/localization.md b/docs/en/framework/fundamentals/localization.md
index 5fd243eeef..4caed371d3 100644
--- a/docs/en/framework/fundamentals/localization.md
+++ b/docs/en/framework/fundamentals/localization.md
@@ -108,9 +108,9 @@ You can also use nesting or array in localization files, like this:
"Hello": {
"World": "Hello World!"
},
- "Hi":[
- "Bye": "Bye World!"
- "Hello": "Hello World!"
+ "Hi": [
+ "Bye World!",
+ "Hello World!"
]
}
}
@@ -189,6 +189,21 @@ public class TestResource
See the Getting Localized Test / Client Side section below.
+### Non-Typed Resources
+
+Most localization resources are represented by a class, which allows you to inject `IStringLocalizer`. You can also register a resource by name without creating a resource class. This is useful when a resource is identified only by its name. An external localization store can also return a non-typed resource for a resource name discovered at runtime.
+
+````csharp
+Configure(options =>
+{
+ options.Resources
+ .Add("CountryNames", "en")
+ .AddVirtualJson("/Localization/Resources/CountryNames");
+});
+````
+
+Use `IStringLocalizerFactory` to access a non-typed resource, as described in the *Creating A Localizer By Resource Name* section below.
+
### Inherit From Other Resources
A resource can inherit from other resources which makes possible to re-use existing localization strings without referring the existing resource. Example:
@@ -215,6 +230,18 @@ services.Configure(options =>
* A resource may inherit from multiple resources.
* If the new resource defines the same localized string, it overrides the string.
+A resource can also inherit from a typed or non-typed resource by its resource name:
+
+````csharp
+Configure(options =>
+{
+ options.Resources
+ .Add("en")
+ .AddVirtualJson("/Localization/Resources/Test")
+ .AddBaseResources("CountryNames");
+});
+````
+
### Extending Existing Resource
Inheriting from a resource creates a new resource without modifying the existing one. In some cases, you may want to not create a new resource but directly extend an existing resource. Example:
@@ -230,6 +257,51 @@ services.Configure(options =>
* If an extension file defines the same localized string, it overrides the string.
+### Culture Fallback
+
+ABP searches for a localized string in the following order:
+
+1. The requested culture of the current resource, such as `en-US`.
+2. The base culture, such as `en`, when `TryToGetFromBaseCulture` is enabled.
+3. The default culture configured for the resource when `TryToGetFromDefaultCulture` is enabled.
+4. The inherited resources, in their configured order. Each inherited resource applies the same culture fallback rules.
+5. The localization key itself, returned with `ResourceNotFound` set to `true`.
+
+Both fallback options are enabled by default. You can disable them independently:
+
+````csharp
+Configure(options =>
+{
+ options.TryToGetFromBaseCulture = false;
+ options.TryToGetFromDefaultCulture = false;
+});
+````
+
+### Global Resource Contributors
+
+`AbpLocalizationOptions.GlobalContributors` adds an `ILocalizationResourceContributor` implementation to every localization resource:
+
+````csharp
+Configure(options =>
+{
+ options.GlobalContributors.Add();
+});
+````
+
+The contributor type must have a parameterless constructor. Its `Initialize` method receives a `LocalizationResourceInitializationContext`, which provides the resource and the application service provider.
+
+Contributors are order-sensitive. A lookup starts with the last registered contributor, so a later contributor overrides an earlier contributor when both provide the same key. Global contributors are appended after contributors configured directly on a resource.
+
+### External Localization Stores
+
+Replace `IExternalLocalizationStore` when localization resources need to be discovered at runtime or loaded from an external system. The default `NullExternalLocalizationStore` does not provide any resources.
+
+The string localizer factory first searches the resources registered in `AbpLocalizationOptions.Resources`. If it cannot find the requested resource name, it queries `IExternalLocalizationStore`. The store exposes synchronous and asynchronous methods for retrieving a resource by name, and asynchronous methods for enumerating resource names and resources.
+
+The factory caches the localizer after it resolves a resource name. Changing the resource object returned by the store does not make the factory resolve that name again. Use dynamic contributors when the localization values themselves need to change while the application is running.
+
+Use the standard [dependency injection service replacement](dependency-injection.md#replace-a-service) mechanism to replace the default implementation.
+
## Getting the Localized Texts
Getting the localized text is pretty standard.
@@ -255,12 +327,70 @@ public class MyService : ITransientDependency
}
````
+### Creating A Localizer By Resource Name
+
+Use `IStringLocalizerFactory` when the resource type is not available or the resource is registered by name:
+
+````csharp
+public class MyService : ITransientDependency
+{
+ private readonly IStringLocalizerFactory _localizerFactory;
+
+ public MyService(IStringLocalizerFactory localizerFactory)
+ {
+ _localizerFactory = localizerFactory;
+ }
+
+ public string GetCountryName()
+ {
+ var localizer = _localizerFactory.CreateByResourceName("CountryNames");
+ return localizer["USA"];
+ }
+}
+````
+
+`CreateByResourceName` throws an `AbpException` when the resource cannot be found. Use `CreateByResourceNameOrNull` when a missing resource is expected. `CreateByResourceNameAsync` and `CreateByResourceNameOrNullAsync` are available for external stores that load resources asynchronously.
+
+### Serializing Localizable Strings
+
+Use `ILocalizableStringSerializer` when an `ILocalizableString` needs to be stored as a string and reconstructed later:
+
+````csharp
+var serialized = localizableStringSerializer.Serialize(
+ LocalizableString.Create("HelloWorld")
+);
+
+var localizableString = localizableStringSerializer.Deserialize(serialized!);
+````
+
+The default serializer uses `L:,` for `LocalizableString` and `F:` for `FixedLocalizableString`. A value without a recognized prefix is deserialized as a `FixedLocalizableString`; values too short to carry both a prefix and content (like the literal `L:`) are treated the same way. An `L:` value without a comma or with an empty or whitespace-only key throws an `AbpException`. Serializing `null` returns `null`; serializing another `ILocalizableString` implementation throws an `AbpException`.
+
### Format Arguments
Format arguments can be passed after the localization key. If your message is `Hello {0}, welcome!`, then you can pass the `{0}` argument to the localizer like `_localizer["HelloMessage", "John"]`.
> Refer to the [Microsoft's localization documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) for details about using the localization.
+### Getting All Localization Strings
+
+The standard `GetAllStrings(includeParentCultures)` method can include values from the resource's default and base cultures. ABP also provides an overload to control inherited resources and dynamic contributors independently:
+
+````csharp
+var strings = localizer.GetAllStrings(
+ includeParentCultures: true,
+ includeBaseLocalizers: true,
+ includeDynamicContributors: false
+);
+````
+
+* `includeParentCultures` includes values from the resource's default culture and the base culture of the current UI culture. Values from the current UI culture override them.
+* `includeBaseLocalizers` includes strings from inherited resources. Values from the current resource override inherited values.
+* `includeDynamicContributors` includes contributors whose `IsDynamic` property is `true`.
+
+`includeParentCultures` controls this bulk enumeration independently of `TryToGetFromBaseCulture` and `TryToGetFromDefaultCulture`, which control single-string lookups.
+
+Use `GetAllStringsAsync` with the same flags when a contributor retrieves strings asynchronously.
+
### Using In A Razor View/Page
Use `IHtmlLocalizer` in razor views/pages;
@@ -331,6 +461,29 @@ Configure(options =>
});
```
+### Mapping Culture Names For Client Packages
+
+Client libraries sometimes use a culture name or localization file name that differs from the application's culture name. Use `AddLanguagesMapOrUpdate` to map the culture passed to a package, and `AddLanguageFilesMapOrUpdate` to map the package's localization file name:
+
+```csharp
+Configure(options =>
+{
+ options.AddLanguagesMapOrUpdate(
+ "MyClientPackage",
+ new NameValue("zh-Hans", "zh-CN")
+ );
+
+ options.AddLanguageFilesMapOrUpdate(
+ "MyClientPackage",
+ new NameValue("zh-Hans", "zh-CN")
+ );
+});
+```
+
+Mappings are scoped by package name. When a mapping is not defined, ABP uses the original culture name.
+
+The `NameValue` name is the application culture and its value is the culture or file name expected by the client package. Use the package's own package-name constant when it provides one.
+
## URL-Based Localization
ABP supports embedding the culture code directly in the URL path (e.g. `/en/products`, `/zh-Hans/about`), which is useful for SEO-friendly and shareable localized URLs. See the [URL-Based Localization](./url-based-localization.md) document for details.
diff --git a/docs/en/framework/fundamentals/logging.md b/docs/en/framework/fundamentals/logging.md
index 94056400c7..7a1928b4c6 100644
--- a/docs/en/framework/fundamentals/logging.md
+++ b/docs/en/framework/fundamentals/logging.md
@@ -11,3 +11,20 @@ ABP doesn't implement any logging infrastructure. It uses the [ASP.NET Core's lo
> .NET Core's logging system is actually independent from the ASP.NET Core. It is usable in any type of application.
+## Serilog Request Enrichers
+
+When the ABP ASP.NET Core Serilog integration is installed, its middleware enriches request log events with the current `TenantId`, `UserId`, `ClientId` and `CorrelationId` values when they are available.
+
+`AbpAspNetCoreSerilogOptions.EnricherPropertyNames` can align these property names with an existing observability schema:
+
+````csharp
+Configure(options =>
+{
+ options.EnricherPropertyNames.TenantId = "tenant_id";
+ options.EnricherPropertyNames.UserId = "user_id";
+ options.EnricherPropertyNames.ClientId = "client_id";
+ options.EnricherPropertyNames.CorrelationId = "correlation_id";
+});
+````
+
+The default names are `TenantId`, `UserId`, `ClientId` and `CorrelationId`.
diff --git a/docs/en/framework/fundamentals/options.md b/docs/en/framework/fundamentals/options.md
index d18c89c675..2bde471f90 100644
--- a/docs/en/framework/fundamentals/options.md
+++ b/docs/en/framework/fundamentals/options.md
@@ -123,3 +123,56 @@ public override void ConfigureServices(ServiceConfigurationContext context)
}
````
+## Dynamic Options
+
+Standard options are created synchronously. `AbpDynamicOptionsManager` can override named option values asynchronously from a runtime source, such as the setting system.
+
+Derive a manager and implement `OverrideOptionsAsync`:
+
+````csharp
+public class MyDynamicOptionsManager : AbpDynamicOptionsManager
+{
+ private readonly ISettingProvider _settingProvider;
+
+ public MyDynamicOptionsManager(
+ IOptionsFactory factory,
+ ISettingProvider settingProvider)
+ : base(factory)
+ {
+ _settingProvider = settingProvider;
+ }
+
+ protected override async Task OverrideOptionsAsync(
+ string name,
+ MyOptions options)
+ {
+ options.Value1 = await _settingProvider.GetAsync("MyOptions.Value1");
+ }
+}
+````
+
+Register the manager for the option type:
+
+````csharp
+context.Services.AddAbpDynamicOptions();
+````
+
+This replaces `IOptions` and `IOptionsSnapshot` with the scoped dynamic manager. Call the `IOptions.SetAsync` extension before reading the value when you need to apply the asynchronous override:
+
+````csharp
+public class MyService : ITransientDependency
+{
+ private readonly IOptions _options;
+
+ public MyService(IOptions options)
+ {
+ _options = options;
+ }
+
+ public async Task GetValueAsync()
+ {
+ await _options.SetAsync();
+ return _options.Value.Value1;
+ }
+}
+````
diff --git a/docs/en/framework/fundamentals/validation.md b/docs/en/framework/fundamentals/validation.md
index 78e93e59f5..3bda1417da 100644
--- a/docs/en/framework/fundamentals/validation.md
+++ b/docs/en/framework/fundamentals/validation.md
@@ -117,7 +117,7 @@ namespace Acme.BookStore
}
````
-> ABP uses the [dynamic proxying / interception](../../dynamic-proxying-interceptors.md) system to perform the validation. In order to make it working, your method should be **virtual** or your service should be injected and used over an **interface** (like `IMyService`).
+> ABP uses the [dynamic proxying / interception](../infrastructure/interceptors.md) system to perform the validation. In order to make it working, your method should be **virtual** or your service should be injected and used over an **interface** (like `IMyService`).
#### Enabling/Disabling Validation
@@ -142,6 +142,25 @@ public class InputClass
}
````
+If a class that is subject to automatic validation (it implements `IValidationEnabled`, like application services do) has `[DisableValidation]`, add `[EnableValidation]` to a method to re-enable automatic validation for that method (`[EnableValidation]` does not activate validation for a class that isn't intercepted at all):
+
+````csharp
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Validation;
+
+[DisableValidation]
+public class MyService : IValidationEnabled, ITransientDependency
+{
+ [EnableValidation]
+ public virtual Task UpdateAsync(MyInput input)
+ {
+ //...
+ return Task.CompletedTask;
+ }
+}
+````
+
### AbpValidationException
Once ABP determines a validation error, it throws an exception of type `AbpValidationException`. Your application code can throw `AbpValidationException`, but most of the times it is not needed.
@@ -180,6 +199,17 @@ public class MyObjectValidationContributor
* Remember to register your class to the [DI](./dependency-injection.md) (implementing `ITransientDependency` does it just like in this example)
* ABP will automatically discover your class and use on any type of object validation (including automatic method call validation).
+### Ignoring Types During Recursive Validation
+
+`AbpValidationOptions.IgnoredTypes` prevents the default data annotation contributor from descending into the properties of matching values during recursive validation. The data annotations on the matching value itself are still validated. Derived and implementing types are also matched.
+
+````csharp
+Configure(options =>
+{
+ options.IgnoredTypes.Add(typeof(MyInfrastructureValue));
+});
+````
+
### IMethodInvocationValidator
`IMethodInvocationValidator` is used to validate a method call. It internally uses the `IObjectValidator` to validate objects passes to the method call. You normally don't need to this service since it is automatically used by the framework, but you may want to reuse or replace it on your application in rare cases.
diff --git a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md
index d3e7d128fc..8d83bfc714 100644
--- a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md
+++ b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md
@@ -86,7 +86,7 @@ public class CommentSummarization
> [!NOTE]
> If you don't specify the workspace name, the full name of the class will be used as the workspace name.
-You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, you will get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client.
+You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If a Chat Client is not configured for the specified workspace, both services fall back to the default workspace. `IChatClientAccessor.ChatClient` is `null` only when neither the specified workspace nor the default workspace has a configured Chat Client. Resolving `IChatClient` requires one of them to be configured.
`IChatClient` or `IChatClientAccessor` can be resolved to access a specific workspace's chat client. This is a typed chat client and can be configured separately from the default chat client.
@@ -215,4 +215,4 @@ public class MyProjectModule : AbpModule
- [Usage of Microsoft.Extensions.AI](./microsoft-extensions-ai.md)
- [Usage of Semantic Kernel](./microsoft-semantic-kernel.md)
- [Microsoft Agent Framework Overview](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
-- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/)
\ No newline at end of file
+- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/)
diff --git a/docs/en/framework/infrastructure/audit-logging.md b/docs/en/framework/infrastructure/audit-logging.md
index 9dc7618281..ab2c59748e 100644
--- a/docs/en/framework/infrastructure/audit-logging.md
+++ b/docs/en/framework/infrastructure/audit-logging.md
@@ -44,10 +44,10 @@ Configure(options =>
Here, a list of the options you can configure:
* `IsEnabled` (default: `true`): A root switch to enable or disable the auditing system. Other options is not used if this value is `false`.
-* `HideErrors` (default: `true`): Audit log system hides and write regular [logs](../fundamentals/localization.md) if any error occurs while saving the audit log objects. If saving the audit logs is critical for your system, set this to `false` to throw exception in case of hiding the errors.
+* `HideErrors` (default: `true`): Audit log system hides and write regular [logs](../fundamentals/logging.md) if any error occurs while saving the audit log objects. If saving the audit logs is critical for your system, set this to `false` to throw exception in case of hiding the errors.
* `IsEnabledForAnonymousUsers` (default: `true`): If you want to write audit logs only for the authenticated users, set this to `false`. If you save audit logs for anonymous users, you will see `null` for `UserId` values for these users.
* `AlwaysLogOnException` (default: `true`): If you set to true, it always saves the audit log on an exception/error case without checking other options (except `IsEnabled`, which completely disables the audit logging).
-* `IsEnabledForIntegrationService` (default: `false`): Audit Logging is disabled for [integration services](../api-development/integration-services.md) by default. Set this property as `true` to enable it.
+* `IsEnabledForIntegrationServices` (default: `false`): Audit Logging is disabled for [integration services](../api-development/integration-services.md) by default. Set this property as `true` to enable it.
* `IsEnabledForGetRequests` (default: `false`): Safe HTTP methods (GET, HEAD and QUERY) should not make any change in the database normally and the audit log system doesn't save audit log objects for these requests. Set this to `true` to enable it also for the safe requests.
* `DisableLogActionInfo` (default: `false`):If you set to true, Will no longer log `AuditLogActionInfo`.
* `ApplicationName`: If multiple applications are saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. If you don't set, it will set from the `IApplicationInfoAccessor.ApplicationName` value, which is the entry assembly name by default.
@@ -311,6 +311,8 @@ An **audit log object** is created for each **web request** by default. An audit
* **Exception**: An audit log object may contain zero or more exception. In this way, you can get a report of the failed requests.
* **Comment**: An arbitrary string value to add custom messages to the audit log entry. An audit log object may contain zero or more comments.
+> When the [Audit Logging Module](../../modules/audit-logging.md) persists exceptions, it uses `AbpExceptionHandlingOptions` to convert them. `SendExceptionsDetailsToClients`, `SendStackTraceToClients` and `SendExceptionDataToClientTypes` therefore also control the exception details stored in audit logs, not only the details sent to clients. Review these options when audit logs may contain sensitive information. See the [Exception Handling](../fundamentals/exception-handling.md#abpexceptionhandlingoptions) document for configuration details.
+
In addition to the standard properties explained above, `AuditLogInfo`, `AuditLogActionInfo` and `EntityChangeInfo` objects implement the `IHasExtraProperties` interface, so you can add custom properties to these objects.
## Audit Log Contributors
diff --git a/docs/en/framework/infrastructure/background-jobs/index.md b/docs/en/framework/infrastructure/background-jobs/index.md
index 57bffefbe0..2d70a9ea3c 100644
--- a/docs/en/framework/infrastructure/background-jobs/index.md
+++ b/docs/en/framework/infrastructure/background-jobs/index.md
@@ -150,7 +150,7 @@ Configure(options =>
{
options.GetBackgroundJobName = (jobType) =>
{
- if (jobTyep == typeof(EmailSendingArgs))
+ if (jobType == typeof(EmailSendingArgs))
{
return "emails";
}
@@ -504,4 +504,4 @@ See pre-built job manager alternatives:
* [TickerQ Background Job Manager](./tickerq.md)
## See Also
-* [Background Workers](../background-workers)
\ No newline at end of file
+* [Background Workers](../background-workers)
diff --git a/docs/en/framework/infrastructure/background-workers/hangfire.md b/docs/en/framework/infrastructure/background-workers/hangfire.md
index 9b65a488a9..43ddaceb19 100644
--- a/docs/en/framework/infrastructure/background-workers/hangfire.md
+++ b/docs/en/framework/infrastructure/background-workers/hangfire.md
@@ -47,6 +47,16 @@ public class YourModule : AbpModule
> Hangfire background worker integration provides an adapter `HangfirePeriodicBackgroundWorkerAdapter` to automatically load any `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived classes as `IHangfireBackgroundWorker` instances. This allows you to still to easily switch over to use Hangfire as the background manager even you have existing background workers that are based on the [default background workers implementation](../background-workers).
+The adapter uses UTC for recurring schedules by default and uses the default Hangfire queue when no queue is specified (a specified queue name is prefixed with `AbpHangfireOptions.DefaultQueuePrefix`, which is empty by default). You can configure both values globally for adapted periodic workers:
+
+````csharp
+Configure(options =>
+{
+ options.TimeZone = TimeZoneInfo.Local;
+ options.Queue = "periodic";
+});
+````
+
## Configuration
You can install any storage for Hangfire. The most common one is SQL Server (see the [Hangfire.SqlServer](https://www.nuget.org/packages/Hangfire.SqlServer) NuGet package).
diff --git a/docs/en/framework/infrastructure/blob-storing/database.md b/docs/en/framework/infrastructure/blob-storing/database.md
index 1b568c247c..ff226ae3c6 100644
--- a/docs/en/framework/infrastructure/blob-storing/database.md
+++ b/docs/en/framework/infrastructure/blob-storing/database.md
@@ -9,6 +9,8 @@
BLOB Storing Database Storage Provider can store BLOBs in a relational or non-relational database.
+The database provider reads the complete input stream into memory before saving a BLOB and returns BLOB content from an in-memory buffer. Database and driver value-size limits still apply. Consider an external object-storage provider for very large BLOBs or workloads that require end-to-end streaming.
+
There are two database providers implemented;
* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) package implements for [EF Core](../../data/entity-framework-core), so it can store BLOBs in [any DBMS supported](https://docs.microsoft.com/en-us/ef/core/providers/) by the EF Core.
@@ -32,16 +34,16 @@ This command adds all the NuGet packages to corresponding layers of your solutio
### Manual Installation
-Here, all the NuGet packages defined by this provider;
+The following NuGet packages are defined by this provider:
* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain.Shared)
* [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain)
* [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore)
* [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB)
-You can only install Volo.Abp.BlobStoring.Database.EntityFrameworkCore or Volo.Abp.BlobStoring.Database.MongoDB (based on your preference) since they depends on the other packages.
+You only need to install Volo.Abp.BlobStoring.Database.EntityFrameworkCore or Volo.Abp.BlobStoring.Database.MongoDB (based on your preference), since they depend on the other packages.
-After installation, add `DepenedsOn` attribute to your related [module](../../architecture/modularity/basics.md). Here, the list of module classes defined by the related NuGet packages listed above:
+After installation, add the `[DependsOn]` attribute to your related [module](../../architecture/modularity/basics.md). Here is the list of module classes defined by the related NuGet packages listed above:
* `BlobStoringDatabaseDomainModule`
* `BlobStoringDatabaseDomainSharedModule`
@@ -52,6 +54,17 @@ Whenever you add a NuGet package to a project, also add the module class depende
If you are using EF Core, you also need to configure your **Migration DbContext** to add BLOB storage tables to your database schema. Call `builder.ConfigureBlobStoring()` extension method inside the `OnModelCreating` method to include mappings to your DbContext. Then you can use the standard `Add-Migration` and `Update-Database` [commands](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create necessary tables in your database.
+If you are using MongoDB and combine module collections in a custom `AbpMongoDbContext`, call `modelBuilder.ConfigureBlobStoring()` inside the `CreateModel` method:
+
+````csharp
+protected override void CreateModel(IMongoModelBuilder modelBuilder)
+{
+ base.CreateModel(modelBuilder);
+
+ modelBuilder.ConfigureBlobStoring();
+}
+````
+
## Configuration
### Connection String
@@ -60,9 +73,17 @@ If you will use your `Default` connection string, you don't need to any addition
If you want to use a separate database for BLOB storage, use the `AbpBlobStoring` as the [connection string](../../fundamentals/connection-strings.md) name in your configuration file (`appsettings.json`). In this case, also read the [EF Core Migrations](../../data/entity-framework-core/migrations.md) document to learn how to create and use a different database for a desired module.
+### Common Database Properties
+
+The `AbpBlobStoringDatabaseDbProperties` class defines the following database settings. Set `DbTablePrefix` and `DbSchema` at application startup, before the database model is created:
+
+* `DbTablePrefix` (`Abp` by default) is the prefix for table and collection names.
+* `DbSchema` (`null` by default) is the database schema used by EF Core. MongoDB does not use this property.
+* `ConnectionStringName` (`AbpBlobStoring`) is the connection-string name used by both database providers.
+
### Configuring the Containers
-If you are using only the database storage provider, you don't need to manually configure it, since it is automatically done. If you are using multiple storage providers, you may want to configure it.
+The database module selects `DatabaseBlobProvider` for the default container when no provider has already been selected. It does not replace an explicitly configured provider. If you use multiple storage providers, configure the database provider for the required default, typed or named containers.
Configuration is done in the `ConfigureServices` method of your [module](../../architecture/modularity/basics.md) class, as explained in the [BLOB Storing document](../blob-storing).
@@ -84,12 +105,18 @@ Configure(options =>
It is expected to use the [BLOB Storing services](../blob-storing) to use the BLOB storing system. However, if you want to work on the database tables/entities, you can use the following information.
+### Database Tables and Collections
+
+With the default `Abp` prefix, EF Core maps the entities to the `AbpBlobContainers` and `AbpBlobs` tables. MongoDB uses collections with the same names. Changing `DbTablePrefix` changes both table and collection names, while `DbSchema` only changes the EF Core schema.
+
### Entities
Entities defined for this module:
-* `DatabaseBlobContainer` (aggregate root) represents a container stored in the database.
-* `DatabaseBlob` (aggregate root) represents a BLOB in the database.
+* `DatabaseBlobContainer` (aggregate root) represents a container stored in the database. It stores the tenant identifier and the container name. Persisted container names have a maximum length of 128 characters.
+* `DatabaseBlob` (aggregate root) represents a BLOB in the database. It stores the container identifier, tenant identifier, BLOB name and content. Persisted BLOB names have a maximum length of 256 characters.
+
+The provider creates a container record lazily when the first BLOB is saved to that container. Read, existence-check and delete operations do not create container records, and deleting the last BLOB does not delete its container record.
See the [entities document](../../architecture/domain-driven-design/entities.md) to learn what is an entity and aggregate root.
@@ -102,4 +129,4 @@ You can also use `IRepository` and `IRepository(options =>
* `CleanOldEventTimeIntervalSpan`: The event inbox system periodically checks and deletes the old processed events from the inbox in the database. You can set this value to determine the check period. Default value is 6 hours (`TimeSpan.FromHours(6)`).
* `WaitTimeToDeleteProcessedInboxEvents`: Inbox events are not deleted from the database for a while even if they are successfully processed. This is for a system to prevent multiple process of the same event (if the event broker sends it twice). This configuration value determines the time to keep the processed events. Default value is 2 hours (`TimeSpan.FromHours(2)`).
* `InboxWaitingEventMaxCount`: The maximum number of events to query at once from the inbox in the database. Default value is 1000.
+* `InboxProcessorFilter`: An expression used to filter incoming event records fetched by the inbox processor. The default value is `null`, which includes all records.
* `OutboxWaitingEventMaxCount`: The maximum number of events to query at once from the outbox in the database. Default value is 1000.
+* `OutboxProcessorFilter`: An expression used to filter outgoing event records fetched by the outbox processor. The default value is `null`, which includes all records.
* `DistributedLockWaitDuration`: ABP uses [distributed locking](../../distributed-locking.md) to prevent concurrent access to the inbox and outbox messages in the database, when running multiple instance of the same application. If an instance of the application can not obtain the lock, it tries after a duration. This is the configuration of that duration. Default value is 15 seconds (`TimeSpan.FromSeconds(15)`).
* `InboxProcessorFailurePolicy`: The policy to handle the failure of the inbox processor. Default value is `Retry`. Possible values are:
* `Retry`: The current exception and subsequent events will continue to be processed in order in the next cycle.
* `RetryLater`: Skip the event that caused the exception and continue with the following events. The failed event will be retried after a delay that doubles with each retry, starting from the configured `InboxProcessorRetryBackoffFactor` (e.g., 10, 20, 40, 80 seconds). The default maximum retry count is 10 (configurable). Discard the event if it still fails after reaching the maximum retry count.
* `Discard`: The event that caused the exception will be discarded and will not be retried.
+* `InboxProcessorMaxRetryCount`: The maximum retry count used by the `RetryLater` failure policy before an event is discarded. Default value is `10`.
* `InboxProcessorRetryBackoffFactor`: The initial retry delay factor (double) used when `InboxProcessorFailurePolicy` is `RetryLater`. The retry delay is calculated as: `delay = InboxProcessorRetryBackoffFactor × 2^retryCount`. Default value is `10`.
### Skipping Outbox
diff --git a/docs/en/framework/infrastructure/event-bus/distributed/rabbitmq.md b/docs/en/framework/infrastructure/event-bus/distributed/rabbitmq.md
index 7b7d64c4c6..5c8fc740f1 100644
--- a/docs/en/framework/infrastructure/event-bus/distributed/rabbitmq.md
+++ b/docs/en/framework/infrastructure/event-bus/distributed/rabbitmq.md
@@ -148,17 +148,20 @@ Configure(options =>
});
````
-**Example: Configure the client, exchange names and prefetchCount**
+**Example: Configure the client, exchange name, exchange type and prefetch count**
````csharp
Configure(options =>
{
options.ClientName = "TestApp1";
options.ExchangeName = "TestMessages";
+ options.ExchangeType = "topic";
options.PrefetchCount = 1;
});
````
+`ExchangeType` uses RabbitMQ's `direct` exchange type when it is `null` or empty.
+
**Example: Configure the queue and exchange optional arguments**
```csharp
diff --git a/docs/en/framework/infrastructure/features.md b/docs/en/framework/infrastructure/features.md
index e450dc2068..744d5cec13 100644
--- a/docs/en/framework/infrastructure/features.md
+++ b/docs/en/framework/infrastructure/features.md
@@ -49,7 +49,7 @@ ABP uses the interception system to make the `[RequiresFeature]` attribute worki
However, there are **some rules should be followed** in order to make it working;
-* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](../../dynamic-proxying-interceptors.md) system can not work.
+* If you are **not injecting** the service over an interface (like `IMyService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](./interceptors.md) system can not work.
* Only `async` methods (methods returning a `Task` or `Task`) are intercepted.
> There is an exception for the **controller and razor page methods**. They **don't require** the following the rules above, since ABP uses the action/page filters to implement the feature checking in this case.
diff --git a/docs/en/framework/infrastructure/interceptors.md b/docs/en/framework/infrastructure/interceptors.md
index 9005c3d612..c6cedf7859 100644
--- a/docs/en/framework/infrastructure/interceptors.md
+++ b/docs/en/framework/infrastructure/interceptors.md
@@ -203,6 +203,26 @@ ABP uses interceptors for features like UOW, auditing, and authorization, which
To avoid generating dynamic proxies for specific types, use the static class `DynamicProxyIgnoreTypes` and add the base classes of the types to the list. Subclasses of any listed base class are also ignored. ABP framework already adds some base classes to the list (`ComponentBase, ControllerBase, PageModel, ViewComponent`); you can add more base classes if needed.
+You can also disable ABP class interceptors for all registrations or for types selected by a predicate:
+
+````csharp
+// Disable all class interceptors.
+context.Services.DisableAbpClassInterceptors();
+
+// Or disable them only for selected types. The predicate runs for class
+// service registrations and receives the exposed class service type,
+// which is the exposed base class rather than the implementation type
+// when a class is exposed through a base class.
+context.Services.DisableAbpClassInterceptors(
+ new NamedTypeSelector(
+ "MyHotPathServices",
+ type => type.Namespace == "MyProject.HotPath"
+ )
+);
+````
+
+These methods control class interception. Interface-based interception is configured separately.
+
> Always use interface-based proxies instead of class-based proxies for better performance.
## See Also
diff --git a/docs/en/framework/infrastructure/json.md b/docs/en/framework/infrastructure/json.md
index a8d17528cb..54281e40b8 100644
--- a/docs/en/framework/infrastructure/json.md
+++ b/docs/en/framework/infrastructure/json.md
@@ -45,6 +45,35 @@ public class ProductManager
}
```
+## IObjectSerializer
+
+`IObjectSerializer` (defined in the `Volo.Abp.Serialization` package, independently of the JSON system) serializes objects to and from `byte[]`. The default implementation uses UTF-8 JSON bytes from `System.Text.Json`:
+
+```csharp
+public interface IObjectSerializer
+{
+ byte[]? Serialize(T? obj);
+ T? Deserialize(byte[] bytes);
+}
+```
+
+Inject `IObjectSerializer` when a storage or transport API works with bytes instead of strings. To customize serialization for a specific type, implement `IObjectSerializer`. ABP automatically exposes conventionally registered implementations through the corresponding closed generic interface, and the default serializer uses that implementation for `T`:
+
+```csharp
+public class ProductSerializer : IObjectSerializer, ITransientDependency
+{
+ public byte[]? Serialize(Product? obj)
+ {
+ return obj is null ? null : JsonSerializer.SerializeToUtf8Bytes(obj);
+ }
+
+ public Product? Deserialize(byte[]? bytes)
+ {
+ return bytes is null ? null : JsonSerializer.Deserialize(bytes);
+ }
+}
+```
+
## Configuration
### AbpJsonOptions
diff --git a/docs/en/framework/infrastructure/mail-kit.md b/docs/en/framework/infrastructure/mail-kit.md
index f483aaa512..307ba61557 100644
--- a/docs/en/framework/infrastructure/mail-kit.md
+++ b/docs/en/framework/infrastructure/mail-kit.md
@@ -37,7 +37,7 @@ MailKit integration package uses the same settings defined by the email sending
In addition to the standard settings, this package defines `AbpMailKitOptions` as a simple [options](../fundamentals/options.md) class. This class defines only one options:
-* **SecureSocketOption**: Used to set one of the `SecureSocketOptions`. Default: `null` (uses the defaults).
+* **SecureSocketOption**: Used to set one of the `SecureSocketOptions`. The default is `null`. In that case, ABP uses `SslOnConnect` when the SMTP `EnableSsl` setting is `true`; otherwise, it uses `StartTlsWhenAvailable`.
**Example: Use *SecureSocketOptions.SslOnConnect***
@@ -52,4 +52,4 @@ Refer to the [MailKit documentation](http://www.mimekit.net/) to learn more abou
## See Also
-* [Email sending](./emailing.md)
\ No newline at end of file
+* [Email sending](./emailing.md)
diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md
index d8484a277e..888655d446 100644
--- a/docs/en/framework/infrastructure/object-to-object-mapping.md
+++ b/docs/en/framework/infrastructure/object-to-object-mapping.md
@@ -224,7 +224,18 @@ public class MyProfile : Profile
}
````
-> AutoMapper 14.x contains a [known vulnerability (GHSA-rvv3-g6hj-g44x)](https://github.com/advisories/GHSA-rvv3-g6hj-g44x). ABP Framework has applied a code-level mitigation (`MaxDepth = 64`) to address this. If you hold a commercial AutoMapper license, you can use [Volo.Abp.LuckyPenny.AutoMapper](luckypenny-automapper.md) to upgrade to the officially patched version. Alternatively, you can migrate to [Mapperly](../../../release-info/migration-guides/AutoMapper-To-Mapperly.md).
+> AutoMapper 14.x contains a [known vulnerability (GHSA-rvv3-g6hj-g44x)](https://github.com/advisories/GHSA-rvv3-g6hj-g44x). ABP Framework has applied a code-level mitigation (`MaxDepth = 64`) to address this. If you hold a commercial AutoMapper license, you can use [Volo.Abp.LuckyPenny.AutoMapper](luckypenny-automapper.md) to upgrade to the officially patched version. Alternatively, you can migrate to [Mapperly](../../release-info/migration-guides/AutoMapper-To-Mapperly.md).
+
+The global maximum depth is configured by `AbpAutoMapperOptions.DefaultMaxDepth` and defaults to `64`. It is applied only when a map does not already configure `MaxDepth`. Set it to `null` to disable ABP's global default:
+
+````csharp
+Configure(options =>
+{
+ options.DefaultMaxDepth = null;
+});
+````
+
+> Disabling the global default also removes ABP's mitigation for unbounded mapping depth. Disable it only when every affected map has an explicit safe depth or the application uses an officially patched mapper.
## Mapperly Integration
diff --git a/docs/en/framework/infrastructure/settings.md b/docs/en/framework/infrastructure/settings.md
index 38c9528d86..14b493e1fc 100644
--- a/docs/en/framework/infrastructure/settings.md
+++ b/docs/en/framework/infrastructure/settings.md
@@ -257,6 +257,15 @@ While a setting value provider is free to use any source to get the setting valu
You can replace this service in the dependency injection system to customize the encryption/decryption process. Default implementation uses the `StringEncryptionService` which is implemented with the AES algorithm by default (see string [encryption document](./string-encryption.md) for more).
+If an encrypted setting value cannot be decrypted, the default service logs a warning and returns the original value. This behavior helps when an existing setting is changed from unencrypted to encrypted. Set `AbpSettingOptions.ReturnOriginalValueIfDecryptFailed` to `false` to return an empty string instead:
+
+````csharp
+Configure(options =>
+{
+ options.ReturnOriginalValueIfDecryptFailed = false;
+});
+````
+
## Setting Management Module
The core setting system is pretty independent and doesn't make any assumption about how you manage (change) the setting values. Even the default `ISettingStore` implementation is the `NullSettingStore` which returns null for all setting values.
diff --git a/docs/en/framework/infrastructure/sms-sending.md b/docs/en/framework/infrastructure/sms-sending.md
index 64439be111..d2532709ec 100644
--- a/docs/en/framework/infrastructure/sms-sending.md
+++ b/docs/en/framework/infrastructure/sms-sending.md
@@ -85,20 +85,19 @@ The given `SendAsync` method in the example is an extension method to send an SM
- `PhoneNumber` (`string`): Target phone number
- `Text` (`string`): Message text
-- `Properties` (`Dictionary`): Key-value pairs to pass custom arguments
+- `Properties` (`IDictionary`): Key-value pairs to pass custom arguments
## NullSmsSender
-`NullSmsSender` is a the default implementation of the `ISmsSender`. It writes SMS content to the [standard logger](../fundamentals/logging.md), rather than actually sending the SMS.
+`NullSmsSender` is the default implementation of `ISmsSender`. It writes SMS content to the [standard logger](../fundamentals/logging.md), rather than actually sending the SMS.
-This class can be useful especially in development time where you generally don't want to send real SMS. **However, if you want to actually send SMS, you should implement the `ISmsSender` in your application code.**
+This class can be useful especially in development time where you generally don't want to send real SMS. To send real SMS, install one of the pre-built providers below or implement `ISmsSender` in your application code.
## Implementing the ISmsSender
You can easily create your SMS sending implementation by creating a class that implements the `ISmsSender` interface, as shown below:
```csharp
-using System.IO;
using System.Threading.Tasks;
using Volo.Abp.Sms;
using Volo.Abp.DependencyInjection;
@@ -107,14 +106,95 @@ namespace AbpDemo
{
public class MyCustomSmsSender : ISmsSender, ITransientDependency
{
- public async Task SendAsync(SmsMessage smsMessage)
+ public Task SendAsync(SmsMessage smsMessage)
{
// Send sms
+ return Task.CompletedTask;
}
}
}
```
+## Pre-Built Providers
+
+Adding a provider module registers its sender as the `ISmsSender` implementation in place of the default `NullSmsSender`.
+
+### Aliyun
+
+Install the Aliyun provider package:
+
+```bash
+abp add-package Volo.Abp.Sms.Aliyun
+```
+
+For manual installation, add the `Volo.Abp.Sms.Aliyun` package and declare a dependency on `AbpSmsAliyunModule`.
+
+Configure the provider in the `AbpAliyunSms` section:
+
+```json
+{
+ "AbpAliyunSms": {
+ "AccessKeyId": "your-access-key-id",
+ "AccessKeySecret": "your-access-key-secret",
+ "EndPoint": "your-endpoint"
+ }
+}
+```
+
+Aliyun sends template-based messages. Set `SmsMessage.Text` to the template parameter JSON and use the `SignName` and `TemplateCode` properties:
+
+```csharp
+var message = new SmsMessage(
+ "+012345678901",
+ "{\"code\":\"123456\"}"
+);
+
+message.Properties["SignName"] = "MySign";
+message.Properties["TemplateCode"] = "SMS_123456789";
+
+await _smsSender.SendAsync(message);
+```
+
+### Tencent Cloud
+
+Install the Tencent Cloud provider package:
+
+```bash
+abp add-package Volo.Abp.Sms.TencentCloud
+```
+
+For manual installation, add the `Volo.Abp.Sms.TencentCloud` package and declare a dependency on `AbpSmsTencentCloudModule`.
+
+Configure the provider in the `AbpTencentCloudSms` section:
+
+```json
+{
+ "AbpTencentCloudSms": {
+ "SmsSdkAppId": "your-sdk-app-id",
+ "SecretId": "your-secret-id",
+ "SecretKey": "your-secret-key",
+ "Endpoint": "sms.tencentcloudapi.com",
+ "Region": "ap-guangzhou"
+ }
+}
+```
+
+`Endpoint` defaults to `sms.tencentcloudapi.com` and `Region` defaults to `ap-guangzhou`.
+
+Set the sign and template identifiers through `TencentCloudSmsProperties`. The provider splits `SmsMessage.Text` by commas and sends the resulting values as template parameters:
+
+```csharp
+var message = new SmsMessage(
+ "+012345678901",
+ "123456,5"
+);
+
+message.Properties[TencentCloudSmsProperties.SignName] = "MySign";
+message.Properties[TencentCloudSmsProperties.TemplateId] = "123456";
+
+await _smsSender.SendAsync(message);
+```
+
## More
[ABP](https://abp.io/) provides Twilio integration package to send SMS over [Twilio service](https://abp.io/docs/latest/modules/twilio-sms).
diff --git a/docs/en/framework/infrastructure/string-encryption.md b/docs/en/framework/infrastructure/string-encryption.md
index de8f6c75bf..ccdc9d8eac 100644
--- a/docs/en/framework/infrastructure/string-encryption.md
+++ b/docs/en/framework/infrastructure/string-encryption.md
@@ -112,8 +112,8 @@ Configure(opts =>
{
opts.DefaultPassPhrase = "MyStrongPassPhrase";
opts.DefaultSalt = Encoding.UTF8.GetBytes("MyStrongSalt");
- opts.InitVectorBytes = Encoding.UTF8.GetBytes("YetAnotherStrongSalt");
- opts.Keysize = 512;
+ opts.InitVectorBytes = Encoding.UTF8.GetBytes("My16ByteInitVect");
+ opts.Keysize = 256;
});
```
@@ -123,10 +123,10 @@ Configure(opts =>
Default value: `Encoding.ASCII.GetBytes("hgt!16kl")`
-- **InitVectorBytes:** This constant string is used as a "salt" value for the PasswordDeriveBytes function calls. This size of the IV (in bytes) must = (keysize / 8). Default keysize is 256, so the IV must be 32 bytes long. Using a 16 character string here gives us 32 bytes when converted to a byte array.
+- **InitVectorBytes:** The initialization vector used by AES. It must be exactly 16 bytes, regardless of the configured key size.
Default value: `Encoding.ASCII.GetBytes("jkE49230Tf093b42")`
-- **Keysize:** This constant is used to determine the keysize of the encryption algorithm.
+- **Keysize:** The AES key size in bits. Use a key size supported by AES: `128`, `192`, or `256`.
Default value: `256`
diff --git a/docs/en/framework/infrastructure/text-templating/index.md b/docs/en/framework/infrastructure/text-templating/index.md
index 853014c1cd..e8912889b6 100644
--- a/docs/en/framework/infrastructure/text-templating/index.md
+++ b/docs/en/framework/infrastructure/text-templating/index.md
@@ -33,6 +33,21 @@ ABP provides two templating engines;
You can use different template engines in the same application, or even create a new custom template engine.
+## Default Rendering Engine
+
+A template can select its rendering engine explicitly with `WithScribanEngine`, `WithRazorEngine` or `WithRenderEngine`. If it does not, the renderer uses `AbpTextTemplatingOptions.DefaultRenderingEngine`.
+
+The Scriban module selects Scriban as the default engine. The Razor module selects Razor only if no default has already been configured. You can explicitly select the application-wide default:
+
+````csharp
+Configure(options =>
+{
+ options.DefaultRenderingEngine = ScribanTemplateRenderingEngine.EngineName;
+});
+````
+
+An engine selected on a template definition takes precedence over this global default.
+
## Source Code
Get [the source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document.
@@ -41,4 +56,4 @@ Get [the source code of the sample application](https://github.com/abpframework/
* [The source code of the sample application](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo) developed and referred through this document.
* [Localization system](../../fundamentals/localization.md).
-* [Virtual File System](../../infrastructure/virtual-file-system.md).
\ No newline at end of file
+* [Virtual File System](../../infrastructure/virtual-file-system.md).
diff --git a/docs/en/framework/infrastructure/virtual-file-system.md b/docs/en/framework/infrastructure/virtual-file-system.md
index 682fcd2f10..51fb3a44aa 100644
--- a/docs/en/framework/infrastructure/virtual-file-system.md
+++ b/docs/en/framework/infrastructure/virtual-file-system.md
@@ -116,6 +116,38 @@ public class MyService : ITransientDependency
}
````
+### Dynamic Files
+
+`IDynamicFileProvider` can add, replace and delete virtual files at runtime. Inside `IVirtualFileProvider`, dynamic files take precedence over configured embedded and replacement physical file sets, so they can temporarily override a file with the same virtual path. ASP.NET Core's physical web-root provider is a separate, higher-precedence layer, as described in the *Physical Files* section below. Dynamic files also support exact file-path change notifications through the standard `Watch` method; directory and wildcard watches are not supported.
+
+````csharp
+public class DynamicFileService : ITransientDependency
+{
+ private readonly IDynamicFileProvider _dynamicFileProvider;
+
+ public DynamicFileService(IDynamicFileProvider dynamicFileProvider)
+ {
+ _dynamicFileProvider = dynamicFileProvider;
+ }
+
+ public void SetFile(string content)
+ {
+ _dynamicFileProvider.AddOrUpdate(
+ new InMemoryFileInfo(
+ "/my-files/runtime.txt",
+ Encoding.UTF8.GetBytes(content),
+ "runtime.txt"
+ )
+ );
+ }
+
+ public bool DeleteFile()
+ {
+ return _dynamicFileProvider.Delete("/my-files/runtime.txt");
+ }
+}
+````
+
## ASP.NET Core Integration
The Virtual File System is well integrated to ASP.NET Core:
@@ -192,4 +224,4 @@ Physical files always override the virtual files. That means if you put a file u
## See Also
-* [Video tutorial](https://abp.io/video-courses/essentials/virtual-file-system)
\ No newline at end of file
+* [Video tutorial](https://abp.io/video-courses/essentials/virtual-file-system)
diff --git a/docs/en/framework/real-time/signalr.md b/docs/en/framework/real-time/signalr.md
index be4160a819..4a3ffe69fc 100644
--- a/docs/en/framework/real-time/signalr.md
+++ b/docs/en/framework/real-time/signalr.md
@@ -223,6 +223,17 @@ app.UseConfiguredEndpoints(endpoints =>
});
```
+### Dynamic Claims
+
+When [dynamic claims](../fundamentals/dynamic-claims.md) are enabled, ABP refreshes the principal when a client connects and periodically during hub method invocations. `AbpSignalROptions.CheckDynamicClaimsInterval` controls the minimum interval between invocation-time checks for a connection. The default is five seconds; set it to `null` to check on every invocation:
+
+```csharp
+Configure(options =>
+{
+ options.CheckDynamicClaimsInterval = TimeSpan.FromMinutes(1);
+});
+```
+
### UserIdProvider
ABP implements SignalR's `IUserIdProvider` interface to provide the current user id from the `ICurrentUser` service of the ABP (see [the current user service](../infrastructure/current-user.md)), so it will be integrated to the authentication system of your application. The implementing class is the `AbpSignalRUserIdProvider`, if you want to change/override it.
diff --git a/docs/en/framework/ui/angular/commercial-ui.md b/docs/en/framework/ui/angular/commercial-ui.md
new file mode 100644
index 0000000000..c2f24aed34
--- /dev/null
+++ b/docs/en/framework/ui/angular/commercial-ui.md
@@ -0,0 +1,88 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to use ABP Commercial Angular date range controls, standalone UI configuration and the public testing entrypoint."
+}
+```
+
+# Commercial UI Components
+
+The `@volo/abp.commercial.ng.ui` package provides shared ABP Commercial Angular controls in addition to the separately documented [lookup components](./lookup-components.md) and [entity filters](./entity-filters.md). The package is included in ABP Commercial Angular application templates.
+
+## Date Range Controls
+
+`DateRangePickerComponent` and `DatetimeRangePickerComponent` are Angular form controls. `startDateProp` and `endDateProp` specify the two properties updated in the bound model:
+
+```ts
+import { Component } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ DateRangePickerModule,
+ DatetimeRangePickerComponent,
+} from '@volo/abp.commercial.ng.ui';
+
+@Component({
+ selector: 'app-report-range',
+ templateUrl: './report-range.component.html',
+ imports: [
+ FormsModule,
+ DateRangePickerModule,
+ DatetimeRangePickerComponent,
+ ],
+})
+export class ReportRangeComponent {
+ dateRange: {
+ startDate: string | Date | null;
+ endDate: string | Date | null;
+ } = {
+ startDate: null,
+ endDate: null,
+ };
+}
+```
+
+```html
+
+```
+
+Use `abp-datetime-range-picker` with the same inputs when the model also needs start and end times. `labelText` is rendered as-is, so pass an already localized string (for example, a value resolved with the `LocalizationService`) instead of a localization key.
+
+## Standalone Configuration
+
+Register commercial UI configuration in the application providers. The following example enables flag icons:
+
+```ts
+import { ApplicationConfig } from '@angular/core';
+import {
+ provideCommercialUiConfig,
+ withEnableFlagIcon,
+} from '@volo/abp.commercial.ng.ui/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideCommercialUiConfig(
+ withEnableFlagIcon(true),
+ ),
+ ],
+};
+```
+
+Calling `provideCommercialUiConfig()` also registers the shared profile-picture, impersonation and tenant-switching providers. Flag icons are disabled unless `withEnableFlagIcon(true)` is supplied.
+
+## Testing
+
+`CommercialUiTestingModule` imports and exports `BaseCommercialUiModule`, making its declarations available from the public testing entrypoint. Its `withConfig()` method returns the module registration without adding test doubles or providers:
+
+```ts
+import { TestBed } from '@angular/core/testing';
+import { CommercialUiTestingModule } from '@volo/abp.commercial.ng.ui/testing';
+
+await TestBed.configureTestingModule({
+ imports: [CommercialUiTestingModule.withConfig()],
+}).compileComponents();
+```
diff --git a/docs/en/framework/ui/angular/component-replacement.md b/docs/en/framework/ui/angular/component-replacement.md
index ec1102f893..4750bbbb64 100644
--- a/docs/en/framework/ui/angular/component-replacement.md
+++ b/docs/en/framework/ui/angular/component-replacement.md
@@ -171,7 +171,7 @@ export const appConfig: ApplicationConfig = {
withOptions({
dynamicLayouts: myDynamicLayouts,
environment,
- registerLocaleFn: registerLocale(),
+ registerLocaleFn: registerLocaleForEsBuild(),
}),
),
],
@@ -180,6 +180,8 @@ export const appConfig: ApplicationConfig = {
In this code, `myDynamicLayouts` is the map of dynamic layouts you defined earlier. We pass this map to the `provideAbpCore` using the `withOptions` method.
+This example uses the Angular application builder. Use `registerLocale()` instead when the application uses the Webpack builder. See [Registering a New Locale](./localization.md#registering-a-new-locale) for the builder-specific setup.
+
Now that you have defined the new layout, you can use it in the router definition. You do this by adding a new route that uses the new layout.
Here's how you can do it:
diff --git a/docs/en/framework/ui/angular/datetime-format-pipe.md b/docs/en/framework/ui/angular/datetime-format-pipe.md
index e0e5493851..190b17aab6 100644
--- a/docs/en/framework/ui/angular/datetime-format-pipe.md
+++ b/docs/en/framework/ui/angular/datetime-format-pipe.md
@@ -1,41 +1,117 @@
```json
//[doc-seo]
{
- "Description": "Learn how to easily format dates in Angular using DateTime format pipes for shortDate, shortTime, and shortDateTime with culture settings."
+ "Description": "Format dates and handle clock-aware timezone conversion in ABP Angular applications with pipes, TimeService, and TimezoneService."
}
```
{%{
-# DateTime Format Pipes
+# Date and Time
-You can format date by Date pipe of angular.
+ABP Angular provides culture-aware format pipes, clock-aware UTC conversion, timezone selection, and date-time services. These APIs use the culture, clock, and timezone values from the application configuration.
+
+## Culture-Aware Format Pipes
+
+Angular's built-in `DatePipe` can format a date directly:
-Example
```html
-{{today | date 'dd/mm/yy'}}
+{{ today | date:'dd/MM/yy' }}
```
-ShortDate, ShortTime and ShortDateTime format data like angular's data pipe but easier. Also the pipes get format from config service by culture.
+The ABP pipes below use the short date and time patterns returned in the application localization configuration.
-## ShortDate Pipe
+### `shortDate`
```html
- {{today | shortDate }}
+{{ today | shortDate }}
```
+### `shortTime`
-## ShortTime Pipe
+```html
+{{ today | shortTime }}
+```
+
+### `shortDateTime`
```html
- {{today | shortTime }}
+{{ today | shortDateTime }}
```
+These pipes extend Angular's `DatePipe`. They select the format pattern from `ConfigStateService`; they do not apply ABP's clock-aware timezone selection. Use `abpUtcToLocal` when the value also needs to follow the application's clock and timezone.
+
+## Clock-Aware UTC Conversion
-## ShortDateTime Pipe
+The `abpUtcToLocal` pipe accepts `date`, `time`, or `datetime` as its format type:
```html
- {{today | shortDateTime }}
+{{ order.creationTime | abpUtcToLocal:'datetime' }}
+```
+
+Its behavior depends on the clock configuration returned by the backend:
+
+- With the UTC clock enabled, it converts the input to `TimezoneService.timezone`, including the daylight-saving-time offset for that date.
+- With a non-UTC clock, it formats the value without applying the configured timezone conversion.
+- Empty or invalid input produces an empty string.
+
+The output pattern is still taken from the current application's short date and time formats.
+
+## `TimezoneService`
+
+Inject `TimezoneService` to read or persist the timezone used by the Angular application:
+
+```ts
+import { TimezoneService } from '@abp/ng.core';
+import { Component, inject } from '@angular/core';
+
+@Component({
+ selector: 'app-timezone-selector',
+ template: ``,
+})
+export class TimezoneSelectorComponent {
+ private readonly timezoneService = inject(TimezoneService);
+
+ selectTimezone(): void {
+ this.timezoneService.setTimezone('Europe/Istanbul');
+ }
+}
+```
+
+`timezone` returns:
+
+- the browser timezone when the backend clock is not UTC;
+- the `Abp.Timing.TimeZone` setting when the clock is UTC and the setting has a value;
+- the browser timezone as a fallback when the UTC setting is empty.
+
+`setTimezone` writes the selected IANA timezone to the `__timezone` cookie only when the UTC clock is enabled.
+
+When you configure the application with `provideAbpCore`, its built-in `timezoneInterceptor` adds the effective timezone to outgoing `HttpClient` requests as the `__timezone` header. It does not add the header when the UTC clock is disabled.
+
+## `TimeService`
+
+`TimeService` returns [Luxon](https://moment.github.io/luxon/#/) `DateTime` values and formats dates with the current Angular locale:
+
+```ts
+import { TimeService } from '@abp/ng.core';
+import { inject, Injectable } from '@angular/core';
+
+@Injectable({ providedIn: 'root' })
+export class ScheduleFormatter {
+ private readonly timeService = inject(TimeService);
+
+ formatForIstanbul(value: string): string {
+ return this.timeService.format(value, 'ff', 'Europe/Istanbul');
+ }
+}
```
+| Method | Behavior |
+| --- | --- |
+| `now(zone = 'local')` | Returns the current time in the requested IANA timezone. |
+| `toZone(value, zone)` | Parses an ISO string or `Date` and returns a Luxon value in the requested timezone. |
+| `format(value, format = 'ff', zone = 'local')` | Converts to the timezone, applies its DST rules, and formats with the current locale. |
+| `formatDateWithStandardOffset(value, format = 'ff', zone?)` | Applies the zone's January 1 offset and formats without any further timezone or DST conversion. |
+| `formatWithoutTimeZone(value, format = 'ff')` | Formats the parsed ISO clock fields without shifting them to another timezone. |
+
}%}
diff --git a/docs/en/framework/ui/angular/http-error-handling.md b/docs/en/framework/ui/angular/http-error-handling.md
index 33c4b0cec2..aa436a8293 100644
--- a/docs/en/framework/ui/angular/http-error-handling.md
+++ b/docs/en/framework/ui/angular/http-error-handling.md
@@ -13,7 +13,11 @@ ABP offers a configurations for errors handling like below
```ts
//app.config.ts
-import { provideAbpThemeShared } from '@abp/ng.theme.shared';
+import { ApplicationConfig } from '@angular/core';
+import {
+ provideAbpThemeShared,
+ withHttpErrorConfig,
+} from '@abp/ng.theme.shared';
import { CustomErrorComponent } from './custom-error.component';
export const appConfig: ApplicationConfig = {
@@ -75,15 +79,13 @@ export function handleHttpErrors(injector: Injector, httpError: HttpErrorRespons
}
// app.config.ts
-import { Error404Component } from './error404/error404.component';
-import { handleHttpErrors } from './http-error-handling';
-import { HTTP_ERROR_HANDLER, ... } from '@abp/ng.theme.shared';
+import { ApplicationConfig } from '@angular/core';
+import { HTTP_ERROR_HANDLER } from '@abp/ng.theme.shared';
+import { handleHttpErrors } from './http-error-handler';
export const appConfig: ApplicationConfig = {
providers: [
- ...
{ provide: HTTP_ERROR_HANDLER, useValue: handleHttpErrors },
- ...
],
};
@@ -116,7 +118,9 @@ export function handleHttpErrors(
- `httpError` is the second parameter of the error handler function which is registered to the `HTTP_ERROR_HANDLER` provider. Type of the `httpError` is `HttpErrorResponse`.
```ts
-import { of } from "rxjs";
+import { HttpErrorResponse } from '@angular/common/http';
+import { Injector } from '@angular/core';
+import { of } from 'rxjs';
export function handleHttpErrors(
injector: Injector,
@@ -163,11 +167,13 @@ See an example:
```ts
// custom-error-handler.service.ts
-import { inject, Injectable } from "@angular/core";
-import { HttpErrorResponse } from "@angular/common/http";
-import { CustomHttpErrorHandlerService } from "@abp/ng.theme.shared";
-import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from "@abp/ng.theme.shared";
-import { ToasterService } from "@abp/ng.theme.shared";
+import { HttpErrorResponse } from '@angular/common/http';
+import { inject, Injectable } from '@angular/core';
+import {
+ CUSTOM_HTTP_ERROR_HANDLER_PRIORITY,
+ CustomHttpErrorHandlerService,
+ ToasterService,
+} from '@abp/ng.theme.shared';
@Injectable({ providedIn: "root" })
export class MyCustomErrorHandlerService
@@ -190,8 +196,8 @@ export class MyCustomErrorHandlerService
// If this service is picked from ErrorHandler, this execute method will be called.
execute() {
this.toaster.error(
- this.error.error?.error?.message || "Bad request!",
- "400"
+ this.error?.error?.error?.message || 'Bad request!',
+ '400',
);
}
}
@@ -200,17 +206,17 @@ export class MyCustomErrorHandlerService
```ts
// app.config.ts
-import { CUSTOM_ERROR_HANDLERS, ... } from '@abp/ng.theme.shared';
+import { ApplicationConfig } from '@angular/core';
+import { CUSTOM_ERROR_HANDLERS } from '@abp/ng.theme.shared';
import { MyCustomErrorHandlerService } from './custom-error-handler.service';
export const appConfig: ApplicationConfig = {
providers: [
- //...
{
provide: CUSTOM_ERROR_HANDLERS,
useExisting: MyCustomErrorHandlerService,
multi: true,
- }
+ },
],
};
```
@@ -227,5 +233,5 @@ In the example above:
- If your service cannot handle the error. Then ABP will check the next Error Service.
- If none of the service handle the error. Then basic confirmation message about the error will be shown to the user.
-- You can provide more than one service, with CUSTOM_ERROR_HANDLER injection token.
+- You can provide more than one service with the `CUSTOM_ERROR_HANDLERS` injection token.
- If you want your custom service to be evaluated (checked) earlier, set the priority variable high.
diff --git a/docs/en/framework/ui/angular/http-requests.md b/docs/en/framework/ui/angular/http-requests.md
index a5ca0dec66..db7c65ab12 100644
--- a/docs/en/framework/ui/angular/http-requests.md
+++ b/docs/en/framework/ui/angular/http-requests.md
@@ -30,7 +30,7 @@ An `HttpInterceptor` is able to catch `HttpErrorResponse` and can be used for a
## RestService
-ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by default.
+ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and reports them through `HttpErrorReporterService`. The error handler provided by the Theme Shared package subscribes to that service and displays the appropriate error UI. When the Theme Shared provider is configured in your application, HTTP errors from `RestService` are handled automatically by default.
### Getting Started with RestService
@@ -110,7 +110,7 @@ deleteFoo(id: number) {
}
```
-`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription.
+The `skipHandleError` config option, when set to `true`, prevents `RestService` from reporting the error through `HttpErrorReporterService`. The returned observable still throws the error, so you can handle it in the caller.
```js
removeFooFromList(id: number) {
diff --git a/docs/en/framework/ui/angular/list-service.md b/docs/en/framework/ui/angular/list-service.md
index dab58d0e33..6c65ac6efc 100644
--- a/docs/en/framework/ui/angular/list-service.md
+++ b/docs/en/framework/ui/angular/list-service.md
@@ -15,11 +15,11 @@
`ListService` is **not provided in root**. The reason is, this way, it will clear any subscriptions on component destroy. You may use the optional `LIST_QUERY_DEBOUNCE_TIME` token to adjust the debounce behavior.
-```js
-import { ListService } from '@abp/ng.core';
+```ts
+import { LIST_QUERY_DEBOUNCE_TIME, ListService } from '@abp/ng.core';
import { BookDto } from '../models';
import { BookService } from '../services';
-import { inject } from '@angular/core';
+import { Component, inject } from '@angular/core';
@Component({
/* class metadata here */
@@ -29,7 +29,7 @@ import { inject } from '@angular/core';
// [Optional]
// Provide this token if you want a different debounce time.
- // Default is 300. Cannot be 0. Any value below 100 is not recommended.
+ // Default is 300. Use 0 to disable debouncing.
{ provide: LIST_QUERY_DEBOUNCE_TIME, useValue: 500 },
],
template: `
@@ -55,7 +55,7 @@ class BookComponent {
this.list.hookToQuery(bookStreamCreator).subscribe(
response => {
this.items = response.items;
- this.count = response.count;
+ this.count = response.totalCount;
// If you use OnPush change detection strategy,
// call detectChanges method of ChangeDetectorRef here.
}
diff --git a/docs/en/framework/ui/angular/localization.md b/docs/en/framework/ui/angular/localization.md
index d50484c83a..b6a77ec0f7 100644
--- a/docs/en/framework/ui/angular/localization.md
+++ b/docs/en/framework/ui/angular/localization.md
@@ -72,6 +72,27 @@ Then, we can use this key like this:
```
+### Using the Async Localization Pipe
+
+Use `abpAsyncLocalization` when the template must wait for the application localization state before resolving a key. The pipe returns an observable, so combine it with Angular's `async` pipe:
+
+```ts
+import { AsyncPipe } from '@angular/common';
+import { Component } from '@angular/core';
+import { AsyncLocalizationPipe } from '@abp/ng.core';
+
+@Component({
+ selector: 'app-greeting',
+ imports: [AsyncPipe, AsyncLocalizationPipe],
+ template: `
+
+ `,
+})
+export class GreetingComponent {}
+```
+
+The observable initially emits an empty string. After the localization configuration is available, it emits the localized value. It also emits an empty string when the key cannot be resolved. Interpolation parameters can be passed in the same way as with `abpLocalization`.
+
### Using the Localization Service
First of all, you should import the `LocalizationService` from **@abp/ng.core**
@@ -212,7 +233,7 @@ The localizations above can be used like this:
```
-> **Note:** If you have specified the same localizations in the UI and backend, the backend localizations override the UI localizations.
+> **Note:** If the same localization key is specified in the UI and backend, the UI localization overrides the backend localization.
## RTL Support
@@ -279,9 +300,42 @@ export class AppComponent {}
## Registering a New Locale
-Since ABP has more than one language, Angular locale files load lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and to register the Angular core using the [`registerLocaleData`](https://angular.dev/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `provideAbpCore(withOptions({...}))`.
+ABP loads Angular locale data lazily and registers it with Angular's [`registerLocaleData`](https://angular.dev/api/common/registerLocaleData) function. The registration function depends on the Angular builder used by your application:
+
+| Builder | Registration function |
+| --- | --- |
+| Angular application builder (`@angular/build:application`) | `registerLocaleForEsBuild()` |
+| Webpack builder | `registerLocale()` |
+
+Pass the selected function as `registerLocaleFn` to `provideAbpCore(withOptions({...}))`.
+
+### Application Builder (EsBuild)
+
+Current ABP Angular application templates use the Angular application builder. Configure them with `registerLocaleForEsBuild`:
-### registerLocaleFn
+```ts
+import { provideAbpCore, withOptions } from '@abp/ng.core';
+import { registerLocaleForEsBuild } from '@abp/ng.core/locale';
+import { ApplicationConfig } from '@angular/core';
+import { environment } from '../environments/environment';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideAbpCore(
+ withOptions({
+ environment,
+ registerLocaleFn: registerLocaleForEsBuild({
+ cultureNameLocaleFileMap: { 'pt-BR': 'pt' },
+ }),
+ }),
+ ),
+ ],
+};
+```
+
+`registerLocaleForEsBuild` uses a fixed list of supported Angular locale imports so the application builder can include them in the bundle.
+
+### Webpack Builder
The `registerLocale` function, exported from the `@abp/ng.core/locale` package, is a **higher-order function**.
@@ -290,7 +344,7 @@ It accepts the following parameters:
- **`cultureNameLocaleFileMap`** – an object that maps culture names to their corresponding locale files.
- **`errorHandlerFn`** – a function that handles any errors that occur during locale loading.
-It returns a **Webpack `import` function**.
+It returns a **Webpack `import` function**. Use it only when the application is built with Webpack.
You should use `registerLocale` within the `withOptions` function of `provideAbpCore`, as shown in the example below:
@@ -326,14 +380,14 @@ Some of the culture names defined in .NET do not match Angular locales. In such

-If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to the `registerLocale` function.
+If you see an error like this, pass the `cultureNameLocaleFileMap` property to the registration function selected for your builder. The following example uses the Angular application builder:
```ts
// app.config.ts
-import { registerLocale } from "@abp/ng.core/locale";
-// if you have commercial license and the language management module, add the below import
-// import { registerLocale } from '@volo/abp.ng.language-management/locale';
+import { registerLocaleForEsBuild } from "@abp/ng.core/locale";
+// If you use the Language Management module, replace the import above with:
+// import { registerLocale as registerLocaleForEsBuild } from '@volo/abp.ng.language-management/locale';
export const appConfig: ApplicationConfig = {
providers: [
@@ -341,7 +395,7 @@ export const appConfig: ApplicationConfig = {
provideAbpCore(
withOptions({
// ...,
- registerLocaleFn: registerLocale({
+ registerLocaleFn: registerLocaleForEsBuild({
cultureNameLocaleFileMap: {
DotnetCultureName: "AngularLocaleFileName",
"pt-BR": "pt", // example
@@ -353,6 +407,8 @@ export const appConfig: ApplicationConfig = {
};
```
+For a Webpack project, pass the same option object to `registerLocale()` instead.
+
See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales).
### Adding a New Culture
@@ -370,7 +426,7 @@ import(
).then((m) => storeLocaleData(m.default, "your-locale"));
```
-You can also configure a custom `registerLocale` function that can be passed to the abp core provider configuration options:
+In a Webpack project, you can also configure a custom `registerLocale` function and pass it to the ABP Core provider options:
```ts
// register-locale.ts
diff --git a/docs/en/framework/ui/angular/lookup-search-component.md b/docs/en/framework/ui/angular/lookup-search-component.md
new file mode 100644
index 0000000000..144f24c1e1
--- /dev/null
+++ b/docs/en/framework/ui/angular/lookup-search-component.md
@@ -0,0 +1,95 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to use the generic ABP Angular lookup search component with remote searches, two-way values and custom templates."
+}
+```
+
+# Lookup Search Component
+
+`LookupSearchComponent` is a generic standalone search control exported by `@abp/ng.components/lookup`. A search function returns observable lookup items, and the component manages debouncing, loading, selection and clearing.
+
+This component is part of the open-source `@abp/ng.components` package. It is different from the [commercial lookup component family](./lookup-components.md), which provides form-oriented typeahead, select and table controls.
+
+The package is included in the Angular application templates. Install it if the application does not already reference it:
+
+```bash
+npm install @abp/ng.components
+```
+
+```ts
+import { Component, inject, signal } from '@angular/core';
+import {
+ LookupItem,
+ LookupSearchComponent,
+ LookupSearchFn,
+} from '@abp/ng.components/lookup';
+import { map } from 'rxjs';
+import { BookService } from '../services/book.service';
+
+interface BookLookupItem extends LookupItem {
+ authorName: string;
+}
+
+@Component({
+ selector: 'app-book-lookup',
+ templateUrl: './book-lookup.component.html',
+ imports: [LookupSearchComponent],
+})
+export class BookLookupComponent {
+ private readonly bookService = inject(BookService);
+
+ readonly selectedBookId = signal('');
+ readonly selectedBookName = signal('');
+
+ readonly searchBooks: LookupSearchFn = filter =>
+ this.bookService.search(filter).pipe(
+ map(books =>
+ books.map(book => ({
+ key: book.id,
+ displayName: book.name,
+ authorName: book.authorName,
+ })),
+ ),
+ );
+
+ onBookSelected(book: BookLookupItem) {
+ console.log(book.key);
+ }
+}
+```
+
+{%{
+```html
+
+
+ {{ book.displayName }}
+ {{ book.authorName }}
+
+
+```
+}%}
+
+Each lookup item uses `key` as its selected value and `displayName` as its displayed value by default. Set `valueKey` or `displayKey` to use another item property.
+
+Searches use a 300 millisecond debounce by default. Configure `debounceTime` and `minSearchLength` when the remote endpoint needs different behavior. `label` and `placeholder` values are passed through ABP localization.
+
+`selectedValue` and `displayValue` are model inputs and support two-way binding. The component also emits `searchChanged` for input changes and `itemSelected` after selection.
+
+Add an `#itemTemplate` template to customize each result, as in the previous example. Add a `#noResultsTemplate` template to replace the default no-results content:
+
+```html
+
+
No matching books
+
+```
diff --git a/docs/en/framework/ui/angular/modifying-the-menu.md b/docs/en/framework/ui/angular/modifying-the-menu.md
index eec9e79ba0..7f381997de 100644
--- a/docs/en/framework/ui/angular/modifying-the-menu.md
+++ b/docs/en/framework/ui/angular/modifying-the-menu.md
@@ -16,7 +16,7 @@ The `logoUrl` property in the environment variables is the url of the logo.
You can add your logo to `src/assets` folder and set the `logoUrl` as shown below:
-```js
+```ts
export const environment = {
// other configurations
application: {
@@ -74,7 +74,7 @@ Add the following to your `src/styles.scss`:
You can add routes to the menu by calling the `add` method of `RoutesService`. It is a singleton service, i.e. provided in root, so you can inject and use it immediately.
-```js
+```ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
import { Component, inject } from '@angular/core';
@@ -106,10 +106,10 @@ export class AppComponent {
An alternative and probably cleaner way is to use a route provider. First create a provider:
-```js
+```ts
// route.provider.ts
import { RoutesService, eLayoutType } from '@abp/ng.core';
-import { provideAppInitializer } from '@angular/core';
+import { inject, provideAppInitializer } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
provideAppInitializer(() => {
@@ -119,7 +119,7 @@ export const APP_ROUTE_PROVIDER = [
function configureRoutes() {
const routesService = inject(RoutesService);
- routes.add([
+ routesService.add([
{
path: '/your-path',
name: 'Your navigation',
@@ -142,13 +142,14 @@ function configureRoutes() {
We can also define a group for navigation elements. It's an optional property
- **Note:** It'll also include groups that were defined at the modules
-```js
+```ts
// route.provider.ts
import { RoutesService } from '@abp/ng.core';
+import { inject } from '@angular/core';
function configureRoutes() {
const routesService = inject(RoutesService);
- routes.add([
+ routesService.add([
{
//etc..
group: 'ModuleName::GroupName'
@@ -167,7 +168,7 @@ function configureRoutes() {
To get the route items as grouped we can use the `groupedVisible` (or Observable one `groupedVisible$`) getter methods
- It returns `RouteGroup[]` if there is any group in the route tree, otherwise it returns `undefined`
-```js
+```ts
import { ABP, RoutesService, RouteGroup } from "@abp/ng.core";
import { Component, inject } from "@angular/core";
import { Observable } from "rxjs";
@@ -185,7 +186,7 @@ export class AppComponent {
...and then in app.config.ts...
- The `groupedVisible` method will return the `Others` group for ungrouped items, the default key is `AbpUi::OthersGroup`, we can change this `key` via the `OTHERS_GROUP` injection token
-```js
+```ts
import { OTHERS_GROUP } from '@abp/ng.core';
import { APP_ROUTE_PROVIDER } from './route.provider';
@@ -239,7 +240,7 @@ You can define your routes by adding `routes` as a child property to `data` prop
You can add the `routes` property like below:
-```js
+```ts
{
path: 'your-path',
data: {
@@ -263,7 +264,7 @@ You can add the `routes` property like below:
Alternatively, you can do this:
-```js
+```ts
{
path: 'your-path',
data: {
@@ -297,7 +298,7 @@ After adding the `routes` property as described above, the navigation menu looks
The `patch` method of `RoutesService` finds a route by its name and replaces its configuration with the new configuration passed as the second parameter. Similarly, `remove` method finds a route and removes it along with its children. Also you can use `removeByParam` method to delete the routes with given properties.
-```js
+```ts
// this.routes is instance of RoutesService
// eThemeSharedRouteNames enum can be imported from @abp/ng.theme.shared
@@ -343,7 +344,7 @@ After the operations above, the new menu looks like below:
You can add elements to the right part of the menu by calling the `addItems` method of `NavItemsService`. It is a singleton service, i.e. provided in root, so you can inject and use it immediately.
-```js
+```ts
import { NavItemsService } from '@abp/ng.theme.shared';
import { Component, inject } from '@angular/core';
@@ -387,7 +388,7 @@ This inserts a search input and a sign out icon to the menu. The final UI looks
The `patchItem` method of `NavItemsService` finds an element by its `id` property and replaces its configuration with the new configuration passed as the second parameter. Similarly, `removeItem` method finds an element and removes it.
-```js
+```ts
export class AppComponent {
private navItems = inject(NavItemsService);
diff --git a/docs/en/framework/ui/angular/oauth-module.md b/docs/en/framework/ui/angular/oauth-module.md
index 9e6d4a8e1e..b8730806e4 100644
--- a/docs/en/framework/ui/angular/oauth-module.md
+++ b/docs/en/framework/ui/angular/oauth-module.md
@@ -7,23 +7,56 @@
# ABP OAuth Package
-The authentication functionality has been moved from @abp/ng.core to @abp/ng.oauth since v7.0.
+The authentication implementation was moved from `@abp/ng.core` to `@abp/ng.oauth` in v7.0. The core package defines the authentication abstractions and tokens, while the OAuth package supplies their `angular-oauth2-oidc` implementations.
-If your app is version 8.3 or higher, you should include "provideAbpOAuth()" after "provideAbpCore()" in the `appConfig` array of your `app.config.ts`.
+The package is included in the Angular application templates. Install it if the application does not already reference it:
-Those abstractions can be found in the @abp/ng-core packages.
+```bash
+npm install @abp/ng.oauth
+```
+
+## Standalone Setup
+
+Add `provideAbpOAuth()` after `provideAbpCore()` in the `providers` array of `app.config.ts`:
+
+```ts
+import { ApplicationConfig } from '@angular/core';
+import { provideAbpCore, withOptions } from '@abp/ng.core';
+import { registerLocaleForEsBuild } from '@abp/ng.core/locale';
+import { provideAbpOAuth } from '@abp/ng.oauth';
+import { environment } from '../environments/environment';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideAbpCore(
+ withOptions({
+ environment,
+ registerLocaleFn: registerLocaleForEsBuild(),
+ }),
+ ),
+ provideAbpOAuth(),
+ ],
+};
+```
+
+`AbpOAuthModule.forRoot()` is deprecated. Use the standalone provider for new applications.
+
+## Registered Authentication Services
+
+`provideAbpOAuth()` registers or replaces these public core abstractions:
-- `AuthService` (the class that implements the IAuthService interface).
-- `NAVIGATE_TO_MANAGE_PROFILE` Inject token.
-- `ApiInterceptor` (the class that implements the IApiInterceptor interface).
+- `AuthService`, `AuthGuard`, `authGuard` and `asyncAuthGuard` with their OAuth implementations.
+- `ApiInterceptor` and its `HTTP_INTERCEPTORS` registration.
+- `PIPE_TO_LOGIN_FN_KEY` with the function used when authentication is required.
+- `CHECK_AUTHENTICATION_STATE_FN_KEY` with the function that checks and stores the current authentication state.
+- `NAVIGATE_TO_MANAGE_PROFILE` with navigation to the authority's account-management page.
+- `AuthErrorFilterService` with the OAuth error filter.
+- `OAuthStorage`, using `BrowserTokenStorageService` or `ServerTokenStorageService` for an SSR-started application and `MemoryTokenStorageService` otherwise.
-Those base classes are overridden by the "AbpOAuthModule" for oAuth. There are also three functions provided with AbpOAuthModule.
+It also registers the OAuth configuration initializer and the providers from `angular-oauth2-oidc`.
-- `PIPE_TO_LOGIN_FN_KEY` a provide that calls a function when the user is not authenticated. The function should be PipeToLoginFn type.
-- `SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY` a provide that calls a function when the user is authenticated. The function should be SetTokenResponseToStorageFn type.
-- `CHECK_AUTHENTICATION_STATE_FN_KEY` a provide that calls a function when the user is authenticated and stores the auth state. The function should be CheckAuthenticationStateFn type.
- The tokens and interfaces are in the `@abp/ng.core` package but the implementation of these interfaces is in the `@abp/ng.oauth` package.
+## API Interceptor
-If you want to make your own authentication system, you must also change these 'abstract' classes.
+For non-external requests, the OAuth API interceptor adds the `X-Requested-With` header and, when the corresponding values are available, an `Authorization` bearer token, `Accept-Language` and the configured tenant header. Existing authorization, language and tenant headers are preserved. Requests marked with the `IS_EXTERNAL_REQUEST` HTTP context token are sent without those ABP headers. The interceptor also integrates every request with the HTTP wait service.
-ApiInterceptor is provided by `@abp/ng.core` but overridden with `@abp/ng.oauth`. The ApiInterceptor adds the token, accepted-language, and tenant id to the header of the HTTP request. It also calls the http-wait service.
+To implement another authentication system, provide replacements for the core services and tokens used by the application instead of depending on the OAuth implementations.
diff --git a/docs/en/framework/ui/angular/permission-management.md b/docs/en/framework/ui/angular/permission-management.md
index 8d30d925bb..5ec8497af4 100644
--- a/docs/en/framework/ui/angular/permission-management.md
+++ b/docs/en/framework/ui/angular/permission-management.md
@@ -42,7 +42,8 @@ const hasIdentityOrAccountPermission = this.permissionService.getGrantedPolicy(
Please consider the following **rules** when creating your permission selectors:
-- Maximum 2 keys can be combined.
+- Two or more keys can be combined with the same operator.
+- Do not mix `&&` and `||` in the same selector.
- `&&` operator looks for both keys.
- `||` operator looks for either key.
- Empty string `''` as key will return `true`
@@ -89,18 +90,14 @@ In some cases, a custom permission management may be needed. All you need to do
- First, create a service of your own. Let's call it `CustomPermissionService` and extend `PermissionService` from `@abp/ng.core` as follows:
-```js
-import { ConfigStateService, PermissionService } from '@abp/ng.core';
-import { Injectable, inject } from '@angular/core';
+```ts
+import { PermissionService } from '@abp/ng.core';
+import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class CustomPermissionService extends PermissionService {
- constructor() {
- super(inject(ConfigStateService));
- }
-
// This is an example to show how to override the methods
getGrantedPolicy$(key: string) {
return super.getGrantedPolicy$(key);
diff --git a/docs/en/framework/ui/angular/ssr-configuration.md b/docs/en/framework/ui/angular/ssr-configuration.md
index 4d889377fd..87f95185a3 100644
--- a/docs/en/framework/ui/angular/ssr-configuration.md
+++ b/docs/en/framework/ui/angular/ssr-configuration.md
@@ -504,6 +504,21 @@ export class MyService {
- `key(index: number): string | null`
- `length: number`
+#### Session and Cookie Storage
+
+`AbpCookieStorageService` provides the cookie-backed `Storage` implementation used during SSR. On the server, it reads cookies from the incoming Angular `REQUEST`; write and remove operations do nothing. In the browser, it reads and writes `document.cookie`. Values written with `setItem` use `Path=/`, `SameSite=Lax`, and `Secure`. Use `setItemWithExpiry` when a cookie also needs a maximum age.
+
+`SessionStateService` selects its storage automatically:
+
+- When the application started with SSR, it persists the `abpSession` value in `AbpCookieStorageService` so the server request can read the session state.
+- Otherwise, it persists `abpSession` in `AbpLocalStorageService`.
+
+Application code normally uses `SessionStateService` for the current language and tenant instead of reading `abpSession` directly.
+
+#### Cross-Tab Authentication Changes
+
+`provideAbpCore` initializes `LocalStorageListenerService`. In the browser, this service listens for storage changes to the `access_token` key. When another browser context adds or removes that token, the current page navigates to `/` so its authentication state is refreshed.
+
### 9.3. Hydration Mismatch Errors
If you see "NG0500" errors in the console:
@@ -515,7 +530,7 @@ If you see "NG0500" errors in the console:
### 9.4. Avoiding Duplicate API Calls
-ABP Core provides a `transferStateInterceptor` that automatically prevents duplicate HTTP GET requests during hydration. When you use `provideAbpCore()`, this interceptor is already active.
+ABP Core provides a `transferStateInterceptor` that automatically prevents duplicate HTTP GET requests during hydration. When you configure `provideAbpCore(withOptions(...))`, this interceptor is already active.
**How it works:**
- Server: Stores HTTP GET responses in `TransferState`
@@ -524,16 +539,26 @@ ABP Core provides a `transferStateInterceptor` that automatically prevents dupli
```typescript
// app.config.ts
-import { provideAbpCore } from '@abp/ng.core';
+import { provideAbpCore, withOptions } from '@abp/ng.core';
+import { registerLocaleForEsBuild } from '@abp/ng.core/locale';
+import { ApplicationConfig } from '@angular/core';
+import { environment } from '../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
- provideAbpCore(),
+ provideAbpCore(
+ withOptions({
+ environment,
+ registerLocaleFn: registerLocaleForEsBuild(),
+ }),
+ ),
// transferStateInterceptor is automatically included
]
};
```
+The example uses the Angular application builder. If your SSR application uses the Webpack builder, use `registerLocale()` instead. See [Registering a New Locale](localization.md#registering-a-new-locale) for the builder-specific configuration.
+
The interceptor works with all HTTP GET requests made through `HttpClient`:
```typescript
diff --git a/docs/en/framework/ui/angular/testing.md b/docs/en/framework/ui/angular/testing.md
index 4ec5577f73..112a785e49 100644
--- a/docs/en/framework/ui/angular/testing.md
+++ b/docs/en/framework/ui/angular/testing.md
@@ -137,6 +137,35 @@ expect(deleteSpy).toHaveBeenCalledWith("some-id");
The template's `home.component.spec.ts` is a good reference for mocking ABP services and asserting DOM behavior with `TestBed`.
+### Configuring Permission Results
+
+`CoreTestingModule.withConfig()` replaces `PermissionService` with `MockPermissionService`. The mock grants every policy by default, which keeps unrelated permission checks from hiding components in a test.
+
+Use `grantPolicies` when a spec needs a specific authorization state. Policies not included in the array are denied:
+
+```ts
+import { PermissionService } from '@abp/ng.core';
+import { MockPermissionService } from '@abp/ng.core/testing';
+import { TestBed } from '@angular/core/testing';
+
+let permissionService: MockPermissionService;
+
+beforeEach(() => {
+ permissionService = TestBed.inject(PermissionService) as MockPermissionService;
+});
+
+it('shows the create action only for the create policy', () => {
+ permissionService.grantPolicies(['BookStore.Books.Create']);
+
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.querySelector('[data-testid="create-book"]')).toBeTruthy();
+ expect(fixture.nativeElement.querySelector('[data-testid="delete-book"]')).toBeFalsy();
+});
+```
+
+Call `grantAllPolicies()` to restore the grant-all behavior in a test that previously selected individual policies.
+
## Tips
### Clearing DOM After Each Spec
diff --git a/docs/en/framework/ui/angular/title-strategy.md b/docs/en/framework/ui/angular/title-strategy.md
new file mode 100644
index 0000000000..831f2e9bdf
--- /dev/null
+++ b/docs/en/framework/ui/angular/title-strategy.md
@@ -0,0 +1,96 @@
+```json
+//[doc-seo]
+{
+ "Description": "Configure localized Angular route titles, the application-name suffix, and a custom TitleStrategy in an ABP application."
+}
+```
+
+# Document Title Strategy
+
+`provideAbpCore` registers `AbpTitleStrategy` as Angular's default `TitleStrategy`. It reads the deepest active route title, localizes it, and updates the browser document title.
+
+## Setting a Route Title
+
+Use Angular's `title` route property. The value can be an ABP localization key:
+
+```ts
+import { Routes } from '@angular/router';
+import { BooksComponent } from './books.component';
+
+export const routes: Routes = [
+ {
+ path: 'books',
+ component: BooksComponent,
+ title: 'BookStore::Menu:Books',
+ },
+];
+```
+
+With an application name of `Book Store`, the resulting title is:
+
+```text
+Books | Book Store
+```
+
+The strategy resolves the application name from the `::AppName` localization key. If a route has no title, it uses only the application name. It also recalculates the active title when the language changes.
+
+## Removing the Application-Name Suffix
+
+Provide `DISABLE_PROJECT_NAME` with `true` to omit the application name from routes that have a title:
+
+```ts
+import { DISABLE_PROJECT_NAME } from '@abp/ng.core';
+import { ApplicationConfig } from '@angular/core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ {
+ provide: DISABLE_PROJECT_NAME,
+ useValue: true,
+ },
+ ],
+};
+```
+
+The route above then produces `Books`. A route without a title still falls back to the application name.
+
+## Replacing the Strategy
+
+Create an Angular `TitleStrategy` and pass it to the `withTitleStrategy` feature when the application needs a different title convention:
+
+```ts
+import { Title } from '@angular/platform-browser';
+import { RouterStateSnapshot, TitleStrategy } from '@angular/router';
+import { inject, Injectable } from '@angular/core';
+
+@Injectable({ providedIn: 'root' })
+export class CustomTitleStrategy extends TitleStrategy {
+ private readonly title = inject(Title);
+
+ override updateTitle(routerState: RouterStateSnapshot): void {
+ const routeTitle = this.buildTitle(routerState);
+ this.title.setTitle(routeTitle ? `My Application - ${routeTitle}` : 'My Application');
+ }
+}
+```
+
+Register it together with the normal ABP Core options:
+
+```ts
+import { provideAbpCore, withOptions, withTitleStrategy } from '@abp/ng.core';
+import { registerLocaleForEsBuild } from '@abp/ng.core/locale';
+import { ApplicationConfig } from '@angular/core';
+import { environment } from '../environments/environment';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideAbpCore(
+ withOptions({
+ environment,
+ registerLocaleFn: registerLocaleForEsBuild(),
+ }),
+ withTitleStrategy(CustomTitleStrategy),
+ ),
+ ],
+};
+```
diff --git a/docs/en/framework/ui/angular/tree-component.md b/docs/en/framework/ui/angular/tree-component.md
new file mode 100644
index 0000000000..2a7b446baa
--- /dev/null
+++ b/docs/en/framework/ui/angular/tree-component.md
@@ -0,0 +1,129 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to use the ABP Angular tree component for hierarchical data, selection, templates and drag-and-drop."
+}
+```
+
+# Tree Component
+
+`TreeComponent` is a standalone tree control exported by `@abp/ng.components/tree`. It supports selection, checkboxes, expansion, context-menu templates and drag-and-drop.
+
+The `@abp/ng.components` package is included in the Angular application templates. Install it if the application does not already reference it:
+
+```bash
+npm install @abp/ng.components
+```
+
+Import it into a standalone component and provide nodes in the format expected by the underlying NG-ZORRO tree:
+
+```ts
+import { Component, signal } from '@angular/core';
+import { DropEvent, TreeComponent } from '@abp/ng.components/tree';
+import { of } from 'rxjs';
+
+interface Category {
+ id: string;
+ name: string;
+}
+
+@Component({
+ selector: 'app-category-tree',
+ templateUrl: './category-tree.component.html',
+ imports: [TreeComponent],
+})
+export class CategoryTreeComponent {
+ readonly nodes = signal([
+ {
+ key: 'books',
+ title: 'Books',
+ entity: { id: 'books', name: 'Books' } as Category,
+ children: [],
+ isLeaf: true,
+ },
+ ]);
+
+ readonly expandedKeys = signal([]);
+ readonly selectedCategory = signal(null);
+
+ readonly allowDrop = () => of(true);
+
+ handleDrop(event: DropEvent) {
+ console.log(event.dragNode?.key);
+ }
+
+ edit(key: string) {
+ console.log(key);
+ }
+}
+```
+
+```html
+
+
+
+
+
+```
+
+The main inputs are:
+
+- `nodes`, `checkedKeys`, `expandedKeys` and `selectedNode` for tree state.
+- `draggable`, `checkable`, `checkStrictly` and `noAnimation` for tree behavior.
+- `changeCheckboxWithNode` to update checked keys when a node is selected.
+- `isNodeSelected` to replace the default selected-node comparison.
+- `beforeDrop` to approve or reject a drag-and-drop operation.
+
+State changes are exposed through `checkedKeysChange`, `expandedKeysChange`, `selectedNodeChange`, `dropOver` and `nzExpandChange`.
+
+The default `beforeDrop` handler rejects drops. Supply a handler that returns an observable accepted by the underlying tree control when drag-and-drop is enabled. Note that the default handler is also what records the drop position, so when you replace it, the `pos` property of the `DropEvent` emitted by `dropOver` is not set.
+
+## Templates
+
+Use the `#menu` template, as in the previous example, to add a context menu for each node. Use `abpTreeNodeTemplate` and `abpTreeExpandedIconTemplate` to replace the node and expanded-icon templates:
+
+{%{
+```html
+
+
+ {{ node.title }}
+
+
+
+ {{ node.isExpanded ? '−' : '+' }}
+
+
+```
+}%}
+
+Import `TreeNodeTemplateDirective` and `ExpandedIconTemplateDirective` from `@abp/ng.components/tree` into the standalone component that uses these templates, because Angular must see each directive used by a component template. If you use only one of these templates, import only its corresponding directive.
+
+## Flat-List Adapter
+
+`TreeAdapter` converts a flat list of `BaseNode` values into tree nodes. Each item requires an `id` and a nullable `parentId`; its `displayName` or `name` becomes the default node title. Use `getTree()`, `handleDrop()`, `handleRemove()` and `handleUpdate()` to keep the flat list and tree representations synchronized.
+
+## Style Loading
+
+The component loads `ng-zorro-antd-tree.css` when it is initialized. Provide `DISABLE_TREE_STYLE_LOADING_TOKEN` with `true` only when the application already includes that stylesheet:
+
+```ts
+import { DISABLE_TREE_STYLE_LOADING_TOKEN } from '@abp/ng.components/tree';
+
+export const providers = [
+ {
+ provide: DISABLE_TREE_STYLE_LOADING_TOKEN,
+ useValue: true,
+ },
+];
+```
diff --git a/docs/en/framework/ui/blazor/authentication.md b/docs/en/framework/ui/blazor/authentication.md
index a793a784bd..40bc6cf40f 100644
--- a/docs/en/framework/ui/blazor/authentication.md
+++ b/docs/en/framework/ui/blazor/authentication.md
@@ -31,4 +31,18 @@ This is a typical and recommended approach to implement authentication in Single
See the [Blazor Security document](https://docs.microsoft.com/en-us/aspnet/core/blazor/security) to understand and customize the authentication process.
-{{end}}
\ No newline at end of file
+{{end}}
+
+## Authentication URLs
+
+`AbpAuthenticationOptions` centralizes the login and logout routes used by ABP Blazor authentication services. The shared web defaults are `Account/Login` and `Account/Logout`. A standalone Blazor WebAssembly application changes them to `authentication/login` and `authentication/logout`; this override is not applied when the client is hosted as part of a Blazor Web App.
+
+Configure the options when the application uses custom routes:
+
+````csharp
+Configure(options =>
+{
+ options.LoginUrl = "authentication/sign-in";
+ options.LogoutUrl = "authentication/sign-out";
+});
+````
diff --git a/docs/en/framework/ui/blazor/global-scripts-styles.md b/docs/en/framework/ui/blazor/global-scripts-styles.md
index 998a16a50a..02ae892089 100644
--- a/docs/en/framework/ui/blazor/global-scripts-styles.md
+++ b/docs/en/framework/ui/blazor/global-scripts-styles.md
@@ -7,7 +7,7 @@
# Blazor UI: Managing Global Scripts & Styles
-You can add your JavaScript and CSS files from your modules or applications to the Blazor global assets system. All the JavaScript and CSS files will be added to the `global.js` and `global.css` files. You can access these files via the following URL in a Blazor WASM project:
+You can add your JavaScript and CSS files from your modules or applications to the Blazor global assets system. By default, all the JavaScript and CSS files are added to the `global.js` and `global.css` files. You can access these files via the following URL in a Blazor WASM project:
- https://localhost/global.js
- https://localhost/global.css
@@ -71,9 +71,37 @@ This is similar to the module. You need to define JavaScript and CSS contributor
## AbpBundlingGlobalAssetsOptions
-You can configure the JavaScript and CSS file names in the `GlobalAssets` property of the `AbpBundlingOptions` class. The default values are `global.js` and `global.css`.
+The `GlobalAssets` property of `AbpBundlingOptions` is shared by the MVC, Blazor WebAssembly and MAUI Blazor bundling integrations. It has the following properties:
+
+* `Enabled`: Enables global asset generation. The Blazor WebAssembly and MAUI Blazor theming modules enable it when they configure their global bundles.
+* `GlobalStyleBundleName`: The style bundle used to generate the global CSS asset.
+* `GlobalScriptBundleName`: The script bundle used to generate the global JavaScript asset.
+* `CssFileName`: The generated CSS file name. The default is `global.css`.
+* `JavaScriptFileName`: The generated JavaScript file name. The default is `global.js`.
+
+```csharp
+Configure(options =>
+{
+ options.GlobalAssets.Enabled = true;
+ options.GlobalAssets.GlobalStyleBundleName =
+ BlazorWebAssemblyStandardBundles.Styles.Global;
+ options.GlobalAssets.GlobalScriptBundleName =
+ BlazorWebAssemblyStandardBundles.Scripts.Global;
+ options.GlobalAssets.CssFileName = "app-global.css";
+ options.GlobalAssets.JavaScriptFileName = "app-global.js";
+});
+```
+
+When you change `CssFileName` or `JavaScriptFileName`, update the host page references to the same names. For a standalone Blazor WebAssembly host, replace the default references in `App.razor`:
+
+```html
+
+
+```
+
+For a Blazor Web App, replace `global.css` and `global.js` in the `GlobalStyles` and `GlobalScripts` lists in `Components/App.razor`. Changing only `AbpBundlingOptions` generates the files under the new names but does not rewrite these host references.
## Reference
-- [ASP.NET Core MVC Bundling & Minification](../mvc-razor-pages/bundling-minification#bundle-contributorsg)
+- [ASP.NET Core MVC Bundling & Minification](../mvc-razor-pages/bundling-minification.md#bundle-contributors)
- [ABP Global Assets - New way to bundle JavaScript/CSS files in Blazor WebAssembly app](https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md)
diff --git a/docs/en/framework/ui/maui/index.md b/docs/en/framework/ui/maui/index.md
index 2622bdc7e0..1bd089b26d 100644
--- a/docs/en/framework/ui/maui/index.md
+++ b/docs/en/framework/ui/maui/index.md
@@ -34,9 +34,9 @@ Open the `appsettings.json` in the `MAUI` project:
{{ end }}
-After ensuring the backend application is running and the `appsettings.json` is properly configured in the mobile application, you can proceed to run the mobile application. You can run the application either by using the `dotnet build` command (e.g. `dotnet build -t:Run -f net9.0-android` for Android or `dotnet build -t:Run -f net9.0-ios` for iOS) or by running it through Visual Studio or any other IDE that supports MAUI.
+After ensuring the backend application is running and the `appsettings.json` is properly configured in the mobile application, you can proceed to run the mobile application. You can run the application either by using the `dotnet build` command (e.g. `dotnet build -t:Run -f net10.0-android` for Android or `dotnet build -t:Run -f net10.0-ios` for iOS) or by running it through Visual Studio or any other IDE that supports MAUI.
-> For more information about running the mobile application, please refer to the [Microsoft's documentation](https://learn.microsoft.com/en-us/dotnet/maui/?view=net-maui-9.0).
+> For more information about running the mobile application, please refer to the [Microsoft's documentation](https://learn.microsoft.com/en-us/dotnet/maui/?view=net-maui-10.0).
You can examine the [Users Page](#users-page) or any other pre-defined page to see how to use CSharp Client Proxy to request backend API and consume the backend API in the same way in your application. Also, if you encounter any errors on specific platforms, you can refer to the following sections for each platform to find common issues and their solutions.
diff --git a/docs/en/framework/ui/mvc-razor-pages/javascript-api/clock.md b/docs/en/framework/ui/mvc-razor-pages/javascript-api/clock.md
new file mode 100644
index 0000000000..520f650087
--- /dev/null
+++ b/docs/en/framework/ui/mvc-razor-pages/javascript-api/clock.md
@@ -0,0 +1,57 @@
+```json
+//[doc-seo]
+{
+ "Description": "Use ABP's MVC JavaScript Clock API for time-zone detection, date normalization, localized display, and the browser time-zone cookie."
+}
+```
+
+# ASP.NET Core MVC / Razor Pages UI: JavaScript Clock API
+
+The `abp.clock` namespace provides date, time-zone and browser-time-zone helpers for MVC / Razor Pages applications. The application configuration script sets `abp.clock.kind` from the server-side ABP clock configuration.
+
+## Time-Zone Support
+
+`abp.clock.supportsMultipleTimezone()` returns `true` when the configured clock kind is `Utc`. `abp.clock.timeZone()` returns the value of the `Abp.Timing.TimeZone` setting when it is available and otherwise returns the browser's IANA time-zone name.
+
+````js
+if (abp.clock.supportsMultipleTimezone()) {
+ console.log(abp.clock.timeZone());
+}
+````
+
+## Normalize Date Values
+
+Use `normalizeToString` before sending a date value to the server and `normalizeToLocaleString` before displaying a server value:
+
+````js
+const requestValue = abp.clock.normalizeToString(new Date());
+const displayValue = abp.clock.normalizeToLocaleString(requestValue);
+````
+
+`normalizeToString` returns an ISO-compatible date-time string. For a non-UTC clock, the core implementation uses `yyyy-MM-ddTHH:mm:ss`. Both normalization methods return empty or invalid values unchanged.
+
+The standard shared MVC theme bundle loads Luxon and replaces the core implementations of `normalizeToString` and `normalizeToLocaleString`. When multiple time zones are supported, this Luxon implementation interprets the input in the configured IANA time zone, converts it to UTC and returns an ISO value ending in `Z`. The output can include milliseconds (for example, `2026-07-17T08:30:00.000Z`), so do not require an exact string length when consuming it.
+
+If an application uses the core scripts without the shared theme's Luxon contributor, the fallback implementation produces a `Z`-suffixed transport value by detecting the numeric offset of the configured time zone (the `abp.clock.timeZone()` value, which falls back to the browser time zone). It is not a full IANA time-zone conversion and does not account for fractional-hour offsets or an offset change between the current date and the input date. Include the Luxon contributor when those cases must be handled.
+
+`normalizeToLocaleString` accepts standard `Intl.DateTimeFormat` options. When no options are supplied, it uses `abp.clock.toLocaleStringOptions`. The default options include the numeric year, long month, numeric day, hour, minute and second. You can replace them to define application-wide display defaults:
+
+````js
+abp.clock.toLocaleStringOptions = {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+};
+````
+
+## Browser Time-Zone Cookie
+
+After application configuration is initialized, ABP writes the browser's time-zone name to the `__timezone` cookie when multiple time zones are supported. Set the following flag before configuration initialization to disable this behavior:
+
+````js
+abp.clock.trySetBrowserTimeZoneToCookie = false;
+````
+
+Use `abp.clock.browserTimeZone()` to read the browser time zone or `abp.clock.setBrowserTimeZoneToCookie()` to refresh the cookie explicitly.
diff --git a/docs/en/framework/ui/mvc-razor-pages/javascript-api/index.md b/docs/en/framework/ui/mvc-razor-pages/javascript-api/index.md
index 193575def6..8bb09645f7 100644
--- a/docs/en/framework/ui/mvc-razor-pages/javascript-api/index.md
+++ b/docs/en/framework/ui/mvc-razor-pages/javascript-api/index.md
@@ -13,6 +13,7 @@ ABP provides a set of JavaScript APIs for ASP.NET Core MVC / Razor Pages applica
* [AJAX](ajax.md)
* [Auth](auth.md)
+* [Clock](clock.md)
* [CurrentUser](current-user.md)
* [DOM](dom.md)
* [Events](events.md)
@@ -24,4 +25,4 @@ ABP provides a set of JavaScript APIs for ASP.NET Core MVC / Razor Pages applica
* [Settings](settings.md)
* [UI Block/Busy](block-busy.md)
* [UI Message](message.md)
-* [UI Notification](notify.md)
\ No newline at end of file
+* [UI Notification](notify.md)
diff --git a/docs/en/framework/ui/mvc-razor-pages/overall.md b/docs/en/framework/ui/mvc-razor-pages/overall.md
index 425c94d2d9..b029fda57c 100644
--- a/docs/en/framework/ui/mvc-razor-pages/overall.md
+++ b/docs/en/framework/ui/mvc-razor-pages/overall.md
@@ -164,4 +164,16 @@ ABP provides a lot of built-in solutions to common application requirements;
## Customization
-There are a lot of ways to customize the theme and the UIs of the pre-built modules. You can override components, pages, static resources, bundles and more. See the [User Interface Customization Guide](customization-user-interface.md).
\ No newline at end of file
+There are a lot of ways to customize the theme and the UIs of the pre-built modules. You can override components, pages, static resources, bundles and more. See the [User Interface Customization Guide](customization-user-interface.md).
+
+### Status-Specific Error Views
+
+The shared MVC theme uses `~/Views/Error/Default.cshtml` by default. Map an HTTP status code to another view with `AbpErrorPageOptions.ErrorViewUrls`:
+
+````csharp
+Configure(options =>
+{
+ options.ErrorViewUrls["404"] = "~/Views/Error/NotFound.cshtml";
+ options.ErrorViewUrls["500"] = "~/Views/Error/InternalError.cshtml";
+});
+````
diff --git a/docs/en/framework/ui/react/authorization.md b/docs/en/framework/ui/react/authorization.md
index 4f6d673d87..9111badaa3 100644
--- a/docs/en/framework/ui/react/authorization.md
+++ b/docs/en/framework/ui/react/authorization.md
@@ -89,6 +89,28 @@ client = createAbpReactOidcAuth({
The template stores the OIDC user in local storage and enables silent renewal with `public/silent-renew.html`.
+## Lower-Level OIDC Client
+
+`@volo/abp-oidc-auth` is the framework-agnostic client used by the React OIDC adapter. Use it directly in another JavaScript runtime or supply it to `createAbpReactOidcAuth`:
+
+```ts
+import { createAbpOidcAuth } from '@volo/abp-oidc-auth'
+import { createAbpReactOidcAuth } from '@volo/abp-react-oidc-auth'
+
+const client = createAbpOidcAuth({
+ authority: 'https://localhost:44301/',
+ clientId: 'MyProject_App',
+ redirectUri: window.location.origin,
+ postLogoutRedirectUri: window.location.origin,
+ scope: 'offline_access MyProject',
+})
+
+const auth = createAbpReactOidcAuth({ client })
+await client.init()
+```
+
+Use `subscribe()` for ABP authentication lifecycle events and `getSnapshot()` for the current user, profile, token and initialization state. `clearStaleState()` removes abandoned OIDC state entries. The client also exposes the underlying `UserManager`, its events and the configured authority for integrations that need lower-level OIDC control.
+
## Auth Provider and Hook
`AuthProvider` wraps the app and handles the OIDC callback:
diff --git a/docs/en/framework/ui/react/permission-management.md b/docs/en/framework/ui/react/permission-management.md
index fc009ce5a2..fabfefcea2 100644
--- a/docs/en/framework/ui/react/permission-management.md
+++ b/docs/en/framework/ui/react/permission-management.md
@@ -29,6 +29,42 @@ export const appConfig = createAbpReactAppConfig({
})
```
+## Framework-Agnostic Application Configuration Client
+
+`@volo/abp-app-config` provides the application-configuration client without a React dependency. Use it directly in another JavaScript runtime or pass an existing client to the React adapter:
+
+```ts
+import { createAbpAppConfig } from '@volo/abp-app-config'
+import { createAbpReactAppConfig } from '@volo/abp-react-app-config'
+
+const client = createAbpAppConfig({
+ baseUrl: 'https://localhost:44300',
+})
+
+const appConfig = createAbpReactAppConfig({ client })
+```
+
+The lower-level client exposes:
+
+- `fetchConfig()` and `fetchLocalization()` to load server data.
+- `refetch()` to reload the application configuration.
+- `getSnapshot()` and `getSections()` to read the current state.
+- `subscribe(listener)` to observe state changes. It returns an unsubscribe function.
+- `t()` and `tWithFallback()` for localization.
+- `clear()` to reset the client state.
+
+```ts
+const unsubscribe = client.subscribe(snapshot => {
+ console.log(snapshot.initialized, snapshot.currentCulture)
+})
+
+await client.fetchConfig()
+await client.fetchLocalization('en')
+
+unsubscribe()
+client.clear()
+```
+
## Fetching Permissions
After the user logs in, `AuthProvider` fetches application configuration with the current access token:
diff --git a/docs/en/images/openiddict-client-credentials-application.png b/docs/en/images/openiddict-client-credentials-application.png
index fdcf4a2e5b..509f2a99a0 100644
Binary files a/docs/en/images/openiddict-client-credentials-application.png and b/docs/en/images/openiddict-client-credentials-application.png differ
diff --git a/docs/en/modules/account-pro.md b/docs/en/modules/account-pro.md
index a79c3d4992..0084a7f10c 100644
--- a/docs/en/modules/account-pro.md
+++ b/docs/en/modules/account-pro.md
@@ -86,7 +86,11 @@ Configure(options =>
* `TenantAdminUserName` (default: admin): The tenant admin user name.
* `ImpersonationTenantPermission`: The permission name for tenant impersonation.
* `ImpersonationUserPermission`: The permission name for user impersonation.
-* `ExternalProviderIconMap`: A dictionary of external provider names and their corresponding font-awesome icon classes. You can add new mapping to this dictionary to change the icon of an external provider.(Popular external provider icons are already defined, such as `Facebook`, `Google`, `Microsoft`, `Twitter`, etc.)
+* `SwitchUserDuringImpersonate` (default: `false`): Signs the target user in with the application cookie while an impersonation flow is in progress.
+* `ExternalProviderIconMap`: A dictionary of external provider names and their icon asset paths or CSS classes. Common providers such as GitHub, Google, X, Apple, LinkedIn, Facebook and Microsoft are already mapped.
+* `IsTenantMultiDomain` (default: `false`): Enables tenant-domain redirects for linked-account and tenant-switching flows.
+* `GetTenantDomain`: Resolves the target tenant's origin for impersonation redirects and, when `IsTenantMultiDomain` is enabled, linked-account and tenant-switching redirects. By default, it returns the current request's scheme and host.
+* `ExternalProfilePictureDownloadTimeout` (default: 5 seconds): Limits how long external-login registration waits while downloading a profile picture.
### AbpProfilePictureOptions
@@ -101,7 +105,26 @@ Configure(options =>
`AbpProfilePictureOptions` properties:
-* `EnableImageCompression` (default: false): Enables the image compression for the profile picture. When enabled, the selected compression library will compress the profile picture to decrease the image size. For more information see [image manipulation](../framework/infrastructure/image-manipulation.md)
+* `EnableImageCompression` (default: `false`): Enables image compression for the profile picture. When enabled, the selected compression library compresses the profile picture to decrease its size. For more information, see [image manipulation](../framework/infrastructure/image-manipulation.md).
+* `AllowedFileExtensions` (default: `.jpg`, `.jpeg` and `.png`): Defines the accepted file-name extensions. The extension check runs when the upload includes a file name.
+* `MaxFileSizeInBytes` (default: 5 MiB): Rejects larger uploads. Set it to `0` to disable the size limit.
+* `MagicBytesVerifiers`: Verifies the file content independently of the file name. The default verifiers accept JPEG and PNG signatures. If you add an allowed extension, add a matching content verifier as well; at least one verifier must accept every uploaded image.
+
+### Registration Email Confirmation Codes
+
+The registration email confirmation code is stored with a 10-minute absolute expiration by default. Configure a different duration with `Account:EmailConfirmation:CodeExpirationTime`:
+
+```json
+{
+ "Account": {
+ "EmailConfirmation": {
+ "CodeExpirationTime": "00:15:00"
+ }
+ }
+}
+```
+
+Sending and checking these codes use separate built-in operation rate-limit policies. Sending a new code resets the check rate-limit state for that email address.
## Local login
@@ -111,6 +134,20 @@ If you use `Social / External Logins`, It is automatically called for authentica

+## Email Login
+
+Email login lets users sign in with a one-time code, a magic link or both. It is disabled by default and also requires **Local login** to remain enabled. Configure it in `Settings > Account > Email Login`.
+
+The available login types are:
+
+* `OtpAndMagicLink` (default): The email contains both a six-digit code and a magic link.
+* `MagicLinkOnly`: The email contains only a magic link and the code-verification endpoint is disabled.
+* `OtpOnly`: The email contains only a code and direct magic-link verification is disabled.
+
+The token lifespan defaults to 90 seconds and accepts values from 30 to 86,400 seconds. Codes and link tokens are single-use. Completing either path invalidates the outstanding credential for the other path, and sending a new email invalidates the previous credentials.
+
+Email login uses built-in send and verification rate limits. When `AccountSettingNames.PreventEmailEnumeration` is enabled, requests for an unknown or locked-out account return the same expiry-shaped response as a valid request without sending an email. This prevents callers from using the send response to distinguish those accounts.
+
### Switching users during OAuth login
If you have an OAuth/Auth Server application using the Account Pro module, you can pass the `prompt=select_account` parameter to force the user to select an account.
@@ -345,9 +382,11 @@ export const APP_ROUTES: Routes = [
You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method:
- **redirectUrl**: Default redirect URL after logging in.
-- **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details.
-- **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details.
-- **entityPropContributors:** Changes table columns. Please check [Data Table Column Extensions for Angular](../framework/ui/angular/data-table-column-extensions.md) for details.
+- **entityActionContributors:** Changes actions on `eAccountComponents.MySecurityLogs`. See [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md).
+- **toolbarActionContributors:** Changes the toolbar on `eAccountComponents.MySecurityLogs`. See [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md).
+- **entityPropContributors:** Changes columns on `eAccountComponents.MySecurityLogs`. See [Data Table Column Extensions for Angular](../framework/ui/angular/data-table-column-extensions.md).
+- **personelInfoEntityPropContributors:** Changes the edit-form properties on `eAccountComponents.PersonalSettings`. The public API uses this spelling. See [Dynamic Form Extensions for Angular](../framework/ui/angular/dynamic-form-extensions.md).
+- **isPersonalSettingsChangedConfirmationActive:** Deprecated. Personal settings refresh the current user's state without requiring a new login.
#### Services / Models
@@ -426,4 +465,3 @@ This module doesn't define any additional distributed event. See the [standard d
* [Idle Session Timeout](./account/idle-session-timeout.md)
* [Web Authentication API (WebAuthn) passkeys](./account/passkey.md)
* [Shared user accounts](./account/shared-user-accounts.md)
-```
\ No newline at end of file
diff --git a/docs/en/modules/account.md b/docs/en/modules/account.md
index 844e00e06f..2148ca5d42 100644
--- a/docs/en/modules/account.md
+++ b/docs/en/modules/account.md
@@ -37,6 +37,8 @@ Social/external login buttons becomes visible if you setup it. See the *Social/E

+New users receive every Identity role marked as `Default`.
+
### Forgot Password & Reset Password
`/Account/ForgotPassword` page provides a way of sending password reset link to user's email address. The user then clicks to the link and determines a new password.
@@ -51,6 +53,34 @@ Social/external login buttons becomes visible if you setup it. See the *Social/E

+`IdentitySettingNames.User.IsUserNameUpdateEnabled` and `IdentitySettingNames.User.IsEmailUpdateEnabled` control whether the profile application service accepts changes to those fields. Both settings are `true` by default. External users can't change a local password; the built-in MVC profile page omits the password group for them and the application service rejects a password change.
+
+### Login and Registration Settings
+
+The Account module defines two client-visible settings. Both are `true` by default:
+
+* `AccountSettingNames.IsSelfRegistrationEnabled` controls self-registration. It is enforced by `IAccountAppService.RegisterAsync` as well as the built-in registration pages.
+* `AccountSettingNames.EnableLocalLogin` controls the local username/password login UI and handlers in the MVC Account pages, the OpenIddict and IdentityServer integrations, and the Angular Account layout. In Angular, `AuthWrapperService` reads the setting; the Basic Theme's `AuthWrapperComponent` shows the account content when it is enabled and a no-login-schemes warning when it is disabled.
+
+These settings are independent. Disabling local login doesn't disable the registration application service. Set `IsSelfRegistrationEnabled` to `false` as well when users must not create local accounts. Change the values with `ISettingManager` like other [settings](../framework/infrastructure/settings.md). Global or tenant values are normally appropriate because the login and registration requests run before a user is authenticated.
+
+### Extending the MVC Profile Page
+
+The MVC `/Account/Manage` page is built from the contributors in `ProfileManagementPageOptions.Contributors`. Implement `IProfileManagementPageContributor` to add a group backed by a view component, then register it from your module:
+
+```csharp
+Configure(options =>
+{
+ options.Contributors.Add(new MyProfileManagementPageContributor());
+});
+```
+
+Each contributor receives a `ProfileManagementPageCreationContext` and appends `ProfileManagementPageGroup` instances to its `Groups` collection. Contributors run in registration order on both GET and POST requests, and can resolve services through `context.ServiceProvider` when visibility depends on the current user or another runtime condition.
+
+### Angular UI Extensibility
+
+The Angular `createRoutes` function accepts three module-specific options: `redirectUrl`, `isPersonalSettingsChangedConfirmationActive` and `editFormPropContributors`. The form contributor key is `eAccountComponents.PersonalSettings`. The login, register, forgot-password, reset-password and manage-profile routes are also registered with the corresponding `eAccountComponents` keys for [component replacement](../framework/ui/angular/component-replacement.md). See [Dynamic Form Extensions](../framework/ui/angular/dynamic-form-extensions.md) for the contributor pattern.
+
## OpenIddict Integration
[Volo.Abp.Account.Web.OpenIddict](https://www.nuget.org/packages/Volo.Abp.Account.Web.OpenIddict) package provides integration for the [OpenIddict](https://github.com/openiddict). This package comes as installed with the [application startup template](../solution-templates/layered-web-application). See the [OpenIddict Module](./openiddict.md) documentation.
@@ -63,6 +93,15 @@ Social/external login buttons becomes visible if you setup it. See the *Social/E
The Account Module has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application.
+The MVC login and registration pages also recognize a Windows authentication scheme. `AbpAccountOptions.WindowsAuthenticationSchemeName` identifies that scheme and defaults to `"Windows"`. Set it when the registered scheme uses another name:
+
+```csharp
+Configure(options =>
+{
+ options.WindowsAuthenticationSchemeName = "Negotiate";
+});
+```
+
### Example: Facebook Authentication
Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application.
diff --git a/docs/en/modules/account/passkey.md b/docs/en/modules/account/passkey.md
index b8f00ee576..a868977ece 100644
--- a/docs/en/modules/account/passkey.md
+++ b/docs/en/modules/account/passkey.md
@@ -8,6 +8,8 @@ You can enable/disable the `Web Authentication API (WebAuthn) passkeys` feature

+Passkeys are disabled by default. The default limit is 10 passkeys per user, and the configured limit must be greater than zero. Registration is rejected when passkeys are disabled or the user has reached the limit.
+
## Manage Passkeys
You can add/rename/delete your passkeys in the `Account/Manage` page:
diff --git a/docs/en/modules/account/shared-user-accounts.md b/docs/en/modules/account/shared-user-accounts.md
index 908b342de2..5205ef10f8 100644
--- a/docs/en/modules/account/shared-user-accounts.md
+++ b/docs/en/modules/account/shared-user-accounts.md
@@ -74,6 +74,17 @@ From the invitation modal, you can view and manage sent invitations, including r

+Invitation links contain a protected, URL-safe token and expire after 7 days by default. Invalid, modified or expired tokens are rejected. Configure the lifespan with `UserInvitationTokenProviderOptions`:
+
+```csharp
+Configure(options =>
+{
+ options.TokenLifespan = TimeSpan.FromDays(3);
+});
+```
+
+Inviting the same email address again while an invitation is still pending reuses that invitation, replaces its assigned roles and refreshes its invitation date. Resending is allowed only for a pending invitation; it refreshes the invitation date and sends a newly generated token.
+
## Accepting an Invitation
If the invited person already has an account, clicking the email link shows a confirmation screen to join the tenant:
@@ -150,13 +161,13 @@ When the Shared strategy is enabled, a user is a **global resource** across host
### Host-only operations
-The following operations can only be performed by a host administrator when Shared is enabled. Both the Identity Pro UI (MVC + Blazor) and the `IdentityUserAppService` enforce this — a direct API call from a tenant context will be rejected with a `UserFriendlyException`:
+The following operations can only be performed by a host administrator when Shared is enabled. Both the Identity Pro UI (MVC + Blazor) and the `IdentityUserAppService` enforce these restrictions.
- Delete a user
-- Activate / deactivate a user (`IsActive`)
- Lock / unlock a user
- Enable or disable two-factor authentication
-- Change `LockoutEnabled` or `ShouldChangePasswordOnNextLogin`
+
+Direct tenant API calls for these operations are rejected with a `UserFriendlyException`. When a tenant update request changes `IsActive`, `LockoutEnabled` or `ShouldChangePasswordOnNextLogin`, the application service restores the current host-managed values and continues processing the remaining editable fields.
> `Delete` here means deleting the **global user account**, not removing a user from a single tenant. Removing a member from one tenant is a tenant-level soft operation and is available to tenant administrators — see **Remove from tenant** below.
@@ -176,6 +187,4 @@ Users can leave a tenant from their own account menu (`Switch Tenant` → `Leave
If you plan to migrate an existing multi-tenant application from an isolated strategy to Shared User Accounts, keep the following in mind:
1. **Uniqueness check**: Before enabling Shared, ensure all existing usernames and emails are unique globally. ABP performs this check when you switch the strategy and reports conflicts.
-2. **Tenants with separate databases**: If some tenants use separate databases, you must ensure the Host database contains matching user records in the `AbpUsers` table (and, if you use social login / passkeys, also sync `AbpUserLogins` and `AbpUserPasskeys`) so the Host-side records match the tenant-side data. After that, the framework can create/manage the user-to-tenant associations.
- - **Important — each host-side shadow row must have a new primary key (`Id`) different from the tenant user's `Id`.** Generate a fresh `Guid` for every shadow row instead of reusing the tenant user's primary key. The framework relies on this to distinguish a separate-database tenant from a shared-database one; reusing the Id can mask "Leave Tenant" and external login / passkey synchronization on legacy data. The other identifying fields (`UserName`, `Email`, `PasswordHash`, `TenantId`, etc.) should still match the tenant-side row.
-
+2. **Tenants with separate databases**: The module detects a separate Identity database by comparing the resolved Identity connection string for the tenant with the host connection string. If some tenants use separate databases, ensure that the host database contains the corresponding shadow users in the `AbpUsers` table. Host-side shadow users are located by the tenant identifier and email address. If you use social login or passkeys, also synchronize `AbpUserLogins` and `AbpUserPasskeys`. Existing shadow rows that reuse the tenant user's primary key remain compatible with leaving a tenant.
diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md
index 1cef520472..9ce3caf865 100644
--- a/docs/en/modules/ai-management/index.md
+++ b/docs/en/modules/ai-management/index.md
@@ -1,7 +1,7 @@
```json
//[doc-seo]
{
- "Description": "Discover how to implement AI management in your ABP Framework application, enhancing workspace dynamics with easy installation options."
+ "Description": "Manage persisted AI workspaces, providers, RAG data sources, MCP tools and remote AI clients with the ABP AI Management module."
}
```
@@ -13,7 +13,7 @@ This module implements AI (Artificial Intelligence) management capabilities on t
## How to Install
-The **AI Management Module** is not included in [the startup templates](../solution-templates/layered-web-application) by default. However, when creating a new application with [ABP Studio](../../tools/abp-studio/index.md), you can easily enable it during setup via the *AI Integration* step in the project creation wizard. Alternatively, you can install it using the ABP CLI or ABP Studio:
+The **AI Management Module** is not included in [the startup templates](../../solution-templates/layered-web-application/index.md) by default. However, when creating a new application with [ABP Studio](../../studio/overview.md), you can easily enable it during setup via the *AI Integration* step in the project creation wizard. Alternatively, you can install it using the ABP CLI or ABP Studio:
**Using ABP CLI:**
@@ -97,11 +97,13 @@ AI Management module packages are designed for various usage scenarios. Packages
## User Interface
-This module provides UI integration for all three officially supported UI frameworks by ABP:
+This module provides administration and client UI for the following UI stacks:
* **MVC / Razor Pages** UI
-* **Angular** UI
-* **Blazor** UI (Server & WebAssembly)
+* **Angular** UI
+* **Blazorise** UI (Server & WebAssembly)
+* **MudBlazor** UI (Server & WebAssembly)
+* **React Admin Console** management UI
### Menu Items
@@ -129,12 +131,12 @@ You can create a new workspace or edit an existing workspace in this page. The w
* **API Key**: Authentication key (if required by provider)
* **API Base URL**: Custom endpoint URL (optional)
* **System Prompt**: Default system instructions
-* **Temperature**: Response randomness (0.0-1.0)
-* **Application Name**: Associate with specific application
-* **Required Permission**: Permission needed to use this workspace
+* **Temperature**: Provider-specific response randomness
* **Embedder Provider / Model**: Embedding generator used for RAG
* **Vector Store Provider / Settings**: Storage backend and connection settings for document vectors
+The **Application Name** is assigned by the application that creates or synchronizes the workspace. Workspace access is managed from the resource permission action; it is not a field in the create/edit form.
+
#### Chat Interface
The AI Management module includes a built-in chat interface for testing workspaces. You can:
@@ -177,6 +179,21 @@ When a workspace has MCP servers associated, the AI model can invoke tools from

+##### MCP Timeouts
+
+Use `McpClientFactoryOptions` when an MCP server needs more time to initialize or when a stdio connection test starts a slow process:
+
+```csharp
+Configure(options =>
+{
+ options.DefaultTimeoutMs = 180_000;
+ options.StdioInitializationTimeoutMs = 240_000;
+ options.StdioConnectionTestTimeoutSeconds = 300;
+});
+```
+
+`DefaultTimeoutMs` defaults to 120 seconds and applies to HTTP transports and, unless overridden, stdio initialization. `StdioConnectionTestTimeoutSeconds` defaults to 180 seconds and is constrained to 30-600 seconds by the connection-test service. HTTP connection tests use a fixed one-minute timeout.
+
#### Workspace Data Sources
Workspace Data Sources page is used to upload and manage RAG documents per workspace. Uploaded files are processed and indexed in the background.
@@ -207,12 +224,12 @@ When creating or managing a workspace, you can configure the following propertie
| `ApiKey` | No | API authentication key (required by some providers) |
| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) |
| `SystemPrompt` | No | Default system prompt for all conversations |
-| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) |
+| `Temperature` | No | Provider-specific response randomness |
| `Description` | No | Workspace description |
| `IsActive` | No | Enable/disable the workspace (default: true) |
-| `ApplicationName` | No | Associate workspace with specific application |
-| `RequiredPermissionName` | No | Permission required to use this workspace |
-| `IsSystem` | No | Whether it's a system workspace (read-only) |
+| `ApplicationName` | No | Application identity recorded during creation or synchronization |
+| `RequiredPermissionName` | No | Optional policy checked by client consumption services |
+| `IsSystem` | No | Whether the workspace was synchronized from code |
| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings |
| `EmbedderProvider` | No | Embedding provider name (e.g., "OpenAI", "Ollama") |
| `EmbedderModelName` | No | Embedding model identifier (e.g., "text-embedding-3-small") |
@@ -231,27 +248,38 @@ The AI Management module supports two types of workspaces:
* **Defined in code** using `PreConfigure`
* **Cannot be deleted** through the UI
-* **Read-only by default**, but can be overridden when `OverrideSystemConfiguration` is enabled
+* **Use the code-defined chat client by default**; persisted provider settings take effect only when `OverrideSystemConfiguration` is enabled
* **Useful for** application-critical AI features that must always be available
-* **Created automatically** when the application starts
+* **Synchronized automatically** when the application starts
Example:
```csharp
-PreConfigure(options =>
+public override void PreConfigureServices(ServiceConfigurationContext context)
{
- options.Workspaces.Configure(configuration =>
+ PreConfigure(options =>
{
- configuration.ConfigureChatClient(chatClientConfiguration =>
- {
- chatClientConfiguration.Builder = new ChatClientBuilder(
- sp => new OpenAIClient(apiKey).GetChatClient("gpt-4")
- );
- });
+ options.Workspaces.Configure();
});
+}
+```
+
+Configure the code-defined keyed client with the [Framework AI provider integration](../../framework/infrastructure/artificial-intelligence/index.md) used by your application.
+
+At startup, AI Management records the registered chat, embedding and vector-store providers and synchronizes the code-defined workspace names for the current application. When at least one code-defined workspace remains, names removed from that application's configuration are also removed from its synchronized set. An empty workspace collection skips the update, so removing the last code-defined workspace doesn't delete its persisted record automatically. Synchronization errors are logged without stopping application startup.
+
+You can disable startup synchronization or override the recorded application name:
+
+```csharp
+Configure(options =>
+{
+ options.AutoUpdateAtStartup = false;
+ options.ApplicationName = "MyAIGateway";
});
```
+Disabling synchronization means code-defined workspaces are not added to or updated in the management database automatically. Their code-defined keyed clients can still be resolved by the Framework AI infrastructure.
+
#### Dynamic Workspaces
* **Created through the UI** or programmatically via `ApplicationWorkspaceManager` and `IWorkspaceRepository`
@@ -264,12 +292,16 @@ Example (data seeding):
```csharp
public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDependency
{
+ private readonly IConfiguration _configuration;
private readonly IWorkspaceRepository _workspaceRepository;
private readonly ApplicationWorkspaceManager _applicationWorkspaceManager;
+
public WorkspaceDataSeederContributor(
+ IConfiguration configuration,
IWorkspaceRepository workspaceRepository,
ApplicationWorkspaceManager applicationWorkspaceManager)
{
+ _configuration = configuration;
_workspaceRepository = workspaceRepository;
_applicationWorkspaceManager = applicationWorkspaceManager;
}
@@ -281,18 +313,32 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe
provider: "OpenAI",
modelName: "gpt-4");
- workspace.ApiKey = "your-api-key";
+ workspace.ApiKey = _configuration["AI:OpenAI:ApiKey"];
workspace.SystemPrompt = "You are a helpful customer support assistant.";
await _workspaceRepository.InsertAsync(workspace);
}
+}
```
+> [!WARNING]
+> Provider API keys, embedding keys, vector-store connection settings, MCP headers and MCP credentials are sensitive persisted configuration. Do not hard-code them in source control. Restrict workspace and MCP administration permissions, protect the application database and configuration backups, and supply secrets from a secure configuration provider. Duplicating a workspace also duplicates its provider and RAG configuration, including credentials.
+
+### Resolution Precedence
+
+When an application requests a workspace chat client, AI Management resolves it in this order:
+
+1. If no persisted configuration exists, the workspace is inactive, or it is a system workspace with `OverrideSystemConfiguration = false`, resolution first tries the code-defined keyed `IChatClient` and then the default `IChatClient`.
+2. An active dynamic workspace, or an overridden system workspace, uses the factory registered for its persisted `Provider` value.
+
+If neither fallback exists, an inactive workspace produces an inactive-workspace error; the other fallback cases produce a provider-not-found error. A configured provider without a registered factory also produces a provider-not-found error.
+
### Workspace Naming Rules
* Workspace names **must be unique**
* Workspace names **cannot contain spaces** (use underscores or camelCase)
-* Workspace names are **case-sensitive**
+
+Workspace names are global identifiers in an AI Management database. Workspaces, MCP server configurations, data sources and the data-source blob container are not tenant-scoped entities. In a multi-tenant application, use workspace resource permissions and application-level design to control access; do not treat these records or blobs as tenant-isolated storage.
## RAG with File Upload
@@ -367,7 +413,12 @@ RAG is enabled per workspace when both embedding and vector store settings are c
* `MongoDb`: Standard MongoDB connection string including database name.
* `Pgvector`: Standard PostgreSQL/Npgsql connection string.
-* `Qdrant`: Qdrant endpoint string (`http://host:port`, `https://host:port`, or `host:port`).
+* `Qdrant`: Qdrant endpoint string (`http://host:port`, `https://host:port`, or `host:port`). The current provider discards the URL scheme and creates a non-TLS client from only the host and port. Do not rely on an `https://` value to enable TLS.
+
+> [!IMPORTANT]
+> The `MongoDb` vector-store provider requires MongoDB Atlas or an Atlas CLI local deployment because it uses `$vectorSearch`. Standard MongoDB Docker images do not provide this feature. Create an Atlas Vector Search index named `vector_index` on the `AIVectorEmbeddings` collection, with `Embedding` as the vector field, cosine similarity and dimensions matching the configured embedding model. The provider creates the collection and its regular workspace index, but it does not create the Atlas vector index.
+
+The Pgvector provider creates the target database when its credentials permit it, enables the `vector` extension and creates workspace tables lazily. Qdrant creates a workspace-specific collection when the first vector is stored. The service account therefore needs the corresponding database, extension, table or collection privileges.
#### Document Processing Pipeline
@@ -376,9 +427,11 @@ When a file is uploaded as a workspace data source:
1. File is stored in blob storage.
2. `IndexDocumentJob` is queued.
3. `DocumentProcessingManager` extracts text using content-type-specific extractors.
-4. Text is chunked (default chunk size: `1000`, overlap: `200`).
-5. Embeddings are generated in batches and stored through the configured vector store.
-6. Data source is marked as processed (`IsProcessed = true`).
+4. Text is chunked with `1000` characters as a target size. The normal paragraph path carries up to `200` characters into the next chunk; oversized paragraphs are split by line with a `50`-character carry. An indivisible line can produce a chunk larger than the target.
+5. Embeddings are generated in ordered batches and stored through the configured vector store.
+6. The data source is marked as processed (`IsProcessed = true`) after the final batch succeeds.
+
+Each indexing run has an identifier and a per-data-source distributed lock. Stale and duplicate batches are ignored, while an out-of-order batch is retried by the background job system. A failed run can therefore leave partial vector data until a retry succeeds or the data source is re-indexed again.
#### Workspace Data Source HTTP API
@@ -395,20 +448,45 @@ The module exposes workspace data source endpoints under `/api/ai-management/wor
#### Chat Integration Behavior
-When a workspace has embedder configuration, AI Management wraps the chat client with a document search tool function named `search_workspace_documents`.
+When an active dynamic workspace, or a system workspace with `OverrideSystemConfiguration = true`, is resolved through its persisted provider factory and has embedder configuration, AI Management wraps the factory-created client with a document search tool function named `search_documents`. The keyed/default fallback branches described in [Resolution Precedence](#resolution-precedence) return their code-defined client directly and don't add persisted RAG or MCP tools.
* The tool delegates to `IDocumentSearchService` (`DocumentSearchService` by default).
* The search currently uses `TopK = 5` chunks.
* If RAG retrieval fails, chat continues without injected context.
+The underlying chat client must be a `FunctionInvokingChatClient`. The built-in OpenAI and Ollama factories add function invocation automatically. A custom chat factory must build its client with `ChatClientBuilder.UseFunctionInvocation()` before RAG or MCP tools can execute.
+
#### Automatic Reindexing on Configuration Changes
When workspace embedder or vector store configuration changes, AI Management automatically:
* Initializes the new vector store configuration (if needed).
-* Deletes existing embeddings when embedder provider/model changes.
+* Attempts to delete existing embeddings when the embedder provider or model changes.
* Re-queues all workspace data sources for re-indexing.
+Embedding cleanup and automatic re-index queueing are best-effort follow-up operations. Their failures are logged without rolling back the workspace update. Use the data-source page's **Re-index All** action after correcting the configuration if automatic re-indexing could not be queued.
+
+### Configuring Indexing Options
+
+`AIManagementIndexingOptions` controls the work performed by background indexing jobs:
+
+```csharp
+Configure(options =>
+{
+ options.EmbeddingBatchSize = 100;
+ options.MaxConcurrentIndexingJobs = 2;
+ options.DistributedLockTimeoutSeconds = 15;
+});
+```
+
+| Property | Default | Description |
+| -------- | ------- | ----------- |
+| `EmbeddingBatchSize` | `50` | Maximum chunks embedded and stored by one batch job |
+| `MaxConcurrentIndexingJobs` | `1` | Maximum indexing batches running concurrently across the application |
+| `DistributedLockTimeoutSeconds` | `0` | Time to wait for an indexing lock; `0` performs an immediate attempt |
+
+Increase concurrency only after checking the embedding provider's rate limits and vector-store capacity. The concurrency limiter and the per-data-source lock both use the [distributed lock](../../framework/infrastructure/distributed-locking.md), so they apply across all application instances when a distributed lock provider is configured (with the default in-process implementation, they only cover a single process). The per-data-source lock prevents two batches from mutating the same data source concurrently.
+
### Configuring Data Source Upload Options
The `WorkspaceDataSourceOptions` class allows you to customize the file upload constraints for workspace data sources. You can configure the allowed file extensions, maximum file size, and content type mappings.
@@ -418,15 +496,13 @@ public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure(options =>
{
- // Add support for additional file types
- options.AllowedFileExtensions = new[] { ".txt", ".md", ".pdf", ".docx", ".csv" };
+ // Allow log files to use the built-in plain-text extractor.
+ options.AllowedFileExtensions = [".txt", ".md", ".pdf", ".log"];
// Increase the maximum file size to 50 MB
options.MaxFileSize = 50 * 1024 * 1024;
- // Add content type mappings for new extensions
- options.ContentTypeMap[".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
- options.ContentTypeMap[".csv"] = "text/csv";
+ options.ContentTypeMap[".log"] = "text/plain";
});
}
```
@@ -439,6 +515,8 @@ public override void ConfigureServices(ServiceConfigurationContext context)
| `MaxFileSize` | `long` | `10485760` (10 MB) | Maximum file size in bytes |
| `ContentTypeMap` | `Dictionary` | `.txt`, `.md`, `.pdf` with their MIME types | Maps file extensions to MIME content types |
+Adding an extension and MIME mapping only allows the upload. The mapped content type must also be supported by an `IDocumentTextExtractor`. Implement and register an extractor when adding a format that cannot use the built-in plain-text, Markdown or PDF extractors.
+
The options class also provides helper methods:
| Method | Description |
@@ -447,9 +525,6 @@ The options class also provides helper methods:
| `GetAllowedExtensionsDisplay()` | Returns a comma-separated display string (e.g., ".txt, .md, .pdf") |
| `GetAcceptAttribute()` | Returns a string for the HTML `accept` attribute (e.g., ".txt,.md,.pdf") |
-> [!NOTE]
-> Adding new file extensions also requires a matching content extractor to be registered for document processing. The built-in extractors support `.txt`, `.md`, and `.pdf` files.
-
#### Hosting-Level Upload Limits
`WorkspaceDataSourceOptions.MaxFileSize` controls the module-level validation, but your hosting stack may reject large uploads before the request reaches AI Management. If you increase `MaxFileSize`, make sure the underlying server and proxy limits are also updated.
@@ -523,6 +598,7 @@ The AI Management module defines the following permissions:
| `AIManagement.Workspaces.Delete` | Delete workspaces |
| `AIManagement.Workspaces.Playground` | Access workspace chat playground |
| `AIManagement.Workspaces.ManagePermissions` | Manage workspace resource permissions |
+| `Volo.AIManagement.Workspaces.Workspace.Consume` | Consume a specific workspace; granted as a resource permission |
| `AIManagement.McpServers` | View MCP servers |
| `AIManagement.McpServers.Create` | Create new MCP servers |
| `AIManagement.McpServers.Update` | Edit existing MCP servers|
@@ -539,9 +615,17 @@ The module also defines workspace data source permissions for RAG document opera
| `AIManagement.WorkspaceDataSources.Download` | Download original uploaded file |
| `AIManagement.WorkspaceDataSources.ReIndex` | Re-index one or all workspace files |
-### Workspace-Level Permissions
+### Workspace Consumption Authorization
+
+The `IChatCompletionClientAppService` client API, OpenAI-compatible API and MCP tool catalog authorize a workspace when the current user satisfies any one of these conditions:
+
+1. The user has `AIManagement.Workspaces.Playground`.
+2. The workspace has a `RequiredPermissionName` and the user has that policy.
+3. The user has the `Volo.AIManagement.Workspaces.Workspace.Consume` resource permission for that workspace ID.
-In addition to module-level permissions, you can restrict access to individual workspaces by setting the `RequiredPermissionName` property:
+Use the **Manage Permissions** action on the workspace page to grant the per-workspace resource permission. `AIManagement.Workspaces.ManagePermissions` controls access to that action.
+
+`RequiredPermissionName` is a programmatic alternative for workspaces tied to an application permission:
```csharp
var workspace = await _applicationWorkspaceManager.CreateAsync(
@@ -549,14 +633,11 @@ var workspace = await _applicationWorkspaceManager.CreateAsync(
provider: "OpenAI",
modelName: "gpt-4"
);
-// Set a specific permission for the workspace
workspace.RequiredPermissionName = MyAppPermissions.AccessPremiumWorkspaces;
+await _workspaceRepository.InsertAsync(workspace);
```
-When a workspace has a required permission:
-
-* Only authorized users with that permission can access the workspace endpoints
-* Users without the permission will receive an authorization error
+Administrative workspace and data-source endpoints still use the module permissions in the tables above. A workspace consumption grant does not grant permission to edit the workspace, manage its credentials or upload RAG documents.
## Usage Scenarios
@@ -569,57 +650,7 @@ The AI Management module is designed to support various usage patterns, from sim
**Use this when:** You want to use AI in your application without any dependency on the AI Management module.
-In this scenario, you only use the ABP Framework's AI features directly. You configure AI providers (like OpenAI) in your code and don't need any database or management UI.
-
-**Required Packages:**
-
-- `Volo.Abp.AI`
-- Any Microsoft AI extensions (e.g., `Microsoft.Extensions.AI.OpenAI`)
-
-**Configuration:**
-
-```csharp
-public class YourModule : AbpModule
-{
- public override void ConfigureServices(ServiceConfigurationContext context)
- {
- PreConfigure(options =>
- {
- options.Workspaces.ConfigureDefault(configuration =>
- {
- configuration.ConfigureChatClient(chatClientConfiguration =>
- {
- chatClientConfiguration.Builder = new ChatClientBuilder(
- sp => new OpenAIClient(apiKey).GetChatClient("gpt-4")
- );
- });
- });
- });
- }
-}
-```
-
-**Usage:**
-
-```csharp
-public class MyService
-{
- private readonly IChatClient _chatClient;
-
- public MyService(IChatClient chatClient)
- {
- _chatClient = chatClient;
- }
-
- public async Task GetResponseAsync(string prompt)
- {
- var response = await _chatClient.CompleteAsync(prompt);
- return response.Message.Text;
- }
-}
-```
-
-> See [Artificial Intelligence](../../framework/infrastructure/artificial-intelligence/index.md) documentation for more details about workspace configuration.
+In this scenario, use the ABP Framework AI infrastructure and configure provider clients in code. No AI Management database, administration UI or commercial client packages are involved. See [Artificial Intelligence](../../framework/infrastructure/artificial-intelligence/index.md) for the current static workspace registration and consumption APIs.
### Scenario 2: AI Management with Domain Layer Dependency (Local Execution)
@@ -646,49 +677,12 @@ In this scenario, you install the AI Management module with its database layer,
> Note: `Volo.AIManagement.EntityFrameworkCore` transitively includes `Volo.AIManagement.Domain` and `Volo.Abp.AI.AIManagement` packages.
-**Workspace Definition Options:**
-
-**Option 1 - System Workspace (Code-based):**
-
-```csharp
-public class YourModule : AbpModule
-{
- public override void ConfigureServices(ServiceConfigurationContext context)
- {
- PreConfigure(options =>
- {
- options.Workspaces.Configure(configuration =>
- {
- configuration.ConfigureChatClient(chatClientConfiguration =>
- {
- // Configuration will be populated from database
- });
- });
- });
- }
-}
-```
-
-**Option 2 - Dynamic Workspace (UI-based):**
-
-No code configuration needed. Define workspaces through:
+Define dynamic workspaces through:
- The AI Management UI (navigate to AI Management > Workspaces)
- Data seeding in your `DataSeeder` class
-**Using Chat Client:**
-
-```csharp
-public class MyService
-{
- private readonly IChatClient _chatClient;
-
- public MyService(IChatClient chatClient)
- {
- _chatClient = chatClient;
- }
-}
-```
+For a system workspace, define the typed workspace and its code client as described in [System Workspaces](#system-workspaces). Enable `OverrideSystemConfiguration` on that workspace only when persisted provider settings should replace the code client.
### Scenario 3: AI Management Client with Remote Execution
@@ -714,25 +708,6 @@ Add the remote service endpoint in your `appsettings.json`:
}
```
-Optionally define workspace in your module:
-
-```csharp
-public class YourModule : AbpModule
-{
- public override void ConfigureServices(ServiceConfigurationContext context)
- {
- PreConfigure(options =>
- {
- // Optional: Pre-define workspace type for type safety
- options.Workspaces.Configure(configuration =>
- {
- // Configuration will be fetched from remote service
- });
- });
- }
-}
-```
-
**Usage:**
```csharp
@@ -749,14 +724,14 @@ public class MyService
{
var request = new ChatClientCompletionRequestDto
{
- Messages = new List
- {
- new ChatMessageDto { Role = "user", Content = prompt }
- }
+ Messages =
+ [
+ new ChatMessageDto { Role = ChatRole.User, Content = prompt }
+ ]
};
var response = await _chatService.ChatCompletionsAsync(workspaceName, request);
- return response.Content;
+ return response.Text;
}
// For streaming responses
@@ -764,15 +739,15 @@ public class MyService
{
var request = new ChatClientCompletionRequestDto
{
- Messages = new List
- {
- new ChatMessageDto { Role = "user", Content = prompt }
- }
+ Messages =
+ [
+ new ChatMessageDto { Role = ChatRole.User, Content = prompt }
+ ]
};
await foreach (var update in _chatService.StreamChatCompletionsAsync(workspaceName, request))
{
- yield return update.Content;
+ yield return update.Text;
}
}
}
@@ -799,8 +774,8 @@ Same as Scenario 3, configure the remote AI Management service in `appsettings.j
Once configured, other applications can call your application's endpoints:
-- `POST /api/ai-management-client/chat-completion` for chat completions
-- `POST /api/ai-management-client/stream-chat-completion` for streaming responses
+- `POST /api/chat-completion/{workspaceName}` for chat completions
+- `POST /api/chat-completion/{workspaceName}/stream` for streaming responses
Your application acts as a proxy, forwarding these requests to the AI Management microservice.
@@ -823,7 +798,7 @@ Each AI Management **workspace** appears as a selectable model in the client app

-#### Available Endpoints
+### Available Endpoints
| Endpoint | Method | Description |
| ---------------------------- | ------ | ----------------------------------------------- |
@@ -832,15 +807,14 @@ Each AI Management **workspace** appears as a selectable model in the client app
| `/v1/models` | GET | List available models (workspaces) |
| `/v1/models/{modelId}` | GET | Retrieve a single model (workspace) |
| `/v1/embeddings` | POST | Generate embeddings |
-| `/v1/files` | GET | List uploaded files |
-| `/v1/files` | POST | Upload a file |
-| `/v1/files/{fileId}` | GET | Get file info |
-| `/v1/files/{fileId}` | DELETE | Delete a file |
-| `/v1/files/{fileId}/content` | GET | Download file content |
-All endpoints require authentication via a **Bearer token** in the `Authorization` header.
+All endpoints require an authenticated user. Non-browser API clients normally send an access token as a **Bearer token** in the `Authorization` header.
+
+The value of `model` is an AI Management workspace name, not the provider's model name. `GET /v1/models` returns only workspaces the current user is authorized to consume. The same workspace authorization is applied before chat, completion, model and embedding operations.
-#### Usage
+The module also exposes file-management routes backed by workspace data sources. They require workspace-specific parameters and are not a drop-in implementation of the OpenAI Files API. Use the [Workspace Data Source HTTP API](#workspace-data-source-http-api) for portable upload, list, download, delete and re-index operations.
+
+### Usage
The general pattern for connecting any OpenAI-compatible client:
@@ -877,13 +851,13 @@ curl -X POST https://localhost:44336/v1/chat/completions \
}'
```
-> The OpenAI-compatible endpoints are available from both the `Volo.AIManagement.Client.HttpApi` and `Volo.AIManagement.HttpApi` packages, depending on your deployment scenario.
+> The OpenAI-compatible `/v1` endpoints are provided by `Volo.AIManagement.Client.HttpApi`. `Volo.AIManagement.HttpApi` instead exposes the management and integration APIs.
## Client Usage
AI Management uses different packages depending on the usage scenario:
-- **`Volo.AIManagement.*` packages**: These contain the core AI functionality and are used when your application hosts and manages its own AI operations. These packages don't expose any application service and endpoints to be consumed by default.
+- **`Volo.AIManagement.*` packages**: These contain the core AI functionality for applications that host and manage AI operations. The application and HTTP API packages expose management and integration services.
- **`Volo.AIManagement.Client.*` packages**: These are designed for applications that need to consume AI services from a remote application. They provide both server and client side of remote access to the AI services.
@@ -923,6 +897,9 @@ You can customize the chat widget with the following properties:
- `Title`: The title of the chat widget.
- `ShowStreamCheckbox`: Whether to show the stream checkbox. Allows user to toggle streaming on and off. Default is `false`.
- `UseStreaming`: Default streaming behavior. Can be overridden by user when `ShowStreamCheckbox` is true.
+- `DisableWhenNoConversation`: Disables input until a `ConversationId` is selected. Default is `false`.
+- `ShowUsageDetails`: Shows token usage and tool calls for assistant messages. Default is `true`.
+- `ShowDetailedSourceInformation`: Shows chunk-level RAG source details instead of grouping sources by file. Default is `false`.
```csharp
@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
@@ -1036,8 +1013,9 @@ chatComponent.off('messageSent', callbackFunction);
In order to configure the application to use the AI Management module, you first need to import `provideAIManagementConfig` from `@volo/abp.ng.ai-management/config` to root application configuration. Then, you will need to append it to the `appConfig` array:
-```js
+```ts
// app.config.ts
+import { ApplicationConfig } from '@angular/core';
import { provideAIManagementConfig } from '@volo/abp.ng.ai-management/config';
export const appConfig: ApplicationConfig = {
@@ -1048,20 +1026,24 @@ export const appConfig: ApplicationConfig = {
};
```
-The AI Management module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. It is available for import from `@volo/abp.ng.ai-management`.
+The AI Management module should be imported and lazy-loaded in your routing array. It has a `createRoutes` function for configuration and is available from `@volo/abp.ng.ai-management`.
-```js
+```ts
// app.routes.ts
+import { Routes } from '@angular/router';
+
const APP_ROUTES: Routes = [
// ...
{
path: 'ai-management',
loadChildren: () =>
- import('@volo/abp.ng.ai-management').then(m => m.createRoutes(/* options here */)),
+ import('@volo/abp.ng.ai-management').then(m => m.createRoutes()),
},
];
```
+`createRoutes` optionally accepts `AIManagementConfigOptions`. It provides entity-action, toolbar-action and entity-property contributors for workspaces and MCP servers, plus create/edit form contributors for workspaces. The Workspaces, Chat Playground, MCP Servers and Workspace Data Sources routes also use keys from `eAIManagementComponents`, so they can be replaced through ABP's replaceable component system.
+
#### Services / Models
AI Management module services and models are generated via `generate-proxy` command of the [ABP CLI](../../cli). If you need the module's proxies, you can run the following command in the Angular project directory:
@@ -1084,14 +1066,19 @@ export const environment = {
AIManagement: {
url: 'AI Management remote url here',
},
+ AIManagementClient: {
+ url: 'AI Management client remote url here',
+ },
// other api configurations
},
};
```
-The AI Management module remote URL configurations shown above are optional.
+Use `AIManagement` for management and integration proxies. Non-streaming requests from the Angular chat widget use the generated `ChatCompletionClientService` proxy with `AIManagementClient`. When streaming is enabled, both the stream-start POST request and the subsequent EventSource connection use `default.url` instead of `AIManagementClient`.
-> If you don't set the `AIManagement` property, the `default.url` will be used as fallback.
+Both named entries are optional and generated proxy requests fall back independently to `default.url`. `default.url` is also the global fallback for other ABP and application requests that don't specify a configured named API, so changing it redirects those requests too.
+
+For split hosting, configure `AIManagement` and `AIManagementClient` for their respective hosts. Before pointing `default.url` to the client API and enabling streaming, make sure every other request uses an appropriate named API or that the client host provides or forwards all endpoints that still rely on the global fallback.
#### The Chat Widget
@@ -1103,12 +1090,19 @@ The `@volo/abp.ng.ai-management` package provides a `ChatInterfaceComponent` (`a
```
- `workspaceName` (required): The name of the workspace to use.
-- `conversationId`: The unique identifier for persisting and retrieving chat history from client-side storage. When provided, the chat history is stored in the browser and restored when the user revisits the page. If `null`, the chat is ephemeral and will be lost when the component is destroyed.
+- `conversationId`: The unique identifier for persisting and retrieving chat history from client-side storage. When provided, the history is restored for the current user and workspace.
- `providerName`: The name of the AI provider. Used for displaying contextual error messages.
+- `allowEphemeral`: Allows sending without a `conversationId`. The default is `false`; set it to `true` for an in-memory conversation that is discarded with the component.
+- `showUsageDetails`: Shows usage and tool-call details. Default is `true`.
+- `showStreamCheckbox`: Shows the streaming toggle. Default is `true`.
+
+The component emits `messageSent` and `messageReceived` events for application-level conversation orchestration.
### Blazor UI
@@ -1117,26 +1111,22 @@ The `@volo/abp.ng.ai-management` package provides a `ChatInterfaceComponent` (`a
The AI Management module remote endpoint URLs can be configured in your `appsettings.json`:
```json
-"RemoteServices": {
- "Default": {
- "BaseUrl": "Default url here"
- },
- "AIManagement": {
- "BaseUrl": "AI Management remote url here"
+{
+ "RemoteServices": {
+ "Default": {
+ "BaseUrl": "Default url here"
+ },
+ "AIManagement": {
+ "BaseUrl": "AI Management remote url here"
+ },
+ "AIManagementClient": {
+ "BaseUrl": "AI Management client remote url here"
+ }
}
}
```
-For **Blazor WebAssembly**, you can also configure the remote endpoint URL via `AIManagementClientBlazorWebAssemblyOptions`:
-
-```csharp
-Configure(options =>
-{
- options.RemoteServiceUrl = builder.Configuration["RemoteServices:AIManagement:BaseUrl"];
-});
-```
-
-> If you don't set the `BaseUrl` for AIManagement, the `Default.BaseUrl` will be used as fallback.
+Use the `AIManagement` remote service for management and integration clients. Use `AIManagementClient` for the remote chat and OpenAI-compatible client APIs. Blazor WebAssembly uses the standard ABP remote-service configuration shown above; it doesn't require a module-specific options class. If a named `BaseUrl` isn't set, ABP uses `Default.BaseUrl` as the fallback.
#### The Chat Widget
@@ -1152,10 +1142,15 @@ The `Volo.AIManagement.Client.Blazor` package provides a `ChatClientChat` Blazor
```
- `WorkspaceName` (required): The name of the workspace to use.
-- `ConversationId`: The unique identifier for persisting and retrieving chat history from client-side storage. When provided, the chat history is stored in the browser's local storage and restored when the user revisits the page. If not provided or `null`, the chat is ephemeral and will be lost when the component is disposed.
+- `ConversationId` (required to send messages): The unique identifier for persisting and retrieving chat history from client-side storage. The component disables message input while this value is null or empty.
- `Title`: The title displayed in the chat widget header.
- `ShowStreamCheckbox`: Whether to show a checkbox that allows the user to toggle streaming on and off. Default is `false`.
-- `OnFirstMessage`: An `EventCallback` that is triggered when the first message is sent in a conversation. It can be used to determine the chat title after the first prompt like applied in the chat playground. The event args contain `ConversationId` and `Message` properties.
+- `UseStreaming`: Initial streaming mode. Default is `true`.
+- `ShowUsageDetails`: Shows token usage and tool calls. Default is `true` in the Blazorise component.
+- `ShowDetailedSourceInformation`: Shows chunk-level RAG source information. Default is `true` in the Blazorise component.
+- `OnFirstMessage`: An `EventCallback` that is triggered when the first message is sent in a conversation. It can be used to determine the chat title after the first prompt like applied in the chat playground. The event args contain `ConversationId` and `Message` properties.
+
+The MudBlazor package exposes the same core `WorkspaceName`, `ConversationId`, `Title`, `ShowStreamCheckbox` and `OnFirstMessage` parameters. The detailed usage/source display parameters above are specific to the Blazorise component.
```xml
```
-## Using Dynamic Workspace Configurations for custom requirements
+## Reading Dynamic Workspace Configuration
-The AI Management module allows you to access only configuration of a workspace without resolving pre-constructed chat client. This is useful when you want to use a workspace for your own purposes and you don't need to use the chat client.
-The `IWorkspaceConfigurationStore` service is used to access the configuration of a workspace. It has multiple implementations according to the usage scenario.
+Use `IWorkspaceConfigurationStore` when trusted server-side code needs the persisted workspace configuration without resolving an `IChatClient`. The store has local and remote implementations for the corresponding deployment scenarios.
```csharp
public class MyService
@@ -1179,34 +1173,29 @@ public class MyService
_workspaceConfigurationStore = workspaceConfigurationStore;
}
- public async Task DoSomethingAsync()
+ public async Task GetModelNameAsync()
{
- // Get the configuration of the workspace that can be managed dynamically.
- var configuration = await _workspaceConfigurationStore.GetAsync("MyWorkspace");
-
- // Do something with the configuration
- var kernel = Kernel.CreateBuilder()
- .AddAzureOpenAIChatClient(
- config.ModelName!,
- new Uri(config.ApiBaseUrl),
- config.ApiKey
- )
- .Build();
+ var configuration = await _workspaceConfigurationStore
+ .GetOrNullAsync("MyWorkspace");
+
+ return configuration?.ModelName;
}
}
```
+The returned configuration can include provider credentials. Keep it inside trusted server-side services and do not serialize it into an application response. Use `IChatClient` or `IChatCompletionClientAppService` when you only need to execute a chat request.
+
## Implementing Custom AI Provider Factories
-While the AI Management module provides built-in support for OpenAI through the `Volo.AIManagement.OpenAI` package, you can easily add support for other AI providers by implementing a custom `IChatClientFactory`.
+AI Management provides built-in OpenAI and Ollama factories through the `Volo.AIManagement.OpenAI` and `Volo.AIManagement.Ollama` packages. You can add another provider by implementing a custom `IChatClientFactory`.
### Understanding the Factory Pattern
-The AI Management module uses a factory pattern to create `IChatClient` instances based on the provider configuration stored in the database. Each provider (OpenAI, Ollama, Azure OpenAI, etc.) needs its own factory implementation.
+The AI Management module uses a factory pattern to create `IChatClient` instances based on the provider configuration stored in the database. Each provider name needs one registered factory implementation.
### Creating a Custom Factory
-Here's how to implement a factory for Ollama as an example:
+The following example registers a separate `CustomOllama` provider to demonstrate the factory contract. Use the built-in `Volo.AIManagement.Ollama` package when you only need the standard Ollama integration.
#### Step 1: Install the Provider's NuGet Package
@@ -1221,6 +1210,8 @@ dotnet add package OllamaSharp
Create a factory class that implements `IChatClientFactory`:
```csharp
+using System;
+using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using OllamaSharp;
using Volo.AIManagement.Factory;
@@ -1228,20 +1219,18 @@ using Volo.Abp.DependencyInjection;
namespace YourNamespace;
-public class OllamaChatClientFactory : IChatClientFactory, ITransientDependency
+public class CustomOllamaChatClientFactory : IChatClientFactory, ITransientDependency
{
- public string Provider => "Ollama";
+ public string Provider => "CustomOllama";
public Task CreateAsync(ChatClientCreationConfiguration configuration)
{
- // Create the Ollama client with configuration from database
- var client = new OllamaApiClient(
+ var builder = new ChatClientBuilder(sp => new OllamaApiClient(
configuration.ApiBaseUrl ?? "http://localhost:11434",
- configuration.ModelName
- );
+ configuration.ModelName ?? throw new InvalidOperationException("A model is required.")));
- // Return as IChatClient
- return Task.FromResult(client);
+ builder.UseFunctionInvocation();
+ return Task.FromResult(builder.Build());
}
}
```
@@ -1255,13 +1244,13 @@ public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure(options =>
{
- options.AddFactory("Ollama");
+ options.AddFactory("CustomOllama");
});
}
```
-> [!TIP]
-> For production scenarios, you may want to add validation for the factory configuration.
+> [!IMPORTANT]
+> Validate every required configuration value in the factory. Build a `FunctionInvokingChatClient` with `UseFunctionInvocation()` when the workspace can use RAG or MCP tools.
### Available Configuration Properties
@@ -1270,6 +1259,7 @@ The `ChatClientCreationConfiguration` object provides the following properties f
| Property | Type | Description |
| ------------------------ | ------- | ------------------------------------------- |
| `Name` | string | Workspace name |
+| `WorkspaceId` | Guid? | Persisted workspace ID, when available |
| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") |
| `ApiKey` | string? | API key for authentication |
| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") |
@@ -1279,6 +1269,7 @@ The `ChatClientCreationConfiguration` object provides the following properties f
| `Description` | string? | Workspace description |
| `IsActive` | bool | Whether the workspace is active |
| `IsSystem` | bool | Whether it's a system workspace |
+| `OverrideSystemConfiguration` | bool | Whether persisted settings override a system workspace |
| `RequiredPermissionName` | string? | Permission required to use this workspace |
| `HasEmbedderConfiguration` | bool | Whether the workspace has embedder/RAG configuration |
@@ -1286,7 +1277,11 @@ The `ChatClientCreationConfiguration` object provides the following properties f
Here's an example of implementing a factory for Azure OpenAI:
+Install the `Azure.AI.OpenAI` and `Microsoft.Extensions.AI.OpenAI` NuGet packages before adding this factory (the `AsIChatClient()` extension method comes from `Microsoft.Extensions.AI.OpenAI`).
+
```csharp
+using System;
+using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure;
using Microsoft.Extensions.AI;
@@ -1306,8 +1301,13 @@ public class AzureOpenAIChatClientFactory : IChatClientFactory, ITransientDepend
new AzureKeyCredential(configuration.ApiKey ?? throw new ArgumentNullException(nameof(configuration.ApiKey)))
);
- var chatClient = client.GetChatClient(configuration.ModelName);
- return Task.FromResult(chatClient.AsIChatClient());
+ var modelName = configuration.ModelName ??
+ throw new ArgumentNullException(nameof(configuration.ModelName));
+ var builder = new ChatClientBuilder(sp =>
+ client.GetChatClient(modelName).AsIChatClient());
+
+ builder.UseFunctionInvocation();
+ return Task.FromResult(builder.Build());
}
}
```
@@ -1318,7 +1318,7 @@ After implementing and registering your factory:
1. **Through UI**: Navigate to the AI Management workspaces page and create a new workspace:
- - Select your provider name (e.g., "Ollama", "AzureOpenAI")
+ - Select your provider name (for this example, `CustomOllama`)
- Configure the API settings
- Set the model name
@@ -1326,8 +1326,8 @@ After implementing and registering your factory:
```csharp
var workspace = await _applicationWorkspaceManager.CreateAsync(
- name: "MyOllamaWorkspace",
- provider: "Ollama",
+ name: "MyCustomOllamaWorkspace",
+ provider: "CustomOllama",
modelName: "mistral"
);
workspace.ApiBaseUrl = "http://localhost:11434";
@@ -1396,16 +1396,16 @@ The module exposes the following integration services for inter-service communic
Workspace configurations are cached for performance. The cache key format:
-```
-WorkspaceConfiguration:{ApplicationName}:{WorkspaceName}
+```text
+WorkspaceConfiguration:{WorkspaceName}
```
+The cache is invalidated when workspaces are created, updated or deleted. A rename invalidates both the old and current workspace names, and invalidation participates in the current unit of work.
+
### HttpApi Client Layer
- `IntegrationWorkspaceConfigurationStore`: Integration service for remote workspace configuration retrieval. Implements `IWorkspaceConfigurationStore` interface.
-The cache is automatically invalidated when workspaces are created, updated, or deleted.
-
## See Also
- [Artificial Intelligence Infrastructure](../../framework/infrastructure/artificial-intelligence/index.md): Learn about the underlying AI workspace infrastructure
diff --git a/docs/en/modules/audit-logging-pro.md b/docs/en/modules/audit-logging-pro.md
index 851787d390..1e7ab562dc 100644
--- a/docs/en/modules/audit-logging-pro.md
+++ b/docs/en/modules/audit-logging-pro.md
@@ -25,7 +25,7 @@ See [the module description page](https://abp.io/modules/Volo.AuditLogging.Ui) f
## How to install
-Identity is pre-installed in [the startup templates](../solution-templates). So, no need to manually install it.
+Audit Logging is pre-installed in [the startup templates](../solution-templates). So, no need to manually install it.
### Packages
@@ -41,7 +41,7 @@ Audit logs module adds the following items to the "Main" menu, under the "Admini
* **Audit Logs**: List, view and filter audit logs and entity changes.
-`IAbpAuditLoggingMainMenuNames` class has the constants for the menu item names.
+`AbpAuditLoggingMainMenuNames` class has the constants for the menu item names.
### Pages
@@ -67,7 +67,7 @@ You can view details of an audit log by clicking the magnifier icon on each audi
##### Export to Excel
-You can export audit logs to Excel by clicking the "Export to Excel" button in the toolbar. If the result set is small (less than a configurable threshold), the file will be generated and downloaded immediately. For larger result sets, the export will be processed as a background job and you'll receive an email with a download link once the export is completed.
+You can export audit logs to Excel by clicking the "Export to Excel" button in the toolbar. The file is generated and downloaded immediately when the result set contains 1,000 records or fewer. If the result set contains more than 1,000 records, the export is processed as a background job and you'll receive an email with a download link once the export is completed.
#### Entity Changes
@@ -97,7 +97,7 @@ You can view details of all changes of an entity by clicking the "Full Change Hi
##### Export to Excel
-You can export entity changes to Excel by clicking the "Export to Excel" button in the toolbar. Similar to audit logs export, for large datasets the export will be processed as a background job and you'll receive an email notification once completed.
+You can export entity changes to Excel by clicking the "Export to Excel" button in the toolbar. As with audit log exports, result sets with 1,000 records or fewer are downloaded immediately. Result sets with more than 1,000 records are processed as a background job, and you'll receive an email notification once the export is completed.
#### Audit Log Settings
@@ -113,6 +113,111 @@ To view the audit log settings, you need to enable the feature. For the host sid
> If you don't enable the *Cleanup Service System Wide* from the host side under *Settings* -> *Audit logs* -> *Global*, it won't remove the expired audit logs, even if there are tenant specific settings.
+## Reusable widgets
+
+The module provides **Error Rate** and **Average Execution Duration Per Day** widgets. The current user needs the `AuditLogging.AuditLogs` permission to load their data.
+
+### Angular
+
+Import the widget components from `@volo/abp.ng.audit-logging`, add them to your component imports and keep references when you need to refresh their date range:
+
+```ts
+import { Component, ViewChild } from '@angular/core';
+import {
+ AverageExecutionDurationWidgetComponent,
+ ErrorRateWidgetComponent,
+} from '@volo/abp.ng.audit-logging';
+
+@Component({
+ selector: 'app-audit-statistics',
+ templateUrl: './audit-statistics.component.html',
+ imports: [
+ AverageExecutionDurationWidgetComponent,
+ ErrorRateWidgetComponent,
+ ],
+})
+export class AuditStatisticsComponent {
+ @ViewChild('averageExecutionDurationWidget')
+ averageExecutionDurationWidget!: AverageExecutionDurationWidgetComponent;
+
+ @ViewChild('errorRateWidget')
+ errorRateWidget!: ErrorRateWidgetComponent;
+
+ refresh(startDate: string, endDate: string) {
+ this.averageExecutionDurationWidget.draw({ startDate, endDate });
+ this.errorRateWidget.draw({ startDate, endDate });
+ }
+}
+```
+
+The `width` and `height` inputs are optional. Both default to `273` and `136`, respectively.
+
+```html
+
+
+
+```
+
+### Blazor
+
+The Bootstrap and MudBlazor packages expose components with the same parameters and `RefreshAsync` method. The following example uses the Bootstrap Blazor package. For MudBlazor, use the corresponding `Volo.Abp.AuditLogging.Blazor.MudBlazor` namespaces.
+
+```razor
+@using Volo.Abp.AuditLogging.Blazor.Pages.Shared.AverageExecutionDurationPerDayWidget
+@using Volo.Abp.AuditLogging.Blazor.Pages.Shared.ErrorRateWidget
+
+
+
+
+
+@code {
+ private DateTime StartDate { get; set; } = DateTime.Today.AddMonths(-1);
+ private DateTime EndDate { get; set; } = DateTime.Today;
+
+ private AuditLoggingAverageExecutionDurationPerDayWidgetComponent AverageExecutionDurationWidget { get; set; } = default!;
+ private AuditLoggingErrorRateWidgetComponent ErrorRateWidget { get; set; } = default!;
+
+ private async Task RefreshAsync()
+ {
+ await AverageExecutionDurationWidget.RefreshAsync();
+ await ErrorRateWidget.RefreshAsync();
+ }
+}
+```
+
+### MVC / Razor Pages
+
+Use `IWidgetManager` to check the widget permission before invoking its view component:
+
+```cshtml
+@using Volo.Abp.AspNetCore.Mvc.UI.Widgets
+@using Volo.Abp.AuditLogging.Web.Pages.Shared.Components.AverageExecutionDurationPerDayWidget
+@using Volo.Abp.AuditLogging.Web.Pages.Shared.Components.ErrorRateWidget
+@inject IWidgetManager WidgetManager
+
+@if (await WidgetManager.IsGrantedAsync(typeof(AuditLoggingErrorRateWidgetViewComponent)))
+{
+ @await Component.InvokeAsync(typeof(AuditLoggingErrorRateWidgetViewComponent))
+}
+
+@if (await WidgetManager.IsGrantedAsync(typeof(AuditLoggingAverageExecutionDurationPerDayWidgetViewComponent)))
+{
+ @await Component.InvokeAsync(typeof(AuditLoggingAverageExecutionDurationPerDayWidgetViewComponent))
+}
+```
+
## Data seed
This module doesn't seed any data.
@@ -145,7 +250,7 @@ Configure(options =>
// The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links:
// https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions
// https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html
- options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 0 23 * * ?"
+ options.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 0 23 * * ?"
});
```
@@ -237,16 +342,30 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
* AbpAuditLogActions
* AbpEntityChanges
* AbpEntityPropertyChanges
+* **AbpAuditLogExcelFiles**
#### MongoDB
##### Collections
* **AbpAuditLogs**
+* **AbpAuditLogExcelFiles**
### Permissions
-See the `AbpAuditLoggingPermissions` class members for all permissions defined for this module.
+The module defines the following feature and permission relationships:
+
+* `AuditLogging.Enable` is enabled by default. The `AuditLogging.AuditLogs` permission requires this feature, and the audit log application service also checks it.
+* `AuditLogging.SettingManagement` is a child feature of `AuditLogging.Enable` and is disabled by default. The `AuditLogging.AuditLogs.SettingManagement` permission requires this feature.
+* `AuditLogging.AuditLogs.Export` is a child permission of `AuditLogging.AuditLogs`. Audit log and entity change export operations require this permission.
+
+See the `AbpAuditLoggingPermissions` and `AbpAuditLoggingFeatures` class members for the complete definitions.
+
+#### Entity-specific change history permissions
+
+You can define a permission for the change history of a specific entity by using the `AuditLogging.ViewChangeHistory:{EntityTypeFullName}` naming convention. For example, the permission name for `Acme.BookStore.Books.Book` is `AuditLogging.ViewChangeHistory:Acme.BookStore.Books.Book`.
+
+When a matching permission is defined and granted, the user can view that entity's change history. If the entity-specific permission is not defined or is not granted, authorization falls back to `AuditLogging.AuditLogs`. Users who have the general audit log permission can therefore still view the entity history.
### Angular UI
diff --git a/docs/en/modules/audit-logging.md b/docs/en/modules/audit-logging.md
index 5cba52cf34..a3f99147db 100644
--- a/docs/en/modules/audit-logging.md
+++ b/docs/en/modules/audit-logging.md
@@ -29,12 +29,37 @@ The source code of this module can be accessed [here](https://github.com/abpfram
- `EntityChange` (collection): Changed entities of audit log.
- `AuditLogAction` (collection): Executed actions of audit log.
+#### Extending the Entities
+
+The `AuditLog`, `AuditLogAction` and `EntityChange` entities support the [Module Entity Extensions](../framework/architecture/modularity/extending/module-entity-extensions.md) system. Configure them in the `Domain.Shared` project before the database model is created. The following example adds a property to `AuditLog`:
+
+````csharp
+ObjectExtensionManager.Instance.Modules()
+ .ConfigureAuditLogging(auditLogging =>
+ {
+ auditLogging.ConfigureAuditLog(auditLog =>
+ {
+ auditLog.AddOrUpdateProperty("ExternalId");
+ });
+ });
+````
+
+Use `ConfigureAuditLogAction` or `ConfigureEntityChange` in the same way to extend the other supported entities.
+
#### Repositories
Following custom repositories are defined for this module:
- `IAuditLogRepository`
+#### Audit Log Conversion
+
+The module uses `IAuditLogInfoToAuditLogConverter` to convert the `AuditLogInfo` collected by the auditing system into the persisted `AuditLog` aggregate. You can inject this service when you need the same conversion in a custom persistence flow, or replace its default implementation using the [dependency injection system](../framework/fundamentals/dependency-injection.md#replace-a-service) to customize the mapping.
+
+#### Persistence Limits
+
+Before saving an audit log, the module truncates fields that have maximum lengths defined by `AuditLogConsts`, `AuditLogActionConsts`, `EntityChangeConsts` and `EntityPropertyChangeConsts`. Action parameters are handled differently: if `AuditLogAction.Parameters` exceeds `AuditLogActionConsts.MaxParametersLength` (2,000 by default), it is persisted as an empty string instead of being truncated.
+
### Database providers
#### Common
@@ -55,13 +80,15 @@ This module uses `AbpAuditLogging` for the connection string name. If you don't
- AbpAuditLogActions
- AbpEntityChanges
- AbpEntityPropertyChanges
+- **AbpAuditLogExcelFiles**
#### MongoDB
##### Collections
- **AbpAuditLogs**
+- **AbpAuditLogExcelFiles**
## See Also
-* [Audit logging system](../framework/infrastructure/audit-logging.md)
\ No newline at end of file
+* [Audit logging system](../framework/infrastructure/audit-logging.md)
diff --git a/docs/en/modules/background-jobs.md b/docs/en/modules/background-jobs.md
index 2d57d0c907..63ba851e2b 100644
--- a/docs/en/modules/background-jobs.md
+++ b/docs/en/modules/background-jobs.md
@@ -41,7 +41,7 @@ Following custom repositories are defined for this module:
##### Table / collection prefix & schema
-All tables/collections use the `Abp` prefix by default. Set static properties on the `BackgroundJobsDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider).
+All tables/collections use the `Abp` prefix by default. Set static properties on the `AbpBackgroundJobsDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider).
##### Connection string
@@ -61,4 +61,4 @@ This module uses `AbpBackgroundJobs` for the connection string name. If you don'
## See Also
-* [Background job system](../framework/infrastructure/background-jobs)
\ No newline at end of file
+* [Background job system](../framework/infrastructure/background-jobs)
diff --git a/docs/en/modules/blogging.md b/docs/en/modules/blogging.md
new file mode 100644
index 0000000000..39492a696c
--- /dev/null
+++ b/docs/en/modules/blogging.md
@@ -0,0 +1,172 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to install and configure the standalone ABP Blogging module, including its MVC UI, routes, permissions, files and database providers."
+}
+```
+
+# Blogging Module
+
+The Blogging module is a free and open-source application module for creating one or more blogs. It provides an MVC / Razor Pages user interface, application services, HTTP APIs and Entity Framework Core and MongoDB integrations for blogs, posts, tags, comments and member profiles.
+
+> This page documents the standalone `Volo.Blogging` module. [CMS Kit: Blogging](cms-kit/blogging.md) is a different feature family with different entities, configuration and UI.
+
+## How to Install
+
+Use the ABP CLI to add the module to an existing solution:
+
+```bash
+abp add-module Volo.Blogging
+```
+
+The command adds the module packages and dependencies to the compatible projects in your solution. Apply the generated database migration after adding the module.
+
+### The Source Code
+
+The source code of this module is available in the [ABP repository](https://github.com/abpframework/abp/tree/dev/modules/blogging). It is licensed with [MIT](https://choosealicense.com/licenses/mit/), so you can use and customize it.
+
+## User Interface and Content Workflow
+
+The module provides two MVC / Razor Pages surfaces:
+
+* The public site lists blogs, posts and popular tags, renders post content as Markdown, displays member profiles and allows authenticated users to add comments.
+* The administration page manages blogs. Post creation and editing are available from the public blog UI to users with the corresponding permissions.
+
+When the application has one blog, the blog index redirects directly to that blog. With multiple blogs, the index displays the available blogs.
+
+Creating a post makes it available to the public list and reading APIs immediately. The standalone module does not add a draft, review or scheduled-publication state. If a post URL is already used in the same blog, the application service appends a generated suffix and returns the resulting URL.
+
+Tags are entered with a post. The application service normalizes tag names to lowercase, removes duplicates and maintains their usage counts.
+
+### Member Profiles
+
+The module keeps a local `BlogUser` record for post and comment authors. It uses ABP's [user lookup and synchronization](identity/user-synchronization.md) infrastructure to obtain Identity user data. The public member page lists an active user's posts and profile information; the current user can edit the Blogging-specific fields on their own profile.
+
+## Permissions
+
+The administration menu is visible with the `Blogging.Blog.Management` permission. Blog operations use separate child permissions:
+
+* `Blogging.Blog.Create`
+* `Blogging.Blog.Update`
+* `Blogging.Blog.Delete`
+* `Blogging.Blog.ClearCache`
+
+Post creation, update and deletion require `Blogging.Post.Create`, `Blogging.Post.Update` and `Blogging.Post.Delete`, respectively.
+
+Any authenticated user can create a comment. A comment can be updated or deleted by its creator or by a user with `Blogging.Comment.Update` or `Blogging.Comment.Delete`.
+
+See the [Authorization](../framework/fundamentals/authorization/index.md) documentation to learn how to grant permissions to roles and users.
+
+## Routing
+
+`BloggingUrlOptions.RoutePrefix` controls the public URL prefix. Its default value produces URLs under `/blog/`. The following example moves the public blog under `/articles/` and enables single-blog mode for the blog whose short name is `engineering`:
+
+```csharp
+Configure(options =>
+{
+ options.RoutePrefix = "articles";
+ options.SingleBlogMode.Enabled = true;
+ options.SingleBlogMode.BlogName = "engineering";
+});
+```
+
+Single-blog mode removes the blog short-name segment from post URLs. If the application contains multiple blogs, set `SingleBlogMode.BlogName` to the short name of the blog to expose. If it contains exactly one blog, the module can select it without setting `BlogName`.
+
+Set `RoutePrefix` to an empty string to serve the blog from `/`. In this mode the route constraint uses `IgnoredPaths` to avoid capturing other top-level application routes. The module already adds its framework endpoints, bundle folder and member route; add application-specific top-level paths when needed:
+
+```csharp
+Configure(options =>
+{
+ options.RoutePrefix = "";
+ options.IgnoredPaths.Add("health");
+});
+```
+
+## Post Images
+
+Post images are saved through the [BLOB Storing](../framework/infrastructure/blob-storing) system in the `blogging-files` container. Configure a BLOB provider for this container as you would for any other typed container.
+
+The upload service accepts JPEG, PNG, GIF and BMP images. The maximum file size is 5 MiB by default. `BloggingWebConsts.FileUploading.MaxFileSize` is a process-wide static value, so set it once during application startup, before the application begins accepting uploads:
+
+```csharp
+BloggingWebConsts.FileUploading.MaxFileSize = 10 * 1024 * 1024;
+```
+
+## Social Media Metadata
+
+The post page emits Twitter card metadata. Configure the site handle with `BloggingTwitterOptions`:
+
+```csharp
+Configure(options =>
+{
+ options.Site = "@myblog";
+});
+```
+
+## Post List Cache
+
+The time-ordered post list is cached per blog for one hour. Creating, updating or deleting posts through `IPostAppService` invalidates that cache within the current unit of work. The blog administration page also provides a **Clear Cache** action to users with the `Blogging.Blog.ClearCache` permission.
+
+If custom code changes posts directly through `IPostRepository`, publish the module's `PostChangedEvent` with the affected blog ID. The built-in event handler then removes that blog's cached list as part of the current unit of work.
+
+## Internals
+
+### Domain Layer
+
+The main aggregates are:
+
+* `Blog`: A named blog identified in public URLs by its short name.
+* `Post`: Markdown content, cover image, URL, tags and read count for a blog.
+* `Comment`: A comment or direct reply attached to a post.
+* `Tag`: A normalized tag and its usage count within a blog.
+* `BlogUser`: The module-local representation of an Identity user and Blogging profile.
+
+### Application Layer
+
+The public application-service contracts are `IBlogAppService`, `IPostAppService`, `ICommentAppService`, `ITagAppService` and `IFileAppService`. `IBlogManagementAppService` provides blog administration operations.
+
+The HTTP API uses the `Blogging` remote-service name for public operations and `BloggingAdmin` for administration operations. Add `BloggingHttpApiClientModule` or `BloggingAdminHttpApiClientModule` to a client application when these services run remotely.
+
+### Database Providers
+
+#### Common
+
+The module uses `Blogging` as its connection string name and falls back to `Default` when that connection is not configured. See the [Connection Strings](../framework/fundamentals/connection-strings.md) documentation.
+
+Tables and collections use the `Blg` prefix by default. Set `AbpBloggingDbProperties.DbTablePrefix` and, for providers that support schemas, `AbpBloggingDbProperties.DbSchema` before the database model is created:
+
+```csharp
+AbpBloggingDbProperties.DbTablePrefix = "MyBlog";
+AbpBloggingDbProperties.DbSchema = "blogging";
+```
+
+#### Entity Framework Core
+
+The Entity Framework Core provider uses these tables:
+
+* `BlgUsers`
+* `BlgBlogs`
+* `BlgPosts`
+* `BlgComments`
+* `BlgTags`
+* `BlgPostTags`
+
+Call `ConfigureBlogging()` from your migration DbContext when integrating the module manually.
+
+#### MongoDB
+
+The MongoDB provider uses these collections:
+
+* `BlgUsers`
+* `BlgBlogs`
+* `BlgPosts`
+* `BlgComments`
+* `BlgTags`
+
+Post-tag links are stored with the post documents, so MongoDB does not use a separate `BlgPostTags` collection.
+
+## See Also
+
+* [CMS Kit: Blogging](cms-kit/blogging.md)
+* [BLOB Storing](../framework/infrastructure/blob-storing)
+* [User Lookup and Synchronization](identity/user-synchronization.md)
diff --git a/docs/en/modules/chat.md b/docs/en/modules/chat.md
index fa87a8eef6..f3d30691a7 100644
--- a/docs/en/modules/chat.md
+++ b/docs/en/modules/chat.md
@@ -36,7 +36,7 @@ If you modified your solution structure, adding module using ABP Suite might not
In order to do that, add packages listed below to matching project on your solution. For example, ```Volo.Chat.Application``` package to your **{ProjectName}.Application.csproj** like below;
-```json
+```xml
```
@@ -49,7 +49,7 @@ After adding the package reference, open the module class of the project (eg: `{
)]
```
-> If you are using Blazor Web App, you need to add the `Volo.Chat.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and ad the `Volo.Chat.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project.
+> If you are using Blazor Web App, you need to add the `Volo.Chat.Blazor.WebAssembly` package to the **{ProjectName}.Blazor.Client.csproj** project and add the `Volo.Chat.Blazor.Server` package to the **{ProjectName}.Blazor.csproj** project.
The `Volo.Chat.SignalR` package must be added according to your project structure:
@@ -107,18 +107,38 @@ You can visit [Chat module package list page](https://abp.io/packages?moduleName
## User interface
-### Manage chat feature
+### Chat feature and permissions
-Chat module defines the chat feature, you need to enable the chat feature to use chat.
+The `Chat.Enable` feature is disabled by default. Enable it for a tenant or edition before granting chat permissions.

+The module defines the following permissions:
+
+* `Chat.Messaging`: Allows a user to open the chat page and exchange messages. A message can only be sent if the target user also has this permission.
+* `Chat.Searching`: A child permission of `Chat.Messaging`. It allows a user to start a conversation with a user who is not already in the conversation history. Without this permission, the user can continue existing conversations.
+* `Chat.SettingManagement`: Allows a user to manage the chat settings.
+
### Chat page
This is the page that users send messages to each other.

+Message text is required. By default, it must contain between 1 and 4,096 characters. The conversation API returns the newest messages first; the built-in user interfaces reverse that result to display messages in chronological order.
+
+### Deleting messages and conversations
+
+The following settings control deletion behavior:
+
+| Setting | Default | Description |
+| --- | --- | --- |
+| `Volo.Chat.Messaging.DeletingMessages` | `Enabled` | `Enabled` allows deletion at any time, `Disabled` prevents deletion, and `EnabledWithDeletionPeriod` allows deletion only during the configured period after the message is created. |
+| `Volo.Chat.Messaging.MessageDeletionPeriod` | `0` | Deletion period in seconds when message deletion is set to `EnabledWithDeletionPeriod`. |
+| `Volo.Chat.Messaging.DeletingConversations` | `Enabled` | Enables conversation deletion. It is effective only when message deletion is set to `Enabled`. |
+
+Deleting a message removes the shared message and both users' message records. Deleting a conversation removes both users' conversation records and all messages between them. These operations are shared deletions, not a "hide for me" operation. Deletion notifications are sent through the real-time channel.
+
### Chat icon on navigation bar
An icon that shows unread message count of the user and leads to chat page when clicked is added to navigation menu.
@@ -213,10 +233,11 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
#### Installation
-In order to configure the application to use the chat module, you first need to import `provideChatConfig` from `@volo/abp.ng.chat/config` to root application confiuration. Then, you will need to append it to the `appConfig` array.
+In order to configure the application to use the chat module, you first need to import `provideChatConfig` from `@volo/abp.ng.chat/config` to the root application configuration. Then, you will need to append it to the `providers` array.
-```js
+```ts
// app.config.ts
+import { ApplicationConfig } from '@angular/core';
import { provideChatConfig } from '@volo/abp.ng.chat/config';
export const appConfig: ApplicationConfig = {
@@ -228,20 +249,26 @@ export const appConfig: ApplicationConfig = {
```
-The chat module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. It is available for import from `@volo/abp.ng.chat`.
+The chat module should be imported and lazy-loaded in your routing array. It exports a `createRoutes` function from `@volo/abp.ng.chat`.
-```js
+```ts
// app.routes.ts
+import { Routes } from '@angular/router';
+
const APP_ROUTES: Routes = [
// ...
{
path: 'chat',
loadChildren: () =>
- import('@volo/abp.ng.chat').then(c => c.createRoutes(/* options here */)),
+ import('@volo/abp.ng.chat').then(c => c.createRoutes()),
},
];
```
+#### Component replacement
+
+The Angular chat route uses the `Chat.ChatComponent` replacement key and `ChatComponent` as its default component. See the [Angular component replacement documentation](../framework/ui/angular/component-replacement.md) if you need to replace the page.
+
#### Services / Models
Chat module services and models are generated via `generate-proxy` command of the [ABP CLI](../cli). If you need the module's proxies, you can run the following command in the Angular project directory:
@@ -254,7 +281,7 @@ abp generate-proxy --module chat
The Chat module remote endpoint URLs can be configured in the environment files.
-```js
+```ts
export const environment = {
// other configurations
apis: {
@@ -274,12 +301,24 @@ The Chat module remote URL configurations shown above are optional.
> If you don't set the `signalRUrl`, `Chat.url` will be used as fallback. If you don't set the `Chat` property, the `default.url` will be used as fallback.
-### Blazor WebAssembly UI
+### Blazor and MAUI UIs
#### Remote Endpoint URL
+The SignalR base URL can be configured with the option type that matches the UI package:
+
+| UI package | Option type | Fallback when `SignalrUrl` is not set |
+| --- | --- | --- |
+| Blazor Server | `ChatBlazorServerOptions` | The current application URL. |
+| Blazor WebAssembly | `ChatBlazorWebAssemblyOptions` | `AbpRemoteServiceOptions.RemoteServices.Default.BaseUrl`. |
+| Blazor MAUI | `ChatBlazorMauiBlazorOptions` | `AbpRemoteServiceOptions.RemoteServices.Default.BaseUrl`. |
+| MudBlazor Server | `ChatBlazorMudBlazorServerOptions` | The current application URL. |
+| MudBlazor WebAssembly | `ChatBlazorMudBlazorWebAssemblyOptions` | `AbpRemoteServiceOptions.RemoteServices.Default.BaseUrl`. |
+| MudBlazor MAUI | `ChatBlazorMudBlazorMauiBlazorOptions` | `AbpRemoteServiceOptions.RemoteServices.Default.BaseUrl`. |
-The Chat module remote endpoint URLs can be configured via the `ChatBlazorWebAssemblyOptions`.
+The module appends the `/signalr-hubs/chat` path to the resolved base URL.
+
+For example, the Chat module remote endpoint URL can be configured for Blazor WebAssembly via the `ChatBlazorWebAssemblyOptions`:
```csharp
Configure(options =>
@@ -303,4 +342,12 @@ Configure(options =>
## Distributed Events
-This module defines an event for messaging. It is published when a new message is sent from a user to another user, with an Event Transfer Object type of `ChatMessageEto`. See the [standard distributed events](../framework/infrastructure/event-bus/distributed) for more information about distributed events.
+The module defines the following Event Transfer Object types for real-time operations:
+
+| Event Transfer Object | Operation | SignalR client method |
+| --- | --- | --- |
+| `ChatMessageEto` | A message is sent. | `ReceiveMessage` |
+| `ChatDeletedMessageEto` | A message is deleted. | `DeleteMessage` |
+| `ChatDeletedConversationEto` | A conversation is deleted. | `DeleteConversation` |
+
+The default application-layer implementation of `IRealTimeChatMessageSender` publishes these events to the distributed event bus. The `Volo.Chat.SignalR` package replaces that implementation in the SignalR host, handles the distributed events and sends them to the target user through the corresponding SignalR client method. See the [standard distributed events](../framework/infrastructure/event-bus/distributed) for more information about distributed events.
diff --git a/docs/en/modules/cms-kit-pro/contact-form.md b/docs/en/modules/cms-kit-pro/contact-form.md
index eafa4b80b3..005e85073c 100644
--- a/docs/en/modules/cms-kit-pro/contact-form.md
+++ b/docs/en/modules/cms-kit-pro/contact-form.md
@@ -13,25 +13,25 @@ CMS Kit provides a widget to create a contact form on your website.
## Enabling the Contact Management System
-By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../framework/infrastructure/global-features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP's [Feature System](../framework/infrastructure/features.md) to disable a CMS Kit feature on runtime.
+By default, CMS Kit features are disabled. Therefore, you need to enable the features you want before starting to use them. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable or disable CMS Kit features at development time. Alternatively, you can use ABP's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature at runtime.
-> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable/disable CMS Kit features on development time.
+> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable or disable CMS Kit features at development time.
## Contact Widget
The contact management system provides a contact form [widget](../../framework/ui/mvc-razor-pages/widgets.md) to create contact forms on the UI:
```csharp
-@await Component.InvokeAsync(typeof(ContactViewComponent))
+@await Component.InvokeAsync(typeof(ContactViewComponent), new { })
```
-Here, a screenshot from the widget:
+Here is a screenshot of the widget:

## Multiple Contact Widgets
-The contact management system allows you to create multiple contact forms. You can define a named contact widget as below:
+The contact management system allows you to create multiple contact forms with different receivers. You can define a named contact widget as shown below:
```csharp
@await Component.InvokeAsync(typeof(ContactViewComponent), new
@@ -40,7 +40,7 @@ The contact management system allows you to create multiple contact forms. You c
});
```
-Then, you need to configure the defined contact widgets in the `ConfigureServices` method of your module class:
+Then, configure the receiver for each name in the `ConfigureServices` method of your module class:
```csharp
Configure(options =>
@@ -50,29 +50,30 @@ Configure(options =>
});
```
-Here, is a screenshot that shows multiple contact forms on a page:
+The following screenshot shows multiple contact forms on a page:

+When the submitted `contactName` matches a configured entry, that entry's receiver is used. Otherwise, the module uses the receiver email address configured on the CMS settings page. The contact name is also prefixed to the email subject when it is not empty.
## Options
-You can configure the `CmsKitContactOptions` to enable/disable recaptcha for contact form in the `ConfigureServices` method of your [module](../../framework/architecture/modularity/basics.md).
+You can configure `CmsKitContactOptions` to enable or disable reCAPTCHA for the contact form in the `ConfigureServices` method of your [module](../../framework/architecture/modularity/basics.md).
Example:
```csharp
Configure(options =>
{
- options.IsRecaptchaEnabled = true; //false by default
+ options.IsRecaptchaEnabled = true;
});
```
`CmsKitContactOptions` properties:
-* `IsRecaptchaEnabled` (default: false): This flag enables or disables the reCaptcha for the contact form. You can set it as **true** if you want to use reCaptcha in your contact form.
+* `IsRecaptchaEnabled` (default: `false`): Enables reCAPTCHA v3 validation for public contact submissions.
-If you set **IsRecaptchaEnabled** as **true**, you also need to specify **SiteKey** and **SiteSecret** options for reCaptcha. To do that, add **CmsKit:Contact** section into your `appsettings.json` file:
+If you set `IsRecaptchaEnabled` to `true`, also specify `SiteKey` and `SiteSecret` for reCAPTCHA. Add the `CmsKit:Contact` section to your `appsettings.json` file:
```json
{
@@ -85,9 +86,9 @@ If you set **IsRecaptchaEnabled** as **true**, you also need to specify **SiteKe
}
```
-## Settings
+## Settings
-You can configure the receiver (email address) by using the CMS tab in the settings page.
+You can configure the fallback receiver email address on the CMS tab of the settings page. This setting is tenant-aware and is used when the form has no matching named receiver. Its default value is `info@mycompanyname.com`; replace it with an address that belongs to your application before deploying to production.

diff --git a/docs/en/modules/cms-kit-pro/faq.md b/docs/en/modules/cms-kit-pro/faq.md
index 23d90d9705..230b9578cc 100644
--- a/docs/en/modules/cms-kit-pro/faq.md
+++ b/docs/en/modules/cms-kit-pro/faq.md
@@ -9,15 +9,15 @@
> You must have an [ABP Team or a higher license](https://abp.io/pricing) to use CMS Kit Pro module's features.
-The CMS kit provides a **FAQ** system to allow users to create, edit and delete FAQ's. Here is a screenshot of the FAQ widget:
+CMS Kit Pro provides an **FAQ** system to organize questions into groups and sections and display them on public pages. Here is a screenshot of the FAQ widget:

## Enabling the FAQ System
-By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP Framework's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature on runtime.
+By default, CMS Kit features are disabled. Therefore, you need to enable the features you want before starting to use them. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable or disable CMS Kit features at development time. Alternatively, you can use ABP's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature at runtime.
-> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable/disable CMS Kit features on development time.
+> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable or disable CMS Kit features at development time.
## User Interface
@@ -25,21 +25,21 @@ By default, CMS Kit features are disabled. Therefore, you need to enable the fea
CMS Kit module admin side adds the following items to the main menu, under the **CMS** menu item:
-**FAQ's**: FAQ management page.
+**FAQs**: FAQ group, section and question management page.
`CmsKitProAdminMenus` class has the constants for the menu item names.
### Pages
-You can list, create, update and delete sections and their questions FAQ's on the admin side of your solution.
+You can list, create, update and delete FAQ groups, sections and questions on the admin side of your solution. A group contains sections, and a section contains questions.



-## Faq Widget
+## FAQ Widget
-The FAQ system provides a FAQ [widget](../../framework/ui/mvc-razor-pages/widgets.md) for users to display FAQ's. You can place the widget on a page like below:
+The FAQ system provides an FAQ [widget](../../framework/ui/mvc-razor-pages/widgets.md) for displaying FAQs. You can place the widget on a page as shown below:
```csharp
@await Component.InvokeAsync(
@@ -47,30 +47,20 @@ The FAQ system provides a FAQ [widget](../../framework/ui/mvc-razor-pages/widget
new
{
groupName = "Community",
- name = "Development"
+ sectionName = "Development"
})
```
`FaqViewComponent` parameters:
-- `groupName` (optional): It allows to specify which FAQ group to show. If not specified, all groups will be shown.
-- `sectionName` (optional): It is used to determine which section within the specified group will be shown. If not specified, all sections in the related group will be shown.
-The FAQ system can also be used in combination with the [dynamic widget](../cms-kit/dynamic-widget.md) feature.
-
-## Options
-
-The FAQ system provides a mechanism to group sections by group name. For example, if you want to use the FAQ system for community and support page, you need to define two group names named Community and Support and add sections under these groups. So, before using the FAQ system, you need to define groups. For that, you can use `FaqOptions`. `FaqOptions` can be configured at the domain layer, in the `ConfigureServices` method of your [module]../../framework/architecture/modularity/basics.md).
+- `groupName` (required): Specifies the FAQ group to show. Create this group on the FAQ administration page before rendering the widget.
+- `sectionName` (optional): Specifies a section within the selected group. If it is not set, all sections in the group are shown.
-```csharp
-Configure(options =>
-{
- options.SetGroups(new[] { "General", "Community", "Support" });
-});
-```
+The FAQ system can also be used in combination with the [dynamic widget](../cms-kit/dynamic-widget.md) feature.
-`FaqOptions` properties:
+## FAQ Groups
-- `Groups`: Dictionary of defined groups in the FAQ system. The `options.SetGroups` method is a shortcut to add a new groups to this dictionary.
+FAQ groups are persisted data and are managed from the FAQ administration page. Create groups such as `Community` or `Support`, then assign each section to one of those groups. Group names must be unique.
## Internals
@@ -82,10 +72,11 @@ This module follows the [Entity Best Practices & Conventions](../../framework/ar
##### FAQ
-A FAQ represents a generated FAQ with its questions:
+An FAQ represents a generated FAQ with its questions:
- `FaqSection` (aggregate root): Represents the defined FAQ sections related to the FAQ in the system.
- `FaqQuestion` (aggregate root): Represents the defined FAQ questions with section identifier related to the FAQ in the system.
+- `FaqGroup` (aggregate root): Represents a named group that contains FAQ sections.
#### Repositories
@@ -95,6 +86,7 @@ The following special repositories are defined for these features:
- `IFaqSectionRepository`
- `IFaqQuestionRepository`
+- `IFaqGroupRepository`
#### Domain services
@@ -108,7 +100,9 @@ This module follows the [Domain Services Best Practices & Conventions](../../fra
- `FaqSectionAdminAppService` (implements `IFaqSectionAdminAppService`): Implements the use cases of FAQ section management for admin side.
- `FaqQuestionAdminAppService` (implements `IFaqQuestionAdminAppService`): Implements the use cases of FAQ question management for admin side.
-- `FaqSectionPublicAppService` (implements `IFaqSectionPublicAppService`): Implements the use cases of FAQ's for public websites.
+- `FaqGroupAdminAppService` (implements `IFaqGroupAdminAppService`): Implements the use cases of FAQ group management for admin side.
+- `FaqSectionPublicAppService` (implements `IFaqSectionPublicAppService`): Implements the use cases of FAQs for public websites.
+- `FaqGroupPublicAppService` (implements `IFaqGroupPublicAppService`): Finds FAQ groups by name for public widgets.
### Database providers
@@ -116,11 +110,11 @@ This module follows the [Domain Services Best Practices & Conventions](../../fra
##### Table / collection prefix & schema
-All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider).
+All tables/collections use the `Cms` prefix by default. Set static properties on the `AbpCmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider).
##### Connection string
-This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string.
+This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it falls back to the `Default` connection string.
See the [connection strings](../../framework/fundamentals/connection-strings.md) documentation for details.
@@ -130,6 +124,7 @@ See the [connection strings](../../framework/fundamentals/connection-strings.md)
- CmsFaqSections
- CmsFaqQuestions
+- CmsFaqGroups
#### MongoDB
@@ -137,7 +132,4 @@ See the [connection strings](../../framework/fundamentals/connection-strings.md)
- CmsFaqSections
- CmsFaqQuestions
-
-## Entity Extensions
-
-Check the ["Entity Extensions" section of the CMS Kit Module documentation](index.md#entity-extensions) to see how to extend entities of the FAQ Feature of the CMS Kit Pro module.
\ No newline at end of file
+- CmsFaqGroups
diff --git a/docs/en/modules/cms-kit-pro/index.md b/docs/en/modules/cms-kit-pro/index.md
index 2aec352cab..13c5f416f7 100644
--- a/docs/en/modules/cms-kit-pro/index.md
+++ b/docs/en/modules/cms-kit-pro/index.md
@@ -11,7 +11,7 @@
This module extends the [open-source CMS Kit module](../cms-kit) and adds additional CMS (Content Management System) capabilities to your application.
-> **This module is currently available for MVC / Razor Pages and Blazor UIs**.
+The administration UI is available for MVC / Razor Pages, Angular and Blazor (Blazorise and MudBlazor). The public website widgets documented in the feature pages are MVC / Razor Pages components.
The following features are provided by the open-source CMS Kit module:
@@ -30,10 +30,10 @@ The following features are provided by the CMS Kit Pro version:
* [**Newsletter**](newsletter.md) It allows users to subscribe to newsletters.
* [**Contact form**](contact-form.md) It allows users to write messages to you.
-* [**URL forwarding**](URL-forwarding.md) It allows the creation of URLs that point to other pages or external websites.
-* [**Poll**](poll.md) It allows to create simple polls for your visitors.
+* [**URL forwarding**](url-forwarding.md) It allows the creation of URLs that point to other pages or external websites.
+* [**Poll**](poll.md) Allows you to create simple polls for your visitors.
* [**Page Feedback**](page-feedback.md) It allows users to send feedback for your pages.
-* [**Faq**](faq.md) system to create dynamic FAQ.
+* [**FAQ**](faq.md) system to create dynamic FAQs.
Click on a feature to understand and learn how to use it. See [the module description page](https://abp.io/modules/Volo.CmsKit.Pro) for an overview of the module features.
@@ -41,7 +41,7 @@ Click on a feature to understand and learn how to use it. See [the module descri
### New Solutions
-CMS Kit Pro is pre-installed in [the startup templates](../../solution-templates) if you create the solution with the **public website** option. If you are using ABP CLI, you should specify the the `--with-public-website` option as shown below:
+CMS Kit Pro is pre-installed in [the startup templates](../../solution-templates) if you create the solution with the **public website** option. If you are using ABP CLI, specify the `--with-public-website` option as shown below:
```bash
abp new Acme.BookStore --with-public-website
@@ -49,7 +49,7 @@ abp new Acme.BookStore --with-public-website
### Existing Solutions
-If you want to add the CMS kit to your existing solution, you can use the ABP CLI `add-module` command:
+If you want to add CMS Kit Pro to your existing solution, you can use the ABP CLI `add-module` command:
```bash
abp add-module Volo.CmsKit.Pro
@@ -77,11 +77,47 @@ Alternatively, you can enable features individually, like `cmsKit.Comments.Enabl
> If you are using Entity Framework Core, remember to add a new migration and update your database.
+### Angular Administration UI
+
+The Angular package publishes the administration routes and their menu configuration in separate entry points. Register the configuration provider in your application configuration:
+
+```typescript
+import { ApplicationConfig } from '@angular/core';
+import { provideCmsKitAdminConfig } from '@abp/ng.cms-kit/admin/config';
+import { provideCmsKitProAdminConfig } from '@volo/abp.ng.cms-kit-pro/admin/config';
+
+export const appConfig: ApplicationConfig = {
+ providers: [provideCmsKitAdminConfig(), provideCmsKitProAdminConfig()],
+};
+```
+
+Then combine the open-source and Pro administration routes under the `cms` path:
+
+```typescript
+import { Routes } from '@angular/router';
+
+export const appRoutes: Routes = [
+ {
+ path: 'cms',
+ loadChildren: () =>
+ Promise.all([
+ import('@volo/abp.ng.cms-kit-pro/admin').then(cmsKitPro =>
+ cmsKitPro.createRoutes(),
+ ),
+ import('@abp/ng.cms-kit/admin').then(cmsKit => cmsKit.createRoutes()),
+ ]).then(([cmsKitProRoutes, cmsKitRoutes]) => [
+ ...cmsKitProRoutes,
+ ...cmsKitRoutes,
+ ]),
+ },
+];
+```
+
## Entity Extensions
-[Module entity extension](../../framework/architecture/modularity/extending/module-entity-extensions.md) system is a **high-level** extension system that allows you to **define new properties** for existing entities of the dependent modules. It automatically **adds properties to the entity**, **database**, **HTTP API and user interface** in a single point.
+The [module entity extension](../../framework/architecture/modularity/extending/module-entity-extensions.md) system allows you to define new properties for supported entities of a dependent module from a single configuration point.
-To extend entities of the CMS Kit Pro module, open your `YourProjectNameModuleExtensionConfigurator` class inside of your `DomainShared` project and change the `ConfigureExtraProperties` method like shown below.
+To extend entities of the CMS Kit Pro module, open your `YourProjectNameModuleExtensionConfigurator` class in the `Domain.Shared` project and change the `ConfigureExtraProperties` method as shown below.
```csharp
public static void ConfigureExtraProperties()
@@ -91,49 +127,44 @@ public static void ConfigureExtraProperties()
ObjectExtensionManager.Instance.Modules()
.ConfigureCmsKitPro(cmsKitPro =>
{
- cmsKitPro.ConfigurePoll(plan => // extend the Poll entity
+ cmsKitPro.ConfigurePoll(poll =>
{
- plan.AddOrUpdateProperty( //property type: string
- "PollDescription", //property name
- property => {
- //validation rules
- property.Attributes.Add(new RequiredAttribute()); //adds required attribute to the defined property
-
- //...other configurations for this property
- }
+ poll.AddOrUpdateProperty(
+ "PollDescription",
+ property =>
+ {
+ property.Attributes.Add(new RequiredAttribute());
+ }
);
- });
+ });
- cmsKitPro.ConfigureNewsletterRecord(newsletterRecord => // extend the NewsletterRecord entity
+ cmsKitPro.ConfigureNewsletterRecord(newsletterRecord =>
{
- newsletterRecord.AddOrUpdateProperty( //property type: string
- "NewsletterRecordDescription", //property name
- property => {
- //validation rules
- property.Attributes.Add(new RequiredAttribute()); //adds required attribute to the defined property
- property.Attributes.Add(
- new StringLengthAttribute(MyConsts.MaximumDescriptionLength) {
- MinimumLength = MyConsts.MinimumDescriptionLength
- }
- );
-
- //...other configurations for this property
- }
+ newsletterRecord.AddOrUpdateProperty(
+ "NewsletterRecordDescription",
+ property =>
+ {
+ property.Attributes.Add(new RequiredAttribute());
+ property.Attributes.Add(
+ new StringLengthAttribute(MyConsts.MaximumDescriptionLength)
+ {
+ MinimumLength = MyConsts.MinimumDescriptionLength
+ }
+ );
+ }
);
- });
+ });
});
});
}
```
-
+
* `ConfigureCmsKitPro` method is used to configure the entities of the CMS Kit Pro module.
-* `cmsKit.ConfigurePoll(...)` is used to configure the **Poll** entity of the CMS Kit Pro module. You can add or update the extra properties of the **Poll** entity.
+* `cmsKitPro.ConfigurePoll(...)` is used to configure the **Poll** entity of the CMS Kit Pro module. You can add or update the extra properties of the **Poll** entity.
-* `cmsKit.ConfigureNewsletterRecord(...)` is used to configure the **NewsletterRecord** entity of the CMS Kit Pro module. You can add or update the extra properties of the **NewsletterRecord** entity.
+* `cmsKitPro.ConfigureNewsletterRecord(...)` is used to configure the **NewsletterRecord** entity of the CMS Kit Pro module. You can add or update the extra properties of the **NewsletterRecord** entity.
-* You can also set some validation rules for the property that you defined. In the above sample, `RequiredAttribute` and `StringLengthAttribute` were added for the property named **"NewsletterRecord"**.
+* You can also set validation rules for the properties you define. In the example above, `RequiredAttribute` and `StringLengthAttribute` are added to the **NewsletterRecordDescription** property.
-* When you define the new property, it will automatically add to **Entity**, **HTTP API** and **UI** for you.
- * Once you define a property, it appears in the create and update forms of the related entity.
- * New properties also appear in the data table on the related page.
+* Extra properties are added to the entity and HTTP API. UI integration is entity- and UI-specific: Poll has create and update forms, while Newsletter records are read-only and expose their extra properties through the application DTOs.
diff --git a/docs/en/modules/cms-kit-pro/newsletter.md b/docs/en/modules/cms-kit-pro/newsletter.md
index 3097011658..6f2837fecd 100644
--- a/docs/en/modules/cms-kit-pro/newsletter.md
+++ b/docs/en/modules/cms-kit-pro/newsletter.md
@@ -9,15 +9,15 @@
> You must have an [ABP Team or a higher license](https://abp.io/pricing) to use CMS Kit Pro module's features.
-CMS Kit provides a **newsletter** system to allow users to subscribe to newsletters. Here a screenshot of the newsletter subscription widget:
+CMS Kit provides a **newsletter** system that allows users to subscribe to newsletters. Here is a screenshot of the newsletter subscription widget:

## Enabling the Newsletter System
-By default, CMS Kit features are disabled. Therefore, you need to enable the features you want, before starting to use it. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable/disable CMS Kit features on development time. Alternatively, you can use the ABP's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature on runtime.
+By default, CMS Kit features are disabled. Therefore, you need to enable the features you want before starting to use them. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable or disable CMS Kit features at development time. Alternatively, you can use ABP's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature at runtime.
-> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable/disable CMS Kit features on development time.
+> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable or disable CMS Kit features at development time.
## User Interface
@@ -29,20 +29,19 @@ By default, CMS Kit features are disabled. Therefore, you need to enable the fea
#### Newsletters
-You can then view the subscribers and export the list as CSV file, in the admin side of your solution:
+You can view subscribers, edit their preferences, import subscriptions from a CSV file and export the filtered list as a CSV file on the admin side of your solution:

#### Email Preferences Management
-You (and users of your public web application) can manage your email preferences and unsubscribe from newsletters by visiting the **Email Preferences page** (*/cms/newsletter/email-preferences*), in the public side of your solution:
+Users can manage their email preferences and unsubscribe from newsletters on the public **Email Preferences** page at `/cms/newsletter/email-preferences`:

## The Newsletter Subscription Widget
-The newsletter subscription system provides a newsletter subscription [widget](../../framework/ui/mvc-razor-pages/widgets.md) to allow users to subscribe to a newsletter.
-You can simply place the widget on a page like below:
+The newsletter subscription system provides a newsletter subscription [widget](../../framework/ui/mvc-razor-pages/widgets.md) to allow users to subscribe to a newsletter. You can place the widget on a page as shown below:
```csharp
@await Component.InvokeAsync(
@@ -52,10 +51,12 @@ You can simply place the widget on a page like below:
preference = "TechNewsletter",
source = "Footer",
requestAdditionalPreferencesLater = false
- })
+})
```
-When you're adding the newsletter component, you can the specify `source` parameter to see where users subscribe to newsletters. See the options to understand the preferences.
+The `preference` and `source` parameters are required. `preference` must match a registered preference. Use `source` to distinguish where subscriptions originate, such as `Footer` or `Blog`. If `requestAdditionalPreferencesLater` is `true`, the widget requests the additional subscriptions in the success dialog instead of the initial form. You can also pass `privacyPolicyConfirmation` to override the preference's configured privacy-policy text for that widget instance.
+
+New subscriptions require email confirmation. Once confirmed, users can manage all registered preferences from `/cms/newsletter/email-preferences`; disabling every preference removes the subscription record.
## Options
@@ -64,25 +65,67 @@ Before using the newsletter system, you need to define the preferences. You can
**Example:**
```csharp
-options.AddPreference("TechNewsletter",
- new NewsletterPreferenceDefinition(
- "Daily Technology Newsletter",
- privacyPolicyConfirmation: "I accept the Privacy Policy.")
- )
-);
+Configure(options =>
+{
+ options.AddPreference(
+ "ProductUpdates",
+ new NewsletterPreferenceDefinition(
+ new LocalizableString(
+ typeof(MyProjectResource),
+ "Newsletter:ProductUpdates")
+ )
+ );
+
+ options.AddPreference(
+ "TechNewsletter",
+ new NewsletterPreferenceDefinition(
+ new LocalizableString(
+ typeof(MyProjectResource),
+ "Newsletter:TechNewsletter"),
+ definition: new LocalizableString(
+ typeof(MyProjectResource),
+ "Newsletter:TechNewsletterDescription"),
+ privacyPolicyConfirmation: new LocalizableString(
+ typeof(MyProjectResource),
+ "Newsletter:PrivacyPolicyConfirmation"),
+ additionalPreferences: new List { "ProductUpdates" }
+ )
+ );
+});
```
`NewsletterOptions` properties:
-- `Preferences`: List of defined newsletter preferences (`NewsletterPreferenceDefinition`) in the newsletter system.
+- `Preferences`: Dictionary of registered preference names and their `NewsletterPreferenceDefinition` values.
- `WidgetViewPath`: Default view path for all newsletter preferences.
`NewsletterPreferenceDefinition` properties:
- `Preference`: Name of the preference. We will use this field while displaying the newsletter component on the UI.
-- `PrivacyPolicyConfirmation`: Privacy policy confirmation text shown in the newsletter subscription widget.
-- `AdditionalPreferences`: Additional preference list that will show up after a user subscribes to the newsletter.
-- `WidgetPath`: If you want to use a different newsletter widget instead of the default widget, you can specify the newsletter widget path using this field.
+- `DisplayPreference`: Localizable display name of the preference.
+- `Definition`: Optional localizable description shown on the email preferences page.
+- `PrivacyPolicyConfirmation`: Privacy policy confirmation text for the newsletter subscription widget. The preference-level value currently reaches the widget only when the selected definition has a non-empty `AdditionalPreferences` list; otherwise the service returns before localizing this value. Pass `privacyPolicyConfirmation` when invoking the widget if you need an override that is independent of that list.
+- `AdditionalPreferences`: Names of other registered preferences that participate in the additional-preference flow.
+- `WidgetViewPath`: Optional Razor view path for this preference. It overrides the default `NewsletterOptions.WidgetViewPath`.
+
+The widget uses `~/Pages/Public/Shared/Components/Newsletter/Default.cshtml` when neither view-path option is set.
+
+The current implementation first checks whether the selected preference has a non-empty `AdditionalPreferences` list. If it does, the service collects registered preference names referenced by the `AdditionalPreferences` lists of all registered definitions, excludes the selected preference and removes duplicates. If the selected preference has no additional preferences, the widget does not offer any. Keep this global collection behavior in mind when multiple definitions reference different additional preferences.
+
+### Email Preferences Page Options
+
+Use `NewsletterPreferencesManagementOptions` to set the source recorded for changes made on the email preferences page and an optional privacy-policy confirmation message:
+
+```csharp
+Configure(options =>
+{
+ options.Source = "EmailPreferences";
+ options.PrivacyPolicyConfirmation = new LocalizableString(
+ typeof(MyProjectResource),
+ "Newsletter:PrivacyPolicyConfirmation"
+ );
+});
+```
## Internals
@@ -94,7 +137,7 @@ This module follows the [Entity Best Practices & Conventions](../../framework/ar
##### NewsletterRecord
-A newsletter record represents a newsletter subscription for a specific email address
+A newsletter record represents a newsletter subscription for a specific email address.
- `NewsletterRecord` (aggregate root): Represents a newsletter subscription in the system.
@@ -127,11 +170,11 @@ This module follows the [Domain Services Best Practices & Conventions](../../fra
##### Table / collection prefix & schema
-All tables/collections use the `Cms` prefix by default. Set static properties on the `CmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider).
+All tables/collections use the `Cms` prefix by default. Set static properties on the `AbpCmsKitDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider).
##### Connection string
-This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it fallbacks to the `Default` connection string.
+This module uses `CmsKit` for the connection string name. If you don't define a connection string with this name, it falls back to the `Default` connection string.
See the [connection strings](../../framework/fundamentals/connection-strings.md) documentation for details.
@@ -150,4 +193,4 @@ See the [connection strings](../../framework/fundamentals/connection-strings.md)
## Entity Extensions
-Check the ["Entity Extensions" section of the CMS Kit Module documentation](index.md#entity-extensions) to see how to extend entities of the Newsletter Feature of the CMS Kit Pro module.
\ No newline at end of file
+Check the ["Entity Extensions" section of the CMS Kit Module documentation](index.md#entity-extensions) to see how to extend entities of the Newsletter Feature of the CMS Kit Pro module.
diff --git a/docs/en/modules/cms-kit-pro/page-feedback.md b/docs/en/modules/cms-kit-pro/page-feedback.md
index 559ed98a3a..495f7b6669 100644
--- a/docs/en/modules/cms-kit-pro/page-feedback.md
+++ b/docs/en/modules/cms-kit-pro/page-feedback.md
@@ -15,9 +15,9 @@ The CMS Kit Pro module provides a comprehensive **Page Feedback** system that en
## Enabling the Page Feedback System
-All CMS Kit features are disabled bu default. Therefore, you need to enable the features you want before starting to use it. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable/disable the CMS Kit features on development time. Alternatively, you can use the ABP's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature on runtime.
+All CMS Kit features are disabled by default. Therefore, you need to enable the features you want before starting to use them. You can use the [Global Feature](../../framework/infrastructure/global-features.md) system to enable or disable CMS Kit features at development time. Alternatively, you can use ABP's [Feature System](../../framework/infrastructure/features.md) to disable a CMS Kit feature at runtime.
-> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable/disable CMS Kit features on development time.
+> Check the ["How to Install" section of the CMS Kit Module documentation](index.md#how-to-install) to see how to enable or disable CMS Kit features at development time.
## User Interface
@@ -27,13 +27,13 @@ The CMS Kit module admin side adds the following items to the main menu, under t
**Page Feedbacks**: Page feedback management page.
-The `CmsKitProAdminMenus` class has the constants for the menu items names.
+The `CmsKitProAdminMenus` class defines the menu item name constants.
### Pages
#### Page Feedbacks
-You can list, view, update and delete page feedbacks in the admin side of your solution, and you can also set the email addresses to send notifications.
+You can list, view, update and delete page feedback from the administration interface. You can also configure the email addresses that receive notifications.


@@ -42,7 +42,16 @@ You can list, view, update and delete page feedbacks in the admin side of your s
## Page Feedback Widget
-The page feedback system provides a page feedback [widget](../../framework/ui/mvc-razor-pages/widgets.md) for users to send feedback about the current page. You can place the widget on a page like the below:
+The page feedback system accepts only registered entity types. Register the entity type in the domain layer before rendering its widget:
+
+```csharp
+Configure(options =>
+{
+ options.EntityTypes.Add(new PageFeedbackEntityTypeDefinition("Page"));
+});
+```
+
+You can then place the page feedback [widget](../../framework/ui/mvc-razor-pages/widgets.md) on a page:
```csharp
@(await Component.InvokeAsync(typeof(PageFeedbackViewComponent), new PageFeedbackViewDto
@@ -59,8 +68,12 @@ The page feedback system provides a page feedback [widget](../../framework/ui/mv
- `YesButtonText`: Yes button text. Used to change the default text of the yes button. Default value is `Yes`.
+- `VeryHelpfulText`: Description shown with the positive feedback choice.
+
- `NoButtonText`: No button text. Used to change the default text of the no button. Default value is `No`.
+- `NeedsImprovementText`: Description shown with the negative feedback choice.
+
- `UserNotePlaceholder`: User note placeholder. Used to change the default placeholder of the user note input.
- `SubmitButtonText`: Submit button text. Used to change the default text of the submit button. Default value is `Submit`.
@@ -77,7 +90,7 @@ The page feedback system provides a page feedback [widget](../../framework/ui/mv
### Page Feedback Modal Widget
-The page feedback system provides a page feedback modal [widget](../../framework/ui/mvc-razor-pages/widgets.md) for users to send feedback about the current page. You can place the widget on a page like the below:
+The page feedback system provides a page feedback modal [widget](../../framework/ui/mvc-razor-pages/widgets.md) for users to send feedback about the current page. You can place the widget on a page as shown below:
```html