diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 32c47a832e5..d48bd856123 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -550,6 +550,10 @@ "text": "Localization", "path": "framework/fundamentals/localization.md" }, + { + "text": "Multi-Lingual Objects", + "path": "multi-lingual-entities.md" + }, { "text": "Logging", "path": "framework/fundamentals/logging.md" @@ -1111,6 +1115,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 +1362,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 +1669,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 +1892,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 +1908,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 +2108,10 @@ "text": "MongoDB", "path": "framework/data/mongodb" }, + { + "text": "In-Memory Database", + "path": "framework/data/memorydb" + }, { "text": "Dapper", "path": "framework/data/dapper" @@ -2615,6 +2647,10 @@ "text": "Background Jobs", "path": "modules/background-jobs.md" }, + { + "text": "Blogging", + "path": "modules/blogging.md" + }, { "text": "Chat (Pro)", "path": "modules/chat.md" @@ -2691,7 +2727,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 +2769,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 2c6f0ee39a1..ee6d790e224 100644 --- a/docs/en/framework/api-development/auto-controllers.md +++ b/docs/en/framework/api-development/auto-controllers.md @@ -93,6 +93,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. @@ -223,4 +233,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 d560b536eb8..bfacc26886b 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 00000000000..754c4816bc3 --- /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 03808d7869c..f07f049555e 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 eed05291122..ea74f0976a2 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 697d7dba801..63ee51149ab 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 727c40f5178..c6ee6f567cf 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 076238b4447..d99ea4de8e0 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 418c0405655..25b3e00b6b2 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 00000000000..247d2b1e8f6 --- /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 b609e229638..5349decde43 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 4c323c1c52d..39a849b7bc0 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 f52cd9e07d6..8f7b85e37e0 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 2e2e3e377ae..9ceedf0ebf7 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 5ccfcf124a0..f9e659f29b1 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 5fd243eeefe..4caed371d35 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 94056400c7d..7a1928b4c65 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 d18c89c675a..2bde471f907 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 78e93e59f52..3bda1417dac 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 d3e7d128fca..8d83bfc7144 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 ee2cdf7e410..c5885f0d01c 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`): HTTP GET requests should not make any change in the database normally and audit log system doesn't save audit log objects for GET request. Set this to `true` to enable it also for the GET 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 57bffefbe06..2d70a9ea3cd 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 9b65a488a9f..43ddaceb195 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 1b568c247c4..ff226ae3c6c 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 7b7d64c4c65..5c8fc740f1a 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 e450dc20683..744d5cec13c 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 9005c3d6125..c6cedf78599 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 a8d17528cb5..54281e40b8e 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 f483aaa512c..307ba61557d 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 d8484a277e7..888655d4466 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 38c9528d861..14b493e1fc7 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 64439be1115..d2532709ecf 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 de8f6c75bfb..ccdc9d8eac4 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 853014c1cd4..e8912889b6c 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 682fcd2f102..51fb3a44aaf 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 be4160a8193..4a3ffe69fcf 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 00000000000..c2f24aed349 --- /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 ec1102f8931..4750bbbb647 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 e0e5493851f..190b17aab6c 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 33c4b0cec29..aa436a82935 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 a5ca0dec663..db7c65ab126 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 dab58d0e33c..6c65ac6efc0 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 d50484c83af..b6a77ec0f76 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: ` +

{%{{{ 'MyProjectName::Greeting' | abpAsyncLocalization | async }}}%}

+ `, +}) +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:
{%{{{ 'MyProjectName::HomePage' | abpLocalization }}}%}
``` -> **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 ![locale-error](./images/locale-error.png) -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 00000000000..144f24c1e14 --- /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 eec9e79ba08..47cce9c49b2 100644 --- a/docs/en/framework/ui/angular/modifying-the-menu.md +++ b/docs/en/framework/ui/angular/modifying-the-menu.md @@ -109,7 +109,7 @@ An alternative and probably cleaner way is to use a route provider. First create ```js // 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', @@ -145,10 +145,11 @@ We can also define a group for navigation elements. It's an optional property ```js // 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' diff --git a/docs/en/framework/ui/angular/oauth-module.md b/docs/en/framework/ui/angular/oauth-module.md index 9e6d4a8e1e8..b8730806e4f 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 8d30d925bba..5ec8497af41 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 4d889377fd2..87f95185a3a 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 4ec5577f732..112a785e498 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 00000000000..831f2e9bdf3 --- /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 00000000000..2a7b446baa9 --- /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 a793a784bd9..40bc6cf40f6 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 998a16a50ad..02ae892089a 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 2622bdc7e03..1bd089b26de 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 00000000000..520f6500872 --- /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 193575def67..8bb09645f78 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 425c94d2d90..b029fda57c1 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 4f6d673d879..9111badaa3e 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 fc009ce5a27..fabfefcea26 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 fdcf4a2e5ba..509f2a99a00 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 a79c3d4992e..0084a7f10c6 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 ![account-pro-module-local-login-setting](../images/account-pro-module-local-login-setting.png) +## 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 844e00e06f0..2148ca5d42c 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 ![account-module-register](../images/account-module-register.png) +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 ![account-module-manage-account](../images/account-module-manage-account.png) +`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 b8f00ee576f..a868977ece0 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 ![passkey-setting](../../images/passkey-setting.png) +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 908b342de2b..5205ef10f8a 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 ![Manage Invitations](../../images/manage-invitations.png) +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 1cef520472d..9ce3caf8657 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 ![ai-management-chat-mcp-tools](../../images/ai-management-chat-mcp-tools.png) +##### 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 ![ai-management-openai-anythingllm2](../../images/ai-management-openai-anythingllm2.png) -#### 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 851787d3908..1e7ab562dc3 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 5cba52cf343..a3f99147db3 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 2d57d0c9078..63ba851e2be 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 00000000000..39492a696cc --- /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 fa87a8eef64..f3d30691a72 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. ![chat-feature](../images/chat-feature.png) +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. ![chat-page](../images/chat-page.png) +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 eafa4b80b35..005e85073c5 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: ![contact-form](../../images/cmskit-module-contact-form.png) ## 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: ![multiple-contact-forms](../../images/cmskit-module-multiple-contact-forms.png) +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. ![contact-settings](../../images/cmskit-module-contact-settings.png) diff --git a/docs/en/modules/cms-kit-pro/faq.md b/docs/en/modules/cms-kit-pro/faq.md index 23d90d97056..230b9578ccd 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: ![cmskit-module-faq-widget](../../images/cmskit-module-faq-widget.png) ## 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-page](../../images/cmskit-module-faq-page.png) ![faq-edit-page](../../images/cmskit-module-faq-edit-page.png) ![faq-edit-question-page](../../images/cmskit-module-faq-edit-question-page.png) -## 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 2aec352caba..13c5f416f71 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 30970116582..6f2837fecd3 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: ![cmskit-module-newsletter-widget](../../images/cmskit-module-newsletter-widget.png) ## 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: ![newsletter-page](../../images/cmskit-module-newsletter-page.png) #### 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`: ![manage-email-preferences](../../images/manage-email-preferences.png) ## 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 559ed98a3af..495f7b66695 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. ![page-feedback-page](../../images/cmskit-module-page-feedback-page.png) ![page-feedback-view-page](../../images/cmskit-module-page-feedback-view-page.png) @@ -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 @@ -96,36 +189,124 @@ There are 2 types of pages that are supported by default. You can define a pre-p ``` ```csharp + using System; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc; + using Microsoft.Extensions.Options; + using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + using Volo.Payment; + using Volo.Payment.Requests; + public class PreCheckoutModel : AbpPageModel { - [BindProperty] public Guid PaymentRequestId { get; set; } + private readonly IOptions _paymentWebOptions; + private readonly IPaymentRequestAppService _paymentRequestAppService; + + [BindProperty] + public Guid PaymentRequestId { get; set; } + + public PreCheckoutModel( + IOptions paymentWebOptions, + IPaymentRequestAppService paymentRequestAppService) + { + _paymentWebOptions = paymentWebOptions; + _paymentRequestAppService = paymentRequestAppService; + } public virtual ActionResult OnGet() { - // GET operation is not supported here. All the selected gateway requests will be sent as POST. return BadRequest(); } - public virtual async Task OnPostAsync() + public virtual IActionResult OnPost() { - // You can get the payment request from the database by using `PaymentRequestId` and render something on the UI side. + return Page(); } - public virtual async Task OnPostContinueToCheckout() + public virtual async Task OnPostContinueToCheckoutAsync() { - return Redirect("the-actual-checkout-link-of-gateway"); + await _paymentWebOptions.SetAsync(); + var rootUrl = _paymentWebOptions.Value.RootUrl.TrimEnd('/'); + + var result = await _paymentRequestAppService.StartAsync( + "MyGateway", + new PaymentRequestStartDto + { + PaymentRequestId = PaymentRequestId, + ReturnUrl = rootUrl + "/MyGateway/PostCheckout", + CancelUrl = rootUrl + }); + + return Redirect(result.CheckoutLink); } } ``` -- Create **PostCheckout.cshtml** and **PostCheckout.cshtml.cs** + The gateway selection page preserves the POST method when it redirects to the pre-payment URL. `OnPost` handles that initial request and displays the page. The `ContinueToCheckout` handler starts the payment only after the user submits the pre-payment form. + +- Create **Pages/MyGateway/PostCheckout.cshtml** and **Pages/MyGateway/PostCheckout.cshtml.cs**. ```html + @page "/MyGateway/PostCheckout" @model PostCheckoutModel

Operation Done

``` + ```csharp + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc; + using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + using Volo.Payment.Requests; + + [IgnoreAntiforgeryToken] + public class PostCheckoutModel : AbpPageModel + { + private readonly IPaymentRequestAppService _paymentRequestAppService; + + public PostCheckoutModel( + IPaymentRequestAppService paymentRequestAppService) + { + _paymentRequestAppService = paymentRequestAppService; + } + + public virtual async Task OnGetAsync() + { + var parameters = Request.Query.ToDictionary( + item => item.Key, + item => item.Value.ToString()); + + return await CompleteAsync(parameters); + } + + public virtual async Task OnPostAsync() + { + var form = await Request.ReadFormAsync(); + var parameters = form.ToDictionary( + item => item.Key, + item => item.Value.ToString()); + + return await CompleteAsync(parameters); + } + + private async Task CompleteAsync( + Dictionary parameters) + { + var paymentRequest = await _paymentRequestAppService.CompleteAsync( + "MyGateway", + parameters); + + return paymentRequest.State == PaymentRequestState.Completed + ? Page() + : BadRequest("The payment was not completed."); + } + } + ``` + + Keep only the callback method used by your provider. If the provider sends an external POST request to the Razor Page, keep `[IgnoreAntiforgeryToken]` and rely on the gateway's provider-signature validation instead of an antiforgery token. Redirect to a success page only after `CompleteAsync` returns the `Completed` state. + - Configure your pages using `PaymentWebOptions` in **Web** layer of your project. ```csharp diff --git a/docs/en/modules/payment.md b/docs/en/modules/payment.md index 487e0dc64de..193580dac27 100644 --- a/docs/en/modules/payment.md +++ b/docs/en/modules/payment.md @@ -11,7 +11,8 @@ Payment module implements payment gateway integration of an application. It provides one time payment and recurring payment options. -* Supports [Stripe](https://stripe.com/), [PayPal](https://www.paypal.com/), [2Checkout](https://www.2checkout.com/), [PayU](https://corporate.payu.com/), [Iyzico](https://www.iyzico.com/en) and [Alipay](https://global.alipay.com/) payment gateways. +* Supports [Stripe](https://stripe.com/), [PayPal](https://www.paypal.com/), [2Checkout](https://www.2checkout.com/), [PayU](https://corporate.payu.com/), [Iyzico](https://www.iyzico.com/en), and [Alipay](https://global.alipay.com/) payment gateways. +* All listed gateways support one-time payments. Stripe also supports subscriptions. See [the module description page](https://abp.io/modules/Volo.Payment) for an overview of the module features. @@ -22,7 +23,7 @@ The Payment module is not installed in [the startup templates](../solution-templ ### Using ABP CLI -ABP CLI allows adding a module to a solution using ```add-module``` command. You can check its [documentation](../cli#add-module) for more information. So, payment module can be added using the command below; +Use the ABP CLI `add-module` command to add the Payment module to an existing solution: ```bash abp add-module Volo.Payment @@ -32,9 +33,9 @@ abp add-module Volo.Payment If you modified your solution structure, adding a module using ABP CLI might not work for you. In such cases, the Payment module can be added to a solution manually. -In order to do that, add packages listed below to matching project on your solution. For example, ```Volo.Payment.Application``` package to your **{ProjectName}.Application.csproj** like below; +In order to do that, add packages listed below to matching project on your solution. For example, `Volo.Payment.Application` package to your **{ProjectName}.Application.csproj** like below; -```json +```xml ``` @@ -53,7 +54,7 @@ After adding the package reference, open the module class of the project (eg: `{ ### Supported Gateway Packages -In order to use a Payment Gateway, you need to add related NuGet packages to your related project as explained in Manual Installation section above and add ```DependsOn``` to your related module. For example, if you don't want to use PayU, you don't have to use its NuGet packages. +In order to use a Payment Gateway, you need to add related NuGet packages to your related project as explained in Manual Installation section above and add `DependsOn` to your related module. For example, if you don't want to use PayU, you don't have to use its NuGet packages. After adding packages of a payment gateway to your application, you also need to configure global payment module options and options for the payment modules you have added. See the Options section below. @@ -75,35 +76,33 @@ The Payment module provides both **public pages** (for payment processing) and * ### MVC / Razor Pages UI -For MVC/Razor Pages applications, the `abp add-module Volo.Payment` command automatically adds the required packages (`Volo.Payment.Web` and gateway-specific Web packages) and the necessary `DependsOn` statements to your module. The only thing you need to do is configure `PaymentWebOptions` as explained in the [PaymentWebOptions](#paymentweboptions) section. +For MVC/Razor Pages applications, add `Volo.Payment.Web` and the Web package for each gateway you want to use, then add the corresponding module dependencies. Configure `PaymentWebOptions` as explained in the [PaymentWebOptions](#paymentweboptions) section. ### Blazor UI -For Blazor applications, the `abp add-module Volo.Payment` command automatically adds the required packages (`Volo.Payment.Blazor.Server` or `Volo.Payment.Blazor.WebAssembly` and gateway-specific Blazor packages) and the necessary `DependsOn` statements to your module. The only thing you need to do is configure `PaymentBlazorOptions` as explained below. +For Blazor applications, add the core Blazor package and the packages for the gateways you want to use, then add the corresponding module dependencies. Configure `PaymentBlazorOptions` as explained below. #### Installation -> **Note:** If you used the `abp add-module Volo.Payment` command to install the Payment module, the following packages and module dependencies are automatically added to your project. You can skip to the [Gateway-Specific Blazor Packages](#gateway-specific-blazor-packages) section. The information below is provided for reference or manual installation scenarios. - To use the Payment module's public pages in a Blazor application, you need to install the core Blazor packages and the gateway-specific Blazor packages for each payment gateway you want to support. ##### Core Blazor Packages For **Blazor Server** applications, add the following package to your **{ProjectName}.Blazor.Server.csproj** (or **{ProjectName}.Blazor.csproj** for Blazor Web App): -```json +```xml ``` For **Blazor WebAssembly** applications, add the following package to your **{ProjectName}.Blazor.csproj** (or **{ProjectName}.Blazor.Client.csproj** for Blazor Web App): -```json +```xml ``` -#### Gateway-Specific Blazor Packages +##### Gateway-Specific Blazor Packages -Each payment gateway requires its own Blazor package. Add the packages for the gateways you want to support: +Each supported Blazor gateway integration requires its own package. Add the packages for the integrations you want to use: **Stripe:** - Blazor Server: `Volo.Payment.Stripe.Blazor.Server` @@ -114,8 +113,8 @@ Each payment gateway requires its own Blazor package. Add the packages for the g - Blazor WebAssembly: `Volo.Payment.PayPal.Blazor.WebAssembly` **PayU:** -- Blazor Server: `Volo.Payment.PayU.Blazor.Server` -- Blazor WebAssembly: `Volo.Payment.PayU.Blazor.WebAssembly` +- Blazor Server: `Volo.Payment.Payu.Blazor.Server` +- Blazor WebAssembly: `Volo.Payment.Payu.Blazor.WebAssembly` **Iyzico:** - Blazor Server: `Volo.Payment.Iyzico.Blazor.Server` @@ -177,55 +176,32 @@ Configure(options => }); ``` -You can also configure these options in your `appsettings.json` file: +##### Gateway-Specific Blazor Options -```json -{ - "Payment": { - "Blazor": { - "RootUrl": "https://localhost:44300", - "CallbackUrl": "https://localhost:44300/PaymentSucceed", - "GatewaySelectionCheckoutButtonStyle": "btn btn-primary" - } - } -} -``` +Each supported Blazor gateway integration has its own options for customizing the UI. The gateway modules bind them from the following configuration sections: -##### Gateway-Specific Blazor Options +| Gateway | Blazor configuration section | +| --- | --- | +| PayU | `Payment:PayuBlazor` | +| PayPal | `Payment:PayPalBlazor` | +| Iyzico | `Payment:IyzicoBlazor` | +| Alipay | `Payment:AlipayBlazor` | +| Stripe | `Payment:Stripe` | +| TwoCheckout | `Payment:TwoCheckout` | -Each payment gateway has its own Blazor options for customizing the UI. These options can be configured in `appsettings.json`: +For example: ```json { "Payment": { - "Blazor": { - "Payu": { - "PrePaymentCheckoutButtonStyle": "btn btn-success", - "Recommended": true, - "ExtraInfos": ["Fast checkout", "Secure payment"] - }, - "TwoCheckout": { - "Recommended": false, - "ExtraInfos": ["International payments"] - }, - "PayPal": { - "Recommended": true, - "ExtraInfos": ["Pay with PayPal balance", "Buyer protection"] - }, - "Stripe": { - "Recommended": true, - "ExtraInfos": ["Credit/Debit cards", "Apple Pay", "Google Pay"] - }, - "Iyzico": { - "PrePaymentCheckoutButtonStyle": "btn btn-primary", - "Recommended": false, - "ExtraInfos": ["Turkish payment gateway"] - }, - "Alipay": { - "PrePaymentCheckoutButtonStyle": "btn btn-info", - "Recommended": false, - "ExtraInfos": ["Chinese payment gateway", "CNY only"] - } + "PayuBlazor": { + "PrePaymentCheckoutButtonStyle": "btn btn-success", + "Recommended": true, + "ExtraInfos": ["Fast checkout", "Secure payment"] + }, + "Stripe": { + "Recommended": true, + "ExtraInfos": ["Credit/Debit cards", "Apple Pay", "Google Pay"] } } } @@ -292,7 +268,7 @@ var paymentRequest = await PaymentRequestAppService.CreateAsync( TotalPrice = 60 } }, - ExtraProperties = new ExtraPropertyDictionary + ExtraProperties = { // For Iyzico - Customer information { "Name", "John" }, @@ -302,7 +278,7 @@ var paymentRequest = await PaymentRequestAppService.CreateAsync( { "City", "Istanbul" }, { "Country", "Turkey" }, { "ZipCode", "34000" }, - + // For PayU - Customer information { "BuyerName", "John" }, { "BuyerSurname", "Doe" }, @@ -313,41 +289,51 @@ var paymentRequest = await PaymentRequestAppService.CreateAsync( #### Handling the Callback (Optional) -When a user completes a payment on the external payment gateway, the following flow occurs: +When a payment provider redirects the user back to the application, the following flow occurs: 1. The user is redirected to the **PostPayment page** (handled internally by the payment module) -2. The PostPayment page validates the payment with the gateway and updates the payment request status to **Completed** +2. The PostPayment page validates the provider response and updates the payment request state 3. If a `CallbackUrl` is configured in `PaymentBlazorOptions`, the user is then redirected to that URL with the `paymentRequestId` as a query parameter -Create a page to handle this callback and perform any application-specific actions: +The callback URL is a browser navigation target, not proof that the payment succeeded. Query the payment request and check its state before showing a result: ```csharp @page "/PaymentSucceed" -@using Microsoft.AspNetCore.WebUtilities +@using Microsoft.AspNetCore.Components +@using Volo.Payment.Requests +@inject IPaymentRequestAppService PaymentRequestAppService -

Payment Successful!

-

Thank you for your purchase.

-

Payment Request ID: @PaymentRequestId

+@if (PaymentRequest is null) +{ +

Payment request not found.

+} +else if (PaymentRequest.State == PaymentRequestState.Completed) +{ +

Payment Successful!

+

Thank you for your purchase.

+} +else +{ +

The payment was not completed.

+} @code { [Parameter] [SupplyParameterFromQuery] public Guid? PaymentRequestId { get; set; } - - protected override async Task OnInitializedAsync() + + private PaymentRequestWithDetailsDto PaymentRequest { get; set; } + + protected override async Task OnParametersSetAsync() { - if (PaymentRequestId.HasValue) - { - // The payment is already completed at this point. - // Perform application-specific actions here: - // e.g., activate subscription, send confirmation email, - // update order status, grant access to purchased content, etc. - } + PaymentRequest = PaymentRequestId.HasValue + ? await PaymentRequestAppService.GetAsync(PaymentRequestId.Value) + : null; } } ``` -> **Note:** By the time the user reaches your callback page, the payment request status has already been set to **Completed** by the PostPayment page. Your callback page is for performing additional application-specific logic. It is also your responsibility to handle if a payment request is used more than once. If you have already delivered your product for a given `PaymentRequestId`, you should not deliver it again when the callback URL is visited a second time. +Keep the callback page limited to displaying the current result because users can revisit a callback URL. `PaymentRequestCompletedEto` is published when `IPaymentRequestAppService.CompleteAsync` completes a request, so an idempotent handler can fulfill orders for flows that finish through that application service. It isn't emitted for every direct request-state update; for example, the built-in Stripe webhook can complete a request without publishing this event. If your gateway can complete payments only through a webhook, add an application-owned, idempotent fulfillment or reconciliation path for that trusted webhook flow and verify the persisted payment-request state before granting access. ### Angular UI @@ -355,38 +341,61 @@ For Angular applications, you need to read and apply the steps explained in the #### Configurations -In order to configure the application to use the payment module, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root configuration. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration: +Add `providePaymentAdminConfig` from `@volo/abp.ng.payment/admin/config` to the root application configuration. The same example shows the optional remote endpoint entries when the public and admin APIs are hosted separately: -```js +```typescript // app.config.ts -import { ApplicationConfig, importProvidersFrom } from '@angular/core'; -import { PaymentAdminConfigModule } from '@volo/abp.ng.payment/admin/config'; +import { ApplicationConfig } from '@angular/core'; +import { providePaymentAdminConfig } from '@volo/abp.ng.payment/admin/config'; export const appConfig: ApplicationConfig = { providers: [ // ... - importProvidersFrom([ - PaymentAdminConfigModule.forRoot() - ]), + providePaymentAdminConfig(), ], }; +// environment.ts +export const environment = { + apis: { + default: { + url: 'https://localhost:44300', + }, + AbpPaymentCommon: { + url: 'https://localhost:44301', + }, + AbpPaymentAdmin: { + url: 'https://localhost:44302', + }, + }, +}; ``` -The payment admin module should be imported and lazy-loaded in your routing array as below: +`AbpPaymentCommon` is used by the payment-request proxies and is the server-side remote service name for the gateway endpoints. The currently published Angular gateway proxy, which is used by the public gateway-selection component, sends gateway requests through `AbpPaymentAdmin`; administration proxies also use `AbpPaymentAdmin`. When the APIs are hosted separately, configure both entries and expose `/api/payment/gateways` through the `AbpPaymentAdmin` URL until the client and server remote service names are aligned. Each missing entry independently falls back to `default.url`. -```js +Lazy-load both the admin and public payment routes under the `payment` path: + +```typescript // app.routes.ts +import { Routes } from '@angular/router'; + const APP_ROUTES: Routes = [ // ... { - path: 'payment', - loadChildren: () => - import('@volo/abp.ng.payment/admin').then(c => c.createRoutes()), + path: 'payment', + loadChildren: () => + Promise.all([ + import('@volo/abp.ng.payment/admin').then(c => c.createRoutes()), + import('@volo/abp.ng.payment').then(c => c.createRoutes()), + ]).then(([adminRoutes, publicRoutes]) => [...adminRoutes, ...publicRoutes]), }, ]; ``` +The public route factory adds `gateway-selection`, `:gateway/prepayment`, and `:gateway/post-payment`. The admin route factory adds plans, gateway plans, requests, and payment request products. + +The public package exposes replaceable component keys through `ePaymentComponents`. Use `registerPrePaymentComponent` or `registerPostPaymentComponent` with `ReplaceableComponentsService` to replace a gateway-specific payment page. The admin `createRoutes` method accepts `PaymentConfigOptions` for entity action, toolbar action, entity property, create form, and edit form contributors on the plans and gateway plans pages. + ### Pages #### Public Pages @@ -425,231 +434,234 @@ This page lists all the payment request operations in the application. `PaymentOptions` is used to store list of payment gateways. You don't have to configure this manually for existing payment gateways. You can, however, add a new gateway like below; +Add `using Volo.Abp.Localization;` to the module class file for `FixedLocalizableString`. + ````csharp Configure(options => { - options.Gateways.Add( - new PaymentGatewayConfiguration( - "MyPaymentGatewayName", - new FixedLocalizableString("MyPaymentGatewayName"), - typeof(MyPaymentGateway) - ) - ); + options.Gateways.Add( + new PaymentGatewayConfiguration( + "MyPaymentGatewayName", + new FixedLocalizableString("MyPaymentGatewayName"), + isSubscriptionSupported: false, + typeof(MyPaymentGateway) + ) + ); }); ```` -`AbpIdentityAspNetCoreOptions` properties: +`PaymentOptions` properties: -* `PaymentGatewayConfigurationDictionary`: List of gateway configuration. - * ```Name```: Name of payment gateway. - * ```DisplayName```: DisplayName of payment gateway. - * ```PaymentGatewayType```: type of payment gateway. - * ```Order```: Order of payment gateway. +* `Gateways`: Dictionary of gateway configurations keyed by gateway name. + * `Name`: Name of payment gateway. + * `DisplayName`: DisplayName of payment gateway. + * `IsSubscriptionSupported`: Whether the gateway can process recurring payment products. + * `PaymentGatewayType`: type of payment gateway. + * `Order`: Order of payment gateway. ### PaymentWebOptions -```PaymentWebOptions``` is used to configure web application related configurations. +`PaymentWebOptions` is used to configure web application related configurations. -* ```CallbackUrl```: Final callback URL for internal payment gateway modules to return. User will be redirected to this URL on your website. -* ```RootUrl```: Root URL of your website. -* ```GatewaySelectionCheckoutButtonStyle```: CSS style to add Checkout button on gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. -* ```PaymentGatewayWebConfigurationDictionary```: Used to store web related payment gateway configuration. - * ```Name```: Name of payment gateway. - * ```PrePaymentUrl```: URL of the page before redirecting user to payment gateway for payment. - * ```PostPaymentUrl```: URL of the page when user redirected back from payment gateway to your website. This page is used to validate the payment mostly. - * ```Order```: Order of payment gateway for gateway selection page. - * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. - * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `CallbackUrl`: Final callback URL for internal payment gateway modules to return. A redirect to this URL is not proof of a successful payment. Query the payment request and check its state before displaying the result. +* `RootUrl`: Root URL of your website. +* `GatewaySelectionCheckoutButtonStyle`: CSS style to add Checkout button on gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* `Gateways`: Used to store web related payment gateway configurations. + * `Name`: Name of payment gateway. + * `PrePaymentUrl`: URL of the page before redirecting user to payment gateway for payment. + * `PostPaymentUrl`: URL of the page when user redirected back from payment gateway to your website. This page is used to validate the payment mostly. + * `Order`: Order of payment gateway for gateway selection page. + * `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. + * `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PaymentBlazorOptions -```PaymentBlazorOptions``` is used to configure Blazor application related configurations. This is the Blazor equivalent of `PaymentWebOptions`. +`PaymentBlazorOptions` is used to configure Blazor application related configurations. This is the Blazor equivalent of `PaymentWebOptions`. -* ```CallbackUrl```: Final callback URL for internal payment gateway modules to return. User will be redirected to this URL on your website after a successful payment. -* ```RootUrl```: Root URL of your Blazor application. -* ```GatewaySelectionCheckoutButtonStyle```: CSS style to add to the Checkout button on the gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. -* ```PaymentGatewayBlazorConfigurationDictionary```: Used to store Blazor related payment gateway configuration. - * ```Name```: Name of payment gateway. - * ```PrePaymentUrl```: URL of the Blazor page before redirecting user to payment gateway for payment. - * ```PostPaymentUrl```: URL of the Blazor page when user is redirected back from payment gateway to your website. - * ```Order```: Order of payment gateway for gateway selection page. - * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. - * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `CallbackUrl`: Final callback URL for internal payment gateway modules to return. A redirect to this URL is not proof of a successful payment. Query the payment request and check its state before displaying the result. +* `RootUrl`: Root URL of your Blazor application. +* `GatewaySelectionCheckoutButtonStyle`: CSS style to add to the Checkout button on the gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* `Gateways`: Used to store Blazor related payment gateway configurations. + * `Name`: Name of payment gateway. + * `PrePaymentUrl`: URL of the Blazor page before redirecting user to payment gateway for payment. + * `PostPaymentUrl`: URL of the Blazor page when user is redirected back from payment gateway to your website. + * `Order`: Order of payment gateway for gateway selection page. + * `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. + * `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayuOptions -```PayuOptions``` is used to configure PayU payment gateway options. +`PayuOptions` is used to configure PayU payment gateway options. -* ```Merchant```: Merchant code for PayU account. -* ```Signature```: Signature of Merchant. -* ```LanguageCode```: Language of the order. This will be used for notification email that are sent to the client, if available. -* ```CurrencyCode```: Currency code of order (USD, EUR, etc...). -* ```VatRate```: Vat rate of order. -* ```PriceType```: Price type of order (GROSS or NET). -* ```Shipping```: A positive number indicating the price of shipping. -* ```Installment```: The number of installments. It can be an integer between 1 and 12. -* ```TestOrder```: Is the order a test order or not (true or false). -* ```Debug```: Writes detailed log on PAYU side. +* `CheckoutLink`: PayU checkout URL. Its default value is `https://secure.payu.ro/order/lu.php`. +* `Merchant`: Merchant code for PayU account. +* `Signature`: Signature of Merchant. +* `LanguageCode`: Language of the order. This will be used for notification email that are sent to the client, if available. +* `VatRate`: Vat rate of order. +* `PriceType`: Price type of order (GROSS or NET). +* `Shipping`: A positive number indicating the price of shipping. +* `Installment`: The number of installments. It can be an integer between 1 and 12. +* `TestOrder`: Is the order a test order or not (true or false). +* `Debug`: Writes detailed log on PAYU side. ### PayuWebOptions -```PayuWebOptions``` is used to configure PayU payment gateway web options. +`PayuWebOptions` is used to configure PayU payment gateway web options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `PrePaymentCheckoutButtonStyle`: CSS style to add to the Checkout button on the PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. ### PayuBlazorOptions -```PayuBlazorOptions``` is used to configure PayU payment gateway Blazor options. +`PayuBlazorOptions` is used to configure PayU payment gateway Blazor options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `PrePaymentCheckoutButtonStyle`: CSS style to add to the Checkout button on the PayU prepayment page. ### TwoCheckoutOptions -```TwoCheckoutOptions``` is used to configure TwoCheckout payment gateway options. +`TwoCheckoutOptions` is used to configure TwoCheckout payment gateway options. -* ```Signature```: Signature of Merchant's 2Checkout account. -* ```CheckoutUrl```: 2Checkout checkout URL (it must be set to https://secure.2checkout.com/order/checkout.php). -* ```LanguageCode```: Language of the order. This will be used for notification email that are sent to the client, if available. -* ```CurrencyCode```: Currency code of order (USD, EUR, etc...). +* `Signature`: Signature of Merchant's 2Checkout account. +* `CheckoutUrl`: 2Checkout checkout URL (it must be set to https://secure.2checkout.com/order/checkout.php). +* `LanguageCode`: Language of the order. This will be used for notification email that are sent to the client, if available. ### TwoCheckoutWebOptions -```TwoCheckoutWebOptions``` is used to configure TwoCheckout payment gateway web options. +`TwoCheckoutWebOptions` is used to configure TwoCheckout payment gateway web options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### TwoCheckoutBlazorOptions -```TwoCheckoutBlazorOptions``` is used to configure TwoCheckout payment gateway Blazor options. +`TwoCheckoutBlazorOptions` is used to configure TwoCheckout payment gateway Blazor options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### StripeOptions -```StripeOptions```: is used to configure Stripe payment gateway options. +`StripeOptions`: is used to configure Stripe payment gateway options. -* ```PublishableKey```: Publishable Key for Stripe account. -* ```SecretKey```: Secret Key for Stripe account. +* `PublishableKey`: Publishable Key for Stripe account. +* `SecretKey`: Secret Key for Stripe account. * `WebhookSecret`: Used for handling webhooks. You can get if from [Stripe Dashboard](https://dashboard.stripe.com/webhooks). If you don't use subscription & recurring payment it's not necessary. -* ```Currency```: Currency code of order (USD, EUR, etc..., see [Stripe docs](https://stripe.com/docs/currencies) for the full list). Its default value is USD. -* ```Locale```: Language of the order. Its default value is 'auto'. -* ```PaymentMethodTypes```: A list of the types of payment methods (e.g., card) this Checkout session can accept. See https://stripe.com/docs/payments/checkout/payment-methods. Its default value is 'card'. +* `Currency`: Currency code of order (USD, EUR, etc..., see [Stripe docs](https://stripe.com/docs/currencies) for the full list). Its default value is USD. +* `Locale`: Language of the order. Its default value is 'auto'. +* `PaymentMethodTypes`: A list of the types of payment methods (e.g., card) this Checkout session can accept. See https://stripe.com/docs/payments/checkout/payment-methods. Its default value is 'card'. ### StripeWebOptions -```StripeWebOptions``` is used to configure Stripe payment gateway web options. +`StripeWebOptions` is used to configure Stripe payment gateway web options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### StripeBlazorOptions -```StripeBlazorOptions``` is used to configure Stripe payment gateway Blazor options. +`StripeBlazorOptions` is used to configure Stripe payment gateway Blazor options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayPalOptions -```PayPalOptions``` is used to configure PayPal payment gateway options. +`PayPalOptions` is used to configure PayPal payment gateway options. -* ```ClientId```: Client Id for the PayPal account. -* ```Secret``` Secret for the PayPal account. -* ```CurrencyCode```: Currency code of order (USD, EUR, etc...). -* ```Environment```: Payment environment. ("Sandbox" or "Live", default value is "Sandbox") -* ```Locale```: PayPal-supported language and locale to localize PayPal checkout pages. See https://developer.paypal.com/docs/api/reference/locale-codes/. +* `ClientId`: Client Id for the PayPal account. +* `Secret`: Secret for the PayPal account. +* `Environment`: Payment environment. ("Sandbox" or "Live", default value is "Sandbox") +* `Locale`: PayPal-supported language and locale to localize PayPal checkout pages. See https://developer.paypal.com/docs/api/reference/locale-codes/. ### PayPalWebOptions -```PayPalWebOptions``` is used to configure PayPal payment gateway web options. +`PayPalWebOptions` is used to configure PayPal payment gateway web options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### PayPalBlazorOptions -```PayPalBlazorOptions``` is used to configure PayPal payment gateway Blazor options. +`PayPalBlazorOptions` is used to configure PayPal payment gateway Blazor options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. ### IyzicoOptions -```IyzicoOptions``` is used to configure Iyzico payment gateway options. +`IyzicoOptions` is used to configure Iyzico payment gateway options. -* ```BaseUrl```: Base API URL for the Iyzico (ex: https://sandbox-api.iyzipay.com). -* ```ApiKey``` Api key for the Iyzico account. -* ```SecretKey ``` Secret for the Iyzico account. -* ```Currency```: Currency code for the order (USD, EUR, GBP and TRY can be used). -* ```Locale```: Language of the order. -* ```InstallmentCount```: Installment count value. For single installment payments it should be 1 (valid values: 1, 2, 3, 6, 9, 12). +* `BaseUrl`: Base API URL for the Iyzico (ex: https://sandbox-api.iyzipay.com). +* `ApiKey`: API key for the Iyzico account. +* `SecretKey`: Secret for the Iyzico account. +* `Currency`: Currency code for the order (USD, EUR, GBP and TRY can be used). +* `Locale`: Language of the order. ### IyzicoWebOptions -```IyzicoWebOptions``` is used to configure Iyzico payment gateway web options. +`IyzicoWebOptions` is used to configure Iyzico payment gateway web options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `PrePaymentCheckoutButtonStyle`: CSS style to add to the Checkout button on the Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. ### IyzicoBlazorOptions -```IyzicoBlazorOptions``` is used to configure Iyzico payment gateway Blazor options. +`IyzicoBlazorOptions` is used to configure Iyzico payment gateway Blazor options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `PrePaymentCheckoutButtonStyle`: CSS style to add to the Checkout button on the Iyzico prepayment page. ### AlipayOptions -```AlipayOptions``` is used to configure Alipay payment gateway options。 **Alipay gateway only supports CNY currency**. +`AlipayOptions` is used to configure Alipay payment gateway options. **Alipay gateway only supports CNY currency**. -* ```Protocol```:Protocol for the Alipay (ex: https). -* ```GatewayHost```: Gateway host for the Aliapy. -* ```SignType```: Sign type for the Alipay. -* ```AppId```: AppId for the Alipay account. -* ```MerchantPrivateKey```: Merchant private key of the Alipay account. -* ```MerchantCertPath```Merchant cert path of the Alipay account. -* ```AlipayCertPath```: Alipay cert path of the Alipay account. -* ```AlipayRootCertPath```: Alipay root cert path of the Alipay account. -* ```AlipayPublicKey```: Alipay public key of the Alipay account. -* ```NotifyUrl```: Notify url of the Alipay. -* ```EncryptKey```: Encrypt key of the Alipay. +* `Protocol`: Protocol for Alipay (for example, https). +* `GatewayHost`: Gateway host for Alipay. +* `SignType`: Sign type for the Alipay. +* `AppId`: AppId for the Alipay account. +* `MerchantPrivateKey`: Merchant private key of the Alipay account. +* `MerchantCertPath`: Merchant certificate path of the Alipay account. +* `AlipayCertPath`: Alipay cert path of the Alipay account. +* `AlipayRootCertPath`: Alipay root cert path of the Alipay account. +* `AlipayPublicKey`: Alipay public key of the Alipay account. +* `NotifyUrl`: Notify url of the Alipay. +* `EncryptKey`: Encrypt key of the Alipay. -#### AlipayWebOptions +### AlipayWebOptions -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `PrePaymentCheckoutButtonStyle`: CSS style to add to the Checkout button on the Alipay prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager. -#### AlipayBlazorOptions +### AlipayBlazorOptions -```AlipayBlazorOptions``` is used to configure Alipay payment gateway Blazor options. +`AlipayBlazorOptions` is used to configure Alipay payment gateway Blazor options. -* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. -* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. -* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. +* `Recommended`: Is payment gateway recommended or not. This information is displayed on payment gateway selection page. +* `ExtraInfos`: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page. +* `PrePaymentCheckoutButtonStyle`: CSS style to add to the Checkout button on the Alipay prepayment page. > You can check the [Alipay document](https://opendocs.alipay.com/open/02np97) for more details. +Set the currency for a one-time payment with `PaymentRequestCreateDto.Currency`. The legacy `CurrencyCode` settings on PayU, PayPal, and TwoCheckout options are obsolete fallbacks. In the current TwoCheckout integration, you must still configure `CurrencyCode` because it is also used when the product price parameters are generated. + Instead of configuring options in your module class, you can configure it in your appsettings.json file like below; ```json -"Payment": { +{ + "Payment": { "Payu": { "Merchant": "TEST", "Signature": "SECRET_KEY", "LanguageCode": "en", - "CurrencyCode": "USD", - "VatRate": "0", + "VatRate": 0, "PriceType": "GROSS", - "Shipping": "0", + "Shipping": 0, "Installment": "1", "TestOrder": "1", "Debug": "1" @@ -659,12 +671,11 @@ Instead of configuring options in your module class, you can configure it in you "CheckoutUrl": "https://secure.2checkout.com/order/checkout.php", "LanguageCode": "en", "CurrencyCode": "USD", - "TestOrder": "1" + "TestOrder": true }, "PayPal": { "ClientId": "CLIENT_ID", "Secret": "SECRET", - "CurrencyCode": "USD", "Environment": "Sandbox", "Locale": "en_US" }, @@ -687,8 +698,11 @@ Instead of configuring options in your module class, you can configure it in you "MerchantPrivateKey": "MERCHANT_PRIVATE_KEY" } } +} ``` +The domain and MVC gateway modules share `Payment:Payu`, `Payment:PayPal`, `Payment:Iyzico`, `Payment:Stripe`, and `Payment:TwoCheckout`. Alipay MVC options use `Payment:AlipayWeb`. + ## Internals ### Domain layer @@ -706,19 +720,21 @@ A payment request represents a request for a payment in the application. * `State` : State of payment request (can be Waiting, Completed, Failed or Refunded). * `Currency` : Currency code of payment request (USD, EUR, etc...). * `Gateway` : Name of payment gateway used for this payment request. - * ```FailReason```: Reason for failed payment requests. + * `FailReason`: Reason for failed payment requests. + +`Complete()` moves a waiting or failed request to `Completed` and is idempotent for an already completed request. `Failed()` accepts waiting or failed requests, while `Refunded()` accepts only completed requests. ##### Plan -A plan is used for subscription payments. Contains PlanGateway list to configure each gateway. +A plan is used for subscription payments. It contains a `GatewayPlans` collection for gateway-specific configurations. - `Plan` (aggregate root): Represents a plan for recurring payments. - - `PlanGateways` (collection): List of gateway plans. - - `Name` : An optional name of plan. + - `GatewayPlans` (collection): List of gateway plans. + - `Name`: Required name of the plan. - `GatewayPlan` (entity): Represents a gateway configuration for a plan. - `PlanId`: Represents a plan belong to. - `Gateway`: Represents a gateway belong to. It has to be unique. - - `ExternalId`: Stores a unique configuration of gateway for subscrtiption, such as priceId, planId, subscriptionId or productId etc. + - `ExternalId`: Stores the gateway's external subscription configuration, such as a price ID, plan ID, subscription ID, or product ID. #### Repositories @@ -747,7 +763,7 @@ All tables/collections use the `Pay` prefix by default. Set static properties on ##### Connection string -This module uses `AbpPayment` 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 `Payment` 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. @@ -756,8 +772,7 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do ##### Tables * **PayPaymentRequests** - * **AbpRoleClaims** - * PayPaymentRequestProducts +* **PayPaymentRequestProducts** * **PayPlans** * **PayGatewayPlans** @@ -832,7 +847,7 @@ public static void ConfigureExtraProperties() ## Distributed Events -- `Volo.Payment.PaymentRequestCompleted` (**PaymentRequestCompletedEto**): Published when a payment is completed. +- `Volo.Payment.PaymentRequestCompleted` (**PaymentRequestCompletedEto**): Published by `IPaymentRequestAppService.CompleteAsync` when the resolved gateway returns a completed payment request. It isn't a notification for every direct payment-request state update; for example, the built-in Stripe webhook can complete a request without publishing this event. - `Id`: Represents PaymentRequest entity Id. - `Gateway`: Represents the gateway which payment was done with. @@ -874,7 +889,7 @@ public static void ConfigureExtraProperties() This module implements one-time payments; -* Supports [Stripe](https://stripe.com/), [PayPal](https://www.paypal.com/), [2Checkout](https://www.2checkout.com/), [PayU](https://corporate.payu.com/) and [Iyzico](https://www.iyzico.com/en) payment gateways. +* Supports [Stripe](https://stripe.com/), [PayPal](https://www.paypal.com/), [2Checkout](https://www.2checkout.com/), [PayU](https://corporate.payu.com/), [Iyzico](https://www.iyzico.com/en), and [Alipay](https://global.alipay.com/) payment gateways. You can get one-time payments from your customers using one or more payment gateways supported by the payment module. Payment module works in a very simple way for one-time payments. It creates a local payment request record and redirects customer to payment gateway (PayPal, Stripe etc...) for processing the payment. When the customer pays on the payment gateway, payment module handles the external payment gateway's response and validates the payment to see if it is really paid or not. If the payment is validated, payment module redirects customer to main application which initiated the payment process at the beginning. @@ -886,18 +901,18 @@ Each payment gateway implementation contains PrePayment and PostPayment pages. PrePayment page asks users for extra information if requested by the external payment gateway. For example, 2Checkout doesn't require any extra information, so PrePayment page for 2Checkout redirects user to 2Checkout without asking any extra information. -PostPayment page is responsible for validation of the response of the external payment gateway. When a user completes the payment, user is redirected to PostPayment page for that payment gateway and PostPayment page validates the status of the payment. If the payment is succeeded, status of the payment request is updated and user is redirected to main application. +PostPayment page is responsible for validating the response of the external payment gateway. When a user returns from the gateway, the PostPayment page validates the payment and updates the payment request state. A subsequent redirect to the main application is a browser navigation target, not proof that the payment succeeded. -Note: It is the main application's responsibility to handle if a payment request is used more than once. For example, if the PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than once manually by end users. If you have already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited a second time. +Query the payment request and check its state before displaying the result. Do not perform fulfillment from the callback page because users can revisit or forge callback URLs. For flows that finish through `IPaymentRequestAppService.CompleteAsync`, use an idempotent `PaymentRequestCompletedEto` handler. For webhook-only completion, use an application-owned, idempotent fulfillment or reconciliation path that runs after the trusted webhook is processed and verifies the persisted request state. ### Creating One-Time Payment -In order to initiate a payment process, inject `IPaymentRequestAppService`, create a payment request using it's `CreateAsync` method and redirect user to gateway selection page with the created payment request's Id. Here is a sample Razor Page code which starts a payment process on it's OnPost method. +In order to initiate a payment process, inject `IPaymentRequestAppService`, create a payment request using its `CreateAsync` method and redirect user to gateway selection page with the created payment request's Id. Here is a sample Razor Page code which starts a payment process on its OnPost method. > Redirection of the gateway selection page has to be a **POST** request. If you implement it as a **GET** request, you will get an error. You can use `LocalRedirectPreserveMethod` to keep the method as POST in the redirected request. -```c# -public class IndexModel: PageModel +```csharp +public class IndexModel : PageModel { private readonly IPaymentRequestAppService _paymentRequestAppService; @@ -919,7 +934,7 @@ public class IndexModel: PageModel Name = "LEGO Super Mario", Count = 2, UnitPrice = 60, - TotalPrice = 200 + TotalPrice = 120 } } }); @@ -929,7 +944,9 @@ public class IndexModel: PageModel } ``` -If the payment is successful, payment module will return to the configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (activating a user account, triggering a shipment start process, etc.). +`TotalPrice` is optional. When it is omitted, the module calculates it as `UnitPrice * Count`. + +The Payment module can redirect the browser to the configured `PaymentWebOptions.CallbackUrl` after processing the gateway response. Treat this URL only as a result page: query the payment request and check its state before displaying the result. `PaymentRequestCompletedEto` can drive idempotent fulfillment for flows completed through `IPaymentRequestAppService.CompleteAsync`; cover webhook-only completion with a trusted, application-owned reconciliation or fulfillment path as described above. ## Subscriptions @@ -1009,4 +1026,4 @@ public class SubscriptionModel : PageModel } ``` -> To track that subscription is continuing or canceled, you should keep the SubscriptionId, all events contain it. +> To track whether the subscription continues or is canceled, store its `ExternalSubscriptionId`. All subscription lifecycle events contain this value. diff --git a/docs/en/modules/permission-management.md b/docs/en/modules/permission-management.md index 5a80648ecb9..342f3556177 100644 --- a/docs/en/modules/permission-management.md +++ b/docs/en/modules/permission-management.md @@ -33,6 +33,87 @@ When you click *Actions* -> *Permissions* for a role, the permission management In this dialog, you can grant permissions for the selected role. The tabs in the left side represents main permission groups and the right side contains the permissions defined in the selected group. +#### Reusing the Permission Management Dialog + +The standard permission management dialog is reusable for any registered permission management provider. The provider name and key identify the object whose permissions are being managed. + +##### MVC / Razor Pages + +Use `abp.ModalManager` to open the built-in modal page: + +````javascript +var permissionModal = new abp.ModalManager( + abp.appPath + 'AbpPermissionManagement/PermissionManagementModal' +); + +permissionModal.open({ + providerName: 'R', + providerKey: roleName, + providerKeyDisplayName: roleName +}); +```` + +##### Blazor + +Add the `PermissionManagementModal` component to the page and call its `OpenAsync` method: + +````razor +@using Volo.Abp.PermissionManagement.Blazor.Components + + + +@code { + private PermissionManagementModal PermissionModal { get; set; } + + private Task OpenPermissionsAsync(string roleName) + { + return PermissionModal.OpenAsync("R", roleName, roleName); + } +} +```` + +The MudBlazor package provides the same component API in the `Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components` namespace. + +##### Angular + +Import the standalone `PermissionManagementComponent` into your component: + +````typescript +import { Component } from '@angular/core'; +import { PermissionManagementComponent } from '@abp/ng.permission-management'; + +@Component({ + selector: 'app-role-actions', + templateUrl: './role-actions.component.html', + imports: [PermissionManagementComponent], +}) +export class RoleActionsComponent { + roleName = 'admin'; + permissionsVisible = false; + + openPermissions() { + this.permissionsVisible = true; + } +} +```` + +Then add the component to the template. It owns the modal, so you only need to control its `visible` value: + +````html + + + +```` + +The reusable dialog calls `IPermissionAppService`. The provider must be registered and mapped to an authorization policy as described in the [Permission Management Providers](#permission-management-providers) section. For non-admin users, only permissions that the current user already has are editable, and update requests are filtered by the same rule. Users with the built-in admin role are not subject to this editability filter. + +Use `entityDisplayName` to customize the modal title and `hideBadges` to hide the granted-provider badges. The component emits `visibleChange`, so the two-way `[(visible)]` binding keeps the caller's visibility state synchronized. + ### Resource Permission Management Dialog In addition to standard permissions, this module provides a reusable dialog for managing **resource-based permissions** on specific resource instances. This allows administrators to grant or revoke permissions for users, roles and clients on individual resources (e.g., a specific document, project, or any entity). @@ -94,32 +175,45 @@ Use the `ResourcePermissionManagementModal` component's `OpenAsync` method to op } ```` +The MudBlazor package provides the same component API in the `Volo.Abp.PermissionManagement.Blazor.MudBlazor.Components` namespace. + #### Angular -Use the `ResourcePermissionManagementComponent`: +Import the standalone `ResourcePermissionManagementComponent` into your component: ````typescript -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { Component } from '@angular/core'; import { ResourcePermissionManagementComponent } from '@abp/ng.permission-management'; @Component({ - // ... + selector: 'app-document-actions', + templateUrl: './document-actions.component.html', + imports: [ResourcePermissionManagementComponent], }) -export class DocumentListComponent { - constructor(private modalService: NgbModal) {} - - openPermissionsModal(document: DocumentDto) { - const modalRef = this.modalService.open( - ResourcePermissionManagementComponent, - { size: 'lg' } - ); - modalRef.componentInstance.resourceName = 'MyApp.Document'; - modalRef.componentInstance.resourceKey = document.id; - modalRef.componentInstance.resourceDisplayName = document.title; +export class DocumentActionsComponent { + documentId = '42'; + documentTitle = 'Permission Management Guide'; + resourcePermissionsVisible = false; + + openPermissions() { + this.resourcePermissionsVisible = true; } } ```` +Render the component directly. It owns the modal and requires `resourceName` and `resourceKey` inputs: + +````html + + + +```` + ## IPermissionManager `IPermissionManager` is the main service provided by this module. It is used to read and change the global permission values. `IPermissionManager` is typically used by the *Permission Management Dialog*. However, you can inject it if you need to set a permission value. @@ -146,7 +240,7 @@ public class MyService : ITransientDependency } public async Task GrantUserPermissionDemoAsync( - Guid userId, string roleName, string permission) + Guid userId, string permission) { await _permissionManager .SetForUserAsync(userId, permission, true); @@ -154,6 +248,8 @@ public class MyService : ITransientDependency } ```` +The OpenIddict integration also provides `SetForClientAsync` for client permissions. + ## IResourcePermissionManager `IResourcePermissionManager` is the service for programmatically managing resource-based permissions. It is typically used by the *Resource Permission Management Dialog*. However, you can inject it when you need to grant, revoke, or query permissions for specific resource instances. @@ -243,32 +339,85 @@ public class MyService : ITransientDependency ## Cleaning Up Resource Permissions -When a resource is deleted, you should clean up its associated permissions to avoid orphaned permission records in the database. You can do this directly in your delete logic or handle it asynchronously through event handlers: +When a resource is deleted, you should clean up its associated permissions to avoid orphaned permission records in the database. Query all grants for the resource and delete the returned entities: + +````csharp +public class DocumentService : ITransientDependency, IUnitOfWorkEnabled +{ + private readonly IDocumentRepository _documentRepository; + private readonly IResourcePermissionGrantRepository _resourcePermissionGrantRepository; + + public DocumentService( + IDocumentRepository documentRepository, + IResourcePermissionGrantRepository resourcePermissionGrantRepository) + { + _documentRepository = documentRepository; + _resourcePermissionGrantRepository = resourcePermissionGrantRepository; + } + + public virtual async Task DeleteDocumentAsync(Guid id) + { + await _documentRepository.DeleteAsync(id); + + var grants = await _resourcePermissionGrantRepository.GetPermissionsAsync( + "MyApp.Document", + id.ToString() + ); + + await _resourcePermissionGrantRepository.DeleteManyAsync( + grants, + autoSave: true + ); + } +} +```` + +`IUnitOfWorkEnabled` keeps the resource deletion and grant cleanup in the same unit of work. `ResourcePermissionGrant` is a multi-tenant entity, so repository queries are scoped by the current tenant data filter. If cleanup runs from the host or a background process, switch `ICurrentTenant` to the resource owner's tenant before calling `GetPermissionsAsync` and `DeleteManyAsync`; otherwise the query will not return that tenant's grants. + +The `providerName` and `providerKey` arguments of `IResourcePermissionManager.DeleteAsync` identify one exact provider and key; a `null` provider key is not a wildcard. The Identity integration cleans up user and role grants, and the OpenIddict integration cleans up client grants when those entities are deleted. For your custom entities, you are responsible for removing all resource grants when the resource is deleted. + +## Application Service and HTTP API + +`IPermissionAppService` exposes the management operations under the `api/permission-management/permissions` route. Standard permission `GET` and `PUT` operations require the authorization policy mapped to the requested provider in `PermissionManagementOptions.ProviderPolicies`. + +Standard and resource update operations handle omitted permissions differently: + +* The standard permission `PUT` operation changes only the permission entries included in `UpdatePermissionsDto`. For non-admin users, entries the current user does not have are ignored. +* The resource permission `PUT` operation treats `UpdateResourcePermissionsDto.Permissions` as the complete desired set for the selected resource, provider, and provider key. Manageable permissions omitted from the list are revoked. Each resource permission is filtered by its `ManagementPermissionName` before it can be returned or changed. + +## Seeding Permission Grants + +Use `IPermissionDataSeeder` in a data seed contributor to add initial permission grants for a provider and key: ````csharp -public async Task DeleteDocumentAsync(Guid id) +public class MyPermissionDataSeedContributor + : IDataSeedContributor, ITransientDependency { - // Delete the document - await _documentRepository.DeleteAsync(id); - - // Clean up all permissions for this resource - await _resourcePermissionManager.DeleteAsync( - resourceName: "MyApp.Document", - resourceKey: id.ToString(), - providerName: "U", - providerKey: null // Deletes for all users - ); - - await _resourcePermissionManager.DeleteAsync( - resourceName: "MyApp.Document", - resourceKey: id.ToString(), - providerName: "R", - providerKey: null // Deletes for all roles - ); + private readonly IPermissionDataSeeder _permissionDataSeeder; + + public MyPermissionDataSeedContributor( + IPermissionDataSeeder permissionDataSeeder) + { + _permissionDataSeeder = permissionDataSeeder; + } + + public Task SeedAsync(DataSeedContext context) + { + return _permissionDataSeeder.SeedAsync( + RolePermissionValueProvider.ProviderName, + "admin", + new[] + { + "MyApp.Books", + "MyApp.Books.Create" + }, + context.TenantId + ); + } } ```` -> ABP modules automatically handle permission cleanup for their own entities. For your custom entities, you are responsible for cleaning up resource permissions when resources are deleted. +The seeder is additive and tenant-aware. It inserts grants that do not exist for the selected provider, key, and tenant; it does not revoke existing grants that are absent from the input. ## Permission Management Providers @@ -279,6 +428,8 @@ Permission Management Module is extensible, just like the [permission system](.. * `UserPermissionManagementProvider`: Manages user-based permissions. * `RolePermissionManagementProvider`: Manages role-based permissions. +The OpenIddict integration also registers a provider for client permissions. + `IPermissionManager` uses these providers when you get/set permissions. You can define your own provider by implementing the `IPermissionManagementProvider` or inheriting from the `PermissionManagementProvider` base class. **Example:** @@ -286,7 +437,9 @@ Permission Management Module is extensible, just like the [permission system](.. ````csharp public class CustomPermissionManagementProvider : PermissionManagementProvider { - public override string Name => "Custom"; + public const string ProviderName = "Custom"; + + public override string Name => ProviderName; public CustomPermissionManagementProvider( IPermissionGrantRepository permissionGrantRepository, @@ -309,10 +462,12 @@ Once you create your provider class, you should register it using the `Permissio Configure(options => { options.ManagementProviders.Add(); + options.ProviderPolicies[CustomPermissionManagementProvider.ProviderName] = + "MyApp.ManageCustomPermissions"; }); ```` -The order of the providers are important. Providers are executed in the reverse order. That means the `CustomPermissionManagementProvider` is executed first for this example. You can insert your provider in any order in the `Providers` list. +`IPermissionManager` enumerates `ManagementProviders` in registration order when it reads permission values. A write operation selects the registered provider whose `Name` matches the requested provider name. Every provider name must therefore be unique. ### Resource Permission Management Providers @@ -399,15 +554,20 @@ Permission value providers are used to determine if a permission is granted. The ### Resource Permission Value Providers -Similar to the standard permission system, you can create custom value providers for resource permissions. ABP comes with two built-in resource permission value providers: +Similar to the standard permission system, you can create custom value providers for resource permissions. ABP comes with three built-in resource permission value providers: * `UserResourcePermissionValueProvider` (`U`): Checks permissions granted directly to users * `RoleResourcePermissionValueProvider` (`R`): Checks permissions granted to roles +* `ClientResourcePermissionValueProvider` (`C`): Checks permissions granted directly to clients You can create your own custom value provider by implementing the `IResourcePermissionValueProvider` interface or inheriting from the `ResourcePermissionValueProvider` base class: ````csharp +using System; +using System.Linq; +using System.Security.Principal; using System.Threading.Tasks; +using Volo.Abp.Authorization.Permissions; using Volo.Abp.Authorization.Permissions.Resources; public class OwnerResourcePermissionValueProvider : ResourcePermissionValueProvider @@ -441,6 +601,33 @@ public class OwnerResourcePermissionValueProvider : ResourcePermissionValueProvi : PermissionGrantResult.Undefined; } + public override async Task CheckAsync( + ResourcePermissionValuesCheckContext context) + { + var permissionNames = context.Permissions + .Select(permission => permission.Name) + .Distinct() + .ToArray(); + var result = new MultiplePermissionGrantResult(permissionNames); + + var currentUserId = context.Principal?.FindUserId(); + if (currentUserId == null || + !await CheckIfUserIsOwnerAsync( + currentUserId.Value, + context.ResourceName, + context.ResourceKey)) + { + return result; + } + + foreach (var permissionName in permissionNames) + { + result.Result[permissionName] = PermissionGrantResult.Granted; + } + + return result; + } + private Task CheckIfUserIsOwnerAsync( Guid userId, string resourceName, @@ -461,6 +648,19 @@ Configure(options => }); ```` +## Database Providers + +The module uses the `AbpPermissionManagement` connection string name. `AbpPermissionManagementDbProperties.DbTablePrefix` and `DbSchema` default to the common ABP database prefix and schema, and can be changed before configuring the database model. + +With the default `Abp` prefix, the EF Core provider maps the following tables and the MongoDB provider maps collections with the same names: + +* `AbpPermissionGrants` +* `AbpResourcePermissionGrants` +* `AbpPermissionGroups` +* `AbpPermissions` + +In EF Core, the permission group and permission definition tables are mapped only when the model is configured as a host database. + ## See Also * [Authorization](../framework/fundamentals/authorization/index.md) diff --git a/docs/en/modules/saas.md b/docs/en/modules/saas.md index 7d78a4e9df9..eb527dc37bf 100644 --- a/docs/en/modules/saas.md +++ b/docs/en/modules/saas.md @@ -9,41 +9,43 @@ > You must have an [ABP Team or a higher license](https://abp.io/pricing) to use this module. -This module is used to manage your tenants and editions in multi-tenant applications; +This module is used to manage tenants and editions in multi-tenant applications: -* Manage **tenants** and **editions** in the system. A tenant is allowed to have one **edition**. -* Set **features** of tenants. -* Set **connection string** of tenants. -* Set **features** of editions and tenants. +- Manage **tenants** and **editions**. A tenant can have one edition. +- Assign application **features** to editions and tenants. +- Configure default and module-specific tenant **connection strings**. +- Control tenant activation and edition expiration. See [the module description page](https://abp.io/modules/Volo.Saas) for an overview of the module features. -## How to install +## How to Install -Saas is pre-installed in [the startup templates](../solution-templates). So, no need to manually install it. +The SaaS module is pre-installed in the [startup templates](../solution-templates), so you don't need to install it manually. ## Packages This module follows the [module development best practices guide](../framework/architecture/best-practices) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. -You can visit [SaaS module package list page](https://abp.io/packages?moduleName=Volo.Saas) to see list of packages related with this module. +See the [SaaS module package list](https://abp.io/packages?moduleName=Volo.Saas) for the related packages. ## Tenant-Edition Subscription -SaaS module implements subscribing to Editions for Tenants using Payment module. To enable it, project must contain `Volo.Saas` and `Volo.Payment` modules and these modules must be configured as shown below. +The SaaS module integrates with the Payment module to subscribe tenants to editions. The solution must contain both the `Volo.Saas` and `Volo.Payment` modules. ### Configuration -Firstly, Payment module must be configured properly: +Configure the Payment module first: + +- Install the `Volo.Payment` module: -- Install `Volo.Payment` module. ```bash abp add-module Volo.Payment ``` - _Or you can install via using ABP Studio._ -- Configure Saas module to use Payment. - + You can also install it with ABP Studio. + +- Enable the Payment integration for the SaaS module: + ```csharp Configure(options => { @@ -51,61 +53,67 @@ Firstly, Payment module must be configured properly: }); ``` - - Follow the [subscriptions](payment#subscriptions) section of [Payment Module Documentation](payment#subscriptions). Complete [enabling webhooks](payment#enabling-webhooks) and [configuring plans](payment#configuring-plans) sections. +- Complete the Payment module's [subscription](payment.md#subscriptions), [webhook](payment.md#enabling-webhooks) and [plan](payment.md#configuring-plans) configuration. -- Run the application and go to `Saas > Editions` page at your Web Application menu. +- Run the application and open the `SaaS > Editions` page. -- Create or Edit an existing Edition. **Plan** dropdown must be visible if you've done earlier steps correctly. Pick a Plan for Edition. +- Create an edition or edit an existing one, then select a Payment plan in the **Plan** field. An edition must have a plan before it can be used to create a subscription. ### Usage -SaaS module doesn't contain a public facing list page for listing editions for new customers/tenants to subscribe. First, you need to create such a page in your application. Then, when a new customer/tenant selects one of those Editions, you can create a subscription and redirect user to payment module as shown below. +The module doesn't provide a public edition catalog. Create that page in your application and call `ISubscriptionAppService` after an authenticated tenant selects an edition. The following same-process Razor Pages example derives the tenant ID from `ICurrentTenant` instead of accepting it from the request: -- Inject `ISubscriptionAppService` to create a subscription for a edition: +```csharp +[Authorize] +public class IndexModel : PageModel +{ + protected ISubscriptionAppService SubscriptionAppService { get; } + protected ICurrentTenant CurrentTenant { get; } - ```csharp - public class IndexModel : PageModel - { - protected ISubscriptionAppService SubscriptionAppService { get; } - - protected ICurrentTenant CurrentTenant { get; } - - public IndexModel( - ISubscriptionAppService subscriptionAppService, - ICurrentTenant currentTenant) - { - SubscriptionAppService = subscriptionAppService; - CurrentTenant = currentTenant; - } - - public async Task OnPostAsync(Guid editionId) - { - var paymentRequest = await SubscriptionAppService.CreateSubscriptionAsync(editionId, CurrentTenant.GetId()); - - return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + paymentRequest.Id); - } - } - -When the payment is completed successfully, the tenant and edition relation will be updated according to subscription status. Make sure Payment Gateway Web Hooks are configured properly. - -After all, payment module will redirect user to the callbackUrl if configured in [payment configuration](payment#paymentweboptions) with a paymentRequestId parameter. In this page, you can check the status of the payment request and show a success message to the user when the payment status is confirmed. Since the payment confirmation is asynchronous, you need to check the payment status repeatedly until it is confirmed. - -## User interface - -### Menu items - -SaaS module adds the following items to the "Main" menu, under the "Administration" menu item: + public IndexModel( + ISubscriptionAppService subscriptionAppService, + ICurrentTenant currentTenant) + { + SubscriptionAppService = subscriptionAppService; + CurrentTenant = currentTenant; + } + + public async Task OnPostAsync(Guid editionId) + { + var paymentRequest = await SubscriptionAppService.CreateSubscriptionAsync( + editionId, + CurrentTenant.GetId() + ); + + return LocalRedirectPreserveMethod( + "/Payment/GatewaySelection?paymentRequestId=" + paymentRequest.Id + ); + } +} +``` + +Keep this operation behind an authenticated application endpoint and never bind an arbitrary tenant ID from public input. In a tiered solution, implement this orchestration in a trusted server-side application layer; the built-in SaaS subscription HTTP endpoint requires the host-side `Saas.Editions` permission. + +Creating a payment request initializes a missing or expired `EditionEndDateUtc` to the current UTC time, but it doesn't assign the selected edition. A subscription-created event assigns the edition and period end. A subscription-updated event refreshes the period end and applies a new edition assignment when the Payment event supplies one. A cancellation keeps the edition assignment and sets its end date to the subscription's period end date. After that date, `Tenant.GetActiveEditionId()` no longer returns the edition. + +Payment confirmation is asynchronous. Configure the gateway webhooks and use the Payment module's [callback URL](payment.md#paymentweboptions) and payment-request status flow to show the final result. + +## User Interface + +### Menu Items + +The SaaS module adds a top-level **SaaS** group to the "Main" menu with the following items: * **Tenants**: Tenant management page. * **Editions**: Edition management page. -`SaasHostMenuNames` and `SaasTenantMenuNames` classes have the constants for the menu item names. +The `SaasHostMenuNames` class contains the host-side menu item name constants. The tenant-side `SaasTenantMenuNames` class currently contains only its group name. ### Pages -#### Tenant management +#### Tenant Management -Tenant page is used to manage tenants in the system. +The Tenants page is used to manage tenants in the system. ![saas-module-tenants-page](../images/saas-module-tenants-page.png) @@ -113,41 +121,55 @@ You can create a new tenant or edit a tenant in this page: ![saas-module-tenant-edit-modal](../images/saas-module-tenant-edit-modal.png) +A tenant has one of the following activation states: + +- `Active`: The tenant is active without an activation deadline. +- `ActiveWithLimitedTime`: The tenant is active through `ActivationEndDate` and becomes inactive after that time. +- `Passive`: The tenant is inactive. + +An edition assignment can also have an `EditionEndDateUtc`. The stored `EditionId` is retained after this date, but `Tenant.GetActiveEditionId()` returns `null`. This lets subscription renewals retain the previous assignment while distinguishing an expired edition. + +The module caches the dynamic edition claim used by feature resolution. `EditionDynamicClaimsPrincipalContributorCacheOptions.CacheAbsoluteExpiration` controls this distributed cache entry and defaults to one hour. Updating or deleting a tenant invalidates the entry, but the passage of `EditionEndDateUtc` alone doesn't. This cache setting doesn't control the lifetime of an already issued token or principal. + ##### Connection String -You can manage connection string of a tenant in case you want to use a separate database for a specific tenant. If you want to use Host database for a tenant, select "Use the Shared Database" option. +You can manage a tenant's connection string when it should use a separate database. Select **Use the Shared Database** to remove the tenant-specific default and module-specific connection strings. Each connection then falls back to the corresponding host-side configuration. ![saas-module-tenant-connection-strings-modal](../images/saas-module-tenant-connection-strings-modal.png) -##### Module Specific Connection Strings +##### Module-Specific Connection Strings You can also use the module-specific database connection string feature. -To use this feature, you should configure the module-specific database in the `ConfigureServices` method of your module class. For example, the following code configures the `Saas` module to use a separate database for each tenant. +To use this feature, configure the module-specific database in the `ConfigureServices` method of your module class. Only databases registered with `IsUsedByTenants = true` are available through the SaaS connection-string management API and UI. The following example makes the `Saas` database available: ```csharp -Configure(options => +Configure(options => { - options.Databases.Configure("Saas", database => + options.Databases.Configure("Saas", database => { database.IsUsedByTenants = true; }); }); ``` -You should select the "Use module specific database connection string" option, then you can determine your modules and their connection strings. Before adding you can check your connection by clicking "Check". +Select **Use module specific database connection string** to configure these databases. Use the **Check** action to validate the supplied values before saving them. + +The `Volo.Saas.EnableTenantBasedConnectionStringManagement` setting controls this feature and defaults to `true`. When it is disabled, the built-in UIs hide connection-string management, tenant creation ignores supplied connection strings, and update requests are rejected. The setting is available on the SaaS tab of the Settings page to users with the `Saas.SettingManagement` permission. + +> Tenant connection strings are sensitive. The module persists and returns the values as supplied; it doesn't encrypt them before persistence. Restrict `Saas.Tenants.ManageConnectionStrings`, protect the database and event transport, and avoid logging connection-string payloads. ![saas-module-tenant-module-specific-connection-strings-modal](../images/saas-module-tenant-module-specific-connection-strings-modal.png) ##### Tenant Features -You can set features of tenants. +You can set features for a tenant. A tenant-level value overrides the value assigned to its edition. If neither level has a value, feature resolution continues with the application's configuration and the feature's default value. ![saas-module-features-modal](../images/saas-module-features-modal.png) -#### Edition management +#### Edition Management -Editions page is used to manage the editions in your system. +The Editions page is used to manage the editions in your system. ![saas-module-editions-page](../images/saas-module-editions-page.png) @@ -155,21 +177,25 @@ You can create a new edition or edit an existing edition in this page: ![saas-module-edition-edit-modal](../images/saas-module-edition-edit-modal.png) +`EditionManager` validates edition display names for uniqueness. Before deleting an edition, you can move all of its tenants to another edition. Deleting an edition without choosing a replacement clears the edition assignment for its tenants. + ##### Edition Features -You can set features of an edition in this page: +You can set features of an edition on this page: ![saas-module-features-modal](../images/saas-module-features-modal.png) -## Data seed +## Data Seed + +Commercial startup templates include an application-level [data seed contributor](../framework/infrastructure/data-seeding.md) that calls `IEditionDataSeeder.CreateStandardEditionsAsync()` during the template's database migration and data-seeding flow. Layered applications run this flow from the `.DbMigrator` application, while no-layer applications run it from the host's database migration service. It creates the following host-side data: -This module adds some initial data (see [the data seed system](../framework/infrastructure/data-seeding.md)) to the database when you run the `.DbMigrator` application: +- A `Standard` edition, if an edition with that name doesn't already exist. -* Creates an `Standard` edition. +When integrating the SaaS module into an existing solution, call this method from your own `IDataSeedContributor` if you want the same initial edition. Referencing the SaaS module alone doesn't execute this seeder. ## Internals -### Domain layer +### Domain Layer #### Aggregates @@ -177,53 +203,75 @@ This module follows the [Entity Best Practices & Conventions](../framework/archi ##### Tenant -A tenant is generally represents a group of users who share a common access with specific privileges to the software instance. +A tenant generally represents a group of users that share access to the software with tenant-specific data and privileges. * `Tenant` (aggregate root): Represents a tenant in the system. * `TenantConnectionString` (collection): Connection strings of a tenant. ##### Edition -An edition is typically a category of features of the application. +An edition is a reusable set of application feature values that can be assigned to tenants. * `Edition` (aggregate root): Represents an edition in the system. +#### Extending the Entities + +The `Tenant` and `Edition` 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 an extra property to each entity: + +```csharp +ObjectExtensionManager.Instance.Modules() + .ConfigureSaas(saas => + { + saas.ConfigureTenant(tenant => + { + tenant.AddOrUpdateProperty("ExternalId"); + }); + + saas.ConfigureEdition(edition => + { + edition.AddOrUpdateProperty("CatalogCode"); + }); + }); +``` + +The module maps the configured extra properties through its extensible application contracts. The built-in MVC, Blazor, MudBlazor and Angular UIs consume the module entity-extension metadata and display the properties in their supported tables and forms. Use the property `UI` options or an Angular UI contributor when you need to customize visibility, order or rendering. + #### Repositories This module follows the [Repository Best Practices & Conventions](../framework/architecture/best-practices/repositories.md) guide. -Following custom repositories are defined for this module: +The following custom repositories are defined for this module: * `ITenantRepository` * `IEditionRepository` -#### Domain services +#### Domain Services This module follows the [Domain Services Best Practices & Conventions](../framework/architecture/best-practices/domain-services.md) guide. -##### Tenant manager +##### Tenant and Edition Managers -`TenantManager` is used to create tenants, change and validate name of tenants. +`TenantManager` creates tenants, changes and validates tenant names, and evaluates tenant activation. `EditionManager` validates edition display names, enforces a Payment plan for subscription editions, and moves tenants between editions. -### Application layer +### Application Layer -#### Application services +#### Application Services -* `TenantAppService` (implements `ITenantAppService`): Implements the use cases of the tenant management UI. -* `EditionAppService` (implement `IEditionAppService`): Implements the use cases of the edition management UI. -* `SubscriptionAppService` (implement`ISubscriptionAppService`): Implements the use cases of Tenant-Edition subscription. +- `TenantAppService` (implements `ITenantAppService`): Implements the tenant management use cases. +- `EditionAppService` (implements `IEditionAppService`): Implements the edition management use cases. +- `SubscriptionAppService` (implements `ISubscriptionAppService`): Creates Payment subscription requests for tenant-edition subscriptions. -### Database providers +### Database Providers #### Common -##### Table / collection prefix & schema +##### Table/Collection Prefix & Schema All tables/collections use the `Saas` prefix by default. Set static properties on the `SaasDbProperties` class if you need to change the table prefix or set a schema name (if supported by your database provider). -##### Connection string +##### Connection String -This module uses `Saas` 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 `Saas` 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. @@ -231,30 +279,53 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do ##### Tables -* **SaasTenants** - * SaasTenantConnectionStrings -* **SaasEditions** +- **SaasTenants** + - SaasTenantConnectionStrings +- **SaasEditions** + +SaaS metadata is host-side data. The EF Core model isn't added to a tenant-only database schema. #### MongoDB ##### Collections -* **SaasTenants** -* **SaasEditions** +- **SaasTenants** (connection strings are embedded in the tenant document) +- **SaasEditions** ### Permissions -See the `SaasHostPermissions` class members for all permissions defined for this module. - +All SaaS permissions are host-side permissions: + +- `Saas.SettingManagement`: Manages the SaaS settings. +- `Saas.Tenants`: Views tenant management. + - `Saas.Tenants.Create` + - `Saas.Tenants.Update` + - `Saas.Tenants.Delete` + - `Saas.Tenants.ManageFeatures` + - `Saas.Tenants.ManageConnectionStrings` + - `Saas.Tenants.SetPassword` + - `Saas.Tenants.Impersonation` + - `AuditLogging.ViewChangeHistory:Volo.Saas.Tenant` +- `Saas.Editions`: Views edition management. + - `Saas.Editions.Create` + - `Saas.Editions.Update` + - `Saas.Editions.Delete` + - `Saas.Editions.ManageFeatures` + - `AuditLogging.ViewChangeHistory:Volo.Saas.Edition` + +The two change-history permissions are disabled when [entity history](../framework/infrastructure/audit-logging.md#entity-history-selectors) isn't enabled for the corresponding entity. + +Tenant impersonation is disabled in the MVC, Blazor and MudBlazor SaaS UI options by default. See the [impersonation documentation](account/impersonation.md) for the UI and Account module configuration required to enable it. ### Angular UI #### Installation -In order to configure the application to use the saas module, you first need to import `provideSaasConfig` from `@volo/abp.ng.saas/config` to root module. Then, you will need to append it to the `appConfig` array. +Add `provideSaasConfig` from `@volo/abp.ng.saas/config` to the root application providers. It registers the menu routes, authentication filter and SaaS settings tab. -```js +```ts // app.config.ts +import { ApplicationConfig } from '@angular/core'; import { provideSaasConfig } from '@volo/abp.ng.saas/config'; export const appConfig: ApplicationConfig = { @@ -265,11 +336,13 @@ export const appConfig: ApplicationConfig = { }; ``` -The saas module should be imported and lazy-loaded in your routing configuration. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.saas`. +Lazy-load the UI with the `createRoutes` function from `@volo/abp.ng.saas`: -```js +```ts // app.routes.ts -const APP_ROUTES: Routes = [ +import { Routes } from '@angular/router'; + +export const APP_ROUTES: Routes = [ // ... { path: 'saas', @@ -283,18 +356,19 @@ const APP_ROUTES: Routes = [

Options

-You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes`: - **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. - **createFormPropContributors:** Changes create form fields. Please check [Dynamic Form Extensions for Angular](../framework/ui/angular/dynamic-form-extensions.md) for details. -- **editFormPropContributors:** Changes create form fields. Please check [Dynamic Form Extensions for Angular](../framework/ui/angular/dynamic-form-extensions.md) for details. +- **editFormPropContributors:** Changes edit form fields. Please check [Dynamic Form Extensions for Angular](../framework/ui/angular/dynamic-form-extensions.md) for details. +Each contributor map accepts the `eSaasComponents.Editions` and `eSaasComponents.Tenants` keys. #### Services / Models -Saas 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: +SaaS module services and models are generated via the `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: ```bash abp generate-proxy --module saas @@ -302,47 +376,71 @@ abp generate-proxy --module saas #### Replaceable Components -`eSaasComponents` enum provides all replaceable component keys. It is available for import from `@volo/abp.ng.saas`. +`eSaasComponents` is available for import from `@volo/abp.ng.saas`. The following keys are wired to replaceable components: -Please check [Component Replacement document](../framework/ui/angular/component-replacement.md) for details. +- `eSaasComponents.Editions`: Editions page. +- `eSaasComponents.Tenants`: Tenants page. +- `eSaasComponents.ConnectionStrings` is also exported, but the current connection-strings template isn't wired to this replacement key. +- `eSaasComponents.SetTenantPassword`: Set-tenant-password modal content. +See the [Component Replacement](../framework/ui/angular/component-replacement.md) documentation for details. #### Remote Endpoint URL -The Saas module remote endpoint URLs can be configured in the environment files. +Configure the SaaS host endpoint in the environment when it is served from a different URL: -```js +```ts export const environment = { - // other configurations + // Other configurations... apis: { default: { - url: 'default url here', + url: 'https://localhost:44300', }, SaasHost: { - url: 'SaasHost remote url here' + url: 'https://localhost:44301', }, - SaasTenant: { - url: 'SaasTenant remote url here' - }, - // other api configurations + // Other API configurations... }, }; ``` -The Saas module remote URL configurations shown above are optional. If you don't set any URLs, the `default.url` will be used as fallback. - +The `SaasHost` entry is optional. If it isn't configured, the generated SaaS Angular services use `default.url`. ## Distributed Events +### Published Events + +The tenant workflows explicitly publish the following integration events: -This module defines the following ETOs (Event Transfer Objects) to allow you to subscribe to changes on the entities of the module; +- `TenantCreatedEto` after a tenant is created. When `TenantAppService.CreateAsync` creates the tenant, the event's `Properties` dictionary contains `AdminEmail` and `AdminPassword` from the request. In the shared-user strategy, tenant creation also publishes `InviteUserToTenantRequestedEto`. +- `TenantConnectionStringUpdatedEto` for default and module-specific connection-string changes. Its `OldValue` and `NewValue` properties contain the connection-string values. +- `ApplyDatabaseMigrationsEto` when database migration is requested for a tenant. +- `InviteUserToTenantRequestedEto` and `UserPasswordChangeRequestedEto` for their corresponding tenant administration actions. -- `TenantEto` is published on changes done on a `Tenant` entity. -- `EditionEto` is published on changes done on an `Edition` entity. +> Some of these events carry passwords or connection strings. Protect the event transport and storage, restrict subscribers, and don't log or retain complete payloads. -**Example: Get notified when a new tenant has been created** +### Consumed Events +The module consumes `SubscriptionCreatedEto`, `SubscriptionUpdatedEto` and `SubscriptionCanceledEto` from the Payment module as described in [Tenant-Edition Subscription](#tenant-edition-subscription), and the following additional integration events: + +- `CreateTenantEto` creates a tenant, then publishes `TenantCreatedEto` and an `InviteUserToTenantRequestedEto` that directly adds the invited user to the tenant. +- A host-side `AppliedDatabaseMigrationsEto` publishes an `ApplyDatabaseMigrationsEto` for each tenant that has a separate connection string for the migrated database. Events that already specify a tenant ID are ignored by this handler. + +### Standard Entity Events + +The module also maps `Tenant` to `TenantEto` and `Edition` to `EditionEto` for ABP's standard distributed entity events. Registering these mappings doesn't enable automatic entity events. Add the entity types to `AbpDistributedEntityEventOptions.AutoEventSelectors` in the application that owns the SaaS data when consumers need create, update or delete notifications: + +```csharp +Configure(options => +{ + options.AutoEventSelectors.Add(); + options.AutoEventSelectors.Add(); +}); ``` + +**Example: Subscribe to the opt-in tenant-created entity event** + +```csharp public class MyHandler : IDistributedEventHandler>, ITransientDependency @@ -355,9 +453,6 @@ public class MyHandler : } ``` +See the [Distributed Event Bus](../framework/infrastructure/event-bus/distributed) documentation for delivery, handlers and pre-defined entity events. - -`TenantEto` and `EditionEto` are configured to automatically publish the events. You should configure yourself for the others. See the [Distributed Event Bus document](https://github.com/abpframework/abp/blob/rel-7.3/docs/en/Distributed-Event-Bus.md) to learn details of the pre-defined events. - -> Subscribing to the distributed events is especially useful for distributed scenarios (like microservice architecture). If you are building a monolithic application, or listening events in the same process that runs the Tenant Management Module, then subscribing to the [local events](https://github.com/abpframework/abp/blob/rel-7.3/docs/en/Local-Event-Bus.md) can be more efficient and easier. - +> Distributed events are especially useful in a distributed system. For same-process notifications in a monolithic application, the [Local Event Bus](../framework/infrastructure/event-bus/local) can be simpler. diff --git a/docs/en/modules/setting-management.md b/docs/en/modules/setting-management.md index dcb021fe149..69433162962 100644 --- a/docs/en/modules/setting-management.md +++ b/docs/en/modules/setting-management.md @@ -74,6 +74,10 @@ namespace Demo So, you can get or set a setting value for different setting value providers (Default, Global, User, Tenant... etc). +The scoped `GetOrNull...` and `GetAll...` extension methods use fallback values by default. Pass `fallback: false` when you need only the value explicitly stored for the requested provider. Setting a value to `null` clears that provider's value. + +For a non-encrypted inherited setting, setting a provider value to the same value as its fallback also clears the provider record by default. The comparison is case-insensitive. Pass `forceToSet: true` to a user- or tenant-scoped `Set...` method when you intentionally need to persist the same value as the fallback. `SetForTenantOrGlobalAsync` honors this parameter only when `tenantId` has a value; its global branch calls `SetGlobalAsync`, which does not expose the parameter. For encrypted settings, fallback-equal clearing does not apply to non-empty values because they are encrypted before comparison; an empty string can still be cleared when it equals the fallback. Pass `null` explicitly when you want to clear the provider value. + > Use the `ISettingProvider` instead of the `ISettingManager` if you only need to read the setting values, because it implements caching and supports all deployment scenarios. You can use the `ISettingManager` if you are creating a setting management UI. ### Setting Cache @@ -88,9 +92,9 @@ Setting Management module is extensible, just like the [setting system](../frame * `ConfigurationSettingManagementProvider`: Gets the value from the [IConfiguration service](../framework/fundamentals/configuration.md). It can not set the configuration value because it is not possible to change the configuration values on runtime. * `GlobalSettingManagementProvider`: Gets or sets the global (system-wide) value for a setting. * `TenantSettingManagementProvider`: Gets or sets the setting value for a tenant. -* `UserSettingManagementProvider`: Gets the setting value for a user. +* `UserSettingManagementProvider`: Gets or sets the setting value for a user. -`ISettingManager` uses the setting management providers on get/set methods. Typically, every setting management provider defines extension methods on the `ISettingManagement` service (like `SetForUserAsync` defined by the user setting management provider). +`ISettingManager` uses the setting management providers on get/set methods. Typically, every setting management provider defines extension methods on the `ISettingManager` service (like `SetForUserAsync` defined by the user setting management provider). If you want to create your own provider, implement the `ISettingManagementProvider` interface or inherit from the `SettingManagementProvider` base class: @@ -119,23 +123,43 @@ Configure(options => The order of the providers are important. Providers are executed in the reverse order. That means the `CustomSettingProvider` is executed first for this example. You can insert your provider in any order in the `Providers` list. -## See Also +## Dynamic Setting Definitions -* [Settings](../framework/infrastructure/settings.md) +The module can persist setting definitions in addition to setting values. `SettingManagementOptions` controls this behavior: + +* `SaveStaticSettingsToDatabase` is `true` by default. During application initialization, definitions contributed by code are synchronized with the `SettingDefinitions` table or collection (`AbpSettingDefinitions` by default). +* `IsDynamicSettingStoreEnabled` is `false` by default. Enable it to include persisted definitions in the setting definition store at runtime. + +```csharp +Configure(options => +{ + options.IsDynamicSettingStoreEnabled = true; +}); +``` + +Both options are disabled automatically in an ABP data migration environment. Static definitions continue to come from the application's setting definition providers when the dynamic store is disabled. ## Setting Management UI -Setting Mangement module provided the Email setting, Feature management and Timezone setting UI by default. +The MVC, Blazor and MudBlazor packages provide built-in Email and Time Zone setting groups. The Angular configuration package provides the built-in Email group. Other modules can contribute their own groups to the same page. For example, the Feature Management module contributes the Feature Management group shown below. ![EmailSettingUi](../images/setting-management-email-ui.png) > You can click the Send test email button to send a test email to check your email settings. +The Email group uses `IEmailSettingsAppService`. Reading and updating the settings requires the `SettingManagement.Emailing` permission. Sending a test email additionally requires `SettingManagement.Emailing.Test`. The operations are exposed under `/api/setting-management/emailing`, with the test operation at `/api/setting-management/emailing/send-test-email`. + +Email operations also require the `SettingManagement.Enable` feature, which is `true` by default. For tenants, they additionally require its child feature, `SettingManagement.AllowChangingEmailSettings`, which is `false` by default. The read operation does not return the stored SMTP password. On update, a blank password leaves the existing password unchanged. + ![FeatureManagementUi](../images/setting-management-feature-management-ui.png) ![TimeZoneSettingUi](../images/setting-management-time-zone-ui.png) -Setting it is extensible; You can add your tabs to this page for your application settings. +The Time Zone group uses `ITimeZoneSettingsAppService` and requires the `SettingManagement.TimeZone` permission. Its HTTP API is exposed at `/api/setting-management/timezone`; `GET /api/setting-management/timezone/timezones` returns the available IANA time zones. Updating the value to `Unspecified` clears the host-global or current-tenant value so the configured fallback is used. + +The built-in MVC, Blazor and MudBlazor contributors add this group only when `IClock.SupportsMultipleTimezone` is `true`. + +The page is extensible, so you can add groups for your application's settings. ### MVC UI @@ -180,9 +204,14 @@ Create a `BookStoreSettingPageContributor.cs` file under the `Settings` folder: The content of the file is shown below: ```csharp -public class BookStoreSettingPageContributor : ISettingPageContributor +public class BookStoreSettingPageContributor : SettingPageContributorBase { - public Task ConfigureAsync(SettingPageCreationContext context) + public BookStoreSettingPageContributor() + { + RequiredPermissions("BookStore.Settings"); + } + + public override Task ConfigureAsync(SettingPageCreationContext context) { context.Groups.Add( new SettingPageGroup( @@ -195,15 +224,13 @@ public class BookStoreSettingPageContributor : ISettingPageContributor return Task.CompletedTask; } - - public Task CheckPermissionsAsync(SettingPageCreationContext context) - { - // You can check the permissions here - return Task.FromResult(true); - } } ``` +Derive from `SettingPageContributorBase` instead of directly implementing `ISettingPageContributor`. Use `RequiredPermissions`, `RequiredFeatures` and `RequiredTenantSideFeatures` in the constructor to declare the conditions for the group. The module batches these checks before calling `ConfigureAsync`. + +`SettingPageGroup` also accepts an optional `parameter` for the view component. Groups are ordered by `order`, then by display name. + Open the `BookStoreWebModule.cs` file and add the following code: ```csharp @@ -246,8 +273,13 @@ The content of the file is shown below: ```csharp public class BookStoreSettingComponentContributor : ISettingComponentContributor { - public Task ConfigureAsync(SettingComponentCreationContext context) + public async Task ConfigureAsync(SettingComponentCreationContext context) { + if (!await CheckPermissionsAsync(context)) + { + return; + } + context.Groups.Add( new SettingComponentGroup( "Volo.Abp.MySettingGroup", @@ -256,18 +288,22 @@ public class BookStoreSettingComponentContributor : ISettingComponentContributor order : 1 ) ); - - return Task.CompletedTask; } - public Task CheckPermissionsAsync(SettingComponentCreationContext context) + public async Task CheckPermissionsAsync(SettingComponentCreationContext context) { - // You can check the permissions here - return Task.FromResult(true); + var authorizationService = context.ServiceProvider + .GetRequiredService(); + + return await authorizationService.IsGrantedAsync("BookStore.Settings"); } } ``` +The settings page calls `ConfigureAsync` for every registered contributor, while menu visibility is checked separately with `CheckPermissionsAsync`. Perform the authorization check before adding the group, as in the example, and return the same result from `CheckPermissionsAsync`. + +MudBlazor provides the equivalent `ISettingComponentContributor`, `SettingComponentCreationContext`, `SettingComponentGroup` and `SettingManagementComponentOptions` types in the `Volo.Abp.SettingManagement.Blazor.MudBlazor` namespace. Use those types for a MudBlazor UI. In both Blazor UI stacks, a group accepts an optional component `parameter` and is ordered by `order`, then by display name. + Open the `BookStoreBlazorModule.cs` file and add the following code: ```csharp @@ -293,34 +329,72 @@ Create a component with the following command: yarn ng generate component my-settings ``` -Open the `app.component.ts` and modify the file as shown below: +Register the module configuration and your setting tab in `app.config.ts`: -```js -import { Component, inject } from '@angular/core'; -import { SettingTabsService } from '@abp/ng.setting-management/config'; +```ts +import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core'; +import { + provideSettingManagementConfig, + SettingTabsService, +} from '@abp/ng.setting-management/config'; import { MySettingsComponent } from './my-settings/my-settings.component'; -@Component({ - // component metadata -}) -export class AppComponent { - private readonly settingTabs = inject(SettingTabsService); - - constructor() { - this.settingTabs.add([ - { - name: 'MySettings', - order: 1, - requiredPolicy: 'policy key here', - component: MySettingsComponent, - }, - ]); - } +function configureSettingTabs() { + const settingTabs = inject(SettingTabsService); + settingTabs.add([ + { + name: 'MySettings', + order: 1, + requiredPolicy: 'BookStore.Settings', + component: MySettingsComponent, + }, + ]); } + +export const appConfig: ApplicationConfig = { + providers: [ + provideSettingManagementConfig(), + provideAppInitializer(configureSettingTabs), + ], +}; ``` +`provideSettingManagementConfig` registers the route, the built-in Email tab and route visibility. The Administration -> Settings route is visible only when it has at least one visible tab and the `SettingManagement.Enable` feature is enabled. A tab can use `requiredPolicy` and `invisible` to control its own visibility. + +The `@abp/ng.setting-management` package exports the `SettingManagementComponent`, `createRoutes` and the `eSettingManagementComponents.SettingManagement` key. Use the key with the [component replacement system](../framework/ui/angular/component-replacement.md) when you need to replace the complete Setting Management page. `SettingManagementModule.forLazy` and `SettingManagementConfigModule.forRoot` are obsolete; use `createRoutes` and `provideSettingManagementConfig` in standalone applications. + #### Run the Application Navigate to `/setting-management` route to see the changes: ![Custom Settings Tab](../images/custom-settings.png) + +## Database Providers + +The Entity Framework Core and MongoDB packages persist setting values and setting definition records. + +### Common + +`AbpSettingManagementDbProperties` exposes the common persistence configuration: + +* `DbTablePrefix` is the prefix used for EF Core tables and MongoDB collections. It defaults to `AbpCommonDbProperties.DbTablePrefix`. +* `DbSchema` is the schema used by EF Core. MongoDB does not use this value. +* `ConnectionStringName` is `AbpSettingManagement`. Define this named connection string to place the module in a separate database; otherwise, it falls back to the `Default` connection string. + +### Entity Framework Core + +The Entity Framework Core provider maps the following tables with the default prefix: + +* `AbpSettings` +* `AbpSettingDefinitions` + +### MongoDB + +The MongoDB provider maps the following collections with the default prefix: + +* `AbpSettings` +* `AbpSettingDefinitions` + +## See Also + +* [Settings](../framework/infrastructure/settings.md) diff --git a/docs/en/modules/tenant-management.md b/docs/en/modules/tenant-management.md index a8b56d0c6bf..101c7f076fe 100644 --- a/docs/en/modules/tenant-management.md +++ b/docs/en/modules/tenant-management.md @@ -37,7 +37,7 @@ In this page, you see the all the tenants. You can create a new tenant as shown In this modal; -* **Name**: The unique name of the tenant. If you use subdomains for your tenants (like https://some-tenant.your-domain.com), this will be the subdomain name. +* **Name**: The tenant name. Tenant names are normalized by the `ITenantNormalizer` service, so duplicate-name validation is case-insensitive by default. This validation is not a database uniqueness constraint. If you use subdomains for your tenants (like https://some-tenant.your-domain.com), this will be the subdomain name. * **Admin Email Address**: Email address of the admin user for this tenant. * **Admin Password**: The password of the admin user for this tenant. @@ -55,13 +55,53 @@ The Features action opens a modal to enable/disable/set [features](../framework/ *Manage Host features* button is used to set features for the host side, if you use the features of your application also in the host side. +### Extending the Angular UI + +For a standalone Angular application, register the Tenant Management menu configuration with `provideTenantManagementConfig` from the `@abp/ng.tenant-management/config` package: + +```ts +import { ApplicationConfig } from '@angular/core'; +import { provideTenantManagementConfig } from '@abp/ng.tenant-management/config'; + +export const appConfig: ApplicationConfig = { + providers: [provideTenantManagementConfig()], +}; +``` + +Lazy-load the module routes with `createRoutes` from `@abp/ng.tenant-management`: + +```ts +import { Routes } from '@angular/router'; + +export const APP_ROUTES: Routes = [ + { + path: 'tenant-management', + loadChildren: () => + import('@abp/ng.tenant-management').then(m => m.createRoutes()), + }, +]; +``` + +`createRoutes` accepts `entityActionContributors`, `toolbarActionContributors`, `entityPropContributors`, `createFormPropContributors` and `editFormPropContributors`. See the Angular guides for [entity actions](../framework/ui/angular/entity-action-extensions.md), [page toolbars](../framework/ui/angular/page-toolbar-extensions.md), [table columns](../framework/ui/angular/data-table-column-extensions.md) and [dynamic forms](../framework/ui/angular/dynamic-form-extensions.md). + +The `eTenantManagementComponents.Tenants` key identifies the Tenants page for these contributors and for [component replacement](../framework/ui/angular/component-replacement.md). + ## Distributed Events -This module defines the following ETOs (Event Transfer Objects) to allow you to subscribe to changes on the entities of the module; +`TenantAppService.CreateAsync` explicitly publishes a `TenantCreatedEto` after it persists a new tenant. Its `Properties` dictionary contains the `AdminEmail` and plain-text `AdminPassword` values supplied by the create request. Treat this event as sensitive: protect it in transit and at rest, and do not log or retain the complete payload. The Framework's EF Core migration/seeding and MongoDB seeding handlers can consume this event when they are configured by the application. -- `TenantEto` is published on changes done on an `Tenant` entity. +`TenantCreatedEto` and `EntityCreatedEto` are separate distributed events. Enabling the automatic selector below publishes `EntityCreatedEto` in addition to the explicitly published `TenantCreatedEto`. If you subscribe to both, ensure that non-idempotent handlers distinguish between them instead of treating them as the same notification. -**Example: Get notified when a new tenant has been created** +The module also defines a `TenantEto` and pre-configures the mapping from `Tenant`. However, ABP does not automatically publish distributed entity-change events by default. Enable them in the application that owns Tenant Management if you want to subscribe to `EntityCreatedEto`, `EntityUpdatedEto` or `EntityDeletedEto`: + +```csharp +Configure(options => +{ + options.AutoEventSelectors.Add(); +}); +``` + +**Example: Subscribe to the opt-in entity-created event** ```cs public class MyHandler : @@ -76,11 +116,9 @@ public class MyHandler : } ``` +See the [Distributed Event Bus](../framework/infrastructure/event-bus/distributed) documentation for details of pre-defined entity events and automatic event selectors. - -`TenantEto` is configured to automatically publish the events. You should configure yourself for the others. See the [Distributed Event Bus document](https://github.com/abpframework/abp/blob/rel-7.3/docs/en/Distributed-Event-Bus.md) to learn details of the pre-defined events. - -> Subscribing to the distributed events is especially useful for distributed scenarios (like microservice architecture). If you are building a monolithic application, or listening events in the same process that runs the Tenant Management Module, then subscribing to the [local events](https://github.com/abpframework/abp/blob/rel-7.3/docs/en/Local-Event-Bus.md) can be more efficient and easier. +> Subscribing to distributed events is especially useful for distributed scenarios (like microservice architecture). If you are building a monolithic application, or listening for events in the same process that runs the Tenant Management module, subscribing to [local events](../framework/infrastructure/event-bus/local) can be more efficient and easier. ## Internals @@ -100,12 +138,16 @@ This section can be used as a reference if you want to [customize](../framework/ * `TenantManager` +`TenantManager` normalizes names on create and rename, and uses `ITenantValidator` to reject duplicate normalized names during those operations. + ### Application Layer #### Application Services * `TenantAppService` +In addition to tenant CRUD operations, `ITenantAppService` provides `GetDefaultConnectionStringAsync`, `UpdateDefaultConnectionStringAsync` and `DeleteDefaultConnectionStringAsync`. The HTTP API exposes these operations as `GET`, `PUT` and `DELETE` on `/api/multi-tenancy/tenants/{id}/default-connection-string`; the `PUT` request receives `defaultConnectionString` as a query parameter. + #### Permissions - `AbpTenantManagement.Tenants`: Tenant management. @@ -113,6 +155,19 @@ This section can be used as a reference if you want to [customize](../framework/ - `AbpTenantManagement.Tenants.Update`: Editing an existing tenant. - `AbpTenantManagement.Tenants.Delete`: Deleting an existing tenant. - `AbpTenantManagement.Tenants.ManageFeatures`: Manage features of the tenants. +- `AbpTenantManagement.Tenants.ManageConnectionStrings`: Read, update or delete the default connection string of a tenant. + +All Tenant Management permissions are available only on the host side. + +### Database Configuration + +`AbpTenantManagementDbProperties` exposes the common persistence configuration: + +* `DbTablePrefix` is the prefix used for EF Core tables and MongoDB collections. It defaults to the common ABP database prefix. +* `DbSchema` is the schema used by EF Core. MongoDB does not use this value. +* `ConnectionStringName` is `AbpTenantManagement`. Define this named connection string to place the module in a separate database; otherwise, it falls back to the `Default` connection string. + +See the [Connection Strings](../framework/fundamentals/connection-strings.md) documentation for named connection-string configuration and fallback behavior. ### Entity Framework Core Integration @@ -133,7 +188,9 @@ This section can be used as a reference if you want to [customize](../framework/ ## Notices -ABP allows to use *database per tenant* approach that allows a tenant can have a dedicated database. This module has the fundamental infrastructure to make that implementation possible (see its source code), however it doesn't implement the application layer and UI functionalities to provide it as an out of the box implementation. You can implement these features yourself, or consider to use the [ABP Saas Module](./saas.md) that fully implements it and provides much more business features. +ABP supports the *database per tenant* approach. This module can store a tenant's default connection string through `ITenantAppService` and the corresponding HTTP API. Its built-in MVC, Blazor, MudBlazor and Angular UIs do not provide a connection-string management screen, and changing the stored value does not create or migrate the tenant database. + +You can build your own UI and migration workflow on top of the application/API contract, or use the [ABP SaaS Module](./saas.md), which provides connection-string management and additional business features. ## See Also diff --git a/docs/en/modules/text-template-management.md b/docs/en/modules/text-template-management.md index 32b7cc31a9c..6941ec34afa 100644 --- a/docs/en/modules/text-template-management.md +++ b/docs/en/modules/text-template-management.md @@ -9,19 +9,21 @@ > You must have an [ABP Team or a higher license](https://abp.io/pricing) to use this module. -This module is used to store and edit template contents for [the text templating system](../framework/infrastructure/text-templating/index.md) of the ABP. So, you may need to understand it to better understand the purpose of this module. +This module stores and lets users edit content for ABP's [text templating system](../framework/infrastructure/text-templating/index.md). Read the text templating documentation to understand how applications define and render templates. -There are different use cases of the text templating system. For example, [the Account Module](account.md) is using it to define templates for sending emails when it needs to send emails to users (like sending "password reset link" email). This module provides UI to easily edit these email templates. +Applications use text templates for many purposes. For example, the [Account Module](account.md) defines templates for emails such as password reset messages. This module provides a UI for editing those templates. See [the module description page](https://abp.io/modules/Volo.TextTemplateManagement) for an overview of the module features. +The `TextManagement.Enable` feature is enabled by default. The module's permissions, application services and menu items require this feature. + ## How to Install -Text Template Management module is pre-installed in [the startup templates](../solution-templates). So, no need to manually install it. +The Text Template Management module is pre-installed in [the startup templates](../solution-templates), so you do not need to install it manually. ### Existing Solutions -If you want to add the **Text Template Management** module to your existing solution, you can use the ABP CLI `add-module` command: +To add the **Text Template Management** module to an existing solution, use the ABP CLI `add-module` command: ```bash abp add-module Volo.TextTemplateManagement @@ -31,7 +33,7 @@ abp add-module Volo.TextTemplateManagement This module follows the [module development best practices guide](../framework/architecture/best-practices) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them. -You can visit [Text Template Management module package list page](https://abp.io/packages?moduleName=Volo.TextTemplateManagement) to see list of packages related with this module. +Visit the [Text Template Management module package list](https://abp.io/packages?moduleName=Volo.TextTemplateManagement) to see the packages related to this module. ## User Interface @@ -47,11 +49,11 @@ Text Template Management module adds the following items to the "Main" menu, und #### Text Templates -Text Templates page is used to view the list of templates defined in the application. +The Text Templates page lists the templates defined in the application. ![text-template-management-text-templates-page](../images/text-template-management-text-templates-page.png) -Click to the `Actions -> Edit Contents` to edit content for a template. There are two types of UI to edit a template content: +Click `Actions > Edit Contents` to edit a template. The module provides two editors: ##### Editing Content for Inline Localized Templates @@ -65,23 +67,45 @@ This kind of templates provides different content for each culture. In this way, ![text-template-management-multiple-culture-edit](../images/text-template-management-multiple-culture-edit.png) +Saved content is an override for the current tenant context. The host and each tenant have separate overrides; a tenant doesn't inherit an override saved in the host context. If no database override exists, the text templating system continues with the configured content contributors, such as the template's virtual-file content. + +For a static template, the framework content provider first tries the requested regional culture and then its parent culture. An inline-localized template then falls back to its culture-independent content, while a culture-specific template falls back to its configured default culture. + +Dynamic definitions use a different lookup order. When `IsDynamicTemplateStoreEnabled` is enabled and a definition exists only in the database, `DatabaseTemplateContentContributor` tries the requested culture, the definition's default culture (or `en` when it has no default), and then culture-independent content. This lookup can return content before the framework tries the requested culture's parent. **Restore to default** deletes the current tenant context's override for the selected template and culture, after which the applicable fallback path is used again. + +### UI Extension Points + +The MVC UI uses `textTemplateManagement.textDefinition` for both [entity action extensions](../framework/ui/mvc-razor-pages/entity-action-extensions.md) and [data table column extensions](../framework/ui/mvc-razor-pages/data-table-column-extensions.md). + +The Blazorise UI exposes entity actions and table columns through the `TextTemplateManagement` page component type. See the Blazor [entity action](../framework/ui/blazor/entity-action-extensions.md) and [data table column](../framework/ui/blazor/data-table-column-extensions.md) extension documents. + ## Configure `TextTemplateManagementOptions` -`TextTemplateManagementOptions` can be used to configure the module. You can use the below code to configure it in the ConfigureServices method of your module (eg: BookStoreApplicationModule). +`TextTemplateManagementOptions` can be used to configure the module. The following example shows the default values: ```csharp Configure(options => { - options.MinimumCacheDuration = TimeSpan.FromHours(1); + options.MinimumCacheDuration = TimeSpan.FromHours(1); + options.SaveStaticTemplatesToDatabase = true; + options.IsDynamicTemplateStoreEnabled = false; }); ``` +| Property | Description | +| --- | --- | +| `MinimumCacheDuration` | Sets the sliding expiration of cached template contents. The default is one hour. | +| `SaveStaticTemplatesToDatabase` | Starts synchronizing static template definitions and their virtual-file contents to the database during application initialization. The default is `true`. | +| `IsDynamicTemplateStoreEnabled` | Enables the database-backed dynamic template-definition store. The default is `false`. Static definitions take precedence when static and dynamic definitions share the same name. | + +Both `SaveStaticTemplatesToDatabase` and `IsDynamicTemplateStoreEnabled` are disabled automatically in a data-migration environment. Disabling static synchronization doesn't disable saved content overrides. + ## Caching -[`DatabaseTemplateContentContributor`](#DatabaseTemplateContentContributor) caches template contents to increase performance. +`DatabaseTemplateContentContributor` caches template contents to increase performance. Cache entries use the `MinimumCacheDuration` sliding expiration. -You can get cache store by injecting `IDistributedCache`. +You can access the cache by injecting `IDistributedCache`. `TemplateContentCacheKey` contains the template definition name and culture; ABP's distributed cache key normalization also isolates entries by the current tenant context. The application service removes the related cache entry when it persists a new override or deletes one through **Restore to default**, so an application restart isn't required. For more information, please check the [Caching](../framework/fundamentals/caching.md) guide. @@ -93,35 +117,35 @@ It has `TemplateDefinitionName` and `Culture` properties. ## Data Seed -This module doesn't seed any data. +This module doesn't define an `IDataSeedContributor`. The optional startup synchronization controlled by `SaveStaticTemplatesToDatabase` is separate from the data seed system. ## Internals ### Domain Layer -#### Aggregates +#### Entities and Aggregate Roots This module follows the [Entity Best Practices & Conventions](../framework/architecture/best-practices/entities.md) guide. -##### TextTemplateContent +##### Domain Model Types -* `TextTemplateContent` (aggregate root): Represents a content of text template. +* `TextTemplateContent` (aggregate root): Represents a tenant-aware template content override. +* `TextTemplateDefinitionRecord` (aggregate root): Stores synchronized template definition metadata. +* `TextTemplateDefinitionContentRecord` (entity): Stores a synchronized definition's file content. #### Repositories This module follows the [Repository Best Practices & Conventions](../framework/architecture/best-practices/repositories.md) guide. -Following custom repositories are defined for this module: +The following custom repositories are defined for this module: * `ITextTemplateContentRepository` +* `ITextTemplateDefinitionRecordRepository` +* `ITextTemplateDefinitionContentRecordRepository` -#### Domain Services +#### Template Content Contributor -This module follows the [Domain Services Best Practices & Conventions](../framework/architecture/best-practices/domain-services.md) guide. - -##### DatabaseTemplateContentContributor - -`DatabaseTemplateContentContributor` is used by `ITemplateContentProvider` to get template contents that stored in DB and Cache. +`DatabaseTemplateContentContributor` is an `ITemplateContentContributor` used by `ITemplateContentProvider` to read tenant-aware content overrides and, when the dynamic definition store is enabled, synchronized definition content from the database and cache. ### Settings @@ -144,7 +168,7 @@ All tables/collections use the `Abp` prefix by default. Set static properties on ##### Connection String -This module uses `TextTemplateManagement` 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 `TextTemplateManagement` as 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. @@ -153,25 +177,32 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do ##### Tables * **AbpTextTemplateContents** +* **AbpTextTemplateDefinitionRecords** +* **AbpTextTemplateDefinitionContentRecords** #### MongoDB ##### Collections -* **AbpTextTemplateContents** +* **AbpTextTemplates** +* **AbpTextTemplateDefinitionRecords** +* **AbpTextTemplateDefinitionContentRecords** ### Permissions -See the `TextTemplateManagementPermissions` class members for all permissions defined for this module. +All permissions require the `TextManagement.Enable` feature. + +The module defines the following permissions: -The module exposes two edit-time permissions with different risk levels: +| Permission | Purpose | +| --- | --- | +| `TextTemplateManagement.TextTemplates` | View and filter template definitions and read their contents. | +| `TextTemplateManagement.TextTemplates.EditContents` | Edit and restore template content. | +| `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` | Additionally authorize editing templates rendered by a non-sandboxed engine, such as Razor. | -| Permission | Required to edit | Default grant | -|------------|------------------|---------------| -| `TextTemplateManagement.TextTemplates.EditContents` | Templates rendered by a sandboxed engine (e.g. Scriban). Editing such templates is safe for content editors because the engine cannot execute arbitrary .NET code. | Granted to roles that need to edit template text. | -| `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` | Templates rendered by a **non-sandboxed** engine (e.g. Razor). Editing such templates is functionally equivalent to granting server-side code execution because the engine compiles the content into a .NET assembly that runs with the host process's privileges. | **Not granted to any role by default**, including `admin`. Must be granted explicitly. | +Whether a template is sandboxed is determined by `ITemplateRenderingEngine.IsSandboxed` on the engine that renders it. An unknown, unregistered or unresolved default engine is treated as non-sandboxed. Editing a non-sandboxed template requires **both** `EditContents` and `EditNonSandboxedContents`. -Whether a template is sandboxed is determined by `ITemplateRenderingEngine.IsSandboxed` on the engine that renders it. Editing a non-sandboxed template requires **both** `EditContents` and `EditNonSandboxedContents`. +When the standard Permission Management data seeder runs, it grants all role-applicable permissions to the `admin` role. This includes `EditNonSandboxedContents`. If the administrators in your application shouldn't be allowed to edit executable template content, customize the seeding policy and remove any existing grant. The Text Template Management UI surfaces this distinction: @@ -185,39 +216,42 @@ The Text Template Management UI surfaces this distinction: #### Installation -In order to configure the application to use the text template management module, you first need to import `provideTextTemplateManagementConfig` from `@volo/abp.ng.text-template-management/config` to root configuration. Then, you will need to append it to the `appConfig` array. +To configure the application to use the text template management module, import `provideTextTemplateManagementConfig` from `@volo/abp.ng.text-template-management/config` and add it to the root `providers` array. -```js +```ts // app.config.ts +import { ApplicationConfig } from '@angular/core'; import { provideTextTemplateManagementConfig } from '@volo/abp.ng.text-template-management/config'; export const appConfig: ApplicationConfig = { providers: [ // ... - provideTextTemplateManagementConfig() + provideTextTemplateManagementConfig(), ], }; ``` -The text template management module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.text-template-management`. +The text template management module should be imported and lazy-loaded in your routing array. It exports a `createRoutes` function from `@volo/abp.ng.text-template-management`. Available options are listed below. -```js +```ts // app.routes.ts +import { Routes } from '@angular/router'; + const APP_ROUTES: Routes = [ // ... { path: 'text-template-management', loadChildren: () => - import('@volo/abp.ng.text-template-management').then(c => c.createRoutes(/* options here */)), + import('@volo/abp.ng.text-template-management').then(c => c.createRoutes()), }, ]; ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations implemented. +> Startup templates already include both settings, so no additional configuration is needed.

Options

-You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: +You can modify the look and behavior of the module pages by passing the following options to the `createRoutes` function: - **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. @@ -226,7 +260,7 @@ You can modify the look and behavior of the module pages by passing the followin #### Services / Models -Text Template 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: +The Text Template Management module's services and models are generated by the [ABP CLI](../cli) `generate-proxy` command. To generate the module's proxies, run the following command in the Angular project directory: ```bash abp generate-proxy --module textTemplateManagement @@ -238,6 +272,12 @@ abp generate-proxy --module textTemplateManagement `eTextTemplateManagementComponents` enum provides all replaceable component keys. It is available for import from `@volo/abp.ng.text-template-management`. +The available keys are: + +* `eTextTemplateManagementComponents.TextTemplates` +* `eTextTemplateManagementComponents.TemplateContents` +* `eTextTemplateManagementComponents.InlineTemplateContent` + Please check [Component Replacement document](../framework/ui/angular/component-replacement.md) for details. @@ -245,7 +285,7 @@ Please check [Component Replacement document](../framework/ui/angular/component- The Text Template Management module remote endpoint URL can be configured in the environment files. -```js +```ts export const environment = { // other configurations apis: { @@ -253,14 +293,14 @@ export const environment = { url: 'default url here', }, TextTemplateManagement: { - url: 'Text Template Management remote url here' - } + url: 'Text Template Management remote url here', + }, // other api configurations }, }; ``` -The Text Template Management module remote URL configuration shown above is optional. If you don't set a URL, the `default.url` will be used as fallback. +The Text Template Management module's remote URL configuration is optional. If you don't set a URL, `default.url` is used as a fallback. ## Distributed Events diff --git a/docs/en/modules/virtual-file-explorer.md b/docs/en/modules/virtual-file-explorer.md index a891e571404..352bbd05043 100644 --- a/docs/en/modules/virtual-file-explorer.md +++ b/docs/en/modules/virtual-file-explorer.md @@ -55,7 +55,7 @@ Or you can also manually install nuget package to `Acme.MyProject.Web` project: ##### 2.2- Adding NPM Package - * Open `package.json` and add `@abp/virtual-file-explorer": "^2.9.0` as shown below: + * Open `package.json` and add `@abp/virtual-file-explorer` with the same version as the other `@abp` packages, as shown below: ```json { @@ -63,8 +63,8 @@ Or you can also manually install nuget package to `Acme.MyProject.Web` project: "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^2.9.0", - "@abp/virtual-file-explorer": "^2.9.0" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0", + "@abp/virtual-file-explorer": "~10.6.0" } } ``` @@ -91,4 +91,4 @@ public override void PreConfigureServices(ServiceConfigurationContext context) options.IsEnabled = false; }); } -``` \ No newline at end of file +``` diff --git a/docs/en/multi-lingual-entities.md b/docs/en/multi-lingual-entities.md index b0419fe2a1a..43e93247f48 100644 --- a/docs/en/multi-lingual-entities.md +++ b/docs/en/multi-lingual-entities.md @@ -1,6 +1,107 @@ -# Multi Lingual Entities +```json +//[doc-seo] +{ + "Description": "Use ABP multi-lingual objects to select translations by culture, apply fallback rules, and process translations in bulk." +} +``` -This feature is still under development. -Follow the below link to get information about the development status +# Multi-Lingual Objects -https://github.com/abpframework/abp/issues/11698 +The `Volo.Abp.MultiLingualObject` package provides a contract and a selection service for objects that store one translation per language. Persistence mapping is application-specific; the package does not create a database relationship for the translations. + +## Installation + +Install the package in the project that defines the consuming module: + +```bash +abp add-package Volo.Abp.MultiLingualObject +``` + +Add `AbpMultiLingualObjectsModule` as a dependency of that module when the package is installed manually: + +````csharp +using Volo.Abp.Modularity; +using Volo.Abp.MultiLingualObjects; + +[DependsOn(typeof(AbpMultiLingualObjectsModule))] +public class MyApplicationModule : AbpModule +{ +} +```` + +## Define a Multi-Lingual Object + +Implement `IMultiLingualObject` on the object and `IObjectTranslation` on its translation type: + +```csharp +using System.Collections.Generic; +using Volo.Abp.MultiLingualObjects; + +public class Product : IMultiLingualObject +{ + public ICollection Translations { get; set; } = + new List(); +} + +public class ProductTranslation : IObjectTranslation +{ + public string Language { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; +} +``` + +`Language` stores a culture name such as `en`, `en-US` or `tr`. + +## Select a Translation + +Inject `IMultiLingualObjectManager` and call `GetTranslationAsync`: + +```csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiLingualObjects; + +public class ProductService + : ITransientDependency +{ + private readonly IMultiLingualObjectManager _multiLingualObjectManager; + + public ProductService( + IMultiLingualObjectManager multiLingualObjectManager) + { + _multiLingualObjectManager = multiLingualObjectManager; + } + + public Task GetTranslationAsync(Product product) + { + return _multiLingualObjectManager + .GetTranslationAsync(product); + } +} +``` + +With the default arguments, the manager uses `CultureInfo.CurrentUICulture.Name`. It selects translations in this order: + +1. An exact translation for the current UI culture. +2. A translation for a parent of the current UI culture when `fallbackToParentCultures` is `true`. +3. A translation for the language configured by `LocalizationSettingNames.DefaultLanguage`. +4. The first available translation. + +The method returns `null` when the collection is null or empty. To disable only the parent-culture fallback, set `fallbackToParentCultures` to `false`. The default-language and first-available fallbacks still apply. + +## Select Translations in Bulk + +Use `GetBulkTranslationsAsync` to select translations for multiple objects with the same culture and fallback settings: + +```csharp +var results = await _multiLingualObjectManager + .GetBulkTranslationsAsync(products); + +foreach (var (product, translation) in results) +{ + // Use product and its selected translation. +} +``` + +Each result keeps the source object together with its selected translation. An object with no translations gets a `null` translation. diff --git a/docs/en/release-info/migration-guides/identityserver-to-openiddict.md b/docs/en/release-info/migration-guides/identityserver-to-openiddict.md index 2770722a51e..b44f1c4b55f 100644 --- a/docs/en/release-info/migration-guides/identityserver-to-openiddict.md +++ b/docs/en/release-info/migration-guides/identityserver-to-openiddict.md @@ -7,12 +7,12 @@ # Migration Identity Server to OpenIddict Guide -This document explains how to migrate to [OpenIddict](https://github.com/openiddict/openiddict-core) from Identity Server. From now on the ABP startup templates uses `OpenIddict` as the auth server by default since version v6.0.0. +This document explains how to migrate an application from IdentityServer4 to [OpenIddict](https://github.com/openiddict/openiddict-core). ABP startup templates have used OpenIddict as the authentication server by default since v6.0.0. -## History -We are not removing Identity Server packages and we will continue to release new versions of Identity Server related NuGet/NPM packages. That means you won't have an issue while upgrading to v6.0 when the stable version releases. We will continue to fix bugs in our packages for a while. ABP 7.0 will be based on .NET 7. If Identity Server continues to work with .NET 7, we will also continue to ship NuGet packages for our IDS integration. +> The checklist below describes the v6.0 transition. For a layer-by-layer migration, use the [IdentityServer to OpenIddict step-by-step guide](openiddict-step-by-step.md) and apply the package version that matches the ABP version of your application. -On the other hand, Identity Server ends support for the open-source Identity Server in the end of 2022. The Identity Server team has decided to move to Duende IDS and ABP will not be migrated to the commercial Duende IDS. You can see the Duende Identity Server announcement from [this link](https://blog.duendesoftware.com/posts/20220111_fair_trade). +## History +IdentityServer4 is archived and no longer maintained by its owners. ABP did not migrate its integration to the commercial Duende IdentityServer product. The ABP IdentityServer integration packages are still shipped for existing applications, while new applications use OpenIddict. See the [Duende IdentityServer announcement](https://blog.duendesoftware.com/posts/20220111_fair_trade) for the background. ## OpenIddict Migration Steps @@ -21,7 +21,7 @@ On the other hand, Identity Server ends support for the open-source Identity Ser * Replace all `IdentityServer` modules with corresponding `OpenIddict` modules. eg `AbpIdentityServerDomainModule` to `AbpOpenIddictDomainModule`, `AbpAccountWebIdentityServerModule` to `AbpAccountWebOpenIddictModule`. * Rename the `ConfigureIdentityServer` to `ConfigureOpenIddict` in your `ProjectNameDbContext` class. * Remove the `UseIdentityServer` and add `UseAbpOpenIddictValidation` after `UseAuthentication`. -* Add follow code to your startup module. +* Add the following code to your startup module. ```cs public override void PreConfigureServices(ServiceConfigurationContext context) @@ -38,7 +38,7 @@ public override void PreConfigureServices(ServiceConfigurationContext context) } ``` -* If your project is not separate AuthServer please also add `ForwardIdentityAuthenticationForBearer` +* If your project does not have a separate AuthServer, also add `ForwardIdentityAuthenticationForBearer`. ```cs private void ConfigureAuthentication(ServiceConfigurationContext context) @@ -48,9 +48,9 @@ private void ConfigureAuthentication(ServiceConfigurationContext context) ``` * Remove the `IdentityServerDataSeedContributor` from the `Domain` project. -* Create a new version of the project, with the same name as your existing project. -* Copy the `ProjectName.Domain\OpenIddict\OpenIddictDataSeedContributor.cs` of new project into your project and update `appsettings.json` base on `ProjectName.DbMigrator\appsettings.json`, Be careful to change the port number. -* Copy the `Index.cshtml.cs` and `Index.cs` of new project to your project if you're using `IClientRepository` in `IndexModel`. +* Generate a temporary project with the same name and architecture as the existing project so you can compare the current OpenIddict setup. +* Copy the generated `ProjectName.Domain\OpenIddict\OpenIddictDataSeedContributor.cs` into your project and update `appsettings.json` based on `ProjectName.DbMigrator\appsettings.json`. Adjust the ports and client URLs for your application. +* Copy the generated `Index.cshtml.cs` and `Index.cshtml` files into your project if your `IndexModel` still uses `IClientRepository`. * Update the scope name from `role` to `roles` in `AddAbpOpenIdConnect` method. * Remove `options.OAuthClientSecret(configuration["AuthServer:SwaggerClientSecret"]);` from `HttpApi.Host` project. * AuthServer no longer requires `JWT bearer authentication`. Please remove it. eg `AddJwtBearer` and `UseJwtTokenMiddleware`. diff --git a/docs/en/release-info/migration-guides/identityserver4-step-by-step.md b/docs/en/release-info/migration-guides/identityserver4-step-by-step.md index 844d5ae071f..da617cd2372 100644 --- a/docs/en/release-info/migration-guides/identityserver4-step-by-step.md +++ b/docs/en/release-info/migration-guides/identityserver4-step-by-step.md @@ -7,11 +7,13 @@ # Migrating from OpenIddict to IdentityServer4 Step by Step Guide -ABP startup templates use `OpenIddict` OpenID provider from v6.0.0 by default and `IdentityServer` projects are renamed to `AuthServer` in tiered/separated solutions. Since OpenIddict is the default OpenID provider library for ABP templates since v6.0, you may want to keep using [IdentityServer4](https://github.com/IdentityServer/IdentityServer4) library, even it is **archived and no longer maintained by the owners**. ABP doesn't provide support for newer versions of IdentityServer. This guide provides layer-by-layer guidance for migrating your existing [OpenIddict](https://github.com/openiddict/openiddict-core) application to IdentityServer4. +ABP startup templates use the `OpenIddict` OpenID provider from v6.0.0 by default, and `IdentityServer` projects were renamed to `AuthServer` in tiered/separated solutions. This guide provides the v6.0-era, layer-by-layer steps for migrating an existing [OpenIddict](https://github.com/openiddict/openiddict-core) application to IdentityServer4. + +> IdentityServer4 is archived and no longer maintained by its owners. ABP doesn't support newer IdentityServer or Duende IdentityServer versions through this module. Use this guide only when an existing system has a compatibility requirement that prevents migration to OpenIddict; new applications should remain on OpenIddict. ## IdentityServer4 Migration Steps -Use the `abp update` command to update your existing application. See [Upgrading docs](../upgrading.md) for more info. Apply required migrations by following the [Migration Guides](../migration-guides) based on your application version. +These steps target an application that is already on ABP 6.0.x. Keep every `Volo.Abp.*` package on the exact 6.0.x patch version used by the application. Complete any version update separately with the CLI and migration guides for that release line before applying these provider-replacement steps. ### Domain.Shared Layer @@ -138,8 +140,6 @@ typeof(AbpIdentityServerEntityFrameworkCoreModule), ```csharp using Volo.Abp.IdentityServer.EntityFrameworkCore; ... - using Volo.Abp.OpenIddict.EntityFrameworkCore; - ... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); @@ -150,7 +150,7 @@ typeof(AbpIdentityServerEntityFrameworkCoreModule), builder.ConfigureIdentityServer(); ``` -> Not: You need to create new migration after updating the fluent api. Navigate to *EntityFrameworkCore* folder and add a new migration. Ex, `dotnet ef migrations add Updated_To_IdentityServer ` +> Note: Create a new migration after updating the fluent API. Navigate to the *EntityFrameworkCore* folder and run, for example, `dotnet ef migrations add Updated_To_IdentityServer`. ### MongoDB Layer @@ -186,14 +186,14 @@ typeof(AbpIdentityServerMongoDbModule), ### DbMigrator Project -- In `appsettings.json` **replace OpenIddict section with IdentityServer** since IdentityServerDataSeeder will be using these information for initial data seeding: +- In `appsettings.json` **replace the OpenIddict section with IdentityServer** because IdentityServerDataSeeder uses this configuration for initial data seeding. Rename the `Applications` property to `Clients` and preserve its existing child entries. The minimal valid structure is: ```json - "IdentityServer": { // Rename OpenIddict to IdentityServer - "Clients ": { // Rename Applications to Clients - ... - } + { + "IdentityServer": { + "Clients": {} } + } ``` @@ -223,7 +223,7 @@ typeof(AbpIdentityServerMongoDbModule), ### UI Layer -You can follow the migrations guides from IdentityServer to OpenIddict in **reverse order** to update your UIs. You can also check the source-code for [Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml) and [Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml.cs) files for **AuthServer** project. +You can follow the migration guides from IdentityServer to OpenIddict in reverse order to update your UIs. You can also check the sample source for [Index.cshtml.cs](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml.cs) and [Index.cshtml](https://github.com/abpframework/abp-samples/blob/master/OpenId2Ids/src/OpenId2Ids.AuthServer/Pages/Index.cshtml) in the **AuthServer** project. - [Angular UI Migration](openiddict-angular.md) - [MVC/Razor UI Migration](openiddict-mvc.md) diff --git a/docs/en/solution-templates/layered-web-application/deployment/identityserver-deployment.md b/docs/en/solution-templates/layered-web-application/deployment/identityserver-deployment.md index db6975e04f3..48f62b9c70b 100644 --- a/docs/en/solution-templates/layered-web-application/deployment/identityserver-deployment.md +++ b/docs/en/solution-templates/layered-web-application/deployment/identityserver-deployment.md @@ -7,19 +7,21 @@ # IdentityServer Deployment -IdentityServer configuration may be different based on deployment configurations. Basically, you need update identityserver client related data and update your hosting preferences based on your deployment environment. +> This page applies only to applications that still use the legacy [IdentityServer module](../../../modules/identity-server.md). Current startup templates use OpenIddict; see [Configuring OpenIddict](../../../deployment/configuring-openIddict.md) for current applications. -## Update Cors Origins +IdentityServer configuration changes between deployment environments. Update the IdentityServer client data and the host settings for the deployed URLs before releasing the application. -Cors origins configuration for **gateways**, **microservices** swagger authorization and **Angular/Blazor** (web assembly) must be updated for deployment. This can be found under **App** configuration in *appsettings.json* +## Update CORS Origins + +CORS origin configuration for **gateways**, **microservices** Swagger authorization and **Angular/Blazor WebAssembly** applications must be updated for deployment. It is under the **App** section in *appsettings.json*. ```json "CorsOrigins": "https://*.MyProjectName.com,http://localhost:4200,https://localhost:44307,https://localhost:44325,https://localhost:44353,https://localhost:44367,https://localhost:44388,https://localhost:44381,https://localhost:44361", ``` -## Update Redirect Allowed Urls +## Update Redirect Allowed URLs -This configuration must be done if **Angular** or **Blazor** (web assembly) is used as back-office web application. It is found under **App** configuration in appsettings.json +Update this configuration when an **Angular** or **Blazor WebAssembly** application is used as the back-office application. It is under the **App** section in *appsettings.json*. ```json "RedirectAllowedUrls": "http://localhost:4200,https://localhost:44307" @@ -29,90 +31,50 @@ This configuration must be done if **Angular** or **Blazor** (web assembly) is u `IdentityServerDataSeedContributor` uses **IdentityServer.Clients** section of `appsettings.json` for `ClientId`, `RedirectUri`, `PostLogoutRedirectUri`, `CorsOrigins`. -Update DbMigrator project `appsettings.json` **IdentityServer.Clients.RootUrls** with production values: +Update the DbMigrator project's `appsettings.json` **IdentityServer.Clients.RootUrls** values for production: ![db-migrator-appsettings](../../../images/db-migrator-appsettings.png) Or, manually add production values to `IdentityServerClientRedirectUris`, `IdentityServerClientPostLogoutRedirectUris`, `IdentityServerClientCorsOrigins` tables in your database. -> If you are using microservice template on-the-fly migration and not using dbmigrator project, update **IdentityService** appsettings. +> If you use the microservice template's on-the-fly migration instead of a DbMigrator project, update the **IdentityService** settings. -Eventually, you shouldn't have `localhost` related data. +Remove all `localhost` values from the production client data. ## Update IdentityServer -You need to update token signing certificate and identityserver midware based on your hosting environment. +Update the token-signing certificate and IdentityServer middleware for your hosting environment. ### Signing Certificate -Default development environment uses [developer signing certificates option](https://github.com/abpframework/abp/blob/dev/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerBuilderOptions.cs#L29). Using developer signing certificates may cause *IDX10501: Signature validation failed* error on production. +The default `AbpIdentityServerBuilderOptions.AddDeveloperSigningCredential` value enables a developer signing credential. Don't use a developer signing credential in production; configure a real certificate through `IIdentityServerBuilder` pre-configuration. Otherwise, signing keys can change between deployments and cause errors such as *IDX10501: Signature validation failed*. -Update **IdentityServerModule** with using real certificate on `IIdentityServerBuilder` pre-configuration. +Update **IdentityServerModule** to use a real certificate in `IIdentityServerBuilder` pre-configuration. ![idsrv-certificate](../../../images/idsrv-certificate.png) -You can also [create self-signed certificate](https://docs.abp.io/en/commercial/5.0/startup-templates/microservice/tye-integration#create-developer-certificates) and use it. - -> If you are using self signed certificate, do not forget to set the certificate (.pfx file) as `EmbeddedResource` and set `CopyToOutputDirectory`. File needs to exist physically. +Load the production certificate from your deployment platform's certificate or secret store. Don't embed the production private key in the application assembly or commit it to the source repository. ### Use HTTPS -Update **IdentityServerModule** to [enfcore https](https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-6.0&tabs=visual-studio). Add `UseHsts` to add hsts headers to clients, add `UseHttpsRedirection` to redirect http requests to https. +Update **IdentityServerModule** to [enforce HTTPS](https://learn.microsoft.com/aspnet/core/security/enforcing-ssl). Add `UseHsts` to send HSTS headers and `UseHttpsRedirection` to redirect HTTP requests to HTTPS. ![use-https](../../../images/use-https.png) ### Behind Load Balancer -To redirect http requests to https from load balancer, update `OnApplicationInitialization` method of the **IdentityServerModule** with the midware below: +When TLS terminates at a reverse proxy or load balancer, use ASP.NET Core Forwarded Headers Middleware so IdentityServer receives the original scheme and host. Follow the [Forwarded Headers](../../../deployment/forwarded-headers.md) guide and the [ASP.NET Core proxy and load balancer guidance](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer). -```csharp -app.Use((httpContext, next) => -{ - httpContext.Request.Scheme = "https"; - return next(); -}); -``` +Configure the proxy addresses or networks in `ForwardedHeadersOptions.KnownProxies` or `KnownNetworks`, and call `UseForwardedHeaders` before authentication, IdentityServer, HTTPS redirection and HSTS middleware. If you enable `X-Forwarded-Host`, restrict `AllowedHosts` and configure the proxy to overwrite incoming forwarded headers. + +Don't set `HttpContext.Request.Scheme` unconditionally. Don't derive the IdentityServer origin from a custom request header unless the application first verifies that the request came through a trusted proxy; internet clients can forge ordinary request headers. ### Kubernetes -A common scenario is running applications in kubernetes environment. While IdentityServer needs to face internet on https, internal requests can be done using http. +A common scenario is running applications in Kubernetes. While IdentityServer must be exposed to the internet over HTTPS, internal requests can use HTTP. ![idsrv-k8s](../../../images/idsrv-k8s.png) -**HttpApi.Host** and **Web** applications authority should be set to http since token validations will done using http request. - -![api-resource-internal-idsrv](../../../images/api-resource-internal-idsrv.png) - -> You can use different appsettings files like *appsettings.production.json* to override these values or directly override environment values from kubernetes. +Keep the externally advertised IdentityServer authority and origin on the public HTTPS URL. If services use cluster-local routing, configure internal DNS or the ingress so that the public authority resolves through the trusted internal route without changing the issuer or accepting a client-controlled origin. -To isolate internal identityserver requests from external network (internet), append extra header instead of overwriting. -For ingress, you can use `nginx.ingress.kubernetes.io/configuration-snippet`: - -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: myidentityserver-ingress - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - nginx.ingress.kubernetes.io/proxy-buffer-size: "32k" - nginx.ingress.kubernetes.io/proxy-buffers-number: "8" - nginx.ingress.kubernetes.io/configuration-snippet: | - more_set_input_headers "from-ingress: true"; -spec: -``` - -You need to set the IdentityServer origin based on header. Update `OnApplicationInitialization` method of the **IdentityServerModule** with the midware below: - -```csharp -app.Use(async (ctx, next) => -{ - if (ctx.Request.Headers.ContainsKey("from-ingress")) - { - ctx.SetIdentityServerOrigin("https://myidentityserver.com"); - } - - await next(); -}); -``` +> You can use environment-specific files such as *appsettings.Production.json* or environment variables to override these values in Kubernetes.