diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml
index f916789293..4cb89fd21d 100644
--- a/.github/workflows/auto-pr.yml
+++ b/.github/workflows/auto-pr.yml
@@ -1,13 +1,13 @@
-name: Merge branch dev with rel-10.0
+name: Merge branch dev with rel-10.1
on:
push:
branches:
- - rel-10.0
+ - rel-10.1
permissions:
contents: read
jobs:
- merge-dev-with-rel-10-0:
+ merge-dev-with-rel-10-1:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
@@ -18,14 +18,14 @@ jobs:
ref: dev
- name: Reset promotion branch
run: |
- git fetch origin rel-10.0:rel-10.0
- git reset --hard rel-10.0
+ git fetch origin rel-10.1:rel-10.1
+ git reset --hard rel-10.1
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
- branch: auto-merge/rel-10-0/${{github.run_number}}
- title: Merge branch dev with rel-10.0
- body: This PR generated automatically to merge dev with rel-10.0. Please review the changed files before merging to prevent any errors that may occur.
+ branch: auto-merge/rel-10-1/${{github.run_number}}
+ title: Merge branch dev with rel-10.1
+ body: This PR generated automatically to merge dev with rel-10.1. Please review the changed files before merging to prevent any errors that may occur.
reviewers: maliming
draft: true
token: ${{ github.token }}
@@ -34,5 +34,5 @@ jobs:
GH_TOKEN: ${{ secrets.BOT_SECRET }}
run: |
gh pr ready
- gh pr review auto-merge/rel-10-0/${{github.run_number}} --approve
- gh pr merge auto-merge/rel-10-0/${{github.run_number}} --merge --auto --delete-branch
+ gh pr review auto-merge/rel-10-1/${{github.run_number}} --approve
+ gh pr merge auto-merge/rel-10-1/${{github.run_number}} --merge --auto --delete-branch
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 67953c873c..fa951f8d7d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,7 +7,6 @@
-
@@ -20,10 +19,10 @@
-
-
-
-
+
+
+
+
@@ -137,11 +136,11 @@
-
+
-
+
@@ -149,7 +148,7 @@
-
+
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
index 7f9b928749..bb6c1aeef4 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
@@ -783,7 +783,7 @@
"Enum:SourceChannel:1": "Studio",
"Enum:SourceChannel:2": "Support Site",
"Enum:SourceChannel:3": "Suite",
- "Menu:OrganizationTokenUsage": "Organization Token Usage",
+ "Menu:AITokens": "AI Tokens",
"Permission:OrganizationTokenUsage": "Organization Token Usage"
}
}
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
index 622f30420c..16b93b9cc5 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
@@ -269,7 +269,7 @@
"Referral.CannotDeleteUsedLink": "You cannot delete a referral link that has already been used.",
"Referral.CannotReferYourself": "You cannot create a referral link for your own email address.",
"Referral:TargetEmail": "Target Email",
- "Referral.CannotReferSameOrganizationMember": "You cannot create a referral link for a user who is already a member of your organization.",
+ "Referral.CannotReferSameOrganizationMember": "Referral links cannot be used for existing organization members.",
"LinkCopiedToClipboard": "Link copied to clipboard",
"AreYouSureToDeleteReferralLink": "Are you sure you want to delete this referral link?",
"DefaultErrorMessage": "An error occurred."
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md
new file mode 100644
index 0000000000..a56dd5a464
--- /dev/null
+++ b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/POST.md
@@ -0,0 +1,173 @@
+# Announcing .NET Aspire Integration for ABP Microservice Template
+
+We are excited to announce the integration of **.NET Aspire** into the ABP microservice solution, available starting with **ABP Studio v2.0.0**. This integration brings a unified development experience for building, running, debugging, and deploying distributed applications. With Aspire, you can now orchestrate your entire microservice ecosystem with a single command, eliminating complex configurations and making local development effortless.
+
+## What is .NET Aspire?
+
+[Aspire](https://aspire.dev/get-started/what-is-aspire/) is a cloud-ready stack designed to streamline the development of distributed applications. It provides:
+
+- **Orchestration**: A code-first approach to defining and running distributed applications, managing dependencies, and launch order.
+- **Integrations**: Pre-built components for common services (databases, caches, message brokers) with automatic configuration.
+- **Tooling**: A developer dashboard for real-time monitoring of logs, traces, metrics, and resource health.
+- **Service Discovery**: Automatic service-to-service communication without hardcoded endpoints.
+- **Observability**: Built-in OpenTelemetry support for distributed tracing, metrics, and structured logging.
+
+## How Does It Work with ABP?
+
+When you enable .NET Aspire in an ABP microservice solution, you get a fully integrated development experience where:
+
+- All microservices, gateways, and applications are orchestrated through a single entry point (AppHost).
+- Infrastructure containers (databases, Redis, RabbitMQ, Elasticsearch, etc.) are managed as code.
+- OpenTelemetry, health checks, and service discovery are automatically configured for all projects via the shared ServiceDefaults project.
+
+## Enabling Aspire in Your Solution
+
+When creating a new microservice solution via ABP Studio:
+
+1. In the solution creation wizard, look for the **".NET Aspire Integration"** step.
+2. Toggle the option to **enable .NET Aspire**.
+3. Complete the wizard—Aspire projects will be generated along with your solution.
+
+
+
+## Solution Structure Changes
+
+When Aspire is enabled, two additional projects are added to your solution:
+
+
+
+### AppHost (Orchestrator)
+
+[`AppHost`](https://aspire.dev/get-started/app-host/) is the .NET Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. It provides:
+
+- **Centralized orchestration**: Start your entire microservice ecosystem with a single command.
+- **Code-first infrastructure**: Databases, Redis, RabbitMQ, Elasticsearch, and observability tools are defined programmatically.
+- **Dependency management**: Services start in the correct order using `WaitFor()` declarations.
+- **Automatic configuration**: Connection strings, endpoints, and environment variables are injected automatically.
+
+### ServiceDefaults
+
+[`ServiceDefaults`](https://aspire.dev/fundamentals/service-defaults/) is a shared library that provides common cloud-native configuration for all projects in the solution. Every service uses the same observability, health check, and resilience patterns.
+
+| Feature | Description |
+|---------|-------------|
+| OpenTelemetry | Tracing, metrics, and structured logging with automatic instrumentation |
+| Health Checks | `/health` and `/alive` endpoints for Kubernetes-style probes |
+| Service Discovery | Automatic resolution of service endpoints |
+| HTTP Resilience | Retry policies, timeouts, and circuit breakers for HTTP clients |
+
+## Running the Solution with Aspire
+
+Running your microservice solution has never been easier:
+
+1. Open **Solution Runner** in ABP Studio.
+2. Select the **Aspire** profile.
+3. Run `AppHost`.
+
+
+
+AppHost automatically:
+
+- Starts all infrastructure containers (database, Redis, RabbitMQ, Elasticsearch, etc.).
+- Launches all microservices, gateways, and applications in dependency order.
+- Injects connection strings and environment variables.
+- Opens the Aspire Dashboard for monitoring.
+
+
+
+## Aspire Dashboard
+
+The Aspire Dashboard provides real-time tracking of your application's state. It enables you to monitor logs, traces, metrics, and environment configurations in an intuitive UI.
+
+
+
+### Key Dashboard Features
+
+#### Console Logs
+
+Display console logs from all resources in real-time. Filter by resource and log level to quickly find relevant information during development and debugging.
+
+
+
+#### Structured Logs
+
+View structured logs from all resources with advanced filtering capabilities. Search and filter logs by resource, log level, timestamp, and custom properties.
+
+
+
+#### Distributed Traces
+
+Explore distributed traces across your microservices to understand request flows and identify performance bottlenecks.
+
+
+
+#### Metrics
+
+Monitor real-time metrics including HTTP requests, response times, garbage collection, memory usage, and custom metrics.
+
+
+
+## Pre-Configured Observability Tools
+
+AppHost comes with pre-configured observability and management tools:
+
+### Grafana
+
+Visualization and analytics platform for monitoring metrics with interactive dashboards.
+
+
+
+### Jaeger
+
+Distributed tracing system to monitor and troubleshoot problems across microservices.
+
+
+
+### Kibana
+
+Visualization tool for Elasticsearch data with search and data visualization capabilities for logs.
+
+
+
+### Prometheus
+
+Monitoring and alerting toolkit that collects and stores metrics as time series data.
+
+
+
+### RabbitMQ Management
+
+Web-based interface for managing and monitoring the RabbitMQ message broker.
+
+
+
+### Redis Insight
+
+Visual tool for Redis that allows you to browse data, run commands, and monitor performance.
+
+
+
+### Database Admin Tools
+
+The database management admin tool varies by database type:
+
+| Database | Tool |
+|----------|------|
+| SQL Server | DBeaver CloudBeaver |
+| MySQL | phpMyAdmin |
+| PostgreSQL | pgAdmin |
+| MongoDB | Mongo Express |
+
+
+
+## Get Started Today
+
+Ready to experience the power of .NET Aspire with ABP? Create a new microservice solution in ABP Studio and enable the .NET Aspire integration option. For detailed documentation, visit our [.NET Aspire Integration documentation](https://abp.io/docs/latest/solution-templates/microservice/aspire-integration).
+
+To learn more about .NET Aspire, visit: [https://aspire.dev](https://aspire.dev/get-started/what-is-aspire/)
+
+We are excited to bring this integration to you and can't wait to hear your feedback. If you have any questions or suggestions, please drop a comment below.
+
+Happy coding!
+
+**The Volosoft Team**
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-apphost-topology.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-apphost-topology.png
new file mode 100644
index 0000000000..a7622cff63
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-apphost-topology.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-configuration.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-configuration.png
new file mode 100644
index 0000000000..74cd5066bb
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-configuration.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-console.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-console.png
new file mode 100644
index 0000000000..fce1ca9355
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-console.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-metrics.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-metrics.png
new file mode 100644
index 0000000000..690c3a910e
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-metrics.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-resources.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-resources.png
new file mode 100644
index 0000000000..7b6777a8b9
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-resources.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-structured-logs.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-structured-logs.png
new file mode 100644
index 0000000000..4c26e1fe48
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-structured-logs.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-traces.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-traces.png
new file mode 100644
index 0000000000..9e5d761582
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-traces.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-database-postgre-pgadmin.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-database-postgre-pgadmin.png
new file mode 100644
index 0000000000..a11107ddd4
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-database-postgre-pgadmin.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-grafana-dashboard.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-grafana-dashboard.png
new file mode 100644
index 0000000000..6c1eb3999e
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-grafana-dashboard.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-jaeger-traces.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-jaeger-traces.png
new file mode 100644
index 0000000000..e9409465b1
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-jaeger-traces.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-kibana-dashboard.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-kibana-dashboard.png
new file mode 100644
index 0000000000..90bcbf14b5
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-kibana-dashboard.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-prometheus-dashboard.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-prometheus-dashboard.png
new file mode 100644
index 0000000000..764f6cfb38
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-prometheus-dashboard.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-rabbitmq-management.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-rabbitmq-management.png
new file mode 100644
index 0000000000..362af7aac3
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-rabbitmq-management.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-redis-insight.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-redis-insight.png
new file mode 100644
index 0000000000..429cbefae7
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-redis-insight.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-solution-structure.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-solution-structure.png
new file mode 100644
index 0000000000..3379892b69
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-solution-structure.png differ
diff --git a/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/solution-runner-aspire-profile.png b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/solution-runner-aspire-profile.png
new file mode 100644
index 0000000000..d11edfe584
Binary files /dev/null and b/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/solution-runner-aspire-profile.png differ
diff --git a/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/cover.png b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/cover.png
new file mode 100644
index 0000000000..69116d7961
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/cover.png differ
diff --git a/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/images/sitemap-architecture.svg b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/images/sitemap-architecture.svg
new file mode 100644
index 0000000000..57721c4550
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/images/sitemap-architecture.svg
@@ -0,0 +1,122 @@
+
diff --git a/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/post.md b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/post.md
new file mode 100644
index 0000000000..fd9d10756e
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/post.md
@@ -0,0 +1,475 @@
+# Building Dynamic XML Sitemaps with ABP Framework
+
+Search Engine Optimization (SEO) is crucial for any web application that wants to be discovered by users. One of the most fundamental SEO practices is providing a comprehensive XML sitemap that helps search engines crawl and index your website efficiently. In this article, we'll use a reusable ABP module that automatically generates dynamic XML sitemaps for both static Razor Pages and dynamic content from your database.
+
+By the end of this tutorial, you'll have a production-ready sitemap solution that discovers your pages automatically, includes dynamic content like blog posts or products, and regenerates sitemaps in the background without impacting performance.
+
+## What is an XML Sitemap?
+
+An XML sitemap is a file that lists all important pages of your website in a structured format that search engines can easily read. It acts as a roadmap for crawlers like Google, Bing, and others, telling them which pages exist, when they were last updated, and how they relate to each other.
+
+For modern web applications with dynamic content, manually maintaining sitemap files quickly becomes impractical. A dynamic sitemap solution that automatically discovers and updates URLs is essential for:
+
+- **Large content sites** with frequently changing blog posts, articles, or products
+- **Multi-tenant applications** where each tenant may have different content
+- **Enterprise applications** with complex page hierarchies
+- **E-commerce platforms** with thousands of product pages
+
+## Why Build a Custom Sitemap Module?
+
+While there are general-purpose sitemap libraries available, building a custom module for ABP Framework provides several advantages:
+
+✅ **Deep ABP Integration**: Leverages ABP's dependency injection, background workers, and module system
+✅ **Automatic Discovery**: Uses ASP.NET Core's Razor Page infrastructure to automatically find pages
+✅ **Type-Safe Configuration**: Strongly-typed attributes and options for configuration
+✅ **Multi-Group Support**: Organize sitemaps by logical groups (main, blog, products, etc.)
+✅ **Background Generation**: Non-blocking sitemap regeneration using ABP's background worker system
+✅ **Repository Integration**: Direct integration with ABP repositories for database entities
+
+## Project Architecture Overview
+
+Before using the module, let's understand its architecture:
+
+
+
+The sitemap module consists of several key components:
+
+1. **Discovery Layer**: Discovers Razor Pages and their metadata using reflection
+2. **Source Layer**: Defines contracts for providing sitemap items (static pages and dynamic content)
+3. **Collection Layer**: Collects items from all registered sources
+4. **Generation Layer**: Transforms collected items into XML format
+5. **Management Layer**: Orchestrates file generation and background workers
+
+## Installation
+
+To get started, clone the demo repository which includes the sitemap module:
+
+```bash
+git clone https://github.com/salihozkara/AbpSitemapDemo
+cd AbpSitemapDemo
+```
+
+The repository contains the sitemap module in the `Modules/abp.sitemap/` directory. To use it in your own project, add a project reference:
+
+```xml
+
+```
+
+## Module Configuration
+
+After installing the package, add the module to your ABP application's module class:
+
+```csharp
+using Abp.Sitemap.Web;
+
+[DependsOn(
+ typeof(SitemapWebModule), // 👈 Add sitemap module
+ // ... other dependencies
+)]
+public class YourProjectWebModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ // Configure sitemap options
+ Configure(options =>
+ {
+ options.BaseUrl = "https://yourdomain.com"; // 👈 Your website URL
+ options.FolderPath = "Sitemaps"; // 👈 Where XML files are stored
+ options.WorkerPeriod = 3600000; // 👈 Regenerate every hour (in milliseconds)
+ });
+ }
+}
+```
+
+> **Note:** In ABP applications, BaseUrl can be resolved from AppUrlOptions to stay consistent with environment configuration.
+
+That's it! The module is now integrated and will automatically:
+- Discover your Razor Pages
+- Generate sitemap XML files on application startup
+- Regenerate sitemaps in the background every hour
+
+## Usage Examples
+
+Let's explore practical examples of using the sitemap module. You can see complete working examples in the [AbpSitemapDemo repository](https://github.com/salihozkara/AbpSitemapDemo).
+
+### Example 1: Mark Static Pages
+
+The simplest way to include pages in your sitemap is using attributes:
+
+```csharp
+using Abp.Sitemap.Web.Sitemap.Sources.Page.Attributes;
+
+namespace YourProject.Pages;
+
+[IncludeSitemapXml] // 👈 Include in default "Main" group
+public class IndexModel : PageModel
+{
+ public void OnGet()
+ {
+ // Your page logic
+ }
+}
+
+[IncludeSitemapXml(Group = "Help")]
+public class FaqModel : PageModel
+{
+ public void OnGet()
+ {
+ // Your page logic
+ }
+}
+```
+
+These pages will be automatically discovered and included in the sitemap XML files.
+
+### Example 2: Add Dynamic Content from Database
+
+For dynamic content like blog posts, products, or articles, create a custom sitemap source. Here's a complete example using a Book entity:
+
+```csharp
+using Abp.Sitemap.Web.Sitemap.Core;
+using Abp.Sitemap.Web.Sitemap.Sources.Group;
+using Volo.Abp.DependencyInjection;
+
+namespace YourProject.Sitemaps;
+
+public class BookSitemapSource : GroupedSitemapItemSource, ITransientDependency
+{
+ public BookSitemapSource(
+ IReadOnlyRepository repository,
+ IAsyncQueryableExecuter executer)
+ : base(repository, executer, group: "Books") // 👈 Creates sitemap-Books.xml
+ {
+ Filter = x => x.IsPublished; // 👈 Only published books
+ }
+
+ protected override Expression> Selector =>
+ book => new SitemapItem(
+ book.Id.ToString(), // 👈 Unique identifier
+ $"/Books/Detail/{book.Id}", // 👈 URL pattern matching your route
+ book.LastModificationTime ?? book.CreationTime // 👈 Last modified date
+ )
+ {
+ ChangeFrequency = "weekly",
+ Priority = 0.7
+ };
+}
+```
+
+Key points:
+- Inherits from `GroupedSitemapItemSource`
+- Specifies the entity type (`Book`)
+- Defines a group name ("Books") which creates `sitemap-Books.xml`
+- Uses `Filter` to include only published books
+- Maps entity properties to sitemap URLs using `Selector`
+- Automatically registered via `ITransientDependency`
+
+### Example 3: Category-Based Dynamic Content
+
+For content with categories, you can build more complex URL patterns:
+
+```csharp
+using Abp.Sitemap.Web.Sitemap.Core;
+using Abp.Sitemap.Web.Sitemap.Sources.Group;
+
+namespace YourProject.Sitemaps;
+
+public class ArticleSitemapSource : GroupedSitemapItemSource, ITransientDependency
+{
+ public ArticleSitemapSource(
+ IReadOnlyRepository repository,
+ IAsyncQueryableExecuter executer)
+ : base(repository, executer, "Articles")
+ {
+ // Multiple filter conditions
+ Filter = x => x.IsPublished &&
+ !x.IsDeleted &&
+ x.PublishDate <= DateTime.Now;
+ }
+
+ protected override Expression> Selector =>
+ article => new SitemapItem(
+ article.Id.ToString(),
+ $"/blog/{article.Category.Slug}/{article.Slug}", // 👈 Category-based URL
+ article.LastModificationTime ?? article.CreationTime
+ );
+}
+```
+
+This example demonstrates:
+- Multiple filter conditions for complex business logic
+- Building URLs with category slugs
+
+## Testing Your Sitemaps
+
+After configuring the module, test your sitemap generation:
+
+### 1. Run Your Application
+
+```bash
+dotnet run
+```
+
+The sitemaps are automatically generated on application startup.
+
+### 2. Check Generated Files
+
+Navigate to `{WebProject}/Sitemaps/` directory (at the root of your web project):
+
+```
+{WebProject}
+└── Sitemaps/
+ ├── sitemap.xml # Main group (static pages)
+ ├── sitemap-Books.xml # Books from database
+ ├── sitemap-Articles.xml # Articles from database
+ └── sitemap-Help.xml # Help pages
+```
+
+### 3. Verify XML Content
+
+Open `sitemap-Books.xml` and verify the structure:
+
+```xml
+
+
+
+ https://yourdomain.com/Books/Detail/3a071e39-12c9-48d7-8c1e-3b4f5c6d7e8f
+ 2025-12-13
+
+
+ https://yourdomain.com/Books/Detail/7b8c9d0e-1f2a-3b4c-5d6e-7f8g9h0i1j2k
+ 2025-12-10
+
+
+```
+
+### 4. Test in Browser
+
+Visit the sitemap URLs directly (the module serves them from the root path):
+- Main sitemap: `https://localhost:5001/sitemap.xml`
+- Books sitemap: `https://localhost:5001/sitemap-Books.xml`
+
+> **Note:** The sitemaps are stored in `{WebProject}/Sitemaps/` directory and served directly from the root URL.
+
+## Advanced Configuration
+
+### Custom Regeneration Schedule
+
+Control when sitemaps are regenerated using cron expressions:
+
+```csharp
+public override void ConfigureServices(ServiceConfigurationContext context)
+{
+ Configure(options =>
+ {
+ options.BaseUrl = "https://yourdomain.com";
+ options.WorkerCronExpression = "0 0 2 * * ?"; // 👈 Every day at 2 AM
+ // Or use period in milliseconds:
+ // options.WorkerPeriod = 7200000; // 2 hours
+ });
+}
+```
+
+### Environment-Specific Configuration
+
+Use different settings for development and production:
+
+```csharp
+public override void ConfigureServices(ServiceConfigurationContext context)
+{
+ var configuration = context.Services.GetConfiguration();
+ var hostingEnvironment = context.Services.GetHostingEnvironment();
+
+ Configure(options =>
+ {
+ if (hostingEnvironment.IsDevelopment())
+ {
+ options.BaseUrl = "https://localhost:5001";
+ options.WorkerPeriod = 300000; // 5 minutes for testing
+ }
+ else
+ {
+ options.BaseUrl = configuration["App:SelfUrl"]!;
+ options.WorkerPeriod = 3600000; // 1 hour in production
+ }
+
+ options.FolderPath = "Sitemaps";
+ });
+}
+```
+
+### Manual Sitemap Generation
+
+Trigger sitemap generation manually (useful for admin panels):
+
+```csharp
+using Abp.Sitemap.Web.Sitemap.Management;
+
+public class SitemapManagementService : ITransientDependency
+{
+ private readonly SitemapFileGenerator _generator;
+
+ public SitemapManagementService(SitemapFileGenerator generator)
+ {
+ _generator = generator;
+ }
+
+ [Authorize("Admin")]
+ public async Task RegenerateSitemapsAsync()
+ {
+ await _generator.GenerateAsync(); // 👈 Manual regeneration
+ }
+}
+```
+
+## Real-World Use Cases
+
+Here are practical scenarios where the sitemap module excels:
+
+### E-Commerce Platform
+```csharp
+// Products grouped by category
+public class ProductSitemapSource : GroupedSitemapItemSource
+{
+ // Automatically includes all active products with stock
+}
+
+// Separate sitemap for categories
+public class CategorySitemapSource : GroupedSitemapItemSource
+{
+ // All browsable categories
+}
+
+// Brand pages
+public class BrandSitemapSource : GroupedSitemapItemSource
+{
+ // All active brands
+}
+```
+
+Result: `sitemap-Products.xml`, `sitemap-Categories.xml`, `sitemap-Brands.xml`
+
+### Content Management System
+```csharp
+// Blog posts by date
+public class BlogPostSitemapSource : GroupedSitemapItemSource
+{
+ // Filter by published date, priority based on view count
+}
+
+// Static CMS pages
+[IncludeSitemapXml]
+public class AboutUsModel : PageModel { }
+```
+
+## Best Practices
+
+### 1. Group Related Content
+Organize your sitemaps logically:
+```csharp
+// ✅ Good: Logical grouping
+"Products", "Categories", "Brands", "Blog", "Help"
+
+// ❌ Bad: Everything in one group
+"Main" // Contains 50,000 mixed URLs
+```
+
+### 2. Use Filters Wisely
+```csharp
+// ✅ Good: Only published, non-deleted content
+Filter = x => x.IsPublished &&
+ !x.IsDeleted &&
+ x.PublishDate <= DateTime.Now
+
+// ❌ Bad: Including draft content
+Filter = x => true // Everything included
+```
+
+### 3. Keep URLs Clean
+```csharp
+// ✅ Good: SEO-friendly URLs
+$"/products/{product.Slug}"
+$"/blog/{year}/{month}/{article.Slug}"
+
+// ❌ Bad: Technical IDs exposed
+$"/product-detail?id={product.Id}"
+```
+
+## Troubleshooting
+
+### Sitemap Not Generated
+**Problem:** No XML files in `{WebProject}/Sitemaps/`
+
+**Solutions:**
+1. Check module is added to dependencies
+2. Verify `SitemapOptions.BaseUrl` is configured
+3. Check application logs for errors
+4. Ensure the web project directory has write permissions
+
+### Pages Not Appearing
+**Problem:** Some pages missing from sitemap
+
+**Solutions:**
+1. Verify `[IncludeSitemapXml]` attribute is present
+2. Check namespace imports: `using Abp.Sitemap.Web.Sitemap.Sources.Page.Attributes;`
+3. Ensure PageModel classes are public
+4. Check filter conditions in custom sources
+
+### Background Worker Not Running
+**Problem:** Sitemaps not regenerating automatically
+
+**Solutions:**
+1. Check `SitemapOptions.WorkerPeriod` is set
+2. Verify background workers are enabled in ABP configuration
+3. Check application logs for worker errors
+
+## Performance Considerations
+
+### Caching Strategy
+Consider adding caching for frequently accessed sitemaps:
+
+```csharp
+public class CachedSitemapFileGenerator : ITransientDependency
+{
+ private readonly SitemapFileGenerator _generator;
+ private readonly IDistributedCache _cache;
+
+ public async Task GetOrGenerateAsync(string group)
+ {
+ var cacheKey = $"Sitemap:{group}";
+ var cached = await _cache.GetStringAsync(cacheKey);
+
+ if (cached != null)
+ return cached;
+
+ await _generator.GenerateAsync();
+ // Read and cache...
+ }
+}
+```
+
+## Conclusion
+
+The ABP Sitemap module provides a production-ready solution for dynamic sitemap generation in ABP Framework applications. By leveraging ABP's architecture—dependency injection, repository pattern, and background workers—the module automatically discovers pages, includes dynamic content, and regenerates sitemaps without manual intervention.
+
+Key benefits:
+✅ **Zero Configuration** for basic scenarios
+✅ **Type-Safe** attribute-based configuration
+✅ **Extensible** for complex business logic
+✅ **Performance** optimized with background processing
+✅ **SEO-Friendly** following XML sitemap standards
+
+Whether you're building a blog, e-commerce platform, or enterprise application, this module provides a solid foundation for search engine optimization.
+
+## Additional Resources
+
+### Documentation
+- [ABP Framework Documentation](https://abp.io/docs/latest/)
+- [ABP Background Workers](https://abp.io/docs/latest/framework/infrastructure/background-workers)
+- [ABP Repository Pattern](https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories)
+- [ABP Dependency Injection](https://abp.io/docs/latest/framework/fundamentals/dependency-injection)
+
+### Source Code
+- [Complete Working Demo](https://github.com/salihozkara/AbpSitemapDemo) - Full implementation with examples
+ - [BookSitemapSource](https://github.com/salihozkara/AbpSitemapDemo/blob/master/AbpSitemapDemo/Pages/Books/Index.cshtml.cs#L23) - Entity-based source example
+ - [Index.cshtml](https://github.com/salihozkara/AbpSitemapDemo/blob/master/AbpSitemapDemo/Pages/Index.cshtml#L9) - Page attribute usage
diff --git a/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/summary.md b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/summary.md
new file mode 100644
index 0000000000..0c20291c95
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/summary.md
@@ -0,0 +1 @@
+Learn how to use the ABP Sitemap module for automatic XML sitemap generation in your ABP Framework applications.
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/cover.png b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/cover.png
new file mode 100644
index 0000000000..3739cbb97a
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/cover.png differ
diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/abp-studio-ai-management.png b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/abp-studio-ai-management.png
new file mode 100644
index 0000000000..5577fb0b33
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/abp-studio-ai-management.png differ
diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-playground.png b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-playground.png
new file mode 100644
index 0000000000..7051772489
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-playground.png differ
diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-widget.png b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-widget.png
new file mode 100644
index 0000000000..bb3a75d8cd
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-widget.png differ
diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/aimanagement-workspace-geminiasopenai.png b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/aimanagement-workspace-geminiasopenai.png
new file mode 100644
index 0000000000..a221b3ed7d
Binary files /dev/null and b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/aimanagement-workspace-geminiasopenai.png differ
diff --git a/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md
new file mode 100644
index 0000000000..80c4a8fbeb
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/post.md
@@ -0,0 +1,106 @@
+# Introducing the AI Management Module: Manage AI Integration Dynamically
+
+We are excited to announce the **AI Management Module**, a powerful new module to the ABP Platform that makes managing AI capabilities in your applications easier. No need to redeploy your application, now you can configure, test, and manage your AI integrations on the fly through an intuitive user interface!
+
+## What is the AI Management Module?
+
+Built on top of the [ABP Framework's AI infrastructure](https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence), the AI Management Module allows you to manage AI workspaces dynamically without touching your code. Whether you're building a customer support chatbot, adding AI-powered search, or creating intelligent automation workflows, this module provides everything you need to manage AI integrations through a user-friendly interface.
+
+> **Note**: The AI Management Module is currently in **preview** and available to ABP Team or higher license holders.
+
+## What it offers?
+
+### Manage AI Without Redeployment
+
+Create, configure, and update AI workspaces directly from the UI. Switch between different AI providers (OpenAI, Azure OpenAI, Ollama, etc.), change models, adjust prompts, and test configurations, all without restarting your application or deploying new code.
+
+### Built-In Chat Interface
+
+Test your AI workspaces immediately with the included chat interface in playground pages. Verify your configurations work correctly before using them in production. Perfect for experimenting with different models, prompts, and settings.
+
+ 
+
+### Flexible for Any Architecture
+
+Whether you're building a monolith, microservices, or something in between, the module adapts to your needs:
+- Host AI management directly in your application with full UI and database
+- Deploy a centralized AI service that multiple applications can consume
+- Use it as an API gateway pattern for your microservices
+
+### Works with Any AI Provider
+
+Even AI Management module doesn't implement all the providers by default, it provides extensibility options with a good abstraction for other providers like Azure, Anthropic Claude, Google Gemini, and more. Or you can directly use the OpenAI adapter with LLMs that support OpenAI API.
+
+- Example of using Gemini as an OpenAI provider:
+
+ 
+
+
+You can even add your own custom AI providers: [learn how to implement a custom AI provider factory in the documentation](https://abp.io/docs/latest/modules/ai-management#implementing-custom-ai-provider-factories).
+
+### Ready to Use Chat Widget
+
+Drop a compact, pre-built chat widget into any page with minimal code. It includes streaming support, conversation history, and API integration for customization.
+
+- Simple to use with minimal code
+ ```cs
+ @await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
+ {
+ WorkspaceName = "StoryTeller",
+ })
+ ```
+
+- And result is a working, pre-integrated widget
+
+ 
+
+- [See the widget documentation](https://abp.io/docs/latest/modules/ai-management#client-usage-mvc-ui) for details and all parameters for customization.
+
+### Security
+
+Control who can manage and use AI workspaces with permission-based access control. Isolate your AI configurations by using workspaces with different permissions. Also, resource based authorization on workspaces is on the way and will be available in the next versions. It'll allow you to manage access to specific workspaces by a user or role.
+
+## Getting Started
+
+Installation is straightforward using the [ABP Studio](https://abp.io/studio). You can just enable **AI Management** module while creating a new project with ABP Studio and configure your preferred AI provider and model in the solution creation wizard.
+
+
+
+## Roadmap
+
+### v10.0 ✅
+- Workspace Management
+- MVC UI
+- Playground
+ - Chat History _(Client-Side)_
+- Client Components
+- Integration to Startup Templates
+
+### v10.1
+- Blazor UI
+- Angular UI
+- Resource based authorization on Workspaces
+- Agent-Framework compatibility examples
+
+### Future Goals
+- Microservice templates
+- MCP Support
+- RAG with file upload _(md, pdf, txt)_
+- Chat History _(Server-Side Conversations)_
+- OpenAI Compatible Endpoints
+- Tenant-Based Configuration
+- Extended RAG capabilities, _(ie. providing application data as tools)_
+
+
+## Ready to Get Started?
+
+The AI Management Module is available now for ABP Team and higher license holders.
+
+**Learn More:**
+- [AI Management Module Documentation](https://abp.io/docs/latest/modules/ai-management) - All features, scenarios, and technical details.
+- [AI Infrastructure Documentation](https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence) - Understanding AI workspaces in the framework.
+- [Usage Scenarios](https://abp.io/docs/latest/modules/ai-management#usage-scenarios) - Examples for different architectures.
+
+---
+
+*The AI Management Module is currently in preview. We're excited to hear your feedback as we continue to improve and add new features!*
\ No newline at end of file
diff --git a/docs/en/Community-Articles/2025-12-23-Referral-Program-Announcement/post.md b/docs/en/Community-Articles/2025-12-23-Referral-Program-Announcement/post.md
new file mode 100644
index 0000000000..77621a1be2
--- /dev/null
+++ b/docs/en/Community-Articles/2025-12-23-Referral-Program-Announcement/post.md
@@ -0,0 +1,38 @@
+We are happy to share some exciting news. We launched **ABP.IO Referral Program** as a way to thank our customers and community members who help introduce ABP.IO to new professionals and organizations\!
+
+If you already use ABP.IO and believe in it, you can now get benefits by recommending it to others.
+
+## **What is ABP.IO Referral Program?**
+
+
+ABP.IO Referral Program rewards people who bring new customers to ABP.IO.
+
+When someone you refer purchases an ABP.IO license, you earn a commission as a thank-you for your contribution.
+
+*\*This referral program is available to users who have an organization.*
+
+## **What Benefits Do You Get?**
+
+* Earn **5% commission** on the total sales price of each new license you refer
+
+* Get rewarded for sharing a platform you already know and trust
+
+## **Who Can Join?**
+
+You can participate if:
+
+* You are an existing ABP.IO customer who has purchased a license before
+
+* You are a license owner or a developer
+
+* Your license is active or expired
+
+* The purchase is not made by your own company
+
+## **Start Referring Today**
+
+If you are interested in joining the program, you can get started right away.
+
+👉 [**Start Referring Now**](https://abp.io/my-referrals)
+
+Thank you for being part of our community. Your support helps us grow and now it pays back.
diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json
index 08b4553d64..1cce28fa3f 100644
--- a/docs/en/docs-nav.json
+++ b/docs/en/docs-nav.json
@@ -563,7 +563,11 @@
{
"text": "Microsoft.Extensions.AI",
"path": "framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md"
- },
+ },
+ {
+ "text": "Agent Framework",
+ "path": "framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md"
+ },
{
"text": "Semantic Kernel",
"path": "framework/infrastructure/artificial-intelligence/microsoft-semantic-kernel.md"
@@ -1561,6 +1565,10 @@
"text": "SSR Configuration",
"path": "framework/ui/angular/ssr-configuration.md"
},
+ {
+ "text": "AI Tools Configuration",
+ "path": "framework/ui/angular/ai-config.md"
+ },
{
"text": "PWA Configuration",
"path": "framework/ui/angular/pwa-configuration.md"
@@ -1861,6 +1869,10 @@
"text": "Overriding the User Interface",
"path": "framework/architecture/modularity/extending/overriding-user-interface.md"
},
+ {
+ "text": "How to Override LeptonX CSS Variables",
+ "path": "framework/ui/common/leptonx-css-variables.md"
+ },
{
"text": "Utilities",
"items": [
@@ -2294,6 +2306,10 @@
"text": "Helm Charts and Kubernetes",
"path": "solution-templates/microservice/helm-charts-and-kubernetes.md"
},
+ {
+ "text": ".NET Aspire Integration",
+ "path": "solution-templates/microservice/aspire-integration.md"
+ },
{
"text": "Guides",
"items": [
@@ -2355,13 +2371,17 @@
"path": "modules/account-pro.md",
"isIndex": true
},
+ {
+ "text": "Idle Session Timeout",
+ "path": "modules/account/idle-session-timeout.md"
+ },
{
"text": "Tenant impersonation & User impersonation",
"path": "modules/account/impersonation.md"
},
{
- "text": "Idle Session Timeout",
- "path": "modules/account/idle-session-timeout.md"
+ "text": "Web Authentication API (WebAuthn) passkeys",
+ "path": "modules/account/passkey.md"
}
]
},
diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md
index c58769e57e..11ac3f7af1 100644
--- a/docs/en/framework/architecture/multi-tenancy/index.md
+++ b/docs/en/framework/architecture/multi-tenancy/index.md
@@ -47,7 +47,7 @@ ABP supports all the following approaches to store the tenant data in the databa
- **Database per Tenant**: Every tenant has a separate, dedicated database to store the data related to that tenant.
- **Hybrid**: Some tenants share a single database while some tenants may have their own databases.
-[Saas module (PRO)](../../../modules/saas.md) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches.
+[SaaS module (PRO)](../../../modules/saas.md) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches.
> You can see the community article *[Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9)* for more details about different database architectures with practical implementation details.
@@ -466,7 +466,7 @@ The [Tenant Management module](../../../modules/tenant-management.md) provides a
### A note about separate database per tenant approach in open source version
-While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have [Saas module (PRO)](../../../modules/saas.md).
+While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have [SaaS module (PRO)](../../../modules/saas.md).
Alternatively, you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly.
## See Also
diff --git a/docs/en/framework/fundamentals/dependency-injection.md b/docs/en/framework/fundamentals/dependency-injection.md
index a1eef66761..2e2e3e377a 100644
--- a/docs/en/framework/fundamentals/dependency-injection.md
+++ b/docs/en/framework/fundamentals/dependency-injection.md
@@ -264,7 +264,7 @@ public class TaxAppService : ApplicationService
``TaxAppService`` gets ``ITaxCalculator`` in its constructor. The dependency injection system automatically provides the requested service at runtime.
-Constructor injection is preffered way of injecting dependencies to a class. In that way, the class can not be constructed unless all constructor-injected dependencies are provided. Thus, the class explicitly declares it's required services.
+Constructor injection is preferred way of injecting dependencies to a class. In that way, the class can not be constructed unless all constructor-injected dependencies are provided. Thus, the class explicitly declares it's required services.
### Property Injection
diff --git a/docs/en/framework/fundamentals/exception-handling.md b/docs/en/framework/fundamentals/exception-handling.md
index 73e5d055fe..85b1c3b9c4 100644
--- a/docs/en/framework/fundamentals/exception-handling.md
+++ b/docs/en/framework/fundamentals/exception-handling.md
@@ -88,7 +88,7 @@ Error **details** in an optional field of the JSON error message. Thrown `Except
}
````
-`AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have higly customised validation logic.
+`AbpValidationException` implements the `IHasValidationErrors` interface and it is automatically thrown by the framework when a request input is not valid. So, usually you don't need to deal with validation errors unless you have highly customized validation logic.
### Logging
@@ -289,7 +289,7 @@ The `IHttpExceptionStatusCodeFinder` is used to automatically determine the HTTP
### Custom Mappings
-Automatic HTTP status code determination can be overrided by custom mappings. For example:
+Automatic HTTP status code determination can be overridden by custom mappings. For example:
````C#
services.Configure(options =>
diff --git a/docs/en/framework/fundamentals/validation.md b/docs/en/framework/fundamentals/validation.md
index 50540b5625..78e93e59f5 100644
--- a/docs/en/framework/fundamentals/validation.md
+++ b/docs/en/framework/fundamentals/validation.md
@@ -121,7 +121,7 @@ namespace Acme.BookStore
#### Enabling/Disabling Validation
-You can use the `[DisableValidation]` to disable it for methods, classs and properties.
+You can use the `[DisableValidation]` to disable it for methods, classes and properties.
````csharp
[DisableValidation]
diff --git a/docs/en/framework/infrastructure/artificial-intelligence/index.md b/docs/en/framework/infrastructure/artificial-intelligence/index.md
index bdd1125e9d..ddf9ad4b07 100644
--- a/docs/en/framework/infrastructure/artificial-intelligence/index.md
+++ b/docs/en/framework/infrastructure/artificial-intelligence/index.md
@@ -27,12 +27,14 @@ abp add-package Volo.Abp.AI
The `Volo.Abp.AI` package provides integration with the following libraries:
* [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
+* [Microsoft.Agents.AI (Agent Framework)](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
* [Microsoft.SemanticKernel](https://learn.microsoft.com/en-us/semantic-kernel/overview/)
-The Microsoft.Extensions.AI library is suggested for library developers to keep the library dependency minimum and simple (since it provides basic abstractions and fundamental AI provider integrations), while Semantic Kernel is suggested for applications that need rich and advanced AI integration features.
+The **Microsoft.Extensions.AI** library is suggested for library developers to keep the library dependency minimum and simple (since it provides basic abstractions and fundamental AI provider integrations). For applications, **Microsoft Agent Framework** is the recommended choice as it combines the best of both AutoGen and Semantic Kernel (it's direct successor of these two frameworks), offering simple abstractions for single- and multi-agent patterns along with advanced features like thread-based state management, type safety, filters, and telemetry. **Semantic Kernel** can still be used if you need its specific AI integration features.
Check the following documentation to learn how to use these libraries with the ABP integration:
- [ABP Microsoft.Extensions.AI integration](./microsoft-extensions-ai.md)
+- [ABP Microsoft.Agents.AI (Agent Framework) integration](./microsoft-agent-framework.md)
- [ABP Microsoft.SemanticKernel integration](./microsoft-semantic-kernel.md)
diff --git a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md
new file mode 100644
index 0000000000..d3e7d128fc
--- /dev/null
+++ b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md
@@ -0,0 +1,218 @@
+# Microsoft.Agents.AI (Agent Framework)
+
+[Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) is an open-source development kit for **building AI agents** and **multi-agent workflows**. It is the direct successor to both *AutoGen* and [*Semantic Kernel*](./microsoft-semantic-kernel.md), combining their strengths while adding new capabilities, and is the suggested framework for building AI agent applications. This documentation is about the usage of this library with ABP Framework. Make sure you have read the [Artificial Intelligence](./index.md) documentation before reading this documentation.
+
+## Usage
+
+**Microsoft Agent Framework** works on top of `IChatClient` from **Microsoft.Extensions.AI**. After obtaining an `IChatClient` instance, you can create an AI agent using the `CreateAIAgent` extension method:
+
+```csharp
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+
+public class MyService
+{
+ private readonly IChatClient _chatClient;
+
+ public MyService(IChatClient chatClient)
+ {
+ _chatClient = chatClient;
+ }
+
+ public async Task GetResponseAsync(string userMessage)
+ {
+ AIAgent agent = _chatClient.CreateAIAgent(
+ instructions: "You are a helpful assistant that provides concise answers."
+ );
+
+ AgentRunResponse response = await agent.RunAsync(userMessage);
+
+ return response.Text;
+ }
+}
+```
+
+You can also use `IChatClientAccessor` to access the `IChatClient` in scenarios where AI capabilities are **optional**, such as when developing a module or a service that may use AI capabilities optionally:
+
+```csharp
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Volo.Abp.AI;
+
+public class MyService
+{
+ private readonly IChatClientAccessor _chatClientAccessor;
+
+ public MyService(IChatClientAccessor chatClientAccessor)
+ {
+ _chatClientAccessor = chatClientAccessor;
+ }
+
+ public async Task GetResponseAsync(string userMessage)
+ {
+ var chatClient = _chatClientAccessor.ChatClient;
+ if (chatClient is null)
+ {
+ return "No chat client configured";
+ }
+
+ AIAgent agent = chatClient.CreateAIAgent(
+ instructions: "You are a helpful assistant that provides concise answers."
+ );
+
+ var response = await agent.RunAsync(userMessage);
+
+ return response.Text;
+ }
+}
+```
+
+### Workspaces
+
+Workspaces are a way to configure isolated AI configurations for a named scope. You can define a workspace by decorating a class with the `WorkspaceNameAttribute` attribute that carries the workspace name.
+- Workspace names must be unique.
+- Workspace names cannot contain spaces _(use underscores or camelCase)_.
+- Workspace names are case-sensitive.
+
+```csharp
+using Volo.Abp.AI;
+
+[WorkspaceName("CommentSummarization")]
+public class CommentSummarization
+{
+}
+```
+
+> [!NOTE]
+> If you don't specify the workspace name, the full name of the class will be used as the workspace name.
+
+You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, you will get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client.
+
+`IChatClient` or `IChatClientAccessor` can be resolved to access a specific workspace's chat client. This is a typed chat client and can be configured separately from the default chat client.
+
+Example of resolving a typed chat client for a workspace:
+
+```csharp
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Volo.Abp.AI;
+
+public class MyService
+{
+ private readonly IChatClient _chatClient;
+
+ public MyService(IChatClient chatClient)
+ {
+ _chatClient = chatClient;
+ }
+
+ public async Task GetResponseAsync(string userMessage)
+ {
+ AIAgent agent = _chatClient.CreateAIAgent(
+ instructions: "You are a customer support assistant. Be polite and helpful."
+ );
+
+ var response = await agent.RunAsync(userMessage);
+ return response.Text;
+ }
+}
+```
+
+Example of resolving a typed chat client accessor for a workspace:
+
+```csharp
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using Volo.Abp.AI;
+
+public class MyService
+{
+ private readonly IChatClientAccessor _chatClientAccessor;
+
+ public MyService(IChatClientAccessor chatClientAccessor)
+ {
+ _chatClientAccessor = chatClientAccessor;
+ }
+
+ public async Task GetResponseAsync(string userMessage)
+ {
+ var chatClient = _chatClientAccessor.ChatClient;
+ if (chatClient is null)
+ {
+ return "No chat client configured";
+ }
+
+ AIAgent agent = chatClient.CreateAIAgent(
+ instructions: "You are a customer support assistant. Be polite and helpful."
+ );
+
+ var response = await agent.RunAsync(userMessage);
+ return response.Text;
+ }
+}
+```
+
+## Configuration
+
+**Microsoft Agent Framework** uses `IChatClient` from **Microsoft.Extensions.AI** as its foundation. Therefore, the configuration process for workspaces is the same as described in the [Microsoft.Extensions.AI documentation](./microsoft-extensions-ai.md#configuration).
+
+You need to configure the Chat Client for your workspace using `AbpAIWorkspaceOptions`, and then you can use the `CreateAIAgent` extension method to create AI agents from the configured chat client.
+
+To configure a chat client, you'll need a LLM provider package such as [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI) or [OllamaSharp](https://www.nuget.org/packages/OllamaSharp/).
+
+_The following example requires [OllamaSharp](https://www.nuget.org/packages/OllamaSharp/) package to be installed._
+
+Demonstration of the default workspace configuration:
+
+```csharp
+[DependsOn(typeof(AbpAIModule))]
+public class MyProjectModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(options =>
+ {
+ options.Workspaces.ConfigureDefault(configuration =>
+ {
+ configuration.ConfigureChatClient(chatClientConfiguration =>
+ {
+ chatClientConfiguration.Builder = new ChatClientBuilder(
+ sp => new OllamaApiClient("http://localhost:11434", "mistral")
+ );
+ });
+ });
+ });
+ }
+}
+```
+
+Demonstration of the isolated workspace configuration:
+
+```csharp
+[DependsOn(typeof(AbpAIModule))]
+public class MyProjectModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ PreConfigure(options =>
+ {
+ options.Workspaces.Configure(configuration =>
+ {
+ configuration.ConfigureChatClient(chatClientConfiguration =>
+ {
+ chatClientConfiguration.Builder = new ChatClientBuilder(
+ sp => new OllamaApiClient("http://localhost:11434", "mistral")
+ );
+ });
+ });
+ });
+ }
+}
+```
+
+## See Also
+
+- [Usage of Microsoft.Extensions.AI](./microsoft-extensions-ai.md)
+- [Usage of Semantic Kernel](./microsoft-semantic-kernel.md)
+- [Microsoft Agent Framework Overview](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
+- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/)
\ No newline at end of file
diff --git a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md
index 35b370585e..b44572838a 100644
--- a/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md
+++ b/docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md
@@ -172,5 +172,6 @@ public class MyProjectModule : AbpModule
## See Also
+- [Usage of Agent Framework](./microsoft-agent-framework.md)
- [Usage of Semantic Kernel](./microsoft-semantic-kernel.md)
- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/)
\ No newline at end of file
diff --git a/docs/en/framework/infrastructure/audit-logging.md b/docs/en/framework/infrastructure/audit-logging.md
index e559e5ed22..9811e542ab 100644
--- a/docs/en/framework/infrastructure/audit-logging.md
+++ b/docs/en/framework/infrastructure/audit-logging.md
@@ -215,7 +215,7 @@ public class MyUser : Entity
public string Email { get; set; }
- [DisableAuditing] //Ignore the Passoword on audit logging
+ [DisableAuditing] //Ignore the Password on audit logging
public string Password { get; set; }
}
````
diff --git a/docs/en/framework/infrastructure/background-workers/hangfire.md b/docs/en/framework/infrastructure/background-workers/hangfire.md
index 832084de4c..9b65a488a9 100644
--- a/docs/en/framework/infrastructure/background-workers/hangfire.md
+++ b/docs/en/framework/infrastructure/background-workers/hangfire.md
@@ -61,7 +61,7 @@ After you have installed these NuGet packages, you need to configure your projec
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
- //... other configarations.
+ //... other configurations.
ConfigureHangfire(context, configuration);
}
diff --git a/docs/en/framework/infrastructure/background-workers/quartz.md b/docs/en/framework/infrastructure/background-workers/quartz.md
index 7f591e0aeb..9693e4795f 100644
--- a/docs/en/framework/infrastructure/background-workers/quartz.md
+++ b/docs/en/framework/infrastructure/background-workers/quartz.md
@@ -43,7 +43,7 @@ public class YourModule : AbpModule
}
````
-> Quartz background worker integration provided `QuartzPeriodicBackgroundWorkerAdapter` to adapt `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived class. So, you can still fllow the [background workers document](../background-workers) to define the background worker.
+> Quartz background worker integration provided `QuartzPeriodicBackgroundWorkerAdapter` to adapt `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived class. So, you can still follow the [background workers document](../background-workers) to define the background worker.
## Configuration
diff --git a/docs/en/framework/infrastructure/current-user.md b/docs/en/framework/infrastructure/current-user.md
index a411d1f4d5..cb5954a778 100644
--- a/docs/en/framework/infrastructure/current-user.md
+++ b/docs/en/framework/infrastructure/current-user.md
@@ -101,7 +101,7 @@ For a web application, it gets the `User` property of the current `HttpContext`.
### Basic Usage
-You can inject `ICurrentPrincipalAccessor` and use the `Principal` property to the the current principal:
+You can inject `ICurrentPrincipalAccessor` and use the `Principal` property to get the current principal:
````csharp
public class MyService : ITransientDependency
diff --git a/docs/en/framework/infrastructure/emailing.md b/docs/en/framework/infrastructure/emailing.md
index 7c240a9d75..a9304b2e57 100644
--- a/docs/en/framework/infrastructure/emailing.md
+++ b/docs/en/framework/infrastructure/emailing.md
@@ -235,7 +235,7 @@ The final rendered message was shown above.
You typically want to replace the standard templates with your own ones, so you can prepare a branded email messages. To do that, you can use the power of the [virtual file system](../infrastructure/virtual-file-system.md) (VFS) or replace them in your own template definition provider.
-Pathes of the templates in the virtual file system are shown below:
+Paths of the templates in the virtual file system are shown below:
* `/Volo/Abp/Emailing/Templates/Layout.tpl`
* `/Volo/Abp/Emailing/Templates/Message.tpl`
@@ -250,7 +250,7 @@ See the [text templating system](./text-templating) document for details.
## NullEmailSender
-`NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the [standard log system](../fundamentals/logging.md), rathen than actually sending the emails.
+`NullEmailSender` is a built-in class that implements the `IEmailSender`, but writes email contents to the [standard log system](../fundamentals/logging.md), rather than actually sending the emails.
This class can be useful especially in development time where you generally don't want to send real emails. The [application startup template](../../solution-templates/layered-web-application) already uses this class in the **DEBUG mode** with the following configuration in the domain layer:
diff --git a/docs/en/framework/infrastructure/features.md b/docs/en/framework/infrastructure/features.md
index 2d135b822b..e450dc2068 100644
--- a/docs/en/framework/infrastructure/features.md
+++ b/docs/en/framework/infrastructure/features.md
@@ -395,8 +395,31 @@ There are three pre-defined value providers, executed by the given order:
* `TenantFeatureValueProvider` tries to get if the feature value is explicitly set for the **current tenant**.
* `EditionFeatureValueProvider` tries to get the feature value for the current edition. Edition Id is obtained from the current principal identity (`ICurrentPrincipalAccessor`) with the claim name `editionid` (a constant defined as`AbpClaimTypes.EditionId`). Editions are not implemented for the [tenant management](../../modules/tenant-management.md) module. You can implement it yourself or consider to use the [SaaS module](https://abp.io/modules/Volo.Saas) of the ABP Commercial.
+* `ConfigurationFeatureValueProvider`: Gets the value from the [IConfiguration service](../fundamentals/configuration.md).
* `DefaultValueFeatureValueProvider` gets the default value of the feature.
+#### Feature Values in the Application Configuration
+
+The `ConfigurationFeatureValueProvider` reads the feature values from the `IConfiguration` service, which can read values from the `appsettings.json` by default. So, the easiest way to configure feature values is to define them in the `appsettings.json` file.
+
+For example, you can configure feature values as shown below:
+
+````json
+{
+ "Features": {
+ "MyApp.Reporting": "true",
+ "MyApp.PdfReporting": "true",
+ "MyApp.MaxProductCount": "50"
+ }
+}
+````
+
+Feature values should be configured under the `Features` section as like in this example.
+
+> `IConfiguration` is an .NET Core service and it can read values not only from the `appsettings.json`, but also from the environment, user secrets... etc. See [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) for more.
+
+#### Custom Feature Value Providers
+
You can write your own provider by inheriting the `FeatureValueProvider`.
**Example: Enable all features for a user with "SystemAdmin" as a "User_Type" claim value**
diff --git a/docs/en/framework/infrastructure/settings.md b/docs/en/framework/infrastructure/settings.md
index b48b1c84dc..38c9528d86 100644
--- a/docs/en/framework/infrastructure/settings.md
+++ b/docs/en/framework/infrastructure/settings.md
@@ -43,7 +43,7 @@ ABP automatically discovers this class and registers the setting definitions.
* **DefaultValue**: A setting may have a default value.
* **DisplayName**: A localizable string that can be used to show the setting name on the UI.
* **Description**: A localizable string that can be used to show the setting description on the UI.
-* **IsVisibleToClients**: A boolean value indicates that whether this setting value is available in the client side or not. Default value is false to prevent accidently publishing an internal critical setting value.
+* **IsVisibleToClients**: A boolean value indicates that whether this setting value is available in the client side or not. Default value is false to prevent accidentally publishing an internal critical setting value.
* **IsInherited**: A boolean value indicates that whether this setting value is inherited from other providers or not. Default value is true and fallbacks to the next provider if the setting value was not set for the requested provider (see the setting value providers section for more).
* **IsEncrypted**: A boolean value indicates that whether this setting value should be encrypted on save and decrypted on read. It makes possible to secure the setting value in the database.
* **Providers**: Can be used to restrict providers available for a particular setting (see the setting value providers section for more).
diff --git a/docs/en/framework/ui/angular/ai-config.md b/docs/en/framework/ui/angular/ai-config.md
new file mode 100644
index 0000000000..06f690016a
--- /dev/null
+++ b/docs/en/framework/ui/angular/ai-config.md
@@ -0,0 +1,262 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to configure AI-powered development tools for ABP Framework Angular applications with automatic setup for Claude, Cursor, Copilot, Gemini, Junie, and Windsurf."
+}
+```
+
+# AI Configuration
+
+ABP Framework provides an **AI Configuration Generator** that helps developers set up AI-powered coding assistants for their Angular applications. This schematic automatically generates configuration files for popular AI tools with pre-configured ABP best practices and guidelines.
+
+## Overview
+
+The AI Configuration Generator is an Angular schematic that creates standardized configuration files for various AI development tools. These configurations include:
+
+- ABP Framework coding standards and best practices
+- Angular development guidelines
+- Project-specific rules and conventions
+- Full-stack development patterns (ABP .NET + Angular)
+
+## Supported AI Tools
+
+The generator supports the following AI coding assistants:
+
+- **Claude** - Creates `.claude/CLAUDE.md` configuration file
+- **Copilot** - Creates `.github/copilot-instructions.md` configuration file
+- **Cursor** - Creates `.cursor/rules/cursor.mdc` configuration file
+- **Gemini** - Creates `.gemini/GEMINI.md` configuration file
+- **Junie** - Creates `.junie/guidelines.md` configuration file
+- **Windsurf** - Creates `.windsurf/rules/guidelines.md` configuration file
+
+## Usage
+
+### Basic Usage
+
+Generate AI configuration for a single tool:
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool=claude
+```
+
+### Multiple Tools
+
+Generate configurations for multiple AI tools at once:
+
+```bash
+# Comma-separated
+ng g @abp/ng.schematics:ai-config --tool=claude,cursor,copilot
+
+# Space-separated (with quotes)
+ng g @abp/ng.schematics:ai-config --tool="claude cursor gemini"
+
+# Multiple --tool flags
+ng g @abp/ng.schematics:ai-config --tool=claude --tool=cursor --tool=gemini
+```
+
+### Target Specific Project
+
+By default, configurations are generated at the workspace root. To target a specific project:
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool=claude --target-project=my-app
+```
+
+This creates the configuration files in the `my-app` project root directory.
+
+### Overwrite Existing Files
+
+If configuration files already exist, use the `--overwrite` flag to replace them:
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool=cursor --overwrite
+```
+
+## Schema Options
+
+The AI Configuration Generator accepts the following options:
+
+### tool
+
+- **Type:** `string`
+- **Required:** Yes
+- **Description:** Comma-separated list of AI tools to generate configurations for
+- **Valid values:** `claude`, `copilot`, `cursor`, `gemini`, `junie`, `windsurf`
+- **Example:** `"claude,cursor,copilot"`
+
+### targetProject
+
+- **Type:** `string`
+- **Required:** No
+- **Description:** The name of the target project in your workspace
+- **Default:** Workspace root (`/`)
+- **Example:** `"my-angular-app"`
+
+### overwrite
+
+- **Type:** `boolean`
+- **Required:** No
+- **Default:** `false`
+- **Description:** Whether to overwrite existing configuration files
+
+## Configuration Content
+
+All generated configuration files include comprehensive guidelines for:
+
+### General Principles
+- Clear separation between backend (ABP/.NET) and frontend (Angular) layers
+- Modular architecture patterns
+- Official ABP documentation references
+- Readability, maintainability, and performance standards
+
+### ABP / .NET Development Rules
+- Standard folder structure (`*.Application`, `*.Domain`, `*.EntityFrameworkCore`, `*.HttpApi`)
+- C# coding conventions and naming patterns
+- Modern C# features (records, pattern matching, null-coalescing)
+- ABP module integration (Permissions, Settings, Audit Logging)
+- Error handling and validation patterns
+
+### Angular Development Rules
+- Angular coding style and best practices
+- Component architecture patterns
+- Reactive programming with RxJS
+- ABP Angular package usage (`@abp/ng.core`, `@abp/ng.theme.shared`)
+- State management and service patterns
+
+### Performance and Testing
+- Performance optimization techniques
+- Unit testing and integration testing guidelines
+- Best practices for both backend and frontend
+
+## Examples
+
+### Example 1: Setup Claude for Development
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool=claude
+```
+
+Output:
+```
+🚀 Generating AI configuration files...
+📁 Target path: /
+🤖 Selected tools: claude
+✅ AI configuration files generated successfully!
+
+📝 Generated files:
+ - .claude/CLAUDE.md
+
+💡 Tip: Restart your IDE or AI tool to apply the new configurations.
+```
+
+### Example 2: Setup Multiple Tools for a Project
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool="cursor,copilot,gemini" --target-project=acme-app
+```
+
+Output:
+```
+🚀 Generating AI configuration files...
+📁 Target path: /acme-app
+🤖 Selected tools: cursor, copilot, gemini
+✅ AI configuration files generated successfully!
+
+📝 Generated files:
+ - /acme-app/.cursor/rules/cursor.mdc
+ - /acme-app/.github/copilot-instructions.md
+ - /acme-app/.gemini/GEMINI.md
+
+💡 Tip: Restart your IDE or AI tool to apply the new configurations.
+```
+
+### Example 3: Update Existing Configuration
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool=windsurf --overwrite
+```
+
+This will regenerate the Windsurf configuration file even if it already exists.
+
+## File Structure
+
+After running the generator, your project will have configuration files in their respective directories:
+
+```
+your-project/
+├── .claude/
+│ └── CLAUDE.md # Claude AI configuration
+├── .cursor/
+│ └── rules/
+│ └── cursor.mdc # Cursor AI configuration
+├── .github/
+│ └── copilot-instructions.md # GitHub Copilot configuration
+├── .gemini/
+│ └── GEMINI.md # Gemini AI configuration
+├── .junie/
+│ └── guidelines.md # Junie AI configuration
+└── .windsurf/
+ └── rules/
+ └── guidelines.md # Windsurf AI configuration
+```
+
+## Best Practices
+
+1. **Generate Early**: Set up AI configurations at the beginning of your project to ensure consistent code quality from the start.
+
+2. **Multiple Tools**: If your team uses different AI assistants, generate configurations for all of them to maintain consistency across the team.
+
+3. **Version Control**: Commit the generated configuration files to your repository so all team members benefit from the same AI guidelines.
+
+4. **Keep Updated**: When ABP releases new best practices or your project evolves, regenerate configurations with the `--overwrite` flag.
+
+5. **Project-Specific**: For monorepos or multi-project workspaces, use `--target-project` to create project-specific configurations.
+
+## Troubleshooting
+
+### Configuration File Already Exists
+
+If you see a warning that a configuration file already exists:
+
+```
+⚠️ Configuration file already exists: .claude/CLAUDE.md
+ Use --overwrite flag to replace existing files.
+```
+
+Add the `--overwrite` flag to replace it:
+
+```bash
+ng g @abp/ng.schematics:ai-config --tool=claude --overwrite
+```
+
+### Invalid Tool Name
+
+If you specify an invalid tool name:
+
+```
+Invalid AI tool(s): chatgpt. Valid options are: claude, copilot, cursor, gemini, junie, windsurf
+```
+
+Make sure to use only the supported tool names listed above.
+
+### No Tools Selected
+
+If you run the command without specifying any tools:
+
+```bash
+ng g @abp/ng.schematics:ai-config
+```
+
+You'll see usage examples and available tools:
+
+```
+ℹ️ No AI tools selected. Skipping configuration generation.
+
+💡 Usage examples:
+ ng g @abp/ng.schematics:ai-config --tool=claude,cursor
+ ng g @abp/ng.schematics:ai-config --tool="claude, cursor"
+ ng g @abp/ng.schematics:ai-config --tool=gemini --tool=cursor
+ ng g @abp/ng.schematics:ai-config --tool=gemini --target-project=my-app
+
+Available tools: claude, copilot, cursor, gemini, junie, windsurf
+```
\ No newline at end of file
diff --git a/docs/en/framework/ui/angular/ssr-configuration.md b/docs/en/framework/ui/angular/ssr-configuration.md
index 258fce99b4..e058beb861 100644
--- a/docs/en/framework/ui/angular/ssr-configuration.md
+++ b/docs/en/framework/ui/angular/ssr-configuration.md
@@ -1,13 +1,13 @@
```json
//[doc-seo]
{
- "Description": "Learn how to configure Server-Side Rendering (SSR) for your Angular application in the ABP Framework to improve performance and SEO."
+ "Description": "Learn how to configure Server-Side Rendering (SSR) for your Angular application in the ABP Framework to improve performance and SEO."
}
```
# SSR Configuration
-[Server-Side Rendering (SSR)](https://angular.io/guide/ssr) is a process that involves rendering pages on the server, resulting in initial HTML content that contains the page state. This allows the browser to show the page to the user immediately, before the JavaScript bundles are downloaded and executed.
+[Server-Side Rendering (SSR)](https://angular.dev/guide/ssr) is a process that involves rendering pages on the server, resulting in initial HTML content that contains the page state. This allows the browser to show the page to the user immediately, before the JavaScript bundles are downloaded and executed.
SSR improves the **performance** (First Contentful Paint) and **SEO** (Search Engine Optimization) of your application.
@@ -105,10 +105,10 @@ If your project uses the **Webpack Builder** (`@angular-devkit/build-angular:bro
```typescript
import {
- AngularNodeAppEngine,
- createNodeRequestHandler,
- isMainModule,
- writeResponseToNodeResponse,
+ AngularNodeAppEngine,
+ createNodeRequestHandler,
+ isMainModule,
+ writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
@@ -128,28 +128,28 @@ const angularApp = new AngularNodeAppEngine();
* Serve static files from /browser
*/
app.use(
- express.static(browserDistFolder, {
- maxAge: '1y',
- index: false,
- redirect: false,
- }),
+ express.static(browserDistFolder, {
+ maxAge: '1y',
+ index: false,
+ redirect: false,
+ }),
);
/**
* Handle all other requests by rendering the Angular application.
*/
app.use((req, res, next) => {
- angularApp
- .handle(req)
- .then(response => {
- if (response) {
- res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false});
- return writeResponseToNodeResponse(response, res);
- } else {
- return next()
- }
- })
- .catch(next);
+ angularApp
+ .handle(req)
+ .then(response => {
+ if (response) {
+ res.cookie('ssr-init', 'true', {...secureCookie, httpOnly: false});
+ return writeResponseToNodeResponse(response, res);
+ } else {
+ return next()
+ }
+ })
+ .catch(next);
});
// ... (Start server logic)
@@ -162,10 +162,10 @@ export const reqHandler = createNodeRequestHandler(app);
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
- {
- path: '**',
- renderMode: RenderMode.Server
- }
+ {
+ path: '**',
+ renderMode: RenderMode.Server
+ }
];
```
@@ -180,16 +180,16 @@ import { serverRoutes } from './app.routes.server';
import { SSR_FLAG } from '@abp/ng.core';
const serverConfig: ApplicationConfig = {
- providers: [
- provideAppInitializer(() => {
- const platformId = inject(PLATFORM_ID);
- const transferState = inject(TransferState);
- if (isPlatformServer(platformId)) {
- transferState.set(SSR_FLAG, true);
- }
- }),
- provideServerRendering(withRoutes(serverRoutes)),
- ],
+ providers: [
+ provideAppInitializer(() => {
+ const platformId = inject(PLATFORM_ID);
+ const transferState = inject(TransferState);
+ if (isPlatformServer(platformId)) {
+ transferState.set(SSR_FLAG, true);
+ }
+ }),
+ provideServerRendering(withRoutes(serverRoutes)),
+ ],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
@@ -237,13 +237,177 @@ The schematic installs `openid-client` to handle authentication on the server si
> Ensure your OpenID Connect configuration (in `environment.ts` or `app.config.ts`) is compatible with the server environment.
-## 5. Deployment
+## 5. Render Modes & Hybrid Rendering
-To deploy your Angular SSR application to a production server, follow these steps:
+Angular 20 provides different rendering modes that you can configure per route in the `app.routes.server.ts` file to optimize performance and SEO.
-### 5.1. Build the Application
+### 5.1. Available Render Modes
+
+```typescript
+import { RenderMode, ServerRoute } from '@angular/ssr';
+
+export const serverRoutes: ServerRoute[] = [
+ // Server-Side Rendering - renders on every request
+ {
+ path: 'dashboard',
+ renderMode: RenderMode.Server
+ },
+
+ // Prerender (SSG) - renders at build time
+ {
+ path: 'about',
+ renderMode: RenderMode.Prerender
+ },
+
+ // Client-Side Rendering - renders only in browser
+ {
+ path: 'admin/**',
+ renderMode: RenderMode.Client
+ },
+
+ // Default fallback
+ {
+ path: '**',
+ renderMode: RenderMode.Server
+ }
+];
+```
+
+#### RenderMode.Server (SSR)
+Renders HTML on every request. Best for dynamic content, personalized pages, and pages requiring authentication.
+
+#### RenderMode.Prerender (SSG)
+Generates static HTML at build time. Best for marketing pages, blog posts, and content that doesn't change frequently.
+
+For dynamic routes, use `getPrerenderParams`:
+
+```typescript
+{
+ path: 'blog/:slug',
+ renderMode: RenderMode.Prerender,
+ getPrerenderParams: async () => {
+ const posts = await fetchBlogPosts();
+ return posts.map(post => ({ slug: post.slug }));
+ }
+}
+```
+
+#### RenderMode.Client (CSR)
+Traditional client-side rendering. Best for highly interactive applications and admin panels that don't need SEO.
+
+### 5.2. Hybrid Rendering
+
+Combine different modes in one application for optimal results:
+
+```typescript
+export const serverRoutes: ServerRoute[] = [
+ // Static pages
+ { path: '', renderMode: RenderMode.Prerender },
+ { path: 'about', renderMode: RenderMode.Prerender },
-Run the build command to generate the production artifacts:
+ // Dynamic pages
+ { path: 'account', renderMode: RenderMode.Server },
+ { path: 'orders', renderMode: RenderMode.Server },
+
+ // Admin area
+ { path: 'admin/**', renderMode: RenderMode.Client },
+];
+```
+
+## 6. Hydration
+
+Hydration is the process where Angular attaches to server-rendered HTML and makes it interactive. The ABP schematic automatically configures hydration for your application.
+
+### 6.1. Common Hydration Issues
+
+**Problem: Browser APIs on Server**
+
+```typescript
+// ❌ Bad - will fail on server
+const width = window.innerWidth;
+
+// ✅ Good - check platform
+import { isPlatformBrowser } from '@angular/common';
+import { PLATFORM_ID, inject } from '@angular/core';
+
+export class MyComponent {
+ platformId = inject(PLATFORM_ID);
+
+ getWidth() {
+ if (isPlatformBrowser(this.platformId)) {
+ return window.innerWidth;
+ }
+ return 0;
+ }
+}
+```
+
+**Problem: Random or Time-Based Values**
+
+```typescript
+// ❌ Bad - generates different values on server and client
+id = Math.random();
+currentTime = new Date();
+
+// ✅ Good - use TransferState for consistent data
+import { TransferState, makeStateKey } from '@angular/core';
+
+const TIME_KEY = makeStateKey('time');
+
+constructor(private transferState: TransferState) {
+ if (isPlatformServer(this.platformId)) {
+ this.transferState.set(TIME_KEY, new Date().toISOString());
+ } else {
+ this.time = this.transferState.get(TIME_KEY, new Date().toISOString());
+ }
+}
+```
+
+**Enable Debug Tracing:**
+
+```typescript
+// app.config.ts
+import { provideClientHydration, withDebugTracing } from '@angular/platform-browser';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideClientHydration(withDebugTracing()),
+ ]
+};
+```
+
+## 7. Environment Variables
+
+Configure your SSR application using environment variables in `server.ts`:
+
+```typescript
+// server.ts
+const PORT = process.env['PORT'] || 4000;
+const HOST = process.env['HOST'] || 'localhost';
+
+// Start the server
+if (isMainModule(import.meta.url)) {
+ app.listen(PORT, () => {
+ console.log(`Server running on http://${HOST}:${PORT}`);
+ });
+}
+```
+
+For production, set environment variables:
+
+```bash
+# .env file or environment configuration
+NODE_ENV=production
+PORT=4000
+HOST=0.0.0.0
+API_URL=https://api.yourdomain.com
+```
+
+## 8. Deployment
+
+To deploy your Angular SSR application to a production server:
+
+### 8.1. Build the Application
```shell
yarn build
@@ -251,30 +415,152 @@ yarn build
yarn run build:ssr
```
-### 5.2. Prepare Artifacts
+### 8.2. Prepare Artifacts
-After the build is complete, you will find the output in the `dist` folder.
-For the **Application Builder**, the output structure typically looks like this:
+Copy the `dist/MyProjectName` folder to your server:
```
dist/MyProjectName/
├── browser/ # Client-side bundles
-└── server/ # Server-side bundles and entry point (server.mjs)
+└── server/ # Server-side bundles (server.mjs)
```
-You need to copy the entire `dist/MyProjectName` folder to your server.
+### 8.3. Install Production Dependencies
+
+On your server, install only the required dependencies (schematic already added them to package.json):
+
+```shell
+npm install --production
+```
-### 5.3. Run the Server
+Required dependencies:
+- `express`: Web server framework
+- `openid-client`: Authentication support
-On your server, navigate to the folder where you copied the artifacts and run the server using Node.js:
+### 8.4. Run the Server
+**Development/Testing:**
```shell
node server/server.mjs
```
-> [!TIP]
-> It is recommended to use a process manager like [PM2](https://pm2.keymetrics.io/) to keep your application alive and handle restarts.
+**Production (with PM2):**
+
+Use [PM2](https://pm2.keymetrics.io/) to keep your application alive and manage restarts:
```shell
+npm install -g pm2
pm2 start server/server.mjs --name "my-app"
+pm2 startup # Configure PM2 to start on boot
+pm2 save # Save current process list
+```
+
+## 9. Troubleshooting
+
+### 9.1. "Window/Document is not defined"
+
+Browser APIs don't exist on the server. Always check the platform:
+
+```typescript
+import { isPlatformBrowser } from '@angular/common';
+
+if (isPlatformBrowser(this.platformId)) {
+ // Safe to use window, document, localStorage, etc.
+}
+```
+
+### 9.2. "LocalStorage is not defined"
+
+ABP Core provides `AbpLocalStorageService` that implements the `Storage` interface and works safely on both server and client:
+
+```typescript
+import { AbpLocalStorageService } from '@abp/ng.core';
+
+@Injectable({ providedIn: 'root' })
+export class MyService {
+ private storage = inject(AbpLocalStorageService);
+
+ saveData(key: string, value: string): void {
+ // Safe on both server and client
+ this.storage.setItem(key, value);
+ }
+
+ getData(key: string): string | null {
+ // Returns null on server, actual value on client
+ return this.storage.getItem(key);
+ }
+}
+```
+
+`AbpLocalStorageService` implements all `Storage` methods:
+- `getItem(key: string): string | null`
+- `setItem(key: string, value: string): void`
+- `removeItem(key: string): void`
+- `clear(): void`
+- `key(index: number): string | null`
+- `length: number`
+
+### 9.3. Hydration Mismatch Errors
+
+If you see "NG0500" errors in the console:
+
+1. Enable debug tracing (see section 6.1)
+2. Check for dynamic content (dates, random IDs)
+3. Ensure server and client render the same HTML
+4. Use `TransferState` for data consistency
+
+### 9.4. Avoiding Duplicate API Calls
+
+ABP Core provides a `transferStateInterceptor` that automatically prevents duplicate HTTP GET requests during hydration. When you use `provideAbpCore()`, this interceptor is already active.
+
+**How it works:**
+- Server: Stores HTTP GET responses in `TransferState`
+- Client: Reuses stored responses during hydration
+- Automatically cleans up stored data after use
+
+```typescript
+// app.config.ts
+import { provideAbpCore } from '@abp/ng.core';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideAbpCore(),
+ // transferStateInterceptor is automatically included
+ ]
+};
```
+
+The interceptor works with all HTTP GET requests made through `HttpClient`:
+
+```typescript
+// This service automatically benefits from the interceptor
+@Injectable({ providedIn: 'root' })
+export class UserService {
+ private http = inject(HttpClient);
+
+ getUsers() {
+ // On server: Response is cached in TransferState
+ // On client: Cached response is used (no duplicate request)
+ return this.http.get('/api/users');
+ }
+}
+```
+
+> [!NOTE]
+> The interceptor only works with GET requests. POST, PUT, DELETE, and PATCH requests are not cached.
+
+## Additional Resources
+
+- [Angular SSR Official Guide](https://angular.dev/guide/ssr)
+- [Angular Hydration Documentation](https://angular.dev/guide/hydration)
+- [PM2 Process Manager](https://pm2.keymetrics.io/)
+
+## Summary
+
+The ABP Angular SSR schematic provides:
+- ✅ Automatic SSR setup with necessary dependencies
+- ✅ Server-side authentication with OpenID Connect
+- ✅ Multiple render modes (Server, Prerender, Client, Hybrid)
+- ✅ Hydration support for better performance
+
+Configure render modes based on your needs, handle platform differences properly, and use environment variables for deployment configuration.
diff --git a/docs/en/framework/ui/common/leptonx-css-variables.md b/docs/en/framework/ui/common/leptonx-css-variables.md
new file mode 100644
index 0000000000..938bc92304
--- /dev/null
+++ b/docs/en/framework/ui/common/leptonx-css-variables.md
@@ -0,0 +1,126 @@
+```json
+//[doc-seo]
+{
+ "Description": "Explore LeptonX CSS Variables to customize theming with ease, controlling colors, spacing, and styles for a cohesive application design."
+}
+```
+
+# LeptonX CSS Variables Documentation
+
+LeptonX uses CSS custom properties (variables) prefixed with `--lpx-*` to provide a flexible theming system. These variables control colors, spacing, shadows, and component-specific styles throughout the application.
+
+## Brand & Semantic Colors
+
+| Variable | Description |
+|----------|-------------|
+| `--lpx-brand` | Brand-specific accent color |
+| `--lpx-brand-text` | Text color used on brand-colored backgrounds |
+
+## Base Colors
+
+| Variable | Description |
+|----------|-------------|
+| `--lpx-light` | Light shade for subtle backgrounds or text |
+| `--lpx-dark` | Dark shade for contrasting elements |
+
+## Layout & Surface Colors
+
+| Variable | Description |
+|----------|-------------|
+| `--lpx-content-bg` | Main content area background color |
+| `--lpx-content-text` | Default text color for content areas |
+| `--lpx-card-bg` | Card component background color |
+| `--lpx-card-title-text-color` | Card title text color |
+| `--lpx-border-color` | Default border color for dividers and outlines |
+| `--lpx-shadow` | Box shadow definition for elevated elements |
+
+## Navigation
+
+| Variable | Description |
+|----------|-------------|
+| `--lpx-navbar-color` | Navbar background color |
+| `--lpx-navbar-text-color` | Navbar default text/icon color |
+| `--lpx-navbar-active-text-color` | Navbar active/hover text color |
+| `--lpx-navbar-active-bg-color` | Navbar active item background color |
+
+## Utility
+
+| Variable | Description |
+|----------|-------------|
+| `--lpx-radius` | Global border-radius value for rounded corners |
+
+## Global Override
+
+Applies to all themes and pages:
+
+```css
+:root {
+ /* Brand & Semantic */
+ --lpx-brand: #f72585;
+
+ /* Base Colors */
+ --lpx-light: #f5f7fb;
+ --lpx-dark: #0b0f19;
+
+ /* Layout & Surface */
+ --lpx-content-bg: #101018;
+ --lpx-content-text: #cfd6e4;
+ --lpx-card-bg: #151a2b;
+ --lpx-card-title-text-color: #ffffff;
+ --lpx-border-color: #242836;
+ --lpx-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
+
+ /* Navigation */
+ --lpx-navbar-color: #0d1020;
+ --lpx-navbar-text-color: #aab2c8;
+ --lpx-navbar-active-text-color: #ffffff;
+ --lpx-navbar-active-bg-color: rgba(247, 37, 133, 0.15);
+
+ /* Utility */
+ --lpx-radius: 10px;
+}
+
+```
+
+## Theme-Scoped Override
+
+Applies only when a specific theme class is active (e.g., `.lpx-theme-dark` on `` or ``):
+
+```css
+:root .lpx-theme-dark {
+ /* Brand & Semantic */
+ --lpx-brand: #4dd0e1;
+
+ /* Base Colors */
+ --lpx-light: #e0f7fa;
+ --lpx-dark: #020617;
+
+ /* Layout & Surface */
+ --lpx-content-bg: #0b1118;
+ --lpx-content-text: #c7d0e0;
+ --lpx-card-bg: #111a24;
+ --lpx-card-title-text-color: #e6f1ff;
+ --lpx-border-color: #1e2a3a;
+ --lpx-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
+
+ /* Navigation */
+ --lpx-navbar-color: #0f1a22;
+ --lpx-navbar-text-color: #9fb3c8;
+ --lpx-navbar-active-text-color: #ffffff;
+ --lpx-navbar-active-bg-color: rgba(77, 208, 225, 0.18);
+
+ /* Utility */
+ --lpx-radius: 12px;
+}
+```
+
+## Component/Page-Specific Override
+
+For targeted customizations that should only affect a specific section:
+
+```css
+.my-custom-page {
+ --lpx-brand: #e91e63;
+ --lpx-card-bg: #1a1a2e;
+}
+```
\ No newline at end of file
diff --git a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png
index a268e4cde5..3b1a4abe68 100644
Binary files a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png and b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png
index fd811125c7..5f2a9b7938 100644
Binary files a/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png and b/docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png
index da7a5f22ea..cb85c3a2b8 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png
index f7f088804b..6bbf3fb22a 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png
new file mode 100644
index 0000000000..74cd5066bb
Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png
index 910b522dd3..e41580ad1e 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png
index 4b8965456c..ed52bac62c 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png
index 3dedb0aba0..455a6bd574 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png
new file mode 100644
index 0000000000..705fee9325
Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png
index e98c96ac07..9b9210b269 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png
index f883555e4a..04e1af7ebe 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png
index 2bc29805ee..3c06ae001d 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png
index 3dff0567fd..40325a5358 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-web-site.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png
index 4a6fbf2d40..27e39f3776 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-microservice.png differ
diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png
index 553f0ca395..53692d03de 100644
Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme-microservice.png differ
diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md
index d57c652b8e..ee7e31bf85 100644
--- a/docs/en/get-started/microservice.md
+++ b/docs/en/get-started/microservice.md
@@ -94,6 +94,25 @@ Once you select the desired modules, click the *Next* button for the *UI Theme*
LeptonX is the suggested UI theme that is proper for production usage. Select one of the themes and configure the additional options if you want.
+Click the Next button to see *Language Selection* selection:
+
+
+
+In this step, you can choose which languages your application will support.
+
+* Default Language: Select the main language for your app.
+
+* Localizable User Interface: Turn this on to support multiple languages.
+
+* Available Languages: Check the languages you want to include.
+
+* Click Add Custom Language if you want to add a language that is not listed.
+
+Click the Next button to see *.NET Aspire* configuration selection:
+
+
+
+In this step, you can enable or disable the .NET Aspire integration for your solution. If you enable it, the solution will be pre-configured to work with .NET Aspire for easier microservice development and deployment. See the [Aspire Integration](../solution-templates/microservice/aspire-integration.md) document for more information about this feature.
Click the Next button to see *Additional Options* selection:
diff --git a/docs/en/images/add-passkey.png b/docs/en/images/add-passkey.png
new file mode 100644
index 0000000000..345cdd644d
Binary files /dev/null and b/docs/en/images/add-passkey.png differ
diff --git a/docs/en/images/ai-management-widget.png b/docs/en/images/ai-management-widget.png
new file mode 100644
index 0000000000..2f396f3e5a
Binary files /dev/null and b/docs/en/images/ai-management-widget.png differ
diff --git a/docs/en/images/my-passkey.png b/docs/en/images/my-passkey.png
new file mode 100644
index 0000000000..2b40045165
Binary files /dev/null and b/docs/en/images/my-passkey.png differ
diff --git a/docs/en/images/passkey-login.png b/docs/en/images/passkey-login.png
new file mode 100644
index 0000000000..dabf324e7f
Binary files /dev/null and b/docs/en/images/passkey-login.png differ
diff --git a/docs/en/images/passkey-login2.png b/docs/en/images/passkey-login2.png
new file mode 100644
index 0000000000..349d8a2dca
Binary files /dev/null and b/docs/en/images/passkey-login2.png differ
diff --git a/docs/en/images/passkey-setting.png b/docs/en/images/passkey-setting.png
new file mode 100644
index 0000000000..6211e91d9c
Binary files /dev/null and b/docs/en/images/passkey-setting.png differ
diff --git a/docs/en/images/pen-test-alert-list-10.1.png b/docs/en/images/pen-test-alert-list-10.1.png
new file mode 100644
index 0000000000..3a9d10feff
Binary files /dev/null and b/docs/en/images/pen-test-alert-list-10.1.png differ
diff --git a/docs/en/modules/account-pro.md b/docs/en/modules/account-pro.md
index 81e1177e33..7944c7448a 100644
--- a/docs/en/modules/account-pro.md
+++ b/docs/en/modules/account-pro.md
@@ -424,3 +424,4 @@ This module doesn't define any additional distributed event. See the [standard d
* [Linked Accounts](./account/linkedaccounts.md)
* [Session Management](./account/session-management.md)
* [Idle Session Timeout](./account/idle-session-timeout.md)
+* [Web Authentication API (WebAuthn) passkeys](./account/passkey.md)
diff --git a/docs/en/modules/account/impersonation.md b/docs/en/modules/account/impersonation.md
index c6fdae0ebf..3335ea50e5 100644
--- a/docs/en/modules/account/impersonation.md
+++ b/docs/en/modules/account/impersonation.md
@@ -7,7 +7,7 @@
# Tenant impersonation & User impersonation
-User impersonation allows you to temporarily sign in as a different user in your tenant's users. This article introduces how to enable impersonation in ABP. Impersonation is enabled by defautl in ABP v5.0 and above.
+User impersonation allows you to temporarily sign in as a different user in your tenant's users. This article introduces how to enable impersonation in ABP. Impersonation is enabled by default in ABP v5.0 and above.
## Introduction
diff --git a/docs/en/modules/account/passkey.md b/docs/en/modules/account/passkey.md
new file mode 100644
index 0000000000..b8f00ee576
--- /dev/null
+++ b/docs/en/modules/account/passkey.md
@@ -0,0 +1,63 @@
+# Web Authentication API (WebAuthn) passkeys
+
+The `Web Authentication API (WebAuthn) passkeys` feature allows users to authenticate using passkeys, which are more secure and user-friendly alternatives to traditional passwords. Passkeys leverage public key cryptography to provide strong authentication without the need for users to remember complex passwords.
+
+## Enabling Passkeys
+
+You can enable/disable the `Web Authentication API (WebAuthn) passkeys` feature in the `Setting > Account > Passkeys` page. Also, there is an option to allow how many passkeys a user can register:
+
+
+
+## Manage Passkeys
+
+You can add/rename/delete your passkeys in the `Account/Manage` page:
+
+
+
+Click the `Add Passkey` button to register a new passkey. You will be prompted to use your device's built-in biometric authentication (such as fingerprint or facial recognition) or an external security key to complete the registration process:
+
+
+
+## Using Passkey for Login
+
+Once you enable the passkey feature and register at least one passkey, you can use it to log in to your account. On the login page, select the `Passkey login` option and follow the prompts to authenticate using your registered passkey:
+
+
+
+
+
+## Configure passkey options
+
+ASP.NET Core Identity provides various options to configure passkey behavior through the `IdentityPasskeyOptions` class, which include:
+
+- **AuthenticatorTimeout**: Gets or sets the time that the browser should wait for the authenticator to provide a passkey as a TimeSpan. This option applies to both creating a new passkey and requesting an existing passkey. This option is treated as a hint to the browser, and the browser may ignore the option. The default value is 5 minutes.
+- **ChallengeSize**: Gets or sets the size of the challenge in bytes sent to the client during attestation and assertion. This option applies to both creating a new passkey and requesting an existing passkey. The default value is 32 bytes.
+- **ServerDomain**: Gets or sets the effective Relying Party ID (domain) of the server. This should be unique and will be used as the identity for the server. This option applies to both creating a new passkey and requesting an existing passkey. If null, which is the default value, the server's origin is used. For more information, see Relying Party Identifier RP ID.
+
+**Example configuration:**
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.ServerDomain = "abp.io";
+ options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
+ options.ChallengeSize = 64;
+});
+```
+
+For a complete list of configuration options, see [IdentityPasskeyOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.identitypasskeyoptions). For the most up-to-date browser defaults, see the [W3C WebAuthn specification](https://www.w3.org/TR/webauthn-3/).
+
+## HTTPS requirement
+
+All passkey operations require HTTPS. The implementation stores authentication data in encrypted and signed cookies that could be intercepted over unencrypted connections.
+
+## Browser Support
+
+Passkeys are supported in most modern browsers, including: Chrome, Edge, Firefox, and Safari. Ensure that you are using the latest version of your browser to take advantage of passkey functionality.
+
+## Additional resources
+
+For more information on WebAuthn and passkeys, refer to the following resources:
+
+- [Enable Web Authentication API (WebAuthn) passkeys](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/passkeys)
+- [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API)
diff --git a/docs/en/modules/ai-management/index.md b/docs/en/modules/ai-management/index.md
index b9492d5a4f..e50253c22c 100644
--- a/docs/en/modules/ai-management/index.md
+++ b/docs/en/modules/ai-management/index.md
@@ -28,7 +28,6 @@ abp add-module Volo.AIManagement
Open ABP Studio, navigate to your solution explorer, **Right Click** on the project and select **Import Module**. Choose `Volo.AIManagement` from `NuGet` tab and check the "Install this Module" checkbox. Click the "OK" button to install the module.
-
## 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.
@@ -44,7 +43,7 @@ AI Management module packages are designed for various usage scenarios. Packages
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`)
+ * **Workspaces**: Workspace management page. (`AIManagement.Workspaces`)
`AIManagementMenus` class has the constants for the menu item names.
@@ -87,21 +86,21 @@ Workspaces are the core concept of the AI Management module. A workspace represe
When creating or managing a workspace, you can configure the following properties:
-| Property | Required | Description |
-|----------|----------|-------------|
-| `Name` | Yes | Unique workspace identifier (cannot contain spaces) |
-| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") |
-| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") |
-| `ApiKey` | No | API authentication key (required by some providers) |
-| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) |
-| `SystemPrompt` | No | Default system prompt for all conversations |
-| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) |
-| `Description` | No | Workspace description |
-| `IsActive` | No | Enable/disable the workspace (default: true) |
-| `ApplicationName` | No | Associate workspace with specific application |
-| `RequiredPermissionName` | No | Permission required to use this workspace |
-| `IsSystem` | No | Whether it's a system workspace (read-only) |
-| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings |
+| Property | Required | Description |
+| ----------------------------- | -------- | -------------------------------------------------------------- |
+| `Name` | Yes | Unique workspace identifier (cannot contain spaces) |
+| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") |
+| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") |
+| `ApiKey` | No | API authentication key (required by some providers) |
+| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) |
+| `SystemPrompt` | No | Default system prompt for all conversations |
+| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) |
+| `Description` | No | Workspace description |
+| `IsActive` | No | Enable/disable the workspace (default: true) |
+| `ApplicationName` | No | Associate workspace with specific application |
+| `RequiredPermissionName` | No | Permission required to use this workspace |
+| `IsSystem` | No | Whether it's a system workspace (read-only) |
+| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings |
**\*Not required for system workspaces**
@@ -165,7 +164,7 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe
workspace.ApiKey = "your-api-key";
workspace.SystemPrompt = "You are a helpful customer support assistant.";
-
+
await _workspaceRepository.InsertAsync(workspace);
}
```
@@ -180,13 +179,12 @@ public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDe
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 | 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 |
### Workspace-Level Permissions
@@ -203,6 +201,7 @@ workspace.RequiredPermissionName = MyAppPermissions.AccessPremiumWorkspaces;
```
When a workspace has a required permission:
+
* Only authorized users with that permission can access the workspace endpoints
* Users without the permission will receive an authorization error
@@ -220,6 +219,7 @@ The AI Management module is designed to support various usage patterns, from sim
In this scenario, you only use the ABP Framework's AI features directly. You configure AI providers (like OpenAI) in your code and don't need any database or management UI.
**Required Packages:**
+
- `Volo.Abp.AI`
- Any Microsoft AI extensions (e.g., `Microsoft.Extensions.AI.OpenAI`)
@@ -257,7 +257,7 @@ public class MyService
{
_chatClient = chatClient;
}
-
+
public async Task GetResponseAsync(string prompt)
{
var response = await _chatClient.CompleteAsync(prompt);
@@ -277,10 +277,12 @@ In this scenario, you install the AI Management module with its database layer,
**Required Packages:**
**Minimum (backend only):**
+
- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`)
- `Volo.AIManagement.OpenAI` (or another AI provider package)
**Full installation (with UI and API):**
+
- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`)
- `Volo.AIManagement.Application`
- `Volo.AIManagement.HttpApi`
@@ -315,6 +317,7 @@ public class YourModule : AbpModule
**Option 2 - Dynamic Workspace (UI-based):**
No code configuration needed. Define workspaces through:
+
- The AI Management UI (navigate to AI Management > Workspaces)
- Data seeding in your `DataSeeder` class
@@ -339,6 +342,7 @@ public class MyService
In this scenario, your application communicates with a separate AI Management microservice that manages configurations and communicates with AI providers on your behalf. The AI Management service handles all AI provider interactions.
**Required Packages:**
+
- `Volo.AIManagement.Client.HttpApi.Client`
**Configuration:**
@@ -399,7 +403,7 @@ public class MyService
var response = await _chatService.ChatCompletionsAsync(workspaceName, request);
return response.Content;
}
-
+
// For streaming responses
public async IAsyncEnumerable StreamAIResponseAsync(string workspaceName, string prompt)
{
@@ -426,6 +430,7 @@ public class MyService
This scenario builds on Scenario 3, but your application exposes its own HTTP endpoints that other applications can call. Your application then forwards these requests to the AI Management service.
**Required Packages:**
+
- `Volo.AIManagement.Client.HttpApi.Client` (to communicate with AI Management service)
- `Volo.AIManagement.Client.Application` (application services)
- `Volo.AIManagement.Client.HttpApi` (to expose HTTP endpoints)
@@ -438,24 +443,170 @@ Same as Scenario 3, configure the remote AI Management service in `appsettings.j
**Usage:**
Once configured, other applications can call your application's endpoints:
+
- `POST /api/ai-management-client/chat-completion` for chat completions
- `POST /api/ai-management-client/stream-chat-completion` for streaming responses
Your application acts as a proxy, forwarding these requests to the AI Management microservice.
-## Comparison Table
+### Comparison Table
+
+| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case |
+| ------------------------- | ----------------- | -------------- | -------------- | ----------- | ----------------------------------------- |
+| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed |
+| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI |
+| **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 |
+
+
+
+## Client Usage (MVC UI)
+
+AI Management uses different packages depending on the usage scenario:
+
+- **`Volo.AIManagement.*` packages**: These contain the core AI functionality and are used when your application hosts and manages its own AI operations. These packages don't expose any application service and endpoints to be consumed by default.
+
+- **`Volo.AIManagement.Client.*` packages**: These are designed for applications that need to consume AI services from a remote application. They provide both server and client side of remote access to the AI services.
+
+**List of packages:**
+- `Volo.AIManagement.Client.Application`
+- `Volo.AIManagement.Client.Application.Contracts`
+- `Volo.AIManagement.Client.HttpApi`
+- `Volo.AIManagement.Client.HttpApi.Client`
+- `Volo.AIManagement.Client.Web`
+
+### The Chat Widget
+The `Volo.AIManagement.Client.Web` package provides a chat widget to allow you to easily integrate a chat interface into your application that uses a specific AI workspace named `ChatClientChatViewComponent`.
+
+#### Basic Usage
+You can invoke the `ChatClientChatViewComponent` Widget in your razor page with the following code:
+
+```csharp
+@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
+{
+ WorkspaceName = "mylama",
+})
+```
+
+
+
+#### Properties
+You can customize the chat widget with the following properties:
+- `WorkspaceName`: The name of the workspace to use.
+- `ComponentId`: Unique identifier for accessing the component via JavaScript API (stored in abp.chatComponents).
+- `ConversationId`: The unique identifier for persisting and retrieving chat history from client-side storage.
+- `Title`: The title of the chat widget.
+- `ShowStreamCheckbox`: Whether to show the stream checkbox. Allows user to toggle streaming on and off. Default is `false`.
+- `UseStreaming`: Default streaming behavior. Can be overridden by user when `ShowStreamCheckbox` is true.
+
+```csharp
+@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
+{
+ WorkspaceName = "mylama",
+ ComponentId = "mylama-chat",
+ ConversationId = "mylama-conversation-" + @CurrentUser.Id,
+ Title = "My Custom Title",
+ ShowStreamCheckbox = true,
+ UseStreaming = true
+})
+```
+
+#### Using the Conversation Id
+You can use the `ConversationId` property to specify the id of the conversation to use. When the Conversation Id is provided, the chat will be stored at the client side and will be retrieved when the user revisits the page that contains the chat widget. If it's not provided or provided as **null**, the chat will be temporary and will not be saved, it'll be lost when the component lifetime ends.
+
+```csharp
+@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
+{
+ WorkspaceName = "mylama",
+ ConversationId = "my-support-conversation-" + @CurrentUser.Id
+})
+```
+
+#### JavaScript API
+The chat components are initialized automatically when the ViewComponent is rendered in the page. All the initialized components in the page are stored in the `abp.chatComponents` object. You can retrieve a specific component by its `ComponentId` which is defined while invoking the ViewComponent.
+
+```csharp
+@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
+{
+ WorkspaceName = "mylama",
+ ComponentId = "mylama-chat"
+})
+```
+
+You can then use the JavaScript API to interact with the component.
+```js
+var chatComponent = abp.chatComponents.get('mylama-chat');
+```
+
+Once you have the component, you can use the following functions to interact with it:
+
+```js
+// Switch to a different conversation
+chatComponent.switchConversation(conversationId);
+
+// Create a new conversation with a specific model
+chatComponent.createConversation(conversationId, modelName);
+
+// Clear the current conversation history
+chatComponent.clearConversation();
+
+// Get the current conversation ID (returns null for ephemeral conversations)
+var currentId = chatComponent.getCurrentConversationId();
+
+// Initialize with a specific conversation ID
+chatComponent.initialize(conversationId);
+
+// Send a message programmatically
+chatComponent.sendMessage();
+
+// Listen to events
+chatComponent.on('messageSent', function(data) {
+ console.log('Message sent:', data.message);
+ console.log('Conversation ID:', data.conversationId);
+ console.log('Is first message:', data.isFirstMessage);
+});
+
+chatComponent.on('messageReceived', function(data) {
+ console.log('AI response:', data.message);
+ console.log('Conversation ID:', data.conversationId);
+ console.log('Is streaming:', data.isStreaming);
+});
+
+chatComponent.on('streamStarted', function(data) {
+ console.log('Streaming started for conversation:', data.conversationId);
+});
-| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case |
-|----------|------------------|----------------|-------------|-------------|----------|
-| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed |
-| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI |
-| **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 |
+// Remove event listeners
+chatComponent.off('messageSent', callbackFunction);
+```
+
+**Best-practices:**
+- Don't try to access the component at the page load time, it's not guaranteed to be initialized yet. Get the component whenever you need it to make sure it's **initialized** and the **latest state** is applied.
+
+❌ Don't do this
+```js
+(function(){
+ var chatComponent = abp.chatComponents.get('mylama-chat');
+ $('#my-button').on('click', function() {
+ chatComponent.clearConversation();
+ });
+});
+```
+✅ Do this
+```js
+(function(){
+ $('#my-button').on('click', function() {
+ var chatComponent = abp.chatComponents.get('mylama-chat');
+ chatComponent.clearConversation();
+ });
+});
+```
## Using Dynamic Workspace Configurations for custom requirements
+
The AI Management module allows you to access only configuration of a workspace without resolving pre-constructed chat client. This is useful when you want to use a workspace for your own purposes and you don't need to use the chat client.
-The `IWorkspaceConfigurationStore` service is used to access the configuration of a workspace. It has multiple implementaations according to the usage scenario.
+The `IWorkspaceConfigurationStore` service is used to access the configuration of a workspace. It has multiple implementations according to the usage scenario.
```csharp
public class MyService
@@ -470,7 +621,7 @@ public class MyService
{
// Get the configuration of the workspace that can be managed dynamically.
var configuration = await _workspaceConfigurationStore.GetAsync("MyWorkspace");
-
+
// Do something with the configuration
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatClient(
@@ -550,24 +701,23 @@ public override void ConfigureServices(ServiceConfigurationContext context)
> [!TIP]
> For production scenarios, you may want to add validation for the factory configuration.
-
### Available Configuration Properties
The `ChatClientCreationConfiguration` object provides the following properties from the database:
-| Property | Type | Description |
-|----------|------|-------------|
-| `Name` | string | Workspace name |
-| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") |
-| `ApiKey` | string? | API key for authentication |
-| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") |
-| `SystemPrompt` | string? | Default system prompt for the workspace |
-| `Temperature` | float? | Temperature setting for response generation |
-| `ApiBaseUrl` | string? | Custom API endpoint URL |
-| `Description` | string? | Workspace description |
-| `IsActive` | bool | Whether the workspace is active |
-| `IsSystem` | bool | Whether it's a system workspace |
-| `RequiredPermissionName` | string? | Permission required to use this workspace |
+| Property | Type | Description |
+| ------------------------ | ------- | ------------------------------------------- |
+| `Name` | string | Workspace name |
+| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") |
+| `ApiKey` | string? | API key for authentication |
+| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") |
+| `SystemPrompt` | string? | Default system prompt for the workspace |
+| `Temperature` | float? | Temperature setting for response generation |
+| `ApiBaseUrl` | string? | Custom API endpoint URL |
+| `Description` | string? | Workspace description |
+| `IsActive` | bool | Whether the workspace is active |
+| `IsSystem` | bool | Whether it's a system workspace |
+| `RequiredPermissionName` | string? | Permission required to use this workspace |
### Example: Azure OpenAI Factory
@@ -604,6 +754,7 @@ public class AzureOpenAIChatClientFactory : IChatClientFactory, ITransientDepend
After implementing and registering your factory:
1. **Through UI**: Navigate to the AI Management workspaces page and create a new workspace:
+
- Select your provider name (e.g., "Ollama", "AzureOpenAI")
- Configure the API settings
- Set the model name
@@ -672,6 +823,7 @@ WorkspaceConfiguration:{ApplicationName}:{WorkspaceName}
```
### HttpApi Client Layer
+
- `IntegrationWorkspaceConfigurationStore`: Integration service for remote workspace configuration retrieval. Implements `IWorkspaceConfigurationStore` interface.
The cache is automatically invalidated when workspaces are created, updated, or deleted.
@@ -680,4 +832,5 @@ The cache is automatically invalidated when workspaces are created, updated, or
- [Artificial Intelligence Infrastructure](../../framework/infrastructure/artificial-intelligence/index.md): Learn about the underlying AI workspace infrastructure
- [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/): Microsoft's unified AI abstractions
+- [Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview): Microsoft's Agent Framework
- [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/): Microsoft's Semantic Kernel integration
diff --git a/docs/en/modules/feature-management.md b/docs/en/modules/feature-management.md
index 300b232f93..e80098a04d 100644
--- a/docs/en/modules/feature-management.md
+++ b/docs/en/modules/feature-management.md
@@ -72,9 +72,10 @@ namespace Demo
## Feature Management Providers
-Features Management Module is extensible, just like the [features system](../framework/infrastructure/features.md). You can extend it by defining feature management providers. There are 3 pre-built feature management providers registered it the following order:
+Features Management Module is extensible, just like the [features system](../framework/infrastructure/features.md). You can extend it by defining feature management providers. There are 4 pre-built feature management providers registered in the following order:
* `DefaultValueFeatureManagementProvider`: Gets the value from the default value of the feature definition. It can not set the default value since default values are hard-coded on the feature definition.
+* `ConfigurationFeatureManagementProvider`: Gets the value from the [IConfiguration service](../framework/fundamentals/configuration.md).
* `EditionFeatureManagementProvider`: Gets or sets the feature values for an edition. Edition is a group of features assigned to tenants. Edition system has not implemented by the Tenant Management module. You can implement it yourself or purchase the ABP [SaaS Module](https://abp.io/modules/Volo.Saas) which implements it and also provides more SaaS features, like subscription and payment.
* `TenantFeatureManagementProvider`: Gets or sets the features values for tenants.
diff --git a/docs/en/modules/saas.md b/docs/en/modules/saas.md
index cf88cf716a..7d78a4e9df 100644
--- a/docs/en/modules/saas.md
+++ b/docs/en/modules/saas.md
@@ -40,7 +40,7 @@ Firstly, Payment module must be configured properly:
```bash
abp add-module Volo.Payment
```
- _Or you can install via using ABP Suite._
+ _Or you can install via using ABP Studio._
- Configure Saas module to use Payment.
diff --git a/docs/en/others/penetration-test-report.md b/docs/en/others/penetration-test-report.md
index 13086fb2a0..cc80d39b8e 100644
--- a/docs/en/others/penetration-test-report.md
+++ b/docs/en/others/penetration-test-report.md
@@ -7,7 +7,7 @@
# ABP Penetration Test Report
-The ABP Commercial MVC `v9.1.0` application template has been tested against security vulnerabilities by the [OWASP ZAP v2.14.0](https://www.zaproxy.org/) tool. The demo web application was started on the `https://localhost:44349` address. The below alerts have been reported by the pentest tool. These alerts are sorted by the risk level as high, medium, and low. The informational alerts are not mentioned in this document.
+The ABP Commercial MVC `v10.0.1` application template has been tested against security vulnerabilities by the [OWASP ZAP v2.14.0](https://www.zaproxy.org/) tool. The demo web application was started on the `https://localhost:44349` address. The below alerts have been reported by the pentest tool. These alerts are sorted by the risk level as high, medium, and low. The informational alerts are not mentioned in this document.
Many of these alerts are **false-positive**, meaning the vulnerability scanner detected these issues, but they are not exploitable. It's clearly explained for each false-positive alert why this alert is a false-positive.
@@ -17,53 +17,75 @@ In the next sections, you will find the affected URLs, attack parameters (reques
There are high _(red flag)_, medium _(orange flag)_, low _(yellow flag)_, and informational _(blue flag)_ alerts.
-
+
-> The informational alerts are not mentioned in this document. These alerts are not raising any risks on your application and they are optional.
+> The informational alerts are not mentioned in this document. These alerts don't raise any risks for your application and they are optional.
-### Spring4Shell [Risk: High] - False Positive
+### Cross Site Scripting (Reflected) [Risk: High] - Positive
-- *[POST] - https://localhost:44349/Account/ForgotPassword* (attack: **class.module.classLoader.DefaultAssertionStatus=nonsense**)
-- *[POST] - https://localhost:44349/Account/Login* (attack: **class.module.classLoader.DefaultAssertionStatus=nonsense**)
-- *[POST] - https://localhost:44349/Account/Login?ReturnUrl=%2FSettingManagement* (attack: **class.module.classLoader.DefaultAssertionStatus=nonsense**)
+- *[GET] - https://localhost:44349/Identity/OrganizationUnits/AddMemberModal?title=SelectAUser&organizationUnitId=...&OrganizationUnitName=%3C%2Fh5%3E%3CscrIpt%3Ealert%281%29%3B%3C%2FscRipt%3E%3Ch5%3E*
+- *[GET] - https://localhost:44349/Identity/OrganizationUnits/AddRoleModal?organizationUnitId=...&OrganizationUnitName=%3C%2Fh5%3E%3CscrIpt%3Ealert%281%29%3B%3C%2FscRipt%3E%3Ch5%3E*
+- *[GET] - https://localhost:44349/Saas/Host/Tenants/ImpersonateTenantModal?tenantId=...&tenantName=%3C%2Fh5%3E%3CscrIpt%3Ealert%281%29%3B%3C%2FscRipt%3E%3Ch5%3E*
**Description**:
-The application appears to be vulnerable to CVE-2022-22965 (otherwise known as Spring4Shell) - remote code execution (RCE) via data binding.
+Cross-site Scripting (XSS) is an attack technique that involves echoing attacker-supplied code into a user's browser instance.
**Explanation**:
-ABP Framework is built on top of ASP.NET Core and does not use the Spring Framework. This application does not rely on Java-based technologies, making it immune to vulnerabilities like Spring4Shell. The detection is a false positive as there are no Spring dependencies in the project.
+This is a **Positive** alert. The application reflects the `OrganizationUnitName` and `tenantName` parameters without proper encoding in the modal headers, allowing for the execution of arbitrary JavaScript. We have created an **internal issue** to track this vulnerability, and it will be fixed in the next release.
-### Absence of Anti-CSRF Tokens [Risk: Medium] — False Positive
+### PII Disclosure [Risk: High] - False Positive
-* *[GET] - https://localhost:44349/Account/LinkUsers/LinkUsersModal?returnUrl=/SettingManagement*
-* *[GET] — https://localhost:44349/Account/Manage* (same URL with different query parameters)
-* *[GET] - https://localhost:44349/HostDashboard*
-* *[GET] - https://localhost:44349/SettingManagement?handler=RenderView&id=Volo.Abp.Account* (other several URLs)
+- *[GET] - https://localhost:44349/* (Evidence: 639002492030480000)
+- *[GET] - https://localhost:44349/?page=...*
-**Description**:
+**Description**:
-No Anti-CSRF tokens were found in an HTML submission form.
-A cross-site request forgery is an attack that involves forcing a victim to send an HTTP request to a target destination without their knowledge or intent in order to perform an action as the victim. The underlying cause is application functionality using predictable URL/form actions in a repeatable way. The nature of the attack is that CSRF exploits the trust that a website has for a user. By contrast, cross-site scripting (XSS) exploits the trust that a user has in a website. Like XSS, CSRF attacks are not necessarily cross-site, but they can be. Cross-site request forgery is also known as CSRF, XSRF, one-click attack, session riding, confused deputy, and sea surf.
+The response contains Personally Identifiable Information, such as CC number, SSN and similar sensitive data.
-**Explanation:**
+**Explanation**:
-This is a **false-positive** alert because ABP provides the Anti-CSRF token via a cookie as seen on the following screenshot:
+This is a **false-positive** alert. The detected numbers (e.g., `639002492030480000`) are cache-busting timestamps (`_v` parameter) generated by the framework for static assets. They coincidentally match the pattern of Credit Card numbers (pattern matching) but are not sensitive data.
-
+### Path Traversal [Risk: High] - False Positive
-### Application Error Disclosure [Risk: Medium] - False Positive
+- *[GET] - https://localhost:44349/Account/Login?returnUrl=Login*
+- *[GET] - https://localhost:44349/api/account/security-logs?action=\security-logs*
-- *[GET] — https://localhost:44349/AuditLogs*
+**Description**:
-**Description**:
+The Path Traversal attack technique allows an attacker access to files, directories, and commands that potentially reside outside the web document root directory.
+
+**Explanation**:
+
+This is a **false-positive** alert. ABP Framework automatically validates `returnUrl` parameters and ensures they are local to the application or within a whitelist. The application does not return file contents based on these parameters.
+
+### SQL Injection [Risk: High] - False Positive
+
+- *[GET] - https://localhost:44349/AbpPermissionManagement/PermissionManagementModal?providerKey=AbpSolution16711_Swagger+AND+1%3D1+--+*
+- *[GET] - https://localhost:44349/Account/Manage?CurrentPassword=ZAP%27+AND+%271%27%3D%271%27+--+*
+
+**Description**:
+
+SQL injection may be possible.
-This page contains an error/warning message that may disclose sensitive information like the location of the file that produced the unhandled exception. This information can be used to launch further attacks against the web application. The alert could be a false positive if the error message is found inside a documentation page.
+**Explanation**:
+
+This is a **false-positive** alert. ABP Framework uses Entity Framework Core, which inherently uses parameterized queries, preventing standard SQL injection attacks. Manual verification showed that injecting SQL syntax into parameters like `providerKey` results in the input being treated as a literal string (resulting in no match or default behavior) rather than altering the query structure.
+
+### SQL Injection - SQLite [Risk: High] - False Positive
+
+- *[POST] - https://localhost:44349/Account/ForgotPassword?returnUrl=%2FAccount%2FManage* (Attack: `case randomblob(100000) ...`)
+- *[POST] - https://localhost:44349/FeatureManagement/FeatureManagementModal*
+
+**Description**:
+
+SQL injection may be possible.
**Explanation**:
-There are only one URL that is reported as exposing error messages. This is a **false-positive** alert. The [Audit Logging Module](../modules/audit-logging.md), shows request & response details and exception information, these are not sensitive information and only can be seen by the users whose related permissions are granted.
+This is a **false-positive** alert. Similar to the standard SQL Injection alert, the application uses parameterized queries. The detected delays are likely due to application processing variations or network latency rather than successful SQL injection.
### Content Security Policy (CSP) Header Not Set [Risk: Medium] — Positive (Fixed)
@@ -238,23 +260,7 @@ A cookie has been set with its `SameSite` attribute set to `none`, which means t
Ensure that the `SameSite` attribute is set to either `lax` or ideally `strict` for all cookies. We discussed setting the **SameSite** attribute to `strict` in the following issue [github.com/abpframework/abp/issues/14215](https://github.com/abpframework/abp/issues/14215) and decided to leave this change to the final developer.
-### Information Disclosure - Debug Error Messages [Risk: Low] — False Positive
-
-* *[GET] - https://localhost:44349/AuditLogs*
-
-**Description:**
-
-The response appeared to contain common error messages returned by platforms such as ASP.NET, and Web-servers such as IIS and Apache. You can configure the list of common debug messages.
-
-**Solution:**
-
-Disable debugging messages before pushing them to production.
-
-**Explanation:**
-
-The response of the endpoints above return localization texts which are not real error messages. As there is no real error in the backend side, this vulnerability is a **false-positive** alert.
-
### Strict-Transport-Security Header Not Set [Risk: Low] - False Positive
diff --git a/docs/en/solution-templates/microservice/aspire-integration.md b/docs/en/solution-templates/microservice/aspire-integration.md
new file mode 100644
index 0000000000..2da2706b34
--- /dev/null
+++ b/docs/en/solution-templates/microservice/aspire-integration.md
@@ -0,0 +1,372 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to integrate .NET Aspire into your ABP-based solution for streamlined development, orchestration, and observability of distributed applications."
+}
+```
+
+# .NET Aspire Integration
+
+````json
+//[doc-nav]
+{
+ "Next": {
+ "Name": "Miscellaneous guides in the Microservice solution",
+ "Path": "solution-templates/microservice/guides"
+ }
+}
+````
+
+> You must have an ABP Business or a higher license to be able to create a microservice solution.
+
+## .NET Aspire Overview
+
+[Aspire](https://aspire.dev/get-started/what-is-aspire/) streamlines building, running, debugging, and deploying distributed apps. Picture your app as a set of services, databases, and frontends—when they’re deployed, they all work together seamlessly, but every time you develop them they need to be individually started and connected. With Aspire, you get a unified toolchain that eliminates complex configs and makes local debugging effortless. Instantly launch and debug your entire app with a single command. Ready to deploy? Aspire lets you publish anywhere—Kubernetes, the cloud, or your own servers. It’s also fully extensible, so you can integrate your favorite tools and services with ease. It provides:
+
+- **Orchestration**: A code-first approach to defining and running distributed applications, managing dependencies, and launch order.
+- **Integrations**: Pre-built components for common services (databases, caches, message brokers) with automatic configuration.
+- **Tooling**: A developer dashboard for real-time monitoring of logs, traces, metrics, and resource health.
+- **Service Discovery**: Automatic service-to-service communication without hardcoded endpoints.
+- **Observability**: Built-in OpenTelemetry support for distributed tracing, metrics, and structured logging.
+
+## ABP Integration
+
+When you enable .NET Aspire in an ABP microservice solution, you get a fully integrated development experience where:
+
+- All microservices, gateways, and applications are orchestrated through a single entry point (AppHost).
+- Infrastructure containers (databases, Redis, RabbitMQ, Elasticsearch, etc.) are managed as code.
+- OpenTelemetry, health checks, and service discovery are automatically configured for all projects via the shared ServiceDefaults project.
+
+## Enabling Aspire
+
+When creating a new microservice solution via ABP Studio:
+
+1. In the solution creation wizard, look for the **".NET Aspire Integration"** step.
+2. Toggle the option to **enable .NET Aspire**.
+3. Complete the wizard—Aspire projects will be generated along with your solution.
+
+
+
+## Solution Structure Changes
+
+When Aspire is enabled, two additional projects are added to your solution:
+
+
+
+### AppHost (Orchestrator)
+
+[`AppHost`](https://aspire.dev/get-started/app-host/) is the .NET Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. All services, gateways, and applications in the solution have their project references added to `AppHost`.
+
+**Why is it added?**
+
+- Centralized orchestration: Start your entire microservice ecosystem with a single command.
+- Code-first infrastructure: Databases, Redis, RabbitMQ, Elasticsearch, and observability tools are defined programmatically.
+- Dependency management: `AppHost` ensures services start in the correct order using `WaitFor()` declarations.
+- Automatic configuration: Connection strings, endpoints, and environment variables are injected automatically.
+
+**Key files in AppHost:**
+| File | Purpose |
+|--------|------------|
+| `AppHost.cs` | Entry point—creates the distributed application builder and adds all resources |
+| `AppHostExtensions.cs` | Extension methods for adding infrastructures, databases, microservices, gateways, and applications |
+
+**What it manages:**
+
+- Database servers (SQL Server, PostgreSQL, MySQL, or MongoDB)
+- Per-service databases
+- Redis (caching)
+- RabbitMQ (messaging)
+- Elasticsearch + Kibana (logging)
+- Prometheus + Grafana (metrics)
+- Jaeger (tracing)
+- OpenTelemetry Collector
+- All microservices, gateways, and web applications
+
+
+
+### ServiceDefaults
+
+[`ServiceDefaults`](https://aspire.dev/fundamentals/service-defaults/) is a shared library that provides common cloud-native configuration for all projects in the solution. The `ServiceDefaults` project reference is added to all services, gateways, and applications in the solution.
+
+**Why is it added?**
+
+- Consistency: Every service uses the same observability, health check, and resilience patterns.
+- Less boilerplate: Add `builder.AddServiceDefaults()` once, get all defaults automatically.
+- Production-ready: OpenTelemetry, health endpoints, and HTTP resilience are preconfigured.
+
+**What it provides:**
+
+| Feature | Description |
+|---------|-------------|
+| OpenTelemetry | Tracing, metrics, and structured logging with automatic instrumentation |
+| Health Checks | `/health` and `/alive` endpoints for Kubernetes-style probes |
+| Service Discovery | Automatic resolution of service endpoints |
+| HTTP Resilience | Retry policies, timeouts, and circuit breakers for HTTP clients |
+
+**Usage example:**
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults(); // Adds all cloud-native defaults
+// ... rest of configuration
+```
+
+## Running the Solution
+
+### Without Aspire
+
+1. Open **Solution Runner** in ABP Studio.
+2. Start all resources in Solution Runner (services, gateways, applications, and tools such as databases, Redis, RabbitMQ, etc.) individually or collectively using the `Default` profile.
+
+
+
+### With Aspire
+1. Open **Solution Runner** in ABP Studio.
+2. Select the **Aspire** profile.
+3. Run `MyCompanyName.MyProjectName.AppHost`.
+4. AppHost automatically:
+ - Starts all infrastructure containers (database, Redis, RabbitMQ, Elasticsearch, etc.).
+ - Launches all microservices, gateways, and applications in dependency order.
+ - Injects connection strings and environment variables.
+ - Opens the Aspire Dashboard for monitoring.
+
+
+
+## Aspire Dashboard
+
+The dashboard enables real-time tracking of key aspects of your app, including logs, traces, and environment configurations. It's designed to enhance the development experience by providing a clear and insightful view of your app's state and structure.
+
+Key features of the dashboard include:
+
+- Real-time tracking of logs, traces, and environment configurations.
+- User interface to stop, start, and restart resources.
+- Collects and displays logs and telemetry; view structured logs, traces, and metrics in an intuitive UI.
+- Enhanced debugging with GitHub Copilot, your AI-powered assistant built into the dashboard.
+
+### Opening the Dashboard
+
+When AppHost starts, the Aspire Dashboard opens automatically in ABP Studio's built-in browser at `https://localhost:15105`. Alternatively, you can right-click on AppHost in Solution Runner and select **Browse** to open it manually.
+
+
+
+**Dashboard login token**: `1q2w3E*` (default, configurable via launch settings)
+
+### Dashboard Features
+
+The dashboard includes several tabs, each offering different insights into your application:
+
+#### Resources
+
+View the status of all resources in your application, including projects, containers, and executables. Monitor health checks, view environment variables, and access endpoints for each resource.
+
+
+
+#### Console
+
+Display console logs from all resources in real-time. Filter by resource and log level to quickly find relevant information during development and debugging.
+
+
+
+#### Structured Logs
+
+View structured logs from all resources with advanced filtering capabilities. Search and filter logs by resource, log level, timestamp, and custom properties to diagnose issues efficiently.
+
+
+
+#### Traces
+
+Explore distributed traces across your microservices to understand request flows and identify performance bottlenecks. Visualize how requests propagate through different services and examine timing information.
+
+
+
+#### Metrics
+
+Monitor real-time metrics including HTTP requests, response times, garbage collection, memory usage, and custom metrics. Visualize metric trends with interactive charts to understand application performance.
+
+
+
+## Tools and Their Management UIs
+
+AppHost pre-configures the following observability and tools. The URLs below are for their **management/dashboard interfaces** (these tools may expose additional internal endpoints for service communication).
+
+All URLs and configurations are defined in the `AppHost` project. If you need to change ports or other settings, you can modify them in the `AppHost` project.
+
+After running AppHost, you can access these tools either by opening the URLs directly in your browser or via Solution Runner **Tools** tab.
+
+### Database Management System Admin Tool
+
+The database management admin tool varies by database type:
+
+| Database | Tool | URL |
+|----------|------|-----|
+| SQL Server | DBeaver CloudBeaver | `http://localhost:8081` |
+| MySQL | phpMyAdmin | `http://localhost:8082` |
+| PostgreSQL | pgAdmin | `http://localhost:8083` |
+| MongoDB | Mongo Express | `http://localhost:8084` |
+
+For example, if using PostgreSQL, access **pgAdmin** at `http://localhost:8083` or via Solution Runner Tools tab:
+
+
+
+
+### Grafana
+
+**URL**: `http://localhost:3001`
+**Credentials**: `admin` / `admin`
+
+Grafana is a visualization and analytics platform for monitoring metrics. It provides interactive dashboards with charts and graphs for tracking application performance.
+
+
+
+### Jaeger
+
+**URL**: `http://localhost:16686`
+**Credentials**: No authentication required
+
+Jaeger is a distributed tracing system to monitor and troubleshoot problems on interconnected software components called microservices.
+
+
+
+### Kibana
+
+**URL**: `http://localhost:5601`
+**Credentials**: No authentication required
+
+Kibana is a visualization tool for Elasticsearch data. It provides search and data visualization capabilities for logs stored in Elasticsearch.
+
+
+
+### Prometheus
+
+**URL**: `http://localhost:9090`
+**Credentials**: No authentication required
+
+Prometheus is a monitoring and alerting toolkit. It collects and stores metrics as time series data, allowing you to query and analyze application performance.
+
+
+
+### RabbitMQ Management
+
+**URL**: `http://localhost:15672`
+**Credentials**: `guest` / `guest`
+
+RabbitMQ Management UI provides a web-based interface for managing and monitoring RabbitMQ message broker, including queues, exchanges, and message flows.
+
+
+
+### Redis Insight
+
+**URL**: `http://localhost:5540`
+**Credentials**: No authentication required
+
+Redis Insight is a visual tool for Redis that allows you to browse data, run commands, and monitor Redis performance.
+
+
+
+## Adding New Services, Gateways, or Applications
+
+When you add a new microservice, gateway, or application via **ABP Studio**:
+
+1. `AppHost` is updated automatically - the new project is registered as a resource with appropriate configurations.
+2. `ServiceDefaults` is referenced - the new project gets cloud-native defaults.
+
+> You don't need to manually edit `AppHost` in most cases.
+
+## Adding a Resource Manually
+
+If you need to add a resource manually (not via **ABP Studio**), follow these steps:
+
+### 1. Reference ServiceDefaults in your new project
+
+```xml
+
+```
+
+> Adjust the path as necessary based on your solution structure.
+
+### 2. Add ServiceDefaults in Program.cs
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults();
+// ... your configuration
+```
+
+### 3. Add Project Reference to AppHost
+
+Add a reference to your resource project in `MySolutionName.MyProjectName.AppHost/MySolutionName.MyProjectName.AppHost.csproj`:
+
+```xml
+
+```
+
+### 4. Register Resource in AppHost
+
+Edit `AppHostExtensions.cs` and add your resource in the `AddAdditionalResources` method:
+
+```csharp
+var myResource = builder
+ .AddProject("myresource", "MySolutionName.MyProjectName.MyResource")
+ .WaitFor(databases.AdministrationDb)
+ .WaitFor(databases.IdentityDb)
+ .WaitFor(databases.MyResourceDb)
+ .WaitFor(databases.AuditLoggingDb)
+ .WaitFor(databases.SaasDb)
+ .WaitFor(databases.LanguageManagementDb)
+ .WaitFor(redis)
+ .WaitFor(rabbitMq)
+ .WithReference(databases.AdministrationDb)
+ .WithReference(databases.IdentityDb)
+ .WithReference(databases.BlobStoringDb)
+ .WithReference(databases.MyResourceDb)
+ .WithReference(databases.AuditLoggingDb)
+ .WithReference(databases.SaasDb)
+ .WithReference(databases.LanguageManagementDb)
+ .ConfigureRabbitMq(rabbitMq, infrastructureDefaultUser, infrastructureDefaultUserPassword)
+ .ConfigureRedis(redis)
+ .ConfigureElasticSearch(elasticsearch);
+applicationResources["MyResource"] = myResource;
+```
+> Adjust the dependencies and configurations as necessary.
+
+### 5. Configure Gateway (if needed)
+
+If your resource should be accessible through a gateway, add the gateway configuration in the `AddAdditionalResources` method:
+
+```csharp
+var webgateway = applicationResources.FirstOrDefault(x => x.Key == "WebGateway").Value;
+if (webgateway != null)
+{
+ webgateway
+ .WaitFor(applicationResources["MyResource"])
+ .WithReference(applicationResources["MyResource"])
+ .WithEnvironment("ReverseProxy__Clusters__MyResource__Destinations__MyResource__Address", "http://MyResource");
+}
+```
+
+### 6. Configure Authentication Server (if needed)
+
+If your resource needs to be added to `CORS` and `RedirectAllowedUrls` configuration for the authentication server, update the `allowedUrls` variable in the `ConfigureAuthServer` method:
+
+```csharp
+var allowedUrls = ReferenceExpression.Create($"{applicationResources["MyResource"].GetEndpoint("http")},...");
+```
+
+### 7. Add Database (if needed)
+
+If your resource requires a dedicated database, add it in the `AddDatabases` method:
+
+```csharp
+var myResourceDb = databaseServers.Postgres.AddDatabase("MyResource", "MySolutionName.MyProjectName_MyResource");
+```
+> Adjust the database management system as necessary.
+
+### 8. Add to Solution Runner Profiles (optional)
+
+To run your resource in **Solution Runner** profiles(Default or Aspire), add it following the instructions in the [Studio running applications documentation](../../studio/running-applications.md#add).
+
+## Deploying the Application
+
+.NET Aspire supports deployment to Azure Container Apps, Kubernetes, and other cloud platforms. For detailed deployment guidance, see the official documentation: [.NET Aspire Deployment](https://aspire.dev/deployment/overview/)
+
+To learn more about .NET Aspire, visit: https://aspire.dev/get-started/what-is-aspire/
diff --git a/docs/en/solution-templates/microservice/helm-charts-and-kubernetes.md b/docs/en/solution-templates/microservice/helm-charts-and-kubernetes.md
index 46da70e88c..041ac00712 100644
--- a/docs/en/solution-templates/microservice/helm-charts-and-kubernetes.md
+++ b/docs/en/solution-templates/microservice/helm-charts-and-kubernetes.md
@@ -11,8 +11,8 @@
//[doc-nav]
{
"Next": {
- "Name": "Miscellaneous guides in the Microservice solution",
- "Path": "solution-templates/microservice/guides"
+ "Name": "Aspire Integration in the Microservice solution",
+ "Path": "solution-templates/microservice/aspire-integration"
}
}
````
diff --git a/docs/en/solution-templates/microservice/images/aspire-apphost-topology.png b/docs/en/solution-templates/microservice/images/aspire-apphost-topology.png
new file mode 100644
index 0000000000..ae571b64e7
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-apphost-topology.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-configuration.png b/docs/en/solution-templates/microservice/images/aspire-configuration.png
new file mode 100644
index 0000000000..74cd5066bb
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-configuration.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-dashboard-console.png b/docs/en/solution-templates/microservice/images/aspire-dashboard-console.png
new file mode 100644
index 0000000000..8d9d088511
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-dashboard-console.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-dashboard-login.png b/docs/en/solution-templates/microservice/images/aspire-dashboard-login.png
new file mode 100644
index 0000000000..a3c6ead56b
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-dashboard-login.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-dashboard-metrics.png b/docs/en/solution-templates/microservice/images/aspire-dashboard-metrics.png
new file mode 100644
index 0000000000..b49ea441b4
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-dashboard-metrics.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-dashboard-resources.png b/docs/en/solution-templates/microservice/images/aspire-dashboard-resources.png
new file mode 100644
index 0000000000..ea8c3bf017
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-dashboard-resources.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-dashboard-structured-logs.png b/docs/en/solution-templates/microservice/images/aspire-dashboard-structured-logs.png
new file mode 100644
index 0000000000..2e732f462b
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-dashboard-structured-logs.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-dashboard-traces.png b/docs/en/solution-templates/microservice/images/aspire-dashboard-traces.png
new file mode 100644
index 0000000000..d099f8659a
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-dashboard-traces.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-database-postgre-pgadmin.png b/docs/en/solution-templates/microservice/images/aspire-database-postgre-pgadmin.png
new file mode 100644
index 0000000000..de857c876d
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-database-postgre-pgadmin.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-grafana-dashboard.png b/docs/en/solution-templates/microservice/images/aspire-grafana-dashboard.png
new file mode 100644
index 0000000000..42072e3337
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-grafana-dashboard.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-jaeger-traces.png b/docs/en/solution-templates/microservice/images/aspire-jaeger-traces.png
new file mode 100644
index 0000000000..da6b87412c
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-jaeger-traces.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-kibana-dashboard.png b/docs/en/solution-templates/microservice/images/aspire-kibana-dashboard.png
new file mode 100644
index 0000000000..7a31ff4fac
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-kibana-dashboard.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-prometheus-dashboard.png b/docs/en/solution-templates/microservice/images/aspire-prometheus-dashboard.png
new file mode 100644
index 0000000000..c1f4de5b5c
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-prometheus-dashboard.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-rabbitmq-management.png b/docs/en/solution-templates/microservice/images/aspire-rabbitmq-management.png
new file mode 100644
index 0000000000..77438c9b08
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-rabbitmq-management.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-redis-insight.png b/docs/en/solution-templates/microservice/images/aspire-redis-insight.png
new file mode 100644
index 0000000000..37780b38b0
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-redis-insight.png differ
diff --git a/docs/en/solution-templates/microservice/images/aspire-solution-structure.png b/docs/en/solution-templates/microservice/images/aspire-solution-structure.png
new file mode 100644
index 0000000000..3379892b69
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/aspire-solution-structure.png differ
diff --git a/docs/en/solution-templates/microservice/images/solution-runner-aspire-profile.png b/docs/en/solution-templates/microservice/images/solution-runner-aspire-profile.png
new file mode 100644
index 0000000000..d11edfe584
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/solution-runner-aspire-profile.png differ
diff --git a/docs/en/solution-templates/microservice/images/solution-runner-default-profile.png b/docs/en/solution-templates/microservice/images/solution-runner-default-profile.png
new file mode 100644
index 0000000000..6867a43872
Binary files /dev/null and b/docs/en/solution-templates/microservice/images/solution-runner-default-profile.png differ
diff --git a/docs/en/studio/installation.md b/docs/en/studio/installation.md
index 6f12953f99..2e1d061920 100644
--- a/docs/en/studio/installation.md
+++ b/docs/en/studio/installation.md
@@ -65,3 +65,7 @@ When you see the "New Version Available" window, follow these steps to upgrade A
2. A progress indicator will display the download status.
3. Once the download is complete, a new modal will appear with the "Install and Relaunch" buttons.
4. Click on the "Install and Relaunch" button to complete the installation process.
+
+## Installing a Specific Version
+
+There is no official support for installing an older version of ABP Studio yet. But, if you want to install an older version of ABP Studio, you can use approach explanined here [https://github.com/enisn/AbpDevTools?tab=readme-ov-file#switch-abp-studio-version](https://github.com/enisn/AbpDevTools?tab=readme-ov-file#switch-abp-studio-version)
\ No newline at end of file
diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md
index c3f57deaaf..d2eba88e0f 100644
--- a/docs/en/studio/release-notes.md
+++ b/docs/en/studio/release-notes.md
@@ -9,6 +9,77 @@
This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version.
+## 2.1.3 (2025-12-15) Latest
+
+* Updated `createCommand` and CLI help for multi-tenancy.
+* Fixed `BookController` templating problem.
+
+## 2.1.2 (2025-12-11)
+
+* Fixed `SLNX` files in templates for macOS.
+* Fixed `DbMigrator` problem on nolayers template.
+
+## 2.1.1 (2025-12-11)
+
+* Fixed duplicate workspace seeding issue.
+* Fixed books sample problems when solution is tiered.
+* Added AI Management module to `abpmdl` file.
+* Improved skip running initial tasks text.
+* Fixed unit test failures.
+* Added `LanguageManagementDbContext` table creation in tests.
+* Removed `ConfigureHttpClientProxies` method.
+* Fixed issue with adding new services to existing Microservices.
+* Fixed AI Management template issues.
+* Reverted browser notification overlay fix.
+
+## 2.1.0 (2025-12-08)
+
+* Enhanced Module Installation UI with improved user experience.
+* Added `AI Management` option to Startup Templates (app-nolayers, app).
+* Added support for new `SLNX` solution file format.
+* Enhanced modularity step in solution creation process.
+* Fixed Swagger authorization issues when projects run via .NET Aspire.
+* Fixed browser notification overlay problems.
+* Added missing `Unit of Work` namespace in solution templates.
+* Fixed JSON file formatting issues.
+* Updated ABP Framework to `10.0.1` and LeptonX to `5.0.1`.
+* Added MySQL compatibility warnings.
+* Fixed initial tasks problems.
+* Improved AI Assistant control UI with better margins and borders.
+
+## 2.0.2 (2025-11-26)
+
+* Fixed `.NET 10` installation problems.
+* Added custom styles for code blocks in **Markdown** view.
+* Fixed `OpenIddictCoreOptions` injection to use `IOptions`.
+* Added IdentityModel package after KubernetesClient.
+
+## 2.0.1 (2025-11-21)
+
+* Added build step before adding EF Core migration.
+* Updated `KubernetesClient` to version `18.0.5`.
+
+## 2.0.0 (2025-11-20)
+
+* Major upgrade to `.NET 10.0` and `ABP Framework 10.0`
+* Replaced `IdentityModel` with `Duende.IdentityModel`.
+* Added "Open on Start in Browser" option for .NET applications in Solution Runner.
+* Added `Mapperly` configuration.
+* Enabled user and tenant impersonation in Blazor client modules.
+* Enhanced notification system to allow text copying.
+* Added environment variable support for DesignTime DbContext.
+* Used C# instead of JSON for Aspire AppHost project configuration.
+* Fixed MongoDB image pulling problems.
+* Improved AI Assistant with better code block visibility across themes.
+* Added different cache paths for each browser instance.
+* Fixed various UI issues including mouse pointer problems in trees and horizontal scrolling.
+* Added `FileManagement` download URL configuration for tiered projects.
+* Added chat SignalR configuration to Microservice Blazor apps.
+* Updated `Blazorise` packages to version `1.8.6`.
+* Fixed `BackToImpersonator` button in Microservice Template.
+* Added log recording while crashing for better debugging.
+* Enhanced tab headers for **Solution Runner** and **Kubernetes**.
+
## 1.4.2 (2025-10-30)
* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.3.6`)
diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md
index ba51cfb17d..f399c97390 100644
--- a/docs/en/studio/version-mapping.md
+++ b/docs/en/studio/version-mapping.md
@@ -11,6 +11,8 @@ This document provides a general overview of the relationship between various ve
| **ABP Studio Version** | **ABP Version of Startup Template** |
|------------------------|---------------------------|
+| 2.1.0 - 2.1.3 | 10.0.1 |
+| 2.0.0 to 2.0.2 | 10.0.0 |
| 1.4.2 | 9.3.6 |
| 1.3.3 to 1.4.1 | 9.3.5 |
| 1.3.0 - 1.3.2 | 9.3.4 |
@@ -19,8 +21,7 @@ This document provides a general overview of the relationship between various ve
| 1.1.2 | 9.2.3 |
| 1.1.0 - 1.1.1 | 9.2.2 |
| 1.0.2 | 9.2.1 |
-| 1.0.1 | 9.2.0 |
-| 1.0.0 | 9.2.0 |
+| 1.0.0 - 1.0.1 | 9.2.0 |
| 0.9.26 | 9.1.1 |
| 0.9.24 - 0.9.25 | 9.1.0 |
| 0.9.22 - 0.9.23 | 9.0.4 |
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png
index adebe653b5..94dfb63a56 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png and b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png
index a0ecbd469d..e3032dfbde 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png and b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png
index 687dca3593..5c4a377b4a 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png
index 5535659b45..e690e9b0e9 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png
index 1bf3253fe7..961e7565a7 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png
index 54860a5e9f..7a2850afc2 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png b/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png
index 47a37bbd9b..bf1394a0a2 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png and b/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png b/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png
index dbb1890c14..b1e4a7d06e 100644
Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png and b/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png differ
diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-02.md b/docs/en/tutorials/book-store-with-abp-suite/part-02.md
index 11725201cb..f9462b9130 100644
--- a/docs/en/tutorials/book-store-with-abp-suite/part-02.md
+++ b/docs/en/tutorials/book-store-with-abp-suite/part-02.md
@@ -96,7 +96,7 @@ Here is the all details for the `Book` entity:
* `Name` is **required**, it's a **string** property and maximum length is **128**.
* `Type` is an **enum** and the enum file path is *\Acme.BookStore.Domain.Shared\Books\BookType.cs*.
* `PublishDate` is a **DateTime** property and **not nullable**.
-* `Price` is a **float** property and **required**.
+* `Price` is a **float** property.
You can leave the other configurations as default.
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs
index ad2f6ba983..26e51dbd38 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.DependencyInjection;
@@ -16,7 +17,7 @@ public class MauiCurrentApplicationConfigurationCacheResetService :
_mauiBlazorCachedApplicationConfigurationClient = mauiBlazorCachedApplicationConfigurationClient;
}
- public async Task ResetAsync()
+ public async Task ResetAsync(Guid? userId = null)
{
await _mauiBlazorCachedApplicationConfigurationClient.InitializeAsync();
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs
index 02de9d8bf7..144d975cb5 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs
@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
@@ -19,10 +20,8 @@ public class BlazorServerCurrentApplicationConfigurationCacheResetService :
_localEventBus = localEventBus;
}
- public async Task ResetAsync()
+ public async Task ResetAsync(Guid? userId = null)
{
- await _localEventBus.PublishAsync(
- new CurrentApplicationConfigurationCacheResetEventData()
- );
+ await _localEventBus.PublishAsync(new CurrentApplicationConfigurationCacheResetEventData(userId));
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs
index c3e33a9e41..2d44399f9d 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs
@@ -1,8 +1,9 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading.Tasks;
namespace Volo.Abp.AspNetCore.Components.Web.Configuration;
public interface ICurrentApplicationConfigurationCacheResetService
{
- Task ResetAsync();
+ Task ResetAsync(Guid? userId = null);
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs
index bb91d70775..1cfaee3315 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
@@ -5,7 +6,7 @@ namespace Volo.Abp.AspNetCore.Components.Web.Configuration;
public class NullCurrentApplicationConfigurationCacheResetService : ICurrentApplicationConfigurationCacheResetService, ISingletonDependency
{
- public Task ResetAsync()
+ public Task ResetAsync(Guid? userId = null)
{
return Task.CompletedTask;
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs
index 40ac508030..359678daf4 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.DependencyInjection;
@@ -16,7 +17,7 @@ public class BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService :
_webAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient;
}
- public async Task ResetAsync()
+ public async Task ResetAsync(Guid? userId = null)
{
await _webAssemblyCachedApplicationConfigurationClient.InitializeAsync();
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs
index cc1180fd20..ea0d778fad 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs
@@ -1,13 +1,26 @@
-using System.Globalization;
-using Volo.Abp.Users;
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Volo.Abp.Caching;
+using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Client;
-public static class MvcCachedApplicationConfigurationClientHelper
+public class MvcCachedApplicationConfigurationClientHelper : ITransientDependency
{
- public static string CreateCacheKey(ICurrentUser currentUser)
+ protected IDistributedCache ApplicationVersionCache { get; }
+
+ public MvcCachedApplicationConfigurationClientHelper(IDistributedCache applicationVersionCache)
+ {
+ ApplicationVersionCache = applicationVersionCache;
+ }
+
+ public virtual async Task CreateCacheKeyAsync(Guid? userId)
{
- var userKey = currentUser.Id?.ToString("N") ?? "Anonymous";
- return $"ApplicationConfiguration_{userKey}_{CultureInfo.CurrentUICulture.Name}";
+ var appVersion = await ApplicationVersionCache.GetOrAddAsync(MvcCachedApplicationVersionCacheItem.CacheKey,
+ () => Task.FromResult(new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N")))) ??
+ new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N"));
+ var userKey = userId?.ToString("N") ?? "Anonymous";
+ return $"ApplicationConfiguration_{appVersion.Version}_{userKey}_{CultureInfo.CurrentUICulture.Name}";
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs
new file mode 100644
index 0000000000..1cd9990a44
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs
@@ -0,0 +1,13 @@
+namespace Volo.Abp.AspNetCore.Mvc.Client;
+
+public class MvcCachedApplicationVersionCacheItem
+{
+ public const string CacheKey = "Mvc_Application_Version";
+
+ public string Version { get; set; }
+
+ public MvcCachedApplicationVersionCacheItem(string version)
+ {
+ Version = version;
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs
index 8d787bec65..ba42b55d18 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs
@@ -20,6 +20,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
protected IHttpClientFactory HttpClientFactory { get; }
protected IRemoteServiceHttpClientAuthenticator HttpClientAuthenticator { get; }
protected IDistributedCache ApplicationConfigurationDtoCache { get; }
+ protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; }
protected ICurrentUser CurrentUser { get; }
public RemoteDynamicClaimsPrincipalContributorCache(
@@ -28,7 +29,8 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
IOptions abpClaimsPrincipalFactoryOptions,
IRemoteServiceHttpClientAuthenticator httpClientAuthenticator,
IDistributedCache applicationConfigurationDtoCache,
- ICurrentUser currentUser)
+ ICurrentUser currentUser,
+ MvcCachedApplicationConfigurationClientHelper cacheHelper)
: base(abpClaimsPrincipalFactoryOptions)
{
Cache = cache;
@@ -36,6 +38,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
HttpClientAuthenticator = httpClientAuthenticator;
ApplicationConfigurationDtoCache = applicationConfigurationDtoCache;
CurrentUser = currentUser;
+ CacheHelper = cacheHelper;
}
protected async override Task GetCacheAsync(Guid userId, Guid? tenantId = null)
@@ -56,7 +59,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
catch (Exception e)
{
Logger.LogWarning(e, $"Failed to refresh remote claims for user: {userId}");
- await ApplicationConfigurationDtoCache.RemoveAsync(MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser));
+ await ApplicationConfigurationDtoCache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id));
throw;
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs
index 0bff9d09b1..e3d3c12370 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
@@ -13,14 +14,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Client;
public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigurationClient, ITransientDependency
{
+ private const string ApplicationConfigurationDtoCacheKey = "ApplicationConfigurationDto_CacheKey";
+
protected IHttpContextAccessor HttpContextAccessor { get; }
protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; }
protected AbpApplicationLocalizationClientProxy ApplicationLocalizationClientProxy { get; }
protected ICurrentUser CurrentUser { get; }
+ protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; }
protected IDistributedCache Cache { get; }
protected AbpAspNetCoreMvcClientCacheOptions Options { get; }
public MvcCachedApplicationConfigurationClient(
+ MvcCachedApplicationConfigurationClientHelper cacheHelper,
IDistributedCache cache,
AbpApplicationConfigurationClientProxy applicationConfigurationAppService,
ICurrentUser currentUser,
@@ -33,13 +38,27 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
HttpContextAccessor = httpContextAccessor;
ApplicationLocalizationClientProxy = applicationLocalizationClientProxy;
Options = options.Value;
+ CacheHelper = cacheHelper;
Cache = cache;
}
- public async Task GetAsync()
+ public virtual async Task GetAsync()
{
- var cacheKey = CreateCacheKey();
+ string? cacheKey = null;
var httpContext = HttpContextAccessor?.HttpContext;
+ if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key)
+ {
+ cacheKey = key;
+ }
+
+ if (cacheKey.IsNullOrWhiteSpace())
+ {
+ cacheKey = await CreateCacheKeyAsync();
+ if (httpContext != null)
+ {
+ httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey;
+ }
+ }
if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
{
@@ -86,8 +105,21 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
public ApplicationConfigurationDto Get()
{
- var cacheKey = CreateCacheKey();
+ string? cacheKey = null;
var httpContext = HttpContextAccessor?.HttpContext;
+ if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key)
+ {
+ cacheKey = key;
+ }
+
+ if (cacheKey.IsNullOrWhiteSpace())
+ {
+ cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync);
+ if (httpContext != null)
+ {
+ httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey;
+ }
+ }
if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
{
@@ -97,8 +129,8 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
return AsyncHelper.RunSync(GetAsync);
}
- protected virtual string CreateCacheKey()
+ protected virtual async Task CreateCacheKeyAsync()
{
- return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser);
+ return await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id);
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs
index 8bd3971779..c32b63249c 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs
@@ -3,7 +3,6 @@ using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
-using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.Mvc.Client;
@@ -11,23 +10,29 @@ public class MvcCurrentApplicationConfigurationCacheResetEventHandler :
ILocalEventHandler,
ITransientDependency
{
- protected ICurrentUser CurrentUser { get; }
protected IDistributedCache Cache { get; }
+ protected IDistributedCache ApplicationVersionCache { get; }
+ protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; }
- public MvcCurrentApplicationConfigurationCacheResetEventHandler(ICurrentUser currentUser,
- IDistributedCache cache)
+ public MvcCurrentApplicationConfigurationCacheResetEventHandler(
+ IDistributedCache cache,
+ IDistributedCache applicationVersionCache,
+ MvcCachedApplicationConfigurationClientHelper cacheHelper)
{
- CurrentUser = currentUser;
Cache = cache;
+ ApplicationVersionCache = applicationVersionCache;
+ CacheHelper = cacheHelper;
}
public virtual async Task HandleEventAsync(CurrentApplicationConfigurationCacheResetEventData eventData)
{
- await Cache.RemoveAsync(CreateCacheKey());
- }
-
- protected virtual string CreateCacheKey()
- {
- return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser);
+ if (eventData.UserId.HasValue)
+ {
+ await Cache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(eventData.UserId));
+ }
+ else
+ {
+ await ApplicationVersionCache.RemoveAsync(MvcCachedApplicationVersionCacheItem.CacheKey);
+ }
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs
index a50cb7b136..fccc295429 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs
@@ -1,9 +1,21 @@
-namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
+using System;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
///
/// This event is used to invalidate current user's cached configuration.
///
public class CurrentApplicationConfigurationCacheResetEventData
{
+ public Guid? UserId { get; set; }
+
+ public CurrentApplicationConfigurationCacheResetEventData()
+ {
+
+ }
+ public CurrentApplicationConfigurationCacheResetEventData(Guid? userId)
+ {
+ UserId = userId;
+ }
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs
index 9935b90903..f2fbb2e4bf 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Breadcrumb/AbpBreadcrumbItemTagHelperService.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Mvc.Rendering;
+using System;
+using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Collections.Generic;
using System.Text.Encodings.Web;
@@ -41,12 +42,12 @@ public class AbpBreadcrumbItemTagHelperService : AbpTagHelperService : AbpTagHelperSe
}
var span = new TagBuilder("span");
- span.InnerHtml.AppendHtml(TagHelper.Text!);
+ span.InnerHtml.Append(TagHelper.Text!);
output.Content.AppendHtml(span);
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs
index b8462f4b5b..dea75d44d7 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Card/AbpCardBodyTagHelperService.cs
@@ -22,7 +22,7 @@ public class AbpCardBodyTagHelperService : AbpTagHelperService
var label = new TagBuilder("label");
label.Attributes.Add("for", GetIdAttributeValue(inputTag));
- label.InnerHtml.AppendHtml(_encoder.Encode(TagHelper.Label));
+ label.InnerHtml.Append(TagHelper.Label);
label.AddCssClass(isCheckbox ? "form-check-label" : "form-label");
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
index ad2b7abe15..0dedb9c755 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpRadioInputTagHelperService.cs
@@ -107,7 +107,7 @@ public class AbpRadioInputTagHelperService : AbpTagHelperService
var label = new TagBuilder("label");
label.AddCssClass("form-label");
label.Attributes.Add("for", GetIdAttributeValue(selectTag));
- label.InnerHtml.AppendHtml(_encoder.Encode(TagHelper.Label));
+ label.InnerHtml.Append(TagHelper.Label);
label.InnerHtml.AppendHtml(GetRequiredSymbol(context, output));
return label.ToHtmlString();
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs
index 5088c08293..dfd9ab60e4 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs
@@ -556,7 +556,7 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp
var label = new TagBuilder("label");
label.Attributes.Add("for", GetIdAttributeValue(inputTag));
- label.InnerHtml.AppendHtml(Encoder.Encode(TagHelper.Label));
+ label.InnerHtml.Append(TagHelper.Label);
label.AddCssClass("form-label");
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs
index 52ced17912..a6e0b76683 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalHeaderTagHelperService.cs
@@ -27,7 +27,7 @@ public class AbpModalHeaderTagHelperService : AbpTagHelperService
anchor.Attributes.Add(attr.Name, attr.Value.ToString());
}
- anchor.InnerHtml.AppendHtml(title);
+ anchor.InnerHtml.Append(title);
return anchor.ToHtmlString();
}
@@ -73,7 +73,7 @@ public class AbpTabTagHelperService : AbpTagHelperService
anchor.Attributes.Add(attr.Name, attr.Value.ToString());
}
- anchor.InnerHtml.AppendHtml(title);
+ anchor.InnerHtml.Append(title);
var listItem = new TagBuilder("li");
listItem.AddCssClass("nav-item");
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs
index 3026860ed6..c27be9a8c6 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Tab/AbpTabsTagHelperService.cs
@@ -225,6 +225,6 @@ public class AbpTabsTagHelperService : AbpTagHelperService
protected virtual string SetTabItemNameIfNotProvided(string content, int index)
{
- return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name) + "_" + index);
+ return content.Replace(TabItemNamePlaceHolder, HtmlGenerator.Encode(TagHelper.Name ?? string.Empty) + "_" + index);
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
index c4701aa7d5..0e87d88370 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs
@@ -19,9 +19,9 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService
IBundleManager bundleManager,
IOptions options,
IWebHostEnvironment hostingEnvironment) : base(
- bundleManager,
- options,
- hostingEnvironment)
+ bundleManager,
+ options,
+ hostingEnvironment)
{
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
index 1ed3a76fe1..a61d000d6b 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs
@@ -22,9 +22,9 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService
IOptions options,
IWebHostEnvironment hostingEnvironment,
IOptions securityHeadersOptions) : base(
- bundleManager,
- options,
- hostingEnvironment)
+ bundleManager,
+ options,
+ hostingEnvironment)
{
SecurityHeadersOptions = securityHeadersOptions.Value;
}
diff --git a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs
index 2e16c79809..bb2f080e71 100644
--- a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs
+++ b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
@@ -33,6 +34,15 @@ public abstract class AbpWebApplicationFactoryIntegratedTest : WebAppl
return base.CreateHost(builder);
}
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureAppConfiguration((hostingContext, config) =>
+ {
+ hostingContext.HostingEnvironment.EnvironmentName = "Production";
+ });
+ base.ConfigureWebHost(builder);
+ }
+
protected virtual T? GetService()
{
return Services.GetService();
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/AbpPermissionOptions.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/AbpPermissionOptions.cs
index 4bdd7c0bb8..c240e1ac07 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/AbpPermissionOptions.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/AbpPermissionOptions.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Volo.Abp.Authorization.Permissions.Resources;
using Volo.Abp.Collections;
namespace Volo.Abp.Authorization.Permissions;
@@ -9,6 +10,8 @@ public class AbpPermissionOptions
public ITypeList ValueProviders { get; }
+ public ITypeList ResourceValueProviders { get; }
+
public HashSet DeletedPermissions { get; }
public HashSet DeletedPermissionGroups { get; }
@@ -17,6 +20,7 @@ public class AbpPermissionOptions
{
DefinitionProviders = new TypeList();
ValueProviders = new TypeList();
+ ResourceValueProviders = new TypeList();
DeletedPermissions = new HashSet();
DeletedPermissionGroups = new HashSet();
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/ICanAddChildPermission.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/ICanAddChildPermission.cs
index 828e4cbd01..2bcf9ba10d 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/ICanAddChildPermission.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/ICanAddChildPermission.cs
@@ -11,4 +11,4 @@ public interface ICanAddChildPermission
ILocalizableString? displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true);
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
index 7ff890bac7..5a1bef8e1d 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
@@ -1,5 +1,7 @@
using System;
+using JetBrains.Annotations;
using Volo.Abp.Localization;
+using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Authorization.Permissions;
@@ -46,4 +48,16 @@ public interface IPermissionDefinitionContext
/// Name of the permission
///
PermissionDefinition? GetPermissionOrNull(string name);
+
+ PermissionDefinition AddResourcePermission(
+ string name,
+ string resourceName,
+ string managementPermissionName,
+ ILocalizableString? displayName = null,
+ MultiTenancySides multiTenancySide = MultiTenancySides.Both,
+ bool isEnabled = true);
+
+ PermissionDefinition? GetResourcePermissionOrNull([NotNull] string resourceName, [NotNull] string name);
+
+ void RemoveResourcePermission([NotNull] string resourceName, [NotNull] string name);
}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionManager.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionManager.cs
index ca04b110f7..d9411d54fa 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionManager.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionManager.cs
@@ -11,7 +11,14 @@ public interface IPermissionDefinitionManager
Task GetOrNullAsync([NotNull] string name);
+ [ItemNotNull]
+ Task GetResourcePermissionAsync([NotNull]string resourceName, [NotNull] string name);
+
+ Task GetResourcePermissionOrNullAsync([NotNull]string resourceName, [NotNull] string name);
+
Task> GetPermissionsAsync();
+ Task> GetResourcePermissionsAsync();
+
Task> GetGroupsAsync();
}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionValueProvider.cs
index ae9252632d..683f513cf4 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionValueProvider.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionValueProvider.cs
@@ -6,7 +6,6 @@ public interface IPermissionValueProvider
{
string Name { get; }
- //TODO: Rename to GetResult? (CheckAsync throws exception by naming convention)
Task CheckAsync(PermissionValueCheckContext context);
Task CheckAsync(PermissionValuesCheckContext context);
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
index 0f5d713e2b..c93992ed49 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
@@ -7,7 +7,7 @@ using Volo.Abp.SimpleStateChecking;
namespace Volo.Abp.Authorization.Permissions;
-public class PermissionDefinition :
+public class PermissionDefinition :
IHasSimpleStateCheckers,
ICanAddChildPermission
{
@@ -16,6 +16,16 @@ public class PermissionDefinition :
///
public string Name { get; }
+ ///
+ /// Resource name of the permission.
+ ///
+ public string? ResourceName { get; set; }
+
+ ///
+ /// Management permission of the resource permission.
+ ///
+ public string? ManagementPermissionName { get; set; }
+
///
/// Parent of this permission if one exists.
/// If set, this permission can be granted only if parent is granted.
@@ -76,6 +86,19 @@ public class PermissionDefinition :
set => Properties[name] = value;
}
+ protected internal PermissionDefinition(
+ [NotNull] string name,
+ string resourceName,
+ string managementPermissionName,
+ ILocalizableString? displayName = null,
+ MultiTenancySides multiTenancySide = MultiTenancySides.Both,
+ bool isEnabled = true)
+ : this(name, displayName, multiTenancySide, isEnabled)
+ {
+ ResourceName = Check.NotNull(resourceName, nameof(resourceName));
+ ManagementPermissionName = Check.NotNull(managementPermissionName, nameof(managementPermissionName));
+ }
+
protected internal PermissionDefinition(
[NotNull] string name,
ILocalizableString? displayName = null,
@@ -99,6 +122,11 @@ public class PermissionDefinition :
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
+ if (ResourceName != null)
+ {
+ throw new AbpException($"Resource permission cannot have child permissions. Resource: {ResourceName}");
+ }
+
var child = new PermissionDefinition(
name,
displayName,
@@ -109,12 +137,12 @@ public class PermissionDefinition :
};
child[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName] = this[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName];
-
+
_children.Add(child);
return child;
}
-
+
PermissionDefinition ICanAddChildPermission.AddPermission(
string name,
ILocalizableString? displayName = null,
@@ -124,7 +152,6 @@ public class PermissionDefinition :
return this.AddChild(name, displayName, multiTenancySide, isEnabled);
}
-
///
/// Sets a property in the dictionary.
/// This is a shortcut for nested calls on this object.
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
index 394cdb9d82..c13f53866a 100644
--- a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using JetBrains.Annotations;
using Volo.Abp.Localization;
+using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Authorization.Permissions;
@@ -11,17 +13,20 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
public Dictionary Groups { get; }
+ public List ResourcePermissions { get; }
+
internal IPermissionDefinitionProvider? CurrentProvider { get; set; }
public static class KnownPropertyNames
{
public const string CurrentProviderName = "_CurrentProviderName";
}
-
+
public PermissionDefinitionContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Groups = new Dictionary();
+ ResourcePermissions = new List();
}
public virtual PermissionGroupDefinition AddGroup(
@@ -43,7 +48,7 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
}
Groups[name] = group;
-
+
return group;
}
@@ -51,37 +56,23 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
public virtual PermissionGroupDefinition GetGroup([NotNull] string name)
{
var group = GetGroupOrNull(name);
-
- if (group == null)
- {
- throw new AbpException($"Could not find a permission definition group with the given name: {name}");
- }
-
- return group;
+ return group ?? throw new AbpException($"Could not find a permission definition group with the given name: {name}");
}
public virtual PermissionGroupDefinition? GetGroupOrNull([NotNull] string name)
{
Check.NotNull(name, nameof(name));
-
- if (!Groups.ContainsKey(name))
- {
- return null;
- }
-
- return Groups[name];
+ return Groups.GetOrDefault(name);
}
public virtual void RemoveGroup(string name)
{
Check.NotNull(name, nameof(name));
- if (!Groups.ContainsKey(name))
+ if (!Groups.Remove(name))
{
throw new AbpException($"Not found permission group with name: {name}");
}
-
- Groups.Remove(name);
}
public virtual PermissionDefinition? GetPermissionOrNull([NotNull] string name)
@@ -100,4 +91,58 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
return null;
}
+
+ public virtual PermissionDefinition AddResourcePermission(
+ string name,
+ string resourceName,
+ string managementPermissionName,
+ ILocalizableString? displayName = null,
+ MultiTenancySides multiTenancySide = MultiTenancySides.Both,
+ bool isEnabled = true)
+ {
+ Check.NotNull(name, nameof(name));
+ Check.NotNull(resourceName, nameof(resourceName));
+ Check.NotNull(managementPermissionName, nameof(managementPermissionName));
+
+ if (ResourcePermissions.Any(x => x.ResourceName == resourceName && x.Name == name))
+ {
+ throw new AbpException($"There is already an existing resource permission with name: {name} for resource: {resourceName}");
+ }
+
+ var permission = new PermissionDefinition(
+ name,
+ resourceName,
+ managementPermissionName,
+ displayName,
+ multiTenancySide,
+ isEnabled)
+ {
+ [KnownPropertyNames.CurrentProviderName] = CurrentProvider?.GetType().FullName
+ };
+
+ ResourcePermissions.Add(permission);
+
+ return permission;
+ }
+
+ public virtual PermissionDefinition? GetResourcePermissionOrNull([NotNull] string resourceName, [NotNull] string name)
+ {
+ Check.NotNull(resourceName, nameof(resourceName));
+ Check.NotNull(name, nameof(name));
+
+ return ResourcePermissions.FirstOrDefault(p => p.ResourceName == resourceName && p.Name == name);
+ }
+
+ public virtual void RemoveResourcePermission([NotNull] string resourceName, [NotNull] string name)
+ {
+ Check.NotNull(resourceName, nameof(resourceName));
+ Check.NotNull(name, nameof(name));
+
+ var resourcePermission = GetResourcePermissionOrNull(resourceName, name);
+ if (resourcePermission == null)
+ {
+ throw new AbpException($"Not found resource permission with name: {name} for resource: {resourceName}");
+ }
+ ResourcePermissions.Remove(resourcePermission);
+ }
}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IHasResourcePermissions.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IHasResourcePermissions.cs
new file mode 100644
index 0000000000..903f6bd2d6
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IHasResourcePermissions.cs
@@ -0,0 +1,8 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public interface IHasResourcePermissions : IKeyedObject
+{
+ Dictionary ResourcePermissions { get; }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionChecker.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionChecker.cs
new file mode 100644
index 0000000000..0270d2c96b
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionChecker.cs
@@ -0,0 +1,33 @@
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public interface IResourcePermissionChecker
+{
+ Task IsGrantedAsync(
+ string name,
+ string resourceName,
+ string resourceKey
+ );
+
+ Task IsGrantedAsync(
+ ClaimsPrincipal? claimsPrincipal,
+ string name,
+ string resourceName,
+ string resourceKey
+ );
+
+ Task IsGrantedAsync(
+ string[] names,
+ string resourceName,
+ string resourceKey
+ );
+
+ Task IsGrantedAsync(
+ ClaimsPrincipal? claimsPrincipal,
+ string[] names,
+ string resourceName,
+ string resourceKey
+ );
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionStore.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionStore.cs
new file mode 100644
index 0000000000..42f751b4c5
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionStore.cs
@@ -0,0 +1,83 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public interface IResourcePermissionStore
+{
+ ///
+ /// Checks if the given permission is granted for the given resource.
+ ///
+ /// The name of the permission.
+ /// The name of the resource.
+ /// Resource key
+ /// The name of the provider.
+ /// The key of the provider.
+ ///
+ /// True if the permission is granted.
+ ///
+ Task IsGrantedAsync(
+ string name,
+ string resourceName,
+ string resourceKey,
+ string providerName,
+ string providerKey
+ );
+
+ ///
+ /// Checks if the given permissions are granted for the given resource.
+ ///
+ /// The name of the permissions.
+ /// The name of the resource.
+ /// Resource key
+ /// The name of the provider.
+ /// The key of the provider.
+ ///
+ /// A object containing the grant results for each permission.
+ ///
+ Task IsGrantedAsync(
+ string[] names,
+ string resourceName,
+ string resourceKey,
+ string providerName,
+ string providerKey
+ );
+
+ ///
+ /// Gets all permissions for the given resource.
+ ///
+ /// Resource name
+ /// Resource key
+ ///
+ /// A object containing the grant results for each permission.
+ ///
+ Task GetPermissionsAsync(
+ string resourceName,
+ string resourceKey
+ );
+
+ ///
+ /// Gets all granted permissions for the given resource.
+ ///
+ /// Resource name
+ /// Resource key
+ ///
+ /// An array of granted permission names.
+ ///
+ Task GetGrantedPermissionsAsync(
+ string resourceName,
+ string resourceKey
+ );
+
+ ///
+ /// Retrieves the keys of resources for which the specified permission is granted.
+ ///
+ /// The name of the resource.
+ /// The name of the permission.
+ ///
+ /// An array of resource keys where the specified permission is granted.
+ ///
+ Task GetGrantedResourceKeysAsync(
+ string resourceName,
+ string name
+ );
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProvider.cs
new file mode 100644
index 0000000000..288b878e5d
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProvider.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public interface IResourcePermissionValueProvider
+{
+ string Name { get; }
+
+ Task CheckAsync(ResourcePermissionValueCheckContext context);
+
+ Task CheckAsync(ResourcePermissionValuesCheckContext context);
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProviderManager.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProviderManager.cs
new file mode 100644
index 0000000000..e07e63d06a
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProviderManager.cs
@@ -0,0 +1,8 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public interface IResourcePermissionValueProviderManager
+{
+ IReadOnlyList ValueProviders { get; }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/NullResourcePermissionStore.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/NullResourcePermissionStore.cs
new file mode 100644
index 0000000000..84bf38ef42
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/NullResourcePermissionStore.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Threading;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class NullResourcePermissionStore : IResourcePermissionStore, ISingletonDependency
+{
+ public ILogger Logger { get; set; }
+
+ public NullResourcePermissionStore()
+ {
+ Logger = NullLogger.Instance;
+ }
+
+ public Task IsGrantedAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey)
+ {
+ return TaskCache.FalseResult;
+ }
+
+ public Task IsGrantedAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey)
+ {
+ return Task.FromResult(new MultiplePermissionGrantResult(names, PermissionGrantResult.Prohibited));
+ }
+
+ public Task GetPermissionsAsync(string resourceName, string resourceKey)
+ {
+ return Task.FromResult(new MultiplePermissionGrantResult());
+ }
+
+ public Task GetGrantedPermissionsAsync(string resourceName, string resourceKey)
+ {
+ return Task.FromResult(Array.Empty());
+ }
+
+ public Task GetGrantedResourceKeysAsync(string resourceName, string name)
+ {
+ return Task.FromResult(Array.Empty());
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionCheckerExtensions.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionCheckerExtensions.cs
new file mode 100644
index 0000000000..c1d153b246
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionCheckerExtensions.cs
@@ -0,0 +1,34 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public static class ResourcePermissionCheckerExtensions
+{
+ ///
+ /// Checks if a specific permission is granted for a resource with a given key.
+ ///
+ /// The type of the resource.
+ /// The resource permission checker instance.
+ /// The name of the permission to check.
+ /// The resource instance to check permission for.
+ /// The unique key identifying the resource instance.
+ /// A task that represents the asynchronous operation. The task result contains a boolean value indicating whether the permission is granted.
+ public static Task IsGrantedAsync(
+ this IResourcePermissionChecker resourcePermissionChecker,
+ string permissionName,
+ TResource resource,
+ object resourceKey
+ )
+ {
+ Check.NotNull(resourcePermissionChecker, nameof(resourcePermissionChecker));
+ Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName));
+ Check.NotNull(resource, nameof(resource));
+ Check.NotNull(resourceKey, nameof(resourceKey));
+
+ return resourcePermissionChecker.IsGrantedAsync(
+ permissionName,
+ typeof(TResource).FullName!,
+ resourceKey.ToString()!
+ );
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionGrantInfo.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionGrantInfo.cs
new file mode 100644
index 0000000000..4eb9604454
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionGrantInfo.cs
@@ -0,0 +1,21 @@
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class ResourcePermissionGrantInfo : PermissionGrantInfo
+{
+ public string ResourceName { get; }
+
+ public string ResourceKey { get; }
+
+ public ResourcePermissionGrantInfo(
+ string name,
+ bool isGranted,
+ string resourceName,
+ string resourceKey,
+ string? providerName = null,
+ string? providerKey = null)
+ : base(name, isGranted, providerName, providerKey)
+ {
+ ResourceName = resourceName;
+ ResourceKey = resourceKey;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionStoreExtensions.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionStoreExtensions.cs
new file mode 100644
index 0000000000..f5604a7683
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionStoreExtensions.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public static class ResourcePermissionStoreExtensions
+{
+ ///
+ /// Retrieves the list of granted permissions for a specific resource with a given key.
+ ///
+ /// The type of the resource.
+ /// The resource permission store instance.
+ /// The resource instance to retrieve permissions for.
+ /// The unique key identifying the resource instance.
+ /// A task that represents the asynchronous operation. The task result contains an array of strings representing the granted permissions.
+ public static async Task GetGrantedPermissionsAsync(
+ this IResourcePermissionStore resourcePermissionStore,
+ TResource resource,
+ object resourceKey
+ )
+ {
+ return (await GetPermissionsAsync(resourcePermissionStore, resource, resourceKey)).Where(x => x.Value).Select(x => x.Key).ToArray();
+ }
+
+ ///
+ /// Retrieves a dictionary of permissions and their granted status for the specified entity.
+ ///
+ /// The type of the resource.
+ /// The resource permission store instance.
+ /// The resource for which the permissions are being retrieved.
+ /// The unique key identifying the resource instance.
+ /// A dictionary where the keys are permission names and the values are booleans indicating whether the permission is granted.
+ public static async Task> GetPermissionsAsync(
+ this IResourcePermissionStore resourcePermissionStore,
+ TResource resource,
+ object resourceKey
+ )
+ {
+ Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
+ Check.NotNull(resource, nameof(resource));
+ Check.NotNull(resourceKey, nameof(resourceKey));
+
+ var result = await resourcePermissionStore.GetPermissionsAsync(
+ typeof(TResource).FullName!,
+ resourceKey.ToString()!
+ );
+
+ return result.Result.ToDictionary(x => x.Key, x => x.Value == PermissionGrantResult.Granted);
+ }
+
+ ///
+ /// Retrieves the keys of the resources granted a specific permission.
+ ///
+ /// The type of the resource.
+ /// The resource permission store instance.
+ /// The resource instance to check granted permissions for.
+ /// The name of the permission to check.
+ /// A task that represents the asynchronous operation. The task result contains an array of strings representing the granted resource keys.
+ public static Task GetGrantedResourceKeysAsync(
+ this IResourcePermissionStore resourcePermissionStore,
+ TResource resource,
+ string permissionName
+ )
+ {
+ Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
+ Check.NotNull(resource, nameof(resource));
+ Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName));
+
+ return resourcePermissionStore.GetGrantedResourceKeysAsync(
+ typeof(TResource).FullName!,
+ permissionName
+ );
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueCheckContext.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueCheckContext.cs
new file mode 100644
index 0000000000..dc8c78be64
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueCheckContext.cs
@@ -0,0 +1,22 @@
+using System.Security.Claims;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class ResourcePermissionValueCheckContext : PermissionValueCheckContext
+{
+ public string ResourceName { get; }
+
+ public string ResourceKey { get; }
+
+ public ResourcePermissionValueCheckContext(PermissionDefinition permission, string resourceName, string resourceKey)
+ : this(permission, null, resourceName, resourceKey)
+ {
+ }
+
+ public ResourcePermissionValueCheckContext(PermissionDefinition permission, ClaimsPrincipal? principal, string resourceName, string resourceKey)
+ : base(permission, principal)
+ {
+ ResourceName = resourceName;
+ ResourceKey = resourceKey;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProvider.cs
new file mode 100644
index 0000000000..ff53f274ad
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProvider.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public abstract class ResourcePermissionValueProvider : IResourcePermissionValueProvider, ITransientDependency
+{
+ public abstract string Name { get; }
+
+ protected IResourcePermissionStore ResourcePermissionStore { get; }
+
+ protected ResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore)
+ {
+ ResourcePermissionStore = resourcePermissionStore;
+ }
+
+ public abstract Task CheckAsync(ResourcePermissionValueCheckContext context);
+
+ public abstract Task CheckAsync(ResourcePermissionValuesCheckContext context);
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValuesCheckContext.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValuesCheckContext.cs
new file mode 100644
index 0000000000..239f74fc19
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValuesCheckContext.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class ResourcePermissionValuesCheckContext : PermissionValuesCheckContext
+{
+ public string ResourceName { get; }
+
+ public string ResourceKey { get; }
+
+ public ResourcePermissionValuesCheckContext(PermissionDefinition permission,string resourceName, string resourceKey)
+ : this([permission], null, resourceName, resourceKey)
+ {
+
+ }
+
+
+ public ResourcePermissionValuesCheckContext(PermissionDefinition permission, ClaimsPrincipal? principal, string resourceName, string resourceKey)
+ : this([permission], principal, resourceName, resourceKey)
+ {
+
+ }
+
+ public ResourcePermissionValuesCheckContext(List permissions, string resourceName, string resourceKey)
+ : this(permissions, null, resourceName, resourceKey)
+ {
+ ResourceName = resourceName;
+ ResourceKey = resourceKey;
+ }
+
+ public ResourcePermissionValuesCheckContext(List permissions, ClaimsPrincipal? principal, string resourceName, string resourceKey)
+ : base(permissions, principal)
+ {
+ ResourceName = resourceName;
+ ResourceKey = resourceKey;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/ResourcePermissionRequirement.cs b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/ResourcePermissionRequirement.cs
new file mode 100644
index 0000000000..03658f3e90
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/ResourcePermissionRequirement.cs
@@ -0,0 +1,21 @@
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Volo.Abp.Authorization;
+
+public class ResourcePermissionRequirement : IAuthorizationRequirement
+{
+ public string PermissionName { get; }
+
+ public ResourcePermissionRequirement([NotNull] string permissionName)
+ {
+ Check.NotNull(permissionName, nameof(permissionName));
+
+ PermissionName = permissionName;
+ }
+
+ public override string ToString()
+ {
+ return $"ResourcePermissionRequirement: {PermissionName}";
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/KeyedObjectResourcePermissionExtenstions.cs b/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/KeyedObjectResourcePermissionExtenstions.cs
new file mode 100644
index 0000000000..e65d5174de
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/KeyedObjectResourcePermissionExtenstions.cs
@@ -0,0 +1,13 @@
+using Microsoft.AspNetCore.Authorization;
+using Volo.Abp.Authorization.Permissions.Resources;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+public static class KeyedObjectResourcePermissionExtenstions
+{
+ public static IServiceCollection AddKeyedObjectResourcePermissionAuthorization(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ return services;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs
index d8226bdee5..65b7e1b390 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Authorization.Localization;
using Volo.Abp.Authorization.Permissions;
+using Volo.Abp.Authorization.Permissions.Resources;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity;
@@ -32,6 +33,7 @@ public class AbpAuthorizationModule : AbpModule
{
context.Services.AddAuthorizationCore();
+ context.Services.AddKeyedObjectResourcePermissionAuthorization();
context.Services.AddSingleton();
context.Services.AddSingleton();
@@ -42,6 +44,9 @@ public class AbpAuthorizationModule : AbpModule
options.ValueProviders.Add();
options.ValueProviders.Add();
options.ValueProviders.Add();
+
+ options.ResourceValueProviders.Add();
+ options.ResourceValueProviders.Add();
});
Configure(options =>
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationPolicyProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationPolicyProvider.cs
index 7958f2a979..a5bd19b539 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationPolicyProvider.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationPolicyProvider.cs
@@ -40,6 +40,14 @@ public class AbpAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
return policyBuilder.Build();
}
+ if ((await _permissionDefinitionManager.GetResourcePermissionsAsync()).Any(x => x.Name == policyName))
+ {
+ //TODO: Optimize & Cache!
+ var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty());
+ policyBuilder.Requirements.Add(new ResourcePermissionRequirement(policyName));
+ return policyBuilder.Build();
+ }
+
return null;
}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/MethodInvocationAuthorizationService.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/MethodInvocationAuthorizationService.cs
index 6316e38f52..6a34b21835 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/MethodInvocationAuthorizationService.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/MethodInvocationAuthorizationService.cs
@@ -20,7 +20,7 @@ public class MethodInvocationAuthorizationService : IMethodInvocationAuthorizati
_abpAuthorizationService = abpAuthorizationService;
}
- public async Task CheckAsync(MethodInvocationAuthorizationContext context)
+ public virtual async Task CheckAsync(MethodInvocationAuthorizationContext context)
{
if (AllowAnonymous(context))
{
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IDynamicPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IDynamicPermissionDefinitionStore.cs
index 366ae2e58a..597b274035 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IDynamicPermissionDefinitionStore.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IDynamicPermissionDefinitionStore.cs
@@ -8,6 +8,10 @@ public interface IDynamicPermissionDefinitionStore
Task GetOrNullAsync(string name);
Task> GetPermissionsAsync();
-
+
+ Task GetResourcePermissionOrNullAsync(string resourceName, string name);
+
+ Task> GetResourcePermissionsAsync();
+
Task> GetGroupsAsync();
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IStaticPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IStaticPermissionDefinitionStore.cs
index 4da8423dd3..da4055d771 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IStaticPermissionDefinitionStore.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IStaticPermissionDefinitionStore.cs
@@ -8,6 +8,10 @@ public interface IStaticPermissionDefinitionStore
Task GetOrNullAsync(string name);
Task> GetPermissionsAsync();
-
+
+ Task GetResourcePermissionOrNullAsync(string resourceName, string name);
+
+ Task> GetResourcePermissionsAsync();
+
Task> GetGroupsAsync();
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/NullDynamicPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/NullDynamicPermissionDefinitionStore.cs
index 5b13288153..cf3a9820b4 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/NullDynamicPermissionDefinitionStore.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/NullDynamicPermissionDefinitionStore.cs
@@ -9,10 +9,15 @@ namespace Volo.Abp.Authorization.Permissions;
public class NullDynamicPermissionDefinitionStore : IDynamicPermissionDefinitionStore, ISingletonDependency
{
private readonly static Task CachedPermissionResult = Task.FromResult((PermissionDefinition?)null);
-
+
private readonly static Task> CachedPermissionsResult =
Task.FromResult((IReadOnlyList)Array.Empty().ToImmutableList());
+ private readonly static Task CachedResourcePermissionResult = Task.FromResult((PermissionDefinition?)null);
+
+ private readonly static Task> CachedResourcePermissionsResult =
+ Task.FromResult((IReadOnlyList)Array.Empty().ToImmutableList());
+
private readonly static Task> CachedGroupsResult =
Task.FromResult((IReadOnlyList)Array.Empty().ToImmutableList());
@@ -26,8 +31,18 @@ public class NullDynamicPermissionDefinitionStore : IDynamicPermissionDefinition
return CachedPermissionsResult;
}
+ public Task GetResourcePermissionOrNullAsync(string resourceName, string name)
+ {
+ return CachedResourcePermissionResult;
+ }
+
+ public Task> GetResourcePermissionsAsync()
+ {
+ return CachedResourcePermissionsResult;
+ }
+
public Task> GetGroupsAsync()
{
return CachedGroupsResult;
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs
index 23f9e7883e..3dbc22fb47 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs
@@ -34,17 +34,36 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
{
Check.NotNull(name, nameof(name));
- return await _staticStore.GetOrNullAsync(name) ??
+ return await _staticStore.GetOrNullAsync(name) ??
await _dynamicStore.GetOrNullAsync(name);
}
+ public virtual async Task GetResourcePermissionAsync(string resourceName, string name)
+ {
+ var permission = await GetResourcePermissionOrNullAsync(resourceName, name);
+ if (permission == null)
+ {
+ throw new AbpException($"Undefined resource permission: {name} for resource: {resourceName}");
+ }
+
+ return permission;
+ }
+
+ public virtual async Task GetResourcePermissionOrNullAsync(string resourceName, string name)
+ {
+ Check.NotNull(name, nameof(name));
+
+ return await _staticStore.GetResourcePermissionOrNullAsync(resourceName, name) ??
+ await _dynamicStore.GetResourcePermissionOrNullAsync(resourceName, name);
+ }
+
public virtual async Task> GetPermissionsAsync()
{
var staticPermissions = await _staticStore.GetPermissionsAsync();
var staticPermissionNames = staticPermissions
.Select(p => p.Name)
.ToImmutableHashSet();
-
+
var dynamicPermissions = await _dynamicStore.GetPermissionsAsync();
/* We prefer static permissions over dynamics */
@@ -53,13 +72,28 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
).ToImmutableList();
}
- public async Task> GetGroupsAsync()
+ public virtual async Task> GetResourcePermissionsAsync()
+ {
+ var staticResourcePermissions = await _staticStore.GetResourcePermissionsAsync();
+ var staticResourcePermissionNames = staticResourcePermissions
+ .Select(p => p.Name)
+ .ToImmutableHashSet();
+
+ var dynamicResourcePermissions = await _dynamicStore.GetResourcePermissionsAsync();
+
+ /* We prefer static permissions over dynamics */
+ return staticResourcePermissions.Concat(
+ dynamicResourcePermissions.Where(d => !staticResourcePermissionNames.Contains(d.Name))
+ ).ToImmutableList();
+ }
+
+ public virtual async Task> GetGroupsAsync()
{
var staticGroups = await _staticStore.GetGroupsAsync();
var staticGroupNames = staticGroups
.Select(p => p.Name)
.ToImmutableHashSet();
-
+
var dynamicGroups = await _dynamicStore.GetGroupsAsync();
/* We prefer static groups over dynamics */
@@ -67,4 +101,4 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
dynamicGroups.Where(d => !staticGroupNames.Contains(d.Name))
).ToImmutableList();
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionCheckerExtensions.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionCheckerExtensions.cs
new file mode 100644
index 0000000000..e8b9e38218
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionCheckerExtensions.cs
@@ -0,0 +1,28 @@
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public static class KeyedObjectResourcePermissionCheckerExtensions
+{
+ ///
+ /// Checks if the specified permission is granted for the given resource.
+ ///
+ /// The type of the object.
+ /// The resource permission checker instance.
+ /// The name of the permission to check.
+ /// The resource for which the permission is being checked.
+ /// A task that represents the asynchronous operation. The task result is a boolean indicating whether the permission is granted.
+ public static Task IsGrantedAsync(this IResourcePermissionChecker resourcePermissionChecker, string permissionName, TResource resource)
+ where TResource : class, IKeyedObject
+ {
+ Check.NotNull(resourcePermissionChecker, nameof(resourcePermissionChecker));
+ Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName));
+ Check.NotNull(resource, nameof(resource));
+
+ return resourcePermissionChecker.IsGrantedAsync(
+ permissionName,
+ resource,
+ resource.GetObjectKey() ?? throw new AbpException("The resource doesn't have a key.")
+ );
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionRequirementHandler.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionRequirementHandler.cs
new file mode 100644
index 0000000000..e43b6d07b5
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionRequirementHandler.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class KeyedObjectResourcePermissionRequirementHandler : AuthorizationHandler
+{
+ protected readonly IResourcePermissionChecker PermissionChecker;
+
+ public KeyedObjectResourcePermissionRequirementHandler(IResourcePermissionChecker permissionChecker)
+ {
+ PermissionChecker = permissionChecker;
+ }
+
+ protected override async Task HandleRequirementAsync(
+ AuthorizationHandlerContext context,
+ ResourcePermissionRequirement requirement,
+ IKeyedObject? resource)
+ {
+ if (resource == null)
+ {
+ return;
+ }
+
+ var resourceName = resource.GetType().FullName!;
+ var resourceKey = resource.GetObjectKey() ?? throw new AbpException("The resource doesn't have a key.");
+
+ if (await PermissionChecker.IsGrantedAsync(context.User, requirement.PermissionName, resourceName, resourceKey))
+ {
+ context.Succeed(requirement);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionStoreExtensions.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionStoreExtensions.cs
new file mode 100644
index 0000000000..3088950510
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionStoreExtensions.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public static class KeyedObjectResourcePermissionStoreExtensions
+{
+ ///
+ /// Retrieves an array of granted permissions for a specific entity.
+ ///
+ /// The type of the resource.
+ /// The resource permission store instance.
+ /// The resource for which the permissions are being checked.
+ /// An array of granted permission names as strings.
+ public static async Task GetGrantedPermissionsAsync(
+ this IResourcePermissionStore resourcePermissionStore,
+ TResource resource
+ )
+ where TResource : class, IKeyedObject
+ {
+ Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
+ Check.NotNull(resource, nameof(resource));
+
+ return (await GetPermissionsAsync(resourcePermissionStore, resource)).Where(x => x.Value).Select(x => x.Key).ToArray();
+ }
+
+ ///
+ /// Retrieves a dictionary of permissions and their granted status for the specified entity.
+ ///
+ /// The type of the entity.
+ /// The resource permission store instance.
+ /// The entity for which the permissions are being retrieved.
+ /// A dictionary where the keys are permission names and the values are booleans indicating whether the permission is granted.
+ public static async Task> GetPermissionsAsync(
+ this IResourcePermissionStore resourcePermissionStore,
+ TEntity entity
+ )
+ where TEntity : class, IKeyedObject
+ {
+ Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
+ Check.NotNull(entity, nameof(entity));
+
+ return await resourcePermissionStore.GetPermissionsAsync(
+ entity,
+ entity.GetObjectKey() ?? throw new AbpException("The entity doesn't have a key.")
+ );
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs
new file mode 100644
index 0000000000..a920c2e464
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs
@@ -0,0 +1,173 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Security.Principal;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Security.Claims;
+using Volo.Abp.SimpleStateChecking;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientDependency
+{
+ protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
+ protected ICurrentPrincipalAccessor PrincipalAccessor { get; }
+ protected ICurrentTenant CurrentTenant { get; }
+ protected IResourcePermissionValueProviderManager PermissionValueProviderManager { get; }
+ protected ISimpleStateCheckerManager StateCheckerManager { get; }
+ protected IPermissionChecker PermissionChecker { get; }
+
+ public ResourcePermissionChecker(
+ ICurrentPrincipalAccessor principalAccessor,
+ IPermissionDefinitionManager permissionDefinitionManager,
+ ICurrentTenant currentTenant,
+ IResourcePermissionValueProviderManager permissionValueProviderManager,
+ ISimpleStateCheckerManager stateCheckerManager,
+ IPermissionChecker permissionChecker)
+ {
+ PrincipalAccessor = principalAccessor;
+ PermissionDefinitionManager = permissionDefinitionManager;
+ CurrentTenant = currentTenant;
+ PermissionValueProviderManager = permissionValueProviderManager;
+ StateCheckerManager = stateCheckerManager;
+ PermissionChecker = permissionChecker;
+ }
+
+ public virtual async Task IsGrantedAsync(string name, string resourceName, string resourceKey)
+ {
+ return await IsGrantedAsync(PrincipalAccessor.Principal, name, resourceName, resourceKey);
+ }
+
+ public virtual async Task IsGrantedAsync(
+ ClaimsPrincipal? claimsPrincipal,
+ string name,
+ string resourceName,
+ string resourceKey)
+ {
+ Check.NotNull(name, nameof(name));
+
+ var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name);
+ if (permission == null)
+ {
+ return false;
+ }
+
+ if (!permission.IsEnabled)
+ {
+ return false;
+ }
+
+ if (!await StateCheckerManager.IsEnabledAsync(permission))
+ {
+ return false;
+ }
+
+ var multiTenancySide = claimsPrincipal?.GetMultiTenancySide()
+ ?? CurrentTenant.GetMultiTenancySide();
+
+ if (!permission.MultiTenancySide.HasFlag(multiTenancySide))
+ {
+ return false;
+ }
+
+ var isGranted = false;
+ var context = new ResourcePermissionValueCheckContext(permission, claimsPrincipal, resourceName, resourceKey);
+ foreach (var provider in PermissionValueProviderManager.ValueProviders)
+ {
+ if (context.Permission.Providers.Any() &&
+ !context.Permission.Providers.Contains(provider.Name))
+ {
+ continue;
+ }
+
+ var result = await provider.CheckAsync(context);
+
+ if (result == PermissionGrantResult.Granted)
+ {
+ isGranted = true;
+ }
+ else if (result == PermissionGrantResult.Prohibited)
+ {
+ return false;
+ }
+ }
+
+ return isGranted;
+ }
+
+ public async Task IsGrantedAsync(string[] names, string resourceName, string resourceKey)
+ {
+ return await IsGrantedAsync(PrincipalAccessor.Principal, names, resourceName, resourceKey);
+ }
+
+ public async Task IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string[] names, string resourceName, string resourceKey)
+ {
+ Check.NotNull(names, nameof(names));
+
+ var result = new MultiplePermissionGrantResult();
+ if (!names.Any())
+ {
+ return result;
+ }
+
+ var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ??
+ CurrentTenant.GetMultiTenancySide();
+
+ var permissionDefinitions = new List();
+ foreach (var name in names)
+ {
+ var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name);
+ if (permission == null)
+ {
+ result.Result.Add(name, PermissionGrantResult.Prohibited);
+ continue;
+ }
+
+ result.Result.Add(name, PermissionGrantResult.Undefined);
+
+ if (permission.IsEnabled &&
+ await StateCheckerManager.IsEnabledAsync(permission) &&
+ permission.MultiTenancySide.HasFlag(multiTenancySide))
+ {
+ permissionDefinitions.Add(permission);
+ }
+ }
+
+ foreach (var provider in PermissionValueProviderManager.ValueProviders)
+ {
+ var permissions = permissionDefinitions
+ .Where(x => !x.Providers.Any() || x.Providers.Contains(provider.Name))
+ .ToList();
+
+ if (permissions.IsNullOrEmpty())
+ {
+ continue;
+ }
+
+ var context = new ResourcePermissionValuesCheckContext(
+ permissions,
+ claimsPrincipal,
+ resourceName,
+ resourceKey);
+
+ var multipleResult = await provider.CheckAsync(context);
+ foreach (var grantResult in multipleResult.Result.Where(grantResult =>
+ result.Result.ContainsKey(grantResult.Key) &&
+ result.Result[grantResult.Key] == PermissionGrantResult.Undefined &&
+ grantResult.Value != PermissionGrantResult.Undefined))
+ {
+ result.Result[grantResult.Key] = grantResult.Value;
+ permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key);
+ }
+
+ if (result.AllGranted || result.AllProhibited)
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionPopulator.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionPopulator.cs
new file mode 100644
index 0000000000..91dfb509d4
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionPopulator.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class ResourcePermissionPopulator : ITransientDependency
+{
+ protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
+ protected IResourcePermissionChecker ResourcePermissionChecker { get; }
+ protected IResourcePermissionStore ResourcePermissionStore { get; }
+ protected IPermissionChecker PermissionChecker { get; }
+
+ public ResourcePermissionPopulator(
+ IPermissionDefinitionManager permissionDefinitionManager,
+ IResourcePermissionChecker resourcePermissionChecker,
+ IResourcePermissionStore resourcePermissionStore,
+ IPermissionChecker permissionChecker)
+ {
+ PermissionDefinitionManager = permissionDefinitionManager;
+ ResourcePermissionChecker = resourcePermissionChecker;
+ ResourcePermissionStore = resourcePermissionStore;
+ PermissionChecker = permissionChecker;
+ }
+
+ public virtual async Task PopulateAsync(TResource resource, string resourceName)
+ where TResource : IHasResourcePermissions
+ {
+ await PopulateAsync([resource], resourceName);
+ }
+
+ public virtual async Task PopulateAsync(List resources, string resourceName)
+ where TResource : IHasResourcePermissions
+ {
+ Check.NotNull(resources, nameof(resources));
+ Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName));
+
+ var resopurcePermissions = (await PermissionDefinitionManager.GetResourcePermissionsAsync())
+ .Where(x => x.ResourceName == resourceName)
+ .ToArray();
+
+ foreach (var resource in resources)
+ {
+ var resourceKey = resource.GetObjectKey();
+ if (resourceKey.IsNullOrEmpty())
+ {
+ throw new AbpException("Resource key can not be null or empty.");
+ }
+
+ var results = await ResourcePermissionChecker.IsGrantedAsync(resopurcePermissions.Select(x => x.Name).ToArray(), resourceName, resourceKey);
+ foreach (var resopurcePermission in resopurcePermissions)
+ {
+ if (resource.ResourcePermissions == null)
+ {
+ ObjectHelper.TrySetProperty(resource, x => x.ResourcePermissions, () => new Dictionary());
+ }
+
+ var hasPermission = results.Result.TryGetValue(resopurcePermission.Name, out var granted) && granted == PermissionGrantResult.Granted;
+ resource.ResourcePermissions![resopurcePermission.Name] = hasPermission;
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProviderManager.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProviderManager.cs
new file mode 100644
index 0000000000..bab3180fea
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProviderManager.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class ResourcePermissionValueProviderManager : IResourcePermissionValueProviderManager, ISingletonDependency
+{
+ public IReadOnlyList ValueProviders => _lazyProviders.Value;
+ private readonly Lazy> _lazyProviders;
+
+ protected AbpPermissionOptions Options { get; }
+ protected IServiceProvider ServiceProvider { get; }
+
+ public ResourcePermissionValueProviderManager(
+ IServiceProvider serviceProvider,
+ IOptions options)
+ {
+ Options = options.Value;
+ ServiceProvider = serviceProvider;
+
+ _lazyProviders = new Lazy>(GetProviders, true);
+ }
+
+ protected virtual List GetProviders()
+ {
+ var providers = Options
+ .ResourceValueProviders
+ .Select(type => (ServiceProvider.GetRequiredService(type) as IResourcePermissionValueProvider)!)
+ .ToList();
+
+ var multipleProviders = providers.GroupBy(p => p.Name).FirstOrDefault(x => x.Count() > 1);
+ if(multipleProviders != null)
+ {
+ throw new AbpException($"Duplicate resource permission value provider name detected: {multipleProviders.Key}. Providers:{Environment.NewLine}{multipleProviders.Select(p => p.GetType().FullName!).JoinAsString(Environment.NewLine)}");
+ }
+
+ return providers;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/RoleResourcePermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/RoleResourcePermissionValueProvider.cs
new file mode 100644
index 0000000000..fc8e91e794
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/RoleResourcePermissionValueProvider.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Security.Claims;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class RoleResourcePermissionValueProvider : ResourcePermissionValueProvider
+{
+ public const string ProviderName = "R";
+
+ public override string Name => ProviderName;
+
+ public RoleResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore)
+ : base(resourcePermissionStore)
+ {
+
+ }
+
+ public override async Task CheckAsync(ResourcePermissionValueCheckContext context)
+ {
+ var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray();
+
+ if (roles == null || !roles.Any())
+ {
+ return PermissionGrantResult.Undefined;
+ }
+
+ foreach (var role in roles.Distinct())
+ {
+ if (await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, role))
+ {
+ return PermissionGrantResult.Granted;
+ }
+ }
+
+ return PermissionGrantResult.Undefined;
+ }
+
+ public override async Task CheckAsync(ResourcePermissionValuesCheckContext context)
+ {
+ var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToList();
+ Check.NotNullOrEmpty(permissionNames, nameof(permissionNames));
+
+ var result = new MultiplePermissionGrantResult(permissionNames.ToArray());
+
+ var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray();
+ if (roles == null || !roles.Any())
+ {
+ return result;
+ }
+
+ foreach (var role in roles.Distinct())
+ {
+ var multipleResult = await ResourcePermissionStore.IsGrantedAsync(permissionNames.ToArray(), context.ResourceName, context.ResourceKey, Name, role);
+
+ foreach (var grantResult in multipleResult.Result.Where(grantResult =>
+ result.Result.ContainsKey(grantResult.Key) &&
+ result.Result[grantResult.Key] == PermissionGrantResult.Undefined &&
+ grantResult.Value != PermissionGrantResult.Undefined))
+ {
+ result.Result[grantResult.Key] = grantResult.Value;
+ permissionNames.RemoveAll(x => x == grantResult.Key);
+ }
+
+ if (result.AllGranted || result.AllProhibited)
+ {
+ break;
+ }
+
+ if (permissionNames.IsNullOrEmpty())
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/UserResourcePermissionValueProvider.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/UserResourcePermissionValueProvider.cs
new file mode 100644
index 0000000000..8cfcb8ca6f
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/UserResourcePermissionValueProvider.cs
@@ -0,0 +1,46 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Security.Claims;
+
+namespace Volo.Abp.Authorization.Permissions.Resources;
+
+public class UserResourcePermissionValueProvider : ResourcePermissionValueProvider
+{
+ public const string ProviderName = "U";
+
+ public override string Name => ProviderName;
+
+ public UserResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore)
+ : base(resourcePermissionStore)
+ {
+
+ }
+
+ public override async Task CheckAsync(ResourcePermissionValueCheckContext context)
+ {
+ var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
+
+ if (userId == null)
+ {
+ return PermissionGrantResult.Undefined;
+ }
+
+ return await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, userId)
+ ? PermissionGrantResult.Granted
+ : PermissionGrantResult.Undefined;
+ }
+
+ public override async Task CheckAsync(ResourcePermissionValuesCheckContext context)
+ {
+ var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToArray();
+ Check.NotNullOrEmpty(permissionNames, nameof(permissionNames));
+
+ var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
+ if (userId == null)
+ {
+ return new MultiplePermissionGrantResult(permissionNames);
+ }
+
+ return await ResourcePermissionStore.IsGrantedAsync(permissionNames, context.ResourceName, context.ResourceKey, Name, userId);
+ }
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs
new file mode 100644
index 0000000000..6bc7b5f165
--- /dev/null
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Volo.Abp.Authorization.Permissions;
+
+[Serializable]
+public class StaticPermissionDefinitionChangedEvent
+{
+
+}
diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
index 4e6ff0d11c..2f95f3fef6 100644
--- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
+++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
@@ -6,74 +6,72 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.StaticDefinitions;
namespace Volo.Abp.Authorization.Permissions;
public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore, ISingletonDependency
{
- protected IDictionary PermissionGroupDefinitions => _lazyPermissionGroupDefinitions.Value;
- private readonly Lazy> _lazyPermissionGroupDefinitions;
-
- protected IDictionary PermissionDefinitions => _lazyPermissionDefinitions.Value;
- private readonly Lazy> _lazyPermissionDefinitions;
-
+ protected IServiceProvider ServiceProvider { get; }
protected AbpPermissionOptions Options { get; }
-
- private readonly IServiceProvider _serviceProvider;
+ protected IStaticDefinitionCache, List)> GroupCache { get; }
+ protected IStaticDefinitionCache> DefinitionCache { get; }
public StaticPermissionDefinitionStore(
IServiceProvider serviceProvider,
- IOptions options)
+ IOptions options,
+ IStaticDefinitionCache, List)> groupCache,
+ IStaticDefinitionCache> definitionCache)
{
- _serviceProvider = serviceProvider;
+ ServiceProvider = serviceProvider;
Options = options.Value;
+ GroupCache = groupCache;
+ DefinitionCache = definitionCache;
+ }
- _lazyPermissionDefinitions = new Lazy>(
- CreatePermissionDefinitions,
- isThreadSafe: true
- );
-
- _lazyPermissionGroupDefinitions = new Lazy>(
- CreatePermissionGroupDefinitions,
- isThreadSafe: true
- );
+ public async Task GetOrNullAsync(string name)
+ {
+ var defs = await GetPermissionDefinitionsAsync();
+ return defs.GetOrDefault(name);
}
-
- protected virtual Dictionary CreatePermissionDefinitions()
+
+ public virtual async Task> GetPermissionsAsync()
{
- var permissions = new Dictionary();
+ var defs = await GetPermissionDefinitionsAsync();
+ return defs.Values.ToImmutableList();
+ }
- foreach (var groupDefinition in PermissionGroupDefinitions.Values)
- {
- foreach (var permission in groupDefinition.Permissions)
- {
- AddPermissionToDictionaryRecursively(permissions, permission);
- }
- }
+ public virtual async Task GetResourcePermissionOrNullAsync(string resourceName, string name)
+ {
+ var (_, resourcePermissions) = await GetPermissionGroupDefinitionsAsync();
+ return resourcePermissions.FirstOrDefault(p => p.ResourceName == resourceName && p.Name == name);
+ }
- return permissions;
+ public virtual async Task> GetResourcePermissionsAsync()
+ {
+ var (_, resourcePermissions) = await GetPermissionGroupDefinitionsAsync();
+ return resourcePermissions.ToImmutableList();
}
- protected virtual void AddPermissionToDictionaryRecursively(
- Dictionary permissions,
- PermissionDefinition permission)
+ public async Task> GetGroupsAsync()
{
- if (permissions.ContainsKey(permission.Name))
- {
- throw new AbpException("Duplicate permission name: " + permission.Name);
- }
+ var (groups, _) = await GetPermissionGroupDefinitionsAsync();
+ return groups.Values.ToImmutableList();
+ }
- permissions[permission.Name] = permission;
+ protected virtual async Task<(Dictionary, List)> GetPermissionGroupDefinitionsAsync()
+ {
+ return await GroupCache.GetOrCreateAsync(CreatePermissionGroupDefinitionsAsync);
+ }
- foreach (var child in permission.Children)
- {
- AddPermissionToDictionaryRecursively(permissions, child);
- }
+ protected virtual async Task> GetPermissionDefinitionsAsync()
+ {
+ return await DefinitionCache.GetOrCreateAsync(CreatePermissionDefinitionsAsync);
}
- protected virtual Dictionary CreatePermissionGroupDefinitions()
+ protected virtual Task<(Dictionary, List)> CreatePermissionGroupDefinitionsAsync()
{
- using (var scope = _serviceProvider.CreateScope())
+ using (var scope = ServiceProvider.CreateScope())
{
var context = new PermissionDefinitionContext(scope.ServiceProvider);
@@ -99,29 +97,43 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
context.CurrentProvider = provider;
provider.PostDefine(context);
}
-
+
context.CurrentProvider = null;
- return context.Groups;
+ return Task.FromResult((context.Groups, context.ResourcePermissions));
}
}
- public Task GetOrNullAsync(string name)
- {
- return Task.FromResult(PermissionDefinitions.GetOrDefault(name));
- }
-
- public virtual Task> GetPermissionsAsync()
+ protected virtual async Task> CreatePermissionDefinitionsAsync()
{
- return Task.FromResult>(
- PermissionDefinitions.Values.ToImmutableList()
- );
+ var permissions = new Dictionary();
+
+ var (groups, _) = await GetPermissionGroupDefinitionsAsync();
+ foreach (var groupDefinition in groups.Values)
+ {
+ foreach (var permission in groupDefinition.Permissions)
+ {
+ AddPermissionToDictionaryRecursively(permissions, permission);
+ }
+ }
+
+ return permissions;
}
- public Task> GetGroupsAsync()
+ protected virtual void AddPermissionToDictionaryRecursively(
+ Dictionary permissions,
+ PermissionDefinition permission)
{
- return Task.FromResult>(
- PermissionGroupDefinitions.Values.ToImmutableList()
- );
+ if (permissions.ContainsKey(permission.Name))
+ {
+ throw new AbpException("Duplicate permission name: " + permission.Name);
+ }
+
+ permissions[permission.Name] = permission;
+
+ foreach (var child in permission.Children)
+ {
+ AddPermissionToDictionaryRecursively(permissions, child);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs
index 9a39acd4fc..0180833054 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs
+++ b/framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs
@@ -23,7 +23,16 @@ public partial class DataGridEntityActionsColumn : DataGridColumn
Caption = UiLocalizer["Actions"];
Width = "150px";
Sortable = false;
- Field = typeof(TItem).GetProperties().First().Name;
+ Field = ResolveFieldName();
+
return ValueTask.CompletedTask;
}
+
+ protected virtual string ResolveFieldName()
+ {
+ var props = typeof(TItem).GetProperties();
+ return props.Length > 0
+ ? props[0].Name
+ : "Id";
+ }
}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
index 7b249f0517..8ff8ad3206 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
@@ -1,5 +1,8 @@
+using System;
+using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Cli.Commands.Internal;
using Volo.Abp.Cli.Http;
@@ -7,9 +10,12 @@ using Volo.Abp.Cli.ServiceProxying;
using Volo.Abp.Cli.ServiceProxying.Angular;
using Volo.Abp.Cli.ServiceProxying.CSharp;
using Volo.Abp.Cli.ServiceProxying.JavaScript;
+using Volo.Abp.Cli.Telemetry;
using Volo.Abp.Domain;
using Volo.Abp.Http;
using Volo.Abp.IdentityModel;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Activity.Providers;
using Volo.Abp.Json;
using Volo.Abp.Localization;
using Volo.Abp.Minify;
@@ -27,6 +33,8 @@ namespace Volo.Abp.Cli;
)]
public class AbpCliCoreModule : AbpModule
{
+ private const string EnableTelemetryVariableName = "ABP_STUDIO_ENABLE_TELEMETRY";
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClient(CliConsts.HttpClientName)
@@ -82,5 +90,23 @@ public class AbpCliCoreModule : AbpModule
options.Generators[AngularServiceProxyGenerator.Name] = typeof(AngularServiceProxyGenerator);
options.Generators[CSharpServiceProxyGenerator.Name] = typeof(CSharpServiceProxyGenerator);
});
+
+ ConfigureTelemetry(context.Services);
+ }
+
+ private static void ConfigureTelemetry(IServiceCollection services)
+ {
+ var enableTelemetryEnvironmentVariable = Environment.GetEnvironmentVariable(EnableTelemetryVariableName , EnvironmentVariableTarget.Machine)
+ ?? Environment.GetEnvironmentVariable(EnableTelemetryVariableName , EnvironmentVariableTarget.User)
+ ?? Environment.GetEnvironmentVariable(EnableTelemetryVariableName , EnvironmentVariableTarget.Process);
+
+ if (enableTelemetryEnvironmentVariable.IsNullOrEmpty() || !enableTelemetryEnvironmentVariable.Equals("false", StringComparison.InvariantCultureIgnoreCase))
+ {
+ services.Remove(services.First(p => p.ImplementationType == typeof(TelemetrySessionInfoEnricher)));
+ }
+ else
+ {
+ services.Replace(ServiceDescriptor.Singleton());
+ }
}
}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
index 0a0c46b6a4..063ceddebf 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
@@ -14,12 +14,15 @@ using Volo.Abp.Cli.Memory;
using Volo.Abp.Cli.Version;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli;
public class CliService : ITransientDependency
{
private readonly MemoryService _memoryService;
+ private readonly ITelemetryService _telemetryService;
public ILogger Logger { get; set; }
protected ICommandLineArgumentParser CommandLineArgumentParser { get; }
protected ICommandSelector CommandSelector { get; }
@@ -35,7 +38,8 @@ public class CliService : ITransientDependency
PackageVersionCheckerService nugetService,
ICmdHelper cmdHelper,
MemoryService memoryService,
- CliVersionService cliVersionService)
+ CliVersionService cliVersionService,
+ ITelemetryService telemetryService)
{
_memoryService = memoryService;
CommandLineArgumentParser = commandLineArgumentParser;
@@ -44,6 +48,7 @@ public class CliService : ITransientDependency
PackageVersionCheckerService = nugetService;
CmdHelper = cmdHelper;
CliVersionService = cliVersionService;
+ _telemetryService = telemetryService;
Logger = NullLogger.Instance;
}
@@ -64,6 +69,7 @@ public class CliService : ITransientDependency
try
{
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliRun);
if (commandLineArgs.IsCommand("prompt"))
{
await RunPromptAsync();
@@ -84,9 +90,14 @@ public class CliService : ITransientDependency
}
catch (Exception ex)
{
+ await _telemetryService.AddErrorActivityAsync(ex.Message);
Logger.LogException(ex);
throw;
}
+ finally
+ {
+ await _telemetryService.AddActivityAsync(ActivityNameConsts.AbpCliExit);
+ }
}
private async Task RunPromptAsync()
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs
index 4756c3f99b..38483517da 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs
@@ -11,6 +11,8 @@ using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@@ -20,6 +22,8 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency
public const string Name = "add-module";
private AddModuleInfoOutput _lastAddedModuleInfo;
+ private readonly ITelemetryService _telemetryService;
+
public ILogger Logger { get; set; }
protected SolutionModuleAdder SolutionModuleAdder { get; }
@@ -39,11 +43,13 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency
public AddModuleCommand(
SolutionModuleAdder solutionModuleAdder,
SolutionPackageVersionFinder solutionPackageVersionFinder,
- IOptions options)
+ IOptions options,
+ ITelemetryService telemetryService)
{
_options = options.Value;
SolutionModuleAdder = solutionModuleAdder;
SolutionPackageVersionFinder = solutionPackageVersionFinder;
+ _telemetryService = telemetryService;
Logger = NullLogger.Instance;
}
@@ -66,6 +72,11 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency
}
var newTemplate = commandLineArgs.Options.ContainsKey(Options.NewTemplate.Long);
+
+ await using var _ = _telemetryService.TrackActivityAsync(newTemplate
+ ? ActivityNameConsts.AbpCliCommandsInstallLocalModule
+ : ActivityNameConsts.AbpCliCommandsInstallModule);
+
var template = commandLineArgs.Options.GetOrNull(Options.Template.Short, Options.Template.Long);
var newProTemplate = !string.IsNullOrEmpty(template) && template == ModuleProTemplate.TemplateName;
var withSourceCode = newTemplate || newProTemplate || commandLineArgs.Options.ContainsKey(Options.SourceCode.Long);
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs
index e9acb7ae24..f397ef8d6e 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs
@@ -9,6 +9,8 @@ using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@@ -16,16 +18,19 @@ public class AddPackageCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "add-package";
+ private readonly ITelemetryService _telemetryService;
+
public ILogger Logger { get; set; }
protected ProjectNugetPackageAdder ProjectNugetPackageAdder { get; }
public ProjectNpmPackageAdder ProjectNpmPackageAdder { get; }
- public AddPackageCommand(ProjectNugetPackageAdder projectNugetPackageAdder, ProjectNpmPackageAdder projectNpmPackageAdder)
+ public AddPackageCommand(ProjectNugetPackageAdder projectNugetPackageAdder, ProjectNpmPackageAdder projectNpmPackageAdder, ITelemetryService telemetryService)
{
ProjectNugetPackageAdder = projectNugetPackageAdder;
ProjectNpmPackageAdder = projectNpmPackageAdder;
+ _telemetryService = telemetryService;
Logger = NullLogger.Instance;
}
@@ -39,6 +44,9 @@ public class AddPackageCommand : IConsoleCommand, ITransientDependency
GetUsageInfo()
);
}
+
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsNewPackage);
+ await using var __ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsAddPackage);
var isNpmPackage = false;
var isNugetPackage = true;
@@ -51,11 +59,11 @@ public class AddPackageCommand : IConsoleCommand, ITransientDependency
var version = commandLineArgs.Options.GetOrNull(Options.Version.Short, Options.Version.Long);
var withSourceCode = commandLineArgs.Options.ContainsKey(Options.SourceCode.Long);
-
+
if (isNugetPackage)
{
var addSourceCodeToSolutionFile = withSourceCode && commandLineArgs.Options.ContainsKey("add-to-solution-file");
-
+
await ProjectNugetPackageAdder.AddAsync(
GetSolutionFile(commandLineArgs),
GetProjectFile(commandLineArgs),
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs
index 092e996095..661cf7995b 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs
@@ -8,6 +8,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@@ -16,17 +18,21 @@ public class CleanCommand : IConsoleCommand, ITransientDependency
public const string Name = "clean";
public ILogger Logger { get; set; }
-
+
protected ICmdHelper CmdHelper { get; }
+
+ private readonly ITelemetryService _telemetryService;
- public CleanCommand(ICmdHelper cmdHelper)
+ public CleanCommand(ICmdHelper cmdHelper, ITelemetryService telemetryService)
{
CmdHelper = cmdHelper;
+ _telemetryService = telemetryService;
Logger = NullLogger.Instance;
}
- public Task ExecuteAsync(CommandLineArgs commandLineArgs)
+ public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsClean);
var binEntries = Directory.EnumerateDirectories(Directory.GetCurrentDirectory(), "bin", SearchOption.AllDirectories);
var objEntries = Directory.EnumerateDirectories(Directory.GetCurrentDirectory(), "obj", SearchOption.AllDirectories);
@@ -49,7 +55,6 @@ public class CleanCommand : IConsoleCommand, ITransientDependency
Logger.LogInformation($"'bin' and 'obj' folders removed successfully!");
Logger.LogInformation("Solution cleaned successfully!");
- return Task.CompletedTask;
}
public string GetUsageInfo()
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs
index 857edf6bd7..2b65b9d00b 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs
@@ -47,6 +47,7 @@ public class RecreateInitialMigrationCommand : IConsoleCommand, ITransientDepend
Directory.Delete(Path.Combine(projectDir, "Migrations"), true);
}
+ CmdHelper.RunCmd($"dotnet build", workingDirectory: projectDir);
var separateDbContext = false;
if (Directory.Exists(Path.Combine(projectDir, "TenantMigrations")))
{
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs
index af685102d8..8a9d4fd6d1 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs
@@ -7,6 +7,8 @@ using System.Threading.Tasks;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@@ -17,15 +19,19 @@ public class ListModulesCommand : IConsoleCommand, ITransientDependency
public ModuleInfoProvider ModuleInfoProvider { get; }
public ILogger Logger { get; set; }
+ private readonly ITelemetryService _telemetryService;
- public ListModulesCommand(ModuleInfoProvider moduleInfoProvider)
+ public ListModulesCommand(ModuleInfoProvider moduleInfoProvider, ITelemetryService telemetryService)
{
ModuleInfoProvider = moduleInfoProvider;
+ _telemetryService = telemetryService;
Logger = NullLogger.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsListModules);
+
var modules = await ModuleInfoProvider.GetModuleListAsync();
var freeModules = modules.Where(m => !m.IsPro).ToList();
var proModules = modules.Where(m => m.IsPro).ToList();
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
index 796a8ff757..149b477d5b 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
@@ -11,11 +11,15 @@ using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectBuilding.Events;
+using Volo.Abp.Cli.ProjectBuilding.Templates.Module;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.Cli.Version;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
namespace Volo.Abp.Cli.Commands;
@@ -26,6 +30,8 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
public ITemplateInfoProvider TemplateInfoProvider { get; }
+ private readonly ITelemetryService _telemetryService;
+
public NewCommand(
ConnectionStringProvider connectionStringProvider,
SolutionPackageVersionFinder solutionPackageVersionFinder,
@@ -40,7 +46,8 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
ITemplateInfoProvider templateInfoProvider,
TemplateProjectBuilder templateProjectBuilder,
AngularThemeConfigurer angularThemeConfigurer,
- CliVersionService cliVersionService) :
+ CliVersionService cliVersionService,
+ ITelemetryService telemetryService) :
base(connectionStringProvider,
solutionPackageVersionFinder,
cmdHelper,
@@ -56,6 +63,7 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
{
TemplateInfoProvider = templateInfoProvider;
TemplateProjectBuilder = templateProjectBuilder;
+ _telemetryService = telemetryService;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
@@ -94,6 +102,19 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
var result = await TemplateProjectBuilder.BuildAsync(
projectArgs
);
+
+ var activityName = ActivityNameConsts.AbpCliCommandsNewSolution;
+
+ if (ModuleTemplateBase.IsModuleTemplate(template))
+ {
+ activityName = ActivityNameConsts.AbpCliCommandsNewModule;
+ }
+
+ await _telemetryService.AddActivityAsync(activityName, o =>
+ {
+ o[ActivityPropertyNames.CreationTool] = AbpTool.OldCli;
+ o[ActivityPropertyNames.Template] = template;
+ });
ExtractProjectZip(result, projectArgs.OutputFolder);
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
index ae3cda8723..68ab346064 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
@@ -9,6 +9,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@@ -20,18 +22,21 @@ public class UpdateCommand : IConsoleCommand, ITransientDependency
private readonly VoloNugetPackagesVersionUpdater _nugetPackagesVersionUpdater;
private readonly NpmPackagesUpdater _npmPackagesUpdater;
+ private readonly ITelemetryService _telemetryService;
public UpdateCommand(VoloNugetPackagesVersionUpdater nugetPackagesVersionUpdater,
- NpmPackagesUpdater npmPackagesUpdater)
+ NpmPackagesUpdater npmPackagesUpdater, ITelemetryService telemetryService)
{
_nugetPackagesVersionUpdater = nugetPackagesVersionUpdater;
_npmPackagesUpdater = npmPackagesUpdater;
+ _telemetryService = telemetryService;
Logger = NullLogger.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsUpdate);
var updateNpm = commandLineArgs.Options.ContainsKey(Options.Packages.Npm);
var updateNuget = commandLineArgs.Options.ContainsKey(Options.Packages.NuGet);
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs
index 67debab2eb..4188a75b68 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
@@ -12,9 +13,9 @@ namespace Volo.Abp.Cli.ProjectModification;
public class LocalReferenceConverter : ITransientDependency
{
-
+
public ILogger Logger { get; set; }
-
+
public async Task ConvertAsync(
[NotNull] string directory,
[NotNull] List localPaths)
@@ -26,14 +27,14 @@ public class LocalReferenceConverter : ITransientDependency
var targetProjects = Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories);
Logger.LogInformation($"Converting projects to local reference.");
-
+
foreach (var targetProject in targetProjects)
{
Logger.LogInformation($"Converting to local reference: {targetProject}");
-
+
await ConvertProjectToLocalReferences(targetProject, localProjects);
}
-
+
Logger.LogInformation($"Converted {targetProjects.Length} projects to local references.");
}
@@ -41,14 +42,14 @@ public class LocalReferenceConverter : ITransientDependency
{
var xmlDocument = new XmlDocument() { PreserveWhitespace = true };
xmlDocument.Load(GenerateStreamFromString(File.ReadAllText(targetProject)));
-
+
var matchedNodes = xmlDocument.SelectNodes($"/Project/ItemGroup/PackageReference[@Include]");
if (matchedNodes == null || matchedNodes.Count == 0)
{
return;
}
-
+
foreach (XmlNode matchedNode in matchedNodes)
{
var packageName = matchedNode!.Attributes!["Include"].Value;
@@ -62,7 +63,7 @@ public class LocalReferenceConverter : ITransientDependency
{
continue;
}
-
+
var parentNode = matchedNode.ParentNode;
parentNode!.RemoveChild(matchedNode);
@@ -72,10 +73,10 @@ public class LocalReferenceConverter : ITransientDependency
newNode.Attributes.Append(includeAttr);
parentNode.AppendChild(newNode);
}
-
+
File.WriteAllText(targetProject, XDocument.Parse(xmlDocument.OuterXml).ToString());
}
-
+
private string CalculateRelativePath(string targetProject, string localProject)
{
return new Uri(targetProject).MakeRelativeUri(new Uri(localProject)).ToString();
@@ -91,8 +92,12 @@ public class LocalReferenceConverter : ITransientDependency
{
continue;
}
-
- list.AddRange(Directory.GetFiles(localPath, "*.csproj", SearchOption.AllDirectories));
+
+ var ignoreFolders = new[] { "bin", "obj", ".vs", ".idea", ".vscode", ".git" };
+ var csprojFiles = Directory.GetFiles(localPath, "*.csproj", SearchOption.AllDirectories)
+ .Where(x => !ignoreFolders.Any(x.Contains))
+ .ToList();
+ list.AddRange(csprojFiles);
}
return list;
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Telemetry/NullTelemetryService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Telemetry/NullTelemetryService.cs
new file mode 100644
index 0000000000..4f3c695f7d
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Telemetry/NullTelemetryService.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Activity;
+
+namespace Volo.Abp.Cli.Telemetry;
+
+public class NullTelemetryService : ITelemetryService
+{
+ public IAsyncDisposable TrackActivity(ActivityEvent activityData)
+ {
+ return NullAsyncDisposable.Instance;
+ }
+ public IAsyncDisposable TrackActivityAsync(string activityName, Action>? additionalProperties = null)
+ {
+ return NullAsyncDisposable.Instance;
+ }
+
+ public Task AddActivityAsync(string activityName, Action>? additionalProperties = null)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task AddErrorActivityAsync(Action> additionalProperties)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task AddErrorActivityAsync(string errorMessage)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task AddErrorForActivityAsync(string failingActivity, string errorMessage)
+ {
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ProjectNameValidator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ProjectNameValidator.cs
index 0f2b287d67..5814ac4a71 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ProjectNameValidator.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ProjectNameValidator.cs
@@ -18,8 +18,9 @@ public static class ProjectNameValidator
private static readonly string[] IllegalKeywords = new[]
{
+ "MauiBlazor",
"Blazor"
- };
+ };
private static void ValidateParentDirectoryString(string projectName)
{
diff --git a/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Telemetry/TelemetryCliSessionProvider.cs b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Telemetry/TelemetryCliSessionProvider.cs
new file mode 100644
index 0000000000..17c340febd
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Telemetry/TelemetryCliSessionProvider.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry.Activity;
+using Volo.Abp.Internal.Telemetry.Activity.Contracts;
+using Volo.Abp.Internal.Telemetry.Activity.Providers;
+using Volo.Abp.Internal.Telemetry.Constants;
+using Volo.Abp.Internal.Telemetry.Constants.Enums;
+
+namespace Volo.Abp.Cli.Telemetry;
+
+[ExposeServices(typeof(ITelemetryActivityEventEnricher))]
+public class TelemetryCliSessionProvider : TelemetryActivityEventEnricher
+{
+ public TelemetryCliSessionProvider(IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ }
+
+ public override int ExecutionOrder { get; set; } = 10;
+
+ protected override Task ExecuteAsync(ActivityContext context)
+ {
+ context.Current[ActivityPropertyNames.SessionType] = SessionType.AbpCli;
+ context.Current[ActivityPropertyNames.SessionId] = Guid.NewGuid();
+ context.Current[ActivityPropertyNames.IsFirstSession] = !File.Exists(TelemetryPaths.ActivityStorage);
+ context.Current["OldCli"] = true;
+
+ if(context.Current.TryGetValue>(ActivityPropertyNames.AdditionalProperties, out var additionalProperties))
+ {
+ additionalProperties["OldCli"] = true;
+ }
+ else
+ {
+ context.Current[ActivityPropertyNames.AdditionalProperties] = new Dictionary
+ {
+ { "OldCli", true }
+ };
+ }
+
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/IKeyedObject.cs b/framework/src/Volo.Abp.Core/Volo/Abp/IKeyedObject.cs
new file mode 100644
index 0000000000..d0cbb47790
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/IKeyedObject.cs
@@ -0,0 +1,6 @@
+namespace Volo.Abp;
+
+public interface IKeyedObject
+{
+ string? GetObjectKey();
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs
index be28f569ea..5ed5561e0e 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@ using Volo.Abp.Logging;
using Volo.Abp.Modularity;
using Volo.Abp.Reflection;
using Volo.Abp.SimpleStateChecking;
+using Volo.Abp.StaticDefinitions;
namespace Volo.Abp.Internal;
@@ -42,7 +43,7 @@ internal static class InternalServiceCollectionExtensions
services.AddAssemblyOf();
services.AddTransient(typeof(ISimpleStateCheckerManager<>), typeof(SimpleStateCheckerManager<>));
-
+ services.AddSingleton(typeof(IStaticDefinitionCache<,>), typeof(StaticDefinitionCache<,>));
services.Configure