Browse Source

Merge branch 'dev' into feat/mudblazor

feat/mudblazor
maliming 4 weeks ago
parent
commit
1f854d26b7
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 2
      Directory.Packages.props
  2. 242
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/POST.md
  3. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/ai-management-openai-anythingllm.png
  4. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/chat-playground.png
  5. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/cover-image.png
  6. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/file-management-rba.png
  7. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/mcp-servers.png
  8. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-embedder.png
  9. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-file-upload.png
  10. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-vector-store.png
  11. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/studio-switch-to-preview.png
  12. BIN
      docs/en/Blog-Posts/2026-02-25 v10_2_Preview/test-connection.png
  13. 4
      docs/en/docs-nav.json
  14. 930
      docs/en/framework/ui/angular/dynamic-form-module.md
  15. BIN
      docs/en/images/ai-management-chat-mcp-tools.png
  16. BIN
      docs/en/images/ai-management-mcp-servers.png
  17. BIN
      docs/en/images/ai-management-mcp-test-connection.png
  18. BIN
      docs/en/images/ai-management-openai-anythingllm.png
  19. BIN
      docs/en/images/ai-management-openai-anythingllm2.png
  20. BIN
      docs/en/images/ai-management-rag-chat.png
  21. BIN
      docs/en/images/ai-management-rag-upload.png
  22. BIN
      docs/en/images/ai-management-workspace-mcp-config.png
  23. BIN
      docs/en/images/workspace-embedder.png
  24. BIN
      docs/en/images/workspace-vector-store.png
  25. 293
      docs/en/modules/ai-management/index.md
  26. 64
      docs/en/modules/permission-management.md
  27. 13
      docs/en/package-version-changes.md
  28. 71
      docs/en/release-info/migration-guides/abp-10-1.md
  29. 23
      docs/en/release-info/migration-guides/abp-10-2.md
  30. 20
      docs/en/release-info/release-notes.md
  31. 17
      docs/en/release-info/road-map.md
  32. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png
  33. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png
  34. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png
  35. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png
  36. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-blazor-webapp.png
  37. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png
  38. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png
  39. BIN
      docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png
  40. BIN
      docs/en/tutorials/modular-crm/images/catalog-module-vs-code-blazor-webapp.png
  41. BIN
      docs/en/tutorials/modular-crm/images/vscode-catalog-index-razor-blazor-webapp.png
  42. 7
      docs/en/tutorials/modular-crm/index.md
  43. 15
      docs/en/tutorials/modular-crm/part-01.md
  44. 15
      docs/en/tutorials/modular-crm/part-02.md
  45. 90
      docs/en/tutorials/modular-crm/part-03.md
  46. 23
      docs/en/tutorials/modular-crm/part-04.md
  47. 124
      docs/en/tutorials/modular-crm/part-05.md
  48. 55
      docs/en/tutorials/modular-crm/part-06.md
  49. 15
      docs/en/tutorials/modular-crm/part-07.md
  50. 9
      docs/en/tutorials/modular-crm/part-08.md
  51. 17
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs
  52. 8
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js
  53. 1538
      modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.Designer.cs
  54. 160
      modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.cs
  55. 138
      modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs
  56. 2
      modules/docs/app/VoloDocs.Migrator/appsettings.json
  57. 3
      modules/docs/app/VoloDocs.Web/VoloDocs.Web.abppkg
  58. 2
      modules/docs/app/VoloDocs.Web/appsettings.json
  59. 47
      modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs
  60. 5
      modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs
  61. 5
      modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/UserResourcePermissionProviderKeyLookupService.cs
  62. 5
      modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs
  63. 13
      modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs
  64. 5
      modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs
  65. 13
      modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs
  66. 1
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs
  67. 2
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/ResourcePermissionManagementModal.razor
  68. 2
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/IResourcePermissionManagementProvider.cs
  69. 2
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/IResourcePermissionProviderKeyLookupService.cs
  70. 5
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs
  71. 73
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs
  72. 1
      modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/AddResourcePermissionManagementModal.cshtml
  73. 1
      modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/UpdateResourcePermissionManagementModal.cshtml
  74. 137
      modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManager_Tests.cs
  75. 2
      modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/AbpPermissionManagementTestBaseModule.cs
  76. 5
      modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestResourcePermissionProviderKeyLookupService.cs
  77. 26
      modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestUnavailableResourcePermissionManagementProvider.cs
  78. 29
      modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestUnavailableResourcePermissionProviderKeyLookupService.cs
  79. 3
      npm/ng-packs/packages/identity/package.json
  80. 72
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.html
  81. 9
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

2
Directory.Packages.props

@ -123,7 +123,7 @@
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
<PackageVersion Include="Minio" Version="6.0.5" />
<PackageVersion Include="MongoDB.Driver" Version="3.6.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.7.0" />
<PackageVersion Include="NEST" Version="7.17.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Nito.AsyncEx.Context" Version="5.1.2" />

242
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/POST.md

@ -0,0 +1,242 @@
# ABP Platform 10.2 RC Has Been Released
We are happy to release [ABP](https://abp.io) version **10.2 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version.
Try this version and provide feedback for a more stable version of ABP v10.2! Thanks to you in advance.
## Get Started with the 10.2 RC
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli).
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI:
![studio-switch-to-preview.png](studio-switch-to-preview.png)
## Migration Guide
There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v10.1 or earlier: [ABP Version 10.2 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/abp-10-2).
## What's New with ABP v10.2?
In this section, I will introduce some major features released in this version.
Here is a brief list of titles explained in the next sections:
- Multi-Tenant Account Usage: Shared User Accounts
- Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions
- `ClientResourcePermissionValueProvider` for OAuth/OpenIddict
- Angular: Hybrid Localization Support
- Angular: Extensible Table Row Detail
- Angular: CMS Kit Module Features
- Blazor: Upgrade to Blazorise 2.0
- Identity: Single Active Token Providers
- TickerQ Package Upgrade to 10.1.1
- AI Management: MCP (Model Context Protocol) Support
- AI Management: RAG with File Upload
- AI Management: OpenAI-Compatible Chat Endpoint
- File Management: Resource-Based Authorization
### Multi-Tenant Account Usage: Shared User Accounts
ABP v10.2 introduces **Shared User Accounts**: a single user account can belong to multiple tenants, and the user can choose or switch the active tenant when signing in. This enables a "one account, multiple tenants" experience — for example, inviting the same email address into multiple tenants.
When you use Shared User Accounts:
- Username/email uniqueness becomes **global** (Host + all tenants)
- Users are prompted to select the tenant at login if they belong to multiple tenants
- Users can switch between tenants using the tenant switcher in the user menu
- Tenant administrators can invite existing or new users to join a tenant
Enable shared accounts by configuring `UserSharingStrategy`:
```csharp
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true;
options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});
```
> See the [Shared User Accounts](https://abp.io/docs/10.2/modules/account/shared-user-accounts) documentation for details.
### Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions
ABP v10.2 implements a unified **privilege escalation prevention** model to address security vulnerabilities where users could assign themselves or others roles or permissions they do not possess.
**Role Assignment Restriction:** Users can only assign or remove roles they currently have. Users cannot add new roles to themselves (removal only) and cannot assign or remove roles they do not possess.
**Permission Grant/Revoke Authorization:** Users can only grant or revoke permissions they currently have. Validation applies to both grant and revoke operations.
**Incremental Permission Protection:** When updating user or role permissions, permissions the current user does not have are treated as non-editable and are preserved as-is during updates.
Users with the `admin` role can assign any role and grant/revoke any permission. All validations are enforced on the backend — the UI is not a security boundary.
> See [#24775](https://github.com/abpframework/abp/pull/24775) for more details.
### `ClientResourcePermissionValueProvider` for OAuth/OpenIddict
ABP v10.2 adds **ClientResourcePermissionValueProvider**, extending resource-based authorization to OAuth clients. When using IdentityServer or OpenIddict, clients can now have resource permissions aligned with the standard user and role permission model.
This allows you to control which OAuth clients can access which resources, providing fine-grained authorization for API consumers. The implementation integrates with ABP's existing resource permission infrastructure.
> See [#24515](https://github.com/abpframework/abp/pull/24515) for more details.
### Angular: Hybrid Localization Support
ABP v10.2 introduces **Hybrid Localization** for Angular applications, combining server-side and client-side localization strategies. This gives you flexibility in how translations are loaded and resolved — you can use server-provided localization, client-side fallbacks, or a mix of both.
This feature is useful when you want to reduce initial load time, support offline scenarios, or have environment-specific localization behavior. The Angular packages have been updated to support the hybrid approach seamlessly.
> See the [Hybrid Localization](https://abp.io/docs/10.2/framework/ui/angular/hybrid-localization) documentation and [#24731](https://github.com/abpframework/abp/pull/24731).
### Angular: Extensible Table Row Detail
ABP v10.2 adds the **ExtensibleTableRowDetailComponent** for expandable row details in extensible tables. You can now display additional information for each row in a collapsible detail section.
The feature supports row detail templates via both direct input and content child component. It adds toggle logic and emits `rowDetailToggle` events, making it easy to customize the behavior and appearance of expandable rows in your data tables.
> See [#24636](https://github.com/abpframework/abp/pull/24636) for more details.
### Angular: CMS Kit Module Features
ABP v10.2 brings **CMS Kit features to Angular**, completing the cross-platform UI coverage for the CMS Kit module. The Angular implementation includes: Blogs, Blog Posts, Comments, Menus, Pages, Tags, Global Resources, and CMS Settings.
Together with the CMS Kit Pro Angular implementation (FAQ, Newsletters, Page Feedbacks, Polls, Url forwarding), ABP now provides full Angular UI coverage for both the open-source CMS Kit and CMS Kit Pro modules.
> See [#24234](https://github.com/abpframework/abp/pull/24234) for more details.
### Blazor: Upgrade to Blazorise 2.0
ABP v10.2 upgrades the [Blazorise](https://blazorise.com/) library to **version 2.0** for Blazor UI. If you are upgrading your project to v10.2 RC, please ensure that all Blazorise-related packages are updated to v2.0 in your application.
Blazorise 2.0 includes various improvements and changes. Please refer to the [Blazorise 2.0 Release Notes](https://blazorise.com/news/release-notes/200) and the [ABP Blazorise 2.0 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/blazorise-2-0-migration) for upgrade instructions.
> See [#24906](https://github.com/abpframework/abp/pull/24906) for more details.
### Identity: Single Active Token Providers
ABP v10.2 introduces a **single active token** policy for password reset, email confirmation, and change-email flows. Three new token providers are available: `AbpPasswordResetTokenProvider`, `AbpEmailConfirmationTokenProvider`, and `AbpChangeEmailTokenProvider`.
When a new token is generated, it invalidates any previously issued tokens for that purpose. This improves security by ensuring that only the most recently issued token is valid. Token lifespan can be customized via the respective options classes for each provider.
> See [#24926](https://github.com/abpframework/abp/pull/24926) for more details.
### TickerQ Package Upgrade to 10.1.1
**If you are using the TickerQ integration packages** (`Volo.Abp.TickerQ`, `Volo.Abp.BackgroundJobs.TickerQ`, or `Volo.Abp.BackgroundWorkers.TickerQ`), you need to apply breaking changes when upgrading to ABP 10.2. TickerQ has been upgraded from 2.5.3 to 10.1.1, which only targets .NET 10.0 and contains several API changes.
Key changes include:
- `UseAbpTickerQ` moved from `IApplicationBuilder` to `IHost` — use `context.GetHost().UseAbpTickerQ()` in your module
- Entity types renamed: `TimeTicker``TimeTickerEntity`, `CronTicker``CronTickerEntity`
- Scheduler and dashboard configuration APIs have changed
- New helpers: `context.GetHost()`, `GetWebApplication()`, `GetEndpointRouteBuilder()`
> **Important:** Do **not** resolve `IHost` from `context.ServiceProvider.GetRequiredService<IHost>()`. Always use `context.GetHost()`. See the [ABP Version 10.2 Migration Guide](https://abp.io/docs/10.2/release-info/migration-guides/abp-10-2) for the complete list of changes.
### AI Management: MCP (Model Context Protocol) Support
_This is a **PRO** feature available for ABP Commercial customers._
The [AI Management Module](https://abp.io/docs/10.2/modules/ai-management) now supports [MCP (Model Context Protocol)](https://modelcontextprotocol.io/), enabling AI workspaces to use external MCP servers as tools. MCP allows AI models to interact with external services, databases, APIs, and more through a standardized protocol.
![mcp-servers](mcp-servers.png)
You can create and manage MCP servers via the AI Management UI. Each MCP server supports one of the following transport types: **Stdio** (runs a local command), **SSE** (Server-Sent Events), or **StreamableHttp**. For HTTP-based transports, you can configure authentication (API Key, Bearer token, or custom headers). Once MCP servers are defined, you can associate them with workspaces. When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations — tool calls and results are displayed in the chat interface.
You can test the connection to an MCP server after creating it to verify connectivity and list available tools before use:
![test-connection](test-connection.png)
When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations. Tool calls and results are displayed in the chat interface.
![chat-playground](chat-playground.png)
> See the [AI Management documentation](https://abp.io/docs/10.2/modules/ai-management#mcp-servers) for details.
### AI Management: RAG with File Upload
_This is a **PRO** feature available for ABP Commercial customers._
The AI Management module supports **RAG (Retrieval-Augmented Generation)** with file upload, which enables workspaces to answer questions based on the content of uploaded documents. When RAG is configured, the AI model searches the uploaded documents for relevant information before generating a response.
To enable RAG, configure an **embedder** (e.g., OpenAI, Ollama) and a **vector store** (e.g., PgVector) on the workspace:
| Embedder | Vector Store |
| --- | --- |
| ![rag-embedder](rag-embedder.png) | ![rag-vector-store](rag-vector-store.png) |
You can then upload documents (PDF, Markdown, or text files, max 10 MB) through the workspace management UI. Uploaded documents are automatically processed — their content is chunked, embedded, and stored in the configured vector store:
![rag-file-upload](rag-file-upload.png)
When you ask questions in the chat interface, the AI model uses the uploaded documents as context for accurate, grounded responses.
> See the [AI Management — RAG with File Upload](https://abp.io/docs/10.2/modules/ai-management#rag-with-file-upload) documentation for configuration details.
### AI Management: OpenAI-Compatible Chat Endpoint
_This is a **PRO** feature available for ABP Commercial customers._
The AI Management module exposes an **OpenAI-compatible REST API** at the `/v1` path. This allows any application or tool that supports the OpenAI API format — such as [AnythingLLM](https://anythingllm.com/), [Open WebUI](https://openwebui.com/), [Dify](https://dify.ai/), or custom scripts using the OpenAI SDK — to connect directly to your AI Management instance.
**Example configuration from AnythingLLM**:
![anythingllm](ai-management-openai-anythingllm.png)
Each AI Management **workspace** appears as a selectable model in the client application. The workspace's configured AI provider handles the actual inference transparently. Available endpoints include `/v1/chat/completions`, `/v1/models`, `/v1/embeddings`, `/v1/files`, and more. All endpoints require authentication via a Bearer token in the `Authorization` header.
> See the [AI Management — OpenAI-Compatible API](https://abp.io/docs/10.2/modules/ai-management#openai-compatible-api) documentation for usage examples.
### File Management: Resource-Based Authorization
_This is a **PRO** feature available for ABP Commercial customers._
The **File Management Module** now supports **resource-based authorization**. You can control access to individual files and folders per user, role, or client. Permissions can be granted at the resource level via the UI, and the feature integrates with ABP's resource permission infrastructure.
![file-management-resource-based-authorization](file-management-rba.png)
This feature is **implemented for all three supported UIs: MVC/Razor Pages, Blazor, and Angular**, providing a consistent experience across your application regardless of the UI framework you use.
### Other Improvements and Enhancements
- **Angular signal APIs**: ABP Angular packages migrated to signal queries, output functions, and signal input functions for alignment with Angular 21 ([#24765](https://github.com/abpframework/abp/pull/24765), [#24766](https://github.com/abpframework/abp/pull/24766), [#24777](https://github.com/abpframework/abp/pull/24777)).
- **Angular Vitest**: ABP Angular templates now use Vitest as the default testing framework instead of Karma/Jasmine ([#24725](https://github.com/abpframework/abp/pull/24725)).
- **Ambient auditing**: Programmatic disable/enable of auditing via `IAuditingHelper.DisableAuditing()` and `IsAuditingEnabled()` ([#24718](https://github.com/abpframework/abp/pull/24718)).
- **Complex property auditing**: Entity History and ModifierId now support EF Core complex properties ([#24767](https://github.com/abpframework/abp/pull/24767)).
- **RabbitMQ correlation ID**: Correlation ID support added to RabbitMQ JobQueue for distributed tracing ([#24755](https://github.com/abpframework/abp/pull/24755)).
- **Concurrent config retrieval**: `MvcCachedApplicationConfigurationClient` now fetches configuration and localization concurrently for faster startup ([#24838](https://github.com/abpframework/abp/pull/24838)).
- **Environment localization fallback**: Angular can use `environment.defaultResourceName` when the backend does not provide it ([#24589](https://github.com/abpframework/abp/pull/24589)).
- **JS proxy namespace fix**: Resolved namespace mismatch for multi-segment company names in generated proxies ([#24877](https://github.com/abpframework/abp/pull/24877)).
- **Audit Logging max length**: Entity/property type full names increased to 512 characters to reduce truncation ([#24846](https://github.com/abpframework/abp/pull/24846)).
- **AI guidelines**: Cursor and Copilot AI guideline documents added for ABP development ([#24563](https://github.com/abpframework/abp/pull/24563), [#24593](https://github.com/abpframework/abp/pull/24593)).
## Community News
### New ABP Community Articles
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:
- [Enis Necipoğlu](https://abp.io/community/members/enisn) has published 2 new posts:
- [ABP Framework's Hidden Magic: Things That Just Work Without You Knowing](https://abp.io/community/articles/hidden-magic-things-that-just-work-without-you-knowing-vw6osmyt)
- [Implementing Multiple Global Query Filters with Entity Framework Core](https://abp.io/community/articles/implementing-multiple-global-query-filters-with-entity-ugnsmf6i)
- [Suhaib Mousa](https://abp.io/community/members/suhaib-mousa) has published 2 new posts:
- [.NET 11 Preview 1 Highlights: Faster Runtime, Smarter JIT, and AI-Ready Improvements](https://abp.io/community/articles/dotnet-11-preview-1-highlights-hspp3o5x)
- [TOON vs JSON for LLM Prompts in ABP: Token-Efficient Structured Context](https://abp.io/community/articles/toon-vs-json-b4rn2avd)
- [Fahri Gedik](https://abp.io/community/members/fahrigedik) has published 2 new posts:
- [Building a Multi-Agent AI System with A2A, MCP, and ADK in .NET](https://abp.io/community/articles/building-a-multiagent-ai-system-with-a2a-mcp-iefdehyx)
- [Async Chain of Persistence Pattern: Designing for Failure in Event-Driven Systems](https://abp.io/community/articles/async-chain-of-persistence-pattern-wzjuy4gl)
- [Alper Ebiçoğlu](https://abp.io/community/members/alper) has published 2 new posts:
- [NDC London 2026: From a Developer's Perspective and My Personal Notes about AI](https://abp.io/community/articles/ndc-london-2026-a-.net-conf-from-a-developers-perspective-07wp50yl)
- [Which Open-Source PDF Libraries Are Recently Popular? A Data-Driven Look At PDF Topic](https://abp.io/community/articles/which-opensource-pdf-libraries-are-recently-popular-a-g68q78it)
- [Stop Spam and Toxic Users in Your App with AI](https://abp.io/community/articles/stop-spam-and-toxic-users-in-your-app-with-ai-3i0xxh0y) by [Engincan Veske](https://abp.io/community/members/EngincanV)
- [How AI Is Changing Developers](https://abp.io/community/articles/how-ai-is-changing-developers-e8y4a85f) by [Liming Ma](https://abp.io/community/members/maliming)
- [JetBrains State of Developer Ecosystem Report 2025 — Key Insights](https://abp.io/community/articles/jetbrains-state-of-developer-ecosystem-report-2025-key-z0638q5e) by [Tarık Özdemir](https://abp.io/community/members/mtozdemir)
- [Integrating AI into ABP.IO Applications: The Complete Guide to Volo.Abp.AI and AI Management Module](https://abp.io/community/articles/integrating-ai-into-abp.io-applications-the-complete-guide-jc9fbjq0) by [Adnan Ali](https://abp.io/community/members/adnanaldaim)
Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community.
## Conclusion
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.2/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.2 RC and provide feedback to help us release a more stable version.
Thanks for being a part of this community!

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/ai-management-openai-anythingllm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/chat-playground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/file-management-rba.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/mcp-servers.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-embedder.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-file-upload.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/rag-vector-store.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/studio-switch-to-preview.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
docs/en/Blog-Posts/2026-02-25 v10_2_Preview/test-connection.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

4
docs/en/docs-nav.json

@ -1846,6 +1846,10 @@
"text": "Card",
"path": "framework/ui/angular/card-component.md"
},
{
"text": "Dynamic Forms",
"path": "framework/ui/angular/dynamic-form-module.md"
},
{
"text": "Password Complexity Indicator",
"path": "framework/ui/angular/password-complexity-indicator-component.md"

930
docs/en/framework/ui/angular/dynamic-form-module.md

@ -0,0 +1,930 @@
```json
//[doc-seo]
{
"Description": "Learn how to use the ABP Dynamic Form Module to create dynamic, configurable forms with validation, conditional logic, nested groups and arrays, many input types, and custom components in Angular applications."
}
```
# Dynamic Form Module
The ABP Dynamic Form Module is a powerful component that allows you to create dynamic, configurable forms without writing extensive HTML templates. It provides a declarative way to define form fields with validation, conditional logic, grid layout, and custom components.
## Installation
The Dynamic Form Module is part of the `@abp/ng.components` package. If you haven't installed it yet, install it via npm:
```bash
npm install @abp/ng.components
```
## Usage
Import the `DynamicFormComponent` in your component:
```ts
import { DynamicFormComponent } from '@abp/ng.components/dynamic-form';
@Component({
selector: 'app-my-component',
imports: [DynamicFormComponent],
templateUrl: './my-component.component.html',
})
export class MyComponent {}
```
## Basic Example
Here's a simple example of how to use the dynamic form:
```ts
import { Component } from '@angular/core';
import { DynamicFormComponent } from '@abp/ng.components/dynamic-form';
import { FormFieldConfig } from '@abp/ng.components/dynamic-form';
@Component({
selector: 'app-user-form',
imports: [DynamicFormComponent],
template: `
<abp-dynamic-form
[fields]="formFields"
[submitButtonText]="'Submit'"
[showCancelButton]="true"
(onSubmit)="handleSubmit($event)"
(formCancel)="handleCancel()"
/>
`,
})
export class UserFormComponent {
formFields: FormFieldConfig[] = [
{
key: 'firstName',
type: 'text',
label: 'First Name',
placeholder: 'Enter your first name',
required: true,
order: 1,
},
{
key: 'lastName',
type: 'text',
label: 'Last Name',
placeholder: 'Enter your last name',
required: true,
order: 2,
},
{
key: 'email',
type: 'email',
label: 'Email',
placeholder: 'Enter your email',
required: true,
order: 3,
},
];
handleSubmit(formValue: any) {
console.log('Form submitted:', formValue);
// Handle form submission
}
handleCancel() {
console.log('Form cancelled');
// Handle form cancellation
}
}
```
## Component Inputs
The `DynamicFormComponent` accepts the following inputs:
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `fields` | `FormFieldConfig[]` | `[]` | Array of field configurations |
| `values` | `Record<string, any>` | `undefined` | Initial values for the form |
| `submitButtonText` | `string` | `'Submit'` | Text for the submit button |
| `submitInProgress` | `boolean` | `false` | Whether form submission is in progress |
| `showCancelButton` | `boolean` | `false` | Whether to show the cancel button |
## Component Outputs
| Output | Type | Description |
|--------|------|-------------|
| `onSubmit` | `EventEmitter<any>` | Emitted when the form is submitted with valid data |
| `formCancel` | `EventEmitter<void>` | Emitted when the cancel button is clicked |
## FormFieldConfig Properties
The `FormFieldConfig` interface defines the structure of each field in the form:
```ts
interface FormFieldConfig {
key: string; // Unique identifier for the field
type: FieldType; // Type of the field
label: string; // Label text for the field
value?: any; // Initial value
placeholder?: string; // Placeholder text
required?: boolean; // Whether the field is required
disabled?: boolean; // Whether the field is disabled
options?: OptionProps; // Options for select/radio (static or API)
validators?: ValidatorConfig[]; // Array of validator configurations
conditionalLogic?: ConditionalRule[]; // Array of conditional rules
order?: number; // Display order (ascending)
gridSize?: number; // Bootstrap grid size (1-12)
component?: Type<ControlValueAccessor>; // Custom component
// Type-specific attributes
min?: number | string; // number, date, time, range
max?: number | string; // number, date, time, range
step?: number | string; // number, time, range
minLength?: number; // text, password
maxLength?: number; // text, password
pattern?: string; // tel, text (regex)
accept?: string; // file (e.g. "image/*")
multiple?: boolean; // file
// Nested forms (group / array)
children?: FormFieldConfig[]; // Child fields for group/array
minItems?: number; // array: minimum items (default 0)
maxItems?: number; // array: maximum items
}
```
### Field Types
The following field types are supported:
| Type | Description |
|------|-------------|
| `text` | Text input |
| `email` | Email input |
| `number` | Number input (supports `min`, `max`, `step`) |
| `select` | Dropdown select (static or API-driven options) |
| `checkbox` | Checkbox |
| `date` | Date picker (supports `min`, `max`) |
| `datetime-local` | Date and time picker |
| `time` | Time picker (supports `min`, `max`, `step`) |
| `textarea` | Multi-line text |
| `password` | Password input (`minLength`, `maxLength`) |
| `tel` | Telephone input (`pattern`) |
| `url` | URL input |
| `radio` | Radio group (uses `options`) |
| `file` | File upload (`accept`, `multiple`) |
| `range` | Range slider (`min`, `max`, `step`) |
| `color` | Color picker |
| `group` | Nested group of fields (uses `children`) |
| `array` | Dynamic list with add/remove (uses `children`, `minItems`, `maxItems`) |
**Notes:**
- `file`: form value is `File` or `File[]` when `multiple` is true. Use `accept` (e.g. `"image/*"`) to limit types.
- `range`: defaults `min` 0, `max` 100, `step` 1 if omitted.
- `radio`: requires `options` (static `defaultValues` or `url`).
## Validators
You can add validators to your form fields using the `validators` property:
```ts
const formFields: FormFieldConfig[] = [
{
key: 'username',
type: 'text',
label: 'Username',
validators: [
{
type: 'required',
message: 'Username is required',
},
{
type: 'minLength',
value: 3,
message: 'Username must be at least 3 characters',
},
{
type: 'maxLength',
value: 20,
message: 'Username must not exceed 20 characters',
},
],
},
{
key: 'age',
type: 'number',
label: 'Age',
validators: [
{
type: 'min',
value: 18,
message: 'You must be at least 18 years old',
},
{
type: 'max',
value: 100,
message: 'Age must not exceed 100',
},
],
},
];
```
### Available Validator Types
| Type | Description | Requires Value |
|------|-------------|----------------|
| `required` | Field is required | No |
| `email` | Must be a valid email | No |
| `minLength` | Minimum string length | Yes |
| `maxLength` | Maximum string length | Yes |
| `min` | Minimum numeric value | Yes |
| `max` | Maximum numeric value | Yes |
| `pattern` | Regular expression pattern | Yes |
| `requiredTrue` | Must be true (for checkboxes) | No |
## Select and Radio Fields with Options
You can create `select` dropdowns or `radio` groups with static or dynamic options. Both use the `options` property (`OptionProps`).
### Static Options
```ts
const formFields: FormFieldConfig[] = [
{
key: 'country',
type: 'select',
label: 'Country',
options: {
defaultValues: [
{ key: 'us', value: 'United States' },
{ key: 'uk', value: 'United Kingdom' },
{ key: 'ca', value: 'Canada' },
],
valueProp: 'key',
labelProp: 'value',
},
},
];
```
### Dynamic Options from API
```ts
const formFields: FormFieldConfig[] = [
{
key: 'department',
type: 'select',
label: 'Department',
options: {
url: '/api/departments',
apiName: 'MyApi',
valueProp: 'id',
labelProp: 'name',
},
},
];
```
### OptionProps Interface
Used for `select` and `radio` fields. Provide either static `defaultValues` or `url` for API-driven options:
```ts
interface OptionProps<T = any> {
defaultValues?: T[]; // Static array of options
url?: string; // API endpoint URL (fetched via RestService)
disabled?: (option: T) => boolean; // Function to disable specific options
labelProp?: string; // Property name for label (default 'value')
valueProp?: string; // Property name for value (default 'key')
apiName?: string; // API name for RestService when using url
}
```
When using `url`, the response array is mapped with `valueProp` / `labelProp` to build options. Localization is applied to labels via `abpLocalization` where applicable.
## Conditional Logic
The Dynamic Form Module supports conditional logic to show/hide or enable/disable fields based on other field values:
```ts
const formFields: FormFieldConfig[] = [
{
key: 'hasLicense',
type: 'checkbox',
label: 'Do you have a driver\'s license?',
order: 1,
},
{
key: 'licenseNumber',
type: 'text',
label: 'License Number',
placeholder: 'Enter your license number',
order: 2,
conditionalLogic: [
{
dependsOn: 'hasLicense',
condition: 'equals',
value: true,
action: 'show',
},
],
},
{
key: 'age',
type: 'number',
label: 'Age',
order: 3,
},
{
key: 'parentConsent',
type: 'checkbox',
label: 'Parent Consent Required',
order: 4,
conditionalLogic: [
{
dependsOn: 'age',
condition: 'lessThan',
value: 18,
action: 'show',
},
],
},
];
```
### Conditional Rule Interface
```ts
interface ConditionalRule {
dependsOn: string; // Key of the field to watch
condition: string; // Condition type
value: any; // Value to compare against
action: string; // Action to perform
}
```
### Available Conditions
- `equals` - Field value equals the specified value
- `notEquals` - Field value does not equal the specified value
- `contains` - Field value contains the specified value (for strings/arrays)
- `greaterThan` - Field value is greater than the specified value (for numbers)
- `lessThan` - Field value is less than the specified value (for numbers)
### Available Actions
- `show` - Show the field when condition is met
- `hide` - Hide the field when condition is met
- `enable` - Enable the field when condition is met
- `disable` - Disable the field when condition is met
## Grid Layout
You can use the `gridSize` property to control the Bootstrap grid layout:
```ts
const formFields: FormFieldConfig[] = [
{
key: 'firstName',
type: 'text',
label: 'First Name',
gridSize: 6, // Half width
order: 1,
},
{
key: 'lastName',
type: 'text',
label: 'Last Name',
gridSize: 6, // Half width
order: 2,
},
{
key: 'address',
type: 'textarea',
label: 'Address',
gridSize: 12, // Full width
order: 3,
},
];
```
The `gridSize` property uses Bootstrap's 12-column grid system. If not specified, it defaults to 12 (full width).
## Nested Forms
The Dynamic Form supports **nested structures** via two field types:
### Group Type
Use `type: 'group'` to group related fields (e.g. address, contact info). Define child fields in `children`:
```ts
{
key: 'address',
type: 'group',
label: 'Address Information',
gridSize: 12,
children: [
{ key: 'street', type: 'text', label: 'Street', gridSize: 8 },
{ key: 'city', type: 'text', label: 'City', gridSize: 4 },
{ key: 'zipCode', type: 'text', label: 'ZIP Code', gridSize: 6 },
],
}
```
**Output:** `{ "address": { "street": "...", "city": "...", "zipCode": "..." } }`
Groups use `<fieldset>` / `<legend>` for semantics and accessibility. Nesting is recursive (groups inside groups).
### Array Type
Use `type: 'array'` for dynamic lists with add/remove (e.g. phone numbers, work experience). Set `children` for each item schema, and optionally `minItems` / `maxItems`:
```ts
{
key: 'phoneNumbers',
type: 'array',
label: 'Phone Numbers',
minItems: 1,
maxItems: 5,
gridSize: 12,
children: [
{
key: 'type',
type: 'select',
label: 'Type',
gridSize: 4,
options: {
defaultValues: [
{ key: 'mobile', value: 'Mobile' },
{ key: 'home', value: 'Home' },
{ key: 'work', value: 'Work' },
],
},
},
{ key: 'number', type: 'tel', label: 'Number', gridSize: 8 },
],
}
```
**Output:** `{ "phoneNumbers": [ { "type": "mobile", "number": "..." }, ... ] }`
Arrays render add/remove buttons, item labels (e.g. "Phone Number #1"), and respect `minItems` / `maxItems`. You can nest groups inside arrays and arrays inside groups.
See `NESTED-FORMS.md` in the package and `apps/dev-app/src/app/dynamic-form-page` for more examples.
## Custom Components
You can use custom components for specific fields by providing a component that implements `ControlValueAccessor`:
```ts
// custom-rating.component.ts
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'app-custom-rating',
template: `
<div class="rating">
@for (star of [1,2,3,4,5]; track star) {
<span
class="star"
[class.filled]="star <= value"
(click)="setValue(star)">
</span>
}
</div>
`,
styles: [`
.star { cursor: pointer; font-size: 24px; color: #ccc; }
.star.filled { color: #ffc107; }
`],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomRatingComponent),
multi: true
}]
})
export class CustomRatingComponent implements ControlValueAccessor {
value = 0;
onChange: any = () => {};
onTouched: any = () => {};
setValue(rating: number) {
this.value = rating;
this.onChange(rating);
this.onTouched();
}
writeValue(value: any): void {
this.value = value || 0;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}
```
Then use it in your form configuration:
```ts
import { CustomRatingComponent } from './custom-rating.component';
const formFields: FormFieldConfig[] = [
{
key: 'rating',
type: 'text', // Type is ignored when using custom component
label: 'Rating',
component: CustomRatingComponent,
value: 3,
},
];
```
## Setting Initial Values
You can set initial values for the form fields in two ways:
### 1. Using the `value` property in FormFieldConfig
```ts
const formFields: FormFieldConfig[] = [
{
key: 'firstName',
type: 'text',
label: 'First Name',
value: 'John',
},
];
```
### 2. Using the `values` input
```ts
@Component({
template: `
<abp-dynamic-form
[fields]="formFields"
[values]="initialValues"
(onSubmit)="handleSubmit($event)"
/>
`,
})
export class MyComponent {
formFields: FormFieldConfig[] = [
{
key: 'firstName',
type: 'text',
label: 'First Name',
},
{
key: 'lastName',
type: 'text',
label: 'Last Name',
},
];
initialValues = {
firstName: 'John',
lastName: 'Doe',
};
handleSubmit(formValue: any) {
console.log(formValue);
}
}
```
## Programmatic Form Control
You can access the form instance using the `exportAs` property and template reference variable:
```ts
@Component({
template: `
<abp-dynamic-form
#myForm="abpDynamicForm"
[fields]="formFields"
(onSubmit)="handleSubmit($event)"
/>
<button (click)="myForm.resetForm()">Reset Form</button>
`,
})
export class MyComponent {
formFields: FormFieldConfig[] = [
// ... field configurations
];
handleSubmit(formValue: any) {
console.log(formValue);
}
}
```
### Available Methods
- `resetForm()` - Resets the form to its initial state
- `submit()` - Programmatically submit the form
## Custom Action Buttons
You can customize the action buttons by projecting your own content:
```ts
@Component({
template: `
<abp-dynamic-form
[fields]="formFields"
(onSubmit)="handleSubmit($event)">
<div actions class="form-actions">
<button type="button" class="btn btn-secondary" (click)="handleCancel()">
Cancel
</button>
<button type="submit" class="btn btn-success">
Save Changes
</button>
<button type="button" class="btn btn-info" (click)="handleDraft()">
Save as Draft
</button>
</div>
</abp-dynamic-form>
`,
})
export class MyComponent {
formFields: FormFieldConfig[] = [
// ... field configurations
];
handleSubmit(formValue: any) {
console.log('Form submitted:', formValue);
}
handleCancel() {
console.log('Cancelled');
}
handleDraft() {
console.log('Saved as draft');
}
}
```
## Accessibility
The Dynamic Form includes built-in accessibility support:
- **ARIA attributes**: `aria-label`, `aria-required`, `aria-invalid`, `aria-describedby`, `aria-busy` on inputs and actions; `role="form"`, `role="group"`, `role="radiogroup"`, `role="alert"` where appropriate.
- **Semantic HTML**: `<fieldset>` / `<legend>` for groups; proper `<label>` / `for` associations.
- **Error handling**: Validation errors are exposed via `aria-describedby` and `aria-live="polite"` so screen readers announce them.
- **Focus management**: On submit when the form is invalid, focus moves to the first invalid field and it scrolls into view.
- **Keyboard navigation**: All controls are keyboard-accessible; range and color inputs use appropriate ARIA value attributes.
When using custom components or projected actions, keep labels, error associations, and focus behavior consistent for a good experience.
## Complete Example
Here's a complete example demonstrating various features:
```ts
import { Component } from '@angular/core';
import { DynamicFormComponent } from '@abp/ng.components/dynamic-form';
import { FormFieldConfig } from '@abp/ng.components/dynamic-form';
@Component({
selector: 'app-employee-form',
imports: [DynamicFormComponent],
template: `
<div class="container">
<h2>Employee Registration</h2>
<abp-dynamic-form
#employeeForm="abpDynamicForm"
[fields]="formFields"
[submitButtonText]="'Register Employee'"
[showCancelButton]="true"
[submitInProgress]="isSubmitting"
(onSubmit)="handleSubmit($event)"
(formCancel)="handleCancel()"
/>
</div>
`,
})
export class EmployeeFormComponent {
isSubmitting = false;
formFields: FormFieldConfig[] = [
// Personal Information
{
key: 'firstName',
type: 'text',
label: 'First Name',
placeholder: 'Enter first name',
required: true,
gridSize: 6,
order: 1,
validators: [
{ type: 'required', message: 'First name is required' },
{ type: 'minLength', value: 2, message: 'First name must be at least 2 characters' },
],
},
{
key: 'lastName',
type: 'text',
label: 'Last Name',
placeholder: 'Enter last name',
required: true,
gridSize: 6,
order: 2,
validators: [
{ type: 'required', message: 'Last name is required' },
{ type: 'minLength', value: 2, message: 'Last name must be at least 2 characters' },
],
},
{
key: 'email',
type: 'email',
label: 'Email',
placeholder: 'Enter email address',
required: true,
gridSize: 6,
order: 3,
validators: [
{ type: 'required', message: 'Email is required' },
{ type: 'email', message: 'Please enter a valid email address' },
],
},
{
key: 'phoneNumber',
type: 'text',
label: 'Phone Number',
placeholder: 'Enter phone number',
gridSize: 6,
order: 4,
},
// Employment Details
{
key: 'department',
type: 'select',
label: 'Department',
required: true,
gridSize: 6,
order: 5,
options: {
defaultValues: [
{ id: 1, name: 'Engineering' },
{ id: 2, name: 'Marketing' },
{ id: 3, name: 'Sales' },
{ id: 4, name: 'Human Resources' },
],
valueProp: 'id',
labelProp: 'name',
},
validators: [
{ type: 'required', message: 'Department is required' },
],
},
{
key: 'position',
type: 'text',
label: 'Position',
placeholder: 'Enter position',
required: true,
gridSize: 6,
order: 6,
validators: [
{ type: 'required', message: 'Position is required' },
],
},
{
key: 'startDate',
type: 'date',
label: 'Start Date',
required: true,
gridSize: 6,
order: 7,
validators: [
{ type: 'required', message: 'Start date is required' },
],
},
// Conditional Fields
{
key: 'isManager',
type: 'checkbox',
label: 'Is this person a manager?',
gridSize: 12,
order: 8,
},
{
key: 'teamSize',
type: 'number',
label: 'Team Size',
placeholder: 'Number of team members',
gridSize: 6,
order: 9,
conditionalLogic: [
{
dependsOn: 'isManager',
condition: 'equals',
value: true,
action: 'show',
},
],
validators: [
{ type: 'min', value: 1, message: 'Team size must be at least 1' },
],
},
{
key: 'managementExperience',
type: 'textarea',
label: 'Management Experience',
placeholder: 'Describe your management experience',
gridSize: 12,
order: 10,
conditionalLogic: [
{
dependsOn: 'isManager',
condition: 'equals',
value: true,
action: 'show',
},
],
},
// Additional Information
{
key: 'notes',
type: 'textarea',
label: 'Additional Notes',
placeholder: 'Any additional information',
gridSize: 12,
order: 11,
},
];
handleSubmit(formValue: any) {
this.isSubmitting = true;
console.log('Employee Data:', formValue);
// Simulate API call
setTimeout(() => {
this.isSubmitting = false;
alert('Employee registered successfully!');
}, 2000);
}
handleCancel() {
if (confirm('Are you sure you want to cancel?')) {
// Navigate back or reset form
console.log('Form cancelled');
}
}
}
```
## API Reference
### DynamicFormComponent
#### Properties
| Property | Type | Description |
|----------|------|-------------|
| `dynamicForm` | `FormGroup` | The underlying Angular FormGroup instance |
| `fieldVisibility` | `{ [key: string]: boolean }` | Object tracking field visibility state |
#### Methods
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| `submit()` | - | `void` | Submits the form if valid |
| `onCancel()` | - | `void` | Emits the formCancel event |
| `resetForm()` | - | `void` | Resets the form to initial values |
| `isFieldVisible(field)` | `FormFieldConfig` | `boolean` | Checks if a field is currently visible |
### DynamicFormService
The `DynamicFormService` provides utility methods for form management. It is `providedIn: 'root'`.
#### Methods
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| `createFormGroup(fields)` | `FormFieldConfig[]` | `FormGroup` | Creates a FormGroup from field configurations (handles `group` / `array` recursively) |
| `getInitialValues(fields)` | `FormFieldConfig[]` | `any` | Extracts initial values from field configurations |
| `getOptions(url, apiName?)` | `string, string?` | `Observable<any[]>` | Fetches options from an API via `RestService`; used for `select` / `radio` when `options.url` is set |
Nested forms use `DynamicFormGroupComponent` and `DynamicFormArrayComponent` internally. You configure them via `type: 'group'` / `type: 'array'` and `children`; you do not need to use these components directly.
## See Also
- [Form Validation](./form-validation.md)
- [Form Input Component](./form-input-component.md)
- [Dynamic Form Extensions](./dynamic-form-extensions.md)

BIN
docs/en/images/ai-management-chat-mcp-tools.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/en/images/ai-management-mcp-servers.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/en/images/ai-management-mcp-test-connection.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/images/ai-management-openai-anythingllm.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/en/images/ai-management-openai-anythingllm2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/en/images/ai-management-rag-chat.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/en/images/ai-management-rag-upload.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/en/images/ai-management-workspace-mcp-config.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/en/images/workspace-embedder.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/images/workspace-vector-store.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

293
docs/en/modules/ai-management/index.md

@ -49,6 +49,34 @@ abp add-package Volo.AIManagement.Ollama
If you need to integrate with a provider that isn't covered by the built-in packages, you can implement your own. See the [Implementing Custom AI Provider Factories](#implementing-custom-ai-provider-factories) section for details.
### Adding RAG Dependencies
> [!TIP]
> RAG is entirely optional. All other AI Management features work without any RAG dependencies installed.
Retrieval-Augmented Generation (RAG) support requires both an embedding provider and a vector store provider.
Install at least one embedding provider package:
```bash
abp add-package Volo.AIManagement.OpenAI
# or
abp add-package Volo.AIManagement.Ollama
```
Install at least one vector store package:
```bash
abp add-package Volo.AIManagement.VectorStores.MongoDB
# or
abp add-package Volo.AIManagement.VectorStores.Pgvector
# or
abp add-package Volo.AIManagement.VectorStores.Qdrant
```
> [!NOTE]
> Available provider names in workspace settings come from registered factories at startup. Built-in names are `OpenAI` and `Ollama` for embedding providers, and `MongoDb`, `Pgvector`, and `Qdrant` for vector stores.
## 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.
@ -71,6 +99,7 @@ The **AI Management Module** adds the following items to the "Main" menu:
* **AI Management**: Root menu item for AI Management module. (`AIManagement`)
* **Workspaces**: Workspace management page. (`AIManagement.Workspaces`)
* **MCP Servers**: MCP server management page. (`AIManagement.McpServers`)
`AIManagementMenus` class has the constants for the menu item names.
@ -93,6 +122,8 @@ You can create a new workspace or edit an existing workspace in this page. The w
* **Temperature**: Response randomness (0.0-1.0)
* **Application Name**: Associate with specific application
* **Required Permission**: Permission needed to use this workspace
* **Embedder Provider / Model**: Embedding generator used for RAG
* **Vector Store Provider / Settings**: Storage backend and connection settings for document vectors
#### Chat Interface
@ -105,6 +136,47 @@ The AI Management module includes a built-in chat interface for testing workspac
> Access the chat interface at: `/AIManagement/Workspaces/{WorkspaceName}`
#### MCP Servers
The [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) Servers page allows you to manage external MCP servers that can be used as tools by your AI workspaces. MCP enables AI models to interact with external services, databases, APIs, and more through a standardized protocol.
![ai-management-mcp-servers](../../images/ai-management-mcp-servers.png)
You can create, edit, delete, and test MCP server connections. Each MCP server supports one of the following transport types:
* **Stdio**: Runs a local command (e.g., `npx`, `dotnet`, `python`) with arguments and environment variables.
* **SSE**: Connects to a remote server using Server-Sent Events.
* **StreamableHttp**: Connects to a remote server using the Streamable HTTP transport.
For HTTP-based transports (SSE and StreamableHttp), you can configure authentication:
* **None**: No authentication.
* **API Key**: Sends an API key in the header.
* **Bearer**: Sends a Bearer token in the Authorization header.
* **Custom**: Sends a custom header name/value pair.
You can test the connection to an MCP server after creating it. The test verifies connectivity and lists available tools from the server.
![ai-management-mcp-test-connection](../../images/ai-management-mcp-test-connection.png)
Once MCP servers are defined, you can associate them with workspaces. Navigate to a workspace's edit page and configure which MCP servers should be available as tools for that workspace.
![ai-management-workspace-mcp-config](../../images/ai-management-workspace-mcp-config.png)
When a workspace has MCP servers associated, the AI model can invoke tools from those servers during chat conversations. Tool calls and results are displayed in the chat interface.
![ai-management-chat-mcp-tools](../../images/ai-management-chat-mcp-tools.png)
#### 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.
* Supported file extensions: `.txt`, `.md`, `.pdf`
* Maximum file size: `10 MB`
* Each uploaded file can be re-indexed individually or re-indexed in bulk per workspace
> Access the page from workspace details, or navigate to `/AIManagement/WorkspaceDataSources?WorkspaceId={WorkspaceId}`.
## Workspace Configuration
Workspaces are the core concept of the AI Management module. A workspace represents an AI provider configuration that can be used throughout your application.
@ -128,6 +200,12 @@ When creating or managing a workspace, you can configure the following propertie
| `RequiredPermissionName` | No | Permission required to use this workspace |
| `IsSystem` | No | Whether it's a system workspace (read-only) |
| `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") |
| `EmbedderApiKey` | No | API key for embedding provider |
| `EmbedderApiBaseUrl` | No | Custom embedding endpoint URL |
| `VectorStoreProvider` | No | Vector store provider name (e.g., "MongoDb", "Pgvector", "Qdrant") |
| `VectorStoreSettings` | No | Provider-specific connection setting string |
**\*Not required for system workspaces**
@ -202,16 +280,135 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe
* Workspace names **cannot contain spaces** (use underscores or camelCase)
* Workspace names are **case-sensitive**
## RAG with File Upload
The AI Management module supports RAG (Retrieval-Augmented Generation), which enables workspaces to answer questions based on the content of uploaded documents. When RAG is configured, the AI model searches the uploaded documents for relevant information before generating a response.
### Supported File Formats
* **PDF** (.pdf)
* **Markdown** (.md)
* **Text** (.txt)
Maximum file size: **10 MB**.
### Prerequisites
RAG requires an **embedder** and a **vector store** to be configured on the workspace:
* **Embedder**: Converts documents and queries into vector embeddings. You can use any provider that supports embedding generation (e.g., OpenAI `text-embedding-3-small`, Ollama `nomic-embed-text`).
* **Vector Store**: Stores and retrieves vector embeddings. Supported providers: **MongoDb**, **Pgvector**, and **Qdrant**.
### Configuring RAG on a Workspace
To enable RAG for a workspace, configure the embedder and vector store settings in the workspace edit page.
#### Configuring Embedder
![workspace-embedder](../../images/workspace-embedder.png)
The **Embedder** tab allows you to configure how documents and queries are converted into vector embeddings:
* **Embedder Provider**: The provider for generating embeddings (e.g., "OpenAI", "Ollama").
* **Embedder Model Name**: The embedding model (e.g., "text-embedding-3-small", "nomic-embed-text").
* **Embedder Base URL**: The endpoint URL for the embedder (optional if using the default endpoint).
#### Configuring Vector Store
![workspace-vector-store](../../images/workspace-vector-store.png)
The **Vector Store** section allows you to configure where the generated embeddings are stored and retrieved:
* **Vector Store Provider**: The vector store to use (e.g., "Pgvector", "MongoDb", "Qdrant").
* **Vector Store Settings**: The connection string for the vector store (e.g., `Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=myPassword`).
### Uploading Documents
![ai-management-rag-upload](../../images/ai-management-rag-upload.png)
Once RAG is configured on a workspace, you can upload documents through the workspace management UI. Uploaded documents are automatically processed -- their content is chunked, embedded, and stored in the configured vector store. You can then ask questions in the chat interface, and the AI model will use the uploaded documents as context.
![ai-management-rag-chat](../../images/ai-management-rag-chat.png)
### RAG Configuration & Indexing Flow
RAG is enabled per workspace when both embedding and vector store settings are configured.
#### VectorStoreSettings Format
`VectorStoreSettings` is passed to the selected provider factory as a connection setting string:
* `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`).
#### Document Processing Pipeline
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`).
#### Workspace Data Source HTTP API
The module exposes workspace data source endpoints under `/api/ai-management/workspace-data-sources`:
* `POST /workspace/{workspaceId}`: Upload a new file.
* `GET /by-workspace/{workspaceId}`: List data sources for a workspace.
* `GET /{id}`: Get a data source.
* `PUT /{id}`: Update data source metadata.
* `DELETE /{id}`: Delete data source and underlying blob.
* `GET /{id}/download`: Download original file.
* `POST /{id}/reindex`: Re-index a single file.
* `POST /workspace/{workspaceId}/reindex-all`: Re-index all files in a workspace.
#### 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`.
* The tool delegates to `IDocumentSearchService` (`DocumentSearchService` by default).
* The search currently uses `TopK = 5` chunks.
* If RAG retrieval fails, chat continues without injected context.
#### 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.
* Re-queues all workspace data sources for re-indexing.
## Permissions
The AI Management module defines the following permissions:
| Permission | Description | Default Granted To |
| -------------------------------- | ------------------------ | ------------------ |
| `AIManagement.Workspaces` | View workspaces | Admin role |
| `AIManagement.Workspaces.Create` | Create new workspaces | Admin role |
| `AIManagement.Workspaces.Update` | Edit existing workspaces | Admin role |
| `AIManagement.Workspaces.Delete` | Delete workspaces | Admin role |
| Permission | Description |
| -------------------------------- | ------------------------ |
| `AIManagement.Workspaces` | View workspaces |
| `AIManagement.Workspaces.Create` | Create new workspaces |
| `AIManagement.Workspaces.Update` | Edit existing workspaces |
| `AIManagement.Workspaces.Delete` | Delete workspaces |
| `AIManagement.Workspaces.Playground` | Access workspace chat playground |
| `AIManagement.Workspaces.ManagePermissions` | Manage workspace resource permissions |
| `AIManagement.McpServers` | View MCP servers |
| `AIManagement.McpServers.Create` | Create new MCP servers |
| `AIManagement.McpServers.Update` | Edit existing MCP servers|
| `AIManagement.McpServers.Delete` | Delete MCP servers |
The module also defines workspace data source permissions for RAG document operations:
| Permission | Description |
| --------------------------------------------- | ----------------------------------- |
| `AIManagement.WorkspaceDataSources` | View workspace data sources |
| `AIManagement.WorkspaceDataSources.Create` | Upload documents |
| `AIManagement.WorkspaceDataSources.Update` | Update data source metadata |
| `AIManagement.WorkspaceDataSources.Delete` | Delete uploaded documents |
| `AIManagement.WorkspaceDataSources.Download` | Download original uploaded file |
| `AIManagement.WorkspaceDataSources.ReIndex` | Re-index one or all workspace files |
### Workspace-Level Permissions
@ -307,6 +504,7 @@ In this scenario, you install the AI Management module with its database layer,
- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`)
- `Volo.AIManagement.OpenAI` (or another AI provider package)
- For RAG: `Volo.AIManagement.VectorStores.MongoDB` or another vector store package (`Volo.AIManagement.VectorStores.Pgvector`, `Volo.AIManagement.VectorStores.Qdrant`)
**Full installation (with UI and API):**
@ -315,6 +513,7 @@ In this scenario, you install the AI Management module with its database layer,
- `Volo.AIManagement.HttpApi`
- `Volo.AIManagement.Web` (for management UI)
- `Volo.AIManagement.OpenAI` (or another AI provider package)
- For RAG: `Volo.AIManagement.VectorStores.MongoDB` or another vector store package (`Volo.AIManagement.VectorStores.Pgvector`, `Volo.AIManagement.VectorStores.Qdrant`)
> Note: `Volo.AIManagement.EntityFrameworkCore` transitively includes `Volo.AIManagement.Domain` and `Volo.Abp.AI.AIManagement` packages.
@ -485,6 +684,71 @@ Your application acts as a proxy, forwarding these requests to the AI Management
| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally |
| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services |
### OpenAI-Compatible API
The AI Management module exposes an **OpenAI-compatible REST API** at the `/v1` path. This allows any application or tool that supports the OpenAI API format -- such as [AnythingLLM](https://anythingllm.com/), [Open WebUI](https://openwebui.com/), [Dify](https://dify.ai/), or custom scripts using the OpenAI SDK -- to connect directly to your AI Management instance.
![ai-management-openai-anythingllm](../../images/ai-management-openai-anythingllm.png)
Each AI Management **workspace** appears as a selectable model in the client application. The workspace's configured AI provider handles the actual inference transparently.
![ai-management-openai-anythingllm2](../../images/ai-management-openai-anythingllm2.png)
#### Available Endpoints
| Endpoint | Method | Description |
| ---------------------------- | ------ | ----------------------------------------------- |
| `/v1/chat/completions` | POST | Chat completions (streaming and non-streaming) |
| `/v1/completions` | POST | Legacy text completions |
| `/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.
#### Usage
The general pattern for connecting any OpenAI-compatible client:
* **Base URL**: `https://<your-app-url>/v1`
* **API Key**: A valid Bearer token obtained from your application's authentication endpoint.
* **Model**: One of the workspace names returned by `GET /v1/models`.
**Example with the OpenAI Python SDK:**
```python
from openai import OpenAI
client = OpenAI(
base_url="https://localhost:44336/v1",
api_key="<your-token>"
)
response = client.chat.completions.create(
model="MyWorkspace",
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
```
**Example with cURL:**
```bash
curl -X POST https://localhost:44336/v1/chat/completions \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"model": "MyWorkspace",
"messages": [{"role": "user", "content": "Hello!"}]
}'
```
> The OpenAI-compatible endpoints are available from both the `Volo.AIManagement.Client.HttpApi` and `Volo.AIManagement.HttpApi` packages, depending on your deployment scenario.
## Client Usage
@ -887,6 +1151,7 @@ The `ChatClientCreationConfiguration` object provides the following properties f
| `IsActive` | bool | Whether the workspace is active |
| `IsSystem` | bool | Whether it's a system workspace |
| `RequiredPermissionName` | string? | Permission required to use this workspace |
| `HasEmbedderConfiguration` | bool | Whether the workspace has embedder/RAG configuration |
### Example: Azure OpenAI Factory
@ -952,24 +1217,35 @@ The AI Management module follows Domain-Driven Design principles and has a well-
#### Aggregates
- **Workspace**: The main aggregate root representing an AI workspace configuration.
- **McpServer**: Aggregate root representing an MCP server configuration.
#### Repositories
The following custom repositories are defined:
- `IWorkspaceRepository`: Repository for workspace management with custom queries.
- `IMcpServerRepository`: Repository for MCP server management with custom queries.
#### Domain Services
- `ApplicationWorkspaceManager`: Manages workspace operations and validations.
- `McpServerManager`: Manages MCP server operations and validations.
- `WorkspaceConfigurationStore`: Retrieves workspace configuration with caching. Implements `IWorkspaceConfigurationStore` interface.
- `ChatClientResolver`: Resolves the appropriate `IChatClient` implementation for a workspace.
- `EmbeddingClientResolver`: Resolves the appropriate embedding client for a workspace (used by RAG).
- `IMcpToolProvider`: Resolves and aggregates MCP tools from all connected MCP servers for a workspace.
- `IMcpServerConfigurationStore`: Retrieves MCP server configurations for workspaces.
- `VectorStoreResolver`: Resolves the configured vector store implementation for a workspace.
- `VectorStoreInitializer`: Initializes vector store artifacts for newly configured workspaces.
- `RagService`: Generates query embeddings and retrieves relevant chunks from vector stores.
- `DocumentProcessingManager`: Extracts and chunks uploaded document contents.
#### Integration Services
The module exposes the following integration services for inter-service communication:
- `IAIChatCompletionIntegrationService`: Executes AI chat completions remotely.
- `IAIEmbeddingIntegrationService`: Generates embeddings remotely.
- `IWorkspaceConfigurationIntegrationService`: Retrieves workspace configuration for remote setup.
- `IWorkspaceIntegrationService`: Manages workspaces remotely.
@ -980,8 +1256,11 @@ The module exposes the following integration services for inter-service communic
#### Application Services
- `WorkspaceAppService`: CRUD operations for workspace management.
- `McpServerAppService`: CRUD operations for MCP server management.
- `WorkspaceDataSourceAppService`: Upload, list, download, and re-index workspace documents.
- `ChatCompletionClientAppService`: Client-side chat completion services.
- `AIChatCompletionIntegrationService`: Integration service for remote AI execution.
- `OpenAICompatibleChatAppService`: OpenAI-compatible API endpoint services.
- `AIChatCompletionIntegrationService`: Integration service for remote AI execution. Returns RAG metadata fields (`HasRagContext`, `RagChunkCount`) and tool call details. Note: `RagChunkCount` reflects the number of RAG tool invocations, not the number of retrieved chunks.
### Caching

64
docs/en/modules/permission-management.md

@ -45,12 +45,23 @@ You can integrate this dialog into your own application to manage permissions fo
#### MVC / Razor Pages
Use the `abp.ModalManager` to open the resource permission management dialog:
First, add the `resource-permission-management-modal.js` script to your page. This script registers the `ResourcePermissionManagement` modal class used by `abp.ModalManager`:
````html
@section scripts
{
<abp-script src="/Pages/MyBook/Index.js"/>
<abp-script src="/Pages/AbpPermissionManagement/resource-permission-management-modal.js" />
}
````
Then use the `abp.ModalManager` to open the resource permission management dialog:
````javascript
var _resourcePermissionsModal = new abp.ModalManager(
abp.appPath + 'AbpPermissionManagement/ResourcePermissionManagementModal'
);
var _resourcePermissionsModal = new abp.ModalManager({
viewUrl: abp.appPath + 'AbpPermissionManagement/ResourcePermissionManagementModal',
modalClass: 'ResourcePermissionManagement'
});
// Open the modal for a specific resource
_resourcePermissionsModal.open({
@ -335,6 +346,51 @@ Configure<PermissionManagementOptions>(options =>
});
````
#### Controlling Provider Availability
You can control whether a provider is active in a given context by overriding `IsAvailableAsync()`. When a provider returns `false`, it is completely excluded from all read, write, and UI listing operations. This is useful for host-only providers that should not be visible or writable in a tenant context.
````csharp
public class CustomResourcePermissionManagementProvider
: ResourcePermissionManagementProvider
{
public override string Name => "Custom";
// ...constructor...
public override Task<bool> IsAvailableAsync()
{
// Only available for the host, not for tenants
return Task.FromResult(CurrentTenant.Id == null);
}
}
````
The same `IsAvailableAsync()` method is available on `IResourcePermissionProviderKeyLookupService`, which controls whether the provider appears in the UI provider picker:
````csharp
public class CustomResourcePermissionProviderKeyLookupService
: IResourcePermissionProviderKeyLookupService, ITransientDependency
{
public string Name => "Custom";
public ILocalizableString DisplayName { get; }
protected ICurrentTenant CurrentTenant { get; }
public CustomResourcePermissionProviderKeyLookupService(ICurrentTenant currentTenant)
{
CurrentTenant = currentTenant;
}
public Task<bool> IsAvailableAsync()
{
return Task.FromResult(CurrentTenant.Id == null);
}
// ...SearchAsync implementations...
}
````
## Permission Value Providers
Permission value providers are used to determine if a permission is granted. They are different from management providers: **value providers** are used when *checking* permissions, while **management providers** are used when *setting* permissions.

13
docs/en/package-version-changes.md

@ -1,5 +1,18 @@
```json
//[doc-seo]
{
"Description": "Explore the latest version changes for ABP Framework packages, including updates and improvements in dependencies for seamless development."
}
```
# Package Version Changes
## 10.2.0-rc.1
| Package | Old Version | New Version | PR |
|---------|-------------|-------------|-----|
| MongoDB.Driver | 3.6.0 | 3.7.0 | #25003 |
## 10.2.0-preview
| Package | Old Version | New Version | PR |

71
docs/en/release-info/migration-guides/abp-10-1.md

@ -11,6 +11,34 @@ This document is a guide for upgrading ABP v10.0 solutions to ABP v10.1. There a
## Open-Source (Framework)
### Swashbuckle.AspNetCore Upgraded to v10
In this version, the `Swashbuckle.AspNetCore` package has been upgraded to v10. This upgrade also updates the underlying `Microsoft.OpenApi` package to v2.x, which introduces breaking changes that may affect your application if you have any custom Swagger/OpenAPI configuration such as custom filters, schema customizations, or security requirements.
**Required Actions:**
- Update any `using` directives that reference types from the `Microsoft.OpenApi.Models` namespace to also include the new `Microsoft.OpenApi` namespace (interfaces such as `IOpenApiSchema` are now in `Microsoft.OpenApi`, while concrete types such as `OpenApiSchema` remain in `Microsoft.OpenApi.Models`).
- Update filter/handler method signatures to accept the new interfaces (e.g. `IOpenApiSchema` instead of `OpenApiSchema`). When you need to mutate properties, cast to the concrete type first. For example:
```csharp
public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
{
if (schema is OpenApiSchema openApiSchema)
{
// Properties are only mutable on the concrete type
openApiSchema.Type = JsonSchemaType.String;
}
}
```
- Replace usage of the `OpenApiSchema.Type` property using a string (e.g. `"string"` or `"boolean"`) with the `JsonSchemaType` flags enumeration.
- Replace usage of the `OpenApiSchema.Nullable` property by OR-ing the `JsonSchemaType.Null` value to `OpenApiSchema.Type` (e.g. `schema.Type |= JsonSchemaType.Null;`).
- Update any use of `.Reference` properties (e.g. `OpenApiSchema.ReferenceV3`) to use the new `*Reference` class instead (e.g. `OpenApiSchemaReference`).
- Update any use of `AddSecurityRequirement()` to use a `Func<OpenApiDocument, OpenApiSecurityRequirement>`.
For a full list of migration steps, please refer to the [Swashbuckle.AspNetCore v10 migration guide](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/docs/migrating-to-v10.md#migration-overview).
> See [#24255](https://github.com/abpframework/abp/pull/24255) for more details.
### Add New EF Core Migrations for Password History/User Passkey Entities
In this version, we added password history/ user passkeys support to the [Identity PRO Module](../../modules/identity-pro.md) to enhance security compliance. A new `IdentityUserPasswordHistory` entity has been added to store previous password hashes, preventing users from reusing recent passwords. Additionally, we have introduced an `IdentityUserPasskey `entity to support passkey-based authentication.
@ -19,6 +47,49 @@ In this version, we added password history/ user passkeys support to the [Identi
> See [#23894](https://github.com/abpframework/abp/pull/23894) for more details.
### Angular Version Upgraded to v21
ABP now targets Angular v21 (up from v20). For existing Angular projects, apply these changes:
- **TypeScript:** Update to `~5.9.0`
- **main.ts:** Add `provideZoneChangeDetection()` to the bootstrap config:
```ts
// angular/src/main.ts
bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideZoneChangeDetection(), ...appConfig.providers],
}).catch(err => console.error(err));
```
- **tsconfig.json:** Align with the new property formats to avoid build issues:
```json
/* angular/tsconfig.json */
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"skipLibCheck": true,
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "dom", "esnext.disposable"],
"paths": {},
"useDefineForClassFields": false
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false
}
}
```
For more details, see the [Angular version reference](https://angular.dev/reference/versions#actively-supported-versions).
## PRO
> Please check the **Open-Source (Framework)** section before reading this section. The listed topics might affect your application and you might need to take care of them.

23
docs/en/release-info/migration-guides/abp-10-2.md

@ -13,7 +13,7 @@ This document is a guide for upgrading ABP v10.1 solutions to ABP v10.2. There a
## Open-Source (Framework)
This version only requires changes on the open-source side. **There are no breaking changes on the PRO side.**
This version contains the following changes on the open-source side:
### Add New EF Core Migrations for Audit Logging Module
@ -109,6 +109,13 @@ x.SetBasePath("/tickerq");
x.WithHostAuthentication();
```
#### Angular Aria feature implementation
ABP now uses Angular's ARIA support for accessible tabs. Add the `@angular/aria` package (version `~21.1.0`) to your Angular project.
> See [#24684](https://github.com/abpframework/abp/issues/24684) for details.
#### Other changes
- **Typo fix:** `result.IsSucceded``result.IsSucceeded`
@ -116,3 +123,17 @@ x.WithHostAuthentication();
- **BatchRunCondition renamed:** `BatchRunCondition? BatchRunCondition``RunCondition? RunCondition` in `AbpBackgroundJobsTimeTickerConfiguration`
> See [#24916](https://github.com/abpframework/abp/pull/24916) for more details.
## Pro
There are no breaking changes on the PRO side. However, if you use the Identity Pro module with Entity Framework Core, you need to make the following update:
### Add `UserInvitations` DbSet for Identity Pro EF Core DbContexts
If your solution uses the Identity Pro module with Entity Framework Core, and your DbContext base class implements `IIdentityProDbContext`, add the new `UserInvitations` DbSet property to your DbContext:
```csharp
public DbSet<UserInvitation> UserInvitations { get; set; }
```
This is required for Identity Pro module users (it is installed by default in most/every startup templates). After adding the property, create a new EF Core migration and apply it to your database.

20
docs/en/release-info/release-notes.md

@ -14,9 +14,27 @@ Also see the following notes about ABP releases:
* [ABP Studio release notes](../studio/release-notes.md)
* [Change logs for ABP pro packages](https://abp.io/pro-releases)
## 10.2 (2026-02-24)
> This is currently an RC (release-candidate) and you can see the detailed **[blog post / announcement](https://abp.io/community/articles/announcing-abp-10-2-release-candidate-05zatjfq)** for the v10.2 release.
* Multi-Tenant Account Usage: Shared User Accounts
* Prevent Privilege Escalation: Assignment Restrictions for Roles and Permissions
* `ClientResourcePermissionValueProvider` for OAuth/OpenIddict
* Angular: Hybrid Localization Support
* Angular: Extensible Table Row Detail
* Angular: CMS Kit Module Features
* Blazor: Upgrade to Blazorise 2.0
* Identity: Single Active Token Providers
* TickerQ Package Upgrade to 10.1.1
* [AI Management Module](../modules/ai-management/index.md): MCP (Model Context Protocol) Support
* [AI Management Module](../modules/ai-management/index.md): RAG with File Upload
* [AI Management Module](../modules/ai-management/index.md): OpenAI-Compatible Chat Endpoint
* [File Management Module](../modules/file-management.md): Resource-Based Authorization
## 10.1 (2026-01-06)
> This is currently a RC (release-candidate) and you can see the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-1-release-candidate-cyqui19d)** for the v10.1 release.
See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-1-stable-release-z4xfn1me)** for the v10.1 release.
* Resource-Based Authorization
* Introducing the [TickerQ Background Worker Provider](../framework/infrastructure/background-workers/tickerq.md)

17
docs/en/release-info/road-map.md

@ -1,7 +1,7 @@
```json
//[doc-seo]
{
"Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.1, launching January 2026."
"Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.3, planned for April 2026."
}
```
@ -11,35 +11,32 @@ This document provides a road map, release schedule, and planned features for th
## Next Versions
### v10.2
### v10.3
The next version will be 10.2 and planned to release the stable 10.2 version in April 2026. We will be mostly working on the following topics:
The next version will be 10.3 and is planned to be released as a stable version in April 2026. We will be mostly working on the following topics:
* Framework
* Resource-Based Authorization Improvements
* Handle datetime/timezon in `AbpExtensibleDataGrid` Component
* Handle datetime/timezone in `AbpExtensibleDataGrid` Component
* Upgrading 3rd-party Dependencies
* Enhancements in the Core Points
* ABP Suite
* Creating enums on-the-fly (without needing to create manually on the code side)
* Improvements on the generated codes for nullability
* Improvements on Master-Detail Page Desing (making it more compact)
* Improvements on Master-Detail Page Design (making it more compact)
* Improvements One-To-Many Scenarios
* File Upload Modal Enhancements
* ABP Studio
* Allow to Directly Create New Solutions with ABP's RC (Release Candidate) Versions
* Integrate AI Management Module with all solution templates and UIs
* Integrate AI Management Module with all solution templates and UIs (for Blazor & Angular UIs)
* Automate More Details on New Service Creation for a Microservice Solution
* Allow to Download ABP Samples from ABP Studio
* Task Panel Documentation
* Support Multiple Concurrent Kubernetes Deployment/Integration Scenarios
* Improve the Module Installation Experience / Installation Guides
* Application Modules
* AI Management: MCP & RAG Supports
* File Management: Using Resource-Based Permission (on file-sharing and more...)
* AI Management: Chat History & Visual Improvements on the playground
* CMS Kit: Enhancements for Some Features (Rating, Dynamic Widgets, FAQ and more...)
* UI/UX Improvements on Existing Application Modules

BIN
docs/en/tutorials/modular-crm/images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/en/tutorials/modular-crm/images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/en/tutorials/modular-crm/images/catalog-module-vs-code-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
docs/en/tutorials/modular-crm/images/vscode-catalog-index-razor-blazor-webapp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

7
docs/en/tutorials/modular-crm/index.md

@ -7,6 +7,13 @@
# Modular Monolith Application Development Tutorial
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{

15
docs/en/tutorials/modular-crm/part-01.md

@ -7,6 +7,13 @@
# Creating the Initial Solution
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -28,7 +35,7 @@ In this first part of this tutorial, we will create a new ABP solution with modu
Follow the *[Get Started](../../get-started/single-layer-web-application.md)* guide to create a single layer web application with the following configuration:
* **Solution name**: `ModularCrm`
* **UI Framework**: ASP.NET Core MVC / Razor Pages
* **UI Framework**: {{if UI == "MVC"}}ASP.NET Core MVC / Razor Pages{{else if UI == "BlazorWebApp"}}Blazor WebApp{{end}}
* **Database Provider**: Entity Framework Core
You can select the other options based on your preference but at the **Modularity** step, check the _Setup as a modular solution_ option and add a new **Standard Module** named `ModularCrm.Catalog`:
@ -62,12 +69,16 @@ Initially, you see a `ModularCrm` solution with two solution folders:
If you expand it, you can see the .NET projects (ABP Studio Packages) of the `ModularCrm.Catalog` module:
{{if UI == "MVC"}}
![abp-studio-catalog-module-expanded-in-solution-explorer](images/abp-studio-catalog-module-expanded-in-solution-explorer.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-catalog-module-expanded-in-solution-explorer](images/abp-studio-catalog-module-expanded-in-solution-explorer-blazor-webapp.png)
{{end}}
- `ModularCrm.Catalog`: The main module project that contains your [entities](../../framework/architecture/domain-driven-design/entities.md), [application service](../../framework/architecture/domain-driven-design/application-services.md) implementations and other business objects
- `ModularCrm.Catalog.Contracts`: Basically contains [application service](../../framework/architecture/domain-driven-design/application-services.md) interfaces and [DTOs](../../framework/architecture/domain-driven-design/data-transfer-objects.md). These interfaces then can be used by client modules for integration purposes or by the user interface to perform use cases related to that module
- `ModularCrm.Catalog.Tests`: Unit and integration tests (if you selected the _Include Tests_ option) for that module
- `ModularCrm.Catalog.UI`: Contains user interface pages and components for the module
- {{if UI == "MVC"}}`ModularCrm.Catalog.UI`: Contains user interface pages and components for the module{{else if UI == "BlazorWebApp"}}`ModularCrm.Catalog.Blazor`: Contains Blazor WebApp user interface pages and components for the module{{end}}
## Summary

15
docs/en/tutorials/modular-crm/part-02.md

@ -7,6 +7,13 @@
# Setting Up the Catalog Module
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -25,7 +32,7 @@ In this part, you will install the `ModularCrm.Catalog` module to the main appli
## Installing the Catalog Module to the Main Application
A module does not contain an executable application inside. The `ModularCrm.Catalog.UI` project is just a class library project, not an executable web application. A module should be installed in an executable application to run it.
A module does not contain an executable application inside. The {{if UI == "MVC"}}`ModularCrm.Catalog.UI`{{else if UI == "BlazorWebApp"}}`ModularCrm.Catalog.Blazor`{{end}} project is just a class library project, not an executable web application. A module should be installed in an executable application to run it.
> **Ensure that the web application is not running in [Solution Runner](../../studio/running-applications.md) or in your IDE. Installing a module to a running application will produce errors.**
@ -41,9 +48,13 @@ Select the `ModularCrm.Catalog` module and check the *Install this module* optio
When you click the *OK* button, ABP Studio opens the *Install Module* dialog:
{{if UI == "MVC"}}
![abp-studio-module-installation-dialog-for-catalog](images/abp-studio-module-installation-dialog-for-catalog.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-module-installation-dialog-for-catalog](images/abp-studio-module-installation-dialog-for-catalog-blazor-webapp.png)
{{end}}
Select the `ModularCrm.Catalog` and `ModularCrm.Catalog.UI` packages from the left area and ensure the `ModularCrm` package from the middle area was checked as shown in the preceding figure. Finally, click _OK_.
Select the `ModularCrm.Catalog` and {{if UI == "MVC"}}`ModularCrm.Catalog.UI`{{else if UI == "BlazorWebApp"}}`ModularCrm.Catalog.Blazor`{{end}} packages from the left area. {{if UI == "MVC"}}Ensure `ModularCrm` was checked in the middle area as shown in the preceding figure.{{else if UI == "BlazorWebApp"}}For `ModularCrm.Catalog`, ensure `ModularCrm` is checked. For `ModularCrm.Catalog.Blazor`, ensure both `ModularCrm` and `ModularCrm.Client` are checked in the middle area as shown in the preceding figure.{{end}} Finally, click _OK_.
## Building the Main Application

90
docs/en/tutorials/modular-crm/part-03.md

@ -7,6 +7,13 @@
# Building the Catalog Module
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -35,7 +42,11 @@ Open the `ModularCrm.Catalog` module in your favorite IDE. You can right-click t
The `ModularCrm.Catalog` .NET solution should look like the following figure:
{{if UI == "MVC"}}
![catalog-module-vs-code](images/catalog-module-vs-code.png)
{{else if UI == "BlazorWebApp"}}
![catalog-module-vs-code](images/catalog-module-vs-code-blazor-webapp.png)
{{end}}
Add a new `Product` class under the `ModularCrm.Catalog` project:
@ -344,6 +355,8 @@ public partial class ProductToProductDtoMapper : MapperBase<Product, ProductDto>
### Exposing Application Services as HTTP API Controllers
{{if UI == "MVC"}}
> This application doesn't need to expose any functionality as HTTP API, because all the module integration and communication will be done in the same process as a natural aspect of a monolith modular application. However, in this section, we will create HTTP APIs because;
>
> 1. We will use these HTTP API endpoints in development to create some example data.
@ -351,6 +364,8 @@ public partial class ProductToProductDtoMapper : MapperBase<Product, ProductDto>
>
> So, follow the instructions in this section and expose the product application service as an HTTP API endpoint.
{{end}}
To create HTTP API endpoints for the catalog module, you have two options:
* You can create a regular ASP.NET Core Controller class in the `ModularCrm.Catalog` project, inject `IProductAppService` and create wrapper methods for each public method of the product application service. You will do this later while you create the Ordering module. (Also, you can check the `SampleController` class under the **Samples** folder in the `ModularCrm.Catalog` project for an example)
@ -372,6 +387,34 @@ This will tell the ABP framework to create API controllers for the application s
Now, ABP will automatically expose the application services defined in the `ModularCrm.Catalog` project as API controllers. The next section will use these API controllers to create some example products.
{{if UI == "BlazorWebApp"}}
### Configuring Client Proxies for the Catalog Module
Since the Blazor WebApp template has a separate `ModularCrm.Client` project, configure HTTP client proxies for the Catalog contracts in the `ModularCrmClientModule` class:
````csharp
using ModularCrm.Catalog;
[DependsOn(
typeof(CatalogContractsModule)
// ...other dependencies
)]
public class ModularCrmClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
...
context.Services.AddHttpClientProxies(typeof(ModularCrmContractsModule).Assembly);
context.Services.AddHttpClientProxies(typeof(CatalogContractsModule).Assembly); // NEW: ADD HttpClientProxies
}
}
````
Also ensure the `ModularCrm.Catalog.Blazor` package is installed for both the `ModularCrm` and `ModularCrm.Client` projects.
{{end}}
### Creating Example Products
This section will create a few example products using the [Swagger UI](../../framework/api-development/swagger.md). Thus, you will have some sample products to show on the UI.
@ -406,6 +449,8 @@ As a first step, you can stop the application on ABP Studio's Solution Runner if
### Creating the Products Page
{{if UI == "MVC"}}
Open the `ModularCrm.Catalog` .NET solution in your IDE, and find the `Pages/Catalog/Index.cshtml` file under the `ModularCrm.Catalog.UI` project:
![vscode-catalog-cshtml](images/vscode-catalog-cshtml.png)
@ -461,7 +506,50 @@ Here, you simply use the `IProductAppService` to get a list of all products and
</abp-card>
````
Right-click the `ModularCrm` application on ABP Studio's solution runner and select the *Start* command:
{{else if UI == "BlazorWebApp"}}
Open the `ModularCrm.Catalog` .NET solution in your IDE, and find the `Pages/Catalog/Index.razor` file under the `ModularCrm.Catalog.Blazor` project.
![vscode-catalog-index-razor-blazor-webapp](images/vscode-catalog-index-razor-blazor-webapp.png)
Replace the `Index.razor` file with the following content:
````razor
@page "/catalog"
@using System.Collections.Generic
@using System.Threading.Tasks
@using ModularCrm.Catalog
@inject IProductAppService ProductAppService
<h1>Products</h1>
<Card>
<CardBody>
<ListGroup>
@foreach (var product in Products)
{
<ListGroupItem>
@product.Name <span class="text-muted">(stock: @product.StockCount)</span>
</ListGroupItem>
}
</ListGroup>
</CardBody>
</Card>
@code {
private List<ProductDto> Products { get; set; } = new();
protected override async Task OnInitializedAsync()
{
Products = await ProductAppService.GetListAsync();
}
}
````
Here, you inject `IProductAppService`, get all products in `OnInitializedAsync`, and then render the result in a simple list.
{{end}}
Right-click the `ModularCrm` application on ABP Studio's Solution Runner and select the *Start* command:
![abp-studio-build-and-restart-application](images/abp-studio-build-and-restart-application.png)

23
docs/en/tutorials/modular-crm/part-04.md

@ -7,6 +7,13 @@
# Creating the Initial Ordering Module
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -39,9 +46,13 @@ That command opens a dialog to define the properties of the new module:
Set `ModularCrm.Ordering` as the *Module name*, leave the *Output folder* as is and click the *Next* button.
{{if UI == "MVC"}}
![abp-studio-add-new-standard-module-ui-dialog](images/abp-studio-add-new-standard-module-ui-dialog.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-add-new-standard-module-ui-dialog](images/abp-studio-add-new-standard-module-ui-dialog-blazor-webapp.png)
{{end}}
You can choose the type of UI you want to support in your module or select *No UI* if you don't need a user interface. In this example, we'll select the *MVC* option and click *Next*.
You can choose the type of UI you want to support in your module or select *No UI* if you don't need a user interface. In this example, we'll select the {{if UI == "MVC"}}*MVC*{{else if UI == "BlazorWebApp"}}*Blazor WebApp*{{end}} option and click *Next*.
![abp-studio-add-new-standard-module-db-dialog](images/abp-studio-add-new-standard-module-db-dialog.png)
@ -53,7 +64,11 @@ You can include or not include unit tests for the new module here. We are unchec
Here is the final solution structure after adding the `ModularCrm.Ordering` module:
{{if UI == "MVC"}}
![abp-studio-modular-crm-with-standard-module](images/abp-studio-modular-crm-with-standard-module.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-modular-crm-with-standard-module](images/abp-studio-modular-crm-with-standard-module-blazor-webapp.png)
{{end}}
## Installing into the Main Application
@ -71,9 +86,13 @@ That command opens the *Import Module* dialog:
Select the `ModularCrm.Ordering` module and check the *Install this module* option as shown in the preceding figure. When you click the OK button, a new dialog is shown to select the packages to install:
{{if UI == "MVC"}}
![abp-studio-install-module-dialog](images/abp-studio-install-module-dialog-v2.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-install-module-dialog](images/abp-studio-install-module-dialog-blazor-webapp.png)
{{end}}
Select the `ModularCrm.Ordering` and `ModularCrm.Ordering.UI` packages from the left area and ensure the `ModularCrm` package from the middle area was checked as shown in the preceding figure. Finally, click _OK_.
Select the `ModularCrm.Ordering` and {{if UI == "MVC"}}`ModularCrm.Ordering.UI`{{else if UI == "BlazorWebApp"}}`ModularCrm.Ordering.Blazor`{{end}} packages from the left area. {{if UI == "MVC"}}Ensure `ModularCrm` was checked in the middle area as shown in the preceding figure.{{else if UI == "BlazorWebApp"}}For `ModularCrm.Ordering`, ensure `ModularCrm` is checked. For `ModularCrm.Ordering.Blazor`, ensure both `ModularCrm` and `ModularCrm.Client` are checked in the middle area as shown in the preceding figure.{{end}} Finally, click _OK_.
## Summary

124
docs/en/tutorials/modular-crm/part-05.md

@ -7,6 +7,13 @@
# Building the Ordering Module
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -357,6 +364,35 @@ Configure<AbpAspNetCoreMvcOptions>(options =>
This will tell the ABP framework to create API controllers for the application services in the `ModularCrm.Ordering` assembly.
{{if UI == "BlazorWebApp"}}
### Configuring Client Proxies for the Ordering Module
In the `ModularCrm.Client` project, configure HTTP client proxies for the Ordering contracts in the `ModularCrmClientModule` class:
````csharp
using ModularCrm.Ordering;
[DependsOn(
typeof(OrderingContractsModule)
// ...other dependencies
)]
public class ModularCrmClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
...
context.Services.AddHttpClientProxies(typeof(ModularCrmContractsModule).Assembly);
context.Services.AddHttpClientProxies(typeof(CatalogContractsModule).Assembly);
context.Services.AddHttpClientProxies(typeof(OrderingContractsModule).Assembly); // NEW: ADD HttpClientProxies
}
}
````
Also ensure the `ModularCrm.Ordering.Blazor` package is installed for both the `ModularCrm` and `ModularCrm.Client` projects.
{{end}}
### Creating Example Orders
This section will create a few example orders using the [Swagger UI](../../framework/api-development/swagger.md). Thus, you will have some sample orders to show on the UI.
@ -379,12 +415,14 @@ If you check the database, you should see the entities created in the *Orders* t
## Creating the User Interface
In this section, you will create a very simple user interface to demonstrate how to build UI in the catalog module and make it work in the main application.
In this section, you will create a very simple user interface to demonstrate how to build UI in the ordering module and make it work in the main application.
As a first step, you can stop the application on ABP Studio's Solution Runner if it is currently running.
### Creating the Orders Page
{{if UI == "MVC"}}
Replace the `Index.cshtml.cs` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.UI` project with the following code block:
````csharp
@ -481,6 +519,90 @@ public class OrderingMenuContributor : IMenuContributor
> You can check the [menu documentation](../../framework/ui/mvc-razor-pages/navigation-menu.md) to learn more about manipulating menu items.
{{else if UI == "BlazorWebApp"}}
Replace the `Index.razor` content in the `Pages/Ordering` folder of the `ModularCrm.Ordering.Blazor` project with the following code block:
````razor
@page "/ordering"
@using System.Collections.Generic
@using System.Threading.Tasks
@using ModularCrm.Ordering
@inject IOrderAppService OrderAppService
<h1>Orders</h1>
<Card>
<CardBody>
<ListGroup>
@foreach (var order in Orders)
{
<ListGroupItem>
<strong>Customer:</strong> @order.CustomerName <br />
<strong>Product:</strong> @order.ProductId <br />
<strong>State:</strong> @order.State
</ListGroupItem>
}
</ListGroup>
</CardBody>
</Card>
@code {
private List<OrderDto> Orders { get; set; } = new();
protected override async Task OnInitializedAsync()
{
Orders = await OrderAppService.GetListAsync();
}
}
````
This page shows a list of orders on the UI. You haven't created a UI to create new orders, and we will not do it to keep this tutorial simple. If you want to learn how to create advanced UIs with ABP, please follow the [Book Store tutorial](../book-store/index.md).
### Editing the Menu Item
ABP provides a modular navigation [menu system](../../framework/ui/blazor/navigation-menu.md) where each module can contribute to the main menu dynamically.
Edit the `OrderingMenuContributor` class in the `ModularCrm.Ordering.Blazor` project:
````csharp
using System.Threading.Tasks;
using Volo.Abp.UI.Navigation;
namespace ModularCrm.Ordering.Blazor.Menus;
public class OrderingMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
context.Menu.AddItem(
new ApplicationMenuItem(
OrderingMenus.Prefix, // Unique menu id
"Orders", // Menu display text
"/ordering", // URL
"fa-solid fa-basket-shopping" // Icon CSS class
)
);
return Task.CompletedTask;
}
}
````
`OrderingMenuContributor` implements the `IMenuContributor` interface, which forces us to implement the `ConfigureMenuAsync` method. In that method, you can manipulate the menu items (add new menu items, remove existing menu items or change the properties of existing menu items). The `ConfigureMenuAsync` method is executed whenever the menu is rendered on the UI, so you can dynamically decide how to manipulate the menu items.
> You can check the [menu documentation](../../framework/ui/blazor/navigation-menu.md) to learn more about manipulating menu items.
{{end}}
### Building the Application
Now, you will run the application to see the result. Please stop the application if it is already running. Then open the *Solution Runner* panel, right-click the `ModularCrm` application, and select the *Build* -> *Graph Build* command:

55
docs/en/tutorials/modular-crm/part-06.md

@ -7,6 +7,13 @@
# Integrating the Modules: Implementing Integration Services
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -138,7 +145,11 @@ Open the ABP Studio UI and stop the application if it is already running. Then o
In the opening dialog, select the *This solution* tab, find and check the `ModularCrm.Catalog.Contracts` package and click the OK button:
{{if UI == "MVC"}}
![abp-studio-add-package-reference-dialog-3](images/abp-studio-add-package-reference-dialog-3.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-add-package-reference-dialog-3](images/abp-studio-add-package-reference-dialog-3-blazor-webapp.png)
{{end}}
ABP Studio adds the package reference and arranges the [module](../../framework/architecture/modularity/basics.md) dependency.
@ -249,7 +260,9 @@ Let's see what we've changed:
* In the last line, we are converting the product list to a dictionary, where the key is `Guid Id` and the value is `string Name`. That way, we can easily find a product's name with its ID.
* Finally, we are mapping the orders to `OrderDto` objects and setting the product name by looking up the product ID in the dictionary.
Open the `Index.cshtml` file, and change the `@order.ProductId` part by `@Order.ProductName` to write the product name instead of the product ID. The final `Index.cshtml` content should be the following:
{{if UI == "MVC"}}
Open the `Index.cshtml` file, and change the `@order.ProductId` part to `@order.ProductName` to write the product name instead of the product ID. The final `Index.cshtml` content should be the following:
````html
@page
@ -273,6 +286,46 @@ Open the `Index.cshtml` file, and change the `@order.ProductId` part by `@Order.
</abp-card>
````
{{else if UI == "BlazorWebApp"}}
Open the `Index.razor` file, and change the `@order.ProductId` part to `@order.ProductName` to write the product name instead of the product ID. The final `Index.razor` content should be the following:
````razor
@page "/ordering"
@using System.Collections.Generic
@using System.Threading.Tasks
@using ModularCrm.Ordering
@inject IOrderAppService OrderAppService
<h1>Orders</h1>
<Card>
<CardBody>
<ListGroup>
@foreach (var order in Orders)
{
<ListGroupItem>
<strong>Customer:</strong> @order.CustomerName <br />
<strong>Product:</strong> @order.ProductName <br />
<strong>State:</strong> @order.State
</ListGroupItem>
}
</ListGroup>
</CardBody>
</Card>
@code {
private List<OrderDto> Orders { get; set; } = new();
protected override async Task OnInitializedAsync()
{
Orders = await OrderAppService.GetListAsync();
}
}
````
{{end}}
That's all. Now, you can graph build the main application and run it in ABP Studio to see the result:
![abp-studio-browser-list-of-orders-with-product-name](images/abp-studio-browser-list-of-orders-with-product-name.png)

15
docs/en/tutorials/modular-crm/part-07.md

@ -7,6 +7,13 @@
# Integrating the Modules: Communication via Messages (Events)
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -158,13 +165,21 @@ In the opening dialog, find and select the `ModularCrm.Ordering` module, check t
Once you click the OK button, the Ordering module is imported to the Catalog module, and an installation dialog is open:
{{if UI == "MVC"}}
![abp-studio-install-module-dialog-for-ordering](images/abp-studio-install-module-dialog-for-ordering-v2.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-install-module-dialog-for-ordering](images/abp-studio-install-module-dialog-for-ordering-v2-blazor-webapp.png)
{{end}}
Here, select the `ModularCrm.Ordering.Contracts` package on the left side (because we want to add that package reference) and `ModularCrm.Catalog` package on the middle area (because we want to add the package reference to that project). Also, select the `ModularCrm.Ordering` package on the right side, and unselect all packages on the middle area (we don't need the implementation or any other packages). Then, click the OK button to finish the installation operation.
You can check the ABP Studio's *Solution Explorer* panel to see the module import and the project reference (dependency).
{{if UI == "MVC"}}
![abp-studio-imports-and-dependencies](images/abp-studio-imports-and-dependencies-v2.png)
{{else if UI == "BlazorWebApp"}}
![abp-studio-imports-and-dependencies](images/abp-studio-imports-and-dependencies-v2-blazor-webapp.png)
{{end}}
### Handling the `OrderPlacedEto` Event

9
docs/en/tutorials/modular-crm/part-08.md

@ -7,6 +7,13 @@
# Integrating the Modules: Joining the Products and Orders Data
````json
//[doc-params]
{
"UI": ["MVC","BlazorWebApp"]
}
````
````json
//[doc-nav]
{
@ -169,7 +176,7 @@ Now, you know the fundamental principles and mechanics of building sophisticated
## Download the Source Code
You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM).
You can download the completed sample solution [here](https://github.com/abpframework/abp-samples/tree/master/ModularCRM-BlazorWebApp).
## See Also

17
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs

@ -16,7 +16,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Client;
public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigurationClient, ITransientDependency
{
private const string ApplicationConfigurationDtoCacheKey = "ApplicationConfigurationDto_CacheKey";
private const string HttpContextItemsCacheKeyFormat = "ApplicationConfigurationDto_{0}_{1}_CacheKey";
protected IHttpContextAccessor HttpContextAccessor { get; }
protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; }
@ -48,7 +48,8 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
{
string? cacheKey = null;
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key)
var itemsKey = GetHttpContextItemsCacheKey();
if (httpContext != null && httpContext.Items[itemsKey] is string key)
{
cacheKey = key;
}
@ -58,7 +59,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
cacheKey = await CreateCacheKeyAsync();
if (httpContext != null)
{
httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey;
httpContext.Items[itemsKey] = cacheKey;
}
}
@ -128,7 +129,8 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
{
string? cacheKey = null;
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key)
var itemsKey = GetHttpContextItemsCacheKey();
if (httpContext != null && httpContext.Items[itemsKey] is string key)
{
cacheKey = key;
}
@ -138,7 +140,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync);
if (httpContext != null)
{
httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey;
httpContext.Items[itemsKey] = cacheKey;
}
}
@ -154,4 +156,9 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
{
return await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id);
}
protected virtual string GetHttpContextItemsCacheKey()
{
return string.Format(CultureInfo.InvariantCulture, HttpContextItemsCacheKeyFormat, CurrentUser.Id?.ToString("N") ?? "Anonymous", CultureInfo.CurrentUICulture.Name);
}
}

8
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js

@ -92,13 +92,19 @@
var formEl = $form[0];
var formData = new FormData(formEl);
var arr = $form.serializeArray();
if (options.beforeSubmit && options.beforeSubmit.call(formEl, arr, $form) === false) {
return;
}
var formData = new FormData(formEl);
var submitter = e.originalEvent && e.originalEvent.submitter;
if (submitter && submitter.name) {
formData.append(submitter.name, submitter.value);
arr.push({ name: submitter.name, value: submitter.value });
}
var method = (options.method || $form.attr("method") || "POST").toUpperCase();
var url = $form.attr("action") || window.location.href;

1538
modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.Designer.cs

File diff suppressed because it is too large

160
modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20260227074745_ABP10_2.cs

@ -0,0 +1,160 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Migrations
{
/// <inheritdoc />
public partial class ABP10_2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_AbpPermissions_Name",
table: "AbpPermissions");
migrationBuilder.AddColumn<DateTimeOffset>(
name: "LastSignInTime",
table: "AbpUsers",
type: "datetimeoffset",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "Leaved",
table: "AbpUsers",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.AlterColumn<string>(
name: "GroupName",
table: "AbpPermissions",
type: "nvarchar(128)",
maxLength: 128,
nullable: true,
oldClrType: typeof(string),
oldType: "nvarchar(128)",
oldMaxLength: 128);
migrationBuilder.AddColumn<string>(
name: "ManagementPermissionName",
table: "AbpPermissions",
type: "nvarchar(128)",
maxLength: 128,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ResourceName",
table: "AbpPermissions",
type: "nvarchar(256)",
maxLength: 256,
nullable: true);
migrationBuilder.CreateTable(
name: "AbpResourcePermissionGrants",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
ProviderName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
ProviderKey = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
ResourceName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
ResourceKey = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpResourcePermissionGrants", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AbpUserPasskeys",
columns: table => new
{
CredentialId = table.Column<byte[]>(type: "varbinary(1024)", maxLength: 1024, nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Data = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AbpUserPasskeys", x => x.CredentialId);
table.ForeignKey(
name: "FK_AbpUserPasskeys_AbpUsers_UserId",
column: x => x.UserId,
principalTable: "AbpUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AbpPermissions_ResourceName_Name",
table: "AbpPermissions",
columns: new[] { "ResourceName", "Name" },
unique: true,
filter: "[ResourceName] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_AbpResourcePermissionGrants_TenantId_Name_ResourceName_ResourceKey_ProviderName_ProviderKey",
table: "AbpResourcePermissionGrants",
columns: new[] { "TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey" },
unique: true,
filter: "[TenantId] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_AbpUserPasskeys_UserId",
table: "AbpUserPasskeys",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AbpResourcePermissionGrants");
migrationBuilder.DropTable(
name: "AbpUserPasskeys");
migrationBuilder.DropIndex(
name: "IX_AbpPermissions_ResourceName_Name",
table: "AbpPermissions");
migrationBuilder.DropColumn(
name: "LastSignInTime",
table: "AbpUsers");
migrationBuilder.DropColumn(
name: "Leaved",
table: "AbpUsers");
migrationBuilder.DropColumn(
name: "ManagementPermissionName",
table: "AbpPermissions");
migrationBuilder.DropColumn(
name: "ResourceName",
table: "AbpPermissions");
migrationBuilder.AlterColumn<string>(
name: "GroupName",
table: "AbpPermissions",
type: "nvarchar(128)",
maxLength: 128,
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "nvarchar(128)",
oldMaxLength: 128,
oldNullable: true);
migrationBuilder.CreateIndex(
name: "IX_AbpPermissions_Name",
table: "AbpPermissions",
column: "Name",
unique: true);
}
}
}

138
modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs

@ -19,7 +19,7 @@ namespace Migrations
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer)
.HasAnnotation("ProductVersion", "10.0.0-rc.2.25502.107")
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
@ -482,6 +482,15 @@ namespace Migrations
b.Property<DateTimeOffset?>("LastPasswordChangeTime")
.HasColumnType("datetimeoffset");
b.Property<DateTimeOffset?>("LastSignInTime")
.HasColumnType("datetimeoffset");
b.Property<bool>("Leaved")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("Leaved");
b.Property<bool>("LockoutEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
@ -678,6 +687,26 @@ namespace Migrations
b.ToTable("AbpUserOrganizationUnits", (string)null);
});
modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b =>
{
b.Property<byte[]>("CredentialId")
.HasMaxLength(1024)
.HasColumnType("varbinary(1024)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<Guid>("UserId")
.HasColumnType("uniqueidentifier");
b.HasKey("CredentialId");
b.HasIndex("UserId");
b.ToTable("AbpUserPasskeys", (string)null);
});
modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b =>
{
b.Property<Guid>("UserId")
@ -865,13 +894,16 @@ namespace Migrations
.HasColumnName("ExtraProperties");
b.Property<string>("GroupName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("ManagementPermissionName")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<byte>("MultiTenancySide")
.HasColumnType("tinyint");
@ -888,6 +920,10 @@ namespace Migrations
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ResourceName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("StateCheckers")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
@ -896,8 +932,9 @@ namespace Migrations
b.HasIndex("GroupName");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("ResourceName", "Name")
.IsUnique()
.HasFilter("[ResourceName] IS NOT NULL");
b.ToTable("AbpPermissions", (string)null);
});
@ -964,6 +1001,50 @@ namespace Migrations
b.ToTable("AbpPermissionGroups", (string)null);
});
modelBuilder.Entity("Volo.Abp.PermissionManagement.ResourcePermissionGrant", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ProviderKey")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("ProviderName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("ResourceKey")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("ResourceName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("TenantId", "Name", "ResourceName", "ResourceKey", "ProviderName", "ProviderKey")
.IsUnique()
.HasFilter("[TenantId] IS NOT NULL");
b.ToTable("AbpResourcePermissionGrants", (string)null);
});
modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b =>
{
b.Property<Guid>("Id")
@ -1292,6 +1373,53 @@ namespace Migrations
.IsRequired();
});
modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasskey", b =>
{
b.HasOne("Volo.Abp.Identity.IdentityUser", null)
.WithMany("Passkeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("Volo.Abp.Identity.IdentityPasskeyData", "Data", b1 =>
{
b1.Property<byte[]>("IdentityUserPasskeyCredentialId");
b1.Property<byte[]>("AttestationObject");
b1.Property<byte[]>("ClientDataJson");
b1.Property<DateTimeOffset>("CreatedAt");
b1.Property<bool>("IsBackedUp");
b1.Property<bool>("IsBackupEligible");
b1.Property<bool>("IsUserVerified");
b1.Property<string>("Name");
b1.Property<byte[]>("PublicKey");
b1.Property<long>("SignCount");
b1.PrimitiveCollection<string>("Transports");
b1.HasKey("IdentityUserPasskeyCredentialId");
b1.ToTable("AbpUserPasskeys");
b1
.ToJson("Data")
.HasColumnType("nvarchar(max)");
b1.WithOwner()
.HasForeignKey("IdentityUserPasskeyCredentialId");
});
b.Navigation("Data");
});
modelBuilder.Entity("Volo.Abp.Identity.IdentityUserPasswordHistory", b =>
{
b.HasOne("Volo.Abp.Identity.IdentityUser", null)
@ -1378,6 +1506,8 @@ namespace Migrations
b.Navigation("OrganizationUnits");
b.Navigation("Passkeys");
b.Navigation("PasswordHistories");
b.Navigation("Roles");

2
modules/docs/app/VoloDocs.Migrator/appsettings.json

@ -1,3 +1,3 @@
{
"ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True",
"ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True"
}

3
modules/docs/app/VoloDocs.Web/VoloDocs.Web.abppkg

@ -1,3 +1,4 @@
{
"role": "host.mvc"
"role": "host.mvc",
"projectId": "47142bf8-4bb8-41c5-9900-990a97a67e8a"
}

2
modules/docs/app/VoloDocs.Web/appsettings.json

@ -12,7 +12,7 @@
"Redis": {
"Configuration": "127.0.0.1"
},
"ConnectionString": "Server=localhost;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True",
"ConnectionString": "Server=(LocalDb)\\MSSQLLocalDB;Database=VoloDocs;Trusted_Connection=True;TrustServerCertificate=True",
"ElasticSearch": {
"Url": "http://localhost:9200"
},

47
modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
namespace Volo.Abp.Identity.AspNetCore;
@ -12,10 +13,9 @@ namespace Volo.Abp.Identity.AspNetCore;
public class AbpSignInManager : SignInManager<IdentityUser>
{
protected AbpIdentityOptions AbpOptions { get; }
protected ISettingProvider SettingProvider { get; }
private readonly IdentityUserManager _identityUserManager;
protected IdentityUserManager IdentityUserManager { get; }
protected ICurrentTenant CurrentTenant { get; }
public AbpSignInManager(
IdentityUserManager userManager,
@ -26,7 +26,8 @@ public class AbpSignInManager : SignInManager<IdentityUser>
IAuthenticationSchemeProvider schemes,
IUserConfirmation<IdentityUser> confirmation,
IOptions<AbpIdentityOptions> options,
ISettingProvider settingProvider) : base(
ISettingProvider settingProvider,
ICurrentTenant currentTenant) : base(
userManager,
contextAccessor,
claimsFactory,
@ -35,9 +36,10 @@ public class AbpSignInManager : SignInManager<IdentityUser>
schemes,
confirmation)
{
SettingProvider = settingProvider;
AbpOptions = options.Value;
_identityUserManager = userManager;
SettingProvider = settingProvider;
IdentityUserManager = userManager;
CurrentTenant = currentTenant;
}
public override async Task<SignInResult> PasswordSignInAsync(
@ -68,17 +70,23 @@ public class AbpSignInManager : SignInManager<IdentityUser>
}
else
{
if (externalLoginProvider is IExternalLoginProviderWithPassword externalLoginProviderWithPassword)
{
await externalLoginProviderWithPassword.UpdateUserAsync(user, externalLoginProviderInfo.Name, password);
}
else
using (CurrentTenant.Change(user.TenantId))
{
await externalLoginProvider.UpdateUserAsync(user, externalLoginProviderInfo.Name);
if (externalLoginProvider is IExternalLoginProviderWithPassword externalLoginProviderWithPassword)
{
await externalLoginProviderWithPassword.UpdateUserAsync(user, externalLoginProviderInfo.Name, password);
}
else
{
await externalLoginProvider.UpdateUserAsync(user, externalLoginProviderInfo.Name);
}
}
}
return await SignInOrTwoFactorAsync(user, isPersistent);
using (CurrentTenant.Change(user.TenantId))
{
return await SignInOrTwoFactorAsync(user, isPersistent);
}
}
}
@ -88,7 +96,10 @@ public class AbpSignInManager : SignInManager<IdentityUser>
return SignInResult.Failed;
}
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
using (CurrentTenant.Change(user.TenantId))
{
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
}
public override async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
@ -109,17 +120,17 @@ public class AbpSignInManager : SignInManager<IdentityUser>
public virtual async Task<IdentityUser> FindByEmailAsync(string email)
{
return await _identityUserManager.FindSharedUserByEmailAsync(email);
return await IdentityUserManager.FindSharedUserByEmailAsync(email);
}
public virtual async Task<IdentityUser> FindByNameAsync(string userName)
{
return await _identityUserManager.FindSharedUserByNameAsync(userName);
return await IdentityUserManager.FindSharedUserByNameAsync(userName);
}
public virtual async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey)
{
return await _identityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey);
return await IdentityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey);
}
/// <summary>
@ -146,7 +157,7 @@ public class AbpSignInManager : SignInManager<IdentityUser>
return SignInResult.NotAllowed;
}
if (await _identityUserManager.ShouldPeriodicallyChangePasswordAsync(user))
if (await IdentityUserManager.ShouldPeriodicallyChangePasswordAsync(user))
{
return SignInResult.NotAllowed;
}

5
modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/RoleResourcePermissionProviderKeyLookupService.cs

@ -24,6 +24,11 @@ public class RoleResourcePermissionProviderKeyLookupService : IResourcePermissio
DisplayName = LocalizableString.Create<IdentityResource>(nameof(RoleResourcePermissionProviderKeyLookupService));
}
public virtual Task<bool> IsAvailableAsync()
{
return Task.FromResult(true);
}
public virtual async Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default)
{
var roles = await UserRoleFinder.SearchRoleAsync(filter, page);

5
modules/identity/src/Volo.Abp.PermissionManagement.Domain.Identity/Volo/Abp/PermissionManagement/Identity/UserResourcePermissionProviderKeyLookupService.cs

@ -25,6 +25,11 @@ public class UserResourcePermissionProviderKeyLookupService : IResourcePermissio
DisplayName = LocalizableString.Create<IdentityResource>(nameof(UserResourcePermissionProviderKeyLookupService));
}
public virtual Task<bool> IsAvailableAsync()
{
return Task.FromResult(true);
}
public virtual async Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default)
{
var users = await UserRoleFinder.SearchUserAsync(filter, page);

5
modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionManagementProvider.cs

@ -20,6 +20,11 @@ public class ClientResourcePermissionManagementProvider : ResourcePermissionMana
{
}
public override Task<bool> IsAvailableAsync()
{
return Task.FromResult(CurrentTenant.Id == null);
}
public override Task<ResourcePermissionValueProviderGrantInfo> CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey)
{
using (CurrentTenant.Change(null))

13
modules/identityserver/src/Volo.Abp.PermissionManagement.Domain.IdentityServer/Volo/Abp/PermissionManagement/IdentityServer/ClientResourcePermissionProviderKeyLookupService.cs

@ -8,6 +8,7 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.IdentityServer.Clients;
using Volo.Abp.IdentityServer.Localization;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.PermissionManagement.IdentityServer;
@ -19,12 +20,22 @@ public class ClientResourcePermissionProviderKeyLookupService : IResourcePermiss
protected IClientFinder ClientFinder { get; }
public ClientResourcePermissionProviderKeyLookupService(IClientFinder clientFinder)
protected ICurrentTenant CurrentTenant { get; }
public ClientResourcePermissionProviderKeyLookupService(
IClientFinder clientFinder,
ICurrentTenant currentTenant)
{
ClientFinder = clientFinder;
CurrentTenant = currentTenant;
DisplayName = LocalizableString.Create<AbpIdentityServerResource>(nameof(ClientResourcePermissionProviderKeyLookupService));
}
public virtual Task<bool> IsAvailableAsync()
{
return Task.FromResult(CurrentTenant.Id == null);
}
public virtual async Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default)
{
var clients = await ClientFinder.SearchAsync(filter, page);

5
modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionManagementProvider.cs

@ -17,6 +17,11 @@ public class ApplicationResourcePermissionManagementProvider : ResourcePermissio
{
}
public override Task<bool> IsAvailableAsync()
{
return Task.FromResult(CurrentTenant.Id == null);
}
public override Task<ResourcePermissionValueProviderGrantInfo> CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey)
{
using (CurrentTenant.Change(null))

13
modules/openiddict/src/Volo.Abp.PermissionManagement.Domain.OpenIddict/Volo/Abp/PermissionManagement/OpenIddict/ApplicationResourcePermissionProviderKeyLookupService.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Volo.Abp.Authorization.Permissions.Resources;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict.Applications;
using Volo.Abp.OpenIddict.Localization;
@ -19,12 +20,22 @@ public class ApplicationResourcePermissionProviderKeyLookupService : IResourcePe
protected IApplicationFinder ApplicationFinder { get; }
public ApplicationResourcePermissionProviderKeyLookupService(IApplicationFinder applicationFinder)
protected ICurrentTenant CurrentTenant { get; }
public ApplicationResourcePermissionProviderKeyLookupService(
IApplicationFinder applicationFinder,
ICurrentTenant currentTenant)
{
ApplicationFinder = applicationFinder;
CurrentTenant = currentTenant;
DisplayName = LocalizableString.Create<AbpOpenIddictResource>(nameof(ApplicationResourcePermissionProviderKeyLookupService));
}
public virtual Task<bool> IsAvailableAsync()
{
return Task.FromResult(CurrentTenant.Id == null);
}
public virtual async Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default)
{
var applications = await ApplicationFinder.SearchAsync(filter, page);

1
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs

@ -276,6 +276,7 @@ public partial class PermissionManagementModal
}
return permissionGrantInfo.IsGranted &&
permissionGrantInfo.GrantedProviders.Any() &&
permissionGrantInfo.GrantedProviders.All(p => p.ProviderName != _providerName);
}

2
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/ResourcePermissionManagementModal.razor

@ -130,7 +130,6 @@
</Validation>
</div>
<div class="mb-3">
<h4>@L["ResourcePermissionPermissions"]</h4>
<Switch TValue="bool" Value="CreateEntity.Permissions.All(x => x.IsGranted)" ValueChanged="GrantAllAsync">@L["GrantAllResourcePermissions"]</Switch>
<div class="mt-2">
@foreach (var permission in CreateEntity.Permissions)
@ -159,7 +158,6 @@
<ModalBody>
<Validations @ref="@EditValidationsRef" Model="@EditEntity" ValidateOnLoad="false">
<div class="mb-3">
<h4>@L["ResourcePermissionPermissions"]</h4>
<Switch TValue="bool" Value="EditEntity.Permissions.All(x => x.IsGranted)" ValueChanged="GrantAllAsync">@L["GrantAllResourcePermissions"]</Switch>
<div class="mt-2">
@foreach (var permission in EditEntity.Permissions)

2
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/IResourcePermissionManagementProvider.cs

@ -8,6 +8,8 @@ public interface IResourcePermissionManagementProvider : ISingletonDependency //
{
string Name { get; }
Task<bool> IsAvailableAsync();
Task<ResourcePermissionValueProviderGrantInfo> CheckAsync(
[NotNull] string name,
[NotNull] string resourceName,

2
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/IResourcePermissionProviderKeyLookupService.cs

@ -9,6 +9,8 @@ public interface IResourcePermissionProviderKeyLookupService
{
public string Name { get; }
Task<bool> IsAvailableAsync();
public ILocalizableString DisplayName { get; }
Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default);

5
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs

@ -26,6 +26,11 @@ public abstract class ResourcePermissionManagementProvider : IResourcePermission
CurrentTenant = currentTenant;
}
public virtual Task<bool> IsAvailableAsync()
{
return Task.FromResult(true);
}
public virtual async Task<ResourcePermissionValueProviderGrantInfo> CheckAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey)
{
var multiplePermissionValueProviderGrantInfo = await CheckAsync(new[] { name }, resourceName, resourceKey, providerName, providerKey);

73
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs

@ -70,17 +70,33 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
);
}
public virtual Task<List<IResourcePermissionProviderKeyLookupService>> GetProviderKeyLookupServicesAsync()
public virtual async Task<List<IResourcePermissionProviderKeyLookupService>> GetProviderKeyLookupServicesAsync()
{
return Task.FromResult(_lazyProviderKeyLookupServices.Value);
var availableServices = new List<IResourcePermissionProviderKeyLookupService>();
foreach (var service in _lazyProviderKeyLookupServices.Value)
{
if (await service.IsAvailableAsync())
{
availableServices.Add(service);
}
}
return availableServices;
}
public virtual Task<IResourcePermissionProviderKeyLookupService> GetProviderKeyLookupServiceAsync(string serviceName)
public virtual async Task<IResourcePermissionProviderKeyLookupService> GetProviderKeyLookupServiceAsync(string serviceName)
{
var service = _lazyProviderKeyLookupServices.Value.FirstOrDefault(s => s.Name == serviceName);
return service == null
? throw new AbpException("Unknown resource permission provider key lookup service: " + serviceName)
: Task.FromResult(service);
if (service == null)
{
throw new AbpException("Unknown resource permission provider key lookup service: " + serviceName);
}
if (!await service.IsAvailableAsync())
{
throw new AbpException("The resource permission provider key lookup service '" + serviceName + "' is not available in the current context.");
}
return service;
}
public virtual async Task<List<PermissionDefinition>> GetAvailablePermissionsAsync(string resourceName)
@ -159,13 +175,15 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
{
var resourcePermissionDefinitions = await GetAvailablePermissionsAsync(resourceName);
var resourcePermissionGrants = await ResourcePermissionGrantRepository.GetPermissionsAsync(resourceName, resourceKey);
var unavailableProviderNames = await GetUnavailableManagementProviderNamesAsync();
var result = new List<PermissionWithGrantedProviders>();
foreach (var resourcePermissionDefinition in resourcePermissionDefinitions)
{
var permissionWithGrantedProviders = new PermissionWithGrantedProviders(resourcePermissionDefinition.Name, false);
var grantedPermissions = resourcePermissionGrants
.Where(x => x.Name == resourcePermissionDefinition.Name && x.ResourceName == resourceName && x.ResourceKey == resourceKey)
.Where(x => x.Name == resourcePermissionDefinition.Name && x.ResourceName == resourceName && x.ResourceKey == resourceKey
&& !unavailableProviderNames.Contains(x.ProviderName))
.ToList();
if (grantedPermissions.Any())
@ -194,7 +212,10 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
{
var resourcePermissions = await GetAvailablePermissionsAsync(resourceName);
var resourcePermissionGrants = await ResourcePermissionGrantRepository.GetPermissionsAsync(resourceName, resourceKey);
resourcePermissionGrants = resourcePermissionGrants.Where(x => resourcePermissions.Any(rp => rp.Name == x.Name)).ToList();
var unavailableProviderNames = await GetUnavailableManagementProviderNamesAsync();
resourcePermissionGrants = resourcePermissionGrants
.Where(x => resourcePermissions.Any(rp => rp.Name == x.Name) && !unavailableProviderNames.Contains(x.ProviderName))
.ToList();
var resourcePermissionGrantsGroup = resourcePermissionGrants.GroupBy(x => new { x.ProviderName, x.ProviderKey });
var result = new List<PermissionProviderWithPermissions>();
foreach (var resourcePermissionGrant in resourcePermissionGrantsGroup)
@ -282,6 +303,12 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
throw new AbpException("Unknown resource permission management provider: " + providerName);
}
if (!await provider.IsAvailableAsync())
{
//TODO: BusinessException
throw new AbpException($"The resource permission management provider '{providerName}' is not available in the current context.");
}
await provider.SetAsync(permissionName, resourceName, resourceKey, providerKey, isGranted);
}
@ -327,6 +354,12 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
public virtual async Task DeleteAsync(string resourceName, string resourceKey, string providerName, string providerKey)
{
var provider = ManagementProviders.FirstOrDefault(m => m.Name == providerName);
if (provider != null && !await provider.IsAvailableAsync())
{
throw new AbpException($"The resource permission management provider '{providerName}' is not available in the current context.");
}
var permissionGrants = await ResourcePermissionGrantRepository.GetListAsync(resourceName, resourceKey, providerName, providerKey);
foreach (var permissionGrant in permissionGrants)
{
@ -336,6 +369,12 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
public virtual async Task DeleteAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey)
{
var provider = ManagementProviders.FirstOrDefault(m => m.Name == providerName);
if (provider != null && !await provider.IsAvailableAsync())
{
throw new AbpException($"The resource permission management provider '{providerName}' is not available in the current context.");
}
var permissionGrant = await ResourcePermissionGrantRepository.FindAsync(name, resourceName, resourceKey, providerName, providerKey);
if (permissionGrant != null)
{
@ -378,6 +417,11 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
foreach (var provider in ManagementProviders)
{
if (!await provider.IsAvailableAsync())
{
continue;
}
permissionNames = resourcePermissions.Select(x => x.Name).ToArray();
var multiplePermissionValueProviderGrantInfo = await provider.CheckAsync(permissionNames, resourceName, resourceKey, providerName, providerKey);
@ -402,4 +446,17 @@ public class ResourcePermissionManager : IResourcePermissionManager, ISingletonD
return multiplePermissionWithGrantedProviders;
}
protected virtual async Task<HashSet<string>> GetUnavailableManagementProviderNamesAsync()
{
var names = new HashSet<string>();
foreach (var provider in ManagementProviders)
{
if (!await provider.IsAvailableAsync())
{
names.Add(provider.Name);
}
}
return names;
}
}

1
modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/AddResourcePermissionManagementModal.cshtml

@ -35,7 +35,6 @@
<span class="text-danger field-validation-valid" data-valmsg-for="AddModel.ProviderKey" data-valmsg-replace="true"></span>
</div>
<div class="position-relative mb-3" id="permissionList">
<h4>@L["ResourcePermissionPermissions"]</h4>
<div class="form-check form-switch mb-2">
<input class="form-check-input" id="grantAllresourcePermissions" type="checkbox">
<label class="form-check-label" for="grantAllresourcePermissions">@L["GrantAllResourcePermissions"]</label>

1
modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/UpdateResourcePermissionManagementModal.cshtml

@ -23,7 +23,6 @@
<input asp-for="@Model.ProviderKey" />
<abp-modal-body>
<div class="mb-3" id="permissionList">
<h4>@L["ResourcePermissionPermissions"]</h4>
<div class="form-check form-switch mb-2">
<input class="form-check-input" id="grantAllresourcePermissions" type="checkbox" checked="@Model.ResourcePermissions.Permissions.All(x => x.IsGranted)">
<label class="form-check-label" for="grantAllresourcePermissions">@L["GrantAllResourcePermissions"]</label>

137
modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManager_Tests.cs

@ -42,6 +42,12 @@ public class ResourcePermissionManager_Tests : PermissionTestBase
await _resourcePermissionManager.GetProviderKeyLookupServiceAsync("UndefinedProvider");
});
exception.Message.ShouldBe("Unknown resource permission provider key lookup service: UndefinedProvider");
var unavailableException = await Assert.ThrowsAsync<AbpException>(async () =>
{
await _resourcePermissionManager.GetProviderKeyLookupServiceAsync("TestUnavailable");
});
unavailableException.Message.ShouldBe("The resource permission provider key lookup service 'TestUnavailable' is not available in the current context.");
}
[Fact]
@ -334,4 +340,135 @@ public class ResourcePermissionManager_Tests : PermissionTestBase
"Test",
"Test")).ShouldBeNull();
}
[Fact]
public async Task GetProviderKeyLookupServicesAsync_Should_Not_Return_Unavailable_Services()
{
var lookupServices = await _resourcePermissionManager.GetProviderKeyLookupServicesAsync();
lookupServices.ShouldContain(s => s.Name == "Test");
lookupServices.ShouldNotContain(s => s.Name == "TestUnavailable");
}
[Fact]
public async Task GetAsync_Should_Not_Return_Grant_From_Unavailable_Provider()
{
// Insert a grant directly via repository to simulate data stored by an unavailable provider
await _resourcePermissionGrantRepository.InsertAsync(new ResourcePermissionGrant(
Guid.NewGuid(),
"MyResourcePermission1",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey")
);
var grantedProviders = await _resourcePermissionManager.GetAsync(
"MyResourcePermission1",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey");
// The unavailable provider is skipped, so the permission should not be considered granted via it
grantedProviders.IsGranted.ShouldBeFalse();
grantedProviders.Providers.ShouldNotContain(p => p.Name == "TestUnavailable");
}
[Fact]
public async Task SetAsync_Should_Throw_When_Provider_Is_Unavailable()
{
var exception = await Assert.ThrowsAsync<AbpException>(async () =>
{
await _resourcePermissionManager.SetAsync(
"MyResourcePermission1",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey",
true);
});
exception.Message.ShouldBe("The resource permission management provider 'TestUnavailable' is not available in the current context.");
}
[Fact]
public async Task GetAllAsync_Should_Not_Include_Grants_From_Unavailable_Provider()
{
// Grant via the available "Test" provider and the unavailable "TestUnavailable" provider
await _resourcePermissionGrantRepository.InsertAsync(new ResourcePermissionGrant(
Guid.NewGuid(),
"MyResourcePermission1",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"Test",
"someKey")
);
await _resourcePermissionGrantRepository.InsertAsync(new ResourcePermissionGrant(
Guid.NewGuid(),
"MyResourcePermission1",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey")
);
var result = await _resourcePermissionManager.GetAllAsync(
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1);
var item = result.FirstOrDefault(x => x.Name == "MyResourcePermission1");
item.ShouldNotBeNull();
item.IsGranted.ShouldBeTrue();
item.Providers.ShouldContain(p => p.Name == "Test");
item.Providers.ShouldNotContain(p => p.Name == "TestUnavailable");
}
[Fact]
public async Task GetAllGroupAsync_Should_Not_Include_Grants_From_Unavailable_Provider()
{
await _resourcePermissionGrantRepository.InsertAsync(new ResourcePermissionGrant(
Guid.NewGuid(),
"MyResourcePermission2",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey")
);
var group = await _resourcePermissionManager.GetAllGroupAsync(
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1);
group.ShouldNotContain(g => g.ProviderName == "TestUnavailable");
}
[Fact]
public async Task DeleteAsync_Should_Throw_When_Provider_Is_Unavailable()
{
var exception = await Assert.ThrowsAsync<AbpException>(async () =>
{
await _resourcePermissionManager.DeleteAsync(
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey");
});
exception.Message.ShouldBe("The resource permission management provider 'TestUnavailable' is not available in the current context.");
}
[Fact]
public async Task DeleteAsyncByName_Should_Throw_When_Provider_Is_Unavailable()
{
var exception = await Assert.ThrowsAsync<AbpException>(async () =>
{
await _resourcePermissionManager.DeleteAsync(
"MyResourcePermission1",
TestEntityResource.ResourceName,
TestEntityResource.ResourceKey1,
"TestUnavailable",
"someKey");
});
exception.Message.ShouldBe("The resource permission management provider 'TestUnavailable' is not available in the current context.");
}
}

2
modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/AbpPermissionManagementTestBaseModule.cs

@ -23,7 +23,9 @@ public class AbpPermissionManagementTestBaseModule : AbpModule
{
options.ManagementProviders.Add<TestPermissionManagementProvider>();
options.ResourceManagementProviders.Add<TestResourcePermissionManagementProvider>();
options.ResourceManagementProviders.Add<TestUnavailableResourcePermissionManagementProvider>();
options.ResourcePermissionProviderKeyLookupServices.Add<TestResourcePermissionProviderKeyLookupService>();
options.ResourcePermissionProviderKeyLookupServices.Add<TestUnavailableResourcePermissionProviderKeyLookupService>();
});
}

5
modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestResourcePermissionProviderKeyLookupService.cs

@ -12,6 +12,11 @@ public class TestResourcePermissionProviderKeyLookupService : IResourcePermissio
public ILocalizableString DisplayName => new LocalizableString("Test", "TestResource");
public Task<bool> IsAvailableAsync()
{
return Task.FromResult(true);
}
public Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();

26
modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestUnavailableResourcePermissionManagementProvider.cs

@ -0,0 +1,26 @@
using System.Threading.Tasks;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.PermissionManagement;
public class TestUnavailableResourcePermissionManagementProvider : ResourcePermissionManagementProvider
{
public override string Name => "TestUnavailable";
public TestUnavailableResourcePermissionManagementProvider(
IResourcePermissionGrantRepository resourcePermissionGrantRepository,
IGuidGenerator guidGenerator,
ICurrentTenant currentTenant)
: base(
resourcePermissionGrantRepository,
guidGenerator,
currentTenant)
{
}
public override Task<bool> IsAvailableAsync()
{
return Task.FromResult(false);
}
}

29
modules/permission-management/test/Volo.Abp.PermissionManagement.TestBase/Volo/Abp/PermissionManagement/TestUnavailableResourcePermissionProviderKeyLookupService.cs

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
namespace Volo.Abp.PermissionManagement;
public class TestUnavailableResourcePermissionProviderKeyLookupService : IResourcePermissionProviderKeyLookupService, ITransientDependency
{
public string Name => "TestUnavailable";
public ILocalizableString DisplayName => new LocalizableString("TestUnavailable", "TestResource");
public Task<bool> IsAvailableAsync()
{
return Task.FromResult(false);
}
public Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string filter = null, int page = 1, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
public Task<List<ResourcePermissionProviderKeyInfo>> SearchAsync(string[] keys, CancellationToken cancellationToken = default)
{
throw new System.NotImplementedException();
}
}

3
npm/ng-packs/packages/identity/package.json

@ -12,9 +12,6 @@
"@abp/ng.theme.shared": "~10.2.0-rc.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/aria": "~21.1.0"
},
"publishConfig": {
"access": "public"
}

72
npm/ng-packs/packages/identity/src/lib/components/users/users.component.html

@ -28,47 +28,39 @@
<ng-template #abpBody>
@if (form) {
<form [formGroup]="form" (ngSubmit)="save()">
<div ngTabs selectionMode="follow">
<div ngTabList [(selectedTab)]="selectedTab" class="nav nav-tabs">
<button ngTab #tabInfo="ngTab" [value]="'user-info'" class="nav-link" [class.active]="tabInfo.selected()" type="button">
{{ 'AbpIdentity::UserInformations' | abpLocalization }}
</button>
<button ngTab #tabRoles="ngTab" [value]="'roles'" class="nav-link" [class.active]="tabRoles.selected()" type="button">
{{ 'AbpIdentity::Roles' | abpLocalization }}
</button>
</div>
<ul ngbNav #newUserNav="ngbNav" class="nav-tabs" [(activeId)]="selectedTab">
<li ngbNavItem="user-info">
<a ngbNavLink>{{ 'AbpIdentity::UserInformations' | abpLocalization }}</a>
<ng-template ngbNavContent>
<abp-extensible-form [selectedRecord]="selected"></abp-extensible-form>
</ng-template>
</li>
<li ngbNavItem="roles">
<a ngbNavLink>{{ 'AbpIdentity::Roles' | abpLocalization }}</a>
<ng-template ngbNavContent>
@for (roleGroup of roleGroups; track $index; let i = $index) {
<div class="form-check mb-2">
<abp-checkbox
*abpReplaceableTemplate="{
inputs: {
checkboxId: 'roles-' + i,
label: roles[i].name,
formControl: roleGroup.controls[roles[i].name]
},
componentKey: inputKey
}"
[checkboxId]="'roles-' + i"
[formControl]="roleGroup.controls[roles[i].name]"
[label]="roles[i].name"
>
</abp-checkbox>
</div>
}
</ng-template>
</li>
</ul>
<div class="mt-2 fade-in-top">
<div ngTabPanel [value]="'user-info'">
<ng-template ngTabContent>
<abp-extensible-form [selectedRecord]="selected"></abp-extensible-form>
</ng-template>
</div>
<div ngTabPanel [value]="'roles'">
<ng-template ngTabContent>
@for (roleGroup of roleGroups; track $index; let i = $index) {
<div class="form-check mb-2">
<abp-checkbox
*abpReplaceableTemplate="{
inputs: {
checkboxId: 'roles-' + i,
label: roles[i].name,
formControl: roleGroup.controls[roles[i].name]
},
componentKey: inputKey
}"
[checkboxId]="'roles-' + i"
[formControl]="roleGroup.controls[roles[i].name]"
[label]="roles[i].name"
>
</abp-checkbox>
</div>
}
</ng-template>
</div>
</div>
</div>
<div class="mt-2 fade-in-top" [ngbNavOutlet]="newUserNav"></div>
</form>
} @else {
<div class="text-center"><i class="fa fa-pulse fa-spinner" aria-hidden="true"></i></div>

9
npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

@ -52,8 +52,7 @@ import {
import { finalize, switchMap, tap } from 'rxjs/operators';
import { eIdentityComponents } from '../../enums/components';
import { PageComponent } from '@abp/ng.components/page';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { Tabs, TabList, Tab, TabPanel, TabContent } from '@angular/aria/tabs';
import { NgbDropdownModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@Component({
@ -71,12 +70,8 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
FormsModule,
PermissionManagementComponent,
PageComponent,
Tabs,
TabList,
Tab,
TabPanel,
TabContent,
NgbDropdownModule,
NgbNavModule,
NgxValidateCoreModule,
LocalizationPipe,
ExtensibleTableComponent,

Loading…
Cancel
Save