+ {
+ new PaymentRequestProductCreateDto
+ {
+ Code = "Product_01",
+ Name = "LEGO Super Mario",
+ Count = 1,
+ UnitPrice = 60,
+ TotalPrice = 60
+ }
+ },
+ ExtraProperties = new ExtraPropertyDictionary
+ {
+ // For Iyzico - Customer information
+ { "Name", "John" },
+ { "Surname", "Doe" },
+ { "Email", "john.doe@example.com" },
+ { "Address", "123 Main St" },
+ { "City", "Istanbul" },
+ { "Country", "Turkey" },
+ { "ZipCode", "34000" },
+
+ // For PayU - Customer information
+ { "BuyerName", "John" },
+ { "BuyerSurname", "Doe" },
+ { "BuyerEmail", "john.doe@example.com" }
+ }
+ });
+```
+
+#### Handling the Callback (Optional)
+
+When a user completes a payment on the external payment gateway, the following flow occurs:
+
+1. The user is redirected to the **PostPayment page** (handled internally by the payment module)
+2. The PostPayment page validates the payment with the gateway and updates the payment request status to **Completed**
+3. If a `CallbackUrl` is configured in `PaymentBlazorOptions`, the user is then redirected to that URL with the `paymentRequestId` as a query parameter
+
+Create a page to handle this callback and perform any application-specific actions:
+
+```csharp
+@page "/PaymentSucceed"
+@using Microsoft.AspNetCore.WebUtilities
+
+Payment Successful!
+Thank you for your purchase.
+Payment Request ID: @PaymentRequestId
+
+@code {
+ [Parameter]
+ [SupplyParameterFromQuery]
+ public Guid? PaymentRequestId { get; set; }
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (PaymentRequestId.HasValue)
+ {
+ // The payment is already completed at this point.
+ // Perform application-specific actions here:
+ // e.g., activate subscription, send confirmation email,
+ // update order status, grant access to purchased content, etc.
+ }
+ }
+}
+```
+
+> **Note:** By the time the user reaches your callback page, the payment request status has already been set to **Completed** by the PostPayment page. Your callback page is for performing additional application-specific logic. It is also your responsibility to handle if a payment request is used more than once. If you have already delivered your product for a given `PaymentRequestId`, you should not deliver it again when the callback URL is visited a second time.
### Angular UI
-#### Installation
+For Angular applications, you need to read and apply the steps explained in the following sections:
+
+#### Configurations
In order to configure the application to use the payment module, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root configuration. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration:
@@ -120,15 +387,35 @@ const APP_ROUTES: Routes = [
];
```
-#### Payment plans page
+### Pages
+
+#### Public Pages
+
+##### Payment Gateway Selection
+
+This page allows selecting a payment gateway. If there is only one payment gateway configured for the application, this page will be skipped.
+
+
+
+##### PrePayment Page
+
+Some payment gateways require additional information before redirecting to the payment gateway. For example, PayU and Iyzico require customer information (Name, Surname, Email Address, etc.) before processing the payment.
+
+
+
+#### Admin Pages
+
+##### Payment Plans Page
+
Payment plans for subscriptions can be managed on this page. You can connect external subscriptions for each gateway to a plan.


-#### Payment request list
-This page lists all the payment request operations in application.
+##### Payment Request List
+
+This page lists all the payment request operations in the application.

@@ -171,7 +458,22 @@ Configure(options =>
* ```PrePaymentUrl```: URL of the page before redirecting user to payment gateway for payment.
* ```PostPaymentUrl```: URL of the page when user redirected back from payment gateway to your website. This page is used to validate the payment mostly.
* ```Order```: Order of payment gateway for gateway selection page.
- * ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+ * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+ * ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+
+### PaymentBlazorOptions
+
+```PaymentBlazorOptions``` is used to configure Blazor application related configurations. This is the Blazor equivalent of `PaymentWebOptions`.
+
+* ```CallbackUrl```: Final callback URL for internal payment gateway modules to return. User will be redirected to this URL on your website after a successful payment.
+* ```RootUrl```: Root URL of your Blazor application.
+* ```GatewaySelectionCheckoutButtonStyle```: CSS style to add to the Checkout button on the gateway selection page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+* ```PaymentGatewayBlazorConfigurationDictionary```: Used to store Blazor related payment gateway configuration.
+ * ```Name```: Name of payment gateway.
+ * ```PrePaymentUrl```: URL of the Blazor page before redirecting user to payment gateway for payment.
+ * ```PostPaymentUrl```: URL of the Blazor page when user is redirected back from payment gateway to your website.
+ * ```Order```: Order of payment gateway for gateway selection page.
+ * ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
### PayuOptions
@@ -193,9 +495,17 @@ Configure(options =>
```PayuWebOptions``` is used to configure PayU payment gateway web options.
-* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
-* ```PrePaymentCheckoutButtonStyle```: Css style to add Checkout button on PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+
+### PayuBlazorOptions
+
+```PayuBlazorOptions``` is used to configure PayU payment gateway Blazor options.
+
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the PayU prepayment page.
### TwoCheckoutOptions
@@ -210,7 +520,14 @@ Configure(options =>
```TwoCheckoutWebOptions``` is used to configure TwoCheckout payment gateway web options.
-* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+
+### TwoCheckoutBlazorOptions
+
+```TwoCheckoutBlazorOptions``` is used to configure TwoCheckout payment gateway Blazor options.
+
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
### StripeOptions
@@ -228,7 +545,14 @@ Configure(options =>
```StripeWebOptions``` is used to configure Stripe payment gateway web options.
-* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+
+### StripeBlazorOptions
+
+```StripeBlazorOptions``` is used to configure Stripe payment gateway Blazor options.
+
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
### PayPalOptions
@@ -245,7 +569,14 @@ Configure(options =>
```PayPalWebOptions``` is used to configure PayPal payment gateway web options.
-* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+
+### PayPalBlazorOptions
+
+```PayPalBlazorOptions``` is used to configure PayPal payment gateway Blazor options.
+
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
### IyzicoOptions
@@ -263,9 +594,17 @@ Configure(options =>
```IyzicoWebOptions``` is used to configure Iyzico payment gateway web options.
-* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+
+### IyzicoBlazorOptions
+
+```IyzicoBlazorOptions``` is used to configure Iyzico payment gateway Blazor options.
+
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
-* ```PrePaymentCheckoutButtonStyle```: CSS style to add Checkout button on Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Iyzico prepayment page.
### AlipayOptions
@@ -285,9 +624,17 @@ Configure(options =>
#### AlipayWebOptions
-* ```Recommended```: Is payment gateway is recommended or not. This information is displayed on payment gateway selection page.
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
+* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
+* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+
+#### AlipayBlazorOptions
+
+```AlipayBlazorOptions``` is used to configure Alipay payment gateway Blazor options.
+
+* ```Recommended```: Is payment gateway recommended or not. This information is displayed on payment gateway selection page.
* ```ExtraInfos```: List of informative strings for payment gateway. These texts are displayed on payment gateway selection page.
-* ```PrePaymentCheckoutButtonStyle```: CSS style to add Checkout button on Iyzico prepayment page. This class can be used for tracking user activity via 3rd party tools like Google Tag Manager.
+* ```PrePaymentCheckoutButtonStyle```: CSS style to add to the Checkout button on the Alipay prepayment page.
> You can check the [Alipay document](https://opendocs.alipay.com/open/02np97) for more details.
@@ -541,7 +888,7 @@ PrePayment page asks users for extra information if requested by the external pa
PostPayment page is responsible for validation of the response of the external payment gateway. When a user completes the payment, user is redirected to PostPayment page for that payment gateway and PostPayment page validates the status of the payment. If the payment is succeeded, status of the payment request is updated and user is redirected to main application.
-Note: It is main application's responsibility to handle if a payment request is used more than one time. For example if PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than onec manually by end users. If you already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited second time.
+Note: It is the main application's responsibility to handle if a payment request is used more than once. For example, if the PostPayment page generates a URL like https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId}, this URL can be visited more than once manually by end users. If you have already delivered your product for a given PaymentRequestId, you shouldn't deliver it when this URL is visited a second time.
### Creating One-Time Payment
@@ -563,7 +910,7 @@ public class IndexModel: PageModel
{
var paymentRequest = await _paymentRequestAppService.CreateAsync(new PaymentRequestCreateDto()
{
- Currency= "USD",
+ Currency = "USD",
Products = new List()
{
new PaymentRequestProductCreateDto
@@ -582,7 +929,7 @@ public class IndexModel: PageModel
}
```
-If the payment is successful, payment module will return to configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (Activating a user account, triggering a shipment start process etc...).
+If the payment is successful, payment module will return to the configured ```PaymentWebOptions.CallbackUrl```. The main application can take necessary actions for a successful payment (activating a user account, triggering a shipment start process, etc.).
## Subscriptions
diff --git a/docs/en/release-info/index.md b/docs/en/release-info/index.md
index e3d3aa56cd..b7da966e43 100644
--- a/docs/en/release-info/index.md
+++ b/docs/en/release-info/index.md
@@ -1,16 +1,135 @@
```json
//[doc-seo]
{
- "Description": "Explore the latest ABP Framework release information, including notes, migration guides, and upgrading tips to enhance your development experience."
+ "Description": "Understand ABP Platform's versioning, release schedule, LTS support policy, and how we handle breaking changes to ensure smooth upgrades for your applications."
}
```
-# Release Information
+# Versioning & Releases
-* [Release Notes](./release-notes.md)
-* [Migration Guides](./migration-guides/index.md)
-* [Road Map](./road-map.md)
-* [Upgrading](./upgrading.md)
-* [Preview Releases](./previews.md)
-* [Nightly Releases](./nightly-builds.md)
-* [Official Packages](https://abp.io/packages)
\ No newline at end of file
+ABP Platform follows a predictable release cycle aligned with .NET releases. This document explains our versioning strategy, release schedule, support policy, and how we handle breaking changes.
+
+## Our Commitment
+
+As a framework you build upon, ABP must be both reliable and evolving — stable enough to trust for long-term projects, yet continuously improving with new features and the latest .NET advancements. To achieve this balance, we commit to:
+
+* **Predictable releases**: Major versions annually, aligned with .NET releases
+* **Long-term support**: Every major version receives 2 years of support
+* **Smooth upgrades**: Comprehensive migration guides and tooling for version updates
+* **Transparent communication**: Clear documentation of breaking changes and deprecations
+
+## ABP Versioning
+
+ABP version numbers indicate the level of changes that are introduced by the release. This use of [semantic versioning](https://semver.org/) helps you understand the potential impact of updating to a new version.
+
+ABP version numbers have three parts: `major.minor.patch`. For example, version 10.1.2 indicates major version 10, minor version 1, and patch level 2.
+
+The version number is incremented based on the level of change included in the release.
+
+| Level of change | Details |
+| --- | --- |
+| **Major release** | Contains significant new features. Aligned with the new major .NET release. Some developer assistance is expected. You should check the [migration guide](migration-guides/index.md) and possibly refactor code to adapt to new APIs. |
+| **Minor release** | Contains new features and improvements. Minor releases are generally backward-compatible; minimal developer assistance is expected, but you can optionally modify your applications to begin using new APIs and features. |
+| **Patch release** | Low risk, bug fix and security patch release. No developer assistance is expected. |
+
+> **Note:** ABP version is aligned with .NET version. For example, ABP 10.x runs on .NET 10, ABP 9.x runs on .NET 9.
+
+### Preview Releases
+
+We provide preview releases for each major and minor release so you can try new features before the stable release:
+
+| Pre-release type | Details |
+| --- | --- |
+| **Release Candidate (RC)** | A release that is feature complete and in final testing. RC releases are indicated by a release tag appended with the `-rc` identifier, such as `10.1.0-rc.1`. |
+| **Nightly builds** | The latest development builds published every weekday night. Nightly builds allow you to try the previous day's development. |
+
+See the [Preview Releases](previews.md) and [Nightly Builds](nightly-builds.md) documents for more information.
+
+## Release Frequency
+
+We work toward a regular schedule of releases, so that you can plan and coordinate your updates with the continuing evolution of ABP and the .NET platform.
+
+> **Note:** Dates are offered as general guidance and are subject to change.
+
+In general, expect the following release cycle:
+
+* **A major release once a year**, typically in November, following the new major .NET release
+* **2-4 minor releases** for each major version, released every ~3 months after the major release
+* **Patch releases** as needed, typically every 2-4 weeks for the latest minor version
+
+This cadence of releases gives eager developers access to new features as soon as they are fully developed and tested, while maintaining the stability and reliability of the platform for production users.
+
+### Release Schedule
+
+| Version | Status | Released | Active Ends | LTS Ends |
+| --- | --- | --- | --- | --- |
+| ^10.0.0 | Active | 2025-11 | 2026-11 | 2027-11 |
+| ^9.0.0 | LTS | 2024-11 | 2025-11 | 2026-11 |
+
+
+See the [Release Notes](release-notes.md) for detailed information about each release.
+
+## Support Policy and Schedule (LTS)
+
+ABP Platform follows a **Long-Term Support (LTS)** policy to ensure your applications remain secure and stable over time.
+
+### Support Window
+
+Every major version has a **2-year lifecycle** with two distinct phases:
+
+| Support Stage | Duration | Details |
+| --- | --- | --- |
+| **Active** | ~1 year | The version is under active development. Regularly-scheduled updates and patches are released. New features and improvements are added in minor versions. |
+| **Long-Term Support (LTS)** | ~1 year | Only critical fixes and security patches are released. No new features are added. |
+
+This means we actively develop a major version for about 1 year (until the next major .NET release), then provide LTS support for another year.
+
+### LTS Fixes
+
+As a general rule, a fix is considered for an LTS version if it resolves one of:
+
+* A newly identified security vulnerability
+* A critical bug that significantly impacts production applications
+* A regression caused by a 3rd party change, such as a new browser version or dependency update
+
+## Deprecation Policy
+
+When the ABP team intends to remove an API or feature, it will be marked as *deprecated*. This occurs when an API is obsolete, superseded by another API, or otherwise discontinued. Deprecated APIs remain available through their deprecated phase, which lasts a minimum of one major version (approximately one year).
+
+To help ensure that you have sufficient time and a clear path to update, this is our deprecation policy:
+
+| Deprecation Stage | Details |
+| --- | --- |
+| **Announcement** | We announce deprecated APIs and features in the [release notes](release-notes.md) and [migration guides](migration-guides/index.md). Deprecated APIs are typically marked with `[Obsolete]` attribute in the code, which enables IDEs to provide warnings if your project depends on them. We also announce a recommended update path. |
+| **Deprecation period** | When an API or feature is deprecated, it is still present in at least the next major release. After that, deprecated APIs and features are candidates for removal. A deprecation can be announced in any release, but the removal of a deprecated API or feature happens only in major releases. |
+| **NuGet/NPM dependencies** | We typically make dependency updates that require changes to your applications in major releases. In minor releases, we may update dependencies by expanding the supported versions, but we try not to require projects to update these dependencies until the next major version. |
+
+## Breaking Changes Policy
+
+Breaking changes require you to do work because the state after the change is not backward compatible with the state before it. Examples of breaking changes include the removal of public APIs, changes to method signatures, changing the timing of events, or updating to a new version of a dependency that includes breaking changes itself.
+
+### How We Handle Breaking Changes
+
+To support you in case of breaking changes:
+
+* We follow our [deprecation policy](#deprecation-policy) before we remove a public API
+* We provide detailed [migration guides](migration-guides/index.md) when a version includes breaking changes
+
+### Update Path
+
+We recommend updating one major version at a time for a smoother upgrade experience. For example, to update from version 8.x to version 10.x:
+
+1. Update from version 8.x to version 9.x
+2. Update from version 9.x to version 10.x
+
+See the [upgrading](upgrading.md) document for detailed instructions on how to upgrade your solutions.
+
+## Related Documents
+
+* [Release Notes](release-notes.md) - Detailed release notes for each version
+* [Migration Guides](migration-guides/index.md) - Step-by-step guides for upgrading between versions
+* [Road Map](road-map.md) - Upcoming features and planned releases
+* [Upgrading](upgrading.md) - How to upgrade your ABP-based solutions
+* [Preview Releases](previews.md) - Information about preview/RC releases
+* [Nightly Builds](nightly-builds.md) - How to use nightly builds
+* [Official Packages](https://abp.io/packages) - Browse all ABP packages
diff --git a/docs/en/release-info/migration-guides/abp-10-1.md b/docs/en/release-info/migration-guides/abp-10-1.md
new file mode 100644
index 0000000000..dce45aff64
--- /dev/null
+++ b/docs/en/release-info/migration-guides/abp-10-1.md
@@ -0,0 +1,52 @@
+```json
+//[doc-seo]
+{
+ "Description": "Upgrade your ABP solutions from v10.0 to v10.1 with this comprehensive migration guide, ensuring compatibility and new features with ABP v10.1."
+}
+```
+
+# ABP Version 10.1 Migration Guide
+
+This document is a guide for upgrading ABP v10.0 solutions to ABP v10.1. There are some changes in this version that may affect your applications. Please read them carefully and apply the necessary changes to your application.
+
+## Open-Source (Framework)
+
+### Add New EF Core Migrations for Password History/User Passkey Entities
+
+In this version, we added password history/ user passkeys support to the [Identity PRO Module](../../modules/identity-pro.md) to enhance security compliance. A new `IdentityUserPasswordHistory` entity has been added to store previous password hashes, preventing users from reusing recent passwords. Additionally, we have introduced an `IdentityUserPasskey `entity to support passkey-based authentication.
+
+**You need to create a new EF Core migration and apply it to your database** after upgrading to ABP 10.1.
+
+> See [#23894](https://github.com/abpframework/abp/pull/23894) for more details.
+
+## PRO
+
+> Please check the **Open-Source (Framework)** section before reading this section. The listed topics might affect your application and you might need to take care of them.
+
+If you are a paid-license owner and using the ABP's paid version, then please follow the following sections to get informed about the breaking changes and apply the necessary ones:
+
+### AI Management Module: `Workspace` Entity Base Class Changed
+
+In this version, the `Workspace` entity in the [AI Management Module](../../modules/ai-management/index.md) has been changed from `FullAuditedAggregateRoot` to `AuditedAggregateRoot` as the base class.
+
+**This change removes support for soft deletion and related auditing features. If you are using the AI Management module, you need to create a new EF Core migration and apply it to your database.**
+
+> **Important:** If you have soft-deleted Workspaces in your database, they will become visible after this update. You may need to create a migration script to clean up already deleted records before applying the migration.
+
+### CMS Kit Pro Module: Dynamic FAQ Group Management
+
+In this version, the FAQ group system in the [CMS Kit Pro Module](../../modules/cms-kit-pro/faq.md) has been redesigned to support dynamic group management. FAQ groups are now **first-class entities** stored in the database, replacing the previous static configuration approach.
+
+**Key Changes:**
+
+- A new `FaqGroup` entity has been introduced with unique names for FAQ groups
+- The `FaqSection` entity now uses `GroupId` (Guid) instead of `GroupName` (string) _(GroupName is deprecated and will be removed soon. Use GroupId instead.)_
+- Static FAQ group configuration (`FaqOptions.SetGroups`) has been removed
+
+**Migration Steps:**
+
+1. **Remove static group configuration** from your code (e.g., `Configure(options => { options.SetGroups([...]); })`)
+2. **Create a new EF Core migration and apply it to your database**
+3. **Run the one-time data migration seeder** to migrate existing FAQ sections to the new group entity model
+
+> **Note:** If you have existing FAQ data, you may need to create a data seeder to migrate your existing group associations to the new entity-based model.
diff --git a/docs/en/release-info/migration-guides/index.md b/docs/en/release-info/migration-guides/index.md
index f0d8a78b84..61bb095434 100644
--- a/docs/en/release-info/migration-guides/index.md
+++ b/docs/en/release-info/migration-guides/index.md
@@ -9,6 +9,7 @@
The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../upgrading.md).
+- [10.0 to 10.1](abp-10-1.md)
- [9.x to 10.0](abp-10-0.md)
- [9.2 to 9.3](abp-9-3.md)
- [9.x to 9.2](abp-9-2.md)
diff --git a/docs/en/release-info/release-notes.md b/docs/en/release-info/release-notes.md
index bf335fa7a2..08d0bcde31 100644
--- a/docs/en/release-info/release-notes.md
+++ b/docs/en/release-info/release-notes.md
@@ -14,8 +14,23 @@ Also see the following notes about ABP releases:
* [ABP Studio release notes](../studio/release-notes.md)
* [Change logs for ABP pro packages](https://abp.io/pro-releases)
+## 10.1 (2026-01-06)
+
+> This is currently a RC (release-candidate) and you can see the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-1-release-candidate-cyqui19d)** for the v10.1 release.
+
+* Resource-Based Authorization
+* Introducing the [TickerQ Background Worker Provider](../framework/infrastructure/background-workers/tickerq.md)
+* Angular UI: Version Upgrade to **v21**
+* [File Management Module](../modules/file-management.md): Public File Sharing Support
+* [Payment Module](../modules/payment.md): Public Page Implementation for Blazor & Angular UIs
+* [AI Management Module](../modules/ai-management/index.md) for Blazor & Angular UIs
+* [Identity PRO Module](../modules/identity-pro.md): Password History Support
+* [Account PRO Module](../modules/account-pro.md): Introducing WebAuthn Passkeys
+
## 10.0 (2025-11-18)
+> **Note**: ABP has upgraded to .NET 10.0, so if you plan to use ABP 10.0, you’ll need to migrate your solutions to .NET 10.0. You can refer to the [Migrate from ASP.NET Core 9.0 to 10.0](https://learn.microsoft.com/en-us/aspnet/core/migration/90-to-100) documentation for guidance. However, ABP’s NuGet packages are compatible with both .NET 9 and .NET 10, allowing developers to continue using .NET 9 while still enjoying the latest features and improvements of the ABP Framework without upgrading their SDK.
+
See the detailed **[blog post / announcement](https://abp.io/community/announcements/abp.io-platform-10.0-final-has-been-released-spknn925)** for the v10.0 release.
* Upgraded to .NET 10.0
diff --git a/docs/en/release-info/road-map.md b/docs/en/release-info/road-map.md
index 0df94ec157..70a9b392c4 100644
--- a/docs/en/release-info/road-map.md
+++ b/docs/en/release-info/road-map.md
@@ -1,7 +1,7 @@
```json
//[doc-seo]
{
- "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 9.1, launching January 2025."
+ "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.1, launching January 2026."
}
```
@@ -11,33 +11,35 @@ This document provides a road map, release schedule, and planned features for th
## Next Versions
-### v10.1
+### v10.2
-The next version will be 10.1 and planned to release the stable 10.1 version in January 2026. We will be mostly working on the following topics:
+The next version will be 10.2 and planned to release the stable 10.2 version in April 2026. We will be mostly working on the following topics:
* Framework
- * OpenTelemetry Protocol Support for 3rd-party Integrations
- * Resource Based Authorization Integration
+ * Resource-Based Authorization Improvements
+ * Handle datetime/timezon in `AbpExtensibleDataGrid` Component
* Upgrading 3rd-party Dependencies
* Enhancements in the Core Points
* ABP Suite
- * Define Navigation Properties Without Target String Property Dependency
+ * Creating enums on-the-fly (without needing to create manually on the code side)
+ * Improvements on the generated codes for nullability
* Improvements on Master-Detail Page Desing (making it more compact)
* Improvements One-To-Many Scenarios
* File Upload Modal Enhancements
* ABP Studio
* Allow to Directly Create New Solutions with ABP's RC (Release Candidate) Versions
+ * Integrate AI Management Module with all solution templates and UIs
* Automate More Details on New Service Creation for a Microservice Solution
* Allow to Download ABP Samples from ABP Studio
- * Task Panel Enhancements (and Documentation)
+ * Task Panel Documentation
* Support Multiple Concurrent Kubernetes Deployment/Integration Scenarios
* Improve the Module Installation Experience / Installation Guides
* Application Modules
- * Payment Module: Public Page Implementation (for Blazor & Angular UIs)
- * AI Management Module: UI Implementation for Blazor & Angular UIs
+ * AI Management: MCP & RAG Supports
+ * File Management: Using Resource-Based Permission (on file-sharing and more...)
* CMS Kit: Enhancements for Some Features (Rating, Dynamic Widgets, FAQ and more...)
* UI/UX Improvements on Existing Application Modules
diff --git a/docs/en/release-info/upgrading.md b/docs/en/release-info/upgrading.md
index 88ce1249d8..974d7ef2d5 100644
--- a/docs/en/release-info/upgrading.md
+++ b/docs/en/release-info/upgrading.md
@@ -21,6 +21,8 @@ Run this command in the terminal while you are in the root folder of your soluti
> If your solution has the Angular UI, you probably have `aspnet-core` and `angular` folders in the solution. Run this command in the parent folder of these two folders.
+You can also specify a target version with `--version` parameter. See the [ABP CLI update command](../cli/index.md#update) for all available options.
+
### Database Migrations
> Warning: Be careful if you are migrating your database since you may have data loss in some cases. Carefully check the generated migration code before executing it. It is suggested to take a backup of your current database.
diff --git a/docs/en/studio/concepts.md b/docs/en/studio/concepts.md
index 1e6afb4fab..1530f3a029 100644
--- a/docs/en/studio/concepts.md
+++ b/docs/en/studio/concepts.md
@@ -43,6 +43,45 @@ An ABP Studio module is a sub-solution that contains zero, one or multiple packa
An ABP Studio Package typically matches to a .NET project (`csproj`).
+### Metadata
+
+Metadata is a collection of key-value pairs that provide additional information for various ABP Studio features. Metadata follows a hierarchical structure where values defined at lower levels override those at higher levels:
+
+**Hierarchy (from highest to lowest priority):**
+1. **Helm Chart Metadata** - Defined in chart properties (Kubernetes context only)
+2. **Kubernetes Profile Metadata** / **Run Profile Metadata** - Defined in profile settings (context-dependent)
+3. **Solution Metadata** - Defined via *Solution Explorer* → right-click solution → *Manage Metadata*
+4. **Global Metadata** - Defined via *Tools* → *Global Metadata*
+
+**Common Metadata Keys:**
+
+| Key | Description | Used By |
+|-----|-------------|---------|
+| `k8ssuffix` | Appends a suffix to Kubernetes namespace (e.g., for multi-developer scenarios) | Kubernetes integration |
+| `dotnetEnvironment` | Specifies the .NET environment (e.g., `Development`, `Staging`) | Helm chart installation |
+| `projectPath` | Path to the project for Docker image building | Docker image build |
+| `imageName` | Docker image name | Docker image build |
+| `projectType` | Project type (`dotnet` or `angular`) | Docker image build |
+
+> Metadata defined in *Global Metadata* is available for all solutions but will not be shared with team members. Metadata defined at *Solution* or *Profile* level will be shared through solution files.
+
+### Secrets
+
+Secrets are key-value pairs designed for storing sensitive information such as passwords, API keys, and connection strings. Unlike metadata, secrets are stored in the local file system and are not included in solution files for security reasons.
+
+**Hierarchy (from highest to lowest priority):**
+1. **Kubernetes Profile Secrets** / **Run Profile Secrets** - Defined in profile settings (context-dependent)
+2. **Solution Secrets** - Defined via *Solution Explorer* → right-click solution → *Manage Secrets*
+3. **Global Secrets** - Defined via *Tools* → *Global Secrets*
+
+**Common Secret Keys:**
+
+| Key | Description | Used By |
+|-----|-------------|---------|
+| `wireGuardPassword` | Password for WireGuard VPN connection to Kubernetes cluster | Kubernetes integration |
+
+> Secrets are stored locally and are not shared with team members by default. Each developer needs to configure their own secrets.
+
## ABP Studio vs .NET Terms
Some ABP Studio terms may seem conflict with .NET and Visual Studio. To make them even more clear, you can use the following table.
diff --git a/docs/en/studio/custom-commands.md b/docs/en/studio/custom-commands.md
new file mode 100644
index 0000000000..af56492f37
--- /dev/null
+++ b/docs/en/studio/custom-commands.md
@@ -0,0 +1,128 @@
+```json
+//[doc-seo]
+{
+ "Description": "Learn how to create and manage custom commands in ABP Studio to automate build, deployment, and other workflows."
+}
+```
+
+# Custom Commands
+
+````json
+//[doc-nav]
+{
+ "Next": {
+ "Name": "Working with ABP Suite",
+ "Path": "studio/working-with-suite"
+ }
+}
+````
+
+Custom commands allow you to define reusable terminal commands that appear in context menus throughout ABP Studio. You can use them to automate repetitive tasks such as building Docker images, installing Helm charts, running deployment scripts, or executing any custom workflow.
+
+> **Note:** This is an advanced feature primarily intended for teams working with Kubernetes deployments or complex build/deployment workflows. If you're developing a standard application without custom DevOps requirements, you may not need this feature.
+
+## Opening the Management Window
+
+To manage custom commands, right-click on the solution root in *Solution Explorer* and select *Manage Custom Commands*.
+
+
+
+The management window displays all defined commands with options to add, edit, or delete them.
+
+## Creating a New Command
+
+Click the *Add New Command* button to open the command editor dialog.
+
+
+
+## Command Properties
+
+| Property | Description |
+|----------|-------------|
+| **Command Name** | A unique identifier for the command (used internally) |
+| **Display Name** | The text shown in context menus |
+| **Terminal Command** | The PowerShell command to execute. Use `&&&` to chain multiple commands |
+| **Working Directory** | Optional. The directory where the command runs (relative to solution path) |
+| **Condition** | Optional. A [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) expression that determines when the command is visible |
+| **Require Confirmation** | When enabled, shows a confirmation dialog before execution |
+| **Confirmation Text** | The message shown in the confirmation dialog |
+
+## Trigger Targets
+
+Trigger targets determine where your command appears in context menus. You can select multiple targets for a single command.
+
+| Target | Location |
+|--------|----------|
+| **Helm Charts Root** | *Kubernetes* panel > *Helm* tab > root node |
+| **Helm Main Chart** | *Kubernetes* panel > *Helm* tab > main chart |
+| **Helm Sub Chart** | *Kubernetes* panel > *Helm* tab > sub chart |
+| **Kubernetes Service** | *Kubernetes* panel > *Kubernetes* tab > service |
+| **Solution Runner Root** | *Solution Runner* panel > profile root |
+| **Solution Runner Folder** | *Solution Runner* panel > folder |
+| **Solution Runner Application** | *Solution Runner* panel > application |
+
+## Execution Targets
+
+Execution targets define where the command actually runs. This enables cascading execution:
+
+- When you trigger a command from a **root or parent item**, it can recursively execute on all matching children
+- For example: trigger from *Helm Charts Root* with execution target *Helm Sub Chart* → the command runs on each sub chart
+
+## Template Variables
+
+Commands support [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) template syntax for dynamic values. Use `{%{{{variable}}}%}` to insert context-specific data.
+
+### Available Variables by Context
+
+**Helm Charts:**
+
+| Variable | Description |
+|----------|-------------|
+| `profile.name` | Kubernetes profile name |
+| `profile.namespace` | Kubernetes namespace |
+| `chart.name` | Current chart name |
+| `chart.path` | Chart directory path |
+| `metadata.*` | Hierarchical metadata values (e.g., `metadata.imageName`) |
+| `secrets.*` | Secret values (e.g., `secrets.registryPassword`) |
+
+**Kubernetes Service:**
+
+| Variable | Description |
+|----------|-------------|
+| `name` | Service name |
+| `profile.name` | Kubernetes profile name |
+| `profile.namespace` | Kubernetes namespace |
+| `mainChart.name` | Parent main chart name |
+| `chart.name` | Related sub chart name |
+| `chart.metadata.*` | Chart-specific metadata |
+
+**Solution Runner (Root, Folder, Application):**
+
+| Variable | Description |
+|----------|-------------|
+| `profile.name` | Run profile name |
+| `profile.path` | Profile file path |
+| `application.name` | Application name (Application context only) |
+| `application.baseUrl` | Application URL (Application context only) |
+| `folder.name` | Folder name (Folder/Application context) |
+| `metadata.*` | Profile metadata values |
+| `secrets.*` | Profile secret values |
+
+## Example: Build Docker Image
+
+Here's an example command that builds a Docker image for Helm charts:
+
+**Command Properties:**
+- **Command Name:** `buildDockerImage`
+- **Display Name:** `Build Docker Image`
+- **Terminal Command:** `./build-image.ps1 -ProjectPath {%{{{metadata.projectPath}}}%} -ImageName {%{{{metadata.imageName}}}%}`
+- **Working Directory:** `etc/helm`
+- **Trigger Targets:** Helm Charts Root, Helm Main Chart, Helm Sub Chart
+- **Execution Targets:** Helm Main Chart, Helm Sub Chart
+- **Condition:** `{%{{{metadata.projectPath}}}%}`
+
+This command:
+1. Appears in the context menu of Helm charts root and all chart nodes
+2. Executes on main charts and sub charts (cascading from root if triggered there)
+3. Only shows for charts that have `projectPath` metadata defined
+4. Runs the `build-image.ps1` script with dynamic parameters from metadata
diff --git a/docs/en/studio/images/custom-commands/create-edit-command.png b/docs/en/studio/images/custom-commands/create-edit-command.png
new file mode 100644
index 0000000000..7091516a8f
Binary files /dev/null and b/docs/en/studio/images/custom-commands/create-edit-command.png differ
diff --git a/docs/en/studio/images/custom-commands/management-window.png b/docs/en/studio/images/custom-commands/management-window.png
new file mode 100644
index 0000000000..3a24ac8dc4
Binary files /dev/null and b/docs/en/studio/images/custom-commands/management-window.png differ
diff --git a/docs/en/studio/images/monitoring-applications/tools-create.png b/docs/en/studio/images/monitoring-applications/tools-create.png
index ac473d50da..7d7e7c8825 100644
Binary files a/docs/en/studio/images/monitoring-applications/tools-create.png and b/docs/en/studio/images/monitoring-applications/tools-create.png differ
diff --git a/docs/en/studio/kubernetes.md b/docs/en/studio/kubernetes.md
index ae23989f6c..eca8d83c5b 100644
--- a/docs/en/studio/kubernetes.md
+++ b/docs/en/studio/kubernetes.md
@@ -11,8 +11,8 @@
//[doc-nav]
{
"Next": {
- "Name": "Working with ABP Suite",
- "Path": "studio/working-with-suite"
+ "Name": "Custom Commands",
+ "Path": "studio/custom-commands"
}
}
````
@@ -100,7 +100,7 @@ It is the root of all subcharts. When you add a new main chart to the root, it i
- `Install Chart(s)`: Installs the selected chart to the current profile.
- `Uninstall Chart(s)`: Uninstalls the selected chart from the current profile.
- `Properties`: It opens the *Chart Properties* window. You can see the chart information in the *Chart Info* tab. In the *Metadata* tab, you can add metadata for the selected main chart. It overrides the metadata in the profile. In the *Kubernetes Services* tab, you can relate a Kubernetes service with the main chart; however, since the main chart usually doesn't create kubernetes service, we can leave it empty.
-- `Refrest Sub Charts`: Refreshes the subcharts of the selected main chart.
+- `Refresh Sub Charts`: Refreshes the subcharts of the selected main chart.
- `Open With`: You can open the selected chart with *Visual Studio Code* or *File Explorer*.
- `Remove`: Removes the selected main chart from the solution.
@@ -137,6 +137,11 @@ While connected, changing the current profile is not possible. Existing applicat

+When connected, you can right-click on a Kubernetes service to see the following context menu options:
+
+- `Browse`: Opens the [browser](./monitoring-applications.md#browse) and navigates to the Kubernetes service URL. This option is only visible if the service has a related Helm chart with matching *Kubernetes Services* regex pattern.
+- `Enable Interception` / `Disable Interception`: Enables or disables traffic interception for the selected service. See the [Intercept a Service](#intercept-a-service) section for more details.
+
When you are connecting to a Kubernetes cluster, it automatically installs the WireGuard VPN to the Kubernetes cluster for a safe connection. You can specify the *wireGuardPassword* in the *Kubernetes Profile* -> *Secrets* tab or at a higher level such as *Solution Secrets* or *Global Secrets*. If you don't provide a password, it generates a random password and stores it in the *Kubernetes Profile* -> *Secrets*. However, if you try to connect to a cluster that already installed WireGuard VPN, then you should give the same password; otherwise, it won't connect. To see the random password, you can click the *eye* icon in the *Kubernetes Profile* -> *Secrets* tab.

@@ -205,46 +210,12 @@ When you connect to a Kubernetes cluster, it uses the selected profile for Kuber
## Advanced Topics
-### Adding a Custom Command
-
-Custom commands can be added to both the *Helm* and *Kubernetes* tabs within the *Kubernetes* panel. For instance, when [redeploy](#redeploy-a-chart) a chart, it involves building the Docker image and reinstalling it. However, if you are working with a different Kubernetes cluster than Docker Desktop, you'll need to push the Docker image to the registry before the installation process. This can be achieved by incorporating a custom command into the *Kubernetes services*. Custom commands can be added to the *Chart Root*, *Main Chart*, and *Subchart* in the *Helm* tab, as well as to the *Service* in the *Kubernetes* tab.
-
-To do that, open the ABP Solution (*.abpsln*) file with *Visual Studio Code* it's a JSON file and you'll see the existing commands in the `commands` section. Before adding a new command, create a powershell script in the `abp-solution-path/etc/helm` folder. For example, we create a `push-image.ps1` script to push the docker image to the registry. Then, add the following command to the `commands` section.
-
-```JSON
- "kubernetesRedeployWithPushImage": {
- "triggerTargets": [
- "KUBERNETES_SERVICE"
- ],
- "executionTargets": [
- "KUBERNETES_SERVICE"
- ],
- "displayName": " Redeploy with Push Image",
- "workingDirectory": "etc/helm",
- "terminalCommand": "./build-image.ps1 -ProjectPath {%{{{chart.metadata.projectPath}}}%} -ImageName {%{{{chart.metadata.imageName}}}%} -ProjectType {%{{{chart.metadata.projectType}}}%} &&& ./push-image.ps1 -ImageName {%{{{chart.metadata.imageName}}}%} &&& ./install.ps1 -ChartName {%{{{mainChart.name}}}%} -Namespace {%{{{profile.namespace}}}%} -ReleaseName {%{{{mainChart.name}}}%}-{%{{{profile.name}}}%} -DotnetEnvironment {%{{{mainChart.metadata.dotnetEnvironment}}}%}",
- "requireConfirmation": "true",
- "confirmationText": "Are you sure to redeploy with push image the related chart '{%{{{chart.name}}}%}' for the service '{%{{{name}}}%}'?",
- "condition": "{%{{{chart != null && chart.metadata.projectPath != null && chart.metadata.imageName != null && chart.metadata.projectType != null}}}%}"
- }
-```
+### Custom Commands
+
+You can add custom commands to context menus in both the *Helm* and *Kubernetes* tabs. This is useful for automating workflows like pushing Docker images to a registry before installation, or running custom deployment scripts.
-Once the command is added, reload the solution from *File* -> *Reload Solution* in the toolbar. After reloading, you will find the *Redeploy with Push Image* command in the context-menu of the service.
+Custom commands can be added to the *Chart Root*, *Main Chart*, and *Sub Chart* in the *Helm* tab, as well as to the *Service* in the *Kubernetes* tab.

-The JSON object has the following properties:
-
-- `triggerTargets`: Specifies the trigger targets for the command. The added command will appear in these targets. You can add one or more trigger targets, accepting values such as *HELM_CHARTS_ROOT*, *HELM_MAIN_CHART*, *HELM_SUB_CHART* and *KUBERNETES_SERVICE*.
-- `executionTargets`: Specifies the execution targets for the command. When executing the command on a root item, it will recursively execute the command for all children. Acceptable values include *HELM_CHARTS_ROOT*, *HELM_MAIN_CHART*, *HELM_SUB_CHART*, and *KUBERNETES_SERVICE*.
-- `displayName`: Specifies the display name of the command.
-- `workingDirectory`: Specifies the working directory of the command. It's relative to the solution path.
-- `terminalCommand`: Specifies the terminal command for the custom command. The `&&&` operator can be used to run multiple commands in the terminal. Utilize the [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) syntax to access input data, which varies based on the execution target.
-- `requireConfirmation`: Specifies whether the command requires confirmation message before execution. Acceptable values include *true* and *false*.
-- `confirmationText`: Specifies the confirmation text for the command. Utilize the [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) syntax to access input data, which varies based on the execution target.
-- `condition`: Specifies the condition for the command. If the condition returns *false*, it skips the current item and attempts to execute the command for the next item or child item. Utilize the [Scriban](https://github.com/scriban/scriban/blob/master/doc/language.md) syntax to access input data, which varies based on the execution target.
-
-You can use the following variables in the scriban syntax based on the execution target:
- - `HELM_CHARTS_ROOT`: *profile*, *metadata*, *secrets*
- - `HELM_MAIN_CHART`: *profile*, *chart*, *metadata*, *secret*
- - `HELM_SUB_CHART`: *profile*, *chart*, *metadata*, *secret*
- - `KUBERNETES_SERVICE`: *name*, *profile*, *mainChart*, *chart*, *metadata*, *secret*
+For detailed information on creating and managing custom commands, see the [Custom Commands](custom-commands.md) documentation.
diff --git a/docs/en/studio/monitoring-applications.md b/docs/en/studio/monitoring-applications.md
index 75390bf904..083a0d384f 100644
--- a/docs/en/studio/monitoring-applications.md
+++ b/docs/en/studio/monitoring-applications.md
@@ -66,91 +66,318 @@ In this tab, you can view comprehensive overall information. You have the option

-In the data grid, details for each application are displayed. It's possible to sort rows by columns. When selecting a row, you can right-click to access the context menu, offering various actions. This menu allows for opening related tabs that are filtered by the selected application.
+In the data grid, details for each application are displayed. It's possible to sort rows by columns.
- `Name`: The name of the application.
-- `State`: The state of the application. It can take on several values such as *Scheduled*, *Starting*, *Started*, *Stopping* and *Stopped*. In the event of an application crash during its starting, the state is mark as *Scheduled*, we can cancel the starting process at that stage.
-- `Health` : The health state of the application. Clicking on the icon shows the latest health check response. Displays `N/A` if the application is not running or health check is not configured for the application.
-- `Instances`: Indicates the count of running instances for the application. This value is particularly helpful when scaling the application within a Kubernetes, providing visibility into the number of currently active instances.
+- `State`: The state of the application. It can take on several values such as *Scheduled*, *Starting*, *Started*, *Stopping* and *Stopped*. The *Scheduled* state indicates the application is waiting for an automatic restart (e.g., after a crash or when watch mode detects changes). You can cancel the scheduled restart at this stage.
+- `Health`: The health state of the application. The icon indicates the current health status: *Healthy* (green), *Unhealthy* (red), *Degraded* (yellow), or *Unknown* (gray). Clicking on the icon shows the latest health check response in JSON format. Displays `N/A` if the application is not running or health check is not configured for the application.
+- `Instances`: Indicates the count of running instances for the application. This value is particularly helpful when scaling the application within a Kubernetes cluster, providing visibility into the number of currently active pods.
- `Uptime`: The time elapsed since the application started.
- `Requests`: The number of HTTP requests received by the application.
-- `Events (R/S)`: The number of [Distributed Event](../framework/infrastructure/event-bus/distributed) received or sent by the application.
+- `Events (R/S)`: The number of [Distributed Events](../framework/infrastructure/event-bus/distributed) received or sent by the application.
- `Exceptions`: The number of exceptions thrown by the application.
- `Actions`: The actions that can be performed on the application. You can start and stop the application.
-> For the events system, you can exclusively view the [Distributed Events](../framework/infrastructure/event-bus/distributed). Generally, the [Local Events](../framework/infrastructure/event-bus/distributed) is not included.
+### Context Menu Actions
+
+When selecting a row, you can right-click to access the context menu with the following actions:
+
+| Action | Description |
+|--------|-------------|
+| **Start / Stop** | Start or stop the selected application. |
+| **Restart** | Restart the application (available when the application is running). |
+| **Build** | Build options including *Build*, *Graph Build*, *Restore*, and *Clean* (available for C# applications when stopped). |
+| **Browse** | Open the application in the [Browse](#browse) tab (available when a Launch URL is configured). |
+| **Health Status** | Submenu with *Browse Health UI* and *Show Latest Health Check Response* options. |
+| **Requests** | Open the [HTTP Requests](#http-requests) tab filtered by this application. |
+| **Exceptions** | Open the [Exceptions](#exceptions) tab filtered by this application. |
+| **Logs** | Open the [Logs](#logs) tab with this application selected. |
+| **Copy Url** | Copy the application's URL to the clipboard. |
+| **Properties** | Open the application properties dialog. |
+
+> For the events system, you can exclusively view the [Distributed Events](../framework/infrastructure/event-bus/distributed). Generally, the [Local Events](../framework/infrastructure/event-bus/local) are not included.
## Browse
-ABP Studio includes a browser tool that allows access to websites and running applications. You can open new tabs to browse different websites or view active applications. It's a convenient utility to access websites and applications without leaving ABP Studio. Clicking the *Browse* tab displays the running applications and an *Open new tab* button.
+ABP Studio includes a built-in browser that allows access to websites and running applications. You can open new tabs to browse different websites or view active applications. It's a convenient utility to access websites and applications without leaving ABP Studio. Clicking the *Browse* tab displays the running applications and an *Open new tab* button.

-You can open the *Browse* tabs as many times as you want. It's possible to open the same application in several tabs simultaneously. To open an application, navigate through *Solution Runner* -> *C# or CLI Application* -> *Browse*. This option is only visible when there is a [Launch URL](./running-applications.md#properties). Additionally, you can access any URL by entering it into the address bar.
+You can open the *Browse* tabs as many times as you want. It's possible to open the same application in several tabs simultaneously. To open an application, you can:
+
+- Double-click on an application in the *Solution Runner* tree.
+- Right-click on an application and select *Browse* from the context menu.
+- Click on a running application in the application list shown in the *Browse* tab.
+
+These options are only available when the application has a [Launch URL](./running-applications.md#properties) configured. Additionally, you can access any URL by entering it into the address bar.

-When you click the *Dev Tools* button it opens the [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools) for the selected tab.
+### Browser Toolbar
+
+The browser toolbar provides the following controls:
+
+| Control | Description |
+|---------|-------------|
+| **Back / Forward** | Navigate through your browsing history within the tab. |
+| **Refresh** | Reload the current page. |
+| **Address Bar** | Enter any URL to navigate directly. The address bar shows the current URL and allows you to navigate to any website. |
+| **Dev Tools** | Opens the [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools) for the selected tab, allowing you to inspect elements, debug JavaScript, and analyze network requests. |
+| **Clear Cookies** | Clears all cookies for the currently selected tab, useful for testing authentication flows or resetting session state. |
+| **Open in External Browser** | Opens the current URL in your system's default web browser. |

+### Default Credentials Notification
+
+When browsing certain applications (such as AppHost dashboards), ABP Studio displays a notification bar with default credentials. This helps you quickly log in during development. You can dismiss this notification permanently by clicking the *Don't show again* option.
+
## HTTP Requests
Within this tab, you can view all *HTTP Requests* received by your C# applications. You have the option to filter requests based on URLs by using the search textbox or by selecting a particular application from the combobox. The *Clear Requests* button removes all received requests. Moreover, you have the ability to sort requests by columns.

-Clicking on a row enables you to view the details of each HTTP request; `URL`, `Method`, `Status Code`, `Timestamp`, `Headers (Request, Response)`, `Request (Payload)` and `Response`.
+### Request List Columns
+
+The request list displays the following information:
+
+| Column | Description |
+|--------|-------------|
+| **Timestamp** | When the request was received (displayed as HH:mm:ss). |
+| **Application** | The name of the application that received the request. |
+| **Path** | The request URL path and query string. |
+| **Method** | The HTTP method (GET, POST, PUT, DELETE, etc.). |
+| **Status Code** | The HTTP response status code. |
+| **Duration** | The time taken to process the request in milliseconds. |
+| **Request Body Size** | The size of the request payload. |
+| **Response Body Size** | The size of the response payload. |
+
+### Request Details
+
+Clicking on a row enables you to view the details of each HTTP request:
+
+- **URL**: The full request URL.
+- **Method**: The HTTP method used.
+- **Status Code**: The response status code.
+- **Duration**: Request processing time in milliseconds.
+- **Timestamp**: When the request was received.
+- **Headers (Request)**: All request headers sent by the client.
+- **Headers (Response)**: All response headers returned by the server.
+- **Request (Payload)**: The request body with size information.
+- **Response**: The response body with size information.

-You can format the JSON content by clicking the *Format* button.
+### Formatting JSON Content
+
+Both request and response payloads have a *Format* button that formats JSON content for better readability. This is available when the content type is `application/json`.

-Furthermore, by clicking the gear icon in the *HTTP Requests* tab, you can access the *Solution Runner HTTP Requests Options* window. Within the *Ignored URLs* tab, you have the ability to exclude particular URLs by applying a regex pattern. Excluded URLs won't be visible in the *HTTP Requests* tab. By default, the metrics URL is already ignored. You can add or remove items as needed.
+### Quick URL Filter
+
+When viewing request details, you can right-click and select *Filter Selected URL* to quickly apply the current URL as a filter. This is useful when you want to see all requests to a specific endpoint.
+
+### Configuring Ignored URLs
+
+By clicking the gear icon in the *HTTP Requests* tab, you can access the *Solution Runner HTTP Requests Options* window. Within the *Ignored URLs* tab, you have the ability to exclude particular URLs by applying a regex pattern. Excluded URLs won't be visible in the *HTTP Requests* tab. By default, the metrics URL is already ignored. You can add or remove items as needed.

-> After adding a new URL, it will only affect subsequent requests.
+> After adding a new URL pattern, it will only affect subsequent requests. Existing requests will remain visible.
## Events
-In this tab, you can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by your C# applications. You can filter them by [Event Name](../framework/infrastructure/event-bus/distributed#event-name) using the search textbox or by selecting a specific application. Additionally, you can choose the *Direction* (Received/Send) and *Source* (Direct/Inbox/Outbox) of events. The *Clear Events* button removes all events.
+In this tab, you can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by your C# applications. You can filter them by [Event Name](../framework/infrastructure/event-bus/distributed#event-name) using the search textbox or by selecting a specific application. Additionally, you can choose the *Direction* (Received/Sent) and *Source* (Direct/Inbox/Outbox) of events. The *Clear Events* button removes all events.

-> In the *Direction* section, there are two options: *Received*, indicating events received by the application, and *Sent*, indicating events sent by the application. Within the *Source* section, three options are available, and their significance comes into play when utilizing the [Inbox/Outbox pattern](../framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events). *Inbox* refers to events received by the application, *Outbox* refers to events sent by the application and *Direct* signifies events sent or received by the application without involving Inbox/Outbox pattern.
+### Direction and Source Filters
+
+| Filter | Options | Description |
+|--------|---------|-------------|
+| **Direction** | *Received*, *Sent* | Filter by whether events were received or sent by the application. |
+| **Source** | *Direct*, *Inbox*, *Outbox* | Filter by event source. Relevant when using the [Inbox/Outbox pattern](../framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events). |
+
+- **Direct**: Events sent or received without using the Inbox/Outbox pattern.
+- **Inbox**: Events received through the transactional inbox.
+- **Outbox**: Events sent through the transactional outbox.
-Clicking on a row enables you to view the details of each event; `Application`, `Event Name`, `Direction`, `Source`, `Timestamp` and `Event Data`.
+### Event Details
+
+Clicking on a row enables you to view the details of each event:
+
+- **Application**: The application that sent or received the event.
+- **Event Name**: The full event type name.
+- **Direction**: Whether the event was received or sent.
+- **Source**: The event source (Direct, Inbox, or Outbox).
+- **Timestamp**: When the event was processed.
+- **Event Data**: The event payload in JSON format.

+### Formatting Event Data
+
+The *Event Data* section includes a *Format* button that formats the JSON content for better readability. This makes it easier to inspect complex event payloads.
+
+> ABP Studio automatically decodes Base64-encoded event data. If your event bus uses Base64 encoding for message transport, the data will be displayed in its decoded, readable form.
+
## Exceptions
-This tab displays all exceptions by your C# applications. You can apply filters using the search textbox based on *Message*, *Source*, *ExceptionType*, and *StackTrace* or by choosing a specific application. Additionally, you have the option to select the [Log Level](../framework/fundamentals/exception-handling.md#log-level) for adding a filter. To clear all exceptions, use the *Clear Exceptions* button.
+This tab displays all exceptions thrown by your C# applications. You can apply filters using the search textbox based on *Message*, *Source*, *ExceptionType*, and *StackTrace* or by choosing a specific application. Additionally, you have the option to select the [Log Level](../framework/fundamentals/exception-handling.md#log-level) for filtering. To clear all exceptions, use the *Clear Exceptions* button.

-Click on a row to inspect the details of each exception; `Application`, `Exception Type`, `Source`, `Timestamp`, `Level`, `Message` and `StackTrace`.
+### Exception List
+
+The exception list shows a summary of each exception:
+
+- **Exception Type**: The short exception type name (e.g., `NullReferenceException` instead of `System.NullReferenceException`).
+- **Message**: A truncated version of the exception message.
+- **Timestamp**: When the exception occurred.
+- **Level**: The log level (Error, Warning, etc.).
+
+### Exception Details
+
+Click on a row to inspect the full details of each exception:
+
+- **Application**: The application where the exception occurred.
+- **Exception Type**: The full exception type name including namespace.
+- **Source**: The source file or component where the exception originated.
+- **Timestamp**: When the exception was thrown.
+- **Level**: The log level associated with this exception.
+- **Message**: The complete exception message.
+- **StackTrace**: The full stack trace showing the call hierarchy.

+### Inner Exceptions
+
+If an exception contains inner exceptions, they are displayed hierarchically in the details panel. This allows you to trace the root cause of wrapped exceptions, which is common in scenarios like database errors wrapped in application-level exceptions.
+
## Logs
-The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*. When you select a *Log Level* it shows selected log level and higher log levels. For example, if you select *Warning* it shows *Warning*, *Error* and *Critical* logs. To clear selected application logs, use the *Clear Logs* button. If *Auto Scroll* is checked, the display automatically scrolls when new logs are received.
+The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application from the dropdown. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*.

+### Log Level Filtering
+
+When you select a *Log Level*, it shows the selected level and all higher severity levels:
+
+| Selected Level | Shows |
+|---------------|-------|
+| **Trace** | Trace, Debug, Information, Warning, Error, Critical |
+| **Debug** | Debug, Information, Warning, Error, Critical |
+| **Information** | Information, Warning, Error, Critical |
+| **Warning** | Warning, Error, Critical |
+| **Error** | Error, Critical |
+| **Critical** | Critical only |
+| **None** | All logs (no filtering) |
+
+### Log Display Features
+
+- **Color Coding**: Log entries are color-coded by level for quick visual identification. Errors and critical logs stand out with distinct colors.
+- **Auto Scroll**: When enabled, the display automatically scrolls to show new logs as they arrive. This is useful for real-time monitoring.
+- **Clear Logs**: Clears the logs for the currently selected application only. Other applications' logs remain intact.
+- **Text Filter**: Search within log messages using the search textbox. The filter is applied in real-time with a slight delay for performance.
+
## Tools
-The *Tools* tab allows you to easily access to the user interfaces of the tools you are using. A *tool* may be related with a docker container, or independent. If it is related with a container (ex: *grafana*), the tool is opened when the container is up. If the tool is independent, it will be always opened.
+The *Tools* tab provides quick access to web-based management interfaces for infrastructure services like Grafana, RabbitMQ, pgAdmin, and Redis Commander. Each tool opens in a dedicated browser tab within ABP Studio, eliminating the need to switch between external browser windows.

-The microservice template comes with pre-defined tools to display related container user interfaces. You can edit existing tools, add new tools or delete existing tools.
+The microservice template includes pre-configured tools for common infrastructure services. You can customize these tools or add new ones based on your project requirements.
-In the example below, a new tool named `My Application Status` will be added to the tools and it will display the URL in the input:
+### Adding a New Tool
+
+To add a new tool, click the *+* button in the *Tools* tab. This opens the *Create Tool* dialog where you can configure the tool properties.

+### Tool Properties
+
+Each tool has the following configurable properties:
+
+| Property | Required | Description |
+|----------|----------|-------------|
+| **Name** | Yes | A unique identifier displayed as the tab header. |
+| **URL** | Yes | The web interface URL (e.g., `http://localhost:3000`). |
+| **Related Container** | No | Docker container name. When set, the tool activates only when this container is running. |
+| **Related Kubernetes Service** | No | A regex pattern to match Kubernetes service names for automatic URL switching. |
+| **Related Kubernetes Service Port** | No | The port to use when connecting via Kubernetes service. |
+
+### Editing and Removing Tools
+
+- **Edit**: Right-click on a tool tab and select *Edit* to modify its properties.
+- **Remove**: Right-click on a tool tab and select *Close* to remove it from the profile.
+- **Clear Cookies**: Right-click on a tool tab and select *Clear Cookies* to reset the browser session for that tool.
+
+### Tool Activation States
+
+Tools can be in different activation states depending on their configuration:
+
+| State | Condition | Behavior |
+|-------|-----------|----------|
+| **Always Active** | No *Related Container* specified | Tool is always accessible regardless of container state. |
+| **Container-Dependent** | *Related Container* specified | Tool activates only when the specified Docker container is running. |
+| **Kubernetes-Aware** | *Related Kubernetes Service* specified | Tool URL switches between local and Kubernetes endpoints automatically. |
+
+### Kubernetes Integration
+
+When you specify a *Related Kubernetes Service*, the tool gains the ability to seamlessly switch between local and Kubernetes environments. This is particularly useful for microservice development where you run some services locally while others remain in a Kubernetes cluster.
+
+**Automatic URL Switching:**
+
+1. **Local Mode**: When the *Related Container* is running, the tool uses the configured *URL* (e.g., `http://localhost:3000`).
+2. **Kubernetes Mode**: When the container stops and you're [connected to a Kubernetes cluster](./kubernetes.md#connecting-to-a-kubernetes-cluster), the tool automatically redirects to the matching Kubernetes service.
+3. **Pattern Matching**: The *Related Kubernetes Service* accepts regex patterns. For example, `.*-grafana` matches any service name ending with `-grafana`.
+
+> This automatic switching eliminates the need to manually update URLs when transitioning between local development and Kubernetes-based testing.
+
+### Run Profile Configuration
+
+Tools are persisted in the Run Profile file (`.abprun.json`). Below is an example configuration with common infrastructure tools:
+
+```json
+{
+ "tools": {
+ "grafana": {
+ "url": "http://localhost:3000",
+ "relatedContainer": "grafana",
+ "relatedKubernetesService": ".*-grafana",
+ "relatedKubernetesServicePort": 3000
+ },
+ "rabbitmq": {
+ "url": "http://localhost:15672",
+ "relatedContainer": "rabbitmq",
+ "relatedKubernetesService": ".*-rabbitmq",
+ "relatedKubernetesServicePort": 15672
+ },
+ "redis-commander": {
+ "url": "http://localhost:8081",
+ "relatedContainer": "redis-commander"
+ },
+ "pgadmin": {
+ "url": "http://localhost:5050",
+ "relatedContainer": "pgadmin"
+ },
+ "seq": {
+ "url": "http://localhost:5341"
+ }
+ }
+}
+```
+
+### Default Credentials
+
+Some tools display a notification bar with default credentials when opened for the first time:
+
+| Tool | Username | Password |
+|------|----------|----------|
+| Grafana | `admin` | `admin` |
+| RabbitMQ | `guest` | `guest` |
+
+> You can dismiss this notification permanently by clicking the *Don't show again* option.
diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md
index d2eba88e0f..13e206127e 100644
--- a/docs/en/studio/release-notes.md
+++ b/docs/en/studio/release-notes.md
@@ -9,7 +9,14 @@
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
+## 2.1.4 (2025-12-30) Latest
+
+* Fixed books sample for blazor-webapp tiered solution.
+* Fixed K8s cluster deployment issues for microservices.
+* Fixed docker build problem on microservice template.
+* Showed logs of the executed tasks.
+
+## 2.1.3 (2025-12-15)
* Updated `createCommand` and CLI help for multi-tenancy.
* Fixed `BookController` templating problem.
diff --git a/docs/en/studio/running-applications.md b/docs/en/studio/running-applications.md
index c922c1b501..1a0b22ae3c 100644
--- a/docs/en/studio/running-applications.md
+++ b/docs/en/studio/running-applications.md
@@ -77,7 +77,9 @@ We can use common [dotnet](https://learn.microsoft.com/en-us/dotnet/core/tools)
### Add
-We can add 3 different item type to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application` and `Folder`.
+We can add 4 different item types to *Profile* for defining the tree structure. Those options are `C# Application`, `CLI Application`, `Docker Container`, and `Folder`.
+
+> Note: The `Docker Container` option is only available when right-clicking the profile root. When right-clicking a folder, only `C# Application`, `CLI Application`, and `Folder` options are available.

@@ -181,13 +183,15 @@ You can see the context menu by right-clicking *Folder*. It will start/stop all
## C# Application
-The .NET icon indicates that the application is a C# project. After we [add](#c-application) the C# applications to the root of the tree or folder, we can go to any C# application and right-click to view the context menu; `Start`, `Build`, `Browse`, `Requests`, `Exceptions`, `Logs`, `Copy URL`, `Properties`, `Remove`.
+The .NET icon indicates that the application is a C# project. After we [add](#c-application) the C# applications to the root of the tree or folder, we can go to any C# application and right-click to view the context menu; `Start`, `Stop`, `Restart`, `Build`, `Browse`, `Health Status`, `Requests`, `Exceptions`, `Logs`, `Copy URL`, `Properties`, `Remove`, and `Open with`.

-### Start
+### Start / Stop / Restart
-Starts the selected application. Once it is started, *Stop* and *Restart* options will be available.
+- **Start**: Starts the selected application.
+- **Stop**: Stops the running application.
+- **Restart**: Restarts the running application. This option is only available when the application is started.
> When you start the C# application, you should see a *chain* icon next to the application name, that means the started application connected to ABP Studio. C# applications can connect to ABP Studio even when running from outside the ABP Studio environment, for example debugging with Visual Studio. If the application is run from outside the ABP Studio environment, it will display *(external)* information next to the chain icon.
@@ -199,14 +203,17 @@ It's the [similar](#build) options like root of the tree options. The only diffe
### Monitoring
-When the C# application is connected to ABP Studio, it starts sending telemetry information to see in one place. We can easily click these options to see the detail; `Browse`, `Requests`, `Exceptions` and `Logs`.
+When the C# application is connected to ABP Studio, it starts sending telemetry information to see in one place. We can easily click these options to see the detail; `Browse`, `Health Status`, `Requests`, `Exceptions`, `Events` and `Logs`.

- `Browse`: ABP Studio includes a browser tool for accessing websites and running applications. You can click this option to view the application in the ABP Studio browser. However, this option is only accessible if the application is started.
-- Health Status : If Health Check endpoints are defined, it allows you to browse Health UI and see the latest health check response.
+- `Health Status`: A submenu that provides health monitoring options when Health Check endpoints are defined:
+ - **Browse Health UI**: Opens the Health UI page of the application in the built-in browser.
+ - **Show Latest Health Check Response**: Displays the latest health check response in JSON format.
- `Requests`: It opens the *HTTP Requests* tab with adding the selected application filter. You can view all *HTTP Requests* received by your applications.
- `Exceptions`: We can display all exceptions on this tab. It opens the *Exceptions* tab with selected application.
+- `Events`: Opens the *Events* tab filtered by this application. You can view all [Distributed Events](../framework/infrastructure/event-bus/distributed) sent or received by this application.
- `Logs`: Clicking this option opens the *Logs* tab with adding the selected application filter.
### Properties
@@ -215,16 +222,35 @@ We can open the *Application Properties* window to change *Launch url*, *Health

-- **Health check endpoint**: Endpoint for controlling the health status of the application periodically. If the application doesn't have a endpoint for health check, you can enter `/` to use the home page of the application as health check endpoint.
+- **Launch URL**: The URL used when browsing the application. This is the address where the application is accessible.
+- **Kubernetes service**: A regex pattern to match Kubernetes service names. When connected to a Kubernetes cluster, ABP Studio uses this pattern to find the corresponding Kubernetes service and uses its URL instead of the Launch URL. This applies to *Browse*, *Copy URL*, and *Health UI* features. For example, if your Helm chart creates a service named `bookstore-identity-service`, you can use `.*-identity-service` or `bookstore-identity.*` as the pattern. For [microservice](../get-started/microservice.md) templates, this is pre-configured.
+- **Health check endpoint**: Endpoint for controlling the health status of the application periodically. If the application doesn't have an endpoint for health check, you can enter `/` to use the home page of the application as health check endpoint.
- **Health UI endpoint**: Endpoint of the Health UI page of the application.
-- **Skip build before starting**: When enabled, application is started without build and it makes starting faster. This is useful when you are working on a single application out of multiple, so you don't need to build others everytime they start.
-- **Watch changes while running**: When enabled, you should see an *eye* icon next to the application name.
+- **Skip build before starting**: When enabled, the application is started without building, making startup faster. This is useful when you are working on a single application out of multiple, so you don't need to build others every time they start.
+- **Watch changes while running**: When enabled, changes in your code are watched and dotnet hot-reloads the application or restarts it if needed. You should see an *eye* icon next to the application name when this is enabled.
+- **Open browser on start**: When enabled, the application is automatically opened in the Browse tab when it starts.
+- **Auto refresh browser on restart**: When enabled, browser tabs showing this application are automatically refreshed when the application restarts.
+- **Runnable**: Controls whether the application can be started from Solution Runner. When disabled, the application will not be included in batch start operations.

+### Open with
+
+The *Open with* submenu provides options to open the application project in external tools:
+
+- **Visual Studio**: Opens the project in Visual Studio (available if installed).
+- **Visual Studio Code**: Opens the project folder in Visual Studio Code (available if installed).
+- **JetBrains Rider**: Opens the project in JetBrains Rider (available if installed).
+- **Terminal**: Opens a terminal window in the project directory.
+- **Explorer / Finder**: Opens the project folder in the system file explorer.
+
+### Custom Commands
+
+You can add custom commands that appear in the context menu of Solution Runner items (root, folders, and applications). These commands allow you to automate custom workflows and scripts. For details on creating and managing custom commands, see the [Custom Commands](custom-commands.md) documentation.
+
### Miscellaneous
-- We can copy the selected application *Browse* URL with *Copy URL*. It copies the *Browse* URL instead *Launch URL* since we could connected to *Kubernetes* service.
+- We can copy the selected application *Browse* URL with *Copy URL*. It copies the *Browse* URL instead of *Launch URL* since we could be connected to a *Kubernetes* service.
- You can change the target framework by right-click the selected application and change the *Target Framework* option. This option visible if the project has multiple target framework such as MAUI applications.
- To remove an application from the tree, open the context menu by right-clicking the application and selecting *Remove*.
diff --git a/docs/en/studio/solution-explorer.md b/docs/en/studio/solution-explorer.md
index 21dde505e5..edbac3b471 100644
--- a/docs/en/studio/solution-explorer.md
+++ b/docs/en/studio/solution-explorer.md
@@ -33,6 +33,9 @@ It is the main solution that you can open with ABP Studio, an ABP solution can c
- `New Folder`: Creates a new folder within the ABP Solution, allowing you to organize your modules.
- `New Module`: Allows you to create a new module.
- `Existing Module`: You can add existing module to your solution.
+- `Analyze`: Analyzes all modules and packages in the solution to extract information like aggregate roots, application services, permissions, etc.
+ - `Analyze`: Analyzes all modules and packages using existing build outputs.
+ - `Build & Analyze`: Builds the solution first, then analyzes all modules and packages.
- `Rename`: Renames the solution.
- `Manage Secrets`: You can edit your solution secrets.
- `Manage Metadata`: You can edit your solution metadata.
@@ -52,6 +55,9 @@ It is the main solution that you can open with ABP Studio, an ABP solution can c
- `Clean`: Cleans the output of the previous build for modules.
- `Restore`: Restores the dependencies for modules.
- `Open With`
+ - `Visual Studio`: Opens the solution in Visual Studio. This option is only available if you have Visual Studio installed.
+ - `Visual Studio Code`: Opens the solution in Visual Studio Code. This option is only available if you have Visual Studio Code installed.
+ - `JetBrains Rider`: Opens the solution in JetBrains Rider. This option is only available if you have JetBrains Rider installed.
- `Terminal`: Opens the terminal in the solution directory.
- `Explorer`: Opens the file explorer in the solution directory.
- `Solution Configuration`: You can see the project creation options in this menu. It opens the *Solution Configuration* window.
@@ -73,6 +79,9 @@ You can click the *OK* button to add the folder to the solution. When you right-
- `New Folder`: Creates a new nested folder within the selected folder, allowing you to organize your modules.
- `New Module`: Allows you to create a new module to selected folder.
- `Existing Module`: You can add existing module to your selected folder.
+- `Analyze`: Analyzes all modules and packages in the selected folder.
+ - `Analyze`: Analyzes all modules and packages using existing build outputs.
+ - `Build & Analyze`: Builds first, then analyzes all modules and packages in the folder.
- `Rename`: Renames the selected folder.
- `Delete`: Deletes the selected folder and all child items from the solution.
- `ABP CLI`
@@ -102,6 +111,9 @@ A [module](./concepts.md#module) is a sub-solution that can contains zero, one o
- `New Package`: Creates a new package within the selected module.
- `Existing Package`: You can add existing package to your selected module.
- `Folder`: Creates a new folder within the selected module, allowing you to organize your packages.
+- `Analyze`: Analyzes all packages in the selected module.
+ - `Analyze`: Analyzes all packages using existing build outputs.
+ - `Build & Analyze`: Builds first, then analyzes all packages in the module.
- `Import Module`: This option allows you to import an existing module from *Solution*, *Local*, or *NuGet* into your selected module.
- `Rename`: Renames the selected module.
- `Remove`: Removes the selected module and all child items from the solution.
@@ -126,6 +138,7 @@ A [module](./concepts.md#module) is a sub-solution that can contains zero, one o
- `JetBrains Rider`: Opens the module in JetBrains Rider. This option is only available if you have JetBrains Rider installed.
- `Terminal`: Opens the terminal in the module directory.
- `Explorer`: Opens the file explorer in the module directory.
+- `Open Readme`: Opens the README file in the module if available. This option is only visible if the module has a README file.
- `Upgrade to Pro`: This will be visible only when you purchased a license but still using the modules came with open-source (free) license. For more details, check out [Migrating from Open Source Templates](../guides/migrating-from-open-source.md) document. This is not shown in the screenshot above.
### Adding a New Empty Module
@@ -238,10 +251,18 @@ A [package](./concepts.md#package) is a project that can be added to a module, a

-- `Add Package Reference`: This option allows you to add a package reference to the selected package.
+- `Add`: This menu is only visible for Host projects (except Blazor WebAssembly).
+ - `NPM Package`: Adds an NPM package reference to the selected package.
+- `Add Package Reference`: This option allows you to add a package reference to the selected package. This option is only visible for non-Host projects.
+- `Analyze`: Analyzes the selected package to extract information like aggregate roots, application services, permissions, etc. This menu is only visible for analyzable package types.
+ - `Analyze`: Analyzes the package using existing build outputs.
+ - `Build & Analyze`: Builds first, then analyzes the package.
- `Reload`: Reloads the selected package.
- `Remove`: Removes the selected package from the module.
- `ABP CLI`
+ - `Generate Proxy`: Generates client-side proxy code for the selected package.
+ - `Javascript`: Generates JavaScript proxy code.
+ - `C#`: Generates C# proxy code.
- `Install Libs`: Install NPM Packages for UI projects in your selected package.
- `Upgrade ABP Packages`: Update all the ABP related NuGet and NPM packages in your selected package.
- `Switch to`: It switches your selected package to the specified version of the ABP.
@@ -264,6 +285,7 @@ A [package](./concepts.md#package) is a project that can be added to a module, a
- `JetBrains Rider`: Opens the package in JetBrains Rider. This option is only available if you have JetBrains Rider installed.
- `Terminal`: Opens the terminal in the package directory.
- `Explorer`: Opens the file explorer in the package directory.
+- `Open Readme`: Opens the README file in the package if available. This option is only visible if the package has a README file.
### Adding a New Package
diff --git a/docs/en/studio/working-with-suite.md b/docs/en/studio/working-with-suite.md
index b436b7b0f8..2dc3a4bcc0 100644
--- a/docs/en/studio/working-with-suite.md
+++ b/docs/en/studio/working-with-suite.md
@@ -39,7 +39,10 @@ If There are more than one module which is openable via Suite, Studio will ask y
### Opening from context menu
-Alternatively, you can right click to a module in [Solution Explorer](solution-explorer.md) and click `ABP Suite` to open it with Suite. Suite will automatically open `Crud Page Generation` screen with the selected module.
+You can right-click on the solution root or a module in [Solution Explorer](solution-explorer.md) and click `ABP Suite` to open it with Suite.
+
+- **Solution root**: If you right-click on the solution root and select `ABP Suite`, Studio will ask you to pick a module if there are multiple modules. Suite will then open with the selected module.
+- **Module**: If you right-click on a specific module and select `ABP Suite`, Suite will automatically open `Crud Page Generation` screen with that module.

@@ -51,10 +54,14 @@ Standard application solutions (`App` & `App-nolayers`) generated by Studio are
You can generate code on the services of the microservice solution. UI code generation is not supported at the moment. It is on the roadmap.
-## Managing Installed Version
+## Managing Installed Version
You can update or downgrade the version of `ABP Suite` by `Change Version` button in `ABP Suite` menu.
+### Automatic Version Matching
+
+When you open ABP Suite for a solution, Studio checks if the installed Suite version matches the solution's ABP version. If they differ, Studio will prompt you to update or downgrade Suite to match your solution's version. This ensures compatibility between Suite and your project.
+


diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md
index afc2536709..160f89216f 100644
--- a/docs/en/suite/generating-crud-page.md
+++ b/docs/en/suite/generating-crud-page.md
@@ -270,6 +270,29 @@ In the example above, the `IdentityUser` entity is selected as the navigation pr
> **Note:** Ensure that your solution is built properly before establishing relationship between your own entity and a module entity because ABP Suite scans assemblies and finds which ABP modules you are using and lists their entities in the navigation property model if you have checked the **Include entities from ABP modules** checkbox.
+#### Extending with Custom Module Entities
+
+If you want to extend ABP Suite's system to list entities from your own custom modules (not just ABP's built-in modules), you can configure the `module-entity-extension.json` file. This file is located in the `.suite` folder at the root of your solution (`/.suite/module-entity-extension.json`).
+
+Here is the default sample file content:
+
+```json
+{
+ "Modules": [
+ {
+ "DomainProjectDllFileName": "MySampleModule.MyProject.Domain.dll"
+ }
+ ]
+}
+```
+
+By defining the `DomainProjectDllFileName` property, ABP Suite will scan the specified module's **.dll** and list its entities in the navigation property model. This allows you to create navigation properties that reference entities from your custom modules.
+
+> **Important:** When extending with custom module entities, ensure that:
+> - Your current solution properly depends on the related module.
+> - All module references are correctly configured.
+> - The solution is built successfully before attempting to establish relationships.
+
#### Adding An Existing Entity as a Navigation Property
Alternatively, you can add `IdentityUser` entity (or any other entity) as a navigation property to an entity by manually entering the required information. See the screenshot below:
diff --git a/docs/en/testing/integration-tests.md b/docs/en/testing/integration-tests.md
index c87d270747..d28ed5ca5b 100644
--- a/docs/en/testing/integration-tests.md
+++ b/docs/en/testing/integration-tests.md
@@ -34,13 +34,13 @@ The startup template is configured to use **in-memory SQLite** database for the
Using in-memory SQLite database has two main advantages;
* It is faster compared to an external DBMS.
-* It create a **new fresh database** for each test case, so tests doesn't affect each other.
+* It creates a **new fresh database** for each test case, so tests don't affect each other.
> **Tip**: Do not use EF Core's In-Memory database for advanced integration tests. It is not a real DBMS and has many differences in details. For example, it doesn't support transaction and rollback scenarios, so you can't truly test the failing scenarios. On the other hand, In-Memory SQLite is a real DBMS and supports the fundamental SQL database features.
## The Seed Data
-Writing tests against an empty database is not practical. In most cases, you need to some initial data in the database. For example, if you write a test class that query, update and delete the products, it would be helpful to have a few products in the database before executing the test case.
+Writing tests against an empty database is not practical. In most cases, you need some initial data in the database. For example, if you write a test class that queries, updates and deletes the products, it would be helpful to have a few products in the database before executing the test case.
ABP's [Data Seeding](../framework/infrastructure/data-seeding.md) system is a powerful way to seed the initial data. The application startup template has a *YourProject*TestDataSeedContributor class in the `.TestBase` project. You can fill it to have an initial data that you can use for each test method.
@@ -401,7 +401,7 @@ public class EfCoreIssueAppService_Tests : IssueAppService_Tests By deriving from the related abstract classes, now we can see the all tests in the test explorers and run them.
+> By deriving from the related abstract classes, now we can see all the tests in the test explorers and run them.

diff --git a/docs/en/testing/overall.md b/docs/en/testing/overall.md
index 154c169d5b..15e9abf725 100644
--- a/docs/en/testing/overall.md
+++ b/docs/en/testing/overall.md
@@ -69,7 +69,7 @@ The startup solution has the following libraries already installed;
* [NSubstitute](https://nsubstitute.github.io/) as the mocking library.
* [Shouldly](https://github.com/shouldly/shouldly) as the assertion library.
-While you are free to replace them with your favorite tools, this document and examples will be base on these tooling.
+While you are free to replace them with your favorite tools, this document and examples will be based on these tooling.
## The Test Explorer
diff --git a/docs/en/testing/unit-tests.md b/docs/en/testing/unit-tests.md
index cfc63a4848..2e06a44efb 100644
--- a/docs/en/testing/unit-tests.md
+++ b/docs/en/testing/unit-tests.md
@@ -73,7 +73,7 @@ namespace MyProject.Issues
Notice that the `IsClosed` and `CloseDate` properties have private setters to force some business rules by using the `Open()` and `Close()` methods:
-* Whenever you close an issue, the `CloseDate` should be set to the [current time](../framework/infrastructure/virtual-file-system.md).
+* Whenever you close an issue, the `CloseDate` should be set to the current time.
* An issue can not be re-opened if it is locked. And if it is re-opened, the `CloseDate` should be set to `null`.
Since the `Issue` entity is a part of the Domain Layer, we should test it in the `Domain.Tests` project. Create an `Issue_Tests` class inside the `Domain.Tests` project:
@@ -160,7 +160,7 @@ public void Should_Not_Allow_To_ReOpen_A_Locked_Issue()
`Assert.Throws` checks if the executed code throws a matching exception.
-> See the [xUnit](https://xunit.net/#documentation) & [Shoudly](https://docs.shouldly.org/) documentation to learn more about these libraries.
+> See the [xUnit](https://xunit.net/#documentation) & [Shouldly](https://docs.shouldly.org/) documentation to learn more about these libraries.
## Classes With Dependencies
diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs
index 247e6e354e..dfe79d7e01 100644
--- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs
+++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/ChatClientAccessor.cs
@@ -29,5 +29,13 @@ public class ChatClientAccessor : IChatClientAccessor
ChatClient = serviceProvider.GetKeyedService(
AbpAIWorkspaceOptions.GetChatClientServiceKeyName(
WorkspaceNameAttribute.GetWorkspaceName()));
+
+ // Fallback to default chat client if not configured for the workspace.
+ if (ChatClient is null)
+ {
+ ChatClient = serviceProvider.GetKeyedService(
+ AbpAIWorkspaceOptions.GetChatClientServiceKeyName(
+ AbpAIModule.DefaultWorkspaceName));
+ }
}
}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs
index 72ccfdaf38..9a52f7edc9 100644
--- a/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs
+++ b/framework/src/Volo.Abp.AI/Volo/Abp/AI/TypedChatClient.cs
@@ -9,9 +9,13 @@ public class TypedChatClient : DelegatingChatClient, IChatClient(
+ serviceProvider.GetKeyedService(
AbpAIWorkspaceOptions.GetChatClientServiceKeyName(
WorkspaceNameAttribute.GetWorkspaceName()))
+ ??
+ serviceProvider.GetRequiredKeyedService(
+ AbpAIWorkspaceOptions.GetChatClientServiceKeyName(
+ AbpAIModule.DefaultWorkspaceName))
)
{
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs
index fc7d372b37..dc51022956 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageLayout.cs
@@ -31,6 +31,8 @@ public class PageLayout : IScopedDependency, INotifyPropertyChanged
}
}
+ public bool ShowToolbar { get; set; } = true;
+
public virtual ObservableCollection BreadcrumbItems { get; } = new();
public virtual ObservableCollection ToolbarItems { get; } = new();
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs
index 647538c121..6e47281a70 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/PageToolbars/SimplePageToolbarContributor.cs
@@ -16,6 +16,8 @@ public class SimplePageToolbarContributor : IPageToolbarContributor
public string? RequiredPolicyName { get; }
+ private bool? _shouldAddComponent;
+
public SimplePageToolbarContributor(
Type componentType,
Dictionary? arguments = null,
@@ -38,15 +40,19 @@ public class SimplePageToolbarContributor : IPageToolbarContributor
protected virtual async Task ShouldAddComponentAsync(PageToolbarContributionContext context)
{
- if (RequiredPolicyName != null)
+ if (_shouldAddComponent.HasValue)
+ {
+ return _shouldAddComponent.Value;
+ }
+
+ if (RequiredPolicyName == null)
{
- var authorizationService = context.ServiceProvider.GetRequiredService();
- if (!await authorizationService.IsGrantedAsync(RequiredPolicyName))
- {
- return false;
- }
+ _shouldAddComponent = true;
+ return _shouldAddComponent.Value;
}
- return true;
+ var authorizationService = context.ServiceProvider.GetRequiredService();
+ _shouldAddComponent = await authorizationService.IsGrantedAsync(RequiredPolicyName);
+ return _shouldAddComponent.Value;
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs
index 1e5abce611..533a5b9187 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/AspNetCore/Mvc/UI/Layout/ContentLayout.cs
@@ -11,6 +11,8 @@ public class ContentLayout
public string? MenuItemName { get; set; }
+ public bool ShowToolbar { get; set; } = true;
+
public ContentLayout()
{
BreadCrumb = new BreadCrumb();
@@ -23,11 +25,6 @@ public class ContentLayout
return true;
}
- if (BreadCrumb.ShowCurrent && !Title.IsNullOrEmpty())
- {
- return true;
- }
-
- return false;
+ return BreadCrumb.ShowCurrent || BreadCrumb.ShowHome;
}
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs
new file mode 100644
index 0000000000..a0c9570fc6
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpNoContentApiDescriptionProvider.cs
@@ -0,0 +1,48 @@
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Reflection;
+
+namespace Volo.Abp.AspNetCore.Mvc.ApiExploring;
+
+public class AbpNoContentApiDescriptionProvider : IApiDescriptionProvider, ITransientDependency
+{
+ public virtual void OnProvidersExecuted(ApiDescriptionProviderContext context)
+ {
+ }
+
+ ///
+ /// The order -999 ensures that this provider is executed right after the
+ /// Microsoft.AspNetCore.Mvc.ApiExplorer.DefaultApiDescriptionProvider.
+ ///
+ public int Order => -999;
+
+ public virtual void OnProvidersExecuting(ApiDescriptionProviderContext context)
+ {
+ foreach (var result in context.Results.Where(x => x.IsRemoteService()))
+ {
+ var actionProducesResponseTypeAttributes =
+ ReflectionHelper.GetAttributesOfMemberOrDeclaringType(
+ result.ActionDescriptor.GetMethodInfo());
+ if (actionProducesResponseTypeAttributes.Any(x => x.StatusCode == (int) HttpStatusCode.NoContent))
+ {
+ continue;
+ }
+
+ var returnType = result.ActionDescriptor.GetReturnType();
+ if (returnType == typeof(Task) || returnType == typeof(void))
+ {
+ result.SupportedResponseTypes.Add(new ApiResponseType
+ {
+ // If the return type is Task, then we should treat it as a void return type since we can't infer anything without additional metadata or requiring unreferenced code.
+ Type = typeof(void),
+ StatusCode = (int) HttpStatusCode.NoContent
+ });
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs
index c37e75ef32..8f78e3292a 100644
--- a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs
+++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs
@@ -46,11 +46,14 @@ public static class CookieAuthenticationOptionsExtensions
{
var openIdConnectOptions = await GetOpenIdConnectOptions(principalContext, oidcAuthenticationScheme);
+ var clientId = principalContext.Properties.GetString("client_id");
+ var clientSecret = principalContext.Properties.GetString("client_secret");
+
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority!.EnsureEndsWith('/') + "connect/introspect",
- ClientId = openIdConnectOptions.ClientId!,
- ClientSecret = openIdConnectOptions.ClientSecret,
+ ClientId = clientId ?? openIdConnectOptions.ClientId!,
+ ClientSecret = clientSecret ?? openIdConnectOptions.ClientSecret,
Token = accessToken
});
@@ -82,7 +85,7 @@ public static class CookieAuthenticationOptionsExtensions
return options;
}
- private async static Task GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme)
+ private static async Task GetOpenIdConnectOptions(CookieValidatePrincipalContext principalContext, string oidcAuthenticationScheme)
{
var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService>().Get(oidcAuthenticationScheme);
var cancellationTokenProvider = principalContext.HttpContext.RequestServices.GetRequiredService();
diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs
new file mode 100644
index 0000000000..2df335abd8
--- /dev/null
+++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/AbpHangfirePeriodicBackgroundWorkerAdapterOptions.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace Volo.Abp.BackgroundWorkers.Hangfire;
+
+public class AbpHangfirePeriodicBackgroundWorkerAdapterOptions
+{
+ public TimeZoneInfo TimeZone { get; set; } = TimeZoneInfo.Utc;
+
+ public string Queue { get; set; } = default!;
+}
diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
index fe9a8ad983..64a4a1be64 100644
--- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
+++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
@@ -5,7 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.Common;
+using Hangfire.Storage;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
@@ -30,8 +32,9 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet
BackgroundJobServer = ServiceProvider.GetRequiredService();
}
- public async override Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default)
+ public override async Task AddAsync(IBackgroundWorker worker, CancellationToken cancellationToken = default)
{
+ var logger = ServiceProvider.GetRequiredService>();
var abpHangfireOptions = ServiceProvider.GetRequiredService>().Value;
var defaultQueuePrefix = abpHangfireOptions.DefaultQueuePrefix;
var defaultQueue = abpHangfireOptions.DefaultQueue;
@@ -42,54 +45,90 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet
{
var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker);
- RecurringJob.AddOrUpdate(
- hangfireBackgroundWorker.RecurringJobId,
- hangfireBackgroundWorker.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + hangfireBackgroundWorker.Queue,
- () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken),
- hangfireBackgroundWorker.CronExpression,
- new RecurringJobOptions
- {
- TimeZone = hangfireBackgroundWorker.TimeZone
- });
+ var queueName = hangfireBackgroundWorker.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + hangfireBackgroundWorker.Queue;
+ if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty))
+ {
+ logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead.");
+ RecurringJob.AddOrUpdate(
+ hangfireBackgroundWorker.RecurringJobId,
+ () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken),
+ hangfireBackgroundWorker.CronExpression,
+ new RecurringJobOptions
+ {
+ TimeZone = hangfireBackgroundWorker.TimeZone
+ });
+ }
+ else
+ {
+ RecurringJob.AddOrUpdate(
+ hangfireBackgroundWorker.RecurringJobId,
+ queueName,
+ () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(cancellationToken),
+ hangfireBackgroundWorker.CronExpression,
+ new RecurringJobOptions
+ {
+ TimeZone = hangfireBackgroundWorker.TimeZone
+ });
+ }
break;
}
case AsyncPeriodicBackgroundWorkerBase or PeriodicBackgroundWorkerBase:
{
int? period = null;
- string? CronExpression = null;
+ string? cronExpression = null;
- if (worker is AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorkerBase)
+ switch (worker)
{
+ case AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorkerBase:
period = asyncPeriodicBackgroundWorkerBase.Period;
- CronExpression = asyncPeriodicBackgroundWorkerBase.CronExpression;
- }
- else if (worker is PeriodicBackgroundWorkerBase periodicBackgroundWorkerBase)
- {
+ cronExpression = asyncPeriodicBackgroundWorkerBase.CronExpression;
+ break;
+ case PeriodicBackgroundWorkerBase periodicBackgroundWorkerBase:
period = periodicBackgroundWorkerBase.Period;
- CronExpression = periodicBackgroundWorkerBase.CronExpression;
+ cronExpression = periodicBackgroundWorkerBase.CronExpression;
+ break;
}
- if (period == null && CronExpression.IsNullOrWhiteSpace())
+ if (period == null && cronExpression.IsNullOrWhiteSpace())
{
+ logger.LogError(
+ $"Cannot add periodic background worker {worker.GetType().FullName} to Hangfire scheduler, because both Period and CronExpression are not set. " +
+ "You can either set Period or CronExpression property of the worker."
+ );
return;
}
- var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
- var workerAdapter = (Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker)!;
-
+ var workerAdapter = (ServiceProvider.GetRequiredService(typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker))) as IHangfireBackgroundWorker)!;
Expression> methodCall = () => workerAdapter.DoWorkAsync(cancellationToken);
var recurringJobId = !workerAdapter.RecurringJobId.IsNullOrWhiteSpace() ? workerAdapter.RecurringJobId : GetRecurringJobId(worker, methodCall);
- RecurringJob.AddOrUpdate(
- recurringJobId,
- workerAdapter.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + workerAdapter.Queue,
- methodCall,
- CronExpression ?? GetCron(period!.Value),
- new RecurringJobOptions
- {
- TimeZone = workerAdapter.TimeZone
- });
+ var queueName = workerAdapter.Queue.IsNullOrWhiteSpace() ? defaultQueue : defaultQueuePrefix + workerAdapter.Queue;
+ if (!JobStorage.Current.HasFeature(JobStorageFeatures.JobQueueProperty))
+ {
+ logger.LogError($"Current storage doesn't support specifying queues({queueName}) directly for a specific job. Please use the QueueAttribute instead.");
+ RecurringJob.AddOrUpdate(
+ recurringJobId,
+ methodCall,
+ cronExpression ?? GetCron(period!.Value),
+ new RecurringJobOptions
+ {
+ TimeZone = workerAdapter.TimeZone
+ });
+ }
+ else
+ {
+ RecurringJob.AddOrUpdate(
+ recurringJobId,
+ queueName,
+ methodCall,
+ cronExpression ?? GetCron(period!.Value),
+ new RecurringJobOptions
+ {
+ TimeZone = workerAdapter.TimeZone
+ });
+ }
+
break;
}
default:
@@ -98,7 +137,7 @@ public class HangfireBackgroundWorkerManager : BackgroundWorkerManager, ISinglet
}
}
- private readonly static MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static);
+ private static readonly MethodInfo? GetRecurringJobIdMethodInfo = typeof(RecurringJob).GetMethod("GetRecurringJobId", BindingFlags.NonPublic | BindingFlags.Static);
protected virtual string? GetRecurringJobId(IBackgroundWorker worker, Expression> methodCall)
{
string? recurringJobId = null;
diff --git a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs
index 43e9b4a95c..cf5945aaf1 100644
--- a/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs
+++ b/framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs
@@ -1,7 +1,9 @@
-using System.Reflection;
+using System;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@@ -11,14 +13,17 @@ public class HangfirePeriodicBackgroundWorkerAdapter : HangfireBackgrou
private readonly MethodInfo _doWorkAsyncMethod;
private readonly MethodInfo _doWorkMethod;
- public HangfirePeriodicBackgroundWorkerAdapter()
+ public HangfirePeriodicBackgroundWorkerAdapter(IOptions options)
{
+ TimeZone = options.Value.TimeZone;
+ Queue = options.Value.Queue;
+ RecurringJobId = BackgroundWorkerNameAttribute.GetNameOrNull();
+
_doWorkAsyncMethod = typeof(TWorker).GetMethod("DoWorkAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic)!;
- RecurringJobId = BackgroundWorkerNameAttribute.GetNameOrNull();
}
- public async override Task DoWorkAsync(CancellationToken cancellationToken = default)
+ public override async Task DoWorkAsync(CancellationToken cancellationToken = default)
{
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider, cancellationToken);
var worker = ServiceProvider.GetRequiredService();
@@ -26,13 +31,11 @@ public class HangfirePeriodicBackgroundWorkerAdapter : HangfireBackgrou
switch (worker)
{
case AsyncPeriodicBackgroundWorkerBase asyncPeriodicBackgroundWorker:
- await (Task)(_doWorkAsyncMethod.Invoke(asyncPeriodicBackgroundWorker, new object[] { workerContext })!);
+ await (Task)(_doWorkAsyncMethod.Invoke(asyncPeriodicBackgroundWorker, [workerContext])!);
break;
case PeriodicBackgroundWorkerBase periodicBackgroundWorker:
- _doWorkMethod.Invoke(periodicBackgroundWorker, new object[] { workerContext });
+ _doWorkMethod.Invoke(periodicBackgroundWorker, [workerContext]);
break;
}
}
-
-
}
diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
index ae571cb5f9..6ffaa96ecf 100644
--- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
+++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/DistributedCache.cs
@@ -19,7 +19,7 @@ namespace Volo.Abp.Caching;
/// Represents a distributed cache of type.
///
/// The type of cache item being cached.
-public class DistributedCache :
+public class DistributedCache :
IDistributedCache
where TCacheItem : class
{
@@ -683,35 +683,30 @@ public class DistributedCache : IDistributedCache x.Value != null))
- {
- return result!;
- }
+ var resultMap = result
+ .Where(x => x.Value != null)
+ .ToDictionary(x => x.Key, x => x.Value);
- var missingKeys = new List();
- var missingValuesIndex = new List();
- for (var i = 0; i < keyArray.Length; i++)
+ if (resultMap.Count == keyArray.Length)
{
- if (result[i].Value != null)
- {
- continue;
- }
-
- missingKeys.Add(keyArray[i]);
- missingValuesIndex.Add(i);
+ return keyArray
+ .Select(key => new KeyValuePair(key, resultMap[key]))
+ .ToArray();
}
+ var missingKeys = keyArray.Where(key => !resultMap.ContainsKey(key)).ToList();
var missingValues = factory.Invoke(missingKeys).ToArray();
- var valueQueue = new Queue>(missingValues);
SetMany(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow);
- foreach (var index in missingValuesIndex)
+ foreach (var pair in missingValues)
{
- result[index] = valueQueue.Dequeue()!;
+ resultMap[pair.Key] = pair.Value;
}
- return result;
+ return keyArray
+ .Select(key => new KeyValuePair(key, resultMap.GetOrDefault(key)))
+ .ToArray();
}
@@ -779,35 +774,30 @@ public class DistributedCache : IDistributedCache x.Value != null))
- {
- return result;
- }
+ var resultMap = result
+ .Where(x => x.Value != null)
+ .ToDictionary(x => x.Key, x => x.Value);
- var missingKeys = new List();
- var missingValuesIndex = new List();
- for (var i = 0; i < keyArray.Length; i++)
+ if (resultMap.Count == keyArray.Length)
{
- if (result[i].Value != null)
- {
- continue;
- }
-
- missingKeys.Add(keyArray[i]);
- missingValuesIndex.Add(i);
+ return keyArray
+ .Select(key => new KeyValuePair(key, resultMap[key]))
+ .ToArray();
}
+ var missingKeys = keyArray.Where(key => !resultMap.ContainsKey(key)).ToList();
var missingValues = (await factory.Invoke(missingKeys)).ToArray();
- var valueQueue = new Queue>(missingValues);
await SetManyAsync(missingValues, optionsFactory?.Invoke(), hideErrors, considerUow, token);
- foreach (var index in missingValuesIndex)
+ foreach (var pair in missingValues)
{
- result[index] = valueQueue.Dequeue()!;
+ resultMap[pair.Key] = pair.Value;
}
- return result;
+ return keyArray
+ .Select(key => new KeyValuePair(key, resultMap.GetOrDefault(key)))
+ .ToArray();
}
///
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
index 078d1aa6cc..23cb5c2b33 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
+++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
@@ -14,6 +14,7 @@
+
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 8ff8ad3206..a188137ea2 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
@@ -79,6 +79,7 @@ public class AbpCliCoreModule : AbpModule
options.Commands[ClearDownloadCacheCommand.Name] = typeof(ClearDownloadCacheCommand);
options.Commands[RecreateInitialMigrationCommand.Name] = typeof(RecreateInitialMigrationCommand);
options.Commands[GenerateRazorPage.Name] = typeof(GenerateRazorPage);
+ options.Commands[McpCommand.Name] = typeof(McpCommand);
options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Pro");
options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Lite");
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs
new file mode 100644
index 0000000000..e9ae5ba77c
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgsExtensions.cs
@@ -0,0 +1,11 @@
+using Volo.Abp.Cli.Commands;
+
+namespace Volo.Abp.Cli.Args;
+
+public static class CommandLineArgsExtensions
+{
+ public static bool IsMcpCommand(this CommandLineArgs args)
+ {
+ return args.IsCommand(McpCommand.Name);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs
index 78a36fe329..43436329fb 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliConsts.cs
@@ -1,4 +1,4 @@
-namespace Volo.Abp.Cli;
+namespace Volo.Abp.Cli;
public static class CliConsts
{
@@ -20,8 +20,12 @@ public static class CliConsts
public static string AppSettingsSecretJsonFileName = "appsettings.secrets.json";
+ public const string McpLogLevelEnvironmentVariable = "ABP_MCP_LOG_LEVEL";
+ public const string DefaultMcpServerUrl = "https://mcp.abp.io";
+
public static class MemoryKeys
{
public const string LatestCliVersionCheckDate = "LatestCliVersionCheckDate";
+ public const string McpToolsLastFetchDate = "McpToolsLastFetchDate";
}
}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
index d47987b220..537c794c15 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using System.Text;
@@ -14,6 +14,9 @@ public static class CliPaths
public static string Memory => Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!, "memory.bin");
public static string Build => Path.Combine(AbpRootPath, "build");
public static string Lic => Path.Combine(Path.GetTempPath(), Encoding.ASCII.GetString(new byte[] { 65, 98, 112, 76, 105, 99, 101, 110, 115, 101, 46, 98, 105, 110 }));
+ public static string McpToolsCache => Path.Combine(Root, "mcp-tools.json");
+ public static string McpLog => Path.Combine(Log, "mcp.log");
+ public static string McpConfig => Path.Combine(Root, "mcp-config.json");
public static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp");
}
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 063ceddebf..52bbac8e43 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
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Versioning;
@@ -10,6 +10,7 @@ using System.Reflection;
using System.Threading.Tasks;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands;
+using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.Memory;
using Volo.Abp.Cli.Version;
using Volo.Abp.Cli.Utils;
@@ -21,8 +22,11 @@ namespace Volo.Abp.Cli;
public class CliService : ITransientDependency
{
+ private const string McpLogSource = nameof(CliService);
+
private readonly MemoryService _memoryService;
private readonly ITelemetryService _telemetryService;
+ private readonly IMcpLogger _mcpLogger;
public ILogger Logger { get; set; }
protected ICommandLineArgumentParser CommandLineArgumentParser { get; }
protected ICommandSelector CommandSelector { get; }
@@ -39,7 +43,8 @@ public class CliService : ITransientDependency
ICmdHelper cmdHelper,
MemoryService memoryService,
CliVersionService cliVersionService,
- ITelemetryService telemetryService)
+ ITelemetryService telemetryService,
+ IMcpLogger mcpLogger)
{
_memoryService = memoryService;
CommandLineArgumentParser = commandLineArgumentParser;
@@ -49,19 +54,27 @@ public class CliService : ITransientDependency
CmdHelper = cmdHelper;
CliVersionService = cliVersionService;
_telemetryService = telemetryService;
+ _mcpLogger = mcpLogger;
Logger = NullLogger.Instance;
}
public async Task RunAsync(string[] args)
{
- var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync();
- Logger.LogInformation($"ABP CLI {currentCliVersion}");
-
var commandLineArgs = CommandLineArgumentParser.Parse(args);
+ var currentCliVersion = await CliVersionService.GetCurrentCliVersionAsync();
+
+ var isMcpCommand = commandLineArgs.IsMcpCommand();
+
+ // Don't print banner for MCP command to avoid corrupting stdout JSON-RPC stream
+ if (!isMcpCommand)
+ {
+ Logger.LogInformation($"ABP CLI {currentCliVersion}");
+ }
#if !DEBUG
- if (!commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
+ // Skip version check for MCP command to avoid corrupting stdout JSON-RPC stream
+ if (!isMcpCommand && !commandLineArgs.Options.ContainsKey("skip-cli-version-check"))
{
await CheckCliVersionAsync(currentCliVersion);
}
@@ -85,13 +98,29 @@ public class CliService : ITransientDependency
}
catch (CliUsageException usageException)
{
- Logger.LogWarning(usageException.Message);
+ // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream
+ if (commandLineArgs.IsMcpCommand())
+ {
+ _mcpLogger.Error(McpLogSource, usageException.Message);
+ }
+ else
+ {
+ Logger.LogWarning(usageException.Message);
+ }
Environment.ExitCode = 1;
}
catch (Exception ex)
{
await _telemetryService.AddErrorActivityAsync(ex.Message);
- Logger.LogException(ex);
+ // For MCP command, use IMcpLogger to avoid corrupting stdout JSON-RPC stream
+ if (commandLineArgs.IsMcpCommand())
+ {
+ _mcpLogger.Error(McpLogSource, "Fatal error", ex);
+ }
+ else
+ {
+ Logger.LogException(ex);
+ }
throw;
}
finally
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs
index 9bfdcfac5c..a409614712 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CommandSelector.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using Volo.Abp.Cli.Args;
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs
index cc13e9d187..c051f6d455 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/HelpCommand.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
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 2b65b9d00b..c3641cac15 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
@@ -54,6 +54,14 @@ public class RecreateInitialMigrationCommand : IConsoleCommand, ITransientDepend
Directory.Delete(Path.Combine(projectDir, "TenantMigrations"), true);
separateDbContext = true;
}
+
+ CmdHelper.RunCmd("dotnet build", workingDirectory: projectDir, exitCode: out var exitCode);
+ if (exitCode != 0)
+ {
+ Logger.LogError("Build failed for project {Project}. Skipping migration recreation.", csprojFile);
+ continue;
+ }
+
if (!separateDbContext)
{
CmdHelper.RunCmd($"dotnet ef migrations add Initial", workingDirectory: projectDir);
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs
new file mode 100644
index 0000000000..ebf59e2ab6
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/McpCommand.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Volo.Abp.Cli.Args;
+using Volo.Abp.Cli.Auth;
+using Volo.Abp.Cli.Commands.Models;
+using Volo.Abp.Cli.Commands.Services;
+using Volo.Abp.Cli.Licensing;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Internal.Telemetry;
+using Volo.Abp.Internal.Telemetry.Constants;
+
+namespace Volo.Abp.Cli.Commands;
+
+public class McpCommand : IConsoleCommand, ITransientDependency
+{
+ private const string LogSource = nameof(McpCommand);
+ public const string Name = "mcp";
+
+ private readonly AuthService _authService;
+ private readonly IApiKeyService _apiKeyService;
+ private readonly McpServerService _mcpServerService;
+ private readonly McpHttpClientService _mcpHttpClient;
+ private readonly IMcpLogger _mcpLogger;
+ private readonly ITelemetryService _telemetryService;
+
+ public ILogger Logger { get; set; }
+
+ public McpCommand(
+ IApiKeyService apiKeyService,
+ AuthService authService,
+ McpServerService mcpServerService,
+ McpHttpClientService mcpHttpClient,
+ IMcpLogger mcpLogger,
+ ITelemetryService telemetryService)
+ {
+ _apiKeyService = apiKeyService;
+ _authService = authService;
+ _mcpServerService = mcpServerService;
+ _mcpHttpClient = mcpHttpClient;
+ _mcpLogger = mcpLogger;
+ _telemetryService = telemetryService;
+ Logger = NullLogger.Instance;
+ }
+
+ public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
+ {
+ await ValidateLicenseAsync();
+
+ var option = commandLineArgs.Target;
+
+ if (!string.IsNullOrEmpty(option) && option.Equals("get-config", StringComparison.OrdinalIgnoreCase))
+ {
+ await PrintConfigurationAsync();
+ return;
+ }
+
+ await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsMcp);
+
+ // Check server health before starting - fail if not reachable
+ _mcpLogger.Info(LogSource, "Checking ABP.IO MCP Server connection...");
+ var isHealthy = await _mcpHttpClient.CheckServerHealthAsync();
+
+ if (!isHealthy)
+ {
+ throw new CliUsageException(
+ "Could not connect to ABP.IO MCP Server. " +
+ "The MCP server requires a connection to fetch tool definitions. " +
+ "Please check your internet connection and try again.");
+ }
+
+ _mcpLogger.Info(LogSource, "Starting ABP MCP Server...");
+
+ var cts = new CancellationTokenSource();
+
+ ConsoleCancelEventHandler cancelHandler = (sender, e) =>
+ {
+ e.Cancel = true;
+ _mcpLogger.Info(LogSource, "Shutting down ABP MCP Server...");
+
+ try
+ {
+ cts.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+ // CTS already disposed
+ }
+ };
+
+ Console.CancelKeyPress += cancelHandler;
+
+ try
+ {
+ await _mcpServerService.RunAsync(cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected when Ctrl+C is pressed
+ }
+ catch (Exception ex)
+ {
+ _mcpLogger.Error(LogSource, "Error running MCP server", ex);
+ throw;
+ }
+ finally
+ {
+ Console.CancelKeyPress -= cancelHandler;
+ cts.Dispose();
+ }
+ }
+
+ private async Task ValidateLicenseAsync()
+ {
+ var loginInfo = await _authService.GetLoginInfoAsync();
+
+ if (string.IsNullOrEmpty(loginInfo?.Organization))
+ {
+ throw new CliUsageException("Please log in with your account!");
+ }
+
+ var licenseResult = await _apiKeyService.GetApiKeyOrNullAsync();
+
+ if (licenseResult == null || !licenseResult.HasActiveLicense)
+ {
+ var errorMessage = licenseResult?.ErrorMessage ?? "No active license found.";
+ throw new CliUsageException(errorMessage);
+ }
+
+ if (licenseResult.LicenseEndTime.HasValue && licenseResult.LicenseEndTime.Value < DateTime.UtcNow)
+ {
+ throw new CliUsageException("Your license has expired. Please renew your license to use the MCP server.");
+ }
+ }
+
+ private Task PrintConfigurationAsync()
+ {
+ var config = new McpClientConfiguration
+ {
+ McpServers = new Dictionary
+ {
+ ["abp"] = new McpServerConfig
+ {
+ Command = "abp",
+ Args = new List { "mcp" },
+ Env = new Dictionary()
+ }
+ }
+ };
+
+ var json = JsonSerializer.Serialize(config, new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ Console.WriteLine(json);
+
+ return Task.CompletedTask;
+ }
+
+ public string GetUsageInfo()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("");
+ sb.AppendLine("Usage:");
+ sb.AppendLine("");
+ sb.AppendLine(" abp mcp [options]");
+ sb.AppendLine("");
+ sb.AppendLine("Options:");
+ sb.AppendLine("");
+ sb.AppendLine("