diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index 6c21bb6baf..d8a1d0b8d7 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -1,13 +1,13 @@ -name: Merge branch dev with rel-10.5 +name: Merge branch dev with rel-10.6 on: push: branches: - - rel-10.5 + - rel-10.6 permissions: contents: read jobs: - merge-dev-with-rel-10-5: + merge-dev-with-rel-10-6: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR @@ -18,14 +18,14 @@ jobs: ref: dev - name: Reset promotion branch run: | - git fetch origin rel-10.5:rel-10.5 - git reset --hard rel-10.5 + git fetch origin rel-10.6:rel-10.6 + git reset --hard rel-10.6 - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: - branch: auto-merge/rel-10-5/${{github.run_number}} - title: Merge branch dev with rel-10.5 - body: This PR generated automatically to merge dev with rel-10.5. Please review the changed files before merging to prevent any errors that may occur. + branch: auto-merge/rel-10-6/${{github.run_number}} + title: Merge branch dev with rel-10.6 + body: This PR generated automatically to merge dev with rel-10.6. Please review the changed files before merging to prevent any errors that may occur. draft: true token: ${{ github.token }} - name: Merge Pull Request @@ -33,5 +33,5 @@ jobs: GH_TOKEN: ${{ secrets.BOT_SECRET }} run: | gh pr ready - gh pr review auto-merge/rel-10-5/${{github.run_number}} --approve - gh pr merge auto-merge/rel-10-5/${{github.run_number}} --merge --auto --delete-branch + gh pr review auto-merge/rel-10-6/${{github.run_number}} --approve + gh pr merge auto-merge/rel-10-6/${{github.run_number}} --merge --auto --delete-branch diff --git a/Directory.Packages.props b/Directory.Packages.props index e4505d2ba3..748e2a6dd6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,72 +58,72 @@ - - - - - - - - - - + + + + + + + + + + - - - - + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - + @@ -169,18 +169,18 @@ - - + + - - + + - - - + + + @@ -195,6 +195,6 @@ - + \ No newline at end of file diff --git a/common.props b/common.props index 8e25ef7520..ef096f5577 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 10.6.0-preview - 5.6.0-preview + 10.6.0-rc.1 + 5.6.0-rc.1 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/POST.md b/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/POST.md new file mode 100644 index 0000000000..1cc3253523 --- /dev/null +++ b/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/POST.md @@ -0,0 +1,71 @@ +# ABP.IO Platform 10.5 Final Has Been Released! + +We are glad to announce that [ABP](https://abp.io/) 10.5 stable version has been released. + +## What's New With Version 10.5? + +All the new features were explained in detail in the [10.5 RC Announcement Post](https://abp.io/community/announcements/announcing-abp-10-5-release-candidate-k6oxdfle), so there is no need to review them again. You can check it out for more details. + +## Getting Started with 10.5 + +### How to Upgrade an Existing Solution + +You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained: + +### Upgrading via ABP Studio + +If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See [the documentation](https://abp.io/docs/latest/studio/installation#upgrading) for more info. + +After upgrading the ABP Studio, then you can open your solution in the application, and simply click the **Upgrade ABP Packages** action button to instantly upgrade your solution: + +![](upgrade-abp-packages.png) + +### Upgrading via ABP CLI + +Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version. + +If you haven't installed it yet, you can run the following command: + +```bash +dotnet tool install -g Volo.Abp.Studio.Cli +``` + +Or to update the existing CLI, you can run the following command: + +```bash +dotnet tool update -g Volo.Abp.Studio.Cli +``` + +After installing/updating the ABP CLI, you can use the [`update` command](https://abp.io/docs/latest/CLI#update) to update all the ABP related NuGet and NPM packages in your solution as follows: + +```bash +abp update +``` + +You can run this command in the root folder of your solution to update all ABP related packages. + +## Migration Guides + +There are no explicitly marked breaking changes in this version. However, there are still some important migration notes for specific scenarios. Please read the migration guide carefully, if you are upgrading from v10.4 or earlier versions: [ABP Version 10.5 Migration Guide](https://abp.io/docs/10.5/release-info/migration-guides/abp-10-5) + +## Community News + +### New ABP Community Articles + +As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: + +- [Sumeyye Kurtulus](https://abp.io/community/members/sumeyye.kurtulus) has published 2 new articles: + - [Angular 22 State Management: Signals, SignalStore, or NgRx?](https://abp.io/community/articles/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw) + - [Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI](https://abp.io/community/articles/customizing-the-abp-framework-a-developers-guide-to-nklweri3) +- [Working with Dapr Workflows in the ABP Framework](https://abp.io/community/articles/working-with-dapr-workflows-in-the-abp-framework-6476or18) by [Engincan Veske](https://abp.io/community/members/EngincanV) +- [Alper Ebicoglu](https://abp.io/community/members/alper) has published 2 new articles: + - [My Speaker's View of CONVEX Summit 2026](https://abp.io/community/articles/my-speakers-view-of-convex-summit-2026-ai-net-conference-3uk6ln1l) + - [AI Isn't Replacing Developers - It's Changing What Good Developers Spend Time On](https://abp.io/community/articles/ai-isnt-replacing-developers-its-changing-what-good-2016q6ng) +- [Deep Dive on ABP AI Agent: The Complete Series](https://abp.io/community/articles/deep-dive-on-abp-ai-agent-the-complete-series-f7jute7n) by [Berkan Sasmaz](https://abp.io/community/members/berkansasmaz) + - We have created a deep-dive series for ABP Studio's AI Coding Agent. You can read this series to learn the main features of the AI Coding Agent and how it can help you while developing ABP-based solutions. + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. + +## About the Next Version + +The next feature version will be 10.6. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version. diff --git a/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/cover-image.png b/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/cover-image.png new file mode 100644 index 0000000000..2fb7ca6dd3 Binary files /dev/null and b/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/cover-image.png differ diff --git a/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/upgrade-abp-packages.png b/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/upgrade-abp-packages.png new file mode 100644 index 0000000000..4ec1d19589 Binary files /dev/null and b/docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/upgrade-abp-packages.png differ diff --git a/docs/en/Blog-Posts/2026-07-06-ABP-Summer-Campaign/post.md b/docs/en/Blog-Posts/2026-07-06-ABP-Summer-Campaign/post.md new file mode 100644 index 0000000000..f7203e5dce --- /dev/null +++ b/docs/en/Blog-Posts/2026-07-06-ABP-Summer-Campaign/post.md @@ -0,0 +1,55 @@ +Summer is here, and so is one of the best times to start building with ABP. + +From **July 6 to July 20**, we're offering exclusive summer savings on **ABP licenses and renewals**. Save **20% on new licenses** or **10% on license renewals**, and receive **up to $300 in AI credits** to power the **ABP AI Agent** in **ABP Studio**. + +Whether you're starting a new project or upgrading your development workflow, this campaign helps you save on your license while accelerating development with AI. + +### **What's Included?** + +**During the campaign period, you'll receive:** + +* **20% off new ABP licenses** +* **10% off license renewals** +* **Up to $300 in AI credits** for the **ABP AI Agent** + +The AI credits can be used with the **ABP AI Agent** in **ABP Studio**, allowing you to automate repetitive development tasks and build applications faster. + +### **Build Faster with the ABP AI Agent** + +The ABP AI Agent is designed specifically for ABP developers. Rather than acting as a generic coding assistant, it understands your ABP solution and helps automate common development workflows. + +With the included AI credits, you can: + +* Generate application features with AI assistance +* Create entities, services, and UI components faster +* Run automated development workflows +* Generate database migrations and update projects +* Inspect exceptions and troubleshoot issues +* Execute development tasks directly from ABP Studio + +The result is less time spent on repetitive work and more time focused on building your application's business value. + +### **Why Choose ABP?** + +ABP is a complete application development platform for building modern, maintainable, and scalable .NET applications. + +With ABP, you can: + +* Build enterprise-grade ASP.NET Core applications faster +* Follow Domain-Driven Design (DDD) and clean architecture principles +* Develop modular, reusable, and maintainable application modules +* Leverage built-in capabilities such as multi-tenancy, authentication, authorization, localization, auditing, and more +* Scale from modular monoliths to microservice architectures +* Boost developer productivity with ABP Studio and the ABP AI Agent + +Instead of spending weeks building common infrastructure, your team can focus on delivering business value and shipping features faster. + +## **Don't Miss This Limited-Time Offer** + +This campaign is available **only between July 6 and July 20**. + +Whether you're purchasing your first ABP license or renewing your existing one, now is the perfect time to save. Get **20% off new licenses** or **10% off renewals**, plus receive **up to $300 in AI credits** to accelerate development with the **ABP AI Agent**. + +**Claim your summer discount before July 20 and start building faster with ABP.** + +**Get your discount now:** [https://abp.io/pricing](https://abp.io/pricing) diff --git a/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/POST.md b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/POST.md new file mode 100644 index 0000000000..ec8e9257c9 --- /dev/null +++ b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/POST.md @@ -0,0 +1,180 @@ +# ABP Platform 10.6 RC Has Been Released + +We are happy to release [ABP](https://abp.io) version **10.6 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. + +Try this version and provide feedback for a more stable version of ABP v10.6! Thanks to you in advance. + +## Get Started with the 10.6 RC + +You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli). + +By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: + +![studio-switch-to-preview](studio-switch-to-preview.png) + +## Migration Guide + +You can check the migration guide if you are upgrading from v10.5 or earlier: [ABP Version 10.6 Migration Guide](https://abp.io/docs/10.6/release-info/migration-guides/abp-10-6). + +## What's New with ABP v10.6? + +In this section, I will introduce some major features released in this version. +Here is a brief list of titles explained in the next sections: + +- Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention +- API Definition and Proxy Improvements for Content Types and Multipart Uploads +- Angular UI: Upgrade to Angular 22 +- Antiforgery and OpenIddict Security Improvements +- OpenIddict: Generate Access Token from the UI +- Dependency Updates + +### Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention + +ABP v10.6 adds three opt-in enhancements to the default background job worker. All of them are disabled by default, so existing applications keep the current behavior unless you enable them explicitly. + +**Storing successful jobs** + +By default, a job is deleted as soon as it runs successfully. You can now set `StoreSuccessfulJobs = true` to keep completed jobs in the store. A new `CompletionTime` column marks completed jobs, and a cleanup worker prunes them after `SuccessfulJobRetentionTime` (default: 7 days). + +**Dedicated workers per job type** + +`AddDedicatedWorker(...)` registers a worker that processes only the configured job argument types, each with its own distributed lock. The default worker continues handling all remaining job types. + +**Parallel job execution** + +Set `MaxParallelJobExecutionCount` greater than 1 to execute multiple jobs in the same poll cycle. In this mode, each job is claimed with its own distributed lock so different application instances can process different jobs concurrently without running the same job twice. + +Example configuration: + +```csharp +Configure(options => +{ + options.StoreSuccessfulJobs = true; + options.SuccessfulJobRetentionTime = TimeSpan.FromDays(30); + + options.AddDedicatedWorker("NotificationWorkerLock"); + options.AddDedicatedWorker("ReportWorkerLock"); + + options.MaxParallelJobExecutionCount = 4; +}); +``` + +These options are useful when you need better isolation between job types, higher throughput in clustered deployments, or an audit trail of successfully completed jobs. + +> See the [Background Jobs](https://abp.io/docs/10.6/framework/infrastructure/background-jobs) documentation and [#25742](https://github.com/abpframework/abp/pull/25742) for details. + +### API Definition and Proxy Improvements for Content Types and Multipart Uploads + +ABP v10.6 improves API definition generation and client proxies for file upload scenarios and non-JSON response types. + +The API definition now exposes response `ContentTypes` and an `IsRemoteStream` flag. C#, jQuery, and Angular proxies can use the declared media type instead of collapsing everything to `application/json` and `text/plain`. + +For upload DTOs containing `IRemoteStreamContent`, generated Angular and jQuery proxies now forward `FormData` as multipart requests instead of silently dropping the file payload or trying to serialize the stream as JSON. + +Server-side setup still follows the existing ABP pattern: + +```csharp +Configure(options => +{ + options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(UploadFileDto)); +}); +``` + +Angular client example after proxy regeneration: + +```typescript +const fd = new FormData(); +fd.append('Name', 'logo'); +fd.append('File', fileInput.files[0], 'logo.png'); +this.fileService.uploadFile(fd).subscribe(result => ...); +``` + +This closes long-standing gaps in generated proxies for stream-based uploads and improves support for text, blob, and custom response types. + +> See [#25639](https://github.com/abpframework/abp/pull/25639) for details. + +### Angular UI: Upgrade to Angular 22 + +ABP v10.6 upgrades the Angular UI stack to **Angular 22.0.x**. + +This release also improves the locale loading mechanism with a fallback path, so culture resources load more reliably when optional locale files are missing or partially available. + +If you maintain a custom Angular UI on top of ABP, plan for the Angular 22 upgrade together with your ABP package update and regenerate proxies after upgrading. + +> See [#25690](https://github.com/abpframework/abp/pull/25690) and [#25734](https://github.com/abpframework/abp/pull/25734) for details. + +### Antiforgery and OpenIddict Security Improvements + +ABP v10.6 includes several security-focused fixes for mixed authentication scenarios. + +**Antiforgery claim issuer normalization** + +When an application serves a token-authenticated SPA and cookie-authenticated MVC pages on the same origin, antiforgery validation could fail because the user id claim issuer differed between JWT and cookie authentication schemes. ABP now normalizes the user id claim issuer while generating and validating antiforgery tokens. + +This behavior is enabled by default through `AbpAntiForgeryOptions.NormalizeUserIdClaimIssuer`. Razor Pages antiforgery validation was also aligned with the same normalization logic, which fixes failures in modules such as Setting Management. + +**Prevent OpenIddict `client_id` from leaking into the interactive auth cookie** + +ABP fixed a case where an OpenIddict authorization request could stamp the requested `client_id` into the interactive authentication cookie during security-stamp refresh. That could corrupt audit logs and make later cookie-authenticated requests appear to belong to the OAuth client. + +The fix strips `client_id` when the interactive cookie is refreshed. Tokens are unaffected, and cookies that were already corrupted self-heal on the next refresh. + +**Forward the current access token for authenticated client requests** + +`HttpContextAbpAccessTokenProvider` now forwards the incoming access token whenever the request is authenticated, including `client_credentials` requests. This prevents unnecessary fallback to configured identity clients in machine-to-machine scenarios. + +> See [#25655](https://github.com/abpframework/abp/pull/25655), [#25669](https://github.com/abpframework/abp/pull/25669), [#25711](https://github.com/abpframework/abp/pull/25711), and [#25740](https://github.com/abpframework/abp/pull/25740) for details. + +### OpenIddict: Generate Access Token from the UI + +ABP Commercial v10.6 RC adds a **Generate Access Token** action to OpenIddict application management pages across MVC, Blazor, MudBlazor, and Angular UIs. + +Administrators can request a token for an OpenIddict application directly from the UI. The backend forwards a `client_credentials` request to `/connect/token` and returns the generated access token to the caller. + +This is especially useful for testing integrations, validating scopes, and troubleshooting machine-to-machine authentication without leaving the admin UI. + +### Dependency Updates + +ABP v10.6 RC includes several dependency and package updates: + +- Angular packages upgraded to **22.0.x** +- `Microsoft.*` and `System.*` packages upgraded to **10.0.9** +- `Microsoft.Data.SqlClient` upgraded to **7.0.2** +- `Swashbuckle.AspNetCore` upgraded to **10.2.3** + +> Check the [Package Version Changes](https://abp.io/docs/10.6/package-version-changes) document for all updates. + +### Other Improvements and Enhancements + +- **Permission management**: Skip dynamic permission initialization during migration runs to avoid noisy logs when the database is unavailable ([#25743](https://github.com/abpframework/abp/pull/25743)). +- **Security / principal access**: `ThreadCurrentPrincipalAccessor` now returns an anonymous principal instead of `null` in non-web contexts ([#25752](https://github.com/abpframework/abp/pull/25752)). +- **Angular proxy generation**: Array parameters are now generated as `readonly` in Angular proxies ([#25687](https://github.com/abpframework/abp/pull/25687)). +- **Date/time normalization**: Removed misleading warnings when normalizing `Unspecified` `DateTime` values near range boundaries ([#25703](https://github.com/abpframework/abp/pull/25703)). +- **AI Management**: Indexing is more resilient under memory pressure in the commercial module. + +## Community News + +### New ABP Community Articles + +As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: + +- [ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support](https://abp.io/community/articles/abp-10.5.0-expands-blazor-ui-options-with-mudblazor-support-03rzmlpm) by [Liming Ma](https://abp.io/community/members/maliming) +- [Angular 22 State Management: Signals, SignalStore, or NgRx?](https://abp.io/community/articles/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw) by [Sumeyye Kurtulus](https://abp.io/community/members/sumeyye.kurtulus) +- [Working with Dapr Workflows in the ABP Framework](https://abp.io/community/articles/working-with-dapr-workflows-in-the-abp-framework-6476or18) by [Engincan Veske](https://abp.io/community/members/EngincanV) +- [My Speaker's View of CONVEX Summit 2026](https://abp.io/community/articles/my-speakers-view-of-convex-summit-2026-ai-net-conference-3uk6ln1l) by [Alper Ebiçoğlu](https://abp.io/community/members/alper) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. + +### ABP Summer Campaign: Get Up To 20% Off + $300 in AI Credits + +![summer-sale](summer-sale.png) + +Summer is a great time to start building with ABP. From **July 6 to July 20**, we're offering exclusive summer savings on **ABP licenses and renewals**: **20% off new licenses**, **10% off renewals**, and **up to $300 in AI credits** for the **ABP AI Agent** in **ABP Studio**. Whether you're starting a new project or upgrading your development workflow, this limited-time offer helps you save on your license while accelerating development with AI. + +> You can read the announcement here: [ABP Summer Campaign: Get Up To 20% Off + $300 in AI Credits](https://abp.io/community/announcements/abp-summer-campaign-get-up-to-20-off-300-in-ai-credits-r5lqtpg9). + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.6/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.6 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! \ No newline at end of file diff --git a/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/cover-image.png b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/cover-image.png new file mode 100644 index 0000000000..efb4466e5d Binary files /dev/null and b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/studio-switch-to-preview.png b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/studio-switch-to-preview.png new file mode 100644 index 0000000000..ad73877834 Binary files /dev/null and b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/studio-switch-to-preview.png differ diff --git a/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/summer-sale.png b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/summer-sale.png new file mode 100644 index 0000000000..f8c5163e45 Binary files /dev/null and b/docs/en/Blog-Posts/2026-07-07 v10_6_Preview/summer-sale.png differ diff --git a/docs/en/Community-Articles/2026-06-24-Meet-ABP-at-WeAreDevelopers-World-Congress-2026/post.md b/docs/en/Community-Articles/2026-06-24-Meet-ABP-at-WeAreDevelopers-World-Congress-2026/post.md new file mode 100644 index 0000000000..cd816964d0 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-24-Meet-ABP-at-WeAreDevelopers-World-Congress-2026/post.md @@ -0,0 +1,22 @@ +We are happy to announce that the ABP team will be heading to Berlin for WeAreDevelopers World Congress 2026, one of the largest gatherings of software developers and technology professionals in Europe. + +Taking place from **8-10 July 2026**, the event brings together thousands of developers, architects, engineering leaders, startups, and technology companies to explore the latest trends, tools, and ideas shaping the future of software development. + +We're excited to be part of this global community once again and look forward to connecting with developers from around the world. + +## **Visit Us at the Event\!** + +If you're attending WeAreDevelopers World Congress, make sure to stop by **Hall A, Booth A-41** and meet the ABP team. + +We'll be showcasing the latest developments across the ABP ecosystem, including ABP Framework, ABP Studio, and our newest AI-powered development capabilities. Whether you're building enterprise applications, modernizing existing systems, or exploring new approaches to software development, we'd love to hear about your projects and challenges. + +Our team will be available throughout the event for product demos, technical discussions, and conversations about modern .NET development, modular architecture, microservices, and AI-assisted software development. + +## **See You in Berlin** + +Nothing replaces meeting developers face-to-face\! + +Whether you're already using ABP, evaluating it for a future project, or simply curious about what we're building, we'd be happy to meet you. + +See you in **Hall A, Booth A-41** at WeAreDevelopers World Congress 2026\! + diff --git a/docs/en/Community-Articles/2026-06-25-ABP-Bootcamp-AI-Assisted-Application-Development-with-ABP/post.md b/docs/en/Community-Articles/2026-06-25-ABP-Bootcamp-AI-Assisted-Application-Development-with-ABP/post.md new file mode 100644 index 0000000000..912ab75d30 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-25-ABP-Bootcamp-AI-Assisted-Application-Development-with-ABP/post.md @@ -0,0 +1,86 @@ +AI is changing how software is built. Today, developers can generate features, services, tests, and even entire applications in minutes. Tasks that once took hours can now be completed with a single prompt. + +But speed is no longer the biggest challenge.Reliability is. + +AI generates probabilistic answers. Production software requires deterministic behavior. When developers receive different implementations for the same problem, applications become harder to maintain, harder to scale, and more difficult to evolve over time. + +Building software with AI is a lot like constructing a building with power tools.The tools make construction faster. They do not make poor foundations safer. + +In fact, they allow mistakes to spread much faster. + +The architectural decisions made during the first few months of a project often determine its long-term success. Security, modularity, authorization, maintainability, and development conventions become part of the foundation that everything else depends on. + +This is where ABP comes in. + +For more than a decade, we've been helping development teams build enterprise-grade .NET applications on solid architectural foundations. Today, companies around the world continue to build and maintain production systems with ABP, even as AI becomes an increasingly important part of the software development process. + +We didn't start thinking about AI yesterday. + +We've integrated AI into our own development workflows, evolved our startup templates, created AI-specific development rules, and built ABP Studio AI Agent to help developers work more effectively with ABP-based applications. + +To help developers learn these practices, we're excited to announce our latest bootcamp: + +## **AI-Assisted Application Development with ABP** + +This live, instructor-led bootcamp is not about generating code faster. + +It's about learning how to build applications faster while maintaining architectural consistency, code quality, and long-term maintainability. + +Over three days of hands-on sessions, you'll learn practical AI-assisted engineering workflows using ABP Studio AI Agent and discover how to combine AI productivity with proven software engineering practices. + +## **Bootcamp Details** + +**Dates:** August 25-27, 2026 +**Time:** 17:00-19:00 UTC each day +**Duration:** 6 hours total +**Format:** Live online sessions via Google Meet +**Price:** $399 (discounted from $799) + +*\*Participants who do not already have access to ABP Studio AI Agent will receive **complimentary trial access** **for the duration of the bootcamp**. Additional AI credits will be provided when needed.* + +## **What You'll Learn** + +Throughout the bootcamp, you'll explore real-world AI-assisted software development workflows, including: + +* Using AI to accelerate application development with ABP +* Working effectively with the ABP Studio AI Agent +* Generating features, services, and application components faster +* Understanding how AI can assist with implementation, debugging, and code exploration +* Applying AI-assisted engineering practices in real ABP projects +* Combining developer expertise with AI capabilities to improve productivity + +The focus will be on practical examples, live demonstrations, and hands-on exercises that you can immediately apply in your own projects. + +## **Why Learn From the ABP Team?** + +Many AI development courses teach how to generate code. + +This bootcamp focuses on something more important: how to generate code that remains maintainable, scalable, and consistent as your application grows. + +The ABP team has spent more than 10 years building and evolving one of the most widely used application frameworks in the .NET ecosystem. + +We've worked closely with development teams across industries, helped companies build production systems, and recently invested heavily in AI-powered development tools such as ABP Studio AI Agent. + +The lessons shared in this bootcamp come directly from our own experience building software with AI, not from theoretical examples or isolated experiments. + +You'll learn the same principles, workflows, and practices we use to combine AI-assisted development with real-world software engineering. + +## **Who It's For** + +This bootcamp is ideal for: + +* ABP developers who want to increase productivity with AI +* Software developers interested in AI-assisted software development +* Teams exploring how AI can improve their development workflows +* Technical leaders evaluating AI-powered development practices +* Anyone looking to stay ahead as software engineering continues to evolve + +## **Reserve Your Spot** + +AI-assisted software development is quickly becoming an essential skill for modern development teams. + +This bootcamp is designed to help you understand not only how AI tools work, but how to use them effectively within a real-world application development framework. + +Join us and learn how to build applications faster with ABP and AI. + +Registration is now open, fill the form: [https://docs.google.com/forms/d/e/1FAIpQLSdREtytTXXEfnOrwuMeTnXs7O10LcVXo-dlyhUNVTX\_dMZriw/viewform?usp=publish-editor](https://docs.google.com/forms/d/e/1FAIpQLSdREtytTXXEfnOrwuMeTnXs7O10LcVXo-dlyhUNVTX_dMZriw/viewform?usp=publish-editor) diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post-v2.md b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post-v2.md deleted file mode 100644 index f8588eecbe..0000000000 --- a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post-v2.md +++ /dev/null @@ -1,207 +0,0 @@ -### 1. Alternative Article Title Suggestions - -1. **Unifying Dev, Architecture, and AI: My Experience Speaking on Conversational SQL at CONVEX 2026** -2. **CONVEX Summit 2026: Notes from the Stage, the Cinema Screens, and the Future of Database-to-Agent Systems** -3. **Beyond the Vibe Coding: Speaking at CONVEX 2026 and Re-Architecting Conversational B2B Systems** - -### 2. Selected Title - -# Unifying Dev, Architecture, and AI: My Experience Speaking on Conversational SQL at CONVEX 2026 - -### 3. Meta Description - -Join Volosoft Co-Founder Alper Ebiçoğlu as he shares his firsthand experience speaking at CONVEX Summit 2026 in Madrid, exploring natural-language-to-SQL architecture, Model Context Protocol (MCP), and lessons in AI security. - -### 4. SEO Slug Suggestion - -``` -convex-summit-2026-speaks-view-conversational-sql -``` - -### 5. Full Article - -Arriving in Madrid this June for the inaugural CONVEX Summit 2026 felt like witnessing a major shift in how our industry talks about building software. For years, developers, database administrators, and software architects have operated in distinct technical silos. We have watched artificial intelligence disrupt daily operations, development teams rapidly adopt new frameworks, and distributed architectures grow increasingly complex. Yet, these massive shifts have largely occurred in parallel. - -To celebrate their twentieth anniversary, the team at Plain Concepts took a bold, necessary step: they unified three of Spain’s flagship tech events—dotNET, the Global Software Architecture Summit (GSAS), and Singularity Tech Day—into a single, cohesive experience. - -As I walked into the Kinépolis Ciudad de la Imagen—the largest cinema complex in Madrid—the scale of this integration was immediately apparent. The venue was buzzing with over 1,200 tech professionals representing more than 25 countries. For me, as a co-founder and software architect at Volosoft, this was more than just another conference. It was a unique convergence point where theory met execution, and where I had the privilege of taking the main stage to speak on a topic I’ve been living and breathing: re-architecting how enterprise applications talk to databases. - -!(convex_keynote_stage.jpg) - -## Speaking at CONVEX: Chat with Your Data - -My session, titled **"Chat with Your Data: Turn any database into a conversational reporting engine,"** was scheduled in front of an incredibly engaged audience of developers, CTOs, and systems architects. The primary problem I wanted to tackle is one that almost every B2B application team faces: the endless cycle of custom report building. Traditional enterprise applications are bottlenecked by the constant demand for custom queries, Excel exports, and visual dashboards. - -My talk introduced a conversational reporting approach designed to let non-technical stakeholders safely generate complex reports from their database simply by chatting—as if they were messaging a human analyst. - -!(alper_ebicoglu_presentation.jpg) - -On stage, I detailed the exact architectural pipeline we built using.NET to securely connect natural language prompts to database engines. Letting an LLM generate SQL queries is easy in a demo, but incredibly dangerous in an enterprise environment. If you simply pass user input directly to an LLM and run the resulting string against your database, you are inviting disastrous SQL injections, massive context bloat, and uncontrolled resource exhaustion. - -To solve this, we designed a multi-stage validation pipeline that prioritizes security and performance: - -``` -[User Natural Language Input] - │ - ▼ - ───► Minimize schema metadata injected into prompt - │ - ▼ - ──► Parse user intent to avoid context bloat - │ - ▼ - ──────► Draft query based on precise prompt constraints - │ - ▼ - ─► AST parsing to block DDL/DML, enforce read-only - │ - ▼ - ────► Safe execution isolated from production database - │ - ▼ - [Output Generation Engine] ───► Dynamic formatting into Excel sheets & charts -``` - -### The Security Mathematics of SQL Validation - -The most critical phase of this pipeline is our dynamic SQL parser. Before any generated query touches the database, the.NET application parses the string into an Abstract Syntax Tree (AST). This allows us to run a deterministic evaluation of the query structure. - -We can model this strict security boundary mathematically. Let $Q$ be the generated SQL query, and let $T(Q)$ be the set of operation tokens identified in the AST. We define the security function $S(Q)$ as: - -$$S(Q) = \begin{cases} 1 & \text{if } T(Q) \subseteq \{\text{SELECT}\} \land T(Q) \cap \{\text{INSERT}, \text{UPDATE}, \text{DELETE}, \text{DROP}, \text{ALTER}, \text{CREATE}\} = \emptyset \\ 0 & \text{otherwise} \end{cases}$$ - -If $S(Q) = 0$, the query is immediately rejected at the application boundary, completely mitigating malicious prompt injections or model hallucinations before they can cause damage. We also run these validated queries exclusively against an isolated read-replica, completely separating conversational reporting workloads from our primary transaction database. - -During the session, I demonstrated how the pipeline extracts metadata to construct the schema map, routes user intents, and dynamically compiles the resulting database rows into structured Excel files and interactive charts. It was highly rewarding to hear from the community afterward about how this approach solves real-world security concerns while dramatically improving the B2B developer experience. - -## What I Learned From the English Sessions - -When I wasn't on stage or talking with attendees, I spent my time attending the English-language sessions. Because the conference unified dotNET, GSAS, and Singularity, the technical depth across the tracks was remarkable. Several sessions provided profound, second-order insights into how enterprise engineering teams are actually putting AI and advanced architecture patterns to work at scale. - -### AI, Security, and System Exploits - -Chema Alonso's Keynote on Day 2, *"Hacking ( with | the ) AI,"* was an eye-opening deep dive into the darker side of generative systems. Alonso, a leading security figure, showcased how malicious actors are actively utilizing AI to accelerate the development of system exploits. - -What struck me most was his analysis of semantic vulnerabilities. Traditional firewalls and security protocols are completely blind to threat vectors like jailbreaking, prompt injection, and model exfiltration. Alonso's core thesis resonated deeply with my own presentation: AI is a powerful assistant, but it cannot be treated as a security boundary. If you build an AI feature, you must assume the output generated by the model is untrusted and validate it with rigorous, deterministic code. - -### Redefining the Next Digital Frontier with MCP - -Another highly practical session was delivered by Manuel Sanchez and Carlos Mendible, titled *"AI, Agents and MCP: Redefining the Next Digital Frontier"*. They introduced the Model Context Protocol (MCP)—an emerging open standard designed to structure how AI agents interact with local applications, databases, and development environments. - -Sanchez and Mendible highlighted a common mistake developers make when building agentic integrations: exposing raw CRUD (Create, Read, Update, Delete) database tables to the model's context window. This "context bloat" dramatically increases token costs and degrades the agent's reasoning speed. - -Instead, they demonstrated how MCP servers should expose high-level, parameter-driven business tools (e.g., executing a specific calculation or pulling a pre-filtered report). This approach pushes computation back onto the database or backend systems, saving tokens and keeping agents highly performant. - -| **Integration Pattern** | **Context Bloat (Tokens)** | **Latency** | **Security Control** | -| ----------------------- | --------------------------------------------------- | ---------------------------------------------- | ------------------------------------------- | -| **Raw CRUD Exposure** | Extremely High (Exposes raw schema & raw tables) | High (Model must process entire dataset) | Very Poor (Relying on model constraints) | -| **MCP Business Tools** | Minimal (Exposes targeted APIs/parameterized tools) | Low (Database/backend handles heavy computing) | Excellent (Enforces strict code boundaries) | - -### Re-Evaluating Architectural Trade-offs and Climate Impact - -Eoin Woods, one of the leading figures in software design, brought invaluable perspective to the GSAS track with his talk, *"The Key to the Prisoners' Dilemma"*. Woods discussed the constant tug-of-war between business speed and long-term architectural stability, using game theory to prove that proper architecture is actually the primary vehicle for sustainable, ongoing business value. - -What made Woods' contribution even more fascinating was his work on green software engineering. He pointed out that the carbon emissions of global computing infrastructure are rising rapidly, with ICT emissions projected to reach 5% by 2030—driven in large part by the extreme computational demands of modern AI models. - -Woods introduced the concept of **Demand Shifting**. By utilizing smart, orchestrating software architectures, enterprise teams can dynamically route heavy, non-time-sensitive AI training and query workloads to data centers currently operating on clean, excess renewable energy. This simple architectural decision can reduce operational emissions by up to 40% with virtually zero impact on system performance. - -## The Conference Experience - -Kinépolis Madrid proved to be an outstanding venue for a tech summit of this scale. Showing complex architecture diagrams, SQL configurations, and C# code on IMAX-sized movie screens was a developer's dream. The audio clarity and amphitheater seating ensured that even the most dense, code-heavy presentations felt incredibly engaging. - -!(convex_networking_hall.jpg) - -But beyond the high-quality presentation rooms, what really set CONVEX apart was the lack of superficial commercial noise. There were no aggressive sales pitches or standard marketing booths trying to reel you in. Instead, the networking areas were filled with genuine technical conversations. - -During the coffee breaks and lunch sessions, I spent hours talking with developer advocates, CTOs, and software architects representing the international.NET and open-source communities. We compared notes on our experiences with Blazor WebAssembly, discussed scaling multi-tenant SaaS structures, and debated the practical limits of "vibe coding". The community-first energy was palpable, showing that the real value of these events is built on the shared experiences and connections made off-stage. - -## My Key Takeaways - -Reflecting on my conversations, my presentation, and the excellent sessions I attended, several core takeaways stand out for any B2B engineering leader: - -- **AI features require absolute data boundaries:** Building conversational database features is a powerful way to eliminate custom report backlogs, but you must validate LLM-generated outputs before they reach your data. Never allow an AI model to write directly to a production database, and always validate queries using AST analysis. -- **The Model Context Protocol (MCP) is the new standard:** Rather than creating custom, ad-hoc integrations for every agent, we must design modular MCP servers that expose clean, business-level APIs to AI models. This reduces context bloat and enforces a cleaner separation of concerns. -- **Green software is an architectural priority:** With AI dramatically increasing energy consumption, we can no longer ignore the environmental impact of our software systems. Implementing patterns like Demand Shifting to run heavy workloads during green energy peaks is becoming a vital non-functional requirement. -- **Developer experience remains a massive competitive advantage:** The success of tools like the ABP Framework and pre-built modular architectures is proof that B2B development teams want to focus on business logic rather than writing repetitive boilerplate code. By automating routine tasks like query generation and report compilation, we can free up engineering teams to focus on core platform value. - -## Closing - -The inaugural CONVEX Summit 2026 was a resounding success. Plain Concepts did an incredible job of transforming three separate industry dialogues into a unified, high-impact event that reflected the real challenges tech organizations face today. - -I want to extend my sincere thanks to the organizers, especially Ivan Suárez Álvarez, for putting together such a high-caliber event. I'm also deeply grateful to all the speakers who shared their hard-earned production lessons, and to every member of the.NET and Volosoft communities who stopped by to talk, share feedback, and celebrate our shared passion for building high-quality software. - -I left Madrid with a notepad full of new architectural ideas, a stronger network of global peers, and an even deeper conviction that the intersection of structured software architecture and generative AI is the most exciting place to be building right now. I cannot wait to see where these conversations take us, and I look forward to returning for the next edition! - -### 6. Used Photos and Placement Details - -1. **Görsel Dosya Adı**: `convex_keynote_stage.jpg` - - **Yerleştirilen Bölüm**: Kısa giriş (Introduction) bölümünün hemen altı. - - **Alt Metin (Alt Text)**: The grand keynote stage at Kinépolis Ciudad de la Imagen welcoming over 1,200 international technology leaders to CONVEX 2026. -2. **Görsel Dosya Adı**: `alper_ebicoglu_presentation.jpg` - - **Yerleştirilen Bölüm**: "Speaking at CONVEX: Chat with Your Data" ana başlığının hemen altı. - - **Alt Metin (Alt Text)**: Alper Ebiçoğlu presenting 'Chat with Your Data' live on stage, detailing the pipeline that bridges natural language with secure enterprise database queries. -3. **Görsel Dosya Adı**: `alper_slide_ast_validation.jpg` - - **Yerleştirilen Bölüm**: "Speaking at CONVEX: Chat with Your Data" bölümündeki teknik SQL analizi ve matematiksel formülün hemen altı. - - **Alt Metin (Alt Text)**: An architecture slide showing the schema discovery, LLM processing, and query validation pipeline of the conversational reporting engine. -4. **Görsel Dosya Adı**: `convex_networking_hall.jpg` - - **Yerleştirilen Bölüm**: "The Conference Experience" bölümünün hemen altı. - - **Alt Metin (Alt Text)**: Attendees engaging in technical discussions and B2B networking during the breaks in the exhibition hall of Kinépolis Madrid. - -### 7. LinkedIn Post Suggestions - -#### Post 1: Short & B2B Professional (General Event Review) - -> Unifying dotNET, GSAS, and Singularity Tech Day, CONVEX Summit 2026 in Madrid brought together over 1,200 international tech leaders to answer a single question: How do we turn technological potential into real-world software impact? -> -> I was thrilled to take the stage to talk about conversational database architectures. Read my complete B2B conference review for technical highlights on AI security, green computing, and agentic workflows: [Link] #CONVEX2026 #SoftwareArchitecture #EnterpriseAI #DotNet - -#### Post 2: Short & Technical (NL-to-SQL Pipeline) - -> Letting an LLM generate SQL queries is easy in a demo, but incredibly risky in production. At CONVEX 2026, I shared our.NET-based pipeline for "Chat with Your Data," demonstrating how to use Abstract Syntax Tree (AST) parsing to enforce strict read-only queries at the application boundary. -> -> Curious about dynamic schema discovery, context injection, and preventing context bloat? Check out my latest technical write-up from the Madrid stage: [Link] #ConversationalSQL #SoftwareEngineering #PostgreSQL #B2BTech - -#### Post 3: Medium & Analytical (Architectural Focus) - -> AI-Guards alone will not save your B2B application. At CONVEX 2026, experts like Chema Alonso reminded us that generative systems are not security boundaries. -> -> As software architects, we must assume LLM outputs are untrusted. In my latest article, I evaluate key architectural takeaways from the Madrid summit—exploring why we must transition to specialized Model Context Protocol (MCP) servers, why we should adopt "Demand Shifting" to lower the carbon footprint of heavy AI workloads, and how to safely design natural-language-to-SQL engines. -> -> Read the full technical breakdown: [Link] #EnterpriseAI #CyberSecurity #SystemDesign #MCP - -#### Post 4: Medium & Developer Productivity (SaaS & Frameworks) - -> Developer experience remains the ultimate competitive edge. As creators of the open-source ABP Framework, we at Volosoft are always looking for ways to cut out boilerplate code and accelerate feature delivery. -> -> At CONVEX 2026, the discussion shifted from "vibe coding" back to spec-driven architecture. By building conversational reporting tools that handle the data translations while our C# code handles validation and dynamic Excel generation, we can eliminate reporting bottlenecks forever. -> -> Here are my reflections on how modern development, architecture, and AI are finally merging into a single, high-impact narrative: [Link] #DX #DotNet #SaaS #ABPFramework - -#### Post 5: Personal & Story-Driven (My Speaker Journey) - -> What an incredible week in Madrid! Speaking at the inaugural CONVEX Summit 2026 was an absolute highlight of my year. Sharing the stage at the stunning Kinépolis cinema venue to present our "Chat with Your Data" conversational reporting pipeline was a fantastic experience. -> -> Beyond presenting, what made this trip truly special was the community. Meeting with fellow software architects at the speaker dinner, exploring historical tech challenges, and exchanging notes on modern.NET configurations over coffee made for some unforgettable conversations. -> -> I want to extend a huge thank you to Plain Concepts and Ivan Suárez Álvarez for organizing a stellar event. I've gathered my favorite technical sessions, personal notes, and major architecture takeaways in my latest article. I hope it sparks some great ideas for your team! [Link] #CONVEXSummit #MySpeakerJourney #Volosoft #TechCommunity - -### 8. SEO Keyword Suggestions - -1. `CONVEX Summit 2026` -2. `Natural language to SQL pipeline` -3. `Alper Ebiçoğlu speaker` -4. `Model Context Protocol MCP` -5. `Abstract Syntax Tree SQL validation` -6. `Dynamic database schema discovery` -7. `Green software demand shifting` -8. `Plain Concepts Madrid` -9. `B2B software architecture AI` -10. `ABP Framework database reporting` - -### 9. Social Media Hashtag Suggestions - -- `#CONVEX2026` -- `#SoftwareArchitecture` -- `#DotNet` -- `#ConversationalData` -- `#EnterpriseAI` \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post.md b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post.md index 8ec3bd4be0..d41f5dadba 100644 --- a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post.md +++ b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post.md @@ -2,106 +2,66 @@ -# My Speaker's View of CONVEX Summit 2026 Madrid +# My Speaker's View of CONVEX Summit 2026 Madrid -## Where AI Hype Met Enterprise Reality +Hi, I'm here for another conference review. This time I attended [Convex Summit 2026 Developer](https://www.convexsummit.com/) conference, held in the capital city of Spain, Madrid. It was my first talk in Madrid. The organizer [Plain Concepts](https://www.plainconcepts.com/) arranged a 2 days conf with 3 parallel sessions. 2 of the sessions were in Spanish and one of them was English. The conf dates were 17, 18 June 2026. It was in a very big cinema. In Lithuania I also spoke in a cinema, I guess it's better to arrange a conf in a cinema because of asthenosphere, acoustic and state visibility for visitors. -**Meta description:** My personal speaker’s view of CONVEX Summit 2026 in Madrid, where AI, .NET, software architecture, and enterprise product thinking came together around real-world technology challenges. +There is a small moment before every conference talk when the room becomes quiet, the slides are ready, and you suddenly remember why the topic matters. For me, that moment happened at Convex. My topic was “Turn any database into a conversational reporting engine.” I can admit it was a cool talk. But I was also there as a listener, a software architect, and someone trying to understand where enterprise AI is really going after the first wave of demos and experiments. CONVEX was interesting because it brought software development, architecture, and AI into the same conversation. These topics are often discussed separately, but in real companies they are tightly connected. AI features do not live in isolation. They live inside applications, databases, identity systems, permission models, workflows, dashboards, and business expectations. That was the real theme I felt throughout the event. -**SEO slug:** `convex-2026-ai-enterprise-reality-speaker-experience` - -[Convex Summit 2026 Developer](https://www.convexsummit.com/) conference held in the capital city of Spain, Madrid for the first time. [Plain Concepts](https://www.plainconcepts.com/) company organized this event. 2 days with 3 parallel sessions. 2 of the sessions were Spanish and one of them was English. It was organized on 17, 18 June 2026. - -There is a small moment before every conference talk when the room becomes quiet, the slides are ready, and you suddenly remember why the topic matters. - -For me, that moment happened at CONVEX Summit 2026 in Madrid. - -I was there as a speaker, presenting my session **“Chat with Your Data: Turn any database into a conversational reporting engine.”** But I was also there as a listener, a software architect, and someone trying to understand where enterprise AI is really going after the first wave of demos and experiments. - -CONVEX was interesting because it brought software development, architecture, and AI into the same conversation. These topics are often discussed separately, but in real companies they are tightly connected. AI features do not live in isolation. They live inside applications, databases, identity systems, permission models, workflows, dashboards, and business expectations. - -That was the real theme I felt throughout the event. +## Speaking at CONVEX ![My CONVEX 2026 speaker badge before the sessions started](images/IMG_20927.jpg) -*The speaker badge made the event feel real before the session even started.* - -![The CONVEX 2026 main stage in Madrid](images/IMG_20891.jpg) - -*The stage setup reflected the scale of the event: three communities, one shared conversation.* - -## Speaking at CONVEX - My talk focused on a question that is becoming more important for enterprise software teams: -**What if users could ask their business data questions in natural language—and receive safe, useful, validated answers?** +> What if users could ask their business data questions in natural language and receive safe, useful, validated answers? -I started my slide with Sobrino de Botin restaurant. It's the oldest restaurant in the world according to the Guinness Book of World Records. Sobrino de Botín **has been open since 1725**. The taste never changed, the restaurant **is keeping the same classic taste with the same cooking techniques**, but in the age of AI; we developers should **use new techniques for reporting**. We are not running a restaurant and this is how the development works. Adjust to the tech standards, new techniques and best practises. +I started my slide with **Sobrino de Botin restaurant**. It's the oldest restaurant in the world according to the Guinness Book of World Records. Sobrino de Botín **has been open since 1725**. The taste never changed, the restaurant **is keeping the same classic taste with the same cooking techniques** but in the age of AI; we developers should **use new techniques for reporting**. We are not running a restaurant and this is how the development works. Adjust to the tech standards, new techniques and recent practices. ![image-20260624200005106](images/the-oldest-restaurant-world.png) -And in one part of my talk, I need to ask questions like “Show me the top customers by revenue this quarter.” An AI model generates SQL. The system runs it. The user gets a result. And I learnt some Spanish sentences before the conf. Even for this I used AI. So I translated 10 diffferent English sentences to Spanish. Later I asked ChatGPT: "Can you score my pronunciation for these sentences.". ChatGPT gave the most ratings to 3 of my sentences' prouncations. And I talked those during my interview with my reporting AI tool. And that was fantastic eye-catching moment of my talk. - - +And in one part of my talk, I need to ask questions like “Show me the top customers by revenue this quarter.” So the LLM answers in my language. And I learnt some Spanish sentences before the conf. Even for this I used AI. So I translated 10 different English sentences to Spanish. Later I asked ChatGPT: "*Can you score my pronunciation for these sentences.*". ChatGPT gave the highest ratings to 3 of my sentences' pronunciations. And I talked those during my interview with my reporting AI tool. And that was fantastic eye-catching moment of my talk. -![Opening slide for “Chat with Your Data” at CONVEX 2026](images/IMG_20976.jpg) - -*My session was about building an AI-powered reporting tool that can understand a database and turn questions into controlled reporting workflows.* - -![Presenting my “Chat with Your Data” session at CONVEX 2026](images/IMG_20977.jpg) +![Some photos from my talk 1](images/my-pictures-1.jpg) *My session focused on building conversational reporting experiences without giving up control, validation, or security.* -I asked the attendees how many of you have written SQL, created reporting screen, and %80 of people wrote SQL and created reporting UIs. And same amount of people also use .NET. - -One of my slides showed the reporting problem in a very familiar way. Traditional reporting tools are powerful, but they can be rigid. In many companies, a new report still means writing SQL, designing a page, testing the output, deploying a change, and waiting for a developer to become available. The result is that business users wait, developers become the bottleneck, and valuable data stays trapped behind technical complexity. - -The approach I shared has several layers. - -First, the system needs **schema awareness**. The AI should understand tables, columns, relationships, keys, data types, and enum values. Depending on the system, it may also need safe sample values or extra DDL information. Without that structured context, natural-language reporting becomes fragile. - -![A slide from my session about teaching the AI the database schema](images/IMG_20974.jpg) - -*One of the most important parts of the talk was schema and system-prompt design: tables, columns, relationships, keys, data types, enum values, and controlled rules for query generation.* +I asked the attendees how many of you have written SQL and created reporting screens, and %80 of people wrote SQL and created reporting UIs. And the same number of people also use .NET. -Second, it needs **intent interpretation**. Users do not always speak in database terms. They ask business questions. A good reporting engine should translate business language into technical meaning without forcing users to know the data model. +![Some photos from my talk 2](images/my-pictures-2.jpg) -Third, it needs **safe SQL generation**. Letting an LLM freely generate and execute SQL is risky. The model should operate under strict rules: read-only access, no destructive operations, tenant-aware filtering, query limits, validation before execution, and clear logging. +> **AI can make data more accessible, but architecture must define the boundaries.** -Fourth, it needs **useful output**. A raw result table is not always enough. Sometimes the user needs an Excel file, a chart, a summary, or a clarification question. - -![A session slide about turning data access into a reporting engine](images/IMG_20980.jpg) - -Most business users do not want to “build a report.” They want an answer. But as developers and architects, our job is to make that experience safe, explainable, and reliable. - -AI can make data more accessible, but architecture must define the boundaries. +--- -## What I Learned From the English Sessions +## What I Learned From the Other Sessions -There were 3 parallel session tracks. One of them was completely English sessions. One of the reasons I enjoyed CONVEX was that the English sessions did not treat AI as magic. +There was a track with completely English sessions. One of the reasons I enjoyed Convex was that the English sessions did not treat AI as magic. The strongest message I heard across different sessions was this: -**AI is becoming part of real systems, and real systems have constraints.** +> **AI is becoming part of real systems and real systems have constraints.** And I can see, software development is rapidly evolving with AI agentic tools. -> We cannot say development is dead, but hand-made development is dead. +> **We cannot say development is dead, but hand-made development is dead.** -From now on we'll use our time less on typing and more on thinking about features, user experiences and robust infrastructure. - -![hand-made-coding](images/hand-made-coding.png) +From now on, we'll use our time less on typing and more on thinking about features, user experiences and robust infrastructure. +**In the age of AI, writing code by hand is the software equivalent of sending a fax to prove commitment.** +![hand-made-coding](images/hand-made-coding.png) -For a while, many AI discussions were focused on what AI could generate: code, tests, text, SQL, documentation, designs. At CONVEX, the more interesting question was: +For a while, many AI discussions were focused on what AI could generate: code, tests, text, SQL, documentation, designs. +At CONVEX, the more interesting question was: **What happens after AI generates something?** - Who validates it? - Who owns the decision? +- Can we say I don't know to a question about how that works!? +- If something blows up or happens a data leakage, who is accountable for it? - How does it fit into the architecture? -- How do we prevent data leakage? - How do we make it useful for the business instead of impressive for five minutes? ### Architecture is also a people problem @@ -112,56 +72,86 @@ One session that stayed with me used the idea of the **Prisoner’s Dilemma** to *The architecture sessions connected technical decisions with incentives, collaboration, and long-term system health.* -Another slide suggested practical ways forward: learn the business, understand the competition, avoid “big bang” changes, work incrementally, create options, make trade-off decisions with the business, and **become a business value creator** rather than someone who only responds to requests. +Another slide suggested practical ways forward: learn the business, understand the competition, avoid “big bang” changes, work incrementally, create options, make trade-off decisions with the business and **become a business value creator** rather than someone who only responds to requests. I liked that message. Architecture is much more valuable when it helps the business create options, not when it only explains why something is risky. -### Power, knowledge, and decision-making +--- + +### Power, knowledge and decision-making -Another interesting thread was about power in organizations. One talk referenced **Power-With**, an idea I found useful because it shifts the conversation away from control and toward collaboration. The slides connected power with access to knowledge, authority, charisma, and the way decisions move through an organization. +Another interesting thread was about power in organizations. One talk referenced **Power-With**, I found it useful because it shifts the conversation away from control and toward collaboration. The slides connected power with access to knowledge, authority, charisma and the way decisions move through an organization. ![A session slide discussing Power-With and organizational dynamics](images/IMG_20914.jpg) *Some of the most interesting moments connected architecture with people, incentives, and organizational reality.* -This may sound less technical than a database, a framework, or a deployment pipeline. But in practice, many technical decisions fail or succeed because of organizational dynamics. A clean architecture can still fail if teams are not aligned. A promising AI feature can still fail if no one trusts the output. +There are 2 powers: + +1. **Power-Over**: Conquer other people's mind, control, force and order for a work output. +2. **Power-With**: Co-operate with others, use everyone's power into a big power, move together for a work output. + +This may sound less technical than a database, a framework, or a deployment pipeline. But in practice, many technical decisions fail or succeed because of organizational dynamics. A clean architecture can still fail if teams are not aligned. + +> A promising AI feature can still fail if no one trusts the output. + +### So Let's Think What's Charisma at Work + +- **What's charisma actually?** Let me tell you my opinion; charisma is experience, wisdom, grace, listening more and speaking less, way of looking to life, trustability (reliability), being a role model and an inspiration to others. +- **Why charisma is important?** It makes your words to be listened by other people **naturally** (without any need of dictation). One slide quoted the idea that knowledge workers “think for a living.” Another referenced Peter Naur’s **Programming as Theory Building**, where program text and documentation are not always enough to carry the most important design ideas. That felt very relevant in the age of AI-generated code. If code becomes easier to produce, shared understanding becomes even more valuable. -### AI agents need more than task execution +As a summary; AI can often understand the architecture from the code. What it usually can't know reliably is **why** + +- the architecture ended up that way; the design decisions +- trade-offs +- historical context +- assumptions that exist mostly in the team's shared understanding rather than in the code itself. + +--- + +### What AI Agents can do and cannot do?! + +One of the most thought-provoking slides at the conf explored the current boundaries of AI agents. Instead of asking whether AI will replace humans, it asked a more interesting question: **What can AI agents actually do today, what can't they do and which limitations might disappear over time?** + +The first column listed the things AI agents already do well. They can execute tasks reliably without getting tired, maintain context across long-running work, report progress, detect problems, follow established processes, generate alternatives, document their work, and scale by running thousands of instances simultaneously. In short, AI excels at **execution**. + +The second column focused on capabilities that are fundamentally human today. AI can perform the role of a manager, mentor, or teammate, but it cannot truly *be* one. It cannot be held accountable for its decisions, earn trust through years of shared experience, genuinely care about an outcome, feel the weight of failure, or belong to a team. It can disagree or refuse a request, but not from genuine conviction or personal values. These qualities come from human relationships, responsibility, and lived experience, not from generating the next token. -The AI-related sessions also made a useful distinction between what AI agents can do today and what humans still bring to teams. +The third column added an important nuance. It wasn't titled "Impossible," but rather "Cannot, but maybe will one day." Some capabilities are already beginning to emerge. Multi-agent systems can delegate work to other agents. Better memory and continuity may allow AI to build trust over time. Multimodal models are becoming better at reading context, and reinforcement learning is an early form of learning from mistakes. The point wasn't that these problems are solved, but that some of today's limitations may become tomorrow's capabilities. + +The overall message was refreshingly balanced. AI agents should not be viewed as human employees, nor should they be underestimated. They are exceptional at executing work at scale, but organizations are built on more than execution. Trust, accountability, judgment, conviction, and belonging remain deeply human qualities, for now. -One slide compared capabilities such as being assigned a task, executing work, holding context, reporting progress, flagging anomalies, following processes, generating options, reviewing work, documenting outputs, working asynchronously, and scaling across many instances. -But the same slide also pointed to harder human qualities: accountability, trust earned over time, judgment under competing priorities, reading the room, feeling the weight of failure, belonging to a team, and genuinely caring about the outcome. ![A slide comparing what AI agents can and cannot do](images/IMG_20935.jpg) *The AI agent discussion was interesting because it did not only focus on automation; it also highlighted accountability, trust, judgment, and team dynamics.* -That is a healthy way to talk about AI. Not “AI will replace everything,” and not “AI is useless.” The real question is where AI can help a team, and where humans still need to own the decision. - -### Charisma at Work +That is a healthy way to talk about AI. Not “*AI will replace everything*,” and not “*AI is useless.*” -- **What's Charisma actually?** Let me tell my opinion; The charisma is the experience, the knowledge, wisdom, elegance, listening more and talking less, way of looking to life, performing good on the responsibilities, trustability, being inspiring. -- **Why Charisma is important?** It makes your words to be listened by other people. +> **The real question is where AI can help a team and where humans still need to own the decision.** ### Better decisions need better records -I also followed sessions about architecture principles and decision records. One slide explained principles as priorities, beliefs, guardrails, and a way to connect requirements to architectural decisions. Another used a simple architectural decision example: **Data Store per Service**, where each service owns its data and other services access it through APIs or events instead of direct database queries. +I also followed sessions about architecture principles and decision records. One slide explained principles as priorities, beliefs, guardrails and a way to connect requirements to architectural decisions. Another used a simple architectural decision example: **Data Store per Service**, where each service owns its data and other services access it through APIs or events instead of direct database queries. ![A slide about architecture principles and decisions](images/IMG_20952.jpg) *Architecture principles were presented as guardrails that connect requirements, trade-offs, and decisions.* -This connected nicely with another slide about ADRs. A minimal ADR, based on Michael Nygard’s format, includes a name, status, context, decision, and consequences. A more comprehensive ADR can also include related requirements, assumptions, constraints, options, reasoning, and trade-offs. +Let me first define what's ADR:.. An **ADR (Architecture Decision Record)** is a short document that explains **an important technical decision, why it was made, and what the consequences are**. See the [Microsoft Document about ADR](https://learn.microsoft.com/en-us/azure/well-architected/architect-role/architecture-decision-record). + +> **Code tells you *what* the system does. An ADR tells you *why* it was designed that way.** + +This connected nicely with another slide about ADRs. A minimal ADR, based on Michael Nygard’s format, includes a name, status, context, decision, and consequences. A more comprehensive ADR can also include related requirements, assumptions, constraints, options, reasoning and trade-offs. ![A slide explaining the minimal ADR structure](images/IMG_20960.jpg) *The ADR discussions were a reminder that good architecture is not only about making decisions, but also about preserving the reasoning behind them.* -For .NET teams, these ideas are practical. AI can sit on top of strong foundations around backend services, identity, data access, cloud integration, and enterprise applications—but it should not bypass them. +For .NET teams, these ideas are practical. AI can sit on top of strong foundations around backend services, identity, data access, cloud integration, and enterprise applications, but it should not bypass them. If anything, AI makes good engineering discipline more important. @@ -169,45 +159,33 @@ If anything, AI makes good engineering discipline more important. The venue, Kinépolis Ciudad de la Imagen in Madrid, gave the conference a different feeling from a typical hotel-based event. The rooms, stage, and screens made the sessions feel cinematic. Outside the session rooms, the networking areas were active throughout the day. People were not only exchanging LinkedIn profiles; they were continuing the technical debates from the talks. -![Participants networking at CONVEX Summit 2026](images/IMG_20888.jpg) +![Participants networking at CONVEX Summit 2026](images/convex-ambiance.jpg) *The networking areas were busy between sessions, creating space for conversations beyond the formal agenda.* -![Attendees moving between sessions at the venue](images/IMG_20890.jpg) - -*The event had a steady flow between talks, networking areas, and informal conversations.* - -For me, the most valuable conversations happened after the talk. Several people came with practical questions about AI-assisted reporting: How much schema information should be given to the model? Should generated SQL be shown to users? How do we prevent dangerous queries? Can this work with multi-tenant applications? How should we evaluate the quality of AI-generated reports? What should be logged for auditing? - -These questions matter because they show that teams are moving from curiosity to implementation. They are no longer asking only, “Can we do this?” They are asking, “How can we do this safely inside our product?” - -That is a much better question. - -![A personal moment from the event floor at CONVEX 2026](images/IMG_20970.jpg) - -*I'm preparing my setup and waiting the attendees from lunch :) One of those small personal moments that makes a conference feel memorable, not only useful.* - -A good conference gives you two things: ideas from the stage and better questions from the people you meet. CONVEX did both. +I met people from my country as well from Bosch and Aselsan companies...For me, the most valuable conversations happened after the talk. I made new friends and learn what other people do. ## My Key Takeaways 1. **AI features need data boundaries.** The more natural the interface becomes, the more important permissions, context, and allowed actions become. -2. **Natural language is becoming a product interface.** Users increasingly want answers, not navigation paths. -3. **Architecture is becoming more important, not less.** AI can accelerate delivery, but it cannot remove product constraints, security requirements, or organizational complexity. -4. **Decisions need memory.** Principles, ADRs, trade-offs, and exceptions help teams preserve reasoning. -5. **Conferences still matter.** A hallway discussion after a session can sometimes teach you more than a full article or video. It's a way of motivation, a way of socializing for developers. You see what other do, you discuss with them, you know your customers... +2. **Natural language is becoming a product interface.** Users increasingly want answers, not get lost in the UI and not navigation paths. +3. **Architecture is becoming more important, not less.** AI can accelerate delivery, but it cannot remove product constraints, security requirements, or organizational complexity. + > When the calculator was first invented, they didn't think problem solving finished with this invention; people spent more time solving problems rather than calculating.... +4. **Decisions need memory.** Principles, ADRs, trade-offs and exceptions help teams preserve reasoning. +5. **Conferences still matter.** A hallway discussion after a session can sometimes teach you more than a full article or video. It's **a way of motivation**, a way of socializing for developers. You see what others do, you discuss with them, you know your customers, and you know where you're at in development. +🖼 All photos of Convex Summit 2026 are available 👉 https://www.flickr.com/photos/204742998@N04/albums/72177720334500733/with/55369833471 ## My Cultural Visits -I visited Toledo and Madrid's most important tourist attractions and museums. Now I know way much better then I knew before about Spanish culture and lifestyle. But this is the most impressive moment for me. As you may know I'm Turkish. My grand grandfathers were Ottomans living in Anatolia. In 1571 they were in a war with Spain, Genoa, Malta and Italy. The battle was in the sea near Greece. It's called Sea Battle of Lepanto. In the below pictures, you can see the Ottoman's highest level sea commander (we call him Kaptan-ı Derya -the captain of seas-) personal items when he was died in this war. For those who wants to see it; it's Royal Palace of Madrid and the items are in Royal Armoury department. +I visited Toledo and Madrid's most important tourist attractions and museums. Now I know way more than I knew before about Spanish culture and lifestyle. But this is the most impressive moment for me. As you may know I'm Turkish. My grand grandfathers were Ottomans coming from Mongolia to Anatolia. In 1571 the Ottomans were in a war with Spain, Genoa, Malta and Italy. The battle was in the sea near Greece. It's called Sea Battle of Lepanto. In the pictures below, you can see the Ottoman's highest-level sea commander's personal items. We call him **Kaptan-ı Derya** -the captain of the seas-. He died in this war. For those who want to see it, it's in the Royal Palace of Madrid, and the items are in the Royal Armory department. -If you're interested in this war, I'll little bit tell you about it. +If you're interested in this war, you can also read this part: ### The Battle of Lepanto ⚔ -The Ottoman army was invading Cyprus. Angered by this, European countries asked Spain—one of the strongest kingdoms of the period—for help. To retake the island, a Holy Christian fleet was assembled under Spanish leadership. On 7 October 1571, the forces arrived at the Gulf of Patras in Greece for what would become known as the Battle of Lepanto. On one side was the Ottoman army, commanded by Ali Pasha, known as Kaptan-ı Derya (the Grand Admiral). On the opposing side were the major European powers: Spain, Venice, the Papacy, Genoa, the Knights of Malta, and Italy. It would become the last major naval battle fought with oared warships. The Ottomans lost the battle, and the sultan of that time Sokollu Mehmed Pasha said the following famous saying: “*You have cut off our beard, but we have cut off your arm. A beard grows back.*” More than 200 Ottoman ships were lost. Tens of thousands of soldiers were killed or captured. In the below photograph, you will see war trophies taken from Ali Pasha. In Spain, this battle would be called “*La Defensa de la Cristiandad*” means “*The Defense of Christianity*” and would be used for propaganda for years. These trophies were used as symbols. The first time I saw the exhibit in a museum, I stood in front of it for 15–20 minutes, simply looking at it. +The Ottoman army was invading Cyprus. Angered by this, European countries asked Spain, one of the strongest kingdoms of the period for help. To retake the island, a Holy Christian fleet was assembled under Spanish leadership. On 7 October 1571, the forces arrived at the Gulf of Patras in Greece for what would become known as the Battle of Lepanto. On one side was the Ottoman army, commanded by Ali Pasha (the Grand Admiral). On the opposing side were the major European powers: Spain, Venice, the Papacy, Genoa, the Knights of Malta, and Italy. It would become the last major naval battle fought with oared warships. The Ottomans lost the battle and Sokollu Mehmed Pasha (he's normally Serbian Turk and he's the most powerful manager after Sultan) said the following famous saying: “*You have cut off our beard, but we have cut off your arm. A beard grows back.*” More than 200 Ottoman ships were lost. Tens of thousands of soldiers were killed or captured. In the below photograph, you will see war trophies taken from Ali Pasha. In Spain, this battle would be called “*La Defensa de la Cristiandad*” means “*The Defense of Christianity*” and would be used for propaganda for years. These trophies were used as symbols. The first time I saw the exhibit in a museum, I stood in front of it for 15–20 minutes, simply looking at it. ![royal-palace-0](images/royal-palace-0.jpg) @@ -230,21 +208,19 @@ From the outside, it looks like nothing more than a “brutal spectacle,” but - **Bulls don’t react to the color red:** They are actually color-blind. What triggers them is the movement of the cape, not its color. Red is purely for visual aesthetics. - **The selected bull is a special, wild breed:** The animal has almost never seen a human before entering the arena. It sees the matador as an enemy and wants to destroy him. - **The greatest honor is to survive:** If a bull shows exceptional nobility and courage, the audience waves white handkerchiefs to ask for its pardon. That bull never enters the arena again and spends the rest of its life like a king on a farm. -- **A dance with death:** Matadors do not see this as a sport, but as a way of confronting death. When making the most critical strike, the matador must also put his own life at risk—he cannot simply stab the bull from behind. He has to face the bull head-on, bravely. In that sense, there is a strange bond of respect between them. The bull is powerful, but the matador is intelligent. He cannot defeat it through strength, only through skill, timing, and agility. +- **A dance with death:** Matadors do not see this as a sport, but as a way of confronting death. When making the most critical strike, the matador must also put his own life at risk, he cannot simply stab the bull from behind. He has to face the bull head-on, bravely. In that sense, there is a strange bond of respect between them. The bull is powerful, but the matador is intelligent. He cannot defeat it through strength, only through skill, timing, and agility. - **It is a controversial subject**, but hearing it firsthand in its own context changed my perspective considerably. At a time when animal rights are more important than ever, this tradition still continues. And I have now learned that bullfighting has a much deeper philosophy behind it than I had realized. ![bull-fight](images/bull-fight.jpg) ## Closing -I left CONVEX 2026 with new ideas, useful feedback, and a stronger belief that the next phase of enterprise AI will be less about impressive demos and more about trusted systems. +I left Convex 2026 and Spain with new ideas, useful feedback, and a stronger belief that the next phase of enterprise AI will be less about impressive demos and more about trusted systems. For me, the most interesting AI features are not the ones that look magical. They are the ones that quietly solve a real problem, respect the architecture around them, and help users make better decisions. -Thank you to the CONVEX organizers, the speakers, and everyone who joined my session or continued the conversation afterward. +Thank you to the Convex organizers, the speakers, and everyone who joined my session or continued the conversation afterward. Madrid was a great place to talk about AI, .NET, architecture, and the future of enterprise software. I hope to see many of you again at the next event. - - --- diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20888.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20888.jpg deleted file mode 100644 index a8f5ef6f66..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20888.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20890.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20890.jpg deleted file mode 100644 index a1b01c9fb8..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20890.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20891.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20891.jpg deleted file mode 100644 index 4e4c270bfd..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20891.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20927.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20927.jpg index bbbe5a65fe..039bcdf1e0 100644 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20927.jpg and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20927.jpg differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20935.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20935.jpg index db857de746..235118bfc7 100644 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20935.jpg and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20935.jpg differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20970.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20970.jpg deleted file mode 100644 index ecf4aeca68..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20970.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20974.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20974.jpg deleted file mode 100644 index 27c58fb724..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20974.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20976.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20976.jpg deleted file mode 100644 index 5a63cc655a..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20976.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20977.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20977.jpg deleted file mode 100644 index 205f4eecd3..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20977.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20980.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20980.jpg deleted file mode 100644 index 53ef1a8338..0000000000 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20980.jpg and /dev/null differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/convex-ambiance.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/convex-ambiance.jpg new file mode 100644 index 0000000000..854c25ef69 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/convex-ambiance.jpg differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/cover.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/cover.jpg new file mode 100644 index 0000000000..27917025b8 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/cover.jpg differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/hand-made-coding.png b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/hand-made-coding.png index ef5fa9d8c9..d71ea044cb 100644 Binary files a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/hand-made-coding.png and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/hand-made-coding.png differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-1.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-1.jpg new file mode 100644 index 0000000000..d3f9b43334 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-1.jpg differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-2.jpg b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-2.jpg new file mode 100644 index 0000000000..a7741b804c Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-2.jpg differ diff --git a/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/volosoft-presentation.pptx b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/volosoft-presentation.pptx new file mode 100644 index 0000000000..8b25522e54 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/volosoft-presentation.pptx differ diff --git a/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/Post.md b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/Post.md new file mode 100644 index 0000000000..a36eab93e8 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/Post.md @@ -0,0 +1,349 @@ +A lot of the current AI discussion in software development swings between two extremes: either AI will write everything, or it is just autocomplete with better marketing. + +Neither view is especially useful. + +What the evidence shows is more practical: AI coding tools can improve developer throughput on certain tasks, especially repetitive work, scaffolding, and first drafts. But they do not remove the need for developers. In many teams, they actually create a new category of work around review, verification, security, and long-term maintainability. + +That is the real story. AI is not replacing developers. It is changing what developers do, what teams optimize for, and where engineering judgment matters most. + +## The productivity gains are real, but they are not magic + +There is enough data now to move beyond hot takes. + +Across multiple studies and industry reports, AI coding assistants show measurable productivity gains, but those gains are usually modest rather than transformational: + +- A BlueOptima analysis across 30,000 developers in 18 enterprises reported an average productivity uplift of 5.4%, with the most active users seeing gains closer to 20%. +- An open source study found roughly a 6.5% project-level productivity increase after Copilot adoption. +- GitHub survey data from more than 2,000 developers showed strong perceived benefits: improved flow, less mental drain on repetitive tasks, and greater job satisfaction. +- A longitudinal study from a large public-sector engineering organization found that developers using Copilot were already highly active, and while they reported productivity improvements, commit-based metrics did not show a statistically significant post-adoption jump. + +That last point matters. + +Perceived productivity and actual output are not always the same thing. Developers may feel faster because they spend less time on boilerplate, search, or syntax recall. That feeling is valuable. Less friction often means better focus. But it does not automatically translate into dramatically more shipped business value. + +In other words, AI helps, but it does not suspend the usual constraints of software delivery: + +- unclear requirements still slow teams down +- poor architecture still creates drag +- bad testing practices still leak defects +- messy codebases are still messy codebases + +If your delivery bottleneck is typing, AI looks revolutionary. If your bottleneck is product ambiguity, compliance, integration complexity, or production risk, AI helps less than the marketing suggests. + +## What AI coding tools are actually good at + +The strongest use case for AI in development is not autonomous software engineering. It is acceleration of narrow, well-bounded tasks. + +AI coding assistants are usually good at: + +- generating boilerplate +- filling in repetitive CRUD patterns +- writing simple tests and test skeletons +- suggesting refactors +- producing documentation drafts +- translating between languages or frameworks +- helping developers recall APIs and syntax +- creating a first pass for routine utility code + +This is why many developers genuinely like these tools. They reduce low-value friction. + +A practical example: + +A developer building an ABP-based application might use AI to: + +- scaffold DTO mappings +- draft validation rules +- generate basic unit test cases +- create repository query examples +- summarize a service class before refactoring + +Those are useful accelerators. But the same tool is much less reliable when asked to decide: + +- whether a module boundary is correct +- how to model a permission system +- what tradeoff to make between consistency and performance +- how multi-tenancy affects data access rules +- which abstraction will still be maintainable a year later + +That is the dividing line. AI handles local code generation better than system-level reasoning. + + + +![Generated illustration](inline-1.png) + +## Where developers are still irreplaceable + +The most valuable parts of software development were never just typing code. + +Developers are still responsible for the parts AI consistently struggles with: + +### Understanding the problem behind the ticket + +Business requirements are often incomplete, contradictory, or politically constrained. A human developer can ask the uncomfortable question, spot hidden assumptions, and translate vague intent into a workable implementation. + +AI can generate an answer. It cannot reliably challenge the question. + +### Making architecture tradeoffs + +Real systems involve tradeoffs, not ideal answers. + +Should this feature live in an existing module or a new service? Is eventual consistency acceptable here? Are we optimizing for onboarding speed, runtime performance, auditability, or cost control? + +These decisions depend on context that usually lives outside the prompt window. + +### Working safely in large, imperfect codebases + +Most production systems are not greenfield demos. They include legacy code, weird integrations, undocumented conventions, and historical constraints. + +This is where experienced developers earn their keep. They know that the technically correct change is not always the operationally safe change. + +### Taking responsibility for outcomes + +An AI assistant does not get paged at 2 a.m. It does not own the incident review. It does not explain a data leak to legal, security, or customers. + +Software engineering is not just generation. It is accountability. + +## The hidden cost: verification debt + +One of the most important ideas in the current AI coding debate is verification debt. + +AI can generate code quickly, but that speed often shifts effort downstream. Instead of spending time writing code, teams spend time validating whether the generated code is correct, secure, idiomatic, and maintainable. + +That creates a new form of debt: + +- code is produced faster than it is reviewed properly +- weak suggestions slip into the codebase because they look plausible +- reviewers must inspect more generated code with lower trust +- maintenance costs rise later because low-context code ages badly + +Recent survey data points in the same direction: + +- 72% of developers reported using AI tools daily +- AI contributes a substantial share of committed code in some teams +- 96% of developers do not fully trust AI-generated code +- less than half consistently review AI-generated code before committing +- 38% say reviewing AI code can take longer than reviewing human-written code + +That combination should worry engineering leaders. + +If teams accept more machine-generated code while also trusting it less, the result is not full automation. The result is a fragile review pipeline. + +This is why senior engineers are not becoming obsolete. Their work is shifting toward validation, standards, and system integrity. + + + +![Generated illustration](inline-2.png) + +## Security is the clearest reason AI won’t replace developers + +If you want one hard reality check, it is security. + +AI-generated code often looks polished. That makes insecure output more dangerous, not less dangerous. + +Research and industry testing have found recurring problems such as: + +- flawed input validation +- weak authentication or authorization logic +- unsafe serialization patterns +- insecure defaults +- cross-site scripting exposure +- log injection issues +- dependency and configuration mistakes + +A Veracode study covering 100 LLMs across 80 coding tasks found that about 45% of AI-generated code samples contained security flaws. Reported failure rates were especially high in some languages and security-sensitive tasks. + +That aligns with what many teams see in practice: AI can produce code that appears complete while quietly missing the exact defensive details that matter in production. + +There is a second security problem too: the tools themselves. + +Recent research into AI-enabled IDE workflows has highlighted risks such as: + +- prompt injection through project content +- data exfiltration from workspace context +- misuse of tool permissions +- remote code execution paths via compromised assistant workflows + +So the risk surface is now two-layered: + +1. the generated code may be unsafe +2. the coding assistant environment may itself introduce supply-chain and data exposure risks + +That is not a path to replacing developers. It is a path to needing more disciplined developers. + + + +![Generated illustration](inline-3.png) + +## Why junior and senior developers benefit differently + +AI does not help every developer in the same way. + +Less experienced developers often benefit the most from: + +- faster onboarding +- easier exploration of unfamiliar APIs +- reduced time spent on repetitive syntax work +- quick examples to unblock momentum + +That is a good thing. Used well, AI can shorten the distance between "I know the concept" and "I can build the first version." + +But there is a catch. + +If juniors over-rely on generated solutions they do not understand, they can ship code without building judgment. That creates a team with higher output but thinner engineering depth. + +Senior developers usually get less value from raw generation and more value from targeted acceleration. Their role shifts toward: + +- architectural direction +- code review and design review +- defining guardrails +- mentoring developers on when not to trust the tool +- shaping prompts and workflows around quality + +That is not replacement. It is role redistribution. + +## The best teams treat AI like a power tool, not a developer + +The most productive framing is simple: AI is a power tool. + +A power tool can make a skilled worker much faster. It can also let an unskilled worker make bigger mistakes faster. + +Teams getting real value from AI coding assistants usually do a few things consistently. + +### They define where AI is allowed to help + +For example: + +- okay for scaffolding and test drafts +- okay for documentation summaries +- okay for refactoring suggestions in low-risk modules +- not okay for auth flows without explicit review +- not okay for security-sensitive changes without human design approval +- not okay for direct commits to critical paths + +### They keep human review non-negotiable + +AI-generated code should be reviewed like code from a new team member who is fast, confident, and occasionally wrong in subtle ways. + +That means checking: + +- correctness +- security +- consistency with project conventions +- operational impact +- maintainability six months from now + +### They invest in guardrails + +Useful guardrails include: + +- secure coding standards +- mandatory tests for generated code +- SAST and dependency scanning +- branch protection and review policies +- secret scanning +- documented AI usage rules +- prompt hygiene, especially around sensitive data + +### They optimize for maintainability, not just speed + +The wrong metric is lines generated. + +Better metrics include: + +- cycle time without increased incident rate +- review burden +- escaped defects +- time to understand generated code later +- security findings per change + + + +![Generated illustration](inline-4.png) + +## When AI helps most — and when it helps least + +A balanced view is more useful than either fear or hype. + +### When to use AI-assisted coding + +AI is a strong fit when: + +- the task is repetitive or pattern-based +- the scope is narrow and easy to verify +- the code is low-risk and well-tested +- you need a first draft, not a final answer +- developers understand the output well enough to challenge it +- the team has solid review and security practices + +Examples: + +- generating DTOs, mappings, and validation stubs +- producing test cases for straightforward services +- drafting migration scripts that will be reviewed carefully +- summarizing unfamiliar code before manual refactoring + +### When not to rely on AI-assisted coding + +AI is a poor fit when: + +- business rules are complex or ambiguous +- security is central to the change +- architecture decisions are still in flux +- the code touches compliance-heavy or highly regulated paths +- the surrounding codebase has lots of undocumented behavior +- the team is unlikely to review the output carefully + +Examples: + +- permission and tenancy boundaries +- payment or identity workflows +- critical infrastructure automation +- cross-service consistency logic +- sensitive data handling and audit trails + +## What this means for the future of software teams + +AI is changing software development, but not in the simplistic way people often describe. + +The likely outcome is not fewer developers because code writes itself. The more plausible outcome is a different distribution of engineering work: + +- more generated code +- more review and verification work +- more emphasis on architecture and systems thinking +- more value placed on security awareness +- more leverage for developers who can guide tools effectively + +There are also organizational effects. + +If AI tools can remove some low-level friction, teams may ship faster. Some studies and industry analyses even project large macroeconomic gains from AI-augmented software work. But inside engineering organizations, those gains depend on whether speed is paired with discipline. + +Without discipline, AI increases noise. + +With discipline, AI increases leverage. + +That is the distinction leaders should care about. + +## The developer job is not disappearing — it is getting more judgment-heavy + +The strongest developers in the AI era will not be the ones who generate the most code. They will be the ones who can: + +- define the problem clearly +- evaluate tradeoffs +- spot incorrect assumptions +- review machine output efficiently +- protect quality under delivery pressure +- turn generated fragments into coherent systems + +That is a more senior version of software engineering, not a smaller one. + +Typing code was never the whole profession. It was just the most visible part. AI is making that easier, which means the less visible parts now matter even more. + +And those parts are deeply human: judgment, context, responsibility, and taste. + +## TL;DR + +- AI coding tools improve productivity on repetitive, bounded tasks, but the gains are usually incremental, not total automation. +- Developers are still needed for architecture, business logic, tradeoffs, security, and accountability. +- AI-generated code often adds verification debt, increasing review and maintenance work later. +- Security remains a major limitation, both in generated code and in AI-assisted development workflows. +- The winning teams use AI as a power tool with strong guardrails, not as a replacement for engineering judgment. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/cover.png b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/cover.png new file mode 100644 index 0000000000..9e5cfb860b Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/cover.png differ diff --git a/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-1.png b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-1.png new file mode 100644 index 0000000000..d2c734c3b7 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-1.png differ diff --git a/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-2.png b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-2.png new file mode 100644 index 0000000000..724d15d84e Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-2.png differ diff --git a/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-3.png b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-3.png new file mode 100644 index 0000000000..ba64f15192 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-3.png differ diff --git a/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-4.png b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-4.png new file mode 100644 index 0000000000..ddbbdb74fd Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-4.png differ diff --git a/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/Post.md b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/Post.md new file mode 100644 index 0000000000..6c9a4e4a76 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/Post.md @@ -0,0 +1,631 @@ +Caching is one of those topics that looks simple until an application starts scaling. The first version works fine with direct database reads. Then traffic grows, page loads become inconsistent, and suddenly the team is debating Redis, stale data, invalidation, and why one node sees fresh data while another still serves old results. + +ABP Framework gives you a solid caching foundation, but the important part is choosing the right caching strategy for the job. Not everything should be cached the same way. A read-only lookup list, a tenant-specific settings object, and an entity that changes every minute do not have the same caching needs. + +This article walks through the practical caching strategies in ABP Framework, what each one is good at, how to configure them, and the mistakes that usually show up in production. + +## Understand ABP's caching model first + +ABP builds its caching support on top of `Microsoft.Extensions.Caching.Distributed.IDistributedCache`. That matters because ABP does not invent a completely separate caching universe. Instead, it adds practical features developers actually need in real systems: + +- typed cache abstractions +- automatic serialization and deserialization +- tenant-aware cache keys +- configurable key prefixes +- batch operations +- optional Unit of Work awareness +- safer error handling defaults + +Out of the box, the default distributed cache implementation is `MemoryDistributedCache`. Despite the name, this is still wired through the distributed cache abstraction, but the storage is in-memory for the current app instance. + +That is fine for: + +- local development +- demos +- single-node monoliths +- low-risk cached reads + +It is not enough for: + +- load-balanced deployments +- Kubernetes or App Service scale-out +- background workers sharing cached data with web apps +- any scenario where multiple instances must see the same cache state + +In those cases, you should move to a real distributed provider such as Redis. + + + +![Generated illustration](inline-1.png) + +## Strategy 1: Use typed distributed cache for application data + +For most ABP applications, the default and most useful strategy is the generic typed distributed cache. + +ABP provides: + +- `IDistributedCache` +- `IDistributedCache` + +These abstractions remove a lot of repetitive work. You do not have to manually serialize objects, invent every cache key shape yourself, or worry about tenant ID inclusion for common cases. + +### Why typed distributed cache is usually the best starting point + +It works well when you want to cache: + +- lookup lists +- settings snapshots +- permission-related read models +- dashboard widgets +- expensive API responses +- aggregated DTOs used by the UI + +This strategy is usually better than caching raw entities because cached application-facing models tend to be: + +- smaller +n- more stable +- easier to version +- less coupled to domain changes + +### Example: cache a product summary DTO + +```csharp +using Microsoft.Extensions.Caching.Distributed; +using Volo.Abp.Caching; + +[CacheName("ProductSummary")] +public class ProductSummaryCacheItem +{ + public Guid Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + public bool IsAvailable { get; set; } +} + +public class ProductAppService : ApplicationService +{ + private readonly IDistributedCache _cache; + private readonly IRepository _productRepository; + + public ProductAppService( + IDistributedCache cache, + IRepository productRepository) + { + _cache = cache; + _productRepository = productRepository; + } + + public async Task GetSummaryAsync(Guid id) + { + return await _cache.GetOrAddAsync( + id, + async () => + { + var product = await _productRepository.GetAsync(id); + + return new ProductSummaryCacheItem + { + Id = product.Id, + Name = product.Name, + Price = product.Price, + IsAvailable = product.StockCount > 0 + }; + }, + () => new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(10), + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) + } + ); + } +} +``` + +A few good things are happening here: + +- the cache item is small and explicit +- the key is strongly typed +- expiration is defined close to the use case +- both sliding and absolute expiration are used + +That last point is important. Sliding expiration alone can keep hot items alive indefinitely. Absolute expiration alone can evict popular items too aggressively. In many business cases, combining them gives you a better balance. + +### When to use + +Use typed distributed cache when: + +- you want a simple, explicit cache around a read operation +- the cached model is a DTO or a lightweight read model +- invalidation can be handled in application logic +- you need tenant-aware behavior without extra plumbing + +### When NOT to use + +Avoid it when: + +- the underlying data changes extremely often and stale reads are unacceptable +- the object is very large and serialization cost outweighs the benefit +- cache invalidation is too complex to reason about safely +- the query is already cheap and highly selective + +## Strategy 2: Use entity cache for read-heavy entity access + +ABP also provides an entity cache abstraction for read-only entity-level caching. This is useful when you repeatedly fetch entities or entity-based DTOs by ID and want cache invalidation to happen automatically on update or delete. + +This is where entity cache can save real effort. Instead of manually wiring remove calls in every update path, you lean on the framework's invalidation behavior. + +### What entity cache is good at + +Entity cache is a good fit for: + +- catalogs +- countries, regions, tax definitions +- organization units that are read often but changed infrequently +- profile-like records fetched by ID repeatedly + +It is a bad fit for highly volatile entities where every read risks becoming stale within seconds. + +### Example use case + +Suppose your application repeatedly loads a `Category` record by ID from both HTTP requests and background jobs. That category changes maybe once a week. Entity cache is a better fit than manually managing many distributed cache entries across the codebase. + +The main advantage is operational simplicity: + +- read-through usage is straightforward +- updates and deletes trigger invalidation automatically +- you get consistency improvements without scattering cache removal logic everywhere + +### A practical warning about entity versioning + +ABP supports entity versioning through `IHasEntityVersion`. If an entity implements it, ABP increments the `EntityVersion` on updates and uses that in invalidation-related behavior. + +That is useful, but there is one common trap: direct SQL updates outside the normal application flow bypass entity versioning and the domain pipeline. + +If your team runs scripts like this: + +```sql +update Products set Name = 'New Name' where Id = '...' +``` + +then your cache may not be invalidated as expected. + +If you use entity cache, make sure updates go through the application and domain stack whenever possible. If operational SQL scripts are unavoidable, explicitly account for cache invalidation. + +### When to use + +Use entity cache when: + +- reads are frequent and mostly by entity key +- entities change infrequently +- automatic invalidation on update/delete is valuable +- you want less manual cache removal code + +### When NOT to use + +Avoid it when: + +- the read model should differ significantly from the entity shape +- data is updated too frequently +- your team often bypasses the application layer with direct SQL updates +- the cached object graph is large or expensive to serialize + + + +![Generated illustration](inline-2.png) + +## Strategy 3: Prefer Redis for real distributed deployments + +A lot of caching problems are not about API design. They are deployment problems. + +If you run multiple application instances and still use the default in-memory distributed cache implementation, each node will maintain its own private cache state. That means: + +- node A may have fresh data +- node B may have stale data +- invalidation on one node does not magically update the others +- behavior becomes inconsistent under load balancing + +For production scale-out, Redis is usually the practical answer. + +ABP provides Redis integration through `Volo.Abp.Caching.StackExchangeRedis`. + +### Basic setup idea + +Install the Redis caching package and configure distributed caching as your backing provider. ABP then continues to use its caching abstractions, while Redis stores the actual cache entries. + +A typical module configuration looks like this: + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Caching; +using Volo.Abp.Modularity; + +[DependsOn(typeof(AbpCachingStackExchangeRedisModule))] +public class MyProjectModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + context.Services.AddStackExchangeRedisCache(options => + { + options.Configuration = configuration["Redis:Configuration"]; + }); + + Configure(options => + { + options.KeyPrefix = "MyApp"; + options.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(20); + options.HideErrors = true; + }); + } +} +``` + +### Why the key prefix matters + +If the same Redis server is shared by multiple applications or environments, a global key prefix is not optional in practice. Without it, key collisions become surprisingly easy. + +Good examples: + +- `MyApp-Prod` +- `SalesService` +- `TenantPortal` + +Bad example: + +- leaving it empty and hoping naming conventions elsewhere are enough + +## Strategy 4: Use batch cache operations for high-volume reads + +If you need to fetch many cache entries at once, ABP supports batch operations such as: + +- `GetManyAsync` +- `SetManyAsync` +- `RemoveManyAsync` + +This matters most in list and aggregation scenarios. + +For example, imagine a product page that needs cached summaries for 50 product IDs. Doing 50 individual round-trips is not ideal. If the provider supports batch operations well, this can reduce latency significantly. + +### Example: batch loading summaries + +```csharp +public async Task> GetManySummariesAsync(Guid[] ids) +{ + var cachedItems = await _cache.GetManyAsync(ids); + + var missingIds = ids + .Where(id => !cachedItems.ContainsKey(id) || cachedItems[id] == null) + .ToArray(); + + if (missingIds.Any()) + { + var products = await _productRepository.GetListAsync(x => missingIds.Contains(x.Id)); + + var newItems = products.ToDictionary( + x => x.Id, + x => new ProductSummaryCacheItem + { + Id = x.Id, + Name = x.Name, + Price = x.Price, + IsAvailable = x.StockCount > 0 + }); + + await _cache.SetManyAsync( + newItems, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(10) + }); + + foreach (var item in newItems) + { + cachedItems[item.Key] = item.Value; + } + } + + return ids + .Where(id => cachedItems.ContainsKey(id) && cachedItems[id] != null) + .Select(id => cachedItems[id]) + .ToList(); +} +``` + +Provider support matters here. With Redis and ABP's Redis package, batch operations are especially useful. If the underlying provider does not support them efficiently, ABP can fall back to single operations. + +That means batch APIs are still worth using from an application-code perspective, but you should validate the real performance characteristics in your deployed environment. + +## Strategy 5: Make cache writes Unit of Work aware when consistency matters + +One subtle but valuable ABP feature is the `considerUow` flag on typed distributed cache operations. + +This is easy to overlook, but it can prevent a nasty class of bugs. + +Imagine this sequence: + +1. You update an entity. +2. You write a corresponding cache value immediately. +3. The database transaction later fails and rolls back. +4. The cache now contains data representing a change that never actually committed. + +That is classic stale-or-phantom cache state. + +When `considerUow` is enabled, ABP can defer cache writes until the Unit of Work completes successfully. + +### Example + +```csharp +await _cache.SetAsync( + id, + cacheItem, + options: new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) + }, + considerUow: true +); +``` + +Use this when cache state depends on transactional data changes in the same operation. + +### When to use + +Use `considerUow` when: + +- you update data and cache in the same business operation +- transaction rollback is possible +- cache correctness matters more than immediate write timing + +### When NOT to use + +You may skip it when: + +- you are caching purely read-side data after a committed fetch +- the operation is outside transactional boundaries +- eventual cache population is acceptable + + + +![Generated illustration](inline-3.png) + +## Strategy 6: Treat multi-tenancy as a cache design concern, not a detail + +ABP automatically includes the current tenant ID in cache keys for typed distributed cache scenarios unless multi-tenancy is explicitly ignored. + +This is one of those features that quietly prevents serious data leaks. + +Without tenant-aware cache keys, this can happen: + +- tenant A requests a settings object +- it gets cached under a generic key +- tenant B requests the same logical object +- tenant B receives tenant A's cached data + +That is not just a bug. In many systems, it is a security incident. + +### Practical guidance + +For multi-tenant systems: + +- keep tenant-aware caching enabled by default +- only ignore multi-tenancy for truly global shared data +- review custom key-building logic carefully +- test cache behavior with at least two tenants in integration tests + +If a cache item is intentionally global, make that decision explicit and document it. + +## Strategy 7: Be deliberate about expiration policy + +A lot of bad caching behavior comes from expiration values chosen almost randomly. + +ABP lets you define expiration using `DistributedCacheEntryOptions`, including: + +- `AbsoluteExpiration` +- `AbsoluteExpirationRelativeToNow` +- `SlidingExpiration` + +ABP also supports global defaults through `AbpDistributedCacheOptions`. If you do not specify item-level options, a default sliding expiration is commonly configured as 20 minutes. + +### A simple rule of thumb + +- Use sliding expiration for frequently accessed, low-volatility items. +- Use absolute expiration when freshness has a hard upper bound. +- Use both when you want hot items to stay warm, but not forever. + +### Example global configuration + +```csharp +Configure(options => +{ + options.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(20); + options.HideErrors = true; + options.KeyPrefix = "MyApp"; +}); +``` + +### Common expiration patterns + +**Reference data** + +- sliding: 30 to 60 minutes +- absolute: 6 to 24 hours + +**User-specific dashboard data** + +- sliding: 5 to 15 minutes +- absolute: 15 to 60 minutes + +**Highly dynamic operational metrics** + +- short absolute expirations, or no cache at all + +These are not universal numbers, but they are more realistic than setting every cache entry to 24 hours and calling it done. + +## Strategy 8: Keep cache items small and serialization-friendly + +Distributed caching always includes serialization and deserialization overhead. ABP handles this for you, with JSON serialization by default, but the cost still exists. + +That means cache item design matters. + +### Prefer this + +- lean DTO-style cache items +- primitive properties +- only fields needed by the consuming path +- stable shapes that do not change constantly + +### Avoid this + +- huge object graphs +- navigation-heavy entities +- deeply nested collections when only a few fields are used +- caching everything just because it was already available in memory + +A cache entry should usually be optimized for read efficiency, not for domain completeness. + +If a page needs only `Name`, `Price`, and `Status`, do not cache the entire entity graph with audit fields, children, and metadata. + +## Strategy 9: Decide how hard cache failures should fail + +ABP defaults to a practical stance: cache errors are hidden and logged so your application can continue functioning. + +This default is often correct. + +If Redis has a transient issue, it is usually better for the request to fall back to the database than to fail completely. Caching should improve performance, not become a single point of failure. + +You can control this behavior globally with `AbpDistributedCacheOptions.HideErrors` and per operation with the `hideErrors` parameter. + +### Good default thinking + +Keep `HideErrors = true` when: + +- cache is a performance optimization +- falling back to source data is acceptable +- temporary cache outages should not break user flows + +Consider stricter behavior when: + +- cache is part of a critical coordination pattern +- silent fallback would overload downstream systems +- you are diagnosing a production issue and want failures surfaced more aggressively + +In most business applications, hidden-and-logged cache failures are the safer default. + +## What about automatic method-level caching? + +You may have seen community implementations that add automatic method-level caching through interception and a `[Cache]` attribute. + +That pattern can be attractive because it reduces boilerplate: + +- decorate a method +- define expiration +- cache the return value transparently +- optionally connect invalidation to entity changes + +It is a useful pattern, but it is important to say clearly: this is not part of ABP core. + +So treat it as an architectural choice, not a built-in feature. + +### Why teams like it + +- less repetitive cache code +- centralized cache policy +- easier adoption for query-heavy services + +### Why teams get into trouble with it + +- invalidation becomes less explicit +- stale data bugs are harder to trace +- cache scope decisions can become too magical +- developers may not realize when a method result is tenant-specific or user-specific + +If you adopt method-level caching, document it aggressively and be strict about invalidation rules. It can be productive, but only when the team fully understands the behavior. + +## Common mistakes in ABP caching + +Here are the mistakes that cause the most pain. + +### Using in-memory distributed cache in a multi-instance production setup + +This is probably the most common one. It works in testing, then becomes inconsistent under scale-out. + +Fix: use Redis or another true distributed cache provider. + +### Caching entities instead of read models by default + +This increases serialization cost and couples cache shape to domain shape. + +Fix: cache DTOs or purpose-built cache items unless entity cache is clearly the better fit. + +### Forgetting invalidation paths + +Manual caches live or die by invalidation quality. + +Fix: centralize writes, remove cache entries on updates, and use entity cache where automatic invalidation helps. + +### Relying only on sliding expiration + +Hot keys may stay forever. + +Fix: combine sliding and absolute expiration for many scenarios. + +### Ignoring tenant boundaries + +This can leak data across tenants. + +Fix: rely on ABP's tenant-aware key behavior and be very careful with custom key generation. + +### Writing to cache before transaction success + +This creates cache values for changes that later roll back. + +Fix: use `considerUow` for transactional cache writes. + +### Treating cache outages as impossible + +Eventually, your cache provider will have a bad day. + +Fix: decide upfront whether fallback or fail-fast behavior is right for each path. + +## A practical decision guide + +If you just want a sensible default approach for most ABP projects, this is a good starting point: + +1. Use typed distributed cache for expensive read models and DTOs. +2. Use Redis for anything beyond a single instance. +3. Use entity cache for read-heavy entities fetched by ID when automatic invalidation is valuable. +4. Combine sliding and absolute expiration for most business data. +5. Keep cache items small. +6. Use tenant-aware keys by default. +7. Use `considerUow` for cache writes tied to transactions. + +That covers a large percentage of real-world ABP caching needs without overengineering the system. + +## When to use / When NOT to use caching in ABP + +### Use caching when + +- the same data is read frequently +- computing or querying the result is expensive +- modest staleness is acceptable +- the cache key can be defined clearly +- invalidation rules are understandable + +### Do NOT use caching when + +- the underlying data changes constantly +- every read must reflect the latest committed value immediately +- the query is already cheap +- object serialization cost is high relative to the saved work +- the team cannot confidently maintain invalidation rules + +Caching is a performance tool, not a default architecture layer for every service method. + +## TL;DR + +- In ABP, typed distributed cache is the best default for caching DTOs and read models. +- `MemoryDistributedCache` is fine for single-instance apps, but scaled deployments should use Redis. +- Entity cache is useful for read-heavy entity access with automatic invalidation on update and delete. +- Use tenant-aware keys, sensible expiration policies, and `considerUow` to avoid subtle consistency bugs. +- Keep cache items small, explicit, and easy to invalidate. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/cover.png b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/cover.png new file mode 100644 index 0000000000..c95b525828 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/cover.png differ diff --git a/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-1.png b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-1.png new file mode 100644 index 0000000000..f64ec05422 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-1.png differ diff --git a/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-2.png b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-2.png new file mode 100644 index 0000000000..5e7881c7d3 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-2.png differ diff --git a/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-3.png b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-3.png new file mode 100644 index 0000000000..0c340d8700 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-3.png differ diff --git a/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/Post.md b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/Post.md new file mode 100644 index 0000000000..b8384ca165 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/Post.md @@ -0,0 +1,693 @@ +Domain events look simple on paper: something happened, react to it. In a real ABP microservices solution, the hard part is not raising the event. The hard part is deciding which event belongs inside the service, which one should cross service boundaries, and how to publish it without losing data or coupling your modules into a distributed monolith. + +ABP gives you the primitives to do this well: local events, distributed events, aggregate-root support, and built-in outbox/inbox infrastructure. Used correctly, they let you keep your domain model clean while still coordinating work across microservices. + +This article walks through a practical way to implement domain events in ABP microservices, including the boundary between domain and integration events, the transactional flow, outbox/inbox configuration, and the pitfalls that usually show up after the first production incident. + +## Start with the right event boundary + +The most important design choice is this: + +- **Domain events** are internal to a bounded context. +- **Integration events** are for other microservices. + +These are not interchangeable, even if the payload looks similar. + +### Domain events + +A domain event represents something meaningful that happened inside your domain model. + +Examples: + +- `OrderPlacedDomainEvent` +- `PaymentCapturedDomainEvent` +- `ProductStockDecreasedDomainEvent` + +These events are typically handled **in-process**. In ABP, that usually means the **local event bus** or ABP's domain event dispatching from aggregates tracked by the ORM. + +Use domain events when you want to: + +- trigger side effects inside the same microservice +- keep aggregate logic focused +- avoid bloated application services +- coordinate rules across domain services without hard references + +### Integration events + +An integration event is a contract for communication between microservices. + +Examples: + +- `OrderPlacedEto` +- `StockCountChangedEto` +- `CustomerDeletedEto` + +In ABP, these go through the **distributed event bus**. With a real provider like RabbitMQ, Kafka, or Azure Service Bus, they leave the current process and get consumed elsewhere. + +Use integration events when you want to: + +- notify another microservice +- update a local projection in another service +- drive eventual consistency across bounded contexts + +### The rule that keeps systems healthy + +A good practical rule is: + +1. Raise a **domain event** from the aggregate or domain layer. +2. Handle it inside the same service. +3. From that handler, publish a **distributed event** if another microservice needs to know. + +That separation prevents leaking internal domain details into your external contracts. + + + +![Generated illustration](inline-1.png) + +## What ABP gives you out of the box + +ABP already supports the eventing model most microservices need. + +### Local event bus + +The local event bus is in-process. It is appropriate for: + +- domain events +- module-to-module communication inside the same app +- internal side effects that should not leave the service boundary + +### Distributed event bus + +The distributed event bus is for cross-process communication. + +A few practical notes matter here: + +- Without a real provider configured, it behaves effectively in-process. +- With RabbitMQ, Kafka, or another provider, it becomes actual inter-service messaging. +- It works best with **ETOs** instead of domain entities. + +### Aggregate roots and generated events + +ABP aggregate roots can generate events directly. In practice, if your entity inherits from `AggregateRoot`, you can use methods like: + +- `AddDomainEvent(...)` +- `AddDistributedEvent(...)` + +ABP collects these events and dispatches them during persistence, typically around `SaveChanges` in EF Core-based applications. + +That means your aggregate can say, "this happened," without knowing who will react. + +## A practical implementation flow + +Let's use a simple example: an Ordering microservice places an order, and an Inventory microservice needs to update its local stock view. + +### Step 1: Raise a domain event in the aggregate + +The aggregate should express business meaning, not infrastructure concerns. + +```csharp +public class Order : AggregateRoot +{ + public OrderStatus Status { get; private set; } + public Guid CustomerId { get; private set; } + + public void Place() + { + if (Status != OrderStatus.Draft) + { + throw new BusinessException("Order is not in draft state."); + } + + Status = OrderStatus.Placed; + + AddDomainEvent(new OrderPlacedDomainEvent(Id, CustomerId)); + } +} + +public record OrderPlacedDomainEvent(Guid OrderId, Guid CustomerId); +``` + +This is internal and business-oriented. It says nothing about RabbitMQ, contracts, queues, or other services. + +### Step 2: Handle the domain event inside the same microservice + +Now handle that event in-process. + +Typical responsibilities here: + +- update other local models +- start internal workflows +- publish an integration event for external consumers + +```csharp +public class OrderPlacedDomainEventHandler : + ILocalEventHandler, + ITransientDependency +{ + private readonly IDistributedEventBus _distributedEventBus; + + public OrderPlacedDomainEventHandler(IDistributedEventBus distributedEventBus) + { + _distributedEventBus = distributedEventBus; + } + + public async Task HandleEventAsync(OrderPlacedDomainEvent eventData) + { + await _distributedEventBus.PublishAsync( + new OrderPlacedEto + { + OrderId = eventData.OrderId, + CustomerId = eventData.CustomerId + } + ); + } +} +``` + +This is where the boundary is enforced: + +- domain event in +- integration event out + +### Step 3: Define a lean ETO + +Your Event Transfer Object should be serializable and intentionally small. + +```csharp +public class OrderPlacedEto +{ + public Guid OrderId { get; set; } + public Guid CustomerId { get; set; } +} +``` + +A few ABP-friendly rules for ETOs: + +- keep only the properties consumers actually need +- avoid navigation properties +- avoid circular references +- avoid polymorphic object graphs unless you really control serialization end to end +- prefer public setters or structures that deserialize cleanly + +Do not publish your aggregate itself. That creates versioning and serialization problems fast. + +### Step 4: Consume the distributed event in another microservice + +In the Inventory microservice, handle the integration event through the distributed event bus. + +```csharp +public class OrderPlacedHandler : + IDistributedEventHandler, + ITransientDependency +{ + private readonly IInventorySyncService _inventorySyncService; + + public OrderPlacedHandler(IInventorySyncService inventorySyncService) + { + _inventorySyncService = inventorySyncService; + } + + [UnitOfWork] + public virtual async Task HandleEventAsync(OrderPlacedEto eventData) + { + await _inventorySyncService.HandleOrderPlacedAsync( + eventData.OrderId, + eventData.CustomerId + ); + } +} +``` + +The `UnitOfWork` attribute is important when the handler writes to the local database. + +## Domain events vs distributed events in ABP + +A lot of design mistakes come from treating these as the same thing. They are not. + +### Domain events + +Characteristics: + +- in-process +- internal to one bounded context +- part of domain modeling +- can trigger multiple internal handlers +- often dispatched during the same persistence flow + +Typical example: + +- an `Order` was placed, so calculate loyalty points internally + +### Distributed events + +Characteristics: + +- cross-process +- integration contract between services +- serialized and brokered +- eventually consistent by nature +- must tolerate retries, duplication, and delayed delivery + +Typical example: + +- Ordering tells Inventory that an order was placed + +### The key difference in failure behavior + +If a local domain event handler fails, that failure is usually part of the current application's execution path. + +If a distributed event consumer fails in another microservice, the original transaction is already committed. You are now in the world of retries, poison messages, compensation, and idempotency. + +That is why integration events need a different level of discipline. + + + +![Generated illustration](inline-2.png) + +## Using AddDistributedEvent directly on aggregates + +ABP also allows aggregates and domain services to add distributed events directly. + +```csharp +public class Product : AggregateRoot +{ + public int StockCount { get; private set; } + + public void ChangeStock(int newCount) + { + StockCount = newCount; + + AddDistributedEvent(new StockCountChangedEto + { + ProductId = Id, + NewCount = newCount + }); + } +} + +public class StockCountChangedEto +{ + public Guid ProductId { get; set; } + public int NewCount { get; set; } +} +``` + +This is convenient, and ABP supports it well. + +Still, I would use it selectively. + +### When it works well + +- the event contract is stable +- the aggregate genuinely owns the integration signal +- the payload is simple +- the team is disciplined about not leaking internal state + +### When to be careful + +- the integration event may change independently from domain behavior +- multiple external contracts may be derived from one domain event +- you want the domain layer isolated from integration messaging concerns + +In larger systems, the domain-event-then-integration-event pattern usually ages better. + + + +![Generated illustration](inline-3.png) + +## The outbox pattern: the part that saves you in production + +Without outbox, the classic failure is simple: + +1. Save business data to the database. +2. Try publishing to the broker. +3. App crashes between the two. +4. Your data is committed, but the event is gone. + +Now one microservice thinks the operation happened, and the others never hear about it. + +Outbox exists to remove that gap. + +### How outbox works in ABP + +With outbox enabled: + +1. Your business data is saved. +2. The outgoing distributed event is also stored in the same database transaction. +3. A background worker reads pending outbox records. +4. It publishes them to the message broker. +5. Published records are marked processed and later cleaned up. + +That gives you transactional safety between your local state change and the fact that an event must be published. + +### EF Core outbox configuration + +Your DbContext needs to participate in event outbox support. + +```csharp +public class OrderingDbContext : AbpDbContext, IHasEventOutbox +{ + public DbSet OutgoingEvents { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureEventOutbox(); + } +} +``` + +Then configure the outbox: + +```csharp +Configure(options => +{ + options.Outboxes.Configure(config => + { + config.UseDbContext(); + config.Selector = type => true; + }); +}); +``` + +The selector lets you choose which events go through that outbox. This becomes useful when a solution has multiple modules or database contexts. + +### Why selectors matter + +In modular ABP solutions, not every event should use every outbox. + +Selectors help you: + +- route specific event types through a specific context +- separate concerns between modules +- avoid a single shared event persistence strategy for everything + +That flexibility matters more as the solution grows. + +## The inbox pattern: the consumer-side safety net + +Outbox protects publishing. Inbox protects consumption. + +Without inbox, a consumer can receive an event and fail mid-processing, leaving you unsure whether the local change happened, whether to retry, or whether the event was already partially applied. + +### How inbox works in ABP + +With inbox enabled: + +1. The incoming event is persisted first. +2. ABP processes it in a transactional scope. +3. Processed records are tracked. +4. Duplicate deliveries can be detected and ignored safely. + +This gives you practical idempotency support and much better operational behavior. + +### EF Core inbox configuration + +Your consumer DbContext participates similarly. + +```csharp +public class InventoryDbContext : AbpDbContext, IHasEventInbox +{ + public DbSet IncomingEvents { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureEventInbox(); + } +} +``` + +Then wire it up: + +```csharp +Configure(options => +{ + options.Inboxes.Configure(config => + { + config.UseDbContext(); + config.EventSelector = type => true; + config.HandlerSelector = type => true; + }); +}); +``` + +### Important operational trade-off + +Inbox and outbox improve reliability, but they add: + +- extra tables/collections +- polling and background processing +- a little more latency +- more database activity + +That trade-off is usually worth it for microservices. It is often unnecessary for a simple monolith. + +## Pre-defined entity distributed events + +ABP can automatically publish distributed entity lifecycle events. + +Common built-in types include: + +- `EntityCreatedEto` +- `EntityUpdatedEto` +- `EntityDeletedEto` + +These are useful when another service needs basic CRUD-oriented synchronization rather than a rich business workflow event. + +### Enabling auto entity events + +```csharp +Configure(options => +{ + options.AutoEventSelectors.Add(); + options.EtoMappings.Add(); +}); +``` + +And the mapped ETO: + +```csharp +public class ProductEto +{ + public Guid Id { get; set; } + public string Name { get; set; } + public int StockCount { get; set; } +} +``` + +### When this is a good fit + +- reference data synchronization +- local read model updates in another service +- straightforward create/update/delete propagation + +### When not to use it + +- when business meaning matters more than CRUD state +- when consumers should react to a specific business action, not a generic update +- when publishing all entity changes leaks too much internal behavior + +A `ProductUpdated` technical event is not the same as a meaningful `StockCountChanged` business event. + +## Entity synchronizer for local copies of remote data + +One common microservice pattern is keeping a local copy of remote entities for querying or validation. + +For example: + +- Catalog owns `Product` +- Ordering keeps a local product snapshot for order creation rules + +ABP's entity synchronizer support helps consume create/update/delete events and persist local copies. This is useful for eventual consistency scenarios where each service needs its own storage and query model. + +This pattern works well when: + +- read performance matters +- cross-service synchronous calls would be too chatty +- temporary staleness is acceptable + +It works poorly when: + +- the downstream service requires strict immediate consistency +- the data changes constantly and synchronization cost gets high +- teams assume replicated data is always current + +## Event naming and contract design + +ABP uses the event type's full class name by default unless you specify an event name explicitly. + +That default is convenient, but contracts deserve some care. + +### Good contract design principles + +- keep ETOs small +- include identifiers and values the consumer truly needs +- avoid domain behavior and private invariants in the payload +- design for versioning from day one +- prefer additive changes over breaking changes + +### A bad ETO usually looks like this + +- dozens of properties copied from the aggregate +- nested child collections that consumers barely use +- serialization-unfriendly types +- assumptions that all consumers share the same domain model + +### A better ETO usually looks like this + +- stable identifiers +- a small number of primitive fields +- explicit timestamps or version fields if useful +- business meaning that survives service evolution + +## Real-world pattern: publish from a domain handler, not the application service + +Many examples online publish distributed events directly from app services after repository calls. That works, but it tends to make orchestration logic pile up in the application layer. + +A cleaner ABP approach is often: + +- aggregate raises domain event +- local handler reacts +- local handler publishes distributed event + +Why this usually scales better: + +- the aggregate stays expressive +- the app service stays thin +- internal reactions remain composable +- multiple handlers can subscribe without changing the original use case + +It also makes testing easier because the business event becomes the seam. + +## Failure modes you should design for + +If you are using distributed events, assume these will happen eventually: + +- duplicate message delivery +- delayed delivery +- consumer failure after partial processing +- contract evolution across independently deployed services +- producer publishes faster than consumers can handle + +### Practical defenses + +- enable outbox on producers +- enable inbox on consumers +- make handlers idempotent +- keep events small and versionable +- avoid side effects that cannot be retried safely +- use compensating actions for multi-service workflows + +A distributed event is not a database transaction stretched across services. Treat it as asynchronous coordination. + +## When to use / When NOT to use + +### Use domain events in ABP when + +- you want to decouple internal side effects +- multiple parts of the same microservice should react to a business action +- your aggregate should express business intent without knowing infrastructure details +- you want cleaner application services + +### Do not use domain events when + +- a plain method call inside the same class is clearer +- the logic is not really event-driven and has only one obvious synchronous step +- the event abstraction makes the code harder to understand than the original flow + +### Use distributed events when + +- another microservice needs to react asynchronously +- eventual consistency is acceptable +- you want to avoid synchronous runtime coupling between services +- local replicas or read models must stay updated + +### Do not use distributed events when + +- the consumer requires immediate consistency before the current request can finish +- the workflow cannot tolerate asynchronous delays +- you have not planned for retries, idempotency, and failure handling +- you are using microservices in name only and everything still depends on lockstep behavior + +## A reference implementation shape + +In a typical ABP microservice, the structure often looks like this: + +### In the domain layer + +- aggregates call `AddDomainEvent(...)` +- optionally aggregates call `AddDistributedEvent(...)` for very stable external contracts +- domain logic stays free from broker-specific code + +### In the application or domain event handling layer + +- implement `ILocalEventHandler` +- translate domain events into integration ETOs +- publish using `IDistributedEventBus` + +### In infrastructure + +- configure RabbitMQ or another provider +- configure outbox/inbox on the relevant DbContexts +- tune event box options for polling, batching, cleanup + +### In consuming microservices + +- implement `IDistributedEventHandler` +- wrap data updates in a unit of work +- make processing idempotent + +That division keeps the model understandable and avoids most of the coupling problems teams introduce accidentally. + +## Common mistakes in ABP event-driven microservices + +### 1. Publishing entities instead of contracts + +This leaks internals and breaks consumers when your domain evolves. + +### 2. Treating domain events as public integration events + +Internal events and external contracts change at different speeds. Keep them separate. + +### 3. Skipping outbox in production + +It works until the day you hit the save-then-crash gap. + +### 4. Forgetting idempotency on consumers + +Brokers and retries do not guarantee single delivery in the way many teams assume. + +### 5. Emitting generic CRUD events for business workflows + +A business process usually deserves a business event, not just `EntityUpdated`. + +### 6. Putting too much data in ETOs + +Large event contracts create versioning pain, serialization issues, and unnecessary coupling. + +## Final recommendations + +If you are implementing domain events in ABP microservices, optimize for clear boundaries first and infrastructure reliability second. + +The pattern that works well in most real systems is: + +- raise domain events inside aggregates +- handle them locally +- publish explicit integration events for other services +- protect publishing with outbox +- protect consumption with inbox + +ABP already gives you the building blocks. The main challenge is not framework support. It is resisting the temptation to blur domain events, application events, and integration contracts into one catch-all mechanism. + +If you keep those boundaries sharp, your services remain easier to evolve, test, and operate. + +## TL;DR + +- In ABP, use domain events for in-process reactions inside one microservice and distributed events for cross-service communication. +- Prefer raising domain events from aggregates, then translating them into lean ETOs in local handlers. +- Enable outbox on producers and inbox on consumers to avoid lost events and improve idempotency. +- Use built-in entity events for synchronization scenarios, but prefer business events when workflow meaning matters. +- Keep integration contracts small, serializable, stable, and separate from your domain model. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/cover.png b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/cover.png new file mode 100644 index 0000000000..d7aa34a813 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/cover.png differ diff --git a/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-1.png b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-1.png new file mode 100644 index 0000000000..dff7d67d5c Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-1.png differ diff --git a/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-2.png b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-2.png new file mode 100644 index 0000000000..5574439be2 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-2.png differ diff --git a/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-3.png b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-3.png new file mode 100644 index 0000000000..0d35b894ae Binary files /dev/null and b/docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-3.png differ diff --git a/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/POST.md b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/POST.md new file mode 100644 index 0000000000..4a97b9d7cb --- /dev/null +++ b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/POST.md @@ -0,0 +1,419 @@ +# Working with Dapr Workflows in the ABP Framework + +Most real business processes don't finish in a single request. + +An order gets placed, inventory gets checked, a payment gets charged, and the customer gets notified. Each step can fail, time out, or need a retry. And the whole thing has to survive a process restart without losing its place or charging someone twice. + +We usually solve this with a pile of queues, a state table, and a lot of defensive code to track where each process is. It works, but the business logic ends up scattered across handlers and database rows, and nobody can read the flow top to bottom anymore. + +[I covered **Elsa** in two earlier articles](https://abp.io/community/search?tag=elsa) as one way to handle workflows in ABP. **Dapr Workflow** takes a different path: instead of an in-app engine, the workflow engine runs in the [**Dapr sidecar**](https://docs.dapr.io/concepts/dapr-services/sidecar/), and you write the process as ordinary C# code that Dapr makes durable. If the host crashes halfway through, the workflow picks up right where it left off. + +In this article, we'll build a small Dapr Workflow inside a fresh ABP project and run it end to end. By the time you reach the bottom, you should be able to copy the code, run it, and watch a workflow march through its steps. + +> **Note:** Versions matter here, because both ABP and Dapr move fast. This article is written in June 2026 against **ABP 10.4** (.NET 10), **Dapr 1.18**, and the **`Dapr.Workflow` 1.18.x** package. The `Dapr.Workflow` package was rewritten in Dapr 1.17, so older tutorials you find online may use a different API. + +## What Dapr Workflow Actually Is? + +You define a [**workflow**](https://docs.dapr.io/developing-applications/building-blocks/workflow/) that orchestrates a process, and [**activities**](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/#workflows-and-activities) that do the actual work (call a database, hit an API, send an email). + +> **This is orchestration rather than choreography:** one place drives the process, instead of services reacting to each other's events. The definitions live in your app, but the engine that executes them runs in the Dapr sidecar next to it. + +![Dapr workflow execution architecture diagram](./mermaid1.png) + +The key idea is **durable execution**. Dapr records every step to a state store, so the workflow can be replayed from history at any time. A crash, a deployment, or a scale-out event doesn't lose progress, and a workflow can run for seconds or for months. + +> ⚠️ One rule follows from this: **workflow code must be deterministic**. No `DateTime.Now`, no random values, no direct I/O. Anything non-deterministic goes into an activity. Even logging is affected, so inside a workflow you use `context.CreateReplaySafeLogger()` instead of a normal logger, otherwise every replay repeats your log lines. + +Under the hood, this all runs on [**Dapr actors**](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/), which is why the state store has to support actors. The good news is that the default local setup already handles this, as you'll see in a moment. + +--- + +## A Quick Note on ABP and Dapr + +ABP already ships a set of Dapr integration packages: `Volo.Abp.Dapr` (the core package), `Volo.Abp.EventBus.Dapr` and `Volo.Abp.AspNetCore.Mvc.Dapr.EventBus` (distributed event bus over Dapr pub/sub), `Volo.Abp.Http.Client.Dapr` (service invocation), and `Volo.Abp.DistributedLocking.Dapr` (distributed locking). You can read all about them in the [ABP Dapr integration documentation](https://abp.io/docs/latest/framework/dapr). + +These cover pub/sub, service-to-service calls, and locking. **Workflows are not part of ABP's Dapr integration**, and that's fine. Dapr Workflow has its own first-class .NET SDK (`Dapr.Workflow`), and you plug it straight into your ABP app like any other .NET library. So in this article we use the Dapr SDK directly, inside an ABP startup template. + +> **Note:** If you'd like to see deeper Dapr integration in ABP, or you'd like us to build a dedicated piece around Dapr Workflow, feel free to open a new issue on the [ABP GitHub repository](https://github.com/abpframework/abp/issues). Telling us what you need is the best way to help us prioritize it. + +--- + +## What We'll Build + +To keep this concrete, we'll build a small **order processing** workflow, the classic example for this kind of thing. + +The workflow takes an order, checks inventory, charges the customer, then notifies them. If the item is out of stock, it stops early and returns a rejected result. Nothing fancy on the business side, but it's enough to show the parts that matter: how a workflow chains activities, how state survives across steps, and how you start and track an instance. + +Here's the flow we're aiming for: + +- An order comes in with a product, a quantity, and a price +- **Check inventory**: if there isn't enough stock, reject the order and stop +- **Process payment**: charge the customer +- **Notify the customer**: let them know the order went through +- Return a final result + +Each of those steps will be an **activity**, and the workflow is the code that orchestrates them. Let's set up the project and build it. + +## Prerequisites + +Before we start, make sure you have these installed: + +- **.NET 10 SDK** +- **ABP CLI** (the current Studio CLI). Install it with `dotnet tool install -g Volo.Abp.Studio.Cli` (or update with `dotnet tool update -g Volo.Abp.Studio.Cli`) +- **Docker**, running on your machine +- [**Dapr CLI**, initialized once with `dapr init`](https://docs.dapr.io/getting-started/) + +That last step matters. When you run `dapr init` in self-hosted mode, Dapr pulls a few containers (including Redis) and writes a default `statestore.yaml` component. That default state store already has `actorStateStore: "true"` set, which is exactly what Dapr Workflow needs. So once `dapr init` finishes, you can run workflows locally with zero extra configuration. + +![dapr-init-run-result](./dapr-init-run-result.png) + +> **Pro Tip:** If you ever swap the default Redis store for your own component, double-check that it sets `actorStateStore: "true"`. Without it, workflows silently fail to start, and it's the line people forget most often. + +## Create the Project + +In this article I'll create a new layered solution with **EF Core** as the database provider, using the ABP CLI. + +> If you already have an ABP project, you don't need a new one. You can apply the following steps to your existing solution and skip this section. + +Create a new solution named `DaprWorkflowDemo` (or whatever you want): + +```bash +abp new DaprWorkflowDemo +``` + +Once the download finishes, your project boilerplate is ready. Open the solution in your IDE and run the `DaprWorkflowDemo.Web` project once to confirm the app starts and the UI works. + +> Since, we have created the solution via ABP Studio CLI, it automatically runs the initial-tasks, which init database, seed initial data and run `abp install-libs` command, so, no need run the **DbMigrator* project. + +> Default admin username is **admin** and the password is **1q2w3E***. You can use these credentials to login... + +We'll do all the workflow work inside the `DaprWorkflowDemo.Web` project, since that's the running host where the workflow engine connects to the sidecar. + +## Install the Dapr.Workflow Package + +Open a terminal in the `DaprWorkflowDemo.Web` project folder and add the package: + +```bash +dotnet add package Dapr.Workflow +``` + +-> **This single package gives you everything:** the base `Workflow` and `WorkflowActivity` types, the `AddDaprWorkflow` registration helper, and the `DaprWorkflowClient` you use to start and query workflows from code. + +## Define the Workflow and Its Activities + +Now let's write the order processing flow we sketched out earlier. + +First, create a `Workflows` folder in the `DaprWorkflowDemo.Web` project. We'll keep everything there for simplicity. + +Every input and output in a workflow gets serialized to the state store, so the types you pass around should be simple, JSON-friendly records (**_ensure they are serializable!_**). Let's define them: + +```csharp +namespace DaprWorkflowDemo.Web.Workflows; + +public record OrderPayload(string OrderId, string ProductName, int Quantity, decimal TotalPrice); + +public record InventoryResult(bool InStock); + +public record OrderResult(string OrderId, string Status); +``` + +Now the workflow itself. A workflow derives from `Workflow` and reads top to bottom like a normal method, even though every step is durably persisted: + +```csharp +using Dapr.Workflow; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace DaprWorkflowDemo.Web.Workflows; + +public class OrderProcessingWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + var logger = context.CreateReplaySafeLogger(); + logger.LogInformation("Starting order {OrderId}: {Quantity} x {ProductName}", + order.OrderId, order.Quantity, order.ProductName); + + // 1. Check inventory + var inventory = await context.CallActivityAsync( + nameof(CheckInventoryActivity), order); + + if (!inventory.InStock) + { + logger.LogWarning("Order {OrderId} rejected: out of stock", order.OrderId); + return new OrderResult(order.OrderId, "Rejected: out of stock"); + } + + // 2. Process the payment + await context.CallActivityAsync(nameof(ProcessPaymentActivity), order); + + // 3. Notify the customer + await context.CallActivityAsync(nameof(NotifyCustomerActivity), order); + + logger.LogInformation("Order {OrderId} completed", order.OrderId); + return new OrderResult(order.OrderId, "Completed"); + } +} +``` + +A couple of things worth pointing out here. + +- `CallActivityAsync` does not invoke the activity directly. It schedules the work with the workflow engine, which records the result once the activity completes. If the process dies right after the payment step, Dapr replays the workflow, feeds it the already-recorded results for the completed steps, and resumes at the notification step. The customer never gets charged twice. This is the **task chaining** pattern. +- Notice the replay-safe logger too. Because the engine replays the workflow to rebuild its state, a normal logger would print the same lines over and over. `context.CreateReplaySafeLogger()` logs only on the first real pass. +- Now the activities. An activity is where the real work happens, and the only place you're allowed to be non-deterministic. It derives from `WorkflowActivity` and supports constructor injection, so you can pull in your ABP services, repositories, or any registered dependency: + +```csharp +using Dapr.Workflow; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; + +namespace DaprWorkflowDemo.Web.Workflows; + +public class CheckInventoryActivity : WorkflowActivity +{ + private readonly ILogger _logger; + + public CheckInventoryActivity(ILogger logger) + { + _logger = logger; + } + + public override Task RunAsync(WorkflowActivityContext context, OrderPayload order) + { + _logger.LogInformation("Checking inventory for {ProductName}", order.ProductName); + + // Pretend we queried a stock service or a repository here. + var inStock = order.Quantity <= 100; + + return Task.FromResult(new InventoryResult(inStock)); + } +} + +public class ProcessPaymentActivity : WorkflowActivity +{ + private readonly ILogger _logger; + + public ProcessPaymentActivity(ILogger logger) + { + _logger = logger; + } + + public override Task RunAsync(WorkflowActivityContext context, OrderPayload order) + { + _logger.LogInformation("Charging {TotalPrice:C} for order {OrderId}", + order.TotalPrice, order.OrderId); + + // Call your real payment provider here. + return Task.FromResult(null); + } +} + +public class NotifyCustomerActivity : WorkflowActivity +{ + private readonly ILogger _logger; + + public NotifyCustomerActivity(ILogger logger) + { + _logger = logger; + } + + public override Task RunAsync(WorkflowActivityContext context, OrderPayload order) + { + _logger.LogInformation("Notifying customer about order {OrderId}", order.OrderId); + + // Send an email, push a notification, publish an event, etc. + return Task.FromResult(null); + } +} +``` + +Each activity is isolated, so Dapr can retry a failed one without re-running the whole workflow. The two activities that don't return anything useful use `object?` as their output type and return `null`. That's why the workflow calls them with the non-generic `CallActivityAsync`, which ignores the result. + +Here's the shape of the process we just wrote: + +![Order processing workflow flowchart](./mermaid2.png) + +## Register the Workflow + +Workflows and activities need to be registered so the engine knows about them. Open your `DaprWorkflowDemoWebModule` class and register them in `ConfigureServices`. Most of the existing code is abbreviated for simplicity: + +```csharp +using DaprWorkflowDemo.Web.Workflows; +using Dapr.Workflow; + +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + // ... existing ABP configuration ... + + //Configure Dapr Workflows... + context.Services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + }); +} +``` + +> `AddDaprWorkflow` does two things for us. It registers a background worker that connects to the sidecar's workflow engine and hosts your workflow definitions, and it registers a `DaprWorkflowClient` in the dependency injection container so you can start and query workflows from your own code later. + +That's all the wiring. There's no component YAML to write, because Dapr ships a built-in workflow component named `dapr` that runs on top of the actor state store we already have. + +## Run It With the Dapr Sidecar + +Here's the part that's different from a normal `dotnet run`. The workflow engine lives in the Dapr sidecar, so the app has to run **alongside** a sidecar. The Dapr CLI handles that for us. + +> In this section, I assume that you already run `dapr init` command before, as explained above. If you haven't run it yet, please first run it and then follow the instructions/commands below. + +First, make sure your database is migrated (run `DaprWorkflowDemo.DbMigrator` if you haven't). Then, from the `DaprWorkflowDemo.Web` project folder, start the app with Dapr: + +```bash +dapr run --app-id dapr-workflow-demo --dapr-http-port 3500 -- dotnet run +``` + +A few notes on this command: + +- `--app-id` is the identity of your app within Dapr. We'll use it nowhere else in this example, but Dapr needs it. +- `--dapr-http-port 3500` pins the sidecar's HTTP port so we know where to send requests. You can leave it out and let Dapr pick one, but pinning it keeps the next step simple. +- Everything after `--` is the command Dapr runs for your app. `dapr run` injects the sidecar's connection details (like the gRPC port) as environment variables, and the `Dapr.Workflow` worker reads them automatically to connect to the engine. + +Notice we don't pass `--app-port` here. That flag is only needed when Dapr has to call **into** your app (for pub/sub or service invocation). For workflows, your app connects **out** to the sidecar over gRPC, so we don't need it for this scenario. + +Once it's running, you'll see both the ABP app logs and the Dapr sidecar logs in the same terminal. + +## Does It Actually Work? + +The quickest way to test is to talk to the sidecar's **Workflow management HTTP API** directly. This hits Dapr, not your app, which makes it a clean smoke test with no extra endpoint code. + +Start a workflow instance. The component name is `dapr` (the built-in one), the workflow name is the class name, and we pass our own instance ID so it's easy to query: + +```bash +curl -i -X POST "http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=order-001" \ + -H "Content-Type: application/json" \ + -d '{"OrderId":"order-001","ProductName":"Mechanical Keyboard","Quantity":2,"TotalPrice":59.90}' +``` + +The request body is the workflow input, and Dapr passes it straight through to your `OrderPayload`. You should get a `202 Accepted` back with the instance ID: + +```json +{ "instanceID": "order-001" } +``` + +Now query the status of that instance: + +```bash +curl "http://localhost:3500/v1.0/workflows/dapr/order-001" +``` + +After the workflow finishes, you'll see a `COMPLETED` status along with the serialized output: + +```json +{ + "instanceID": "order-001", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2026-06-29T15:30:15.038490Z", + "lastUpdatedAt": "2026-06-29T15:30:15.360885500Z", + "runtimeStatus": "COMPLETED", + "properties": { + "dapr.workflow.input": "{\"ProductName\":\"Mechanical Keyboard\",\"Quantity\":2,\"OrderId\":\"order-001\",\"TotalPrice\":59.9}", + "dapr.workflow.output": "{\"orderId\":\"order-001\",\"status\":\"Completed\"}" + } +} +``` + +If you check the terminal, you'll also see the log lines from the workflow and each activity in order: + +![dapr-workflow-response](./dapr-workflow-response.png) + +The same management API also lets you `terminate`, `pause`, `resume`, and `purge` instances, and `raiseEvent` to send external events into a waiting workflow. For example: + +```bash +# Permanently delete a finished workflow's state +curl -X POST "http://localhost:3500/v1.0/workflows/dapr/order-001/purge" +``` + +The `DaprWorkflowClient` exposes the same operations in code (terminating, suspending and resuming, purging, and raising external events on an instance), which is the way to go for anything beyond a quick manual test. + +## Triggering Workflows From Your ABP Code + +Hitting the sidecar API by hand is great for a quick check, but in a real app you'll start workflows from your own code, and this is the recommended path. That's what the `DaprWorkflowClient` is for, and `AddDaprWorkflow` already registered it for you. + +You can inject it anywhere, for example into a controller or an application service. Here's a minimal controller in the `DaprWorkflowDemo.Web` project that starts an order and reads its status: + +```csharp +using System.Threading.Tasks; +using DaprWorkflowDemo.Web.Workflows; +using Dapr.Workflow; +using Microsoft.AspNetCore.Mvc; + +namespace DaprWorkflowDemo.Web.Controllers; + +[ApiController] +[Route("api/orders")] +public class OrderController : ControllerBase +{ + private readonly DaprWorkflowClient _workflowClient; + + public OrderController(DaprWorkflowClient workflowClient) + { + _workflowClient = workflowClient; + } + + [HttpPost] + public async Task StartAsync(OrderPayload order) + { + var instanceId = await _workflowClient.ScheduleNewWorkflowAsync( + name: nameof(OrderProcessingWorkflow), + instanceId: order.OrderId, + input: order); + + return Accepted($"/api/orders/{instanceId}", new { instanceId }); + } + + [HttpGet("{instanceId}")] + public async Task GetStatusAsync(string instanceId) + { + var state = await _workflowClient.GetWorkflowStateAsync(instanceId); + + if (state is null || !state.Exists) + { + return NotFound(); + } + + return Ok(new + { + RuntimeStatus = state.RuntimeStatus.ToString(), + Output = state.ReadOutputAs() + }); + } +} +``` + +`ScheduleNewWorkflowAsync` returns immediately and the workflow runs in the background, so this fits the asynchronous request pattern nicely: return `202 Accepted` and let the client poll the status endpoint. + +> One ABP-specific thing to keep in mind: ABP enforces antiforgery validation for unsafe HTTP methods on cookie-authenticated requests. Server-to-server or `curl` calls without an auth cookie usually pass straight through, but if you call the `POST` endpoint from a logged-in browser session and get a `400` antiforgery error, you can relax the auto validation for this controller through `AbpAntiForgeryOptions`, the same way the Elsa articles did for the Elsa endpoints. + +## Going Further + +We built a simple linear flow, but **Dapr Workflow** supports the patterns you'll actually need in production, all in plain C#: + +- **Fan-out / fan-in**: schedule many activities in parallel and aggregate the results (it's just `Select` plus `Task.WhenAll`). +- **External events**: pause a workflow until a human approves something or another system calls back. This is great for approval flows. +- **Timers**: durably wait for minutes, days, or months without holding a thread. +- **Child workflows**: break a big process into smaller workflows with their own history and status. +- **Retry policies**: give an activity an exponential backoff policy so transient failures recover on their own. + +## Conclusion + +**Dapr Workflow** gives you durable execution for long-running processes without bolting a heavy orchestration engine into your code. The process is plain C# that reads top to bottom, Dapr makes it fault-tolerant by replaying from the state store, and the orchestration stays deterministic while the side effects live in activities. + +The nice part for us is that none of this fights with ABP. You create a normal ABP solution, add the `Dapr.Workflow` package, register your workflows in a module, and run with `dapr run`. ABP's own Dapr packages still cover pub/sub, service invocation, and locking, so you can mix all of these in the same solution when you need them. + +All the code in this article is self-contained, so you can copy it into a fresh ABP project and follow along from top to bottom. + +Thanks for reading, see you in the next one! diff --git a/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/cover-image.png b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/cover-image.png new file mode 100644 index 0000000000..e880a97ab0 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/cover-image.png differ diff --git a/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-init-run-result.png b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-init-run-result.png new file mode 100644 index 0000000000..38ab1848e5 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-init-run-result.png differ diff --git a/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-workflow-response.png b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-workflow-response.png new file mode 100644 index 0000000000..a26143b053 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-workflow-response.png differ diff --git a/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid1.png b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid1.png new file mode 100644 index 0000000000..c73af2bf63 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid1.png differ diff --git a/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid2.png b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid2.png new file mode 100644 index 0000000000..5f108a4842 Binary files /dev/null and b/docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid2.png differ diff --git a/docs/en/Community-Articles/2026-06-29-customizing-the-abp-framework/POST.md b/docs/en/Community-Articles/2026-06-29-customizing-the-abp-framework/POST.md new file mode 100644 index 0000000000..a343173dbf --- /dev/null +++ b/docs/en/Community-Articles/2026-06-29-customizing-the-abp-framework/POST.md @@ -0,0 +1,216 @@ +# Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI + +Enterprise ASP.NET Boilerplate (ABP) projects rarely stay with default theme behavior for long. At some point, teams need stricter brand alignment, user experience (UX) consistency across modules, or product-specific shell behavior that goes beyond palette and typography tweaks. + +This article explains a practical way to customize the LeptonX theme in Angular projects through two primary layers : + +1. **Style Overriding:** Utilizing design tokens, global CSS custom properties (variables), and component-level styling. +2. **Element Overriding:** Replacing or extending UI fragments and layout pieces using ABP's built-in services. + +Finally, we connect this customization mindset to ABP’s new React direction, where application development teams own more of the user interface (UI) implementation directly from day one. + +## Why Overriding Matters in Real ABP Solutions + +In enterprise software engineering, frontend customization is not a cosmetic task. Instead, it directly supports core technical and architectural goals : + +- **Brand System Compliance:** Enforcing strict color palettes, layouts, and typography across tenant-facing portals and internal back-office administration pages. +- **Accessibility (a11y) Improvements:** Optimizing focus states, color contrast ratios, screen reader compatibility, and keyboard navigation to meet WCAG standards. +- **Product Differentiation:** Structuring distinct top-level layouts, sidebar behavior, and navigation elements to separate multiple products within the same suite. +- **Operational Usability:** Reorganizing application spaces to match domain-specific workflows and simplify intensive data-entry tasks. + +To avoid building fragile CSS overrides that break during framework updates, development teams must follow a strict, highly structured hierarchy of customization : + +| Level | Customization Type | Technical Mechanism | Strategic Role | +| :---: | :--- | :--- | :--- | +| **1** | **Token-Level Variables** | CSS Custom Properties | 🛡️ *First Line of Defense* | +| **2** | **Component-Style Patch** | Class-Based Overrides | 🎨 *Moderate Visual Tweaks* | +| **3** | **Element Replacement** | ReplaceableComponents | 🏗️ *Deep Structural Overrides* | + +Adhering to this hierarchy reduces "style debt" and ensures that theme upgrades remain manageable throughout the application lifecycle. + +### Layer 1: Style Overriding in LeptonX (Angular) + +Style overriding is the safest and most maintainable way to alter your application's presentation layer. The LeptonX engine relies heavily on CSS custom properties (variables) defined at the `:root` level. + +### Customizing Brand Colors and Typography Tokens + +To modify the default colors and branding assets, developers can define custom properties within the global `src/styles.scss` file : + +```scss +:root { + /* Set the primary brand color used on active elements, buttons, and focuses */ + --lpx-brand: #1e3a8a; + + /* Set the physical paths for the application logos */ + --lpx-logo: url('/assets/images/logo.png'); + --lpx-logo-icon: url('/assets/images/logo-icon.png'); /* Displayed when sidebar is collapsed */ +} +``` + +For applications utilizing multi-theme layouts (such as LeptonX Pro's Light, Dark, or Dim modes), variables can be scoped under individual theme classes to dynamically swap brand colors or assets : + +```scss +/* Scoping theme-specific logos to prevent visibility issues on dark backgrounds */ +:root.lpx-theme-dark, :root.lpx-theme-dim { + --lpx-logo: url('/assets/images/logo-light.png'); + --lpx-logo-icon: url('/assets/images/logo-icon-light.png'); +} +``` + +#### Solving the "Visual Branding Blink" on Initial Page Load + +A common issue in production occurs when the default LeptonX logo is briefly displayed on screen before the client browser parses the custom stylesheet. This latency creates a noticeable "blink" or flicker. + +To eliminate this rendering gap, bypass the CSS variable load phase by replacing the physical logo assets inside the web host project's public directory. Write your custom branding files directly to `/images/logo/leptonx/logo-light.png` inside the server's public folder. Because the fallback variable defaults directly to this location, the client browser displays the custom logo asset immediately without waiting to parse the custom CSS rules. + +Additionally, note that styles registered solely in the application's global `styles.scss` may fail to apply to the **Account Layout** (such as the standard login page) because it compiles within an isolated module lifecycle. To ensure your styling overrides apply globally, register the assets and styles in the Virtual File System (VFS) of the.NET backend host, making them universally accessible across all client routing contexts. + +### Layer 2: Element Overriding in LeptonX (Angular) + +When CSS modifications cannot support your required user experience (such as adding search interfaces, custom profile controls, or custom action layouts), teams must override the underlying UI elements. + +ABP provides the `ReplaceableComponentsService` to dynamically replace pre-built layout pieces with custom, project-owned Angular components without breaking core module logic. + +### Troubleshooting the Mobile User Profile Freeze + +In compiled editions of the LeptonX Lite layout library (specifically versions 3.1.x through 4.3.1), developers have identified a rendering bug affecting mobile layouts. When a user logs in via a mobile device and taps the profile dropdown menu, the page freezes. Instead of displaying the profile options, the sidebar area recursively renders a duplicate copy of the active route page. This layout loop completely breaks navigation until the page is refreshed. + +The root cause is a layout bug inside the compiled LeptonX library template (`mn-user-profile.component.html`), where the template markup is wrapped inside an `` tag instead of a structurally neutral `` tag. + +To resolve this issue, you can implement a custom component replacement : + +1. Generate a custom mobile profile component using the Angular CLI + + ```bash + ng g component components/my-mobile-profile + ``` + +2. Implement the component template, ensuring the wrapper elements utilize `` instead of ``. +3. Inject the `ReplaceableComponentsService` into your root `app.component.ts` to swap the underlying component keys during application bootstrap : + + ```tsx + import { Component, OnInit } from '@angular/core'; + import { ReplaceableComponentsService } from '@abp/ng.core'; + import { eThemeLeptonXComponents } from '@volosoft/ngx-lepton-x'; + import { MyMobileUserProfileComponent } from './components/my-mobile-profile.component'; + + @Component({ + selector: 'app-root', + template: '' + }) + export class AppComponent implements OnInit{ + private replaceableComponents = inject(ReplaceableComponentsService); + + ngOnInit() { + this.replaceableComponents.add({ + component: MyMobileUserProfileComponent, + key: eThemeLeptonXComponents.MobileUserProfile + }); + } + } + ``` + + +### Template Context: From LeptonX Demo Setup to Real ABP Application Templates + +When transitioning customized designs from local prototypes to production environments, development teams must choose between two operating modes : + +| **Operational Mode** | **Core Architecture** | **Rationale & Trade-offs** | +| --- | --- | --- | +| **Standard Template Mode** | Consumes LeptonX packages as standard dependencies (`@abp/ng.theme.lepton-x`) from npm registries. All overrides are applied at the application layer. | **Highly Recommended.** Keeps local project codebases clean, simplifies dependency updates, and avoids style debt. | +| **Source-Inspection Mode** | Utilizes the ABP CLI `get-source` command to download the raw theme code and configure temporary local path aliases. | **Diagnostic Only.** Best used for deep debugging, prototyping layout behaviors, or tracing framework-level bugs. | + +### Resolving Strict MIME Type CSS Loading Exceptions + +During local development or initial production deployments of LeptonX Lite Angular applications, browsers may refuse to apply the theme's styles. This issue manifests as a console exception: + +`Refused to apply style from 'http://localhost:4200/bootstrap-dim.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.` + +This error occurs when the browser requests static layout stylesheets from paths that do not exist, causing the back-end host to return a default 404 HTML fallback page. To resolve this, run the installation command in your client-side workspace : + +```bash +abp install-libs +``` + +This command forces the ABP CLI to parse package dependencies, copy the compiled stylesheets directly into the physical output directories, and make them available to the web server. + +### Deep Implementation: Integrating Theme Source Code and the Upgrade Trade-Off + +For complex enterprise scenarios requiring structural changes that cannot be achieved via standard token configurations or component replacements, developers have the option to bypass compiled packages entirely and integrate the theme’s raw source code. + +### How to Retrieve the Source Code + +ABP Commercial customers have full access to the complete source code of the LeptonX Pro theme. This can be downloaded directly through the ABP Suite user interface or by executing the following command in the ABP CLI within your project directory : + +``` +abp get-source Volo.Abp.LeptonXTheme +``` + +This command downloads the raw C# and Angular source files directly into your local solution structure. Once downloaded, you can modify the underlying HTML templates, restructure Angular modules, and alter core layout scripts to meet your product requirements. + +#### The Upgrade Warning: Maintenance Overhead and Style Debt + +While direct access to the source code provides complete design freedom, it comes with a major warning regarding long-term maintenance : + +- **Bypassing the Update Stream:** Once you replace official package references (such as `@volosoft/abp.ng.theme.lepton-x` or NuGet packages) with local project references, your application is disconnected from the automatic update pipeline. +- **Manual Merge Burden:** When Volosoft releases framework updates, security patches, or compatibility fixes (such as aligning with newer Angular or.NET compiler baselines), these updates will not automatically apply to your customized code. Your team must manually compare, diff, and merge upstream changes, which can introduce regressions and increase technical debt. +- **VFS and APIs as the First Line of Defense:** Before choosing a full source code integration, try using the Virtual File System (VFS) on the backend or standard component replacement APIs in the frontend to override only the specific elements you need to change. This allows you to customize the UI while keeping the rest of your theme packages fully upgradeable. + +### Connecting the Mindset to ABP’s New React Era + +The introduction of the React UI option in ABP 10.4 represents a major architectural shift. While the Angular implementation relies on structured layout packages and runtime component overrides, the React architecture prioritizes **direct developer ownership** of the presentation layer. + +```mermaid +graph TD + %% Styling + classDef react fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#0d47a1; + classDef dotnet fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#4a148c; + classDef proxy fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#e65100; + classDef tool fill:#f5f5f5,stroke:#757575,color:#333; + + %% React App Box + subgraph ReactApp ["React App Repository"] + C1["Custom Business Components
(Local Source Code)"]:::react + C2["TanStack Router & Query
(Type-Safe Client Routes)"]:::react + + T1["Vite Dev Server & Bundling
(Fast HMR, Vitest)"]:::tool + T2["Tailwind CSS / shadcn/ui
(Accessible UI Components)"]:::tool + + C1 --> C2 + T1 --> T2 + end + + %% Backend Box + subgraph NetCore ["ASP.NET Core Web API Host"] + P1["Dynamic API Client Proxies
(Auto-Generated Endpoints)"]:::proxy + A1["ABP Admin Console
(Delivered via NuGet)"]:::dotnet + + P1 <==> A1 + end + + %% Inter-Repository Flow + ReactApp -- "Generates Dynamic Proxies" --> P1 + + %% Layout Tweaks + style ReactApp fill:#fafafa,stroke:#1e88e5,stroke-width:1px,stroke-dasharray: 5 5; + style NetCore fill:#fafafa,stroke:#8e24aa,stroke-width:1px,stroke-dasharray: 5 5; +``` + +### What Stays Consistent vs. What Changes + +Understanding how patterns transfer between frameworks is key for teams migrating to the React UI: + +- **What Stays Consistent:** Core DDD infrastructure, backend integration, dynamic API proxy generation, multi-tenancy models, and permission-aware routing configurations. +- **What Changes:** Direct ownership of page layouts, faster iteration of UI composition, and modern utility-first styling tools. + +### A New Frontend Philosophy + +In the Angular model, developers import pre-built layouts from compiled packages and selectively override elements using classes or replacing components. While structured, this approach can sometimes feel like "fighting" the framework. + +The React UI model, by contrast, gives developers direct control over the UI components from day one. Standard administrative pages (such as Identity, Tenants, and Settings) are managed separately by the **ABP Admin Console** on the back-end host, while all application layouts and views remain locally in your React project. + +Built with modern tools like **Vite**, **Tailwind CSS**, and **shadcn/ui**, developers can customize and extend components directly in their local source files without needing complex overriding wrappers. + +Additionally, because the layout and page templates reside in local source directories rather than compiled packages, this architecture is highly optimized for AI-driven development. Automated coding agents (such as the ABP Studio AI Agent) can easily inspect and modify local layouts, run API proxy generation, and deploy updates quickly. + +Whether your enterprise solution leverages the structured, component-driven architecture of ABP's Angular UI or is stepping into the modern, developer-owned era of the Vite-powered React UI , establishing an intentional, upgrade-safe customization strategy is crucial. By resolving design changes through token-level custom properties first, documenting structural element overrides, and preparing public-facing technical resources to be highly citable by conversational search agents , development teams can insulate their codebases from technical debt. Ultimately, the transition from rigid theme packages to direct frontend ownership not only streamlines day-to-day software delivery but also ensures that your application framework remains flexible, performant, and visible in an AI-driven ecosystem. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-06-30-state-management-for-angular/POST.md b/docs/en/Community-Articles/2026-06-30-state-management-for-angular/POST.md new file mode 100644 index 0000000000..8028b23923 --- /dev/null +++ b/docs/en/Community-Articles/2026-06-30-state-management-for-angular/POST.md @@ -0,0 +1,344 @@ +# Angular 22 State Management: Signals, SignalStore, or NgRx? + +Angular has been steadily moving toward a signal-first architecture since the introduction of Signals in Angular 16. With Angular 22, that transition reaches another milestone. Signals are now at the center of Angular's reactive programming model, while APIs such as Resource and Signal Forms have matured into production-ready solutions. Combined with the framework's continued investment in zoneless change detection, these improvements significantly influence how Angular applications should manage state. + +This shift also changes the role of NgRx. While the classic NgRx Store remains a powerful solution for large, event-driven applications, many scenarios that previously required reducers, selectors, and effects can now be implemented with much simpler, feature-scoped signal stores. Rather than replacing NgRx, Angular 22 encourages developers to choose the right state management strategy based on the scope and complexity of the problem. + +In this article, we'll explore how Angular 22 changes the state management landscape, compare the classic NgRx Store with NgRx SignalStore, and demonstrate best practices for building modern Angular applications. We'll also discuss how Angular's new reactive APIs fit into enterprise applications and what these changes mean for projects built with the ABP Framework. + +## Why Angular 22 Changes State Management + +Angular Signals introduced a fundamentally different approach by providing fine-grained reactivity built directly into the framework. Instead of propagating changes through Observable streams, Signals allow Angular to track exactly which pieces of state are consumed and update only the affected parts of the UI. This results in more predictable rendering, less boilerplate, and improved runtime performance. + +Angular 22 builds on this foundation by making Signals the preferred reactive primitive throughout the framework. New APIs such as **Resource** for asynchronous data loading and **Signal Forms** for reactive forms integrate naturally with Signals, reducing the need for custom RxJS pipelines in many common scenarios. + +For developers using NgRx, this doesn't mean abandoning existing applications or rewriting every store. Instead, it changes how state management should be approached. Component-local state can often be managed with plain Signals, feature-level state fits naturally into SignalStore, and the classic NgRx Store continues to excel for large-scale applications that benefit from centralized event streams, auditing, and global state synchronization. + +Understanding these changing responsibilities is the key to designing maintainable Angular applications in the Angular 22 era. A practical way to think about state management is to start with the simplest solution and introduce additional abstractions only when the application's complexity requires them. + +## Use Signals for Local Component State + +Plain Angular Signals are ideal for state that belongs exclusively to a single component. Examples include dialog visibility, selected tabs, loading indicators, filter values, or temporary form data. + +Signals provide a straightforward API with minimal overhead and integrate seamlessly with Angular's change detection. For state that never needs to be shared outside a component or its immediate children, introducing a dedicated store often adds unnecessary complexity. + +A settings page often contains UI state that doesn't need to be shared with the rest of the application. Using a dedicated store for this would introduce unnecessary complexity. + +```ts +@Component({...}) +export class UserListComponent { + readonly search = signal(''); + readonly showInactive = signal(false); + + readonly filteredUsers = computed(() => + this.users().filter(user => + user.name.includes(this.search()) && + (this.showInactive() || user.active) + ) + ); +} +``` + +This state is entirely local to the component and doesn't justify introducing a SignalStore. + +## Use NgRx SignalStore for Feature State + +As applications grow, state often needs to be shared across multiple components within the same feature. Examples include user profiles, shopping carts, administration screens, dashboards, or settings pages. + +NgRx SignalStore is designed specifically for these scenarios. It combines Angular Signals with a lightweight, feature-oriented architecture where state, computed values, and business logic are defined together. Instead of scattering logic across reducers, selectors, effects, and services, developers can keep everything related to a feature inside a single store. + +SignalStore also integrates naturally with Angular's signal-based APIs, making it an excellent choice for modern Angular applications built around Resources and Signal Forms. + +A User Management module is shared by multiple pages. The selected user, filters, and loaded entities should remain synchronized across those pages. + +```ts +export const UserStore = signalStore( + withState({ + users: [] as User[], + selectedUserId: null as number | null, + loading: false, + }), + + withComputed(({ users, selectedUserId }) => ({ + selectedUser: computed(() => + users().find(x => x.id === selectedUserId()) + ), + })), + + withMethods((store) => ({ + selectUser(id: number) { + patchState(store, { selectedUserId: id }); + }, + })), +); +``` + +Everything related to the feature lives in one place: state, derived values, and business operations. + +## NgRx Store vs. NgRx SignalStore + +Although both solutions belong to the NgRx ecosystem, they are designed to solve different architectural problems. + +The classic NgRx Store follows the Redux pattern, where every state change is represented by an action that flows through reducers before producing a new immutable state. This explicit, event-driven architecture provides excellent traceability and scales well for applications with extensive global interactions. + +SignalStore takes a different approach. Instead of centering the application around dispatched actions, it treats state as a reactive service built with Angular Signals. A SignalStore typically contains three core building blocks: + +- **State**, which represents the application's reactive data. +- **Computed signals**, which derive values from existing state. +- **Methods**, which encapsulate business logic and state updates. + +This functional model significantly reduces boilerplate while remaining predictable and testable. Since it builds directly on Angular Signals, it also integrates naturally with Angular's fine-grained change detection without requiring selectors or `async` pipes for many common scenarios. + +The following comparison summarizes the strengths of each approach. + + +| Feature | Classic NgRx Store | NgRx SignalStore | +| ---------------- | ------------------------------- | --------------------------------- | +| Architecture | Redux-based global store | Feature-oriented reactive store | +| Reactivity | RxJS Observables | Angular Signals | +| Boilerplate | Higher | Lower | +| State Scope | Global application state | Feature or route state | +| Side Effects | Effects | Store methods or `rxMethod` | +| Change Detection | Observable subscriptions | Native signal reactivity | +| Best For | Large event-driven applications | Modern feature-based applications | + + +For most new Angular 22 applications, SignalStore is an excellent default choice for feature-level state management because it embraces the framework's signal-first architecture while keeping code concise and maintainable. The classic NgRx Store remains indispensable for applications that rely heavily on centralized event processing, global synchronization, or advanced debugging capabilities. + +Instead of asking *"Which one should I use?"*, the better question is *"Which scope of state am I trying to manage?"* The answer usually determines the appropriate solution. + +## Angular 22 Features That Improve State Management + +Angular 22 introduces several framework APIs that naturally complement modern state management patterns. Rather than replacing NgRx, these APIs reduce the amount of custom infrastructure developers previously had to build around it. + +### Resource API + +One of the most significant additions is the **Resource API**, which provides a signal-based approach to asynchronous data loading. + +Historically, fetching remote data in Angular involved coordinating `HttpClient`, RxJS operators, subscriptions, loading flags, and error handling. While these patterns remain valid, they often require considerable boilerplate even for straightforward scenarios. + +Resources encapsulate these concerns into a single reactive abstraction. A Resource automatically tracks the signals it depends on, performs requests when those dependencies change, cancels obsolete requests, and exposes its lifecycle through reactive state such as the current value, loading status, and errors. + +This makes Resources particularly well suited for read-oriented operations where data should stay synchronized with application state. + +For example, changing a selected user ID can automatically trigger a new request without manually wiring `switchMap` or managing subscription lifecycles. + +```tsx +const userResource = httpResource(() => ({ + url: `/api/users/${selectedUserId()}` +})); +``` + +### Signal Forms + +Another major improvement is the stabilization of **Signal Forms**. + +Traditional Reactive Forms expose their state through `FormControl` and `FormGroup` instances, requiring developers to query validation status, dirty state, touched state, and values through an imperative API. + +Signal Forms expose these properties as signals instead. Every field becomes reactive by default, making templates easier to read while eliminating much of the manual state synchronization commonly found in form-heavy applications. + +```html +@if (profileForm.email.invalid() && profileForm.email.touched()) { + Please enter a valid email. +} +``` + +Because field state is already reactive, components rarely need additional subscriptions or helper observables to keep the UI synchronized. + +It's important to note that Signal Forms are responsible for **UI state**, while business operations such as saving data, loading entities, or handling server responses still belong in a dedicated service or SignalStore. Keeping these responsibilities separate results in components that remain focused on presentation while stores continue to own application logic. + +## Best Practices for Building Modern SignalStores + +SignalStore significantly reduces the ceremony traditionally associated with state management, but the same architectural principles still apply. A well-designed store should encapsulate business logic without becoming responsible for concerns that belong elsewhere. + +1. Keep Stores Focused on a Single Feature + A SignalStore should represent a cohesive business feature rather than becoming a global container for unrelated state. + For example, an administration module might expose separate stores for users, roles, and permissions instead of combining all administrative functionality into a single, monolithic store. Smaller stores are easier to test, understand, and maintain over time. +2. Store Business State, Not UI State + Not every piece of state belongs in a store. + Transient UI concerns such as dialog visibility, selected tabs, expanded panels, or temporary input values are usually better managed with plain Signals inside the component. + Stores should own state that represents the application's business domain—entities, filters, permissions, settings, or data shared across multiple components. +3. Derive State Instead of Duplicating It + Whenever possible, compute values instead of storing them. + SignalStore's `withComputed()` feature makes it easy to derive reactive values from existing state, reducing the likelihood of inconsistent or stale data. + Instead of storing both a list of users and an active user count, derive the count directly from the collection. + ```tsx + withComputed(({ users }) => ({ + activeUsers: computed(() => + users().filter(user => user.active).length + ), + })) + ``` + Keeping a single source of truth simplifies updates and reduces maintenance. +4. Prefer Immutable State Updates + Although SignalStore simplifies updates through `patchState()`, state should still be treated as immutable. + Updating only the affected portions of state makes changes predictable and allows Angular's signal system to efficiently notify dependent computations. + ```tsx + patchState(store, { + users: [...store.users(), newUser] + }); + ``` + +## Integrating Resources with SignalStore + +Resources and SignalStore solve different problems, and understanding their responsibilities leads to a cleaner architecture. + +A **Resource** is responsible for synchronizing data with a remote source. It knows how to load data, react to parameter changes, expose loading and error states, and keep requests up to date. + +A **SignalStore**, on the other hand, owns the application's business state. It coordinates operations, exposes domain-specific methods, derives computed values, and serves as the single source of truth for a feature. + +Rather than replacing one another, they work best together. + +A common pattern is to use a Resource for loading entities while allowing the store to expose business operations that modify those entities. + +```tsx +export const UserStore = signalStore( + withState({ + selectedUserId: undefined as number | undefined, + }), + + withComputed(({ selectedUserId }) => ({ + userResource: httpResource(() => { + const id = selectedUserId(); + + return id + ? { + url: `/api/users/${id}`, + } + : undefined; + }), + })), + + withMethods((store) => ({ + selectUser(id: number) { + patchState(store, { + selectedUserId: id, + }); + }, + })), +); +``` + +In this example, changing the selected user automatically causes the Resource to fetch new data. The store doesn't need to manage subscriptions or manually coordinate loading indicators because the Resource already exposes this information through signals. + +This separation keeps data synchronization declarative while allowing the store to remain focused on business behavior. + +## Integrating Signal Forms with SignalStore + +Signal Forms and SignalStore naturally complement one another because both are built on Angular Signals. However, they should not be treated as interchangeable. + +Signal Forms are responsible for managing user input and validation, while SignalStore coordinates business operations such as loading, updating, and persisting data. + +A common workflow consists of four steps: + +1. Load the entity through the store. +2. Populate the Signal Form. +3. Allow the user to edit the data. +4. Submit the updated values back to the store. + +The component remains responsible only for orchestrating the interaction between the form and the store. + +```tsx +@Component({ + // ... +}) +export class UserEditorComponent { + readonly store = inject(UserStore); + + readonly form = form({ + name: '', + email: '', + }); + + async save() { + if (this.form.invalid()) { + return; + } + + await this.store.updateUser(this.form.value()); + } +} +``` + +This approach keeps presentation concerns inside the component while allowing business rules to remain centralized in the store. + +## Handling Asynchronous Operations + +One challenge when combining Signal Forms with SignalStore is coordinating asynchronous operations. + +A form submission typically expects an asynchronous operation to complete before updating its own state. Meanwhile, the store is responsible for managing loading indicators, server errors, and successful updates. + +Instead of placing HTTP requests directly inside components, expose descriptive methods such as `createUser()`, `updateProfile()`, or `changePassword()` from the store. Components simply invoke these methods and react to the outcome. + +This keeps components lightweight while making business logic reusable across multiple views. + +## A Clear Separation of Responsibilities + +A useful guideline is to divide responsibilities as follows: + + +| Concern | Recommended Owner | +| ------------------- | ------------------------------------------ | +| User input | Signal Forms | +| Validation | Signal Forms | +| Loading remote data | Resource | +| Business rules | SignalStore | +| State mutations | SignalStore | +| HTTP persistence | Service or repository invoked by the store | + + +Following these boundaries results in components that focus on presentation, stores that encapsulate business logic, and Resources that handle server synchronization. Each part has a single responsibility, making the application easier to understand, test, and maintain as it grows. + +## Migrating from Classic NgRx to SignalStore + +Migrating to SignalStore doesn't require replacing an entire application's state management strategy overnight. In fact, most enterprise applications can adopt SignalStore incrementally while continuing to use the classic NgRx Store where it provides the greatest value. + +A practical migration strategy is to start with isolated features rather than the application's global state. + +### Keep the Classic Store for Global State + +Global concerns such as authentication, user sessions, application configuration, notifications, and cross-feature communication often continue to benefit from the centralized architecture of the classic NgRx Store. + +These areas typically rely on dispatched actions and event-driven workflows that remain well suited to Redux patterns. + +### Introduce SignalStore for New Features + +New feature modules are excellent candidates for SignalStore. + +Instead of creating actions, reducers, selectors, and effects, developers can define state, computed values, and business methods in a single store. This reduces boilerplate while aligning the feature with Angular's signal-first architecture. + +Existing features can also be migrated gradually as they evolve, avoiding large-scale refactoring efforts. + +### Move Component State First + +The easiest migration is often replacing component-local Observables and `BehaviorSubject`s with Signals. + +Many components don't require a dedicated store at all. Converting temporary UI state to Signals simplifies the codebase immediately and familiarizes teams with Angular's reactive model before introducing SignalStore. + +Incremental adoption minimizes risk while allowing teams to modernize applications at a sustainable pace. + +## What This Means for ABP Applications + +Angular 22's signal-first architecture aligns well with ABP's modular application model. + +Most ABP applications consist of independent feature modules such as Identity, Tenant Management, SaaS, or CMS. These modules naturally map to feature-scoped SignalStores, allowing state and business logic to remain encapsulated within each module. However, the full support will be introduced in the next version. + +As Angular continues investing in Signals, Resources, and Signal Forms, future ABP applications can increasingly rely on the framework's native reactive APIs instead of custom state management patterns. + +This doesn't diminish the importance of RxJS or the classic NgRx Store. RxJS remains an essential foundation of Angular's HTTP infrastructure and many third-party libraries, while the traditional Store continues to provide an excellent solution for complex global state management. + +Instead, Angular 22 encourages developers to use each reactive tool where it provides the greatest value. + +Whether you're upgrading an existing ABP application or starting a new project, adopting SignalStore for feature-level state can simplify development while remaining fully compatible with Angular's evolving ecosystem. + +## Conclusion + +Angular's evolution toward a signal-first architecture represents more than a new reactive API—it changes how applications should be designed. + +Rather than treating every piece of state as part of a centralized store, Angular now encourages developers to choose the appropriate abstraction for each responsibility. Plain Signals excel at local component state, SignalStore provides a lightweight solution for feature-level business logic, Resources simplify server synchronization, and Signal Forms modernize user input management. + +The classic NgRx Store continues to play an important role in large, event-driven applications, but it no longer needs to be the default choice for every state management scenario. + +By embracing these complementary tools, developers can build Angular applications that are simpler to maintain, require less boilerplate, and integrate naturally with the framework's latest capabilities. + +As Angular continues to evolve around Signals and fine-grained reactivity, adopting these patterns today will help applications remain aligned with the framework's direction while providing a solid foundation for future improvements. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/cover.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/cover.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/cover.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/cover.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-identity-users.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-identity-users.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-identity-users.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-identity-users.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-dashboard.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-dashboard.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-dashboard.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-dashboard.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-permission-management.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-permission-management.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-permission-management.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-permission-management.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-saas-tenants.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-saas-tenants.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-saas-tenants.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-saas-tenants.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-first-run.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-first-run.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-first-run.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-first-run.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png similarity index 100% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png diff --git a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/post.md similarity index 96% rename from docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md rename to docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/post.md index ca3098f8a1..fef2a61d03 100644 --- a/docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md +++ b/docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/post.md @@ -1,10 +1,10 @@ -# ABP 10.4.2 Expands Blazor UI Options with MudBlazor Support +# ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support -With ABP 10.4.2, new Blazor projects can now use **MudBlazor** (Material Design) as an alternative to the long-standing default, **Blazorise** (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.4.2 packages are live on nuget.org. +With ABP 10.5.0, new Blazor projects can now use **MudBlazor** (Material Design) as an alternative to the long-standing default, **Blazorise** (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.5.0 packages are live on nuget.org. ## Why add another Blazor UI library? -Blazorise has been ABP's default Blazor UI library for years and **remains the default and is fully supported** — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.4 does not change anything for them. +Blazorise has been ABP's default Blazor UI library for years and **remains the default and is fully supported** — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.5.0 does not change anything for them. We added MudBlazor because one Blazor UI choice cannot fit every team: @@ -190,7 +190,7 @@ Documentation: ## FAQ -**I'm already using Blazorise — will upgrading to 10.4 / 10.4.2 break my project?** +**I'm already using Blazorise — will upgrading to 10.5.0 break my project?** No. Blazorise stays the default, and package paths, type names, and namespaces are fully compatible. Follow the standard ABP upgrade flow. **Can I use Blazorise and MudBlazor in the same project?** @@ -201,7 +201,7 @@ Your custom Razor pages are tied to the UI library they were built with, so swit ## Wrapping up -MudBlazor is now a first-class Blazor UI library in ABP. With 10.4.2 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single `abp new` command. +MudBlazor is now a first-class Blazor UI library in ABP. With 10.5.0 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single `abp new` command. If you hit a bug, have a suggestion, or want a particular module's MudBlazor UX prioritized, let us know via [GitHub Issues](https://github.com/abpframework/abp/issues) or [abp.io support](https://abp.io/support). diff --git a/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/Post.md b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/Post.md new file mode 100644 index 0000000000..0a2a7ea1cb --- /dev/null +++ b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/Post.md @@ -0,0 +1,1330 @@ +# Building Scalable Enterprise Applications with ABP Framework + +If you have ever started an enterprise ASP.NET Core project with good intentions, you already know how the story usually goes. The first version is clean. A few months later, business rules are spread across controllers, repositories start leaking EF Core details everywhere, authorization gets duplicated, and cross-cutting concerns like audit logging, background jobs, and multi-tenancy become expensive retrofits. + +That is exactly the gap ABP Framework tries to close. + +ABP Framework is an open-source application framework for .NET that gives you a strong architectural baseline for building modular, maintainable, and scalable applications. It is opinionated in the right places: Domain-Driven Design, layered architecture, modularity, dependency injection, unit of work, repository abstractions, permission management, and multi-tenancy are already part of the platform instead of being left as team conventions. + +In this article, we will look at what ABP Framework is, why it fits enterprise applications well, how its architecture works, and how to apply it in a real implementation. The examples use a Library Management System, but the same approach works for internal business systems, SaaS products, and large back-office platforms. + +## What Is ABP Framework and Why It Matters for Enterprise Apps + +ABP Framework is a modular application framework built on top of ASP.NET Core. It provides infrastructure and conventions for building business applications without forcing you to rebuild the same plumbing on every project. + +At a practical level, ABP helps with problems enterprise teams hit repeatedly: + +- Managing complex business rules +- Keeping code maintainable as teams grow +- Separating domain logic from infrastructure concerns +- Supporting multiple tenants and deployment models +- Standardizing security, permissions, logging, validation, and settings +- Avoiding boilerplate around common application patterns + +### A short history and philosophy + +ABP was developed by Volosoft and evolved from the earlier ASP.NET Boilerplate ecosystem into a modern .NET framework centered on modularity and DDD-friendly design. + +Its core philosophy is straightforward: + +- Convention over configuration +- Reusable modules over copy-paste architecture +- Clear separation of concerns +- Built-in support for enterprise cross-cutting concerns +- Keep domain code independent from infrastructure + +That combination matters because enterprise applications usually fail from architectural drift, not from missing one more ORM feature. + +### ABP vs plain ASP.NET Core + +Plain ASP.NET Core gives you a solid web framework, but it does not prescribe your application architecture. That flexibility is great for small apps and libraries, but in enterprise systems it often turns into inconsistency. + +With plain ASP.NET Core, you usually need to assemble or define: + +- Layering rules +- Repository and unit of work patterns +- Domain events +- Permission infrastructure +- Multi-tenancy model +- Audit logging +- Background job processing integration +- Modular composition strategy +- DTO conventions and API exposure rules + +ABP gives you these as a coherent whole. + +That does not mean ABP is always the right choice. It means ABP is a better fit when your application has enough business complexity that architecture is no longer optional. + +### When to use ABP / When NOT to use ABP + +Use ABP when: + +- You are building a long-lived business application +- The project needs modularity and team scalability +- You expect complex authorization rules +- Multi-tenancy is a requirement or likely future need +- You want DDD and layered architecture without building all the infrastructure yourself +- You want consistency across multiple services or products + +Do not use ABP when: + +- You are building a tiny CRUD app with a short lifespan +- The team wants ultra-minimal infrastructure and is comfortable creating architecture from scratch +- The domain is trivial and unlikely to grow +- You need a highly custom low-level stack with minimal conventions + +For many enterprise teams, ABP is not about adding complexity. It is about preventing accidental complexity later. + +## Core Architectural Principles Behind ABP + +ABP is not just a package collection. Its real value is the architectural direction it gives your codebase. + +### Domain-Driven Design + +ABP strongly supports Domain-Driven Design concepts: + +- Entities +- Aggregate roots +- Value objects +- Domain services +- Repositories +- Domain events +- Specifications and domain rules + +The key idea is simple: business rules should live in the domain model, not be scattered across controllers, EF Core query code, or UI handlers. + +For example, in a library system, the rule "a book cannot be borrowed if there are no available copies" belongs in the domain layer, ideally in the aggregate root or a domain service. That rule should not depend on a controller or HTTP request. + +### Layered architecture + +A typical ABP layered solution separates responsibilities clearly: + +- Domain: business model and rules +- Application: use cases and orchestration +- Infrastructure: persistence and external integrations +- Presentation: APIs and UI + +This structure reduces coupling and makes code easier to test. It also gives teams a shared mental model: everyone knows where a new piece of logic belongs. + +### Clean Architecture ideas + +ABP aligns well with Clean Architecture principles, especially dependency direction. + +Dependencies should point inward: + +- UI depends on application layer +- Application depends on domain +- Infrastructure implements abstractions defined by inner layers +- Domain should not depend on EF Core, web frameworks, or UI libraries + +That matters in real projects because the domain usually changes more slowly than infrastructure. You can replace EF Core, expose a new API, or add a worker process without rewriting business rules. + +### SOLID principles in practice + +ABP naturally encourages SOLID design: + +- Single Responsibility: application services coordinate, domain objects enforce business rules +- Open/Closed: modules can extend behavior without rewriting existing code +- Liskov Substitution: abstractions like repositories and services are interface-driven +- Interface Segregation: contracts project keeps client-facing abstractions focused +- Dependency Inversion: inner layers define abstractions, outer layers implement them + +### Dependency Injection as a first-class citizen + +ABP uses Microsoft.Extensions.DependencyInjection under the hood, but adds strong conventions and auto-registration support. + +That means less setup code and more consistency. Application services, domain services, repositories, controllers, and many framework components are registered by convention. + + + +![Generated illustration](inline-1.png) + +## Built-in Features of ABP Framework + +ABP ships with a large set of enterprise-oriented features. The important point is not just that these features exist, but that they work together within one application model. + +### Modular system + +The modular system is one of ABP's strongest features. Every module derives from `AbpModule` and declares dependencies with `DependsOn`. + +Example: + +```csharp +[DependsOn( + typeof(AbpIdentityApplicationModule), + typeof(AbpPermissionManagementApplicationModule) +)] +public class LibraryApplicationModule : AbpModule +{ +} +``` + +Why it matters: + +- Encourages bounded contexts +- Makes features reusable +- Supports modular monolith and microservice styles +- Keeps startup configuration organized + +A common mistake is creating too many modules too early. Modules should follow meaningful business boundaries, not every folder. + +### Dependency Injection + +ABP automatically registers many services by convention. You can also use marker interfaces such as: + +- `ITransientDependency` +- `IScopedDependency` +- `ISingletonDependency` + +Example: + +```csharp +public class IsbnGenerator : ITransientDependency +{ + public string Generate() => Guid.NewGuid().ToString("N")[..13]; +} +``` + +This removes repetitive registration code while keeping lifetime choices explicit. + +### Repository pattern + +ABP provides generic repositories like `IRepository`. + +Example use in an application service: + +```csharp +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; + + public BookAppService(IRepository bookRepository) + { + _bookRepository = bookRepository; + } + + public async Task GetAsync(Guid id) + { + var book = await _bookRepository.GetAsync(id); + return ObjectMapper.Map(book); + } +} +``` + +This keeps the domain and application layers independent from direct EF Core usage. + +### Unit of Work + +ABP applies unit of work automatically for application service methods, repository operations, and many framework workflows. + +Benefits: + +- Transaction management is consistent +- Multiple repository actions can succeed or fail together +- You write less transaction boilerplate + +In practice, this is one of those features you only fully appreciate after working on a project without it. + +### Domain events + +Domain events let your domain model announce important business events without tightly coupling components. + +Example events in a library system: + +- `BookBorrowedEvent` +- `BookReturnedEvent` +- `OverdueNoticeTriggeredEvent` + +Use local domain events when the reaction stays inside the same application boundary. Use distributed events when other modules or services need to react. + +### Entity Framework Core integration + +ABP integrates deeply with EF Core while keeping the dependency in the infrastructure layer. + +You get: + +- DbContext integration +- Code-first migrations +- Repository implementations +- Query support +- Concurrency and common entity conventions + +This is a good balance: you still use EF Core where it fits, but you do not let EF Core define your whole architecture. + +### Multi-tenancy + +ABP supports three common multi-tenancy approaches: + +- Single database: all tenants share tables, separated by `TenantId` +- Database per tenant: each tenant has its own database +- Hybrid: some tenants share, some get dedicated databases + +Entities can implement `IMultiTenant`, and ABP applies tenant filtering automatically. + +Example: + +```csharp +public class Book : AggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; private set; } + public string Title { get; private set; } +} +``` + +This is a major advantage for SaaS applications because tenant-awareness is built into the application model rather than bolted on later. + +### Authorization and permission management + +ABP's permission system is more flexible than hardcoded role checks. + +Instead of writing authorization logic like this everywhere: + +- if user is admin, allow +- else if user has manager role, allow +- else deny + +You define permissions centrally and use them consistently. + +Example: + +```csharp +public static class LibraryPermissions +{ + public const string GroupName = "Library"; + public const string BooksDefault = GroupName + ".Books"; + public const string BooksCreate = GroupName + ".Books.Create"; +} +``` + +Then protect application services with permission attributes or policies. + +This becomes especially valuable when roles differ per tenant or evolve over time. + +### Identity management + +ABP includes the Identity module, which builds on ASP.NET Core Identity and works well with token-based authentication setups. + +You get: + +- Users and roles +- Claims support +- Password and security management +- Integration with external authentication approaches + +### Localization + +Localization is built in through resource files and framework conventions. + +For enterprise systems serving multiple regions, this saves a lot of custom plumbing. + +### Validation + +DTO validation is automatic for many common scenarios. You can use data annotations and integrate FluentValidation when needed. + +This keeps application services focused on use cases rather than repetitive input checks. + +### Audit logging + +Audit logging is essential in enterprise systems, especially in regulated or internal administrative applications. + +ABP can log: + +- Requests +- Method calls +- User actions +- Entity changes +- Exceptions + +That gives teams traceability without rewriting the same logging logic across modules. + +### Exception handling + +ABP standardizes exception handling and maps known exceptions to appropriate HTTP responses. + +That leads to cleaner APIs and more consistent client behavior. + +### Background jobs + +For non-interactive work, ABP supports background jobs and integrations with providers like Hangfire or Quartz. + +Typical uses: + +- Sending overdue reminders +- Rebuilding search indexes +- Generating reports +- Syncing data with external systems + +### Distributed event bus + +When your system grows into multiple modules or services, distributed events help decouple workflows. + +Example: + +- Library service publishes `MemberSuspended` +- Billing service stops auto-renewals +- Notification service sends an email +- Reporting service updates tenant analytics + +### Caching + +ABP supports in-memory and distributed caching. + +Good caching targets include: + +- Permission lookups +- Settings +- Read-heavy reference data +- Tenant-specific configuration + +In real systems, distributed cache usually matters more than memory cache once you have multiple instances behind a load balancer. + +### Setting management + +ABP supports application and tenant-level settings. + +That is useful for configurable enterprise software where behavior differs between customers or environments. + +Examples: + +- Max borrow days per tenant +- SMTP settings +- Branding settings +- Feature limits + +## Understanding the ABP Project Structure + +One of the most useful things ABP gives new teams is a solution structure that already reflects good architectural boundaries. + +Let us walk through the common projects in a layered solution. + +### `MyProject.Domain` + +This is the heart of the business model. + +Typical contents: + +- Entities +- Aggregate roots +- Value objects +- Domain services +- Repository interfaces +- Domain events +- Business rules + +Why it exists: + +- Keeps business logic independent from infrastructure +- Makes the domain testable in isolation +- Prevents EF Core or HTTP concerns from leaking into core rules + +### `MyProject.Application` + +This layer implements use cases. + +Typical contents: + +- Application services +- Orchestration logic +- DTO mapping +- Permission checks +- Transaction boundaries through unit of work + +Why it exists: + +- Coordinates domain objects and repositories +- Exposes business capabilities to clients cleanly +- Keeps controllers thin or fully unnecessary in many cases + +### `MyProject.Application.Contracts` + +This project is the public contract of the application layer. + +Typical contents: + +- DTOs +- Application service interfaces +- Permission definitions +- Setting definitions + +Why it exists: + +- Allows clients to depend on contracts without depending on implementation +- Keeps API models explicit +- Helps generated clients and UI layers stay decoupled + +### `MyProject.EntityFrameworkCore` + +This is the EF Core integration layer. + +Typical contents: + +- DbContext +- EF Core entity configuration +- Migration files +- Repository implementations + +Why it exists: + +- Contains persistence-specific code +- Prevents infrastructure concerns from contaminating the domain +- Lets you switch or extend persistence strategy more cleanly + +### `MyProject.HttpApi` + +This project exposes the application to HTTP clients. + +Typical contents: + +- API controllers +- API configuration +- Swagger integration +- HTTP endpoint concerns + +Why it exists: + +- Separates transport concerns from use case implementation +- Makes the application accessible to web, mobile, and external systems + +### `MyProject.HttpApi.Client` + +This project usually contains generated or reusable client-side proxies. + +Why it exists: + +- Simplifies service-to-service or UI-to-API communication +- Reduces manual HTTP client boilerplate +- Keeps clients aligned with the server contract + +### `MyProject.Web` + +This is the presentation layer for MVC, Razor Pages, Blazor, or similar UI approaches. + +Why it exists: + +- Contains user-facing concerns +- Reuses application contracts or API clients +- Keeps UI code out of business and persistence layers + +### `MyProject.DbMigrator` + +This project is easy to underestimate, but it matters a lot in deployment. + +Typical responsibilities: + +- Apply database migrations +- Seed initial data +- Prepare tenant databases + +Why it exists: + +- Decouples schema upgrade from web app startup +- Fits better into CI/CD pipelines +- Makes production deployment safer and more predictable + +## Building a Sample Enterprise Project: Library Management System + +To make this concrete, let us build a simplified Library Management System. + +The goal is not to show every file. The goal is to show how ABP's architecture shapes a realistic implementation. + +### Domain design + +We will model: + +- `Book` as an aggregate root +- `Author` as a related entity or separate aggregate depending on your domain needs +- `Loan` as another aggregate root +- `Isbn` as a value object + +### Creating an aggregate root + +A `Book` should protect its own invariants. + +```csharp +public class Book : AggregateRoot, IMultiTenant +{ + public Guid? TenantId { get; private set; } + public string Title { get; private set; } + public string Isbn { get; private set; } + public int TotalCopies { get; private set; } + public int BorrowedCopies { get; private set; } + + protected Book() + { + } + + public Book(Guid id, Guid? tenantId, string title, string isbn, int totalCopies) + : base(id) + { + TenantId = tenantId; + Title = Check.NotNullOrWhiteSpace(title, nameof(title)); + Isbn = Check.NotNullOrWhiteSpace(isbn, nameof(isbn)); + TotalCopies = totalCopies; + BorrowedCopies = 0; + } + + public void Borrow() + { + if (BorrowedCopies >= TotalCopies) + { + throw new BusinessException("Library:NoAvailableCopies"); + } + + BorrowedCopies++; + } + + public void Return() + { + if (BorrowedCopies <= 0) + { + throw new BusinessException("Library:InvalidReturn"); + } + + BorrowedCopies--; + } +} +``` + +This is a good example of domain logic staying inside the aggregate instead of being spread across services. + +### Adding a value object + +If you want stronger modeling, represent ISBN as a value object rather than a string. + +```csharp +public class Isbn : ValueObject +{ + public string Value { get; private set; } + + private Isbn() + { + } + + public Isbn(string value) + { + Value = Check.NotNullOrWhiteSpace(value, nameof(value)); + } + + protected override IEnumerable GetAtomicValues() + { + yield return Value; + } +} +``` + +Use value objects when they carry meaning, validation, and equality semantics. Do not create them just to look architecturally sophisticated. + +### Repository interface + +In the domain layer, define repository abstractions when needed. + +```csharp +public interface IBookRepository : IRepository +{ + Task FindByIsbnAsync(string isbn); +} +``` + +Then implement the repository in `MyProject.EntityFrameworkCore`. + +### Application service + +The application service orchestrates the use case. + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IBookRepository _bookRepository; + + public BookAppService(IBookRepository bookRepository) + { + _bookRepository = bookRepository; + } + + public async Task CreateAsync(CreateBookDto input) + { + var book = new Book( + GuidGenerator.Create(), + CurrentTenant.Id, + input.Title, + input.Isbn, + input.TotalCopies + ); + + await _bookRepository.InsertAsync(book, autoSave: true); + + return ObjectMapper.Map(book); + } +} +``` + +Notice what the application service does and does not do. + +It does: + +- Receive DTO input +- Create domain objects +- Call repositories +- Return DTO output +- Use tenant context and infrastructure abstractions + +It does not: + +- Contain persistence details +- Manually manage SQL or DbContext +- Hold core borrowing rules + +### DTOs and contracts + +ABP encourages clear application contracts. + +```csharp +public class CreateBookDto +{ + [Required] + public string Title { get; set; } + + [Required] + public string Isbn { get; set; } + + [Range(1, 1000)] + public int TotalCopies { get; set; } +} + +public class BookDto : EntityDto +{ + public string Title { get; set; } + public string Isbn { get; set; } + public int TotalCopies { get; set; } + public int BorrowedCopies { get; set; } +} +``` + +### AutoMapper profile + +Mapping stays centralized. + +```csharp +public class LibraryApplicationAutoMapperProfile : Profile +{ + public LibraryApplicationAutoMapperProfile() + { + CreateMap(); + } +} +``` + +### CRUD APIs + +ABP can expose application services as APIs with minimal boilerplate, depending on your setup. + +That means common CRUD operations do not require hand-written controllers unless you need custom behavior. + +Typical endpoints: + +- `GET /api/app/books/{id}` +- `GET /api/app/books` +- `POST /api/app/books` +- `PUT /api/app/books/{id}` +- `DELETE /api/app/books/{id}` + +### Authentication and permissions + +For the library example, define permissions such as: + +- `Library.Books` +- `Library.Books.Create` +- `Library.Books.Edit` +- `Library.Books.Delete` +- `Library.Loans.Create` +- `Library.Loans.Return` + +Then apply them at the application service or method level. + +### Database migration + +Once entities are mapped in EF Core, generate migrations and run them through the `DbMigrator` project. + +That approach is safer than relying on app startup to modify production schema. + + + +![Generated illustration](inline-2.png) + +## Dependency Injection in ABP + +ABP makes DI feel almost invisible, which is usually a good sign. + +### Lifetimes explained + +#### Transient + +A new instance is created each time it is requested. + +Use transient for: + +- Application services +- Domain services +- Stateless helpers + +#### Scoped + +One instance is created per request or scope. + +Use scoped for: + +- DbContext +- Request-specific services +- Services that depend on scoped state + +#### Singleton + +One instance exists for the lifetime of the application. + +Use singleton for: + +- Stateless shared services with no scoped dependencies +- Caches with thread-safe behavior +- Expensive-to-create infrastructure components + +### Automatic service registration + +ABP registers many services by convention. + +Examples include: + +- Types deriving from `ApplicationService` +- Types deriving from `DomainService` +- Implementations using marker interfaces like `ITransientDependency` +- Controllers and other framework-integrated services + +This reduces startup clutter and nudges teams toward consistent patterns. + +### A practical warning about lifetimes + +The usual DI rules still apply. + +Do not inject scoped services into singletons. Do not store request state inside singleton services. If a service needs tenant-aware or user-aware context, it is almost never a singleton. + +## Domain-Driven Design with ABP + +ABP is one of the better .NET frameworks for teams that want DDD support without building an entire internal platform. + +### Entities and aggregate roots + +An entity has identity. An aggregate root is the consistency boundary. + +In the library example: + +- `Book` is an aggregate root if it owns borrowing-related invariants +- `Loan` can be another aggregate root if loan lifecycle has its own rules + +A useful rule of thumb: if a change must preserve invariants atomically, it probably belongs inside one aggregate. + +### Domain services + +Use a domain service when business logic does not fit naturally inside a single entity. + +Example: + +- Evaluating whether a member can borrow based on overdue items, membership level, and tenant settings + +That logic is still domain logic, but may span multiple aggregates or policies. + +### Domain events + +Domain events help you model side effects cleanly. + +A typical flow might be: + +1. A loan is created. +2. The `Book` aggregate updates available copies. +3. A domain event is raised. +4. A handler schedules a notification or updates read models. + +This avoids giant application service methods that try to do everything directly. + +### Repositories + +Repositories should represent aggregate access, not become a dumping ground for every imaginable query. + +Good repository methods: + +- Find a book by ISBN +- Get active loans for a member +- Load an aggregate for a transaction + +Less good repository methods: + +- Huge query APIs mixing reporting, filtering, exports, and admin dashboard logic + +For reporting-heavy scenarios, consider dedicated query services or projections. + +### Specifications + +Specifications are useful when domain selection rules become complex and reusable. + +Examples: + +- Members eligible for renewal +- Loans overdue by more than 7 days +- Books that can be archived + +ABP does not force a single specification implementation style, but the pattern fits well when query rules need to stay explicit and reusable. + +### DDD flow in prose + +A typical request flow in an ABP DDD application looks like this: + +1. An HTTP request reaches the API layer. +2. The API layer calls an application service. +3. The application service validates input, checks permissions, and starts the use case. +4. Repositories load aggregates. +5. Aggregates and domain services enforce business rules. +6. Changes are persisted inside a unit of work. +7. Domain events trigger follow-up behavior. +8. The application service returns DTOs to the client. + +That flow is predictable, testable, and much easier to maintain than controller-centric business logic. + +## Multi-Tenancy in ABP + +Multi-tenancy is one of those features that is painful to retrofit. ABP handles it as a foundational concern. + +### Single database approach + +All tenants share the same database and usually the same tables. Rows are separated by `TenantId`. + +Pros: + +- Simple deployment +- Lower infrastructure cost +- Easy onboarding for new tenants + +Cons: + +- Lower isolation +- Noisy-neighbor risks at scale +- Harder to tune per tenant + +This works well for small to mid-sized SaaS applications. + +### Database per tenant + +Each tenant gets its own database. + +Pros: + +- Stronger data isolation +- Easier per-tenant backup and restore +- Better fit for regulated customers + +Cons: + +- More operational complexity +- More connection and migration management +- Higher cost + +This is common in enterprise SaaS where large customers demand isolation. + +### Hybrid approach + +Some tenants share infrastructure, while large or regulated tenants get dedicated databases. + +Pros: + +- Flexible commercial model +- Better cost/isolation balance +- Easier migration path from shared to dedicated + +Cons: + +- More operational complexity than either pure model + +### How ABP supports multi-tenancy + +ABP provides: + +- Tenant resolution pipeline +- Tenant-aware entities via `IMultiTenant` +- Automatic data filters +- Per-tenant settings and permissions +- Support for different tenant database strategies + +In practice, this means your code can stay mostly business-focused while the framework handles tenant context propagation. + +### Multi-tenancy pitfalls + +Be careful with: + +- Caching keys that ignore tenant context +- Background jobs that accidentally execute under the wrong tenant +- Reports or batch operations that bypass filters unintentionally +- Shared resources that should actually be tenant-specific + +Multi-tenancy bugs are often subtle because the app works fine in single-tenant local development. + + + +![Generated illustration](inline-3.png) + +## Authentication and Authorization in ABP + +Authentication proves who the user is. Authorization decides what they can do. ABP supports both well, but its real strength is authorization. + +### Identity module + +ABP's Identity module gives you user, role, and claim management on top of ASP.NET Core Identity. + +This covers most enterprise basics out of the box. + +### OpenIddict and token-based authentication + +For API-centric systems, OpenIddict is a strong option in the ABP ecosystem for issuing tokens and supporting standard authentication flows. + +For SPA or mobile clients, JWT-based authentication is a common setup. + +A typical flow looks like this: + +1. The client authenticates against the identity server or auth endpoint. +2. A token is issued. +3. The client calls ABP HTTP APIs with the token. +4. ABP resolves the user, roles, claims, and permissions. +5. Application service methods enforce authorization rules. + +### Role management vs permission management + +Roles are useful, but permissions are the more precise tool. + +Prefer this approach: + +- Define permissions by capability +- Assign permissions to roles +- Let tenants customize role-permission mappings + +That is much more maintainable than hardcoding logic around role names in application code. + +### Claims + +Claims are useful for identity context and external provider integration, but they should not replace a clear permission model. + +Use claims for identity data. Use permissions for business capabilities. + +## Developing a Custom ABP Module: InventoryModule + +A custom module is a good way to package a coherent business capability. + +Let us imagine an `InventoryModule` for tracking stock movement across branches. + +### Define the module + +```csharp +[DependsOn( + typeof(AbpDddApplicationModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class InventoryModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Register services, configure options, add mappings + } +} +``` + +### Why module boundaries matter + +A good module should: + +- Have a clear business purpose +- Expose explicit contracts +- Depend only on what it actually needs +- Avoid reaching directly into internal details of sibling modules + +### Service registration and configuration + +Inside a module you typically configure: + +- AutoMapper profiles +- Permission definitions +- Localization resources +- Background jobs or event handlers +- Repository implementations + +### API exposure + +A module can expose its own application services and APIs. That makes it easier to reuse in a modular monolith or extract later into a separate service if needed. + +This is one of the most practical advantages of ABP's design: module boundaries can become future service boundaries if your system evolves. + + + +![Generated illustration](inline-4.png) + +## Performance Optimization in ABP Applications + +ABP gives you strong architecture, but architecture alone does not guarantee performance. + +### Use distributed cache for shared deployments + +If your app runs on multiple instances, prefer distributed cache for data that must be shared consistently. + +Good candidates: + +- Settings +- Permission snapshots +- Tenant-specific configuration +- Frequently accessed reference data + +Redis is a common choice. + +### Use memory cache carefully + +Memory cache is fine for: + +- Small local read optimizations +- Data that can tolerate per-instance variance +- Lightweight computed values + +Do not rely on memory cache for data that must stay synchronized across instances. + +### Favor async programming end to end + +Use async repository, application service, and external I/O calls consistently. + +The main benefit is not raw speed. It is better scalability under concurrent load. + +### Optimize repository usage + +Common performance mistakes include: + +- Loading full aggregates for simple list screens +- N+1 query patterns +- Missing pagination +- Performing row-by-row updates for bulk operations + +Better patterns: + +- Project directly to DTOs for read-heavy endpoints +- Paginate lists aggressively +- Use batch operations where appropriate +- Keep aggregate loading focused on business transactions + +### Pagination and filtering + +Never return unbounded lists in enterprise APIs. + +Even if the first tenant has 200 records, the fifth customer may have 20 million. + +### Batch operations + +For imports, exports, and scheduled processing: + +- Chunk large workloads +- Use background jobs when user interaction is not required +- Avoid long-running web requests + +## Testing Enterprise Applications with ABP + +ABP's layered structure makes testing easier because responsibilities are clearer. + +### Unit testing + +Best targets for unit tests: + +- Entities +- Value objects +- Domain services +- Business rules + +These tests should run without web or database dependencies whenever possible. + +### Integration testing + +Use integration tests for: + +- Application services +- Repository behavior +- Authorization flows +- Module wiring +- Transaction behavior + +ABP provides test base infrastructure that helps bootstrap realistic test contexts. + +### Mock repositories vs real database testing + +Use mocks when you want fast, focused tests around business logic. + +Use real database-backed tests when you need confidence in: + +- EF Core mappings +- Query behavior +- Transaction boundaries +- Multi-tenancy filters + +A practical strategy is to keep most domain tests fast and add a smaller set of integration tests around high-risk workflows. + +### What to test first + +If time is limited, prioritize: + +- Aggregate invariants +- Permission-sensitive application services +- Tenant-aware behavior +- Critical workflows such as order placement, payment, borrowing, or approval paths + +## Deployment Considerations for ABP Applications + +Enterprise deployment is where clean architecture starts paying off. + +### Docker + +Containerizing ABP applications is usually straightforward and gives you consistent runtime environments. + +Typical container targets: + +- Web UI +- HTTP API host +- Auth server if separated +- Background worker +- DbMigrator as a job or pipeline step + +### Kubernetes + +Kubernetes makes sense when you need: + +- Horizontal scaling +- Rolling deployments +- Service isolation +- Centralized operations across environments + +It is powerful, but also operationally expensive. Do not adopt it just because it sounds modern. + +### Azure, IIS, and Linux + +ABP applications remain standard .NET applications, so they can run in familiar hosting environments: + +- Azure App Service or containers +- IIS for traditional Windows-hosted setups +- Linux VMs or container platforms + +Choose based on your team's operational maturity and constraints, not trends. + +### CI/CD + +A practical pipeline usually includes: + +1. Build and run tests +2. Package the application +3. Run database migrations through `DbMigrator` +4. Deploy services +5. Run smoke checks + +### Database migration strategy + +Do not leave schema updates to chance. + +For enterprise environments: + +- Version migrations carefully +- Run them in controlled deployment steps +- Seed tenant and host data explicitly +- Test rollback assumptions before production day + +## Common Challenges in ABP Projects + +ABP solves many problems, but it does not remove the need for design discipline. + +### Module dependency issues + +A common mistake is creating circular dependencies between modules. + +Avoid this by: + +- Defining clean contracts +- Depending on abstractions, not internals +- Moving shared concepts into a lower-level shared module only when necessary + +### Circular dependencies in layers + +The classic smell looks like this: + +- Domain depends on Application +- Application depends on Infrastructure +- Infrastructure depends back on Domain details in the wrong direction + +Stick to the dependency direction and the problem mostly disappears. + +### Performance bottlenecks + +ABP's abstractions do not automatically prevent slow queries. + +Watch for: + +- Over-fetching with repositories +- Poor indexing +- Heavy tenant-shared tables +- Chatty distributed calls + +### Incorrect repository usage + +A repository is not a replacement for every query pattern. + +Use repositories for aggregate access and domain-oriented persistence. For analytics, dashboards, or reporting screens, dedicated read models are often cleaner. + +### Multi-tenancy pitfalls + +The biggest mistakes are usually operational rather than syntactic: + +- Data leaks due to wrong tenant context +- Cross-tenant caches +- Misconfigured connection strings +- Jobs running against the host instead of the intended tenant + +## Best Practices for Building Scalable ABP Applications + +These are the habits that consistently pay off. + +### Keep application services thin + +Application services should coordinate the use case, not become the business logic layer. + +### Put business rules in the domain layer + +If a rule matters to the business, it should not live only in a controller, UI component, or ad hoc EF query. + +### Avoid direct DbContext usage outside repositories + +You can always bypass abstractions, but you usually pay for it later with inconsistency and harder tests. + +### Design reusable modules + +A module should represent a coherent capability, not just a code folder with a nice name. + +### Use DTOs instead of exposing entities + +Entities are domain objects, not API contracts. + +Exposing entities directly couples clients to your internal model and makes change expensive. + +### Prefer permissions over hardcoded authorization logic + +Permissions scale better than role-name checks spread across the codebase. + +### Be intentional about aggregate boundaries + +Do not make aggregates too large or too anemic. + +Large aggregates hurt performance and concurrency. Weak aggregates let invariants leak into services. + +### Start simple, then modularize where it matters + +ABP supports a lot, but you do not need every pattern at maximum depth on day one. Use the framework to create clear boundaries early, then refine where complexity actually appears. + +## TL;DR + +- ABP Framework gives enterprise .NET teams a strong architectural baseline with modularity, DDD support, multi-tenancy, permissions, and built-in cross-cutting features. +- Its layered project structure helps keep business rules in the domain, use cases in application services, and infrastructure concerns isolated. +- For real systems, ABP shines when you need consistency across modules, tenant-aware behavior, maintainability, and long-term scalability. +- The biggest wins come from using ABP as intended: thin application services, rich domain logic, clean module boundaries, and disciplined repository usage. +- ABP does not replace good design, but it removes a huge amount of architectural boilerplate so teams can focus on business problems. \ No newline at end of file diff --git a/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/cover.png b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/cover.png new file mode 100644 index 0000000000..55c2950d28 Binary files /dev/null and b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/cover.png differ diff --git a/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-1.png b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-1.png new file mode 100644 index 0000000000..65cb32d200 Binary files /dev/null and b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-1.png differ diff --git a/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-2.png b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-2.png new file mode 100644 index 0000000000..55193a127d Binary files /dev/null and b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-2.png differ diff --git a/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-3.png b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-3.png new file mode 100644 index 0000000000..839be08ebd Binary files /dev/null and b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-3.png differ diff --git a/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-4.png b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-4.png new file mode 100644 index 0000000000..3d614eca2f Binary files /dev/null and b/docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-4.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index bbb1e53eaa..6fcb8ebda1 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -2113,6 +2113,10 @@ "text": "Template Guide", "path": "solution-templates/guide.md" }, + { + "text": "Modern vs Classic Templates", + "path": "solution-templates/modern-vs-classic.md" + }, { "text": "Single-Layer Solution", "isLazyExpandable": true, diff --git a/docs/en/framework/fundamentals/dynamic-claims.md b/docs/en/framework/fundamentals/dynamic-claims.md index 24d95755c5..6481654ef9 100644 --- a/docs/en/framework/fundamentals/dynamic-claims.md +++ b/docs/en/framework/fundamentals/dynamic-claims.md @@ -70,7 +70,7 @@ There are three pre-built implementations of `IAbpDynamicClaimsPrincipalContribu * `IdentityDynamicClaimsPrincipalContributor`: Provided by the [Identity module](../../modules/identity.md) and generates and overrides the actual dynamic claims, and writes to the distributed cache. Typically works in the authentication server in a distributed system. * `RemoteDynamicClaimsPrincipalContributor`: For distributed scenarios, this implementation works in the UI application. It tries to get dynamic claim values in the distributed cache. If not found in the distributed cache, it makes an HTTP call to the authentication server and requests filling it by the authentication server. `AbpClaimsPrincipalFactoryOptions.RemoteRefreshUrl` should be properly configure to make it running. -* `WebRemoteDynamicClaimsPrincipalContributor`: Similar to the `RemoteDynamicClaimsPrincipalContributor` but works in the microservice applications. +* `WebRemoteDynamicClaimsPrincipalContributor`: Similar to the `RemoteDynamicClaimsPrincipalContributor` but works in the microservice applications. Both remote contributors run on the UI/API (resource-server) side that authenticates against a remote authentication server, not on the authentication server itself. ### IAbpDynamicClaimsPrincipalContributor @@ -82,7 +82,8 @@ If you want to add your own dynamic claims contributor, you can create a class t * `IsDynamicClaimsEnabled`: Enable or disable the dynamic claims feature. * `RemoteRefreshUrl`: The `url ` of the Auth Server to refresh the cache. It will be used by the `RemoteDynamicClaimsPrincipalContributor`. The default value is `/api/account/dynamic-claims/refresh ` and you should provide the full URL in the authentication server, like `http://my-account-server/api/account/dynamic-claims/refresh `. -* `DynamicClaims`: A list of dynamic claim types. Only the claims in that list will be overridden by the dynamic claims system. +* `IsRemoteRefreshEnabled`: Controls whether the remote contributors (`RemoteDynamicClaimsPrincipalContributor` and `WebRemoteDynamicClaimsPrincipalContributor`) are registered. `true` by default, but the Identity module sets it to `false`. So an application that includes the Identity module builds the dynamic claims locally and does not register the remote contributors, even if `WebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled` is set to `true`. +* `DynamicClaims`: A list of dynamic claim types. Only the claims in that list will be overridden by the dynamic claims system. Adding a claim type here makes the dynamic claims system authoritative for that type, so the source that fills the cache (the Identity-side claims principal factory in the local case) must actually produce it; otherwise the claim is cached with a null value and removed from the principal on each refresh. * `ClaimsMap`: A dictionary to map the claim types. This is used when the claim types are different between the Auth Server and the client. Already set up for common claim types by default. ## WebRemoteDynamicClaimsPrincipalContributorOptions @@ -91,6 +92,8 @@ If you want to add your own dynamic claims contributor, you can create a class t * `IsEnabled`: Enable or disable the `WebRemoteDynamicClaimsPrincipalContributor`. `false` by default. * `AuthenticationScheme`: The authentication scheme to authenticate the HTTP call to the authentication server. + +> Setting `IsEnabled = true` registers the contributor only when `AbpClaimsPrincipalFactoryOptions.IsRemoteRefreshEnabled` is also `true`. Because the Identity module disables `IsRemoteRefreshEnabled`, this contributor is not registered in applications that include the Identity module; it is intended for the resource-server/microservice side of a tiered solution. ## See Also diff --git a/docs/en/framework/infrastructure/background-jobs/index.md b/docs/en/framework/infrastructure/background-jobs/index.md index cdc4a1b5fb..57bffefbe0 100644 --- a/docs/en/framework/infrastructure/background-jobs/index.md +++ b/docs/en/framework/infrastructure/background-jobs/index.md @@ -225,7 +225,7 @@ ABP includes a simple `IBackgroundJobManager` implementation that; - **Retries** job execution until the job **successfully runs** or **timeouts**. Default timeout is 2 days for a job. Logs all exceptions. - **Deletes** a job from the store (database) when it's successfully executed. If it's timed out, it sets it as **abandoned** and leaves it in the database. - **Increasingly waits between retries** for a job. It waits 1 minute for the first retry, 2 minutes for the second retry, 4 minutes for the third retry and so on. -- **Polls** the store for jobs in fixed intervals. It queries jobs, ordering by priority (asc) and then by try count (asc). +- **Polls** the store for jobs in fixed intervals. It queries jobs, ordering by priority (desc) and then by try count (asc). > `Volo.Abp.BackgroundJobs` nuget package contains the default background job manager and it is installed to the startup templates by default. @@ -248,11 +248,76 @@ public class MyModule : AbpModule ```` * `JobPollPeriod` is used to determine the interval between two job polling operations. Default is 5000 ms (5 seconds). -* `MaxJobFetchCount` is used to determine the maximum job count to fetch in a single polling operation. Default is 1000. +* `MaxJobFetchCount` is used to determine the maximum job count to fetch in a single polling operation. It is also used as the batch size for the retention cleanup deletions. Default is 1000. * `DefaultFirstWaitDuration` is used to determine the duration to wait before the first retry. Default is 60 seconds. * `DefaultTimeout` is used to determine the timeout duration for a job. Default is 172800 seconds (2 days). * `DefaultWaitFactor` is used to determine the factor to increase the wait duration between retries. Default is 2.0. * `DistributedLockName` is used to determine the distributed lock name to use. Default is `AbpBackgroundJobWorker`. +* `StoreSuccessfulJobs` is used to determine whether to keep successfully completed jobs in the store instead of deleting them. Default is `false`. See the *Storing Successful Jobs* section. +* `SuccessfulJobRetentionTime` is used to determine how long a kept job is retained before the cleanup deletes it. Default is 7 days. Set to `null` to keep completed jobs forever. Only relevant when `StoreSuccessfulJobs` is enabled. +* `CleanSuccessfulJobsPeriod` is used to determine the interval between cleanup runs that delete expired completed jobs. Default is 3600000 ms (1 hour). +* `CleanupDistributedLockName` is used to determine the distributed lock name for the cleanup worker. Default is `AbpBackgroundJobCleanup`. +* `MaxParallelJobExecutionCount` is used to determine the maximum number of jobs a worker executes in parallel within one poll cycle. Default is 1. See the *Parallel Job Execution* section. +* `PerJobDistributedLockPrefix` is used to determine the prefix of the per-job distributed lock name used when `MaxParallelJobExecutionCount` is greater than 1. Default is `AbpBackgroundJob:`. + +### Storing Successful Jobs + +By default, the background job manager deletes a job from the store as soon as it runs successfully. If you want to keep completed jobs (for auditing or history), enable `StoreSuccessfulJobs`: + +````csharp +Configure(options => +{ + options.StoreSuccessfulJobs = true; + options.SuccessfulJobRetentionTime = TimeSpan.FromDays(30); //null to keep forever +}); +```` + +When enabled, a successful job is not deleted; instead its `CompletionTime` is set and it stays in the store. Completed jobs are excluded from the waiting jobs query, so they are not executed again. A cleanup worker periodically deletes completed jobs older than `SuccessfulJobRetentionTime`. + +> **Note:** The `IBackgroundJobStore` interface has new overloads (a `GetWaitingJobsAsync` overload that takes a job name filter and a `DeleteAsync` overload for cleanup). If you have a custom `IBackgroundJobStore` implementation, you must implement them for your code to compile. The built-in stores already implement them. + +### Dedicated Workers per Job Type + +By default, a single worker processes all job types. If you want to process certain job types separately (for example, slow or high-volume jobs), you can register dedicated workers, each handling only the specified job argument types with its own distributed lock: + +````csharp +Configure(options => +{ + options.AddDedicatedWorker("NotificationWorkerLock"); + options.AddDedicatedWorker("ReportWorkerLock"); +}); +```` + +Each dedicated worker processes only its configured job types. An additional default worker is automatically started to process all the remaining job types. In sequential mode, each worker (including the default one) runs independently under its own distributed lock (see *Parallel Job Execution* for how this changes when running jobs in parallel). + +If you don't want to specify a lock name, use the overloads without the `lockName` parameter; a stable, length-bounded lock name is then derived from the job argument types: + +````csharp +Configure(options => +{ + options.AddDedicatedWorker(); + options.AddDedicatedWorker(); +}); +```` + +> **Note:** Each job type can be handled by only one dedicated worker, and each worker must have a unique lock name; `AddDedicatedWorker` throws if this is violated. Dedicated workers require an `IBackgroundJobStore` that can filter jobs by name (the built-in stores can). + +### Parallel Job Execution + +By default, a worker executes waiting jobs one by one under a single worker-level distributed lock, so only one job runs at a time across all application instances. If you want to execute multiple jobs concurrently, set `MaxParallelJobExecutionCount` to a value greater than 1: + +````csharp +Configure(options => +{ + options.MaxParallelJobExecutionCount = 4; +}); +```` + +When it is greater than 1, the worker-level lock is not used. Instead, each job is claimed with its own distributed lock, so multiple application instances can execute different jobs at the same time. With a properly configured distributed lock provider, a job is not executed by more than one instance at a time. + +`MaxParallelJobExecutionCount` is a per-worker, per-poll-cycle limit — it is not a cluster-wide limit. A worker first fetches up to `MaxJobFetchCount` waiting jobs, then executes up to `MaxParallelJobExecutionCount` of them in parallel, so a single worker runs up to `min(MaxJobFetchCount, MaxParallelJobExecutionCount)` jobs per cycle; keep `MaxJobFetchCount` at least as large as `MaxParallelJobExecutionCount` to avoid capping the parallelism. When you also configure dedicated workers, each worker runs its own timer and claims up to `MaxParallelJobExecutionCount` jobs, so the effective concurrency is up to (number of workers) × `MaxParallelJobExecutionCount` per application instance, and up to (number of application instances) × (number of workers) × `MaxParallelJobExecutionCount` across the whole cluster. + +> **Important:** Configure `MaxParallelJobExecutionCount` and `PerJobDistributedLockPrefix` consistently across all application instances. Mixing sequential (worker lock) and parallel (per-job lock) instances removes the common mutual exclusion, and a different prefix produces a different per-job lock name for the same job — either case may let the same job run on more than one instance. As with the sequential mode, configure a real [distributed lock](../distributed-locking.md) provider for clustered deployments. ### Data Store diff --git a/docs/en/get-started/index.md b/docs/en/get-started/index.md index 1522f3f955..2bf27ce498 100644 --- a/docs/en/get-started/index.md +++ b/docs/en/get-started/index.md @@ -20,7 +20,7 @@ Please select one of the following documents best fits for your application: - [WPF Application](wpf.md) - [Console Application](console.md) -If you seek a React-based web UI, use the **modern template system** with ABP Studio or `abp new --modern`. See [UI options](../framework/ui/index.md) for the full list of officially supported UI frameworks. +If you are choosing between the **Modern** and **Classic** ABP Studio template families, see [Modern vs Classic Templates](../solution-templates/modern-vs-classic.md). If you seek a React-based web UI, use the **modern template system** with ABP Studio or `abp new --modern`. See [UI options](../framework/ui/index.md) for the full list of officially supported UI frameworks. ## Which Startup Template is Suitable for Me? diff --git a/docs/en/modules/background-jobs.md b/docs/en/modules/background-jobs.md index 971284b7c9..2d57d0c907 100644 --- a/docs/en/modules/background-jobs.md +++ b/docs/en/modules/background-jobs.md @@ -33,6 +33,8 @@ Following custom repositories are defined for this module: - `IBackgroundJobRepository` +> `IBackgroundJobRepository` supports filtering the waiting jobs for dedicated workers and cleaning up retained completed jobs. See the *Dedicated Workers per Job Type* and *Storing Successful Jobs* sections of the [background jobs](../framework/infrastructure/background-jobs) document. + ### Database providers #### Common diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index 8049ffa986..8865db1073 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -7,6 +7,79 @@ # Package Version Changes +## 10.6.0-rc.1 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Authentication.OpenIdConnect | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Authorization | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components.Authorization | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components.Web | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components.WebAssembly | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components.WebAssembly.Authentication | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components.WebAssembly.DevServer | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Components.WebAssembly.Server | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.DataProtection.StackExchangeRedis | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Mvc.NewtonsoftJson | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.Mvc.Testing | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.TestHost | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.AspNetCore.WebUtilities | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Bcl.AsyncInterfaces | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Data.SqlClient | 6.1.1 | 7.0.2 | #25706 | +| Microsoft.Data.Sqlite | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.Design | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.Proxies | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.Relational | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.SqlServer | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.Sqlite | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.EntityFrameworkCore.Tools | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Caching.Hybrid | 9.9.0 | 10.7.0 | #25706 | +| Microsoft.Extensions.Caching.Memory | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Caching.StackExchangeRedis | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Configuration.Binder | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Configuration.CommandLine | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Configuration.EnvironmentVariables | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Configuration.UserSecrets | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.DependencyInjection | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.FileProviders.Composite | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.FileProviders.Embedded | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.FileProviders.Physical | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.FileSystemGlobbing | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Hosting | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Hosting.Abstractions | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Http | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Identity.Core | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Localization | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Logging | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Logging.Abstractions | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Logging.Console | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Options | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.Extensions.Options.ConfigurationExtensions | 10.0.7 | 10.0.9 | #25706 | +| Microsoft.IdentityModel.JsonWebTokens | 8.16.0 | 8.19.1 | #25706 | +| Microsoft.IdentityModel.Protocols.OpenIdConnect | 8.16.0 | 8.19.1 | #25706 | +| Microsoft.IdentityModel.Tokens | 8.16.0 | 8.19.1 | #25706 | +| MongoDB.Driver | 3.9.0 | 3.10.0 | #25773 | +| System.Collections.Immutable | 10.0.7 | 10.0.9 | #25706 | +| System.IdentityModel.Tokens.Jwt | 8.16.0 | 8.19.1 | #25706 | +| System.Management | 10.0.7 | 10.0.9 | #25706 | +| System.Security.Cryptography.Xml | 10.0.7 | 10.0.9 | #25706 | +| System.Security.Permissions | 10.0.7 | 10.0.9 | #25706 | +| System.Text.Encoding.CodePages | 10.0.7 | 10.0.9 | #25706 | +| System.Text.Encodings.Web | 10.0.7 | 10.0.9 | #25706 | +| System.Text.Json | 10.0.7 | 10.0.9 | #25706 | + +## 10.5.1 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| Swashbuckle.AspNetCore | 10.0.1 | 10.2.3 | #25759 | + ## 10.5.0-rc.4 | Package | Old Version | New Version | PR | diff --git a/docs/en/release-info/migration-guides/abp-10-5.md b/docs/en/release-info/migration-guides/abp-10-5.md index 634506e7c9..4727824d08 100644 --- a/docs/en/release-info/migration-guides/abp-10-5.md +++ b/docs/en/release-info/migration-guides/abp-10-5.md @@ -171,7 +171,7 @@ Keep `DisablePayloadSigning` disabled for providers that support the default AWS **What changed** -- Blazorise packages were upgraded to **2.1.3**. +- Blazorise packages were upgraded to **2.2.1**. - MongoDB.Driver was upgraded to **3.9.0**. - `@abp/codemirror` was updated to CodeMirror **6.0.2**. diff --git a/docs/en/release-info/migration-guides/abp-10-6.md b/docs/en/release-info/migration-guides/abp-10-6.md new file mode 100644 index 0000000000..724e30ec2b --- /dev/null +++ b/docs/en/release-info/migration-guides/abp-10-6.md @@ -0,0 +1,237 @@ +```json +//[doc-seo] +{ + "Description": "Upgrade your ABP solutions from v10.5 to v10.6 with this migration guide covering important behavior and integration changes." +} +``` + +# ABP Version 10.6 Migration Guide + +This document is a guide for upgrading ABP v10.5 solutions to ABP v10.6. There are some important changes that may require action in specific application scenarios. + +> **Package Version Changes:** Before upgrading, review the [Package Version Changes](../../package-version-changes.md) document to see version changes on dependent NuGet and NPM packages and align your project with ABP's internal package versions. + +## Open-Source (Framework) + +This version contains the following changes on the open-source side: + +### Background Jobs Infrastructure Extensions + +**Who is affected** + +- Applications using the default background job worker and wanting dedicated workers, parallel execution, or successful job retention. +- Applications with a custom `IBackgroundJobStore` implementation. +- Applications using the Background Jobs module with EF Core and enabling successful job retention. + +**What changed** + +- ABP adds opt-in support for: + - storing successfully completed jobs (`StoreSuccessfulJobs`) + - dedicated workers per job argument type (`AddDedicatedWorker(...)`) + - parallel job execution (`MaxParallelJobExecutionCount`) +- `IBackgroundJobStore`, `IBackgroundJobWorker`, and related infrastructure gained new members. +- EF Core stores add a `CompletionTime` column to background job records for retention scenarios. +- All new runtime features are disabled by default. + +**What to do** + +No action is required if you do not enable the new options and do not maintain a custom background job store. + +If you maintain a custom `IBackgroundJobStore`, implement the new interface members so your solution compiles. + +If you enable `StoreSuccessfulJobs`, add/review the EF Core migration for the `CompletionTime` column and configure retention options explicitly: + +```csharp +Configure(options => +{ + options.StoreSuccessfulJobs = true; + options.SuccessfulJobRetentionTime = TimeSpan.FromDays(7); +}); +``` + +If you enable dedicated workers or parallel execution, configure the options consistently across all application instances and use a real distributed lock provider in clustered deployments. + +> See the [Background Jobs](../../framework/infrastructure/background-jobs/index.md) document and [#25742](https://github.com/abpframework/abp/pull/25742) for details. + +### API Definition and Proxy Generation for Uploads and Content Types + +**Who is affected** + +- Applications using generated Angular, jQuery, or C# proxies for upload endpoints. +- Applications returning non-JSON response types from application services. +- Applications that customized generated upload proxy signatures. + +**What changed** + +- API definition now exposes response `ContentTypes` and `IsRemoteStream`. +- Generated Angular and jQuery proxies forward upload DTOs containing `IRemoteStreamContent` as multipart `FormData`. +- Generated Angular upload method signatures may collapse the upload argument to `FormData`. +- `RestService` now unwraps ABP error envelopes more consistently for text and blob response modes. + +**What to do** + +- Keep upload DTO types in `FormBodyBindingIgnoredTypes` as before. +- Regenerate client proxies after upgrading. +- Update custom client code that assumed upload proxies accepted the original DTO type instead of `FormData`. +- Re-test file upload flows in Angular, MVC/jQuery, and C# client integrations. + +> See [#25639](https://github.com/abpframework/abp/pull/25639) for details. + +### Angular 22 Upgrade + +**Who is affected** + +- Applications using the ABP Angular UI. +- Applications with custom Angular code, third-party Angular libraries, or CI pipelines pinned to Angular 21. + +**What changed** + +- ABP Angular packages and templates now target **Angular 22.0.x**. +- Locale loading was improved with a fallback mechanism for missing or partial locale resources. + +**What to do** + +- Upgrade your Angular application dependencies together with ABP NPM packages. +- Follow the official Angular update guidance for your current Angular version. +- Re-run UI tests and rebuild custom Angular libraries after the upgrade. +- Regenerate Angular proxies after upgrading backend packages. + +> See [#25690](https://github.com/abpframework/abp/pull/25690) and [#25734](https://github.com/abpframework/abp/pull/25734) for details. + +### Antiforgery User Id Claim Issuer Normalization + +**Who is affected** + +- Applications that serve a token-authenticated SPA and cookie-authenticated MVC/Razor Pages on the same origin. +- Applications using Razor Pages modules such as Setting Management with antiforgery-protected POST handlers. + +**What changed** + +- ABP normalizes the user id claim issuer while generating and validating antiforgery tokens. +- Razor Pages now use ABP's antiforgery validation path instead of only the built-in ASP.NET Core filter. +- The behavior is enabled by default through `AbpAntiForgeryOptions.NormalizeUserIdClaimIssuer`. + +**What to do** + +- Re-test SPA + MVC mixed authentication flows, especially pages that POST immediately on load. +- If you implemented custom antiforgery logic that depends on the raw claim issuer, review it after upgrading. +- Disable the behavior only if you intentionally rely on the previous issuer-specific antiforgery identity: + +```csharp +Configure(options => +{ + options.NormalizeUserIdClaimIssuer = false; +}); +``` + +> See [#25655](https://github.com/abpframework/abp/pull/25655) and [#25669](https://github.com/abpframework/abp/pull/25669) for details. + +### OpenIddict Interactive Cookie `client_id` Fix + +**Who is affected** + +- Applications using OpenIddict authorization-code flows together with interactive cookie authentication. +- Applications relying on audit logs or current-client resolution from cookie-authenticated requests. + +**What changed** + +- ABP removes `client_id` from the interactive authentication cookie when the cookie principal is refreshed. +- Access tokens are unaffected. +- Cookies that were already corrupted self-heal on the next refresh. + +**What to do** + +No action is required. Re-test authorization, account, and audit-log scenarios if you previously observed intermittent incorrect `ClientId` values in cookie-authenticated requests. + +> See [#25711](https://github.com/abpframework/abp/pull/25711) for details. + +### Access Token Forwarding for Authenticated Client Requests + +**Who is affected** + +- Applications using `HttpContextAbpAccessTokenProvider`. +- Machine-to-machine integrations that authenticate with `client_credentials` and then call other protected APIs from the same request pipeline. + +**What changed** + +- The provider now forwards the incoming access token whenever the request is authenticated, not only when there is an interactive user. +- `client_credentials` requests no longer fall back to configured identity clients in that scenario. + +**What to do** + +- Re-test service-to-service calls that rely on the current HTTP context access token. +- Verify downstream API authorization when the caller authenticates as a client rather than a user. + +> See [#25740](https://github.com/abpframework/abp/pull/25740) for details. + +### Thread Current Principal Accessor Behavior + +**Who is affected** + +- Background jobs, hosted services, and other non-web code that reads `ICurrentPrincipalAccessor.Principal`. +- Code that explicitly checks for `null` principals in non-web contexts. + +**What changed** + +- `ThreadCurrentPrincipalAccessor` now returns an anonymous `ClaimsPrincipal` instead of `null` when `Thread.CurrentPrincipal` is not set. + +**What to do** + +- Re-test background jobs and hosted services that branch on `Principal == null`. +- Prefer checking authentication/identity state through claims or ABP's current user/client abstractions instead of relying on a `null` principal. + +> See [#25752](https://github.com/abpframework/abp/pull/25752) for details. + +### Dependency Updates + +**Who is affected** + +- Applications that pin ABP transitive dependencies directly. +- Applications using `Microsoft.Data.SqlClient`, Swashbuckle, or Angular with fixed versions. + +**What changed** + +- `Microsoft.*` and `System.*` packages were upgraded to **10.0.9**. +- `Microsoft.Data.SqlClient` was upgraded to **7.0.2**. +- `Swashbuckle.AspNetCore` was upgraded to **10.2.3**. +- ABP Angular packages were upgraded to **Angular 22.0.x**. + +**What to do** + +- Review your direct package references and align them with ABP's package versions where needed. +- Rebuild and run database/integration tests if you directly use `Microsoft.Data.SqlClient`. +- Re-test Swagger/OpenAPI integration if you customized Swashbuckle configuration. + +## Pro + +There are no explicitly marked breaking changes on the PRO side in this release scope. However, check the following if they apply to your application. + +### OpenIddict Generate Access Token UI + +**Who is affected** + +- Applications using OpenIddict application management UIs in ABP Commercial. + +**What changed** + +- Administrators can generate access tokens for OpenIddict applications from MVC, Blazor, MudBlazor, and Angular UIs. +- The backend forwards `client_credentials` requests to `/connect/token`. + +**What to do** + +- Re-test OpenIddict application administration pages after upgrading. +- Review who can access the new token-generation action in your authorization setup. + +### AI Management Indexing Resilience + +**Who is affected** + +- Applications using the AI Management module with document indexing enabled. + +**What changed** + +- Indexing is more resilient under memory pressure. + +**What to do** + +- Re-test document indexing on large datasets or memory-constrained environments after upgrading. diff --git a/docs/en/release-info/migration-guides/index.md b/docs/en/release-info/migration-guides/index.md index 418ec7d586..ec169d8242 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.5 to 10.6](abp-10-6.md) - [10.4 to 10.5](abp-10-5.md) - [10.3 to 10.4](abp-10-4.md) - [10.x to 10.3](abp-10-3.md) diff --git a/docs/en/release-info/release-notes.md b/docs/en/release-info/release-notes.md index 6ad76aabc4..546d795ad1 100644 --- a/docs/en/release-info/release-notes.md +++ b/docs/en/release-info/release-notes.md @@ -1,7 +1,7 @@ ```json //[doc-seo] { - "Description": "Explore the latest ABP Framework release notes, highlighting major features and enhancements for each version, including migration guidance." + "Description": "Explore the latest ABP Framework release notes, highlighting major features and enhancements for each version, including migration guidance." } ``` @@ -14,19 +14,19 @@ 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.6 () +## 10.6 (2026-07-07) -See the detailed **[blog post / announcement]()** for the v10.6 release. +See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-6-release-candidate-reoq6kzw)** for the v10.6 release. -- Angular has been upgraded to version 22.0.x. +- Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention +- API Definition and Proxy Improvements for Content Types and Multipart Uploads +- Angular UI: Upgrade to Angular 22 +- Antiforgery and OpenIddict Security Improvements +- OpenIddict: Generate Access Token from the UI - For a complete list of changes, including breaking changes, migration steps, and package updates, see the **[Angular Release Notes for v10.6](./../framework/ui/angular/release-notes/angular-22-typescript-6.md)**. +## 10.5 (2026-06-30) - - -## 10.5 (2026-06-02) - -See the detailed **[blog post / announcement](https://abp.io/community/articles/announcing-abp-10-5-release-candidate-k6oxdfle)** for the v10.5 release. +See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-5-stable-release-2u589bsc)** for the v10.5 release. - S3-Compatible Blob Storage Support - OpenIddict: Default Scope Fallback Options @@ -56,7 +56,7 @@ See the detailed **[blog post / announcement](https://abp.io/community/announcem - Event Bus: String-Based Event Publishing with Dynamic Payload - Background Jobs/Workers: String-Based Publishing with Dynamic Payload - API Definition Endpoint: Descriptions and Documentation Support -- Entity Cache: New Batch APIs (`FindMany`* / `GetMany*`) +- Entity Cache: New Batch APIs (`FindMany`_ / `GetMany_`) - Angular: User/Tenant Sharing and Tenant Switch Experience - Angular: Upgrade to 21.2 + TypeScript 5.9 - Introducing the `Volo.Abp.LuckyPenny.AutoMapper` Provider @@ -467,7 +467,7 @@ See the detailed **blog post / announcement** for the v2.8 release: [https://abp ## 2.7 (2020-05-07) -See the detailed **blog post / announcement** for the v2.7 release: [https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released](https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released) +See the detailed **blog post / announcement** for the v2.7 release: [https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released](https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released) - New module: **Text template management** (with angular and mvc UI - document is [coming](../modules/text-template-management.md)). - **Dynamically add properties** to current entities of the depended modules (see [module entity extensions](../framework/architecture/modularity/extending/module-entity-extensions.md)) @@ -478,9 +478,8 @@ See the detailed **blog post / announcement** for the v2.7 release: [https://ab - **Optimize database migrations** & seed code for multi-tenant multi-database systems. - ABP Suite: Make **menu item active** on navigation menu when selected. - ABP Suite: Improve **enum usage** while creating new entities. -- Bug fixes in the [Lepton Theme](https://abp.io/themes), [ABP Suite](https://abp.io/tools/suite) and other modules. +- Bug fixes in the [Lepton Theme](https://abp.io/themes), [ABP Suite](https://abp.io/tools/suite) and other modules. ## See Also - [Road map](road-map.md) - diff --git a/docs/en/release-info/road-map.md b/docs/en/release-info/road-map.md index a0b3f856e7..6df6ca81fa 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 10.5, planned for June 2026." + "Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.7, planned for August 2026." } ``` @@ -11,28 +11,29 @@ This document provides a road map, release schedule, and planned features for th ## Next Versions -### v10.6 +### v10.7 -The next planned version will be 10.6, which is scheduled to be released as a stable version in July 2026. We will be mostly working on the following topics: +The next planned version will be 10.7, which is scheduled to be released as a stable version in August 2026. Based on the currently open issues and pull requests across the ABP ecosystem, we will be mostly working on the following topics: * Framework - * Token verification improvements with refresh token support and distributed locking - * Angular UI fixes and proxy generation improvements - * Better handling for extra properties, object mapping and auditing edge cases - * Hybrid UI / page embedding infrastructure - * Upgrading 3rd-party dependencies and evaluating replacements where needed - * General bug fixing and improvements in core framework packages + * Cookie authentication: refresh token support and distributed locking ([#25011](https://github.com/abpframework/abp/issues/25011)) + * jQuery 4.x upgrade, or removing jQuery as a dependency ([#25123](https://github.com/abpframework/abp/issues/25123)) + * AI agent skills distributed as versioned plugins ([#25712](https://github.com/abpframework/abp/issues/25712)) + * Hybrid UI / page embedding infrastructure ([#23102](https://github.com/abpframework/abp/issues/23102), [#23161](https://github.com/abpframework/abp/issues/23161)) + * Microsoft Agent Framework migration and native agent skills support ([#24310](https://github.com/abpframework/abp/issues/24310), [#25194](https://github.com/abpframework/abp/issues/25194)) + * Better ExtraProperties mapping for EF Core ([#23546](https://github.com/abpframework/abp/issues/23546)) + * Upgrading 3rd-party dependencies and general bug fixing in core framework packages * ABP Suite - * Improvements on generated codes for nullability - * Improvements on master-detail page design (making it more compact) + * Replace the templating system with Scriban while preserving backward compatibility + * Support for additional property types like `DateTimeOffset`, `TimeSpan` and numeric enums + * Display names and ordering for properties and navigation properties + * Filter on inherited properties and namespace-based UI foldering * Low-Code system integration - * Better support for additional property types like `DateTimeOffset`, `TimeSpan` and numeric enum scenarios - * Improvements for generated file upload, navigation property and display-name experiences + * Improvements on generated code nullability, master-detail pages and file upload experiences * ABP Studio - * AI Coding Agent and MCP integration - * Modern solution wizard improvements and low-code support + * Low-Code platform integration * Theme Builder: live preview, project integration and import/export * Linux support and packaging improvements * Better React / React Native / Thin UI template experience @@ -41,11 +42,12 @@ The next planned version will be 10.6, which is scheduled to be released as a st * Terminal, browser and built-in developer productivity enhancements * Application Modules - * AI Management: chat history, multi-tenancy and tenant-scoped workspace capabilities + * AI Management: chat history + * AI Management: multi-tenancy and tenant-scoped workspace capabilities + * RAG: Cloudflare `/crawl` endpoint as a data source * New module: Chat with your data - * Low-Code designer and low-code platform integrations - * CMS Kit and public website improvements * Payment module e-mail notification improvements + * CMS Kit and public website improvements * UI/UX improvements on existing application modules * Updating existing tutorials & documents (with other UI & DB options) @@ -61,42 +63,46 @@ The *Next Versions* section above shows the main focus of the planned versions. The ABP framework is [open source](https://github.com/abpframework/abp) and free for everyone. You can see its [public backlog](https://github.com/abpframework/abp/milestone/2). Here are some of the selected backlog items and longer-term topics: * [#23102](https://github.com/abpframework/abp/issues/23102) / ABP Hybrid UI System: Re-using module UIs in different technologies +* [#23161](https://github.com/abpframework/abp/issues/23161) / Page Embedding Feature (aka Hybrid UI) * [#25123](https://github.com/abpframework/abp/issues/25123) / Upgrade jQuery to 4.x, or consider removing it as dependency -* [#24742](https://github.com/abpframework/abp/issues/24742) / Add Support for LiteDB as a Database Provider -* [#24442](https://github.com/abpframework/abp/issues/24442) / Add Couchbase EF Core Provider Integration +* [#17093](https://github.com/abpframework/abp/issues/17093) / MVC UI: decouple jQuery * [#24310](https://github.com/abpframework/abp/issues/24310) / Migrate Volo.Abp.AI Semantic Kernel to Microsoft Agent Framework +* [#25194](https://github.com/abpframework/abp/issues/25194) / Integrate Microsoft.Agents.AI for native Agent Skills support * [#23575](https://github.com/abpframework/abp/issues/23575) / Support list/enumerable of complex types for ABP dynamic/static C# proxies on GET requests * [#23546](https://github.com/abpframework/abp/issues/23546) / Better ExtraProperties mapping for EF Core * [#23935](https://github.com/abpframework/abp/issues/23935) / Hybrid Cache Support for EntityCache * [#22931](https://github.com/abpframework/abp/issues/22931) / Angular - Support dynamic URLs for breadcrumbs -* [#25032](https://github.com/abpframework/abp/issues/25032) / Guidance and infrastructure considerations for gRPC-based scenarios -* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure -* [#57](https://github.com/abpframework/abp/issues/57) / Built-in CQRS infrastructure +* [#24742](https://github.com/abpframework/abp/issues/24742) / Add Support for LiteDB as a Database Provider +* [#24442](https://github.com/abpframework/abp/issues/24442) / Add Couchbase EF Core Provider Integration +* [#2882](https://github.com/abpframework/abp/issues/2882) / ABP gRPC Integration +* [#57](https://github.com/abpframework/abp/issues/57) / CQRS infrastructure * [#58](https://github.com/abpframework/abp/issues/58) / Content localization system (multilingual entities) -* [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook system -* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB integration for multitenancy -* [#2296](https://github.com/abpframework/abp/issues/2296) / Feature toggling infrastructure +* [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook System +* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB Integration for multitenancy +* [#2296](https://github.com/abpframework/abp/issues/2296) / Implementing Feature Toggle * [#15932](https://github.com/abpframework/abp/issues/15932) / Introduce ABP Diagnostics Module * [#16744](https://github.com/abpframework/abp/issues/16744) / State Management API -* [#119](https://github.com/abpframework/abp/issues/119) / REST API versioning improvements -* [#2087](https://github.com/abpframework/abp/issues/2087) / RavenDB database support +* [#119](https://github.com/abpframework/abp/issues/119) / REST API Versioning Improvements +* [#2087](https://github.com/abpframework/abp/issues/2087) / Add RavenDB Database support ### Application Modules / UI Themes ABP Platform provides many (free and commercial) [pre-built application modules](../modules/index.md) and modern [UI themes](../ui-themes/index.md). In every release, many enhancements and bugfixes are delivered for these modules and themes. Important backlog topics currently include: * AI Management module: chat history, multi-tenancy, tenant workspaces and operational hardening -* CMS Kit module: media gallery and richer public website capabilities +* New module: Chat with your data +* RAG with external data sources such as website crawling * Payment module: richer notifications and invoice-oriented scenarios +* CMS Kit: Meta information for SEO * Audit logging UI: filter redesign and UX improvements * Identity Pro: richer filtering and organization-unit UX improvements * LeptonX and existing UIs: new layouts, styles and usability refinements -* New module ideas: Chat with your data, AI Search, user notification and dynamic dashboard ### ABP Studio [ABP Studio](../studio/index.md) is a cross-platform desktop application for ABP and .NET developers to simplify and automate daily tasks of developers. It has a community (free) edition as well as commercial capabilities. Here are some of the important planned features and active backlog topics for the next ABP Studio versions: +* Low-Code: ABP Studio Integration * Theme builder for LeptonX, including live preview, management UI and project integration * Analyze user solutions to explore entities, domain services, application services, pages and other fundamental objects * AI agent/browser capabilities and developer-assistant experiences @@ -107,16 +113,14 @@ ABP Platform provides many (free and commercial) [pre-built application modules] * More options while creating new solutions, modules and services * Better environment-variable, deployment and Kubernetes experiences * Compare changes on startup templates when a new ABP version is published -* Rapid application development and low-code oriented features * ABP support integration and better diagnostics/error experiences ### ABP Suite [ABP Suite](../suite/index.md) is a GUI application that is mainly used to generate CRUD-style pages in your application. You define your entity and it can generate all the code from the database to the UI. Here are some of the important planned features for the next ABP Suite versions: +* Replace the current templating system with Scriban while preserving backward compatibility * Better nullability support in generated code -* MudBlazor support -* Replacing the current templating system with a text engine while preserving backward compatibility * Support for additional property types like `DateTimeOffset` and `TimeSpan` * Handle image properties for entities (in addition to file properties, which are already supported) * Allow to define display names and better ordering for properties and navigation properties diff --git a/docs/en/solution-templates/index.md b/docs/en/solution-templates/index.md index 56969fc6ef..602f4b0216 100644 --- a/docs/en/solution-templates/index.md +++ b/docs/en/solution-templates/index.md @@ -9,7 +9,7 @@ ABP provides pre-architected and production-ready templates to jump start a new solution. -> **You can see the [Solution Template Selection Guide](guide.md) if you are not sure which solution template is suitable for you.** +> **You can see the [Solution Template Selection Guide](guide.md) if you are not sure which solution template is suitable for you. See [Modern vs Classic Templates](modern-vs-classic.md) if you are choosing between ABP Studio's Modern and Classic template families.** The reference pages below cover both classic and modern ABP Studio template families. The Single-Layer and Layered pages document the classic templates. The Modular Monolith and Microservice pages call out the current modern structure where it differs. diff --git a/docs/en/solution-templates/modern-vs-classic.md b/docs/en/solution-templates/modern-vs-classic.md new file mode 100644 index 0000000000..a41ea05422 --- /dev/null +++ b/docs/en/solution-templates/modern-vs-classic.md @@ -0,0 +1,55 @@ +```json +//[doc-seo] +{ + "Description": "Compare Modern and Classic ABP Studio templates, including architecture mapping, UI choices, mobile options, and when to choose each template family." +} +``` + +# Modern vs Classic Templates + +ABP Studio provides two solution template families: **Modern** and **Classic**. Both families create production-ready ABP solutions and share the same ABP backend concepts, such as Entity Framework Core and MongoDB database options, OpenIddict authentication, multi-tenancy, optional modules, test projects, language selection and deployment-related configuration where they are supported. + +The main difference is how you choose and shape the solution. **Classic templates** are the traditional template-first flow. You first choose a concrete template, such as Single-Layer, Layered or Microservice, and then select UI, database, mobile and module options. **Modern templates** are the newer architecture-first flow. You first choose the backend architecture, then ABP Studio maps your selection to the proper modern template. + +## Template Families + +| Template family | Template names | Main idea | +| --- | --- | --- | +| Classic | `app-nolayers`, `app`, `microservice` | The traditional ABP Studio solution templates with the broadest UI framework choices. | +| Modern | `app-nolayers-modern`, `app-modern`, `microservice-modern` | The newer ABP Studio templates focused on React-based web applications and an architecture-first creation flow. | + +## Modern Architecture Mapping + +When you use the Modern solution creation flow in ABP Studio, your selected architecture maps to a modern template: + +| Modern architecture | Generated template | Notes | +| --- | --- | --- | +| Simple Monolith | `app-nolayers-modern` | A simpler application structure with the main backend code in one host project. | +| Layered Monolith | `app-modern` | A layered solution based on Domain Driven Design practices. | +| Modular Monolith | `app-nolayers-modern` | Uses the modern single-layer template with modular solution options enabled. | +| Microservice | `microservice-modern` | A distributed solution with dedicated services, gateways and applications. | + +## Practical Differences + +| Area | Classic templates | Modern templates | +| --- | --- | --- | +| Creation flow | Template-first: select Single-Layer, Layered or Microservice first. | Architecture-first: select Simple Monolith, Layered Monolith, Modular Monolith or Microservice first. | +| Web UI choices | Supports MVC / Razor Pages, Angular, Blazor WebAssembly, Blazor Server, Blazor Web App, MAUI Blazor and No UI depending on the selected template. | Supports React or No UI. | +| Mobile choices | Keeps broader mobile choices, including MAUI and React Native in templates that support mobile applications. | Supports React Native or no mobile application. | +| Public website | Uses the classic public website structure where the selected template supports it. | Uses React-based public web assets where the selected modern template supports a public website. | +| Frontend assets | Uses the established ABP UI framework integrations and theme options for MVC, Angular and Blazor applications. | Uses newer React and `shadcn`-oriented frontend assets. | +| Best fit | Existing projects, teams using MVC / Angular / Blazor / MAUI, and scenarios that need the broadest UI framework choices. | New React-focused solutions and teams that prefer the newer Studio creation experience. | + +## Which One Should I Choose? + +Choose **Modern** if you are starting a new solution with React UI, want a React Native mobile application, prefer the newer architecture-first ABP Studio flow, or plan to use AI-assisted programming for an AI-driven project. + +Choose **Classic** if you want MVC / Razor Pages, Angular, Blazor, MAUI Blazor or MAUI mobile options, or if your team is following existing Classic-template tutorials, conventions or project structure. + +ABP Studio may show or hide some templates and options based on your license. For example, Microservice templates require a higher license level than regular application templates, and Modern application templates are not shown in the Community Edition. + +## See Also + +* [Solution Template Selection Guide](guide.md) +* [Startup Solution Templates](index.md) +* [Get Started](../get-started/index.md) diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Security/AbpComponentsClaimsCache.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Security/AbpComponentsClaimsCache.cs index c98edaef19..e5356da660 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Security/AbpComponentsClaimsCache.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Security/AbpComponentsClaimsCache.cs @@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Components.Web.Security; public class AbpComponentsClaimsCache : IScopedDependency { - public ClaimsPrincipal Principal { get; private set; } = default!; + public ClaimsPrincipal Principal { get; private set; } = new ClaimsPrincipal(new ClaimsIdentity()); private readonly AuthenticationStateProvider? _authenticationStateProvider; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs index ac6e44ed6c..35c62a626f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs @@ -42,14 +42,14 @@ public class AbpDateTimeModelBinder : IModelBinder _clock.SupportsMultipleTimezone && !_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace()) { + var timeZone = _currentTimezoneProvider.TimeZone; try { - var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone); - dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime; + dateTime = _timezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone); } catch { - _logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone); + _logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone); } } diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs index b339508028..f0dc0aa500 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs @@ -1,4 +1,8 @@ -namespace Volo.Abp.BackgroundJobs; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.BackgroundJobs; public class AbpBackgroundJobWorkerOptions { @@ -15,6 +19,7 @@ public class AbpBackgroundJobWorkerOptions /// /// Maximum count of jobs to fetch from data store in one loop. + /// Also used as the batch size for the retention cleanup deletions (see ). /// Default: 1000. /// public int MaxJobFetchCount { get; set; } @@ -42,7 +47,61 @@ public class AbpBackgroundJobWorkerOptions /// Distributed lock name for the worker. /// Default value: "AbpBackgroundJobWorker". /// - public string DistributedLockName { get; set; } + public string DistributedLockName { get; set; } + + /// + /// When set to true, a successfully completed job is kept (its is set) + /// instead of being deleted, so it can be used for auditing/history. Completed jobs are excluded from the + /// waiting jobs query and are removed by the retention cleanup (see ). + /// Default value: false (successful jobs are deleted). + /// + public bool StoreSuccessfulJobs { get; set; } + + /// + /// How long a kept (successfully completed) job is retained before the cleanup deletes it. + /// Only relevant when is true. Set to null to keep them forever (no automatic cleanup). + /// Default value: 7 days. + /// + public TimeSpan? SuccessfulJobRetentionTime { get; set; } + + /// + /// Interval (as milliseconds) between cleanup runs that delete retained successful jobs older than . + /// Default value: 3,600,000 (1 hour). + /// + public int CleanSuccessfulJobsPeriod { get; set; } + + /// + /// Distributed lock name for the cleanup worker. + /// Default value: "AbpBackgroundJobCleanup". + /// + public string CleanupDistributedLockName { get; set; } + + /// + /// Dedicated workers, each processing only the configured job types with its own distributed lock. + /// When this list is not empty, an additional default worker is started that processes all + /// other job types. When it is empty, a single default worker processes all job types (the default behavior). + /// Use to add a dedicated worker. + /// + public List WorkerConfigurations { get; } + + /// + /// Maximum number of jobs that a single worker executes in parallel within one poll cycle. + /// When it is 1 (default), jobs are executed sequentially under a single worker distributed lock + /// (the default behavior). When greater than 1, the worker distributed lock is not used; instead + /// each job is claimed with its own distributed lock so multiple application instances can execute + /// different jobs concurrently. + /// This value should be configured consistently across all application instances: mixing sequential + /// (worker lock) and parallel (per-job lock) instances removes the common mutual exclusion and may + /// let the same job run on more than one instance. + /// Default value: 1. + /// + public int MaxParallelJobExecutionCount { get; set; } + + /// + /// Prefix of the per-job distributed lock name used when is greater than 1. + /// Default value: "AbpBackgroundJob:". + /// + public string PerJobDistributedLockPrefix { get; set; } public AbpBackgroundJobWorkerOptions() { @@ -52,5 +111,97 @@ public class AbpBackgroundJobWorkerOptions DefaultTimeout = 172800; DefaultWaitFactor = 2.0; DistributedLockName = "AbpBackgroundJobWorker"; + WorkerConfigurations = new List(); + MaxParallelJobExecutionCount = 1; + PerJobDistributedLockPrefix = "AbpBackgroundJob:"; + SuccessfulJobRetentionTime = TimeSpan.FromDays(7); + CleanSuccessfulJobsPeriod = 3600000; + CleanupDistributedLockName = "AbpBackgroundJobCleanup"; + } + + /// + /// Adds a dedicated worker that processes only the given job argument types. + /// + /// A unique distributed lock name for this worker. + /// The job argument types handled exclusively by this worker. + public AbpBackgroundJobWorkerOptions AddDedicatedWorker(string lockName, params Type[] jobArgsTypes) + { + Check.NotNullOrEmpty(jobArgsTypes, nameof(jobArgsTypes)); + + var configuration = new BackgroundJobWorkerConfiguration(lockName, jobArgsTypes.Distinct().ToArray()); + + var duplicateType = configuration.JobArgsTypes.FirstOrDefault( + type => WorkerConfigurations.Any(c => c.JobArgsTypes.Contains(type))); + if (duplicateType != null) + { + throw new AbpException( + $"The background job args type '{duplicateType.FullName}' is already assigned to a dedicated worker. " + + $"Each job type can be handled by only one dedicated worker."); + } + + if (lockName == DistributedLockName || WorkerConfigurations.Any(c => c.LockName == lockName)) + { + throw new AbpException( + $"The distributed lock name '{lockName}' is already used by another background job worker. " + + $"Each worker must have a unique lock name."); + } + + WorkerConfigurations.Add(configuration); + return this; + } + + public AbpBackgroundJobWorkerOptions AddDedicatedWorker(string lockName) + { + return AddDedicatedWorker(lockName, typeof(TArgs)); + } + + public AbpBackgroundJobWorkerOptions AddDedicatedWorker(string lockName) + { + return AddDedicatedWorker(lockName, typeof(TArgs1), typeof(TArgs2)); + } + + public AbpBackgroundJobWorkerOptions AddDedicatedWorker(string lockName) + { + return AddDedicatedWorker(lockName, typeof(TArgs1), typeof(TArgs2), typeof(TArgs3)); + } + + /// + /// Adds a dedicated worker with a lock name derived from the given job argument type names. + /// Use the AddDedicatedWorker(string lockName, ...) overloads to set an explicit lock name. + /// + /// The job argument types handled exclusively by this worker. + public AbpBackgroundJobWorkerOptions AddDedicatedWorker(params Type[] jobArgsTypes) + { + return AddDedicatedWorker(GetDedicatedWorkerLockName(jobArgsTypes), jobArgsTypes); + } + + public AbpBackgroundJobWorkerOptions AddDedicatedWorker() + { + return AddDedicatedWorker(typeof(TArgs)); + } + + public AbpBackgroundJobWorkerOptions AddDedicatedWorker() + { + return AddDedicatedWorker(typeof(TArgs1), typeof(TArgs2)); + } + + public AbpBackgroundJobWorkerOptions AddDedicatedWorker() + { + return AddDedicatedWorker(typeof(TArgs1), typeof(TArgs2), typeof(TArgs3)); + } + + protected virtual string GetDedicatedWorkerLockName(Type[] jobArgsTypes) + { + Check.NotNullOrEmpty(jobArgsTypes, nameof(jobArgsTypes)); + + if (jobArgsTypes.Any(t => t == null)) + { + throw new ArgumentException("Job args types cannot contain null.", nameof(jobArgsTypes)); + } + + // Hash the (stable, sorted) full type names so the derived lock name stays short and within the + // length limits of distributed lock providers (e.g. SQL Server sp_getapplock is limited to 255 chars). + var key = string.Join(",", jobArgsTypes.Select(t => t.FullName).Distinct().OrderBy(n => n, StringComparer.Ordinal)); + return "AbpBackgroundJobDedicatedWorker:" + key.ToMd5(); } } diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs index b3601ad1be..0f4b20cd65 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.BackgroundWorkers; @@ -23,9 +23,15 @@ public class AbpBackgroundJobsModule : AbpModule { public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { - if (context.ServiceProvider.GetRequiredService>().Value.IsJobExecutionEnabled) + // The manager decides (based on options) whether and which workers to run. + await context.AddBackgroundWorkerAsync(); + + // Only register the cleanup worker when it has something to do (retained successful jobs). + var jobOptions = context.ServiceProvider.GetRequiredService>().Value; + var workerOptions = context.ServiceProvider.GetRequiredService>().Value; + if (jobOptions.IsJobExecutionEnabled && workerOptions.StoreSuccessfulJobs && workerOptions.SuccessfulJobRetentionTime != null) { - await context.AddBackgroundWorkerAsync(); + await context.AddBackgroundWorkerAsync(); } } diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker.cs new file mode 100644 index 0000000000..72c36b8ffb --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Periodically deletes retained successfully completed jobs older than +/// . +/// Only relevant when is enabled. +/// +public class BackgroundJobCleanupWorker : AsyncPeriodicBackgroundWorkerBase +{ + protected AbpBackgroundJobOptions JobOptions { get; } + + protected AbpBackgroundJobWorkerOptions WorkerOptions { get; } + + protected IAbpDistributedLock DistributedLock { get; } + + public BackgroundJobCleanupWorker( + AbpAsyncTimer timer, + IServiceScopeFactory serviceScopeFactory, + IOptions jobOptions, + IOptions workerOptions, + IAbpDistributedLock distributedLock) + : base(timer, serviceScopeFactory) + { + JobOptions = jobOptions.Value; + WorkerOptions = workerOptions.Value; + DistributedLock = distributedLock; + Timer.Period = WorkerOptions.CleanSuccessfulJobsPeriod; + } + + protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + { + if (!JobOptions.IsJobExecutionEnabled || + !WorkerOptions.StoreSuccessfulJobs || + WorkerOptions.SuccessfulJobRetentionTime == null) + { + return; + } + + var store = workerContext.ServiceProvider.GetRequiredService(); + var clock = workerContext.ServiceProvider.GetRequiredService(); + var completedBefore = clock.Now.Subtract(WorkerOptions.SuccessfulJobRetentionTime.Value); + + await using (var handle = await DistributedLock.TryAcquireAsync(WorkerOptions.CleanupDistributedLockName, cancellationToken: StoppingToken)) + { + if (handle == null) + { + return; + } + + int deletedCount; + do + { + deletedCount = await store.DeleteAsync(WorkerOptions.ApplicationName, completedBefore, WorkerOptions.MaxJobFetchCount, StoppingToken); + } + while (deletedCount > 0 && deletedCount >= WorkerOptions.MaxJobFetchCount && !StoppingToken.IsCancellationRequested); + } + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs index 4a9cec922c..51f73ae7b8 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs @@ -50,6 +50,14 @@ public class BackgroundJobInfo /// public virtual bool IsAbandoned { get; set; } + /// + /// The time this job was completed successfully. + /// When set, the job is kept as history and excluded from the waiting jobs query. + /// It is only set when is enabled; + /// otherwise successfully completed jobs are deleted. + /// + public virtual DateTime? CompletionTime { get; set; } + /// /// Priority of this job. /// diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilter.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilter.cs new file mode 100644 index 0000000000..06c0ebf398 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilter.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Filters the waiting jobs of a background job worker by job name. +/// A worker is exactly one of: no filter (), include-only (a dedicated worker) or +/// exclude-only (the default worker in a multi-worker setup) — the two can never be combined. +/// +public class BackgroundJobNameFilter +{ + /// + /// A filter that matches every job name. + /// + public static BackgroundJobNameFilter None { get; } = new(BackgroundJobNameFilterMode.None); + + public BackgroundJobNameFilterMode Mode { get; } + + public IReadOnlyList JobNames { get; } + + public BackgroundJobNameFilter(BackgroundJobNameFilterMode mode, IReadOnlyList? jobNames = null) + { + if (!Enum.IsDefined(typeof(BackgroundJobNameFilterMode), mode)) + { + throw new ArgumentException($"Invalid background job name filter mode: {mode}", nameof(mode)); + } + + var names = jobNames?.Where(x => !x.IsNullOrWhiteSpace()).Distinct(StringComparer.Ordinal).ToList() ?? new List(); + + if (mode == BackgroundJobNameFilterMode.None && names.Count > 0) + { + throw new ArgumentException("Job names must be empty when the filter mode is None.", nameof(jobNames)); + } + + if (mode != BackgroundJobNameFilterMode.None && names.Count == 0) + { + throw new ArgumentException("Job names cannot be empty when the filter mode is Include or Exclude.", nameof(jobNames)); + } + + Mode = mode; + JobNames = names.AsReadOnly(); + } + + public static BackgroundJobNameFilter Include(IReadOnlyList jobNames) + { + return new BackgroundJobNameFilter(BackgroundJobNameFilterMode.Include, jobNames); + } + + public static BackgroundJobNameFilter Exclude(IReadOnlyList jobNames) + { + return new BackgroundJobNameFilter(BackgroundJobNameFilterMode.Exclude, jobNames); + } + + /// + /// Whether the given job name passes this filter, using an ordinal (case-sensitive) comparison for the + /// in-memory eligibility re-check. The persistent stores translate and + /// into a database query instead, so their filtering follows the database collation. + /// Job names are expected to be unique beyond case (they are derived from the type name by default). + /// + public virtual bool IsMatch(string jobName) + { + return Mode switch + { + BackgroundJobNameFilterMode.Include => JobNames.Contains(jobName, StringComparer.Ordinal), + BackgroundJobNameFilterMode.Exclude => !JobNames.Contains(jobName, StringComparer.Ordinal), + _ => true + }; + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilterMode.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilterMode.cs new file mode 100644 index 0000000000..6ed2801b44 --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilterMode.cs @@ -0,0 +1,19 @@ +namespace Volo.Abp.BackgroundJobs; + +public enum BackgroundJobNameFilterMode : byte +{ + /// + /// No filter; all job names match. + /// + None = 0, + + /// + /// Only the job names in the filter match. + /// + Include = 1, + + /// + /// All job names except those in the filter match. + /// + Exclude = 2 +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs index a015e32d66..3053bb426f 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs @@ -1,17 +1,22 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DependencyInjection; using Volo.Abp.DistributedLocking; +using Volo.Abp.ExceptionHandling; using Volo.Abp.Threading; using Volo.Abp.Timing; namespace Volo.Abp.BackgroundJobs; -public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroundJobWorker +public class BackgroundJobWorker : IBackgroundJobWorker, ITransientDependency { protected AbpBackgroundJobOptions JobOptions { get; } @@ -19,95 +24,321 @@ public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroun protected IAbpDistributedLock DistributedLock { get; } + protected IServiceScopeFactory ServiceScopeFactory { get; } + + protected AbpAsyncTimer Timer { get; } + + public ILogger Logger { get; set; } + + protected string DistributedLockName { get; set; } = default!; + + protected BackgroundJobNameFilter JobNameFilter { get; set; } = BackgroundJobNameFilter.None; + + protected CancellationTokenSource StoppingTokenSource { get; } + + protected CancellationToken StoppingToken { get; } + public BackgroundJobWorker( AbpAsyncTimer timer, IOptions jobOptions, IOptions workerOptions, IServiceScopeFactory serviceScopeFactory, IAbpDistributedLock distributedLock) - : base( - timer, - serviceScopeFactory) { + Timer = timer; DistributedLock = distributedLock; + ServiceScopeFactory = serviceScopeFactory; WorkerOptions = workerOptions.Value; JobOptions = jobOptions.Value; + Logger = NullLogger.Instance; + Timer.Period = WorkerOptions.JobPollPeriod; + Timer.Elapsed = TimerOnElapsed; + + StoppingTokenSource = new CancellationTokenSource(); + StoppingToken = StoppingTokenSource.Token; + } + + public virtual Task StartAsync( + string? distributedLockName = null, + BackgroundJobNameFilter? jobNameFilter = null, + CancellationToken cancellationToken = default) + { + DistributedLockName = distributedLockName ?? WorkerOptions.DistributedLockName; + JobNameFilter = jobNameFilter ?? BackgroundJobNameFilter.None; + + Timer.Start(cancellationToken); + + return Task.CompletedTask; + } + + public virtual Task StopAsync(CancellationToken cancellationToken = default) + { + StoppingTokenSource.Cancel(); + Timer.Stop(cancellationToken); + StoppingTokenSource.Dispose(); + + return Task.CompletedTask; + } + + private async Task TimerOnElapsed(AbpAsyncTimer timer) + { + await RunAsync(); + } + + protected virtual async Task RunAsync() + { + using var scope = ServiceScopeFactory.CreateScope(); + + try + { + var workerContext = new PeriodicBackgroundWorkerContext(scope.ServiceProvider, StoppingToken); + + if (WorkerOptions.MaxParallelJobExecutionCount > 1) + { + await ExecuteJobsInParallelAsync(workerContext); + } + else + { + await ExecuteJobsWithWorkerLockAsync(workerContext); + } + } + catch (Exception ex) + { + await scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } } - protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) + protected virtual async Task ExecuteJobsWithWorkerLockAsync(PeriodicBackgroundWorkerContext workerContext) { - await using (var handler = await DistributedLock.TryAcquireAsync(WorkerOptions.DistributedLockName, cancellationToken: StoppingToken)) + await using (var handler = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: StoppingToken)) { if (handler != null) { - var store = workerContext.ServiceProvider.GetRequiredService(); + await ExecuteWaitingJobsAsync(workerContext); + } + else + { + await WaitForNextTryAsync(); + } + } + } + + protected virtual async Task ExecuteWaitingJobsAsync(PeriodicBackgroundWorkerContext workerContext) + { + var store = workerContext.ServiceProvider.GetRequiredService(); + + var waitingJobs = await GetWaitingJobsAsync(workerContext, store); - var waitingJobs = await store.GetWaitingJobsAsync(WorkerOptions.ApplicationName, WorkerOptions.MaxJobFetchCount); + if (!waitingJobs.Any()) + { + return; + } - if (!waitingJobs.Any()) + var jobExecuter = workerContext.ServiceProvider.GetRequiredService(); + var clock = workerContext.ServiceProvider.GetRequiredService(); + var serializer = workerContext.ServiceProvider.GetRequiredService(); + + foreach (var jobInfo in waitingJobs) + { + await TryExecuteJobAsync(workerContext, store, jobInfo, jobExecuter, clock, serializer); + } + } + + /// + /// Executes waiting jobs in parallel across application instances, up to + /// jobs per cycle. + /// + protected virtual async Task ExecuteJobsInParallelAsync(PeriodicBackgroundWorkerContext workerContext) + { + var store = workerContext.ServiceProvider.GetRequiredService(); + + var waitingJobs = await GetWaitingJobsAsync(workerContext, store); + + if (!waitingJobs.Any()) + { + return; + } + + var runningTasks = new List(); + + // Await already-started jobs even if acquiring a lock for a later job throws, + // so no claimed job is left running detached from this cycle. + try + { + foreach (var jobInfo in waitingJobs) + { + if (runningTasks.Count >= WorkerOptions.MaxParallelJobExecutionCount || StoppingToken.IsCancellationRequested) { - return; + break; } - var jobExecuter = workerContext.ServiceProvider.GetRequiredService(); - var clock = workerContext.ServiceProvider.GetRequiredService(); - var serializer = workerContext.ServiceProvider.GetRequiredService(); - - foreach (var jobInfo in waitingJobs) + var handle = await DistributedLock.TryAcquireAsync(GetPerJobDistributedLockName(jobInfo), cancellationToken: StoppingToken); + if (handle == null) { - jobInfo.TryCount++; - jobInfo.LastTryTime = clock.Now; - - try - { - var jobConfiguration = JobOptions.GetJob(jobInfo.JobName); - var jobArgs = serializer.Deserialize(jobInfo.JobArgs, jobConfiguration.ArgsType); - var context = new JobExecutionContext( - workerContext.ServiceProvider, - jobConfiguration.JobType, - jobArgs, - workerContext.CancellationToken); - - try - { - await jobExecuter.ExecuteAsync(context); - - await store.DeleteAsync(jobInfo.Id); - } - catch (BackgroundJobExecutionException) - { - var nextTryTime = CalculateNextTryTime(jobInfo, clock); - - if (nextTryTime.HasValue) - { - jobInfo.NextTryTime = nextTryTime.Value; - } - else - { - jobInfo.IsAbandoned = true; - } - - await TryUpdateAsync(store, jobInfo); - } - } - catch (Exception ex) - { - Logger.LogException(ex); - jobInfo.IsAbandoned = true; - await TryUpdateAsync(store, jobInfo); - } + // Another instance is already processing this job. + continue; } + + runningTasks.Add(ExecuteClaimedJobAsync(jobInfo, handle)); } - else + } + finally + { + await Task.WhenAll(runningTasks); + } + } + + protected virtual async Task ExecuteClaimedJobAsync(BackgroundJobInfo jobInfo, IAbpDistributedLockHandle handle) + { + await using (handle) + { + // Each concurrently executed job runs in its own service scope so that scoped services + // (e.g. the DbContext and the unit of work) are not shared across parallel jobs. + using var scope = ServiceScopeFactory.CreateScope(); + + try { - try + var workerContext = new PeriodicBackgroundWorkerContext(scope.ServiceProvider, StoppingToken); + var store = scope.ServiceProvider.GetRequiredService(); + var clock = scope.ServiceProvider.GetRequiredService(); + + // Re-read under the lock: another instance may have completed, abandoned or rescheduled this job + // between fetching the waiting list and acquiring the per-job lock. + var currentJobInfo = await store.FindAsync(jobInfo.Id); + if (!IsJobEligible(currentJobInfo, clock)) { - await Task.Delay(WorkerOptions.JobPollPeriod * 12, StoppingToken); + return; } - catch (TaskCanceledException) { } + + var jobExecuter = scope.ServiceProvider.GetRequiredService(); + var serializer = scope.ServiceProvider.GetRequiredService(); + + await TryExecuteJobAsync(workerContext, store, currentJobInfo, jobExecuter, clock, serializer); + } + catch (Exception ex) + { + await scope.ServiceProvider + .GetRequiredService() + .NotifyAsync(new ExceptionNotificationContext(ex)); + + Logger.LogException(ex); + } + } + } + + protected virtual bool IsJobEligible(BackgroundJobInfo? jobInfo, IClock clock) + { + return jobInfo != null && + jobInfo.ApplicationName == WorkerOptions.ApplicationName && + !jobInfo.IsAbandoned && + jobInfo.CompletionTime == null && + jobInfo.NextTryTime <= clock.Now && + JobNameFilter.IsMatch(jobInfo.JobName); + } + + protected virtual string GetPerJobDistributedLockName(BackgroundJobInfo jobInfo) + { + return WorkerOptions.PerJobDistributedLockPrefix + jobInfo.Id; + } + + protected virtual async Task> GetWaitingJobsAsync( + PeriodicBackgroundWorkerContext workerContext, + IBackgroundJobStore store) + { + return await store.GetWaitingJobsAsync( + WorkerOptions.ApplicationName, + WorkerOptions.MaxJobFetchCount, + JobNameFilter); + } + + protected virtual async Task TryExecuteJobAsync( + PeriodicBackgroundWorkerContext workerContext, + IBackgroundJobStore store, + BackgroundJobInfo jobInfo, + IBackgroundJobExecuter jobExecuter, + IClock clock, + IBackgroundJobSerializer serializer) + { + jobInfo.TryCount++; + jobInfo.LastTryTime = clock.Now; + + try + { + var jobConfiguration = JobOptions.GetJob(jobInfo.JobName); + var jobArgs = serializer.Deserialize(jobInfo.JobArgs, jobConfiguration.ArgsType); + var context = new JobExecutionContext( + workerContext.ServiceProvider, + jobConfiguration.JobType, + jobArgs, + workerContext.CancellationToken); + + try + { + await jobExecuter.ExecuteAsync(context); + + await HandleJobSuccessAsync(store, jobInfo, clock); + } + catch (BackgroundJobExecutionException) + { + await HandleJobFailureAsync(store, jobInfo, clock); } } + catch (Exception ex) + { + await HandleJobErrorAsync(store, jobInfo, ex); + } + } + + protected virtual async Task HandleJobSuccessAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, IClock clock) + { + if (WorkerOptions.StoreSuccessfulJobs) + { + // Keep the job as history: mark it completed instead of deleting. It is then excluded from the + // waiting jobs query and removed later by the retention cleanup. + jobInfo.CompletionTime = clock.Now; + await store.UpdateAsync(jobInfo); + } + else + { + await store.DeleteAsync(jobInfo.Id); + } + } + + protected virtual async Task HandleJobFailureAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, IClock clock) + { + var nextTryTime = CalculateNextTryTime(jobInfo, clock); + + if (nextTryTime.HasValue) + { + jobInfo.NextTryTime = nextTryTime.Value; + } + else + { + jobInfo.IsAbandoned = true; + } + + await TryUpdateAsync(store, jobInfo); + } + + protected virtual async Task HandleJobErrorAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, Exception ex) + { + Logger.LogException(ex); + jobInfo.IsAbandoned = true; + await TryUpdateAsync(store, jobInfo); + } + + protected virtual async Task WaitForNextTryAsync() + { + try + { + await Task.Delay(WorkerOptions.JobPollPeriod * 12, StoppingToken); + } + catch (TaskCanceledException) { } } protected virtual async Task TryUpdateAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo) diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerConfiguration.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerConfiguration.cs new file mode 100644 index 0000000000..848a2a6a1c --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerConfiguration.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Configuration of a dedicated that processes only specific job types. +/// +public class BackgroundJobWorkerConfiguration +{ + /// + /// A unique distributed lock name for this worker. It must be different from the names used by other workers. + /// It is used to serialize the worker across application instances when + /// is 1; in parallel mode + /// (greater than 1) jobs are claimed with per-job locks instead and this lock is not acquired. + /// + public string LockName { get; } + + /// + /// The job argument types that are processed exclusively by this worker. + /// + public IReadOnlyList JobArgsTypes { get; } + + public BackgroundJobWorkerConfiguration(string lockName, params Type[] jobArgsTypes) + { + LockName = Check.NotNullOrWhiteSpace(lockName, nameof(lockName)); + Check.NotNullOrEmpty(jobArgsTypes, nameof(jobArgsTypes)); + + if (jobArgsTypes.Any(t => t == null)) + { + throw new ArgumentException("Job args types cannot contain null.", nameof(jobArgsTypes)); + } + + JobArgsTypes = jobArgsTypes.ToList(); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerManager.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerManager.cs new file mode 100644 index 0000000000..929c4803da --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerManager.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.BackgroundWorkers; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Owns and controls the background job workers. +/// When no is configured, a single +/// default worker processes all jobs. Otherwise, one dedicated worker is started per configuration +/// (each with its own distributed lock and job-type filter) plus a default worker for the remaining jobs. +/// The workers are resolved from DI, so a replaced is respected. +/// +public class BackgroundJobWorkerManager : IBackgroundWorker +{ + protected AbpBackgroundJobOptions JobOptions { get; } + + protected AbpBackgroundJobWorkerOptions WorkerOptions { get; } + + protected IServiceProvider ServiceProvider { get; } + + protected List Workers { get; } + + public BackgroundJobWorkerManager( + IOptions jobOptions, + IOptions workerOptions, + IServiceProvider serviceProvider) + { + JobOptions = jobOptions.Value; + WorkerOptions = workerOptions.Value; + ServiceProvider = serviceProvider; + Workers = new List(); + } + + public virtual async Task StartAsync(CancellationToken cancellationToken = default) + { + if (!JobOptions.IsJobExecutionEnabled) + { + return; + } + + if (!WorkerOptions.WorkerConfigurations.Any()) + { + await StartWorkerAsync(cancellationToken: cancellationToken); + return; + } + + // AddDedicatedWorker already rejects duplicate job types and lock names eagerly. This is the backstop + // for what can only be known here: two different args types that resolve to the same job name. + // Validate all configurations first, so a misconfiguration does not leave already-started workers running. + var dedicatedWorkers = new List(); + var allDedicatedJobNames = new List(); + + // The default worker uses WorkerOptions.DistributedLockName, so dedicated workers must not reuse it. + var lockNames = new List { WorkerOptions.DistributedLockName }; + + foreach (var configuration in WorkerOptions.WorkerConfigurations) + { + var jobNames = configuration.JobArgsTypes + .Select(GetJobName) + .Distinct() + .ToList(); + + var alreadyConfigured = jobNames.Intersect(allDedicatedJobNames).ToList(); + if (alreadyConfigured.Any()) + { + throw new AbpException( + $"The following background job(s) are configured for more than one dedicated worker: {string.Join(", ", alreadyConfigured)}. " + + $"Each job type can be handled by only one dedicated worker."); + } + + if (lockNames.Contains(configuration.LockName)) + { + throw new AbpException( + $"The distributed lock name '{configuration.LockName}' is used by more than one background job worker " + + $"(the default worker uses '{WorkerOptions.DistributedLockName}'). Each worker must have a unique lock name to run independently."); + } + + lockNames.Add(configuration.LockName); + allDedicatedJobNames.AddRange(jobNames); + dedicatedWorkers.Add(new DedicatedWorkerDefinition(configuration.LockName, jobNames)); + } + + foreach (var dedicatedWorker in dedicatedWorkers) + { + await StartWorkerAsync(dedicatedWorker.LockName, BackgroundJobNameFilter.Include(dedicatedWorker.JobNames), cancellationToken); + } + + // Default worker processes every job that is not handled by a dedicated worker. + await StartWorkerAsync(null, BackgroundJobNameFilter.Exclude(allDedicatedJobNames), cancellationToken); + } + + protected virtual string GetJobName(Type argsType) + { + try + { + return JobOptions.GetJob(argsType).JobName; + } + catch (AbpException ex) + { + throw new AbpException( + $"No background job is registered for the args type '{argsType.FullName}' configured via AddDedicatedWorker. " + + $"Register the job before configuring a dedicated worker for it.", ex); + } + } + + protected virtual async Task StartWorkerAsync( + string? distributedLockName = null, + BackgroundJobNameFilter? jobNameFilter = null, + CancellationToken cancellationToken = default) + { + var worker = ServiceProvider.GetRequiredService(); + await worker.StartAsync(distributedLockName, jobNameFilter, cancellationToken); + Workers.Add(worker); + } + + public virtual async Task StopAsync(CancellationToken cancellationToken = default) + { + foreach (var worker in Workers) + { + await worker.StopAsync(cancellationToken); + } + + Workers.Clear(); + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DedicatedWorkerDefinition.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DedicatedWorkerDefinition.cs new file mode 100644 index 0000000000..ba98c2c1dc --- /dev/null +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DedicatedWorkerDefinition.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// A validated, ready-to-start dedicated worker: the distributed lock name it runs under and the +/// resolved job names it is responsible for. Built by from a +/// after all configurations have been validated. +/// +public class DedicatedWorkerDefinition +{ + /// + /// The distributed lock name this worker runs under. + /// + public string LockName { get; } + + /// + /// The resolved job names this worker is responsible for. + /// + public IReadOnlyList JobNames { get; } + + public DedicatedWorkerDefinition(string lockName, IReadOnlyList jobNames) + { + LockName = lockName; + JobNames = jobNames; + } +} diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs index 7fcf8cc5b6..421ec55434 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Volo.Abp.BackgroundJobs; @@ -24,7 +25,7 @@ public interface IBackgroundJobStore /// /// Gets waiting jobs. It should get jobs based on these: - /// Conditions: ApplicationName is applicationName And !IsAbandoned And NextTryTime <= Clock.Now. + /// Conditions: ApplicationName is applicationName And !IsAbandoned And CompletionTime == null And NextTryTime <= Clock.Now. /// Order by: Priority DESC, TryCount ASC, NextTryTime ASC. /// Maximum result: . /// @@ -32,12 +33,35 @@ public interface IBackgroundJobStore /// Maximum result count. Task> GetWaitingJobsAsync(string? applicationName, int maxResultCount); + /// + /// Gets waiting jobs (see ), additionally filtered by job name + /// according to . + /// + /// Application name. + /// Maximum result count. + /// Job name filter. When null, no job name filter is applied. + Task> GetWaitingJobsAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter); + /// /// Deletes a job. /// /// The Job Unique Identifier. Task DeleteAsync(Guid jobId); + /// + /// Deletes successfully completed jobs ( is set) of the given + /// application that completed before . Used by the retention cleanup. + /// + /// The number of deleted jobs. + Task DeleteAsync( + string? applicationName, + DateTime completedBefore, + int maxResultCount, + CancellationToken cancellationToken = default); + /// /// Updates a job. /// diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobWorker.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobWorker.cs index ecae732b8d..9aa4ac25a8 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobWorker.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobWorker.cs @@ -1,8 +1,27 @@ -using Volo.Abp.BackgroundWorkers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Volo.Abp.BackgroundJobs; -public interface IBackgroundJobWorker : IBackgroundWorker +/// +/// A background job worker that polls and executes waiting jobs. +/// Instances are created, configured and started by . +/// +public interface IBackgroundJobWorker { + /// + /// Starts this worker. + /// + /// + /// Distributed lock name for this worker. When null, is used. + /// + /// Filters the jobs this worker processes by name. When null, all jobs are processed. + /// Cancellation token. + Task StartAsync( + string? distributedLockName = null, + BackgroundJobNameFilter? jobNameFilter = null, + CancellationToken cancellationToken = default); + Task StopAsync(CancellationToken cancellationToken = default); } diff --git a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/InMemoryBackgroundJobStore.cs b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/InMemoryBackgroundJobStore.cs index 85916e8c37..d0fa34e7e8 100644 --- a/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/InMemoryBackgroundJobStore.cs +++ b/framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/InMemoryBackgroundJobStore.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Timing; @@ -37,9 +38,20 @@ public class InMemoryBackgroundJobStore : IBackgroundJobStore, ISingletonDepende public virtual Task> GetWaitingJobsAsync(string? applicationName, int maxResultCount) { + return GetWaitingJobsAsync(applicationName, maxResultCount, null); + } + + public virtual Task> GetWaitingJobsAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter) + { + var filter = jobNameFilter ?? BackgroundJobNameFilter.None; + var waitingJobs = _jobs.Values .Where(t => t.ApplicationName == applicationName) - .Where(t => !t.IsAbandoned && t.NextTryTime <= Clock.Now) + .Where(t => !t.IsAbandoned && t.CompletionTime == null && t.NextTryTime <= Clock.Now) + .Where(t => filter.IsMatch(t.JobName)) .OrderByDescending(t => t.Priority) .ThenBy(t => t.TryCount) .ThenBy(t => t.NextTryTime) @@ -57,6 +69,28 @@ public class InMemoryBackgroundJobStore : IBackgroundJobStore, ISingletonDepende return Task.CompletedTask; } + public virtual Task DeleteAsync( + string? applicationName, + DateTime completedBefore, + int maxResultCount, + CancellationToken cancellationToken = default) + { + var idsToDelete = _jobs.Values + .Where(t => t.ApplicationName == applicationName) + .Where(t => t.CompletionTime != null && t.CompletionTime < completedBefore) + .OrderBy(t => t.CompletionTime) + .Take(maxResultCount) + .Select(t => t.Id) + .ToList(); + + foreach (var id in idsToDelete) + { + _jobs.TryRemove(id, out _); + } + + return Task.FromResult(idsToDelete.Count); + } + public virtual Task UpdateAsync(BackgroundJobInfo jobInfo) { if (jobInfo.IsAbandoned) diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs index b993b006e3..a359a3ef10 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs @@ -1,10 +1,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; -using Volo.Abp.Users; namespace Volo.Abp.Http.Client.IdentityModel.Web; @@ -26,7 +24,7 @@ public class HttpContextAbpAccessTokenProvider : IAbpAccessTokenProvider, ITrans return null; } - if (!httpContext.RequestServices.GetRequiredService().IsAuthenticated) + if (httpContext.User.Identity?.IsAuthenticated != true) { return null; } diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs index 55c270d14e..3b278d5252 100644 --- a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs +++ b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs @@ -134,14 +134,14 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime); } + var timeZone = _currentTimezoneProvider.TimeZone; try { - var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone); - dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime; + dateTime = _timezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone); } catch { - Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone); + Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone); } return _skipDateTimeNormalization diff --git a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs index ee39a66678..02aaec00b2 100644 --- a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs +++ b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs @@ -101,14 +101,14 @@ public abstract class AbpDateTimeConverterBase : JsonConverter return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime); } + var timeZone = CurrentTimezoneProvider.TimeZone; try { - var timezoneInfo = TimezoneProvider.GetTimeZoneInfo(CurrentTimezoneProvider.TimeZone); - dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime; + dateTime = TimezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone); } catch { - Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", CurrentTimezoneProvider.TimeZone); + Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone); } return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime); diff --git a/framework/src/Volo.Abp.Maui.Client/Volo/Abp/Maui/Client/AbpMauiClientModule.cs b/framework/src/Volo.Abp.Maui.Client/Volo/Abp/Maui/Client/AbpMauiClientModule.cs index 046f1cc642..2472bfc40d 100644 --- a/framework/src/Volo.Abp.Maui.Client/Volo/Abp/Maui/Client/AbpMauiClientModule.cs +++ b/framework/src/Volo.Abp.Maui.Client/Volo/Abp/Maui/Client/AbpMauiClientModule.cs @@ -11,7 +11,7 @@ namespace Volo.Abp.Maui.Client; )] public class AbpMauiClientModule : AbpModule { - public async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { await context.ServiceProvider.GetRequiredService().ServiceProvider.GetRequiredService().InitializeAsync(); } diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorBase.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorBase.cs index 1d3e296e38..48e9801049 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorBase.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/CurrentPrincipalAccessorBase.cs @@ -8,7 +8,7 @@ public abstract class CurrentPrincipalAccessorBase : ICurrentPrincipalAccessor { public ClaimsPrincipal Principal => _currentPrincipal.Value ?? GetClaimsPrincipal(); - private readonly AsyncLocal _currentPrincipal = new AsyncLocal(); + private readonly AsyncLocal _currentPrincipal = new AsyncLocal(); protected abstract ClaimsPrincipal GetClaimsPrincipal(); @@ -19,10 +19,10 @@ public abstract class CurrentPrincipalAccessorBase : ICurrentPrincipalAccessor private IDisposable SetCurrent(ClaimsPrincipal principal) { - var parent = Principal; + var parent = _currentPrincipal.Value; _currentPrincipal.Value = principal; - return new DisposeAction, ClaimsPrincipal>>(static (state) => + return new DisposeAction, ClaimsPrincipal?>>(static (state) => { var (currentPrincipal, parent) = state; currentPrincipal.Value = parent; diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/ThreadCurrentPrincipalAccessor.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/ThreadCurrentPrincipalAccessor.cs index ab42441f98..99e8536eec 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/ThreadCurrentPrincipalAccessor.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/ThreadCurrentPrincipalAccessor.cs @@ -8,6 +8,12 @@ public class ThreadCurrentPrincipalAccessor : CurrentPrincipalAccessorBase, ISin { protected override ClaimsPrincipal GetClaimsPrincipal() { - return (Thread.CurrentPrincipal as ClaimsPrincipal)!; + var principal = Thread.CurrentPrincipal; + if (principal == null) + { + return new ClaimsPrincipal(new ClaimsIdentity()); + } + + return principal as ClaimsPrincipal ?? new ClaimsPrincipal(principal); } } diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs new file mode 100644 index 0000000000..29779a08be --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs @@ -0,0 +1,37 @@ +using System; + +namespace Volo.Abp.Timing; + +public static class TimezoneProviderExtensions +{ + /// + /// Interprets as local time in + /// and converts it to its UTC equivalent. The caller is expected to pass a + /// value; the kind is not inspected, so a value is always + /// treated as wall-clock time in the given timezone regardless of its kind. + /// + /// + /// Returns unchanged when applying the timezone offset would move it + /// outside the supported range. This happens for values within the offset + /// distance of /, typically the + /// placeholder that does not represent a real instant. Computing the + /// UTC ticks directly avoids the that + /// new DateTimeOffset(dateTime, offset) would throw for such values. + /// + public static DateTime ConvertUnspecifiedToUtc( + this ITimezoneProvider timezoneProvider, + DateTime dateTime, + string windowsOrIanaTimeZoneId) + { + Check.NotNull(timezoneProvider, nameof(timezoneProvider)); + + var timezoneInfo = timezoneProvider.GetTimeZoneInfo(windowsOrIanaTimeZoneId); + var utcTicks = dateTime.Ticks - timezoneInfo.GetUtcOffset(dateTime).Ticks; + if (utcTicks < DateTime.MinValue.Ticks || utcTicks > DateTime.MaxValue.Ticks) + { + return dateTime; + } + + return new DateTime(utcTicks, DateTimeKind.Utc); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpAutoLockNameWorkerTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpAutoLockNameWorkerTestModule.cs new file mode 100644 index 0000000000..38b640b84b --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpAutoLockNameWorkerTestModule.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpAutoLockNameWorkerTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + context.Services.Replace(ServiceDescriptor.Transient()); + + // No lock name is given: it is derived from the job argument type names. + Configure(options => + { + options.AddDedicatedWorker(); + options.AddDedicatedWorker(); + }); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobCleanupTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobCleanupTestModule.cs new file mode 100644 index 0000000000..b29c60784e --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobCleanupTestModule.cs @@ -0,0 +1,23 @@ +using System; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpBackgroundJobCleanupTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // IsJobExecutionEnabled is true by default, which the cleanup worker requires. + Configure(options => + { + options.StoreSuccessfulJobs = true; + options.SuccessfulJobRetentionTime = TimeSpan.FromDays(1); + }); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerTestModule.cs new file mode 100644 index 0000000000..db56843a4b --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerTestModule.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpBackgroundJobWorkerTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Drive the worker manually in tests; don't run the real periodic worker. + Configure(options => + { + options.IsJobExecutionEnabled = false; + }); + + context.Services.AddSingleton(); + context.Services.AddScoped(); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpDuplicateLockNameTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpDuplicateLockNameTestModule.cs new file mode 100644 index 0000000000..6e35c81dfc --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpDuplicateLockNameTestModule.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpDuplicateLockNameTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + context.Services.Replace(ServiceDescriptor.Transient()); + + // Two workers with different job types but the same lock name, which must fail at initialization. + Configure(options => + { + options.AddDedicatedWorker("dup-lock"); + options.AddDedicatedWorker("dup-lock"); + }); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpDuplicateWorkerTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpDuplicateWorkerTestModule.cs new file mode 100644 index 0000000000..f4d9aae208 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpDuplicateWorkerTestModule.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpDuplicateWorkerTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + context.Services.Replace(ServiceDescriptor.Transient()); + + // The same job type is assigned to two dedicated workers, which must fail at initialization. + Configure(options => + { + options.AddDedicatedWorker("lock-a"); + options.AddDedicatedWorker("lock-b"); + }); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpMultiWorkerTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpMultiWorkerTestModule.cs new file mode 100644 index 0000000000..d437c9325f --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpMultiWorkerTestModule.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpMultiWorkerTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + + // Replace the real worker with a recording one to assert how the manager resolves and starts workers. + context.Services.Replace(ServiceDescriptor.Transient()); + + Configure(options => + { + options.AddDedicatedWorker("lock-a"); + options.AddDedicatedWorker("lock-b"); + }); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpSameJobNameTestModule.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpSameJobNameTestModule.cs new file mode 100644 index 0000000000..f4ddbb8be8 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpSameJobNameTestModule.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Autofac; +using Volo.Abp.Modularity; + +namespace Volo.Abp.BackgroundJobs; + +[DependsOn( + typeof(AbpBackgroundJobsModule), + typeof(AbpAutofacModule), + typeof(AbpTestBaseModule) +)] +public class AbpSameJobNameTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + context.Services.Replace(ServiceDescriptor.Transient()); + + // Two dedicated workers with different args types that both resolve to "shared-job-name". + // AddDedicatedWorker's eager check compares by type (both pass); only the manager's backstop + // (which resolves job names) can catch this. + Configure(options => + { + options.AddDedicatedWorker("lock-a"); + options.AddDedicatedWorker("lock-b"); + }); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker_Tests.cs new file mode 100644 index 0000000000..a7a638ed7b --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker_Tests.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Testing; +using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Xunit; + +namespace Volo.Abp.BackgroundJobs; + +public class BackgroundJobCleanupWorker_Tests : AbpIntegratedTest +{ + private readonly IBackgroundJobStore _store; + private readonly IClock _clock; + private readonly AbpBackgroundJobWorkerOptions _workerOptions; + + public BackgroundJobCleanupWorker_Tests() + { + _store = GetRequiredService(); + _clock = GetRequiredService(); + _workerOptions = GetRequiredService>().Value; + } + + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + private TestableBackgroundJobCleanupWorker CreateWorker() + { + return new TestableBackgroundJobCleanupWorker( + GetRequiredService(), + GetRequiredService(), + GetRequiredService>(), + GetRequiredService>(), + GetRequiredService()); + } + + private Task RunCleanupAsync() + { + return CreateWorker().DoWorkPublicAsync(new PeriodicBackgroundWorkerContext(ServiceProvider)); + } + + private async Task InsertCompletedJobAsync(DateTime completionTime) + { + var id = Guid.NewGuid(); + await _store.InsertAsync(new BackgroundJobInfo + { + Id = id, + JobName = "job-a", + JobArgs = "{}", + CreationTime = _clock.Now, + NextTryTime = _clock.Now, + CompletionTime = completionTime + }); + return id; + } + + [Fact] + public async Task Should_Delete_Completed_Jobs_Older_Than_Retention() + { + // Retention is 1 day (configured in the module). + var oldJobId = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(2))); + var recentJobId = await InsertCompletedJobAsync(_clock.Now); + + await RunCleanupAsync(); + + (await _store.FindAsync(oldJobId)).ShouldBeNull(); // older than retention → deleted + (await _store.FindAsync(recentJobId)).ShouldNotBeNull(); // within retention → kept + } + + [Fact] + public async Task Should_Not_Delete_When_StoreSuccessfulJobs_Disabled() + { + _workerOptions.StoreSuccessfulJobs = false; + + var oldJobId = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(2))); + + await RunCleanupAsync(); + + (await _store.FindAsync(oldJobId)).ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Not_Delete_When_Retention_Is_Null() + { + _workerOptions.SuccessfulJobRetentionTime = null; + + var oldJobId = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(2))); + + await RunCleanupAsync(); + + (await _store.FindAsync(oldJobId)).ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Delete_All_Old_Jobs_In_Batches() + { + _workerOptions.MaxJobFetchCount = 2; + + var ids = new List(); + for (var i = 0; i < 5; i++) + { + ids.Add(await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(2)))); + } + + await RunCleanupAsync(); + + foreach (var id in ids) + { + (await _store.FindAsync(id)).ShouldBeNull(); + } + } + + [Fact] + public async Task Should_Delete_Oldest_Completed_Jobs_First_When_Limited_By_MaxResultCount() + { + var oldest = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(5))); + var middle = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(3))); + var newest = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(1))); + + // All three are completed before now, but a single call may delete only two. + var deletedCount = await _store.DeleteAsync(null, _clock.Now, maxResultCount: 2); + + deletedCount.ShouldBe(2); + (await _store.FindAsync(oldest)).ShouldBeNull(); + (await _store.FindAsync(middle)).ShouldBeNull(); + (await _store.FindAsync(newest)).ShouldNotBeNull(); // newest survives, proving oldest-first deletion + } + + [Fact] + public async Task Should_Not_Loop_Forever_When_MaxJobFetchCount_Is_Zero() + { + _workerOptions.MaxJobFetchCount = 0; + + var oldJobId = await InsertCompletedJobAsync(_clock.Now.Subtract(TimeSpan.FromDays(2))); + + // Must return (not hang) even though nothing can be fetched/deleted with a zero page size. + await RunCleanupAsync(); + + (await _store.FindAsync(oldJobId)).ShouldNotBeNull(); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorkerTestBase.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorkerTestBase.cs new file mode 100644 index 0000000000..2824c1a231 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorkerTestBase.cs @@ -0,0 +1,11 @@ +using Volo.Abp.Testing; + +namespace Volo.Abp.BackgroundJobs; + +public abstract class BackgroundJobWorkerTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_AutoLockName_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_AutoLockName_Tests.cs new file mode 100644 index 0000000000..76e4734b05 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_AutoLockName_Tests.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using Shouldly; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.BackgroundJobs; + +public class BackgroundJobWorker_AutoLockName_Tests : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + [Fact] + public void Should_Derive_Bounded_Lock_Name_From_Job_Args_Types() + { + var records = GetRequiredService().Records; + + var dedicated = records.Where(r => r.JobNameFilter?.Mode == BackgroundJobNameFilterMode.Include).ToList(); + dedicated.Count.ShouldBe(2); + + // Lock name is derived (prefix + MD5 of the full type name), so it is stable and length-bounded. + var expectedA = "AbpBackgroundJobDedicatedWorker:" + typeof(WorkerJobAArgs).FullName!.ToMd5(); + var expectedB = "AbpBackgroundJobDedicatedWorker:" + typeof(WorkerJobBArgs).FullName!.ToMd5(); + + dedicated.ShouldContain(r => r.DistributedLockName == expectedA); + dedicated.ShouldContain(r => r.DistributedLockName == expectedB); + + // Bounded length regardless of how long the type names are. + dedicated.ShouldAllBe(r => r.DistributedLockName!.Length <= 64); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_DuplicateConfiguration_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_DuplicateConfiguration_Tests.cs new file mode 100644 index 0000000000..bdd80efa7a --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_DuplicateConfiguration_Tests.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Autofac; +using Xunit; + +namespace Volo.Abp.BackgroundJobs; + +public class BackgroundJobWorker_DuplicateConfiguration_Tests +{ + [Fact] + public void Should_Throw_Without_Starting_Any_Worker_When_A_Job_Type_Is_Assigned_To_Multiple_Workers() + { + using var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + }); + + var exception = Record.Exception(() => application.Initialize()); + + exception.ShouldNotBeNull(); + exception.ToString().ShouldContain("dedicated worker"); + + // Validation must happen before any worker is started. + var recorder = application.ServiceProvider.GetRequiredService(); + recorder.Records.ShouldBeEmpty(); + } + + [Fact] + public void Should_Throw_Without_Starting_Any_Worker_When_Two_Workers_Share_A_Lock_Name() + { + using var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + }); + + var exception = Record.Exception(() => application.Initialize()); + + exception.ShouldNotBeNull(); + exception.ToString().ShouldContain("lock name"); + + var recorder = application.ServiceProvider.GetRequiredService(); + recorder.Records.ShouldBeEmpty(); + } + + [Fact] + public void Should_Throw_Without_Starting_Any_Worker_When_Different_Args_Types_Resolve_To_The_Same_Job_Name() + { + using var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + }); + + // The two args types pass the eager (by-type) check but resolve to the same job name, + // so only the manager's backstop validation can reject them. + var exception = Record.Exception(() => application.Initialize()); + + exception.ShouldNotBeNull(); + exception.ToString().ShouldContain("more than one dedicated worker"); + + var recorder = application.ServiceProvider.GetRequiredService(); + recorder.Records.ShouldBeEmpty(); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_MultiWorkerRegistration_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_MultiWorkerRegistration_Tests.cs new file mode 100644 index 0000000000..8279d2d493 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_MultiWorkerRegistration_Tests.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Shouldly; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.BackgroundJobs; + +public class BackgroundJobWorker_MultiWorkerRegistration_Tests : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } + + [Fact] + public void Should_Start_Dedicated_Workers_And_A_Default_Worker() + { + // The workers are resolved from DI (RecordingBackgroundJobWorker replaces the real one), + // proving the manager honors the registered/replaced IBackgroundJobWorker. + var records = GetRequiredService().Records; + + records.Count.ShouldBe(3); + + var jobAName = BackgroundJobNameAttribute.GetName(); + var jobBName = BackgroundJobNameAttribute.GetName(); + + var dedicated = records.Where(r => r.JobNameFilter?.Mode == BackgroundJobNameFilterMode.Include).ToList(); + dedicated.Count.ShouldBe(2); + dedicated.ShouldContain(r => r.DistributedLockName == "lock-a" && r.JobNameFilter!.JobNames.Contains(jobAName)); + dedicated.ShouldContain(r => r.DistributedLockName == "lock-b" && r.JobNameFilter!.JobNames.Contains(jobBName)); + + var defaultWorker = records.Single(r => r.JobNameFilter?.Mode == BackgroundJobNameFilterMode.Exclude); + defaultWorker.DistributedLockName.ShouldBeNull(); + defaultWorker.JobNameFilter!.JobNames.ShouldContain(jobAName); + defaultWorker.JobNameFilter!.JobNames.ShouldContain(jobBName); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_Tests.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_Tests.cs new file mode 100644 index 0000000000..4d8d506f19 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobWorker_Tests.cs @@ -0,0 +1,300 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Threading; +using Volo.Abp.Timing; +using Xunit; + +// ReSharper disable PossibleMultipleEnumeration + +namespace Volo.Abp.BackgroundJobs; + +public class BackgroundJobWorker_Tests : BackgroundJobWorkerTestBase +{ + private readonly IBackgroundJobStore _store; + private readonly IClock _clock; + private readonly AbpBackgroundJobWorkerOptions _workerOptions; + + public BackgroundJobWorker_Tests() + { + _store = GetRequiredService(); + _clock = GetRequiredService(); + _workerOptions = GetRequiredService>().Value; + } + + private TestableBackgroundJobWorker CreateWorker() + { + return new TestableBackgroundJobWorker( + GetRequiredService(), + GetRequiredService>(), + GetRequiredService>(), + GetRequiredService(), + GetRequiredService()); + } + + private BackgroundJobInfo NewJob(string jobName) + { + return new BackgroundJobInfo + { + Id = Guid.NewGuid(), + JobName = jobName, + JobArgs = "{}", + CreationTime = _clock.Now, + NextTryTime = _clock.Now.AddMinutes(-1) + }; + } + + private PeriodicBackgroundWorkerContext Context() + { + return new PeriodicBackgroundWorkerContext(ServiceProvider); + } + + // Storing successful jobs + + [Fact] + public async Task Should_Keep_Successful_Job_As_History_When_Enabled() + { + _workerOptions.StoreSuccessfulJobs = true; + + var jobInfo = NewJob("job-a"); + await _store.InsertAsync(jobInfo); + + await CreateWorker().HandleJobSuccessPublicAsync(_store, jobInfo, _clock); + + // The job is kept (marked completed), not deleted. + var kept = await _store.FindAsync(jobInfo.Id); + kept.ShouldNotBeNull(); + kept.CompletionTime.ShouldNotBeNull(); + + // ...but it is excluded from the waiting jobs. + (await _store.GetWaitingJobsAsync(null, 1000)).ShouldNotContain(j => j.Id == jobInfo.Id); + } + + [Fact] + public async Task Should_Delete_Successful_Job_When_Disabled() + { + // StoreSuccessfulJobs is false by default. + var jobInfo = NewJob("job-a"); + await _store.InsertAsync(jobInfo); + + await CreateWorker().HandleJobSuccessPublicAsync(_store, jobInfo, _clock); + + (await _store.FindAsync(jobInfo.Id)).ShouldBeNull(); + } + + // Dedicated workers by job name + + [Fact] + public async Task Should_Return_Only_Included_Jobs() + { + await _store.InsertAsync(NewJob("job-a")); + await _store.InsertAsync(NewJob("job-a")); + await _store.InsertAsync(NewJob("job-b")); + + var worker = CreateWorker(); + worker.ConfigureTest(BackgroundJobNameFilter.Include(new[] { "job-a" })); + + var jobs = await worker.GetWaitingJobsPublicAsync(Context(), _store); + + jobs.Count.ShouldBe(2); + jobs.ShouldAllBe(j => j.JobName == "job-a"); + } + + [Fact] + public async Task Should_Exclude_Given_Jobs() + { + await _store.InsertAsync(NewJob("job-a")); + await _store.InsertAsync(NewJob("job-b")); + + var worker = CreateWorker(); + worker.ConfigureTest(BackgroundJobNameFilter.Exclude(new[] { "job-a" })); + + var jobs = await worker.GetWaitingJobsPublicAsync(Context(), _store); + + jobs.Count.ShouldBe(1); + jobs.Single().JobName.ShouldBe("job-b"); + } + + [Fact] + public void Should_Match_Job_Names_By_Filter() + { + BackgroundJobNameFilter.None.IsMatch("any").ShouldBeTrue(); + + var include = BackgroundJobNameFilter.Include(new[] { "job-a" }); + include.IsMatch("job-a").ShouldBeTrue(); + include.IsMatch("job-b").ShouldBeFalse(); + + var exclude = BackgroundJobNameFilter.Exclude(new[] { "job-a" }); + exclude.IsMatch("job-a").ShouldBeFalse(); + exclude.IsMatch("job-b").ShouldBeTrue(); + } + + [Fact] + public void BackgroundJobNameFilter_Should_Reject_Invalid_Mode_And_Names_Combinations() + { + Should.Throw(() => new BackgroundJobNameFilter(BackgroundJobNameFilterMode.Include)); + Should.Throw(() => new BackgroundJobNameFilter(BackgroundJobNameFilterMode.None, new[] { "job-a" })); + Should.Throw(() => new BackgroundJobNameFilter((BackgroundJobNameFilterMode)99, new[] { "job-a" })); + } + + // Parallel execution / eligibility + + [Fact] + public void Should_Evaluate_Job_Eligibility() + { + var worker = CreateWorker(); + + worker.IsJobEligiblePublic(null, _clock).ShouldBeFalse(); + + var future = NewJob("job-a"); + future.NextTryTime = _clock.Now.AddMinutes(5); + worker.IsJobEligiblePublic(future, _clock).ShouldBeFalse(); + + var abandoned = NewJob("job-a"); + abandoned.IsAbandoned = true; + worker.IsJobEligiblePublic(abandoned, _clock).ShouldBeFalse(); + + var completed = NewJob("job-a"); + completed.CompletionTime = _clock.Now; + worker.IsJobEligiblePublic(completed, _clock).ShouldBeFalse(); + + var eligible = NewJob("job-a"); + worker.IsJobEligiblePublic(eligible, _clock).ShouldBeTrue(); + } + + [Fact] + public void Should_Not_Be_Eligible_When_Job_Name_Filtered_Out() + { + var worker = CreateWorker(); + worker.ConfigureTest(BackgroundJobNameFilter.Include(new[] { "job-a" })); + + var otherJob = NewJob("job-b"); + worker.IsJobEligiblePublic(otherJob, _clock).ShouldBeFalse(); + } + + [Fact] + public void Should_Require_Job_Args_Types_For_A_Dedicated_Worker() + { + Should.Throw(() => new BackgroundJobWorkerConfiguration("lock-a")); + } + + [Fact] + public void AddDedicatedWorker_Should_Throw_At_Registration_When_A_Job_Type_Is_Added_Twice() + { + var options = new AbpBackgroundJobWorkerOptions(); + options.AddDedicatedWorker("lock-a"); + + Should.Throw(() => options.AddDedicatedWorker("lock-b")); + } + + [Fact] + public void AddDedicatedWorker_Should_Throw_At_Registration_When_A_Lock_Name_Is_Reused() + { + var options = new AbpBackgroundJobWorkerOptions(); + options.AddDedicatedWorker("dup-lock"); + + Should.Throw(() => options.AddDedicatedWorker("dup-lock")); + } + + [Fact] + public void AddDedicatedWorker_Should_Throw_At_Registration_When_Lock_Name_Equals_The_Default() + { + var options = new AbpBackgroundJobWorkerOptions(); + + Should.Throw(() => options.AddDedicatedWorker(options.DistributedLockName)); + } + + // Parallel execution + + [Fact] + public async Task Should_Execute_Multiple_Jobs_In_Parallel() + { + _workerOptions.MaxParallelJobExecutionCount = 3; + + var jobManager = GetRequiredService(); + await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = "1" }); + await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = "2" }); + await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = "3" }); + + await CreateWorker().ExecuteJobsInParallelPublicAsync(Context()); + + var tracker = GetRequiredService(); + tracker.Executed.Count.ShouldBe(3); + tracker.Executed.ShouldContain("1"); + tracker.Executed.ShouldContain("2"); + tracker.Executed.ShouldContain("3"); + + // Each job must run in its own service scope (isolated DbContext/UOW). + tracker.ScopeIds.Distinct().Count().ShouldBe(3); + + (await _store.GetWaitingJobsAsync(null, 1000)).ShouldBeEmpty(); + } + + [Fact] + public async Task Should_Execute_At_Most_MaxParallel_Jobs_Per_Cycle() + { + _workerOptions.MaxParallelJobExecutionCount = 2; + + var jobManager = GetRequiredService(); + for (var i = 0; i < 5; i++) + { + await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = i.ToString() }); + } + + await CreateWorker().ExecuteJobsInParallelPublicAsync(Context()); + + var tracker = GetRequiredService(); + tracker.Executed.Count.ShouldBe(2); + (await _store.GetWaitingJobsAsync(null, 1000)).Count.ShouldBe(3); + } + + [Fact] + public async Task Should_Skip_Job_Already_Claimed_By_Another_Instance() + { + _workerOptions.MaxParallelJobExecutionCount = 5; + + var jobManager = GetRequiredService(); + var lockedJobId = Guid.Parse(await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = "locked" })); + await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = "free" }); + + var distributedLock = GetRequiredService(); + var lockName = _workerOptions.PerJobDistributedLockPrefix + lockedJobId; + + await using (await distributedLock.TryAcquireAsync(lockName)) + { + await CreateWorker().ExecuteJobsInParallelPublicAsync(Context()); + } + + var tracker = GetRequiredService(); + tracker.Executed.ShouldContain("free"); + tracker.Executed.ShouldNotContain("locked"); + (await _store.FindAsync(lockedJobId)).ShouldNotBeNull(); + } + + [Fact] + public async Task Should_Mark_Job_Completed_On_Successful_Execution_When_Storing_Enabled() + { + _workerOptions.StoreSuccessfulJobs = true; + _workerOptions.MaxParallelJobExecutionCount = 2; + + var jobManager = GetRequiredService(); + var jobId = Guid.Parse(await jobManager.EnqueueAsync(new ParallelTestJobArgs { Value = "1" })); + + await CreateWorker().ExecuteJobsInParallelPublicAsync(Context()); + + GetRequiredService().Executed.ShouldContain("1"); + + // The job is kept (marked completed), not deleted, and excluded from the waiting list. + var job = await _store.FindAsync(jobId); + job.ShouldNotBeNull(); + job.CompletionTime.ShouldNotBeNull(); + (await _store.GetWaitingJobsAsync(null, 1000)).ShouldNotContain(j => j.Id == jobId); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/RecordingBackgroundJobWorker.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/RecordingBackgroundJobWorker.cs new file mode 100644 index 0000000000..6acb5ca78d --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/RecordingBackgroundJobWorker.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Records every call so multi-worker registration +/// (resolved from DI by ) can be asserted. Does not start any timer. +/// +public class WorkerStartRecorder +{ + public List Records { get; } = new List(); +} + +public class WorkerStartRecord +{ + public string? DistributedLockName { get; set; } + + public BackgroundJobNameFilter? JobNameFilter { get; set; } +} + +public class RecordingBackgroundJobWorker : IBackgroundJobWorker, ITransientDependency +{ + private readonly WorkerStartRecorder _recorder; + + public RecordingBackgroundJobWorker(WorkerStartRecorder recorder) + { + _recorder = recorder; + } + + public Task StartAsync( + string? distributedLockName = null, + BackgroundJobNameFilter? jobNameFilter = null, + CancellationToken cancellationToken = default) + { + _recorder.Records.Add(new WorkerStartRecord + { + DistributedLockName = distributedLockName, + JobNameFilter = jobNameFilter + }); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/TestableBackgroundJobCleanupWorker.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/TestableBackgroundJobCleanupWorker.cs new file mode 100644 index 0000000000..7c6cc16dec --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/TestableBackgroundJobCleanupWorker.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Threading; + +namespace Volo.Abp.BackgroundJobs; + +public class TestableBackgroundJobCleanupWorker : BackgroundJobCleanupWorker +{ + public TestableBackgroundJobCleanupWorker( + AbpAsyncTimer timer, + IServiceScopeFactory serviceScopeFactory, + IOptions jobOptions, + IOptions workerOptions, + IAbpDistributedLock distributedLock) + : base(timer, serviceScopeFactory, jobOptions, workerOptions, distributedLock) + { + } + + public Task DoWorkPublicAsync(PeriodicBackgroundWorkerContext workerContext) + { + return DoWorkAsync(workerContext); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/TestableBackgroundJobWorker.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/TestableBackgroundJobWorker.cs new file mode 100644 index 0000000000..27274320fb --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/TestableBackgroundJobWorker.cs @@ -0,0 +1,55 @@ +#nullable enable +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Threading; +using Volo.Abp.Timing; + +namespace Volo.Abp.BackgroundJobs; + +/// +/// Exposes the protected members of for unit testing. +/// +public class TestableBackgroundJobWorker : BackgroundJobWorker +{ + public TestableBackgroundJobWorker( + AbpAsyncTimer timer, + IOptions jobOptions, + IOptions workerOptions, + IServiceScopeFactory serviceScopeFactory, + IAbpDistributedLock distributedLock) + : base(timer, jobOptions, workerOptions, serviceScopeFactory, distributedLock) + { + } + + public void ConfigureTest( + BackgroundJobNameFilter? jobNameFilter = null, + string? distributedLockName = null) + { + JobNameFilter = jobNameFilter ?? BackgroundJobNameFilter.None; + DistributedLockName = distributedLockName ?? WorkerOptions.DistributedLockName; + } + + public Task HandleJobSuccessPublicAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, IClock clock) + { + return HandleJobSuccessAsync(store, jobInfo, clock); + } + + public Task ExecuteJobsInParallelPublicAsync(PeriodicBackgroundWorkerContext workerContext) + { + return ExecuteJobsInParallelAsync(workerContext); + } + + public Task> GetWaitingJobsPublicAsync(PeriodicBackgroundWorkerContext workerContext, IBackgroundJobStore store) + { + return GetWaitingJobsAsync(workerContext, store); + } + + public bool IsJobEligiblePublic(BackgroundJobInfo? jobInfo, IClock clock) + { + return IsJobEligible(jobInfo, clock); + } +} diff --git a/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/WorkerTestJobs.cs b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/WorkerTestJobs.cs new file mode 100644 index 0000000000..dd9feec657 --- /dev/null +++ b/framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/WorkerTestJobs.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs; + +public class ParallelJobTracker +{ + public ConcurrentBag Executed { get; } = new ConcurrentBag(); + + public ConcurrentBag ScopeIds { get; } = new ConcurrentBag(); +} + +/// +/// Scoped service used to verify that each parallel job runs in its own service scope. +/// +public class ScopeMarker +{ + public Guid Id { get; } = Guid.NewGuid(); +} + +public class ParallelTestJobArgs +{ + public string Value { get; set; } = default!; +} + +public class ParallelTestJob : AsyncBackgroundJob, ITransientDependency +{ + private readonly ParallelJobTracker _tracker; + private readonly ScopeMarker _scopeMarker; + + public ParallelTestJob(ParallelJobTracker tracker, ScopeMarker scopeMarker) + { + _tracker = tracker; + _scopeMarker = scopeMarker; + } + + public override Task ExecuteAsync(ParallelTestJobArgs args) + { + _tracker.Executed.Add(args.Value); + _tracker.ScopeIds.Add(_scopeMarker.Id); + return Task.CompletedTask; + } +} + +public class WorkerJobAArgs +{ + public string Value { get; set; } = default!; +} + +public class WorkerJobA : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(WorkerJobAArgs args) + { + return Task.CompletedTask; + } +} + +public class WorkerJobBArgs +{ + public string Value { get; set; } = default!; +} + +public class WorkerJobB : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(WorkerJobBArgs args) + { + return Task.CompletedTask; + } +} + +// Two different args types that resolve to the same job name, to exercise the manager's +// backstop validation (eager AddDedicatedWorker validation compares by type, not resolved name). +[BackgroundJobName("shared-job-name")] +public class SharedNameJobAArgs +{ +} + +[BackgroundJobName("shared-job-name")] +public class SharedNameJobBArgs +{ +} + +public class SharedNameJobA : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(SharedNameJobAArgs args) + { + return Task.CompletedTask; + } +} + +public class SharedNameJobB : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(SharedNameJobBArgs args) + { + return Task.CompletedTask; + } +} diff --git a/framework/test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider_Tests.cs b/framework/test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider_Tests.cs new file mode 100644 index 0000000000..f2a79016aa --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider_Tests.cs @@ -0,0 +1,74 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Http.Client.Authentication; +using Volo.Abp.Http.Client.IdentityModel.Web.Tests; +using Volo.Abp.Security.Claims; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Http.Client.IdentityModel.Web; + +public class HttpContextAbpAccessTokenProvider_Tests : AbpIntegratedTest +{ + private readonly IAbpAccessTokenProvider _accessTokenProvider; + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpContextAbpAccessTokenProvider_Tests() + { + _accessTokenProvider = GetRequiredService(); + _httpContextAccessor = GetRequiredService(); + } + + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.Services.AddHttpContextAccessor(); + } + + [Fact] + public async Task Should_Forward_Token_For_Authenticated_User() + { + var identity = new ClaimsIdentity(TestTokenAuthHandler.SchemeName); + identity.AddClaim(new Claim(AbpClaimTypes.UserId, "d3c1a6b0-1234-4a6f-9e3b-8c2f1d4a5b6e")); + _httpContextAccessor.HttpContext = CreateHttpContext(new ClaimsPrincipal(identity)); + + (await _accessTokenProvider.GetTokenAsync()).ShouldBe(TestTokenAuthHandler.TestAccessToken); + } + + [Fact] + public async Task Should_Forward_Token_For_Authenticated_Client_Without_User() + { + // client_credentials token: authenticated identity but no user id claim. + var identity = new ClaimsIdentity(TestTokenAuthHandler.SchemeName); + identity.AddClaim(new Claim(AbpClaimTypes.ClientId, "test-client")); + _httpContextAccessor.HttpContext = CreateHttpContext(new ClaimsPrincipal(identity)); + + (await _accessTokenProvider.GetTokenAsync()).ShouldBe(TestTokenAuthHandler.TestAccessToken); + } + + [Fact] + public async Task Should_Not_Forward_Token_For_Anonymous_Request() + { + // No authentication type => Identity.IsAuthenticated is false (e.g. before authentication middleware). + _httpContextAccessor.HttpContext = CreateHttpContext(new ClaimsPrincipal(new ClaimsIdentity())); + + (await _accessTokenProvider.GetTokenAsync()).ShouldBeNull(); + } + + private static DefaultHttpContext CreateHttpContext(ClaimsPrincipal user) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddAuthentication(TestTokenAuthHandler.SchemeName) + .AddScheme(TestTokenAuthHandler.SchemeName, _ => { }); + + return new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider(), + User = user + }; + } +} diff --git a/framework/test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo/Abp/Http/Client/IdentityModel/Web/TestTokenAuthHandler.cs b/framework/test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo/Abp/Http/Client/IdentityModel/Web/TestTokenAuthHandler.cs new file mode 100644 index 0000000000..2bc41d239d --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.IdentityModel.Web.Tests/Volo/Abp/Http/Client/IdentityModel/Web/TestTokenAuthHandler.cs @@ -0,0 +1,32 @@ +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Volo.Abp.Http.Client.IdentityModel.Web; + +public class TestTokenAuthHandler : AuthenticationHandler +{ + public const string SchemeName = "TestToken"; + public const string TestAccessToken = "test-access-token"; + + public TestTokenAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override Task HandleAuthenticateAsync() + { + // The current HttpContext.User already carries the principal set up by the test. + // This handler only makes an "access_token" retrievable via HttpContext.GetTokenAsync. + var properties = new AuthenticationProperties(); + properties.StoreTokens(new[] { new AuthenticationToken { Name = "access_token", Value = TestAccessToken } }); + + var ticket = new AuthenticationTicket(Context.User, properties, SchemeName); + return Task.FromResult(AuthenticateResult.Success(ticket)); + } +} diff --git a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs new file mode 100644 index 0000000000..202c1d59e2 --- /dev/null +++ b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Shouldly; +using Volo.Abp.Timing; +using Xunit; + +namespace Volo.Abp.Json; + +/// +/// Regression tests for the warning +/// "Could not convert DateTime with unspecified Kind using timezone '...'." +/// logged by AbpDateTimeConverterBase.Normalize. +/// +/// When is the converter treats +/// an value as local time in the current user's timezone and +/// converts it to UTC. For a value within the offset distance of / +/// (most commonly the placeholder) a +/// non-zero offset pushes the UTC equivalent outside the supported range, which used to be swallowed +/// and logged as a warning on every serialization. The converter now detects this boundary case and +/// keeps the value unchanged, so no warning is emitted while the serialized output stays the same. +/// +public class AbpDateTimeConverterTimezone_Tests : AbpJsonSystemTextJsonTestBase +{ + private const string WarningFragment = "Could not convert DateTime with unspecified Kind"; + + private static readonly CapturingLoggerProvider LogCapture = new(); + + private readonly IJsonSerializer _jsonSerializer; + private readonly ICurrentTimezoneProvider _currentTimezoneProvider; + + public AbpDateTimeConverterTimezone_Tests() + { + _jsonSerializer = GetRequiredService(); + _currentTimezoneProvider = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + // The warning only happens with a UTC clock, where Unspecified values get converted to UTC. + services.Configure(options => options.Kind = DateTimeKind.Utc); + + LogCapture.Clear(); + services.AddSingleton(LogCapture); + + base.AfterAddApplication(services); + } + + private sealed class FileModel + { + public DateTime DateModified { get; set; } + + public DateTime DateCreated { get; set; } + } + + [Theory] + [InlineData("Asia/Shanghai")] // +08:00 + [InlineData("Europe/Brussels")] // +01:00 / +02:00 + public void Should_Not_Warn_When_Serializing_MinValue_Under_Positive_Offset_Timezone(string timeZoneId) + { + _currentTimezoneProvider.TimeZone = timeZoneId; + LogCapture.Clear(); + + DateTime.MinValue.Kind.ShouldBe(DateTimeKind.Unspecified); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = DateTime.MinValue, + DateCreated = DateTime.MinValue + }); + + // The placeholder is serialized unchanged and no warning is logged. + json.ShouldContain("0001-01-01"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Not_Warn_When_Serializing_Value_Near_MinValue_Under_Positive_Offset_Timezone() + { + // Not exactly DateTime.MinValue: any value within the offset distance of the lower bound + // overflows the same way, so the converter must absorb it rather than warn. + _currentTimezoneProvider.TimeZone = "Asia/Shanghai"; // +08:00 + LogCapture.Clear(); + + var nearMin = DateTime.MinValue.AddHours(3); // 0001-01-01T03:00:00 - 08:00 underflows + + _jsonSerializer.Serialize(new FileModel + { + DateModified = nearMin, + DateCreated = nearMin + }); + + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Not_Warn_When_Serializing_MaxValue_Under_Negative_Offset_Timezone() + { + // The symmetric case: a negative offset would push MaxValue past DateTime.MaxValue. + _currentTimezoneProvider.TimeZone = "America/New_York"; + LogCapture.Clear(); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = DateTime.MaxValue, + DateCreated = DateTime.MaxValue + }); + + json.ShouldContain("9999-12-31"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Not_Warn_When_Serializing_Real_Utc_Timestamp_Under_Positive_Offset_Timezone() + { + _currentTimezoneProvider.TimeZone = "Asia/Shanghai"; + LogCapture.Clear(); + + var utc = new DateTime(2026, 6, 27, 10, 28, 7, DateTimeKind.Utc); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = utc, + DateCreated = utc + }); + + json.ShouldContain("2026-06-27T10:28:07Z"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Still_Convert_Real_Unspecified_Timestamp_To_Utc_Under_Positive_Offset_Timezone() + { + // A genuine (non-sentinel) Unspecified value must still be converted to UTC using the offset. + _currentTimezoneProvider.TimeZone = "Asia/Shanghai"; // +08:00 + LogCapture.Clear(); + + var unspecified = new DateTime(2026, 6, 27, 18, 0, 0, DateTimeKind.Unspecified); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = unspecified, + DateCreated = unspecified + }); + + // 18:00 in +08:00 == 10:00 UTC. + json.ShouldContain("2026-06-27T10:00:00Z"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + private sealed class CapturingLoggerProvider : ILoggerProvider + { + public ConcurrentQueue Warnings { get; } = new(); + + public void Clear() + { + Warnings.Clear(); + } + + public ILogger CreateLogger(string categoryName) + { + return new CapturingLogger(Warnings); + } + + public void Dispose() + { + } + + private sealed class CapturingLogger : ILogger + { + private readonly ConcurrentQueue _sink; + + public CapturingLogger(ConcurrentQueue sink) + { + _sink = sink; + } + + public IDisposable BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= LogLevel.Warning; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) + { + if (logLevel >= LogLevel.Warning) + { + _sink.Enqueue(formatter(state, exception)); + } + } + } + } +} diff --git a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/CurrentPrincipalAccessor_Tests.cs b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/CurrentPrincipalAccessor_Tests.cs index bbc18089be..6480af9c74 100644 --- a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/CurrentPrincipalAccessor_Tests.cs +++ b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/CurrentPrincipalAccessor_Tests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Security.Claims; +using System.Threading; using Shouldly; using Volo.Abp.Testing; using Xunit; @@ -30,20 +31,71 @@ public class CurrentPrincipalAccessor_Tests : AbpIntegratedTest + { + new Claim(ClaimTypes.NameIdentifier, "123456") + })); + + using (accessor.Change(changedPrincipal)) + { + accessor.Principal.ShouldBe(changedPrincipal); + } + + // Disposing the Change scope must not pin the accessor to the fallback principal; + // a principal that becomes available afterwards has to be reflected. + var sourcePrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim(ClaimTypes.NameIdentifier, "654321") + })); + accessor.SourcePrincipal = sourcePrincipal; + + accessor.Principal.ShouldBe(sourcePrincipal); + } + + private class TestCurrentPrincipalAccessor : CurrentPrincipalAccessorBase + { + public ClaimsPrincipal? SourcePrincipal { get; set; } + + protected override ClaimsPrincipal GetClaimsPrincipal() + { + return SourcePrincipal ?? new ClaimsPrincipal(new ClaimsIdentity()); } - _currentPrincipalAccessor.Principal.ShouldBeNull(); } } diff --git a/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs new file mode 100644 index 0000000000..6707eb8b82 --- /dev/null +++ b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs @@ -0,0 +1,60 @@ +using System; +using Shouldly; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Timing; + +public class TimezoneProviderExtensions_Tests : AbpIntegratedTest +{ + private readonly ITimezoneProvider _timezoneProvider; + + public TimezoneProviderExtensions_Tests() + { + _timezoneProvider = GetRequiredService(); + } + + [Theory] + [InlineData("Asia/Shanghai")] // +08:00 + [InlineData("Europe/Brussels")] // +01:00 / +02:00 + public void Should_Keep_MinValue_Unchanged_Under_Positive_Offset(string timeZoneId) + { + // A positive offset would push DateTime.MinValue below the supported range; keep it as-is. + var result = _timezoneProvider.ConvertUnspecifiedToUtc(DateTime.MinValue, timeZoneId); + + result.ShouldBe(DateTime.MinValue); + result.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public void Should_Keep_Value_Near_MinValue_Unchanged_Under_Positive_Offset() + { + var nearMin = DateTime.MinValue.AddHours(3); // 0001-01-01T03:00:00 - 08:00 underflows + + var result = _timezoneProvider.ConvertUnspecifiedToUtc(nearMin, "Asia/Shanghai"); + + result.ShouldBe(nearMin); + result.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public void Should_Keep_MaxValue_Unchanged_Under_Negative_Offset() + { + var result = _timezoneProvider.ConvertUnspecifiedToUtc(DateTime.MaxValue, "America/New_York"); + + result.ShouldBe(DateTime.MaxValue); + result.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public void Should_Convert_Unspecified_Value_To_Utc_Using_Offset() + { + var unspecified = new DateTime(2026, 6, 27, 18, 0, 0, DateTimeKind.Unspecified); + + var result = _timezoneProvider.ConvertUnspecifiedToUtc(unspecified, "Asia/Shanghai"); // +08:00 + + // 18:00 in +08:00 == 10:00 UTC. + result.ShouldBe(new DateTime(2026, 6, 27, 10, 0, 0, DateTimeKind.Utc)); + result.Kind.ShouldBe(DateTimeKind.Utc); + } +} diff --git a/latest-versions.json b/latest-versions.json index 5b6987a34f..79868e94a9 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,4 +1,13 @@ [ + { + "version": "10.5.0", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "5.5.0" + } + }, { "version": "10.4.1", "releaseDate": "", diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/DemoAppModule.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/DemoAppModule.cs index 976705b07d..5569f6800a 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/DemoAppModule.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/DemoAppModule.cs @@ -1,5 +1,7 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Autofac; +using Volo.Abp.BackgroundJobs.DemoApp.Jobs; using Volo.Abp.BackgroundJobs.DemoApp.Shared; using Volo.Abp.BackgroundJobs.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -32,17 +34,31 @@ public class DemoAppModule : AbpModule options.JobPollPeriod = 1000; options.DefaultFirstWaitDuration = 1; options.DefaultWaitFactor = 1; + + // Keep every successfully completed job as history (marks CompletionTime instead of deleting). + // Completed jobs are excluded from the waiting query and pruned after SuccessfulJobRetentionTime. + options.StoreSuccessfulJobs = true; + options.SuccessfulJobRetentionTime = System.TimeSpan.FromDays(1); + + // A dedicated worker (with its own distributed lock "DemoFeesWorkerLock") that only processes + // the slow fee-calculation jobs, so they don't block other jobs. A default worker is added automatically + // and processes all the remaining job types (e.g. SendEmailJob). + options.AddDedicatedWorker("DemoFeesWorkerLock"); + + // Let each worker execute up to 4 jobs in parallel (each job claimed with its own distributed lock, + // so multiple application instances can execute different jobs concurrently). + options.MaxParallelJobExecutionCount = 4; }); } - public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context) + public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { - //TODO: Configure console logging - //context - // .ServiceProvider - // .GetRequiredService() - // .AddConsole(LogLevel.Debug); + // Enqueue a few demo jobs. The fee-calculation jobs are handled by the dedicated worker, + // while SendEmailJob is handled by the default worker. + var backgroundJobManager = context.ServiceProvider.GetRequiredService(); - return Task.CompletedTask; + await backgroundJobManager.EnqueueAsync(new CalculateAwsFeesJobArgs { AccountId = "acc-1" }); + await backgroundJobManager.EnqueueAsync(new CalculateAzureFeesJobArgs { SubscriptionId = "sub-1" }); + await backgroundJobManager.EnqueueAsync(new SendEmailJobArgs { To = "user@example.com", Subject = "Welcome" }); } } diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/CalculateAwsFeesJob.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/CalculateAwsFeesJob.cs new file mode 100644 index 0000000000..2262bbc68d --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/CalculateAwsFeesJob.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs.DemoApp.Jobs; + +public class CalculateAwsFeesJobArgs +{ + public string AccountId { get; set; } = default!; +} + +public class CalculateAwsFeesJob : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(CalculateAwsFeesJobArgs args) + { + // A slow, resource-intensive job that is isolated on a dedicated worker (see DemoAppModule). + Logger.LogInformation($"[AWS fees] Calculating fees for account '{args.AccountId}'..."); + return Task.CompletedTask; + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/CalculateAzureFeesJob.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/CalculateAzureFeesJob.cs new file mode 100644 index 0000000000..d0dc43cd94 --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/CalculateAzureFeesJob.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs.DemoApp.Jobs; + +public class CalculateAzureFeesJobArgs +{ + public string SubscriptionId { get; set; } = default!; +} + +public class CalculateAzureFeesJob : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(CalculateAzureFeesJobArgs args) + { + // Isolated on the same dedicated fees worker as CalculateAwsFeesJob. + Logger.LogInformation($"[Azure fees] Calculating fees for subscription '{args.SubscriptionId}'..."); + return Task.CompletedTask; + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/SendEmailJob.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/SendEmailJob.cs new file mode 100644 index 0000000000..846b06f4fe --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Jobs/SendEmailJob.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.BackgroundJobs.DemoApp.Jobs; + +public class SendEmailJobArgs +{ + public string To { get; set; } = default!; + + public string Subject { get; set; } = default!; +} + +public class SendEmailJob : AsyncBackgroundJob, ITransientDependency +{ + public override Task ExecuteAsync(SendEmailJobArgs args) + { + // A fast job. It is NOT configured for a dedicated worker, so the default worker processes it + // without waiting behind the slow fee-calculation jobs. + Logger.LogInformation($"[Email] Sending '{args.Subject}' to '{args.To}'..."); + return Task.CompletedTask; + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260701082002_Added_CompletionTime_To_BackgroundJobs.Designer.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260701082002_Added_CompletionTime_To_BackgroundJobs.Designer.cs new file mode 100644 index 0000000000..01241ef94a --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260701082002_Added_CompletionTime_To_BackgroundJobs.Designer.cs @@ -0,0 +1,99 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.BackgroundJobs.DemoApp.Db; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations +{ + [DbContext(typeof(DemoAppDbContext))] + [Migration("20260701082002_Added_CompletionTime_To_BackgroundJobs")] + partial class Added_CompletionTime_To_BackgroundJobs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Volo.Abp.BackgroundJobs.BackgroundJobRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApplicationName") + .HasMaxLength(96) + .HasColumnType("nvarchar(96)"); + + b.Property("CompletionTime") + .HasColumnType("datetime2"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("IsAbandoned") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("JobArgs") + .IsRequired() + .HasMaxLength(1048576) + .HasColumnType("nvarchar(max)"); + + b.Property("JobName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("LastTryTime") + .HasColumnType("datetime2"); + + b.Property("NextTryTime") + .HasColumnType("datetime2"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint") + .HasDefaultValue((byte)15); + + b.Property("TryCount") + .ValueGeneratedOnAdd() + .HasColumnType("smallint") + .HasDefaultValue((short)0); + + b.HasKey("Id"); + + b.HasIndex("ApplicationName", "CompletionTime", "IsAbandoned", "NextTryTime"); + + b.ToTable("AbpBackgroundJobs", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260701082002_Added_CompletionTime_To_BackgroundJobs.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260701082002_Added_CompletionTime_To_BackgroundJobs.cs new file mode 100644 index 0000000000..96f35f3379 --- /dev/null +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/20260701082002_Added_CompletionTime_To_BackgroundJobs.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations +{ + /// + public partial class Added_CompletionTime_To_BackgroundJobs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AbpBackgroundJobs_IsAbandoned_NextTryTime", + table: "AbpBackgroundJobs"); + + migrationBuilder.AddColumn( + name: "CompletionTime", + table: "AbpBackgroundJobs", + type: "datetime2", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_AbpBackgroundJobs_ApplicationName_CompletionTime_IsAbandoned_NextTryTime", + table: "AbpBackgroundJobs", + columns: new[] { "ApplicationName", "CompletionTime", "IsAbandoned", "NextTryTime" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AbpBackgroundJobs_ApplicationName_CompletionTime_IsAbandoned_NextTryTime", + table: "AbpBackgroundJobs"); + + migrationBuilder.DropColumn( + name: "CompletionTime", + table: "AbpBackgroundJobs"); + + migrationBuilder.CreateIndex( + name: "IX_AbpBackgroundJobs_IsAbandoned_NextTryTime", + table: "AbpBackgroundJobs", + columns: new[] { "IsAbandoned", "NextTryTime" }); + } + } +} diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs index ab91bc354c..af43eb368f 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Migrations/DemoAppDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("ProductVersion", "10.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -34,6 +34,9 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations .HasMaxLength(96) .HasColumnType("nvarchar(96)"); + b.Property("CompletionTime") + .HasColumnType("datetime2"); + b.Property("ConcurrencyStamp") .IsConcurrencyToken() .IsRequired() @@ -83,7 +86,7 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Migrations b.HasKey("Id"); - b.HasIndex("IsAbandoned", "NextTryTime"); + b.HasIndex("ApplicationName", "CompletionTime", "IsAbandoned", "NextTryTime"); b.ToTable("AbpBackgroundJobs", (string)null); }); diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobRecord.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobRecord.cs index c01f933781..18862c8b1d 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobRecord.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobRecord.cs @@ -48,6 +48,12 @@ public class BackgroundJobRecord : AggregateRoot, IHasCreationTime /// public virtual bool IsAbandoned { get; set; } + /// + /// The time this job was completed successfully. When set, the job is kept as history and excluded + /// from the waiting jobs query (set only when successful job persistence is enabled). + /// + public virtual DateTime? CompletionTime { get; set; } + /// /// Priority of this job. /// diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobStore.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobStore.cs index c7dbd6fbef..9d24c6c236 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobStore.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobStore.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.ObjectMapping; @@ -22,9 +23,13 @@ public class BackgroundJobStore : IBackgroundJobStore, ITransientDependency public virtual async Task FindAsync(Guid jobId) { - return ObjectMapper.Map( - await BackgroundJobRepository.FindAsync(jobId) - ); + var backgroundJobRecord = await BackgroundJobRepository.FindAsync(jobId); + if (backgroundJobRecord == null) + { + return null!; + } + + return ObjectMapper.Map(backgroundJobRecord); } public virtual async Task InsertAsync(BackgroundJobInfo jobInfo) @@ -34,18 +39,37 @@ public class BackgroundJobStore : IBackgroundJobStore, ITransientDependency ); } - public virtual async Task> GetWaitingJobsAsync(string applicationName, int maxResultCount) + public virtual async Task> GetWaitingJobsAsync(string? applicationName, int maxResultCount) { return ObjectMapper.Map, List>( await BackgroundJobRepository.GetWaitingListAsync(applicationName, maxResultCount) ); } + public virtual async Task> GetWaitingJobsAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter) + { + return ObjectMapper.Map, List>( + await BackgroundJobRepository.GetWaitingListAsync(applicationName, maxResultCount, jobNameFilter) + ); + } + public virtual async Task DeleteAsync(Guid jobId) { await BackgroundJobRepository.DeleteAsync(jobId); } + public virtual async Task DeleteAsync( + string? applicationName, + DateTime completedBefore, + int maxResultCount, + CancellationToken cancellationToken = default) + { + return await BackgroundJobRepository.DeleteAsync(applicationName, completedBefore, maxResultCount, cancellationToken); + } + public virtual async Task UpdateAsync(BackgroundJobInfo jobInfo) { var backgroundJobRecord = await BackgroundJobRepository.FindAsync(jobInfo.Id); diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs index 9ac5cef9bf..a605582201 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Riok.Mapperly.Abstractions; using Volo.Abp.Mapperly; -namespace Volo.Abp.BackgroundJobs; +namespace Volo.Abp.BackgroundJobs; [Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] public partial class BackgroundJobInfoToBackgroundJobRecordMapper @@ -31,6 +31,6 @@ public partial class BackgroundJobRecordToBackgroundJobInfoMapper : MapperBase { public override partial BackgroundJobInfo Map(BackgroundJobRecord source); - + public override partial void Map(BackgroundJobRecord source, BackgroundJobInfo destination); } diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/IBackgroundJobRepository.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/IBackgroundJobRepository.cs index 7e189df5c8..d4e8df2d83 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/IBackgroundJobRepository.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/IBackgroundJobRepository.cs @@ -2,12 +2,26 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Volo.Abp.Domain.Repositories; namespace Volo.Abp.BackgroundJobs; public interface IBackgroundJobRepository : IBasicRepository { - Task> GetWaitingListAsync([CanBeNull] string applicationName, int maxResultCount, CancellationToken cancellationToken = default); + Task> GetWaitingListAsync( + string? applicationName, + int maxResultCount, + CancellationToken cancellationToken = default); + + Task> GetWaitingListAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter, + CancellationToken cancellationToken = default); + + Task DeleteAsync( + string? applicationName, + DateTime completedBefore, + int maxResultCount, + CancellationToken cancellationToken = default); } diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs index 53a2c53843..70a73a8562 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/BackgroundJobsDbContextModelCreatingExtensions.cs @@ -29,9 +29,10 @@ public static class BackgroundJobsDbContextModelCreatingExtensions b.Property(x => x.NextTryTime); b.Property(x => x.LastTryTime); b.Property(x => x.IsAbandoned).HasDefaultValue(false); + b.Property(x => x.CompletionTime); b.Property(x => x.Priority).HasDefaultValue(BackgroundJobPriority.Normal).HasSentinel(BackgroundJobPriority.Normal); - b.HasIndex(x => new { x.IsAbandoned, x.NextTryTime }); + b.HasIndex(x => new { x.ApplicationName, x.CompletionTime, x.IsAbandoned, x.NextTryTime }); b.ApplyObjectExtensionMappings(); }); diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/EfCoreBackgroundJobRepository.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/EfCoreBackgroundJobRepository.cs index a81de3cdfb..5b55a7adf8 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/EfCoreBackgroundJobRepository.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.EntityFrameworkCore/Volo/Abp/BackgroundJobs/EntityFrameworkCore/EfCoreBackgroundJobRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -23,20 +22,71 @@ public class EfCoreBackgroundJobRepository : EfCoreRepository> GetWaitingListAsync([CanBeNull] string applicationName, int maxResultCount, CancellationToken cancellationToken = default) + public virtual Task> GetWaitingListAsync( + string? applicationName, + int maxResultCount, + CancellationToken cancellationToken = default) { - return await (await GetWaitingListQueryAsync(applicationName, maxResultCount)).ToListAsync(GetCancellationToken(cancellationToken)); + return GetWaitingListAsync(applicationName, maxResultCount, null, cancellationToken); } - protected virtual async Task> GetWaitingListQueryAsync([CanBeNull] string applicationName, int maxResultCount) + public virtual async Task> GetWaitingListAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter, + CancellationToken cancellationToken = default) + { + return await (await GetWaitingListQueryAsync(applicationName, maxResultCount, jobNameFilter)) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + protected virtual async Task> GetWaitingListQueryAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter = null) { var now = Clock.Now; - return (await GetDbSetAsync()) + var filter = jobNameFilter ?? BackgroundJobNameFilter.None; + var jobNames = filter.JobNames.ToList(); + + var query = (await GetDbSetAsync()) .Where(t => t.ApplicationName == applicationName) - .Where(t => !t.IsAbandoned && t.NextTryTime <= now) + .Where(t => !t.IsAbandoned && t.CompletionTime == null && t.NextTryTime <= now); + + if (filter.Mode == BackgroundJobNameFilterMode.Include) + { + query = query.Where(t => jobNames.Contains(t.JobName)); + } + else if (filter.Mode == BackgroundJobNameFilterMode.Exclude) + { + query = query.Where(t => !jobNames.Contains(t.JobName)); + } + + return query .OrderByDescending(t => t.Priority) .ThenBy(t => t.TryCount) .ThenBy(t => t.NextTryTime) .Take(maxResultCount); } + + public virtual async Task DeleteAsync(string? applicationName, DateTime completedBefore, int maxResultCount, CancellationToken cancellationToken = default) + { + var token = GetCancellationToken(cancellationToken); + var dbSet = await GetDbSetAsync(); + + var ids = await dbSet + .Where(t => t.ApplicationName == applicationName) + .Where(t => t.CompletionTime != null && t.CompletionTime < completedBefore) + .OrderBy(t => t.CompletionTime) + .Select(t => t.Id) + .Take(maxResultCount) + .ToListAsync(token); + + if (ids.Count == 0) + { + return 0; + } + + return await dbSet.Where(t => ids.Contains(t.Id)).ExecuteDeleteAsync(token); + } } diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.MongoDB/Volo/Abp/BackgroundJobs/MongoDB/MongoBackgroundJobRepository.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.MongoDB/Volo/Abp/BackgroundJobs/MongoDB/MongoBackgroundJobRepository.cs index 71a5680524..af4ab7ef89 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.MongoDB/Volo/Abp/BackgroundJobs/MongoDB/MongoBackgroundJobRepository.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.MongoDB/Volo/Abp/BackgroundJobs/MongoDB/MongoBackgroundJobRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.Linq; -using JetBrains.Annotations; using MongoDB.Driver; using MongoDB.Driver.Linq; using Volo.Abp.Domain.Repositories.MongoDB; @@ -24,20 +23,78 @@ public class MongoBackgroundJobRepository : MongoDbRepository> GetWaitingListAsync([CanBeNull] string applicationName, int maxResultCount, CancellationToken cancellationToken = default) + public virtual Task> GetWaitingListAsync( + string? applicationName, + int maxResultCount, + CancellationToken cancellationToken = default) { - return await (await GetWaitingListQuery(applicationName, maxResultCount, cancellationToken)).ToListAsync(GetCancellationToken(cancellationToken)); + return GetWaitingListAsync(applicationName, maxResultCount, null, cancellationToken); } - protected virtual async Task> GetWaitingListQuery([CanBeNull] string applicationName, int maxResultCount, CancellationToken cancellationToken = default) + public virtual async Task> GetWaitingListAsync( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter, + CancellationToken cancellationToken = default) + { + return await (await GetWaitingListQuery(applicationName, maxResultCount, jobNameFilter, cancellationToken)) + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + protected virtual async Task> GetWaitingListQuery( + string? applicationName, + int maxResultCount, + BackgroundJobNameFilter? jobNameFilter = null, + CancellationToken cancellationToken = default) { var now = Clock.Now; - return (await GetQueryableAsync(cancellationToken)) + var filter = jobNameFilter ?? BackgroundJobNameFilter.None; + var jobNames = filter.JobNames.ToList(); + + var query = (await GetQueryableAsync(cancellationToken)) .Where(t => t.ApplicationName == applicationName) - .Where(t => !t.IsAbandoned && t.NextTryTime <= now) + .Where(t => !t.IsAbandoned && t.CompletionTime == null && t.NextTryTime <= now); + + if (filter.Mode == BackgroundJobNameFilterMode.Include) + { + query = query.Where(t => jobNames.Contains(t.JobName)); + } + else if (filter.Mode == BackgroundJobNameFilterMode.Exclude) + { + query = query.Where(t => !jobNames.Contains(t.JobName)); + } + + return query .OrderByDescending(t => t.Priority) .ThenBy(t => t.TryCount) .ThenBy(t => t.NextTryTime) .Take(maxResultCount); } + + public virtual async Task DeleteAsync(string? applicationName, DateTime completedBefore, int maxResultCount, CancellationToken cancellationToken = default) + { + var token = GetCancellationToken(cancellationToken); + + var ids = await (await GetQueryableAsync(token)) + .Where(t => t.ApplicationName == applicationName) + .Where(t => t.CompletionTime != null && t.CompletionTime < completedBefore) + .OrderBy(t => t.CompletionTime) + .Select(t => t.Id) + .Take(maxResultCount) + .ToListAsync(token); + + if (ids.Count == 0) + { + return 0; + } + + var dbContext = await GetDbContextAsync(token); + var collection = dbContext.Collection(); + + var result = dbContext.SessionHandle != null + ? await collection.DeleteManyAsync(dbContext.SessionHandle, x => ids.Contains(x.Id), cancellationToken: token) + : await collection.DeleteManyAsync(x => ids.Contains(x.Id), cancellationToken: token); + + return (int)result.DeletedCount; + } } diff --git a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobRepository_Tests.cs b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobRepository_Tests.cs index c62cb43309..b717d28d15 100644 --- a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobRepository_Tests.cs +++ b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobRepository_Tests.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Modularity; @@ -36,4 +38,99 @@ public abstract class BackgroundJobRepository_Tests : Background backgroundJobs.Any(j => j.ApplicationName == "App2").ShouldBeFalse(); backgroundJobs.Any(j => j.ApplicationName == null).ShouldBeFalse(); } + + [Fact] + public async Task Should_Filter_Waiting_List_By_Included_Job_Names() + { + // App1 waiting jobs: two "TestJobName" + one "OtherJobName". + var testJobs = await _backgroundJobRepository.GetWaitingListAsync("App1", 10, BackgroundJobNameFilter.Include(new[] { "TestJobName" })); + testJobs.Count.ShouldBe(2); + testJobs.ShouldAllBe(j => j.JobName == "TestJobName"); + + var otherJobs = await _backgroundJobRepository.GetWaitingListAsync("App1", 10, BackgroundJobNameFilter.Include(new[] { "OtherJobName" })); + otherJobs.Count.ShouldBe(1); + otherJobs.Single().JobName.ShouldBe("OtherJobName"); + + var none = await _backgroundJobRepository.GetWaitingListAsync("App1", 10, BackgroundJobNameFilter.Include(new[] { "NonExistentJob" })); + none.ShouldBeEmpty(); + } + + [Fact] + public async Task Should_Filter_Waiting_List_By_Excluded_Job_Names() + { + var withoutTestJob = await _backgroundJobRepository.GetWaitingListAsync("App1", 10, BackgroundJobNameFilter.Exclude(new[] { "TestJobName" })); + withoutTestJob.Count.ShouldBe(1); + withoutTestJob.Single().JobName.ShouldBe("OtherJobName"); + + var withoutNonExistent = await _backgroundJobRepository.GetWaitingListAsync("App1", 10, BackgroundJobNameFilter.Exclude(new[] { "NonExistentJob" })); + withoutNonExistent.Count.ShouldBe(3); + } + + [Fact] + public async Task Should_Return_Null_From_Store_When_Job_Not_Found() + { + // The parallel worker re-reads a claimed job under the lock; a job removed by another instance must return null (not throw). + var store = GetRequiredService(); + var found = await store.FindAsync(Guid.NewGuid()); + found.ShouldBeNull(); + } + + [Fact] + public async Task Should_Exclude_Completed_Jobs_From_Waiting_List() + { + var completedJobId = Guid.NewGuid(); + await _backgroundJobRepository.InsertAsync( + new BackgroundJobRecord(completedJobId) + { + ApplicationName = "App1", + JobName = "TestJobName", + JobArgs = "{ value: 1 }", + NextTryTime = _clock.Now.Subtract(TimeSpan.FromMinutes(1)), + Priority = BackgroundJobPriority.Normal, + IsAbandoned = false, + CompletionTime = _clock.Now, + CreationTime = _clock.Now.Subtract(TimeSpan.FromMinutes(2)), + TryCount = 1 + }, + autoSave: true); + + var waitingJobs = await _backgroundJobRepository.GetWaitingListAsync("App1", 100); + waitingJobs.ShouldNotContain(j => j.Id == completedJobId); + } + + [Fact] + public async Task Should_Delete_Old_Successful_Jobs_Of_The_Given_Application_Only() + { + var oldApp1Id = Guid.NewGuid(); + await _backgroundJobRepository.InsertAsync(NewCompletedJob(oldApp1Id, "App1", _clock.Now.Subtract(TimeSpan.FromDays(2))), autoSave: true); + + var recentApp1Id = Guid.NewGuid(); + await _backgroundJobRepository.InsertAsync(NewCompletedJob(recentApp1Id, "App1", _clock.Now), autoSave: true); + + var oldApp2Id = Guid.NewGuid(); + await _backgroundJobRepository.InsertAsync(NewCompletedJob(oldApp2Id, "App2", _clock.Now.Subtract(TimeSpan.FromDays(2))), autoSave: true); + + var deleted = await _backgroundJobRepository.DeleteAsync("App1", _clock.Now.Subtract(TimeSpan.FromDays(1)), 100); + deleted.ShouldBe(1); + + (await _backgroundJobRepository.FindAsync(oldApp1Id)).ShouldBeNull(); // App1 old completed → deleted + (await _backgroundJobRepository.FindAsync(recentApp1Id)).ShouldNotBeNull(); // App1 recent → kept + (await _backgroundJobRepository.FindAsync(oldApp2Id)).ShouldNotBeNull(); // App2 old → not touched (isolation) + } + + private BackgroundJobRecord NewCompletedJob(Guid id, string applicationName, DateTime completionTime) + { + return new BackgroundJobRecord(id) + { + ApplicationName = applicationName, + JobName = "TestJobName", + JobArgs = "{ value: 1 }", + NextTryTime = _clock.Now.Subtract(TimeSpan.FromMinutes(1)), + Priority = BackgroundJobPriority.Normal, + IsAbandoned = false, + CompletionTime = completionTime, + CreationTime = _clock.Now.Subtract(TimeSpan.FromMinutes(5)), + TryCount = 1 + }; + } } diff --git a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestData.cs b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestData.cs index 0d8b2bfd29..cd01e1b09a 100644 --- a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestData.cs +++ b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestData.cs @@ -8,4 +8,5 @@ public class BackgroundJobsTestData : ISingletonDependency public Guid JobId1 { get; } = Guid.NewGuid(); public Guid JobId2 { get; } = Guid.NewGuid(); public Guid JobId3 { get; } = Guid.NewGuid(); + public Guid JobId4 { get; } = Guid.NewGuid(); } diff --git a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestDataBuilder.cs b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestDataBuilder.cs index f6f4c37e12..47ebf103e2 100644 --- a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestDataBuilder.cs +++ b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.TestBase/Volo/Abp/BackgroundJobs/BackgroundJobsTestDataBuilder.cs @@ -67,5 +67,21 @@ public class BackgroundJobsTestDataBuilder : ITransientDependency TryCount = 2 } ); + + // App1 waiting job with a different job name, to verify job-name filtering. + await _backgroundJobRepository.InsertAsync( + new BackgroundJobRecord(_testData.JobId4) + { + ApplicationName = "App1", + JobName = "OtherJobName", + JobArgs = "{ value: 4 }", + NextTryTime = _clock.Now.Subtract(TimeSpan.FromMinutes(1)), + Priority = BackgroundJobPriority.Normal, + IsAbandoned = false, + LastTryTime = null, + CreationTime = _clock.Now.Subtract(TimeSpan.FromMinutes(3)), + TryCount = 0 + } + ); } } diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json index ea01f3ed3e..e64272f329 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json @@ -3,8 +3,8 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.shared": "~10.5.0-rc.4", - "@abp/prismjs": "~10.5.0-rc.4", - "@abp/highlight.js": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.shared": "~10.6.0-rc.1", + "@abp/prismjs": "~10.6.0-rc.1", + "@abp/highlight.js": "~10.6.0-rc.1" } } diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock index 32c8a72692..aee38c54cf 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock @@ -2,204 +2,204 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.5.0-rc.4.tgz#3d48c5e461997c50f532a8af6394d90de2a80c42" - integrity sha512-V77e/wptcMJbF3Zhki1/jGVAzlbd5W/Q0wWEa3xiSJqa97g8VmjNB+Kzz6FoTzAUHT2oBJllGO8d2+nhmqLl4g== +"@abp/clipboard@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.6.0-rc.1.tgz#b05335d32ce98395ed9d0dcf9fd50068d99ca064" + integrity sha512-Y/77csSlTFBnPwPTopYUgjv5Md5foz03aKozQE9EAgrTezjg48GAaJmZuvrA3BuHZcbNO9J56VsXbLwFKvQreA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" clipboard "^2.0.11" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.5.0-rc.4.tgz#591a78f06b86ba3c46ab548e4380c486252562ef" - integrity sha512-FA7U9B559I0wug29E30vvNYw9fxjv3mVvq4ashftw7V5H9oQcztUbaDe+nOeSdPLwI8YQRa/0QdXky4QmsdDQQ== +"@abp/highlight.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.6.0-rc.1.tgz#0a905a85aabe963b31fcca5e96f464114db2b1f0" + integrity sha512-hMKksUbAXagabN2+JFj0LrcUPD1UbLq3iok72Y9QRARRAJE+hDMnYfqHXjQtzeLqaIS+S6gzu109cRhN9v6OVw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.5.0-rc.4.tgz#d130f9e872eef068da63497b53ef1823347dc403" - integrity sha512-epb+8m21G8glvjHh2NgzO6kK3JDgliwXmfyIvRBg2/dUyovCyQ9A4V9jOA0VfaqTHz7sl0liM7M66/gP3XiGhA== +"@abp/prismjs@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.6.0-rc.1.tgz#e862f6c7e74a1db4ce34a67ca24f3dc9be5fdb37" + integrity sha512-pjFezZHnmNv+v7dgUWGGwP0+BXUgtlZqeNebaYwST5sdGHQw0zQ5GqY3yISlrA32OWjGvDrI3+7aOKIDoPxySA== dependencies: - "@abp/clipboard" "~10.5.0-rc.4" - "@abp/core" "~10.5.0-rc.4" + "@abp/clipboard" "~10.6.0-rc.1" + "@abp/core" "~10.6.0-rc.1" prismjs "^1.30.0" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json index 723fa049d0..3883f4d08c 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json @@ -3,8 +3,8 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4", - "@abp/prismjs": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1", + "@abp/prismjs": "~10.6.0-rc.1" }, "devDependencies": {} } diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock index e36895c5fe..a176ebd40e 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock @@ -2,203 +2,203 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.5.0-rc.4.tgz#3d48c5e461997c50f532a8af6394d90de2a80c42" - integrity sha512-V77e/wptcMJbF3Zhki1/jGVAzlbd5W/Q0wWEa3xiSJqa97g8VmjNB+Kzz6FoTzAUHT2oBJllGO8d2+nhmqLl4g== +"@abp/clipboard@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.6.0-rc.1.tgz#b05335d32ce98395ed9d0dcf9fd50068d99ca064" + integrity sha512-Y/77csSlTFBnPwPTopYUgjv5Md5foz03aKozQE9EAgrTezjg48GAaJmZuvrA3BuHZcbNO9J56VsXbLwFKvQreA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" clipboard "^2.0.11" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.5.0-rc.4.tgz#d130f9e872eef068da63497b53ef1823347dc403" - integrity sha512-epb+8m21G8glvjHh2NgzO6kK3JDgliwXmfyIvRBg2/dUyovCyQ9A4V9jOA0VfaqTHz7sl0liM7M66/gP3XiGhA== +"@abp/prismjs@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.6.0-rc.1.tgz#e862f6c7e74a1db4ce34a67ca24f3dc9be5fdb37" + integrity sha512-pjFezZHnmNv+v7dgUWGGwP0+BXUgtlZqeNebaYwST5sdGHQw0zQ5GqY3yISlrA32OWjGvDrI3+7aOKIDoPxySA== dependencies: - "@abp/clipboard" "~10.5.0-rc.4" - "@abp/core" "~10.5.0-rc.4" + "@abp/clipboard" "~10.6.0-rc.1" + "@abp/core" "~10.6.0-rc.1" prismjs "^1.30.0" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/blogging/app/Volo.BloggingTestApp/package.json b/modules/blogging/app/Volo.BloggingTestApp/package.json index 005d340712..8e97bd420b 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/package.json +++ b/modules/blogging/app/Volo.BloggingTestApp/package.json @@ -3,7 +3,7 @@ "name": "volo.blogtestapp", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4", - "@abp/blogging": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1", + "@abp/blogging": "~10.6.0-rc.1" } } diff --git a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock index 7d8c134605..cc8fdff78f 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock +++ b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock @@ -2,229 +2,229 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/blogging@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.5.0-rc.4.tgz#c3decde27feaaff1abd4290a1b350800340aa7e0" - integrity sha512-z5NetRKM/Luv9GoidsGYgnCAqHVRZz52TUhQAvavpmM42QySmur4N8v0bAoT5oyMPgT4qXkAJKpaw7MflF3mtA== +"@abp/blogging@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-10.6.0-rc.1.tgz#1317305728e036b4817b20839039af97f993b6aa" + integrity sha512-Q9b5BwM1wm77AnFOy064ScPAz62KyVnl8ypgIjuGclwO3YYcZDH5XCyZn9eN4J+uK8QTmyodejFCPWB5xKGP/A== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" - "@abp/owl.carousel" "~10.5.0-rc.4" - "@abp/prismjs" "~10.5.0-rc.4" - "@abp/tui-editor" "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" + "@abp/owl.carousel" "~10.6.0-rc.1" + "@abp/prismjs" "~10.6.0-rc.1" + "@abp/tui-editor" "~10.6.0-rc.1" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.5.0-rc.4.tgz#3d48c5e461997c50f532a8af6394d90de2a80c42" - integrity sha512-V77e/wptcMJbF3Zhki1/jGVAzlbd5W/Q0wWEa3xiSJqa97g8VmjNB+Kzz6FoTzAUHT2oBJllGO8d2+nhmqLl4g== +"@abp/clipboard@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.6.0-rc.1.tgz#b05335d32ce98395ed9d0dcf9fd50068d99ca064" + integrity sha512-Y/77csSlTFBnPwPTopYUgjv5Md5foz03aKozQE9EAgrTezjg48GAaJmZuvrA3BuHZcbNO9J56VsXbLwFKvQreA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" clipboard "^2.0.11" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/owl.carousel@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.5.0-rc.4.tgz#f6a30590475b5f67b656de1ee6aed12ec58af255" - integrity sha512-kkQyi6dvw6rfz6DRseNZS0aazofzwIREyHyZ6KjJivUsHlb8tlIuIrIGNjO46SzvtV+vE+neJFg5chLEi2HtQQ== +"@abp/owl.carousel@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-10.6.0-rc.1.tgz#7d3461db7d7b142bb3b31663f2c34686a34ce4da" + integrity sha512-zUWMzX97bqAjMQD3miEsmG1Js80tILUItFkA8aBDMQb6d2fQhAediF0AnbMbPPV5t6B+hwG1fVTi/ItA40gaEQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" owl.carousel "^2.3.4" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.5.0-rc.4.tgz#d130f9e872eef068da63497b53ef1823347dc403" - integrity sha512-epb+8m21G8glvjHh2NgzO6kK3JDgliwXmfyIvRBg2/dUyovCyQ9A4V9jOA0VfaqTHz7sl0liM7M66/gP3XiGhA== +"@abp/prismjs@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.6.0-rc.1.tgz#e862f6c7e74a1db4ce34a67ca24f3dc9be5fdb37" + integrity sha512-pjFezZHnmNv+v7dgUWGGwP0+BXUgtlZqeNebaYwST5sdGHQw0zQ5GqY3yISlrA32OWjGvDrI3+7aOKIDoPxySA== dependencies: - "@abp/clipboard" "~10.5.0-rc.4" - "@abp/core" "~10.5.0-rc.4" + "@abp/clipboard" "~10.6.0-rc.1" + "@abp/core" "~10.6.0-rc.1" prismjs "^1.30.0" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/tui-editor@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.5.0-rc.4.tgz#e4d8359e11fadb84ccc9d1e3445f5a3370fd4656" - integrity sha512-hDC1gGPF6PtRWWkaSY0lu/WcpwnPSirG7394PNLKOXBuKoJss9pCJWbTkJYavz3wWOtOe/cwhuv29vg6q2+p6g== +"@abp/tui-editor@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.6.0-rc.1.tgz#b01072a83e315fc4dd2c25445fe76343bad71b6b" + integrity sha512-tSiOc2X1RrA8NIafqJi+6EFQpTKTYqGf8VRDmO1IhwXDi9M9aK+kDKdfafvFaACotFINTcGsX6wSgVU8CWEyVg== dependencies: - "@abp/jquery" "~10.5.0-rc.4" - "@abp/prismjs" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" + "@abp/prismjs" "~10.6.0-rc.1" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json index d05ad638f3..69b57f41a9 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json @@ -3,6 +3,6 @@ "name": "client-simulation-web", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1" } } diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock index d5580eb529..8e7257cd57 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/cms-kit/angular/package.json b/modules/cms-kit/angular/package.json index 4e7074e872..6099330f64 100644 --- a/modules/cms-kit/angular/package.json +++ b/modules/cms-kit/angular/package.json @@ -15,11 +15,11 @@ }, "private": true, "dependencies": { - "@abp/ng.account": "~10.5.0-rc.4", - "@abp/ng.identity": "~10.5.0-rc.4", - "@abp/ng.setting-management": "~10.5.0-rc.4", - "@abp/ng.tenant-management": "~10.5.0-rc.4", - "@abp/ng.theme.basic": "~10.5.0-rc.4", + "@abp/ng.account": "~10.5.0", + "@abp/ng.identity": "~10.6.0-rc.1", + "@abp/ng.setting-management": "~10.6.0-rc.1", + "@abp/ng.tenant-management": "~10.6.0-rc.1", + "@abp/ng.theme.basic": "~10.6.0-rc.1", "@angular/animations": "~10.0.0", "@angular/common": "~10.0.0", "@angular/compiler": "~10.0.0", diff --git a/modules/cms-kit/angular/projects/cms-kit/package.json b/modules/cms-kit/angular/projects/cms-kit/package.json index 1f5633b582..4350d13198 100644 --- a/modules/cms-kit/angular/projects/cms-kit/package.json +++ b/modules/cms-kit/angular/projects/cms-kit/package.json @@ -4,8 +4,8 @@ "peerDependencies": { "@angular/common": "^9.1.11", "@angular/core": "^9.1.11", - "@abp/ng.core": ">=10.5.0-rc.4", - "@abp/ng.theme.shared": ">=10.5.0-rc.4" + "@abp/ng.core": ">=10.6.0-rc.1", + "@abp/ng.theme.shared": ">=10.6.0-rc.1" }, "dependencies": { "tslib": "^2.0.0" diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json index 90ad3d4154..6ed11ebec3 100644 --- a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json +++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/package.json @@ -3,6 +3,6 @@ "name": "my-app-identityserver", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock index d5580eb529..8e7257cd57 100644 --- a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json b/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json index 1f5589b59e..60935294dc 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/package.json @@ -3,6 +3,6 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock index d5580eb529..8e7257cd57 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/yarn.lock @@ -2,186 +2,186 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json index e141e199f8..340c289e74 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/package.json @@ -3,7 +3,7 @@ "name": "my-app", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4", - "@abp/cms-kit": "10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1", + "@abp/cms-kit": "10.6.0-rc.1" } } diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock index a2f15f92bf..7d3f4e0f3f 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/yarn.lock @@ -2,298 +2,298 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.5.0-rc.4.tgz#3d48c5e461997c50f532a8af6394d90de2a80c42" - integrity sha512-V77e/wptcMJbF3Zhki1/jGVAzlbd5W/Q0wWEa3xiSJqa97g8VmjNB+Kzz6FoTzAUHT2oBJllGO8d2+nhmqLl4g== +"@abp/clipboard@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.6.0-rc.1.tgz#b05335d32ce98395ed9d0dcf9fd50068d99ca064" + integrity sha512-Y/77csSlTFBnPwPTopYUgjv5Md5foz03aKozQE9EAgrTezjg48GAaJmZuvrA3BuHZcbNO9J56VsXbLwFKvQreA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" clipboard "^2.0.11" -"@abp/cms-kit.admin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.5.0-rc.4.tgz#75a922a15ea76865df8a36b61a6b3a49711ad2d1" - integrity sha512-OlLZ8NgG1UC+zQfoP8o4VLqCcslapgwxrPhbCzs0JjiBJ+yr+89ZbSQq+vLaHS9UlSNgtkGI7nH62UDijuz1DA== +"@abp/cms-kit.admin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/cms-kit.admin/-/cms-kit.admin-10.6.0-rc.1.tgz#db2b082fb04a9ee334272eebb75905528978a1c7" + integrity sha512-Z3LAr0GagAjmXb3SuasV95oKgU8U43eGMy5AAS1Nhuemz+fe+YL067te/3LtsCCm4iSXzs0oX/h+NkZnhYA8/A== dependencies: - "@abp/codemirror" "~10.5.0-rc.4" - "@abp/jstree" "~10.5.0-rc.4" - "@abp/markdown-it" "~10.5.0-rc.4" - "@abp/slugify" "~10.5.0-rc.4" - "@abp/tui-editor" "~10.5.0-rc.4" - "@abp/uppy" "~10.5.0-rc.4" + "@abp/codemirror" "~10.6.0-rc.1" + "@abp/jstree" "~10.6.0-rc.1" + "@abp/markdown-it" "~10.6.0-rc.1" + "@abp/slugify" "~10.6.0-rc.1" + "@abp/tui-editor" "~10.6.0-rc.1" + "@abp/uppy" "~10.6.0-rc.1" -"@abp/cms-kit.public@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.5.0-rc.4.tgz#c56440257773490aae6ea25b4ceea0bcf2983dea" - integrity sha512-GwCoJ5/3+Oto2xqU/rysdg2z0G3VMgMhe/7y0XxPdBE+pxKaYzA6qurDrP2Hj9h06EzV97+vWCMHhkvkvPrFtA== +"@abp/cms-kit.public@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/cms-kit.public/-/cms-kit.public-10.6.0-rc.1.tgz#a51c70f2e3c02a708e3c832e9c14753729ddb41e" + integrity sha512-fuN3mseDkCA6c3DC53xqsJ/BJ/6P2rFPvlsrdPp2amEcNwR8T+IipjtH67byH3XE62UEf/Z39s0bVSHuYNN9og== dependencies: - "@abp/highlight.js" "~10.5.0-rc.4" - "@abp/star-rating-svg" "~10.5.0-rc.4" + "@abp/highlight.js" "~10.6.0-rc.1" + "@abp/star-rating-svg" "~10.6.0-rc.1" -"@abp/cms-kit@10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.5.0-rc.4.tgz#ee87be54f59f15d7b6cffe94bce78b01f57d95f6" - integrity sha512-sLAQZwsUlN42EOA6lJOIihJ8gx96tv2f2qhOk6yDMClYlwpZ26hW6w2fz+QO+WNmu8FTFSIDNvmab6P9Y18S+w== +"@abp/cms-kit@10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/cms-kit/-/cms-kit-10.6.0-rc.1.tgz#9bc6bb6465d2e8047918c14a1af9a0a4f1f7ebf0" + integrity sha512-hoSMdrew2fgoE2ew8s7p4a2TmVwGhIsa6FVU5VUBQdWe5QTnQJS6fuz6AQ4pVHiGkWWHRjYMpgQhWT7UD0kRWg== dependencies: - "@abp/cms-kit.admin" "~10.5.0-rc.4" - "@abp/cms-kit.public" "~10.5.0-rc.4" + "@abp/cms-kit.admin" "~10.6.0-rc.1" + "@abp/cms-kit.public" "~10.6.0-rc.1" -"@abp/codemirror@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.5.0-rc.4.tgz#855a7c16da6ca36ad855bfa9051b742494f8fe23" - integrity sha512-Hy5MqK5LRiITm63MW8nCX6MudV3xUdHmp/vOhMOQbMZI/Xhjd1yL865VvzHiUSUJOHTV4XNBgdT6NLBCKNqbzQ== +"@abp/codemirror@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-10.6.0-rc.1.tgz#3d14df1b845cedc92681d32eaf0aee0babb75ed6" + integrity sha512-eh/2hSROSAAXRhRzzwW+N0oGLoJV3qH31d2KEHe+hzGd9kNJofptIKg/1DXKPlsqil5Ulk9VUTRxHc4dkBlgLw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@codemirror/lang-css" "^6.0.0" "@codemirror/lang-javascript" "^6.0.0" "@codemirror/state" "^6.0.0" "@codemirror/view" "^6.0.0" codemirror "^6.0.2" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/highlight.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.5.0-rc.4.tgz#591a78f06b86ba3c46ab548e4380c486252562ef" - integrity sha512-FA7U9B559I0wug29E30vvNYw9fxjv3mVvq4ashftw7V5H9oQcztUbaDe+nOeSdPLwI8YQRa/0QdXky4QmsdDQQ== +"@abp/highlight.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-10.6.0-rc.1.tgz#0a905a85aabe963b31fcca5e96f464114db2b1f0" + integrity sha512-hMKksUbAXagabN2+JFj0LrcUPD1UbLq3iok72Y9QRARRAJE+hDMnYfqHXjQtzeLqaIS+S6gzu109cRhN9v6OVw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@highlightjs/cdn-assets" "~11.11.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/jstree@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.5.0-rc.4.tgz#f1c99fbda6fb18d356cf14fe9da83a867ecbbaa5" - integrity sha512-VYdsmmIqPJd1aHR3XWDQ7FKl35i7t+O4x5PBUT0/jFwHqJSlNSRcl77vQULTltr4uYOt8SE6xIu3l9CVb/DLMw== +"@abp/jstree@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jstree/-/jstree-10.6.0-rc.1.tgz#546059db3e0994fc3c5ad42d396fd7f3193765ce" + integrity sha512-8nZSlt3qsLTPAJp628tq8KuqqG4LCI0tiChwzqT6itIZmhS1ilLdtwJn/X0xZsjDjMSss6635ZXbSXIzr+5tIQ== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jstree "^3.3.17" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/markdown-it@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.5.0-rc.4.tgz#e05ef3bbef1857aeb0933dd3fd9d6814e2c04821" - integrity sha512-iVUKOqN1FXZwS2hQ63IE1RpDEogGsbhrsWGern2XxGU2PrIMfQxRfqrBBd70AF9ETL8abTV+o6tSajG6pm2N1w== +"@abp/markdown-it@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-10.6.0-rc.1.tgz#b234476bd6ce58049dcc6c68cd4c42c33e44062b" + integrity sha512-vp8fel0VftMJxSybP2JMrdSOV9xiuyVA0p4b4R5V92QdwK8fmlR/LiBOZnWNRL1gnjXCmnO12k0G/MuCjqxgDg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" markdown-it "^14.1.0" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.5.0-rc.4.tgz#d130f9e872eef068da63497b53ef1823347dc403" - integrity sha512-epb+8m21G8glvjHh2NgzO6kK3JDgliwXmfyIvRBg2/dUyovCyQ9A4V9jOA0VfaqTHz7sl0liM7M66/gP3XiGhA== +"@abp/prismjs@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.6.0-rc.1.tgz#e862f6c7e74a1db4ce34a67ca24f3dc9be5fdb37" + integrity sha512-pjFezZHnmNv+v7dgUWGGwP0+BXUgtlZqeNebaYwST5sdGHQw0zQ5GqY3yISlrA32OWjGvDrI3+7aOKIDoPxySA== dependencies: - "@abp/clipboard" "~10.5.0-rc.4" - "@abp/core" "~10.5.0-rc.4" + "@abp/clipboard" "~10.6.0-rc.1" + "@abp/core" "~10.6.0-rc.1" prismjs "^1.30.0" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/slugify@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.5.0-rc.4.tgz#247c6bdc9cf2e6056809e1f3c5dfe7267af54e55" - integrity sha512-s9Nr7husDQtFAwaB2aY2b/65JwcylJvUuOUQtLR0NitxJfLq7RhfxxVWR9+0PeHiGzU9osbaE66fXse+vo4+sg== +"@abp/slugify@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/slugify/-/slugify-10.6.0-rc.1.tgz#55e5c9abd1828b6b4c83d1b3ef44c72e8fe9ee05" + integrity sha512-PmaSgJ4TvbQo4dg0JblvhHT/HGVOU21AywJILstYfNvvaaDBlVtTbqVU8/QTudtQDh+IFr0BZuPYxgCTaTrhcA== dependencies: slugify "^1.6.6" -"@abp/star-rating-svg@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.5.0-rc.4.tgz#e6cbd11e8aa4c5308ed2b6a79424f30f66c9a68e" - integrity sha512-RV2lKxblJ2DtR9+sdhdTsJGGvVGITQu6adPCXcSBkHB8yRWF93K7WayTvNLJV3XwAzJWBLp1GyMQtX9X/wXAzQ== +"@abp/star-rating-svg@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/star-rating-svg/-/star-rating-svg-10.6.0-rc.1.tgz#532a7bcbabaa7da9e02a1e07b8523cd853984567" + integrity sha512-eD5gl7szk5gOw9w78jPL2lDSiXK1AOglex7jen6WKwTpSfSN8MGaSFM3Ljkn26rDmKn2XHK3TzUZMwZ+QKupjQ== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" star-rating-svg "^3.5.0" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/tui-editor@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.5.0-rc.4.tgz#e4d8359e11fadb84ccc9d1e3445f5a3370fd4656" - integrity sha512-hDC1gGPF6PtRWWkaSY0lu/WcpwnPSirG7394PNLKOXBuKoJss9pCJWbTkJYavz3wWOtOe/cwhuv29vg6q2+p6g== +"@abp/tui-editor@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-10.6.0-rc.1.tgz#b01072a83e315fc4dd2c25445fe76343bad71b6b" + integrity sha512-tSiOc2X1RrA8NIafqJi+6EFQpTKTYqGf8VRDmO1IhwXDi9M9aK+kDKdfafvFaACotFINTcGsX6wSgVU8CWEyVg== dependencies: - "@abp/jquery" "~10.5.0-rc.4" - "@abp/prismjs" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" + "@abp/prismjs" "~10.6.0-rc.1" -"@abp/uppy@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.5.0-rc.4.tgz#64b9d40290e7f2d4c17d6be026bd3a9afc2201af" - integrity sha512-vbb5BNcXWIz6riPjj4bPPDdMvU5jeCHc3Lr02tWI9376xRF0+fI/o9g/RsnmlTQGBrELuCoJY2z+Meye/DFNRw== +"@abp/uppy@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/uppy/-/uppy-10.6.0-rc.1.tgz#d306f177cf8d03d11f2c03979e0c5afbab606b2b" + integrity sha512-3Z02MCFe3KJXk2HmB9zQ9ANnp+zr+h29nCaR8EzYLyv8IbBuv4udGQdF7FeEtiEAG+ECsEGUs/1oTCuTNgCVVw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" uppy "^5.1.2" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/docs/app/VoloDocs.Web/package.json b/modules/docs/app/VoloDocs.Web/package.json index ce433c4f43..21840bad0f 100644 --- a/modules/docs/app/VoloDocs.Web/package.json +++ b/modules/docs/app/VoloDocs.Web/package.json @@ -3,7 +3,7 @@ "name": "volo.docstestapp", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "~10.5.0-rc.4", - "@abp/docs": "~10.5.0-rc.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "~10.6.0-rc.1", + "@abp/docs": "~10.6.0-rc.1" } } diff --git a/modules/docs/app/VoloDocs.Web/yarn.lock b/modules/docs/app/VoloDocs.Web/yarn.lock index a09bc09797..73ea484848 100644 --- a/modules/docs/app/VoloDocs.Web/yarn.lock +++ b/modules/docs/app/VoloDocs.Web/yarn.lock @@ -2,222 +2,222 @@ # yarn lockfile v1 -"@abp/anchor-js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.5.0-rc.4.tgz#6643cea87515094b018bd4201234297ab7b97305" - integrity sha512-cVRU8cUbYp6R5FQBAEaYZ28RyB7rciV9XoBfcAjKPC3QGOeDKN25mLn7mu1R78JI3Bz8lfsU4BUUuhl7aMEAVw== +"@abp/anchor-js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-10.6.0-rc.1.tgz#11eeb2f55ec3df7b12d11c1b3c36a91791773eef" + integrity sha512-gfchO7Md7CsA45p/yHLvTSP8BPrs5Bf131gGKroNSbShfLCYGlj0F/bhDg9UMi25i7DwlxIyTOjJY1ij59Kr+g== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" anchor-js "^5.0.0" -"@abp/aspnetcore.mvc.ui.theme.basic@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.5.0-rc.4.tgz#c24c2528ab89c12876d3051c7af3e4487573eba6" - integrity sha512-f/B88/lhr1aGWW8FNgqvqTHWtCL8psc11cna0I4dwbfk0vAOxsoMKoksX7tLK2spxm5y4j7ZET0c+DehLlMHuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.5.0-rc.4.tgz#b14d0142a2b7d37e4b6d1ff700e495ff7561891a" - integrity sha512-i0AC6ZSYTxK42psQc1VbyHVouKHGii80E0Eax42jGoTlim8D1kNgetO+8+Pltg/5rL0HJViWrWPwyG7mBipA2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~10.5.0-rc.4" - "@abp/bootstrap" "~10.5.0-rc.4" - "@abp/bootstrap-datepicker" "~10.5.0-rc.4" - "@abp/bootstrap-daterangepicker" "~10.5.0-rc.4" - "@abp/datatables.net-bs5" "~10.5.0-rc.4" - "@abp/font-awesome" "~10.5.0-rc.4" - "@abp/jquery-validation-unobtrusive" "~10.5.0-rc.4" - "@abp/lodash" "~10.5.0-rc.4" - "@abp/luxon" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/moment" "~10.5.0-rc.4" - "@abp/select2" "~10.5.0-rc.4" - "@abp/sweetalert2" "~10.5.0-rc.4" - "@abp/timeago" "~10.5.0-rc.4" - -"@abp/aspnetcore.mvc.ui@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.5.0-rc.4.tgz#79822aadec8133133201d59a1c3dc6b7ca226c1f" - integrity sha512-149kt2gvpdG/rUn6Z44YF3m2Bsz5Z8mZfEDXwt7x+MjB84cXbm16sVcKdSkPkg2mXcLycJYpMGG/cgSlH+FvHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-10.6.0-rc.1.tgz#14682d214e7c6cffe16b1faadf4069bdbf3cfc43" + integrity sha512-ehOD2ax9r8gd0wpGJ3V/7ngHQRNQRfgxtqvFGrl1gDQy8wZftmvcNEQRoUasRNrksGYzPCZ4McdOKtSkBAk2dQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui.theme.shared@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-10.6.0-rc.1.tgz#445d6b5e3836c27c8626099e23443597af5cf5d7" + integrity sha512-KNdNuV8aoMOdAYwNIucGdedBYXO2jUoohaVJTs7nDJ06Ct4WEYqNJ+BXSc+vxyWaYBCVK76DEJwQooshT7dTKw== + dependencies: + "@abp/aspnetcore.mvc.ui" "~10.6.0-rc.1" + "@abp/bootstrap" "~10.6.0-rc.1" + "@abp/bootstrap-datepicker" "~10.6.0-rc.1" + "@abp/bootstrap-daterangepicker" "~10.6.0-rc.1" + "@abp/datatables.net-bs5" "~10.6.0-rc.1" + "@abp/font-awesome" "~10.6.0-rc.1" + "@abp/jquery-validation-unobtrusive" "~10.6.0-rc.1" + "@abp/lodash" "~10.6.0-rc.1" + "@abp/luxon" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/moment" "~10.6.0-rc.1" + "@abp/select2" "~10.6.0-rc.1" + "@abp/sweetalert2" "~10.6.0-rc.1" + "@abp/timeago" "~10.6.0-rc.1" + +"@abp/aspnetcore.mvc.ui@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-10.6.0-rc.1.tgz#9c8fc06510744ed8a12e76b30a220ac659ea5e31" + integrity sha512-D9iQjntdNRcGYtgoXr+WfGYQijtAxyEpyBPm0qxSwxO4SrtDUt9xYw6MLN0PfkiHjtCwtjpGlWUD4mbXOc2D1A== dependencies: ansi-colors "^4.1.3" -"@abp/bootstrap-datepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.5.0-rc.4.tgz#66d04f31a8d021d151452afc4fcbc7315d89758e" - integrity sha512-EMcZlWJWU+Ypbw3TGraC8K9hM3+HWLhmEz+0vj6yRajmP5MczJfCyxt4AODaPEmf4uRV3WQ3wV0OjHZdmG+QhQ== +"@abp/bootstrap-datepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-10.6.0-rc.1.tgz#693bb1c12fc92e6819d321b91e3dffccf24725e4" + integrity sha512-yXpLQRBaFJ6DOW1pt1GicQ+DmoD7bR3lTn3Qhv6I56DVSu5WqtFvbyPkLoMfgLpDvIyHPLXhgfI3ptvUMgjrag== dependencies: bootstrap-datepicker "^1.10.1" -"@abp/bootstrap-daterangepicker@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.5.0-rc.4.tgz#1aa514548b762d0b3bd628a29edd59585510532b" - integrity sha512-TnEg+k6XVWROtni7sXCBkouBjjDLmN7C0lP4XcUztSSHGo7l+r+cl+pi1iSOuHdRX304/CO+3PZUBKYku3A3+A== +"@abp/bootstrap-daterangepicker@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-daterangepicker/-/bootstrap-daterangepicker-10.6.0-rc.1.tgz#18e6aa031e2f381ca7f3a6abb2dd492ac51f5971" + integrity sha512-lbbKnz4cMEmHLXO/Q/8w0eALWQL9CCTkAvSqa3PT4cCGEGjfqt0sSZLRiKT7MwOqcgClomAqKlZR/zSMR1u0jQ== dependencies: - "@abp/moment" "~10.5.0-rc.4" + "@abp/moment" "~10.6.0-rc.1" bootstrap-daterangepicker "^3.1.0" -"@abp/bootstrap@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.5.0-rc.4.tgz#591b5a9da1f4fcf48c9a419f4acb985ddd48d876" - integrity sha512-aJtjmCaLg+TSrA/PL1X4d6AYsIv/8AHprDFJ6cXTsZkQTYjyTZUfjw15JHA+mLe6BionsGOtsXryid29IbX2Mg== +"@abp/bootstrap@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-10.6.0-rc.1.tgz#d58f0173e13a4b75678e290898da08d98447e939" + integrity sha512-+3C8A1B31b6llr09HHQ1hmohdL0aoJINP/R1OF/bO+b1AUp+lLcRsLci3dkmc0ImWhZ/xDzIpRf/lIcmrg+nzg== dependencies: - "@abp/core" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" bootstrap "^5.3.8" -"@abp/clipboard@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.5.0-rc.4.tgz#3d48c5e461997c50f532a8af6394d90de2a80c42" - integrity sha512-V77e/wptcMJbF3Zhki1/jGVAzlbd5W/Q0wWEa3xiSJqa97g8VmjNB+Kzz6FoTzAUHT2oBJllGO8d2+nhmqLl4g== +"@abp/clipboard@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-10.6.0-rc.1.tgz#b05335d32ce98395ed9d0dcf9fd50068d99ca064" + integrity sha512-Y/77csSlTFBnPwPTopYUgjv5Md5foz03aKozQE9EAgrTezjg48GAaJmZuvrA3BuHZcbNO9J56VsXbLwFKvQreA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" clipboard "^2.0.11" -"@abp/core@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.5.0-rc.4.tgz#1a5aec4eb83daf1400150d7c0c4dc81c7265ae49" - integrity sha512-AA4NdfAzA7zLWmAdS5dCjVshyidTn/bEMqlhfIOtWOnp3nEjdtURhmz6yA22MM6YAuKkda9iE1OtHu+jMEG3vQ== +"@abp/core@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-10.6.0-rc.1.tgz#08a7e8bf6fd995915c6783e65d9ccca950334ba1" + integrity sha512-VQZpj6hAHiluJbc6Yew9h1QDt8+ZAplzNi3/ialKh4MquDpIAL5lMdQSoExGtAbjlL4o6yRKSYV0Ge9kcYvWUw== dependencies: - "@abp/utils" "~10.5.0-rc.4" + "@abp/utils" "~10.6.0-rc.1" -"@abp/datatables.net-bs5@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.5.0-rc.4.tgz#ec26b8a22d58880755f3593ac635b0daf082c886" - integrity sha512-f5WEXTEQC8S73Hz2QpCMkrh9vyFJifQ2Q9Jp6r0iLRURpBItzliUsSHcDAhdo3oPtuH9HOumQvISH4Y8S+EPzQ== +"@abp/datatables.net-bs5@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs5/-/datatables.net-bs5-10.6.0-rc.1.tgz#32ff4be2cc3d48e4615565b4f1f02894d8e1acd5" + integrity sha512-4Zdj8JQBNQtgYWtuPqvHYm7pCgEV6fUSLd5KdERxef1/aWwab4tnio+5PJGcOg4yHsDRDrtz+t9KVZceizsMnw== dependencies: - "@abp/datatables.net" "~10.5.0-rc.4" + "@abp/datatables.net" "~10.6.0-rc.1" datatables.net-bs5 "^2.3.4" -"@abp/datatables.net@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.5.0-rc.4.tgz#46254de6e712640f12058e2f167083b3011e2c1b" - integrity sha512-i81nFF+Qevs602t0zZWAhQWBpCV9FBHixwWQmVDCfyhDYJbiMLUBPcscIqF7EIv1tz/dEdW2GofvEzIMcffbAg== +"@abp/datatables.net@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-10.6.0-rc.1.tgz#276061b09f53a26db646fc597536a30f38b59fe0" + integrity sha512-MJ/g1ag4TVfOJU4tkuTHr+ICxp7fY4MWsPFZ6DLYl/wE+qw1nq7NdYtk5OXWKZ4+5YRLoqumJFx93MwuQHFYfA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" datatables.net "^2.3.4" -"@abp/docs@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.5.0-rc.4.tgz#520938f87c245a11c6b61cbc72da6146deb17c08" - integrity sha512-+u4R/bLqrPE4NGUIdj6goHed5HtSFIf2Z2FUQj1GJEb/mOLZm3FF5h7lJ3p0E3ZjPydpFY41sYei0b2WyftqXg== +"@abp/docs@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-10.6.0-rc.1.tgz#f9ab2a97735c95ed645641f4ac526bfd00622ece" + integrity sha512-Tc6O2jkDn25FkUZzI5xV/6F9M7FcHQI5SnnFzHN985itxBfvLDyuUKVuvpOQjevVsHihsDqhCfMGcM1+2bRLMQ== dependencies: - "@abp/anchor-js" "~10.5.0-rc.4" - "@abp/clipboard" "~10.5.0-rc.4" - "@abp/malihu-custom-scrollbar-plugin" "~10.5.0-rc.4" - "@abp/popper.js" "~10.5.0-rc.4" - "@abp/prismjs" "~10.5.0-rc.4" + "@abp/anchor-js" "~10.6.0-rc.1" + "@abp/clipboard" "~10.6.0-rc.1" + "@abp/malihu-custom-scrollbar-plugin" "~10.6.0-rc.1" + "@abp/popper.js" "~10.6.0-rc.1" + "@abp/prismjs" "~10.6.0-rc.1" -"@abp/font-awesome@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.5.0-rc.4.tgz#d7a8e4d32f84bfbe46c55dc93ad33f185e0f3848" - integrity sha512-guGsjSWTlUYtrvNlekEL3qEgEp1HeABnNEQdQF5yyukHXoOTC0pR/aPTmMiKHTiYfqpc3LoQMv+TU+ZAVQrU4w== +"@abp/font-awesome@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-10.6.0-rc.1.tgz#ac61a181d73fdec1e5a0eeb7a0aa4fd8b020ed30" + integrity sha512-4A0IhbR94yJ0x+RuzvK3rQLLz9HUTM6i0KWE4+jJJEIHsSLoxgbgRphEDCxtfDTiBOfPOk7C8DSWKU+UyVxKlQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@fortawesome/fontawesome-free" "^7.0.1" -"@abp/jquery-validation-unobtrusive@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.5.0-rc.4.tgz#93bb721e8dfff9a824d44980f6ce859c8c3866bb" - integrity sha512-U1b+2lnfdNQIVBUM8CjbP/EJaLDZ7EHrvWeJFn0aJYHglV8Jqka4+jPu0Be8NP1NqyQQSrhtqdf8axp742COmQ== +"@abp/jquery-validation-unobtrusive@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-10.6.0-rc.1.tgz#8a111b4a69107d360785ae26a3a00fc7be201583" + integrity sha512-3SlIb/INaQz00mnpQXvVZBtAhMUzL7DUvAQN3cPLEcPtEn26OLk3VmzEKGmYpc1/JqTJFhvcylInqmeaOxK47g== dependencies: - "@abp/jquery-validation" "~10.5.0-rc.4" + "@abp/jquery-validation" "~10.6.0-rc.1" jquery-validation-unobtrusive "^4.0.0" -"@abp/jquery-validation@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.5.0-rc.4.tgz#b90ceb992b9c0a7fc4f8faccaaaa628c259f028e" - integrity sha512-0qYfsAwhcatyiJpeZejmG3DgOAoCbWQcHOUM0Bgxg8gh+vcuzXu//956SBJqbkeTJiaeY4z8uULaQZpAiF+ZRQ== +"@abp/jquery-validation@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-10.6.0-rc.1.tgz#bd30ba8e141141e230147b94c6fe2f71d21ae7ea" + integrity sha512-dSV26enWtDGTkqzY4nVCEndIVg7ElCLZ1MlX/RYB6YJI2T9/Gurl+nqdSUf3iZprrA9BGjXiYPmB6pitEqocZA== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" jquery-validation "^1.21.0" -"@abp/jquery@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.5.0-rc.4.tgz#623d7be22842b37505795e15069f0993f7c417eb" - integrity sha512-IATc1SVqRPd3xDCPHQZh1GaMgGGcVmxUYFDo/+t6c768iLCwLpJPuoNebrBSeN+z9+opbuZrFLNdwWQ+Ch72QA== +"@abp/jquery@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-10.6.0-rc.1.tgz#f900f1713747087b7617cec624d2d8122698d2c5" + integrity sha512-I9GrYDvfF7beSZqOG7jRuVVZseH1aQkdDzfLXZwQ6/2v5jxGX3mvUGljnHZHe2JIfyiS+2GGXWeHkjaFp5QIpg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" jquery "~3.7.1" -"@abp/lodash@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.5.0-rc.4.tgz#284d4ebf314cfb1d96e66e8994f22b44055e4130" - integrity sha512-rswQ/laMhlu1MA+51CcIbQR1iZcOdSwF2HzxWccCqsaBVCOWy4G9TIj1gk2QfQSYMkBl6OWNprOea7XKhaSfzw== +"@abp/lodash@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-10.6.0-rc.1.tgz#3f009621c6c649f300d4aee9c7598b0a1ca3d176" + integrity sha512-ySdJb3wHfMX7ikAJ/Tv1Xk4ULbCH9WJbU1IT8vaW3bINuCKpEeJ1RsxR48cfn/J7v2imbzZianLXavI7wtMQUA== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" lodash "^4.18.1" -"@abp/luxon@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.5.0-rc.4.tgz#c35120586c16afbafdefbb0a4b1cefb3e7e97690" - integrity sha512-Mk5oMzecFf5v35/EW2FvVYmQX16giT8J/QpeqyaPWbp0UptOSYrA7E+gHoJGvs3SvY24KkPzTfdcaWzCyV6zIg== +"@abp/luxon@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-10.6.0-rc.1.tgz#c7179fb674739df97b072dd922c12671ce43d6e7" + integrity sha512-529BediwCRVVX+Wx8fnjnAusiZl6E3EuoCPOC4tbcUmiz6nv/5NPNEN6W4Whl52+ilkR+lgqNBZs0a4rLBfASg== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" luxon "^3.7.2" -"@abp/malihu-custom-scrollbar-plugin@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.5.0-rc.4.tgz#f7e215c48012306d449544be35b384face75caa8" - integrity sha512-nu00haqvL68AnGIX0enLy3XvhDq72/ws7f+oh8Fr5gPPtywI1fzMnkXIGoXewE/5wcD7Fs/hHjJorFlX0yWOgw== +"@abp/malihu-custom-scrollbar-plugin@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-10.6.0-rc.1.tgz#8a33c49b5eb57ac602925bd8cac2caa99d8542bb" + integrity sha512-kiqR2B2kQthrhZl6HIOE/FyzQkGbNRsHhKqouR6vu+NENBBtcvhdAiqrBpI3OKDFGb8MCcyYZAJYSgQ/k9WEkw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/moment@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.5.0-rc.4.tgz#4d02915b3650b8afe1160ab91c09d274bd9108d6" - integrity sha512-6giU879bnFltkefbb/it4bWxBfZn+QDzDgP1aPC4xNIa134ZXzkcSMRlMyUBa/96OZ6mmmYo/6wLFZLCAsOPZw== +"@abp/moment@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/moment/-/moment-10.6.0-rc.1.tgz#2a78ecd9ccab9010a032a07e0123727b05ffbbae" + integrity sha512-/ecQSjKfgZpRhI6tuQRXXlFPBcgSWIK8MlKZUskrzDqHKAq4DO6fQrnObNod/M9Cw74dEKwaRhtmnlgdttBKsw== dependencies: moment "^2.30.1" -"@abp/popper.js@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.5.0-rc.4.tgz#4a74c6e1f72da2ba8f67b33856196558a5efff72" - integrity sha512-oJ4pDrVCgu0RadsdwlppGN3tPzMymQ5DWhqK6CKEeir4lBFubKDe1NAhawZ4qGtIXKFWIbE30prFg11pf/zj9Q== +"@abp/popper.js@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-10.6.0-rc.1.tgz#959104bb04204c6890e3c37b54629b3bf828ba3b" + integrity sha512-6ZK/Ybn+nFGFzNAz2f5x5T6t34fnEjrze1BEfwS4Od605oFHdBI5pyKRltc22GNT55KmQNgaeuk7EXhezPKmvQ== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" "@popperjs/core" "^2.11.8" -"@abp/prismjs@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.5.0-rc.4.tgz#d130f9e872eef068da63497b53ef1823347dc403" - integrity sha512-epb+8m21G8glvjHh2NgzO6kK3JDgliwXmfyIvRBg2/dUyovCyQ9A4V9jOA0VfaqTHz7sl0liM7M66/gP3XiGhA== +"@abp/prismjs@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-10.6.0-rc.1.tgz#e862f6c7e74a1db4ce34a67ca24f3dc9be5fdb37" + integrity sha512-pjFezZHnmNv+v7dgUWGGwP0+BXUgtlZqeNebaYwST5sdGHQw0zQ5GqY3yISlrA32OWjGvDrI3+7aOKIDoPxySA== dependencies: - "@abp/clipboard" "~10.5.0-rc.4" - "@abp/core" "~10.5.0-rc.4" + "@abp/clipboard" "~10.6.0-rc.1" + "@abp/core" "~10.6.0-rc.1" prismjs "^1.30.0" -"@abp/select2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.5.0-rc.4.tgz#87448d8b1d2ec97c0cb97e1c08db3ba2618e29f7" - integrity sha512-64utn6ikY5q71TIfO9qhXWpy0pGrj1YEpAdO/q3qYoFYf9RDz9mSFd2cJ8Noasph/CmZSKFK3YwPcHFkSeLTYQ== +"@abp/select2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-10.6.0-rc.1.tgz#3f140edc6c8cd4d3f2c5c210210f8b6e7c3acd11" + integrity sha512-OgTPq/acSbQiSbuHIU4F86FsEcoHMLUryKCtA/VUxaf4r0DsKNr6eJLOEyDkHSPAI1GErFn7NQAny0h5VkM2Pw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" select2 "^4.0.13" -"@abp/sweetalert2@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.5.0-rc.4.tgz#aa667ee6edb6b008961a463f7f477a4671d28381" - integrity sha512-+/OKRL2hzohXryUD2rZO7aFLL7G7A300/QK82BKnYSXr7fh2wsVEC9g4/DXnaoygad7k5KS3NWUMIpEL21ROpw== +"@abp/sweetalert2@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/sweetalert2/-/sweetalert2-10.6.0-rc.1.tgz#3be5c41239be5ddfb2a968b7c0de11eff7fb0f26" + integrity sha512-2S7aadCx/TBWl5tHaM4iMbMmSrFORRupER+gdHrAogA8fjXudIGNIgISlEHMuE+YiL1uAk6eOuWMW09mxCCJfw== dependencies: - "@abp/core" "~10.5.0-rc.4" + "@abp/core" "~10.6.0-rc.1" sweetalert2 "^11.23.0" -"@abp/timeago@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.5.0-rc.4.tgz#02b27e97c20b6b226cbdaa43d7bfb367218a626f" - integrity sha512-XCopnK5kphD6+OQ6+RHsgZHL7WtaXBbiYHpQmo7JHr9iVBwvLmWSzJZMIbrfZTczkhHk2VEwMQWSUwnJsWCWvQ== +"@abp/timeago@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-10.6.0-rc.1.tgz#404d388698fa1ab31d7e8e8b1bbc393c92f4d3be" + integrity sha512-5yBy9Lu9IW0nnw01fRvpfOXLtcIP0uH5Rt6JDWATqwpHXWteJ9LOggJNCC44I4aYZb82KDhvpnP5F6cPt8D1ew== dependencies: - "@abp/jquery" "~10.5.0-rc.4" + "@abp/jquery" "~10.6.0-rc.1" timeago "^1.6.7" -"@abp/utils@~10.5.0-rc.4": - version "10.5.0-rc.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.5.0-rc.4.tgz#3bddb096f7537b69c08c2c81c99607519b58c200" - integrity sha512-jYNIF72jXBYHWxrHTe3Na5yhPm1e+b76U0PpA2chJ+Qs6LHWdwQXGGvplQO3KrInrA+dNqrWzera7a1N9U1WhQ== +"@abp/utils@~10.6.0-rc.1": + version "10.6.0-rc.1" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-10.6.0-rc.1.tgz#e8fdb1b398bce3a797703ee5a8287f978de5654e" + integrity sha512-YbWuFpnOtp0vqbPaPYoRQhGnVrJQr8Khp7V49H/CiGwjkMf7Ln6tCBcTzFa7nXNEckzMxxr6wOFlpRfLX1DBaw== dependencies: just-compare "^2.3.0" diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs index 8d301395c0..3a770e6781 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs @@ -173,7 +173,7 @@ namespace Volo.Docs.GitHub.Documents { if (commits == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } var gitHubCommit = isFirstCommit ? @@ -182,20 +182,20 @@ namespace Volo.Docs.GitHub.Documents if (gitHubCommit == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } if (gitHubCommit.Commit == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } if (gitHubCommit.Commit.Author == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } - return gitHubCommit.Commit.Author.Date.DateTime; + return gitHubCommit.Commit.Author.Date.UtcDateTime; } private async Task> GetGitHubCommitsOrNull(Project project, string documentName, string languageCode, string version) @@ -230,8 +230,8 @@ namespace Volo.Docs.GitHub.Documents var fileCommitsAfterCreation = commits.Take(commits.Count - 1); var commitsToEvaluate = (lastKnownSignificantUpdateTime != null - ? fileCommitsAfterCreation.Where(c => c.Commit.Author.Date.DateTime > lastKnownSignificantUpdateTime) - : fileCommitsAfterCreation).Where(c => c.Commit.Author.Date.DateTime > DateTime.Now.AddDays(-14)); + ? fileCommitsAfterCreation.Where(c => c.Commit.Author.Date.UtcDateTime > lastKnownSignificantUpdateTime) + : fileCommitsAfterCreation).Where(c => c.Commit.Author.Date.UtcDateTime > DateTime.UtcNow.AddDays(-14)); foreach (var gitHubCommit in commitsToEvaluate) { @@ -243,7 +243,7 @@ namespace Volo.Docs.GitHub.Documents if (_githubPatchAnalyzer.HasPatchSignificantChanges(fullCommit.Files.First(f => f.Filename == fileName).Patch)) { - return gitHubCommit.Commit.Author.Date.DateTime; + return gitHubCommit.Commit.Author.Date.UtcDateTime; } } diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ar.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ar.json index f7d3533653..38b56e96b3 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ar.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ar.json @@ -29,6 +29,7 @@ "FullSearch": "ابحث في المستندات", "Volo.Docs.Domain:010001": "لم يتم تمكين البحث المرن.", "MultipleVersionDocumentInfo": "هذا المستند له إصدارات متعددة. حدد الخيارات الأنسب لك.", + "DocumentOptions": "خيارات المستند", "New": "جديد", "Upd": "مُحدث", "NewExplanation": "تم إنشاؤه في الأسبوعين الماضيين.", @@ -41,4 +42,4 @@ "GoogleTranslate": "ترجمة جوجل", "DownloadPDF": "تحميل PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/cs.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/cs.json index ed6c8767b8..a4a16fc883 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/cs.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/cs.json @@ -29,6 +29,7 @@ "FullSearch": "Hledat v dokumentech", "Volo.Docs.Domain:010001": "Elastické vyhledávání není povoleno.", "MultipleVersionDocumentInfo": "Tento dokument má více verzí. Vyberte možnosti, které vám nejlépe vyhovují.", + "DocumentOptions": "Možnosti dokumentu", "New": "Nové", "Upd": "Akt", "NewExplanation": "Vytvořeno v posledních dvou týdnech.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Přeložit do češtiny", "DownloadPDF": "Stáhnout PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/de.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/de.json index 6a614441eb..10ff142b20 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/de.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/de.json @@ -29,6 +29,7 @@ "FullSearch": "In Dokumenten suchen", "Volo.Docs.Domain:010001": "Elastic search ist nicht aktiviert.", "MultipleVersionDocumentInfo": "Dieses Dokument hat mehrere Versionen. Wählen Sie die für Sie am besten geeigneten Optionen aus.", + "DocumentOptions": "Dokumentoptionen", "New": "Neu", "Upd": "Upd", "NewExplanation": "Erstellt in den letzten zwei Wochen.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Ins Deutsche übersetzen", "DownloadPDF": "PDF herunterladen" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/el.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/el.json index 2a1a72dca2..e2c8ce222e 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/el.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/el.json @@ -29,6 +29,7 @@ "FullSearch": "Αναζήτηση σε έγγραφα", "Volo.Docs.Domain:010001": "Η ελαστική αναζήτηση δεν είναι ενεργοποιημένη.", "MultipleVersionDocumentInfo": "Αυτό το έγγραφο έχει πολλές εκδόσεις. Επιλέξτε τις επιλογές που σας ταιριάζουν καλύτερα.", + "DocumentOptions": "Επιλογές εγγράφου", "New": "Νέο", "Upd": "Επικαιροποιημένο", "NewExplanation": "Δημιουργήθηκε τις τελευταίες δύο εβδομάδες", @@ -40,4 +41,4 @@ "GoogleTranslate": "Μετάφραση στα ελληνικά", "DownloadPDF": "Κατέβασμα PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en-GB.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en-GB.json index d56e8da6df..884d9ce8ba 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en-GB.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en-GB.json @@ -29,6 +29,7 @@ "FullSearch": "Search in documents", "Volo.Docs.Domain:010001": "Elastic search is not enabled.", "MultipleVersionDocumentInfo": "This document has multiple versions. Select the options that best fit you.", + "DocumentOptions": "Document Options", "New": "New", "Upd": "Upd", "NewExplanation": "Created in the last two weeks.", diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json index 6715146b50..9859a7a4a2 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/en.json @@ -29,6 +29,7 @@ "FullSearch": "Search in documents", "Volo.Docs.Domain:010001": "Elastic search is not enabled.", "MultipleVersionDocumentInfo": "There are multiple versions of this document. Pick the options that suit you best.", + "DocumentOptions": "Document Options", "New": "New", "Upd": "Upd", "NewExplanation": "Created in the last two weeks.", diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/es.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/es.json index 3dcd97e7fe..451cc363aa 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/es.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/es.json @@ -29,6 +29,7 @@ "FullSearch": "Buscar en documentos", "Volo.Docs.Domain:010001": "Búsqueda elástica no habilida", "MultipleVersionDocumentInfo": "Este documento tiene múltiple versiones. Selecciona las opciones que más te interesan.", + "DocumentOptions": "Opciones del documento", "New": "Nuevo", "Upd": "Upd", "NewExplanation": "Creado en las últimas dos semanas.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Traducir con Google", "DownloadPDF": "Descargar PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fi.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fi.json index db74d481a0..4abfa3082f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fi.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fi.json @@ -29,6 +29,7 @@ "FullSearch": "Hae asiakirjoista", "Volo.Docs.Domain:010001": "Elastinen haku ei ole käytössä.", "MultipleVersionDocumentInfo": "Tällä asiakirjalla on useita versioita. Valitse sinulle parhaiten sopivat vaihtoehdot.", + "DocumentOptions": "Asiakirjan asetukset", "New": "Uusi", "Upd": "Päivitykset", "NewExplanation": "Luotu kahden viime viikon aikana.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google-käännös", "DownloadPDF": "Lataa PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fr.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fr.json index 1bab402f73..f337478461 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fr.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/fr.json @@ -29,6 +29,7 @@ "FullSearch": "Rechercher dans les documents", "Volo.Docs.Domain:010001": "La recherche élastique n'est pas activée.", "MultipleVersionDocumentInfo": "Ce document a plusieurs versions. Sélectionnez les options qui vous conviennent le mieux.", + "DocumentOptions": "Options du document", "New": "Nouveau", "Upd": "Mettre à jour", "NewExplanation": "Créé au cours des deux dernières semaines.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google Traduction", "DownloadPDF": "Télécharger PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hi.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hi.json index 2ba78ec460..19e0a89278 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hi.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hi.json @@ -29,6 +29,7 @@ "FullSearch": "दस्तावेजों में खोजें", "Volo.Docs.Domain:010001": "लोचदार खोज सक्षम नहीं है।", "MultipleVersionDocumentInfo": "इस दस्तावेज़ के कई संस्करण हैं। आपके लिए सबसे उपयुक्त विकल्प का चयन करें।", + "DocumentOptions": "दस्तावेज़ विकल्प", "New": "नवीन व", "Upd": "युपीडी", "NewExplanation": "पिछले दो हफ्तों में बनाया गया।", @@ -41,4 +42,4 @@ "GoogleTranslate": "गूगल अनुवाद", "DownloadPDF": "PDF डाउनलोड करें" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hr.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hr.json index 1061dee7ab..c699f2ba50 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hr.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hr.json @@ -29,6 +29,7 @@ "FullSearch": "Traži u dokumentima", "Volo.Docs.Domain:010001": "Elastic search nije omogućen.", "MultipleVersionDocumentInfo": "Postoji više verzija ovog dokumenta. Odaberite opcije koje vam najbolje odgovaraju.", + "DocumentOptions": "Opcije dokumenta", "New": "Novi", "Upd": "Upd", "NewExplanation": "Stvoreno u posljednja dva tjedna.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google prevoditelj", "DownloadPDF": "Preuzmi PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hu.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hu.json index b8dee63ca4..f61ad53e0c 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hu.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/hu.json @@ -29,6 +29,7 @@ "FullSearch": "Keresés a dokumentumokban", "Volo.Docs.Domain:010001": "A Elastic search nincs engedélyezve.", "MultipleVersionDocumentInfo": "Ennek a dokumentumnak több változata van. Válassza ki az Ön számára legmegfelelőbb lehetőségeket.", + "DocumentOptions": "Dokumentumbeállítások", "New": "Új", "Upd": "Mód", "NewExplanation": "Az elmúlt két hétben készült.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google Fordító", "DownloadPDF": "PDF letöltése" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/is.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/is.json index 62235d3f62..ca4d3f184f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/is.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/is.json @@ -29,6 +29,7 @@ "FullSearch": "Leita í öllum skjölum", "Volo.Docs.Domain:010001": "Teygjanleg leit er ekki virkjuð.", "MultipleVersionDocumentInfo": "Þetta skjal er með margar útgáfur. Veldu þá valkosti sem henta þér best.", + "DocumentOptions": "Skjalastillingar", "New": "Nýtt", "Upd": "Upd", "NewExplanation": "Búið til á síðustu tveimur vikum.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google þýðing", "DownloadPDF": "Sækja PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/it.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/it.json index 91ef732c01..05c3a1af8f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/it.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/it.json @@ -29,6 +29,7 @@ "FullSearch": "Cerca nei documenti", "Volo.Docs.Domain:010001": "La ricerca elastica non è abilitata.", "MultipleVersionDocumentInfo": "Questo documento ha più versioni. Seleziona le opzioni più adatte a te.", + "DocumentOptions": "Opzioni del documento", "New": "Nuovo", "Upd": "Agg", "NewExplanation": "Creato nelle ultime due settimane.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Traduci con Google", "DownloadPDF": "Scarica PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/nl.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/nl.json index 48a82699d7..b6a47a9b6d 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/nl.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/nl.json @@ -29,6 +29,7 @@ "FullSearch": "Zoek in documenten", "Volo.Docs.Domain:010001": "Elastisch zoeken is niet ingeschakeld.", "MultipleVersionDocumentInfo": "Dit document heeft meerdere versies. Selecteer de opties die het beste bij u passen.", + "DocumentOptions": "Documentopties", "New": "Nieuw", "Upd": "Bew", "NewExplanation": "Gemaakt in de afgelopen twee weken.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google Vertalen", "DownloadPDF": "PDF downloaden" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pl-PL.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pl-PL.json index 35ae3e6801..7ed8cf3aff 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pl-PL.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pl-PL.json @@ -29,6 +29,7 @@ "FullSearch": "Szukaj w dokumentach", "Volo.Docs.Domain:010001": "Wyszukiwanie elastyczne nie jest włączone.", "MultipleVersionDocumentInfo": "Ten dokument ma wiele wersji. Wybierz opcje, które najbardziej Ci odpowiadają.", + "DocumentOptions": "Opcje dokumentu", "New": "Nowy", "Upd": "Aktualizacja", "NewExplanation": "Utworzony w ciągu ostatnich dwóch tygodni.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Tłumaczenie na polski", "DownloadPDF": "Pobierz PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pt-BR.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pt-BR.json index 497f0c93e2..aab4f4f7d9 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pt-BR.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/pt-BR.json @@ -29,6 +29,7 @@ "FullSearch": "Pesquisa em documentos", "Volo.Docs.Domain:010001": "A pesquisa elástica não está ativada.", "MultipleVersionDocumentInfo": "Este documento possui várias versões. Selecione as opções mais adequadas para você.", + "DocumentOptions": "Opções do documento", "New": "Novo", "Upd": "Atualizar", "NewExplanation": "Criado nas últimas duas semanas.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Traduzir com Google", "DownloadPDF": "Baixar PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ro-RO.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ro-RO.json index 155dad0754..e6b9afa522 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ro-RO.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ro-RO.json @@ -29,6 +29,7 @@ "FullSearch": "Caută în documente", "Volo.Docs.Domain:010001": "Căutarea elastică nu este activată.", "MultipleVersionDocumentInfo": "Acest document are multiple versiuni. Selectaţi opţiunile cele mai potrivite pentru dumneavoastră.", + "DocumentOptions": "Opțiuni document", "New": "Nou", "Upd": "Upd", "NewExplanation": "Creat în ultimele două săptămâni.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Traducere Google", "DownloadPDF": "Descărcare PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ru.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ru.json index 2b5a200187..aa124e4666 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ru.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/ru.json @@ -29,6 +29,7 @@ "FullSearch": "Искать в документах", "Volo.Docs.Domain:010001": "Эластичный поиск не включен.", "MultipleVersionDocumentInfo": "У этого документа есть несколько версий. Выберите наиболее подходящие для вас варианты.", + "DocumentOptions": "Параметры документа", "New": "Новый", "Upd": "Upd", "NewExplanation": "Создано за последние две недели.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google Translate", "DownloadPDF": "Скачать PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sk.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sk.json index 8f0f246a16..5f4ddcafe7 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sk.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sk.json @@ -29,6 +29,7 @@ "FullSearch": "Vyhľadať v dokumentoch", "Volo.Docs.Domain:010001": "Elastic vyhľadávanie nie je povolené.", "MultipleVersionDocumentInfo": "Tento dokument má viacero verzií. Vyberte si možnosť, ktorá vám vyhovuje najviac.", + "DocumentOptions": "Možnosti dokumentu", "New": "Nový", "Upd": "Upd", "NewExplanation": "Vytvorené za posledné dva týždne.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google preklad", "DownloadPDF": "Stiahnuť PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sl.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sl.json index c182e66b3e..e8b559db6f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sl.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sl.json @@ -29,6 +29,7 @@ "FullSearch": "Išči v dokumentih", "Volo.Docs.Domain:010001": "Elastično iskanje ni omogočeno.", "MultipleVersionDocumentInfo": "Ta dokument ima več verzij. Izberite možnosti, ki so najprimernejše.", + "DocumentOptions": "Možnosti dokumenta", "New": "Nov", "Upd": "Posod.", "NewExplanation": "Ustvarjeno v zadnjih dveh tednih.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Google prevajalnik", "DownloadPDF": "Prenesi PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sv.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sv.json index 29db42be57..0c543da150 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sv.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/sv.json @@ -29,6 +29,7 @@ "FullSearch": "Sök i dokument", "Volo.Docs.Domain:010001": "Elastisk sökning är inte aktiverad.", "MultipleVersionDocumentInfo": "Det finns flera versioner av detta dokument. Välj de alternativ som passar dig bäst.", + "DocumentOptions": "Dokumentalternativ", "New": "Nya", "Upd": "Uppdatering", "NewExplanation": "Skapad under de senaste två veckorna.", @@ -46,4 +47,4 @@ "GoogleTranslate": "Google Översätt", "DownloadPDF": "Ladda ner PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/tr.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/tr.json index 35b08c6a62..763448fb00 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/tr.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/tr.json @@ -29,6 +29,7 @@ "FullSearch": "Dokümanlarda Ara", "Volo.Docs.Domain:010001": "Elastic search etkin değil", "MultipleVersionDocumentInfo": "Bu dökümanın birden çok versiyonu bulunmaktadır. Sizin için en uygun olan seçenekleri seçiniz.", + "DocumentOptions": "Belge Seçenekleri", "New": "Yeni", "Upd": "Günc", "NewExplanation": "Son iki hafta içinde oluşturuldu.", diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/vi.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/vi.json index d6916d05ad..0e50fd8569 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/vi.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/vi.json @@ -29,6 +29,7 @@ "FullSearch": "Tìm kiếm trong tài liệu", "Volo.Docs.Domain:010001": "Tìm kiếm đàn hồi không được bật.", "MultipleVersionDocumentInfo": "Tài liệu này có nhiều phiên bản. Chọn các tùy chọn phù hợp nhất với bạn.", + "DocumentOptions": "Tùy chọn tài liệu", "New": "Mới", "Upd": "Cập nhật", "NewExplanation": "Được tạo trong hai tuần qua.", @@ -41,4 +42,4 @@ "GoogleTranslate": "Dịch Google", "DownloadPDF": "Tải xuống PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hans.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hans.json index d214b9b4a6..d462a22ae5 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hans.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hans.json @@ -29,6 +29,7 @@ "FullSearch": "在文档中搜索", "Volo.Docs.Domain:010001": "未启用Elastic search。", "MultipleVersionDocumentInfo": "本文档有多个版本。请选择最适合您的选项。", + "DocumentOptions": "文档选项", "New": "新", "Upd": "更新文档", "NewExplanation": "最近两周创建。", @@ -41,4 +42,4 @@ "GoogleTranslate": "谷歌翻译", "DownloadPDF": "下载PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hant.json b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hant.json index ee4a291b61..9bc8aa6230 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hant.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Localization/Domain/zh-Hant.json @@ -29,6 +29,7 @@ "FullSearch": "搜索文件", "Volo.Docs.Domain:010001": "Elastic search未啟用.", "MultipleVersionDocumentInfo": "此份文件有多個版本。請選擇一個最適合的。", + "DocumentOptions": "文件選項", "New": "新文檔", "Upd": "更新", "NewExplanation": "在最近兩周內創建.", @@ -41,4 +42,4 @@ "GoogleTranslate": "谷歌翻譯", "DownloadPDF": "下載PDF" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index a68298a431..e031837165 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -482,6 +482,13 @@ @if (visibleParameters.Count > 0) {
+
+ @L["DocumentOptions"] + +
+

@@ -534,6 +541,7 @@ } } +

} diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js index c9bfb3e18f..b4dc38de81 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Scripts/vs.js @@ -175,8 +175,51 @@ var docsContentWidth = $('.docs-content').width() - 74; $('.alert-criteria').width(docsContentWidth); } + + // Toggle the collapsed document-options popover by click/tap (works on touch, unlike a hover-only reveal). + function docsOptionsToggle() { + var $criteria = $('.alert-criteria'); + var $btn = $criteria.find('.options-header .toggle-btn'); + if (!$criteria.length || !$btn.length) { + return; + } + function setOpen(isOpen) { + $criteria.toggleClass('is-open', isOpen); + $btn.attr('aria-expanded', isOpen ? 'true' : 'false'); + } + // clear any previous bindings so a second init (e.g. partial reload) does not stack handlers + $btn.off('.docsOptions'); + $criteria.off('.docsOptions'); + $(document).off('.docsOptions'); + $(window).off('.docsOptions'); + $btn.on('click.docsOptions', function (e) { + e.preventDefault(); + e.stopPropagation(); + setOpen(!$criteria.hasClass('is-open')); + }); + $criteria.on('click.docsOptions', function (e) { + e.stopPropagation(); + }); + $(document).on('click.docsOptions', function () { + setOpen(false); + }); + $(document).on('keydown.docsOptions', function (e) { + if (e.key === 'Escape' && $criteria.hasClass('is-open')) { + setOpen(false); + // keyboard close: return focus to the button (focus ring is appropriate here) + $btn.trigger('focus'); + } + }); + $(window).on('scroll.docsOptions', function () { + if (!$('body').hasClass('scrolledMore')) { + setOpen(false); + } + }); + } + $(document).ready(function () { docsCriteria(); + docsOptionsToggle(); }); $(window).resize(function () { docsCriteria(); diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css index 71840b79d2..e0f242df24 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css @@ -52,19 +52,136 @@ body .mCS-autoHide > .mCustomScrollBox ~ .mCSB_scrollTools { filter: "alpha(opacity=1)"; -ms-filter: "alpha(opacity=1)"; } +body .alert-criteria .options-header { + display: none; +} body.scrolledMore { padding-top: 107px; } body.scrolledMore .alert-criteria { + position: fixed !important; + left: auto !important; + right: 20px !important; + bottom: 90px !important; + width: 360px !important; + max-width: calc(100vw - 40px) !important; + margin: 0 !important; + padding: 16px 18px 18px !important; + border-radius: 16px !important; + color: #2d2d2d !important; + z-index: 9999 !important; + visibility: hidden !important; + transition: none !important; + pointer-events: none; + background: transparent !important; + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + border: 0 !important; + box-shadow: none !important; +} +body.scrolledMore .alert-criteria .options-header { + display: block !important; + color: #6f42c1 !important; + font-weight: 700; +} +body.scrolledMore .alert-criteria .options-header .toggle-btn { position: fixed; - bottom: 0; - z-index: 10; - border: 0; - margin-left: 0; + right: 20px; + bottom: 20px; + width: 60px; + height: 60px; + border-radius: 50%; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + color: #6f42c1; + background: linear-gradient(135deg, #ffffff 0%, #f5f0ff 100%); + border: 1px solid #d9ccf2; + box-shadow: 0 8px 22px rgba(111, 66, 193, 0.16), 0 2px 6px rgba(31, 41, 55, 0.08); + visibility: visible; + pointer-events: auto; + cursor: pointer; + z-index: 9999; + transition: transform 0.2s ease, box-shadow 0.2s ease; } -body.scrolledMore .alert-criteria p.alert-p { +body.scrolledMore .alert-criteria .options-header .toggle-btn > i { display: none; } +body.scrolledMore .alert-criteria .options-header .toggle-btn::before { + content: "\f1de"; + font-family: "Font Awesome 6 Free", "Font Awesome 6 Pro", "FontAwesome"; + font-weight: 900; + font-size: 24px; +} +body.scrolledMore .alert-criteria .options-header .toggle-btn:hover { + transform: translateY(-2px); + box-shadow: 0 12px 28px rgba(111, 66, 193, 0.28); +} +body.scrolledMore .alert-criteria .options-header .toggle-btn:focus-visible { + outline: 2px solid #6f42c1; + outline-offset: 2px; +} +body.scrolledMore .alert-criteria.is-open { + visibility: visible !important; + pointer-events: auto; + background: #fff !important; + border: 1px solid #e6def7 !important; + box-shadow: 0 16px 40px rgba(40, 20, 80, 0.18) !important; +} +body.scrolledMore .alert-criteria.is-open .options-header { + display: flex !important; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} +body.scrolledMore .alert-criteria.is-open .options-header .toggle-btn::before { + content: "\f00d"; + font-size: 22px; +} +body.scrolledMore .alert-criteria:not(.is-open), body.scrolledMore .alert-criteria:not(.is-open):hover, body.scrolledMore .alert-criteria:not(.is-open):focus-within { + background: transparent !important; + border: 0 !important; + box-shadow: none !important; +} +body.scrolledMore .alert-criteria p.alert-p { + margin: 0 0 12px !important; + color: #6f42c1 !important; + font-weight: 600; +} +body.scrolledMore .alert-criteria .row > div { + width: 100% !important; + max-width: 100% !important; + flex: 0 0 100% !important; + margin-bottom: 1rem !important; +} +body.scrolledMore .alert-criteria .row > div:last-child { + margin-bottom: 0 !important; +} +@media (max-width: 768px) { + body.scrolledMore .alert-criteria { + right: 10px !important; + bottom: 75px !important; + width: calc(100vw - 20px) !important; + } + body.scrolledMore .alert-criteria .options-header .toggle-btn { + right: 10px; + bottom: 10px; + width: 55px; + height: 55px; + } + body.scrolledMore .alert-criteria .options-header .toggle-btn::before { + font-size: 22px; + } +} +body.scrolledMore:has(#abp-chatbot-container) .alert-criteria .options-header .toggle-btn { + right: 92px; +} +@media (max-width: 768px) { + body.scrolledMore:has(#abp-chatbot-container) .alert-criteria .options-header .toggle-btn { + right: 75px; + } +} .docs-page { padding: 0 1rem; diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css.map b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css.map index 9892f76098..bf55acaf25 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css.map +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["vs.scss"],"names":[],"mappings":"AAGA;EACI;EACA;;AAEA;EACI;EACA;;AAIJ;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAIA;EACI;;AAGJ;AAAA;AAAA;AAAA;EAII;;AAIR;AAAA;EAEI;;AAGJ;EACI;;AAEA;EACI;;AAEJ;EACI;;AAIR;AAAA;EAEI;EACA;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;;AAMhB;EACI;;AAEA;EACI;EACA;EACA,KA9FK;EA+FL,QA/FK;EAgGL;EACA;EACA;EACA;EACA;EACA;EACA;;AAIQ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAMhB;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;;AAEJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAII;;AAIR;EACI;EACA;;AAEJ;EACI;EACA;;AAEA;EACI;;AAIR;EACI;;AAEA;EAII;;AAIR;EACI;EACA;EACA;;AAIR;EACI;;AAIR;EAEI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAIR;EACI;;AAGI;EACI;;AAGJ;EACI;;AAGI;EACI;;AAQhB;EACI;EACA;;AAIA;EACI;EACA;;AAMA;EACI;;AAOpB;EACI;;AAKhB;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AACA;EACI;EACA;EACA;EACA;EACA;;AAMA;EACI;EACA;EACA;EACA;EACA;;AACA;EACI;;AAEJ;EACI;EACA;;AAMhB;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAEJ;EACI;;AAGJ;EACI;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAMA;EACI;;AAQhB;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;EAII;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;EAMI;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAGJ;AAAA;EAEI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AACA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAMR;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;EACA;EACA,KAtsBC;EAusBD;EACA;EACA;;AAGI;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAMA;EACI;;AAMA;EACI;;AAQxB;EACI;;AAIR;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA,eAlxBC;EAmxBD;;AAEA;EACI;EACA;EACA;;AACA;EACI;;AAGR;EACI;EACA,SAhyBH;;AAkyBD;EACI;EACA;EACA;;AAGJ;EACI;;;AAMhB;EACI;IACI;;EAEA;IACI;;EAGJ;IACI;;EAGJ;AAAA;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;;EAQR;IACI;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAGI;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAIR;IACI;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;;EAEA;IACI;;EAKZ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAIR;IACI;IACA;IACA;;EAIR;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAMZ;IACI;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAGJ;IACI;;EAIA;IACI;;EAEJ;IACI;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAKZ;IACI;IACA;IACA;;EAEA;IACI;IACA;;EAIR;IACI;;EAEA;IACI;;;AAMhB;EAIgB;IACI;;EAEJ;IACI;;EAMI;IACI;IACA;;;AAS5B;EAEQ;IACI;IACA;;EAEJ;IAII;;EAHA;IACI;;;AAOhB;EAEQ;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA","file":"vs.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["vs.scss"],"names":[],"mappings":"AAGA;EACI;EACA;;AAEA;EACI;EACA;;AAIJ;EACI;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAGJ;EACI;EACA;EACA;;AAIA;EACI;;AAGJ;AAAA;AAAA;AAAA;EAII;;AAIR;AAAA;EAEI;;AAGJ;EACI;;AAEA;EACI;;AAEJ;EACI;;AAIR;AAAA;EAEI;EACA;EACA;;AAGJ;EACI;;AAKA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EAEI;EACA;;AAIJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EAEI;EACA;EACA;EACA;EACA;;AAIJ;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EA5GJ;IA6GQ;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;;AAOR;EAEI;;AAGJ;EACI;IAEI;;;;AAOpB;EACI;;AAEA;EACI;EACA;EACA,KAtOK;EAuOL,QAvOK;EAwOL;EACA;EACA;EACA;EACA;EACA;EACA;;AAIQ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAMhB;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;;AAEJ;EACI;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;AAAA;EAEI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAII;;AAIR;EACI;EACA;;AAEJ;EACI;EACA;;AAEA;EACI;;AAIR;EACI;;AAEA;EAII;;AAIR;EACI;EACA;EACA;;AAIR;EACI;;AAIR;EAEI;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;AAKZ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAIR;EACI;;AAGI;EACI;;AAGJ;EACI;;AAGI;EACI;;AAQhB;EACI;EACA;;AAIA;EACI;EACA;;AAMA;EACI;;AAOpB;EACI;;AAKhB;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AACA;EACI;EACA;EACA;EACA;EACA;;AAMA;EACI;EACA;EACA;EACA;EACA;;AACA;EACI;;AAEJ;EACI;EACA;;AAMhB;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAEJ;EACI;;AAGJ;EACI;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAIR;EACI;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;AAKJ;EACI;EACA;EACA;;AAGJ;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAMA;EACI;;AAQhB;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;EAII;EACA;EACA;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;EAMI;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAIA;AAAA;AAAA;AAAA;AAAA;AAAA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGI;EACI;;AAIR;EACI;;AAGJ;AAAA;EAEI;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAGJ;EACI;;AAKZ;EACI;EACA;EACA;EACA;;AACA;EACI;EACA;EACA;EACA;EACA;;AAEJ;EACI;EACA;EACA;EACA;;AACA;EACI;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAMR;EACI;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;;AAKZ;EACI;EACA;;AAEA;EACI;EACA;EACA,KA90BC;EA+0BD;EACA;EACA;;AAGI;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAMA;EACI;;AAMA;EACI;;AAQxB;EACI;;AAIR;EACI;;AAGJ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAIR;EACI;EACA;EACA;EACA;EACA,eA15BC;EA25BD;;AAEA;EACI;EACA;EACA;;AACA;EACI;;AAGR;EACI;EACA,SAx6BH;;AA06BD;EACI;EACA;EACA;;AAGJ;EACI;;;AAMhB;EACI;IACI;;EAEA;IACI;;EAGJ;IACI;;EAGJ;AAAA;IAEI;IACA;IACA;IACA;IACA;IACA;IACA;;EAQR;IACI;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAGI;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAIR;IACI;;EAGJ;IACI;IACA;;EAGJ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA;;EAEA;IACI;;EAKZ;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAIR;IACI;IACA;IACA;;EAIR;IACI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAMZ;IACI;IACA;IACA;IACA;IACA;;EAEA;IACI;;EAGJ;IACI;;EAIA;IACI;;EAEJ;IACI;;EAGJ;IACI;IACA;IACA;IACA;;EAGJ;IACI;IACA;IACA;IACA;;EAKZ;IACI;IACA;IACA;;EAEA;IACI;IACA;;EAIR;IACI;;EAEA;IACI;;;AAMhB;EAIgB;IACI;;EAEJ;IACI;;EAMI;IACI;IACA;;;AAS5B;EAEQ;IACI;IACA;;EAEJ;IAII;;EAHA;IACI;;;AAOhB;EAEQ;IACI;IACA;IACA;IACA;;EAEA;IACI;IACA;IACA","file":"vs.css"} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss index 9d5de59393..1561fd57ac 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss @@ -70,18 +70,167 @@ body { -ms-filter: 'alpha(opacity=1)'; } + // the options-header only exists to host the floating button; hide it while the bar is inline + // (non-scrolled), so the inline bar keeps its original look. It reappears once .scrolledMore collapses. + .alert-criteria .options-header { + display: none; + } + &.scrolledMore { padding-top: 107px; + // Collapse the floating document-options bar into a round button (like the chat widget). + // The bar slides out of the way; clicking/tapping the button (.toggle-btn) reveals the options + // by toggling .is-open (see vs.js). Click-based so it works on touch, unlike a hover-only reveal. + // The stylesheet is bundled into abp.io too. .alert-criteria { - position: fixed; - bottom: 0; - z-index: 10; - border: 0; - margin-left: -0; + position: fixed !important; + left: auto !important; + right: 20px !important; + bottom: 90px !important; + width: 360px !important; + max-width: calc(100vw - 40px) !important; + margin: 0 !important; + padding: 16px 18px 18px !important; + border-radius: 16px !important; + color: #2d2d2d !important; + z-index: 9999 !important; + visibility: hidden !important; + // hide instantly: the base 0.3s transition otherwise fades the bar out on collapse, + // flashing a translucent half-open panel while scrolling on narrow viewports + transition: none !important; + pointer-events: none; + background: transparent !important; + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; + border: 0 !important; + box-shadow: none !important; + + // collapsed: only the round FAB (a real - - {{ 'AbpAccount::Submit' | abpLocalization }} - - +

{{ 'AbpAccount::ResetPassword_Information' | abpLocalization }}

+
+ + * + +
+
+ + * + +
+ + + {{ 'AbpAccount::Submit' | abpLocalization }} + + + } } @else {

{{ 'AbpAccount::YourPasswordIsSuccessfullyReset' | abpLocalization }} diff --git a/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts b/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts index f4567c48bb..25879ac653 100644 --- a/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts @@ -1,6 +1,14 @@ import { AccountService } from '@abp/ng.account.core/proxy'; import { ButtonComponent, getPasswordValidators } from '@abp/ng.theme.shared'; -import { Component, Injector, OnInit, inject } from '@angular/core'; +import { + Component, + effect, + Injector, + inject, + signal, + ChangeDetectionStrategy, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { ReactiveFormsModule, UntypedFormBuilder, @@ -15,6 +23,7 @@ import { LocalizationPipe } from '@abp/ng.core'; const PASSWORD_FIELDS = ['password', 'confirmPassword']; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-reset-password', templateUrl: './reset-password.component.html', imports: [ @@ -25,18 +34,19 @@ const PASSWORD_FIELDS = ['password', 'confirmPassword']; ButtonComponent, ], }) -export class ResetPasswordComponent implements OnInit { +export class ResetPasswordComponent { private fb = inject(UntypedFormBuilder); private accountService = inject(AccountService); private route = inject(ActivatedRoute); private router = inject(Router); private injector = inject(Injector); - form!: UntypedFormGroup; + private readonly queryParams = toSignal(this.route.queryParams); - inProgress = false; + form!: UntypedFormGroup; - isPasswordReset = false; + readonly inProgress = signal(false); + readonly isPasswordReset = signal(false); mapErrorsFn: Validation.MapErrorsFn = (errors, groupErrors, control) => { if (PASSWORD_FIELDS.indexOf(String(control?.name)) < 0) return errors; @@ -44,9 +54,16 @@ export class ResetPasswordComponent implements OnInit { return errors.concat(groupErrors.filter(({ key }) => key === 'passwordMismatch')); }; - ngOnInit(): void { - this.route.queryParams.subscribe(({ userId, resetToken }) => { - if (!userId || !resetToken) this.router.navigateByUrl('/account/login'); + constructor() { + effect(() => { + const params = this.queryParams(); + if (!params) return; + + const { userId, resetToken } = params; + if (!userId || !resetToken) { + void this.router.navigateByUrl('/account/login'); + return; + } this.form = this.fb.group( { @@ -63,9 +80,9 @@ export class ResetPasswordComponent implements OnInit { } onSubmit() { - if (this.form.invalid || this.inProgress) return; + if (this.form.invalid || this.inProgress()) return; - this.inProgress = true; + this.inProgress.set(true); this.accountService .resetPassword({ @@ -73,9 +90,9 @@ export class ResetPasswordComponent implements OnInit { resetToken: this.form.get('resetToken')?.value, password: this.form.get('password')?.value, }) - .pipe(finalize(() => (this.inProgress = false))) + .pipe(finalize(() => this.inProgress.set(false))) .subscribe(() => { - this.isPasswordReset = true; + this.isPasswordReset.set(true); }); } } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html index 2f8a80d721..fc8048fd41 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.html @@ -1,13 +1,13 @@

- @if (form && (!isEditMode || blogPost)) { -
+ @if (form && (!isEditMode() || blogPost())) { +
- @if (coverImagePreview) { + @if (coverImagePreview()) {
- Cover Image + Cover Image
- +
@@ -38,14 +38,15 @@ - @if (isTagsEnabled) { + @if (isTagsEnabled()) {
@@ -63,7 +64,7 @@ - @if (isEditMode) { + @if (isEditMode()) { {{ 'CmsKit::Publish' | abpLocalization }} diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts index 36a7cc2629..14b996a507 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-form/blog-post-form.component.ts @@ -1,6 +1,14 @@ import { CommonModule } from '@angular/common'; import { ActivatedRoute } from '@angular/router'; -import { Component, OnInit, inject, Injector, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + inject, + Injector, + OnInit, + signal, +} from '@angular/core'; import { ReactiveFormsModule, FormsModule, FormGroup, FormControl } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @@ -29,6 +37,7 @@ import { eCmsKitAdminComponents } from '../../../enums'; import { BlogPostFormService } from '../../../services'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-blog-post-form', templateUrl: './blog-post-form.component.html', providers: [ @@ -60,33 +69,33 @@ export class BlogPostFormComponent implements OnInit { private destroyRef = inject(DestroyRef); form: FormGroup; - blogPost: BlogPostDto | null = null; + readonly blogPost = signal(null); blogPostId: string | null = null; - isEditMode = false; + readonly isEditMode = signal(false); coverImageFile: File | null = null; - coverImagePreview: string | null = null; - tags: string = ''; - isTagsEnabled = true; + readonly coverImagePreview = signal(null); + readonly tags = signal(''); + readonly isTagsEnabled = signal(true); readonly BLOG_POST_ENTITY_TYPE = 'BlogPost'; ngOnInit() { const id = this.route.snapshot.params['id']; if (id) { - this.isEditMode = true; + this.isEditMode.set(true); this.blogPostId = id; this.loadBlogPost(id); } else { - this.isEditMode = false; + this.isEditMode.set(false); this.buildForm(); } } private loadBlogPost(id: string) { this.blogPostService.get(id).subscribe(blogPost => { - this.blogPost = blogPost; + this.blogPost.set(blogPost); if (blogPost.coverImageMediaId) { - this.coverImagePreview = `/api/cms-kit/media/${blogPost.coverImageMediaId}`; + this.coverImagePreview.set(`/api/cms-kit/media/${blogPost.coverImageMediaId}`); } this.buildForm(); this.loadTags(id); @@ -94,7 +103,6 @@ export class BlogPostFormComponent implements OnInit { } private loadTags(blogPostId: string) { - // TODO: use the public service to load the tags this.restService .request({ method: 'GET', @@ -102,28 +110,27 @@ export class BlogPostFormComponent implements OnInit { }) .subscribe(tags => { if (tags && tags.length > 0) { - this.tags = tags.map(t => t.name || '').join(', '); + this.tags.set(tags.map(t => t.name || '').join(', ')); } }); } private buildForm() { - const data = new FormPropData(this.injector, this.blogPost || {}); + const currentBlogPost = this.blogPost(); + const data = new FormPropData(this.injector, currentBlogPost || {}); const baseForm = generateFormFromProps(data); this.form = new FormGroup({ ...baseForm.controls, - content: new FormControl(this.blogPost?.content || ''), - coverImageMediaId: new FormControl(this.blogPost?.coverImageMediaId || null), + content: new FormControl(currentBlogPost?.content || ''), + coverImageMediaId: new FormControl(currentBlogPost?.coverImageMediaId || null), }); prepareSlugFromControl(this.form, 'title', 'slug', this.destroyRef); - // Check if tags feature is enabled for the blog - const blogId = this.form.get('blogId')?.value || this.blogPost?.blogId; + const blogId = this.form.get('blogId')?.value || currentBlogPost?.blogId; if (blogId) { this.checkTagsFeature(blogId); } - // Listen for blog selection changes this.form .get('blogId') ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) @@ -142,7 +149,7 @@ export class BlogPostFormComponent implements OnInit { }) .subscribe(feature => { const { isEnabled } = feature || {}; - this.isTagsEnabled = isEnabled; + this.isTagsEnabled.set(isEnabled); }); } @@ -152,7 +159,7 @@ export class BlogPostFormComponent implements OnInit { this.coverImageFile = input.files[0]; const reader = new FileReader(); reader.onload = (e: any) => { - this.coverImagePreview = e.target.result; + this.coverImagePreview.set(e.target.result); }; reader.readAsDataURL(this.coverImageFile); } @@ -160,7 +167,7 @@ export class BlogPostFormComponent implements OnInit { removeCoverImage() { this.coverImageFile = null; - this.coverImagePreview = null; + this.coverImagePreview.set(null); this.form.patchValue({ coverImageMediaId: null }); } @@ -183,11 +190,12 @@ export class BlogPostFormComponent implements OnInit { } private setTags(blogPostId: string) { - if (!this.tags || !this.tags.trim()) { + const tagsValue = this.tags(); + if (!tagsValue || !tagsValue.trim()) { return of(null); } - const tagArray = this.tags + const tagArray = tagsValue .split(',') .map(t => t.trim()) .filter(t => t.length > 0); @@ -204,7 +212,6 @@ export class BlogPostFormComponent implements OnInit { } private executeSaveOperation(operation: 'save' | 'draft' | 'publish' | 'sendToReview') { - // First upload cover image if selected this.uploadCoverImage() .pipe( tap(coverImageMediaId => { @@ -213,11 +220,12 @@ export class BlogPostFormComponent implements OnInit { } }), switchMap(() => { - if (this.isEditMode) { - if (!this.blogPost || !this.blogPostId) { + if (this.isEditMode()) { + const currentBlogPost = this.blogPost(); + if (!currentBlogPost || !this.blogPostId) { return of(null); } - return this.blogPostFormService.update(this.blogPostId, this.form, this.blogPost); + return this.blogPostFormService.update(this.blogPostId, this.form, currentBlogPost); } switch (operation) { @@ -236,7 +244,6 @@ export class BlogPostFormComponent implements OnInit { if (!result || !result.id) { return of(null); } - // Set tags after blog post is created/updated return forkJoin([of(result), this.setTags(result.id)]); }), ) diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.html index 9c8b768945..47f2632cf8 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.html @@ -1,4 +1,4 @@ - +
@@ -35,6 +35,10 @@
- +
diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.ts index 67f90689f4..f14960df35 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blog-posts/blog-post-list/blog-post-list.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; @@ -14,6 +15,7 @@ import { import { eCmsKitAdminComponents } from '../../../enums'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-blog-post-list', templateUrl: './blog-post-list.component.html', providers: [ @@ -25,21 +27,35 @@ import { eCmsKitAdminComponents } from '../../../enums'; ], imports: [ExtensibleTableComponent, PageComponent, LocalizationPipe, FormsModule, CommonModule], }) -export class BlogPostListComponent implements OnInit { - data: PagedResultDto = { items: [], totalCount: 0 }; - +export class BlogPostListComponent { public readonly list = inject(ListService); private blogPostService = inject(BlogPostAdminService); private confirmationService = inject(ConfirmationService); + readonly data = toSignal( + this.list.hookToQuery(query => { + let filters: Partial = {}; + if (this.list.filter) { + filters.filter = this.list.filter; + } + if (this.statusFilter !== null) { + filters.status = this.statusFilter; + } + const input: BlogPostGetListInput = { + ...query, + ...filters, + }; + return this.blogPostService.getList(input); + }), + { + initialValue: { items: [], totalCount: 0 } as PagedResultDto, + }, + ); + filter = ''; statusFilter: BlogPostStatus | null = null; BlogPostStatus = BlogPostStatus; - ngOnInit() { - this.hookToQuery(); - } - onSearch() { this.list.filter = this.filter; this.list.get(); @@ -49,25 +65,6 @@ export class BlogPostListComponent implements OnInit { this.list.get(); } - private hookToQuery() { - this.list - .hookToQuery(query => { - let filters: Partial = {}; - if (this.list.filter) { - filters.filter = this.list.filter; - } - if (this.statusFilter !== null) { - filters.status = this.statusFilter; - } - const input: BlogPostGetListInput = { - ...query, - ...filters, - }; - return this.blogPostService.getList(input); - }) - .subscribe(res => (this.data = res)); - } - delete(id: string, title: string) { this.confirmationService .warn('CmsKit::BlogPostDeletionConfirmationMessage', 'AbpUi::AreYouSure', { @@ -77,9 +74,7 @@ export class BlogPostListComponent implements OnInit { }) .subscribe((status: Confirmation.Status) => { if (status === Confirmation.Status.confirm) { - this.blogPostService.delete(id).subscribe(() => { - this.list.get(); - }); + this.blogPostService.delete(id).subscribe(() => this.list.get()); } }); } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.html index da9240ac8c..1f04f5073e 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.html @@ -4,8 +4,8 @@ - @if (form) { -
+ @if (form(); as currentForm) { +
@for (featureControl of featuresFormArray.controls; track $index; let i = $index) {
@@ -19,7 +19,7 @@ formControlName="isEnabled" />
} @else { @@ -33,10 +33,10 @@ class="form-check-input" id="feature-{{ i }}" formControlName="isEnabled" - disabled + [disabled]="true" />
@@ -58,7 +58,7 @@ - + {{ 'AbpUi::Save' | abpLocalization }} diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.ts index 4b4bd4f420..30785fbeb5 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-features-modal/blog-features-modal.component.ts @@ -1,4 +1,12 @@ -import { Component, OnInit, inject, input, output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + input, + OnInit, + output, + signal, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @@ -22,6 +30,7 @@ export interface BlogFeaturesModalVisibleChange { } @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-blog-features-modal', templateUrl: './blog-features-modal.component.html', imports: [ @@ -42,8 +51,8 @@ export class BlogFeaturesModalComponent implements OnInit { blogId = input(); visibleChange = output(); - form: FormGroup; - features: BlogFeatureDto[] = []; + readonly form = signal(undefined); + readonly features = signal([]); private initialFeatureStates: Map = new Map(); ngOnInit() { @@ -54,19 +63,19 @@ export class BlogFeaturesModalComponent implements OnInit { private loadFeatures() { this.blogFeatureService.getList(this.blogId()!).subscribe(features => { - this.features = features.sort((a, b) => + const sorted = features.sort((a, b) => (a.featureName || '').localeCompare(b.featureName || ''), ); - // Store initial states + this.features.set(sorted); this.initialFeatureStates = new Map( - this.features.map(f => [f.featureName || '', f.isEnabled || false]), + sorted.map(f => [f.featureName || '', f.isEnabled || false]), ); - this.buildForm(); + this.buildForm(sorted); }); } - private buildForm() { - const featureControls = this.features.map(feature => + private buildForm(features: BlogFeatureDto[]) { + const featureControls = features.map(feature => this.fb.group({ featureName: [feature.featureName], isEnabled: [feature.isEnabled], @@ -74,13 +83,15 @@ export class BlogFeaturesModalComponent implements OnInit { }), ); - this.form = this.fb.group({ - features: this.fb.array(featureControls), - }); + this.form.set( + this.fb.group({ + features: this.fb.array(featureControls), + }), + ); } get featuresFormArray(): FormArray { - return this.form.get('features') as FormArray; + return this.form()!.get('features') as FormArray; } onVisibleChange(visible: boolean, refresh = false) { @@ -88,20 +99,19 @@ export class BlogFeaturesModalComponent implements OnInit { } save() { - if (!this.form.valid || !this.blogId()) { + const currentForm = this.form(); + if (!currentForm?.valid || !this.blogId()) { return; } - const featuresArray = this.form.get('features') as FormArray; + const featuresArray = currentForm.get('features') as FormArray; - // Only save features that have changed const changedFeatures: BlogFeatureInputDto[] = featuresArray.controls .map(control => { const featureName = control.get('featureName')?.value; const isEnabled = control.get('isEnabled')?.value; const initialIsEnabled = this.initialFeatureStates.get(featureName); - // Only include if the value has changed if (featureName && initialIsEnabled !== isEnabled) { return { featureName, @@ -112,23 +122,18 @@ export class BlogFeaturesModalComponent implements OnInit { }) .filter((input): input is BlogFeatureInputDto => input !== null); - // If no features changed, just close the modal if (changedFeatures.length === 0) { this.onVisibleChange(false, false); return; } - // Save only changed features const saveObservables = changedFeatures.map(input => this.blogFeatureService.set(this.blogId()!, input), ); - // Use forkJoin to save all changed features at once - forkJoin(saveObservables).subscribe({ - next: () => { - this.onVisibleChange(false, true); - this.toasterService.success('AbpUi::SavedSuccessfully'); - }, + forkJoin(saveObservables).subscribe(() => { + this.onVisibleChange(false, true); + this.toasterService.success('AbpUi::SavedSuccessfully'); }); } } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.html index e9f224383f..93c0819475 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.html @@ -1,15 +1,15 @@ - +
- +
- @if (isModalVisible) { - + @if (isModalVisible()) { + } - @if (isFeaturesModalVisible) { + @if (isFeaturesModalVisible()) { } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.ts index 4b5c84495a..0fc9fb129e 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-list/blog-list.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ListService, PagedResultDto, LocalizationPipe } from '@abp/ng.core'; @@ -14,6 +15,7 @@ import { } from '../blog-features-modal/blog-features-modal.component'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-blog-list', templateUrl: './blog-list.component.html', providers: [ @@ -33,22 +35,33 @@ import { BlogFeaturesModalComponent, ], }) -export class BlogListComponent implements OnInit { - data: PagedResultDto = { items: [], totalCount: 0 }; - +export class BlogListComponent { public readonly list = inject(ListService); private blogService = inject(BlogAdminService); private confirmationService = inject(ConfirmationService); - filter = ''; - isModalVisible = false; - selected?: BlogDto; - isFeaturesModalVisible = false; - selectedBlogId?: string; + readonly data = toSignal( + this.list.hookToQuery(query => { + let filters: Partial = {}; + if (this.list.filter) { + filters.filter = this.list.filter; + } + const input: BlogGetListInput = { + ...query, + ...filters, + }; + return this.blogService.getList(input); + }), + { + initialValue: { items: [], totalCount: 0 } as PagedResultDto, + }, + ); - ngOnInit() { - this.hookToQuery(); - } + filter = ''; + readonly isModalVisible = signal(false); + readonly selected = signal(undefined); + readonly isFeaturesModalVisible = signal(false); + readonly selectedBlogId = signal(undefined); onSearch() { this.list.filter = this.filter; @@ -56,14 +69,14 @@ export class BlogListComponent implements OnInit { } add() { - this.selected = {} as BlogDto; - this.isModalVisible = true; + this.selected.set({} as BlogDto); + this.isModalVisible.set(true); } edit(id: string) { this.blogService.get(id).subscribe(blog => { - this.selected = blog; - this.isModalVisible = true; + this.selected.set(blog); + this.isModalVisible.set(true); }); } @@ -74,34 +87,14 @@ export class BlogListComponent implements OnInit { }) .subscribe((status: Confirmation.Status) => { if (status === Confirmation.Status.confirm) { - this.blogService.delete(id).subscribe(() => { - this.list.get(); - }); + this.blogService.delete(id).subscribe(() => this.list.get()); } }); } openFeatures(id: string) { - this.selectedBlogId = id; - this.isFeaturesModalVisible = true; - } - - private hookToQuery() { - this.list - .hookToQuery(query => { - let filters: Partial = {}; - if (this.list.filter) { - filters.filter = this.list.filter; - } - const input: BlogGetListInput = { - ...query, - ...filters, - }; - return this.blogService.getList(input); - }) - .subscribe(res => { - this.data = res; - }); + this.selectedBlogId.set(id); + this.isFeaturesModalVisible.set(true); } onVisibleModalChange(visibilityChange: BlogModalVisibleChange) { @@ -111,8 +104,8 @@ export class BlogListComponent implements OnInit { if (visibilityChange.refresh) { this.list.get(); } - this.selected = null; - this.isModalVisible = false; + this.selected.set(undefined); + this.isModalVisible.set(false); } onFeaturesModalChange(visibilityChange: BlogFeaturesModalVisibleChange) { @@ -122,7 +115,7 @@ export class BlogListComponent implements OnInit { if (visibilityChange.refresh) { this.list.get(); } - this.selectedBlogId = null; - this.isFeaturesModalVisible = false; + this.selectedBlogId.set(undefined); + this.isFeaturesModalVisible.set(false); } } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.html index 83d235bcd3..f5d5ff7523 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.html @@ -1,4 +1,4 @@ - +

{{ (selected()?.id ? 'AbpUi::Edit' : 'AbpUi::New') | abpLocalization }} @@ -7,7 +7,7 @@ @if (form) { - + } @else { diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.ts index d9a4d07441..c87109f6ce 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/blogs/blog-modal/blog-modal.component.ts @@ -1,4 +1,14 @@ -import { Component, OnInit, inject, Injector, input, output, DestroyRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + inject, + Injector, + input, + OnInit, + output, + signal, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule, FormGroup } from '@angular/forms'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @@ -24,6 +34,7 @@ export interface BlogModalVisibleChange { } @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-blog-modal', templateUrl: './blog-modal.component.html', imports: [ @@ -46,7 +57,7 @@ export class BlogModalComponent implements OnInit { selected = input(); visibleChange = output(); - modalBusy = false; + readonly modalBusy = signal(false); form: FormGroup; @@ -65,12 +76,13 @@ export class BlogModalComponent implements OnInit { } save() { - if (this.modalBusy) { + if (this.modalBusy()) { return; } - this.modalBusy = true; + this.modalBusy.set(true); if (!this.form.valid) { + this.modalBusy.set(false); return; } @@ -86,7 +98,7 @@ export class BlogModalComponent implements OnInit { } as UpdateBlogDto); } - observable$.pipe(finalize(() => (this.modalBusy = false))).subscribe(() => { + observable$.pipe(finalize(() => this.modalBusy.set(false))).subscribe(() => { this.onVisibleChange(false, true); this.toasterService.success('AbpUi::SavedSuccessfully'); }); diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.html index 0f4cf1fd1d..6f54edd503 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.html @@ -28,7 +28,7 @@


- + {{ 'AbpAccount::Save' | abpLocalization }}
diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.ts index 34bf59385e..38ccdd5ff5 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/cms-settings/cms-settings.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { NgbNav, @@ -33,11 +33,13 @@ import { CMS_KIT_COMMENTS_REQUIRE_APPROVEMENT } from '../comments'; ReactiveFormsModule, ], }) -export class CmsSettingsComponent { +export class CmsSettingsComponent implements OnInit { readonly commentAdminService = inject(CommentAdminService); readonly configState = inject(ConfigStateService); readonly toaster = inject(ToasterService); + readonly loading = signal(false); + commentApprovalControl = new FormControl(false); ngOnInit() { @@ -47,10 +49,12 @@ export class CmsSettingsComponent { } submit() { + this.loading.set(true); this.commentAdminService .updateSettings({ commentRequireApprovement: this.commentApprovalControl.value }) .pipe( finalize(() => { + this.loading.set(false); this.configState.refreshAppState().subscribe(); }), ) diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.html index 7b4c18c4a3..708859176b 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.html @@ -1,5 +1,5 @@ - @if (comment) { + @if (comment(); as comment) {
@@ -59,7 +59,7 @@
- +
@@ -142,6 +142,6 @@

{{ 'CmsKit::RepliesToThisComment' | abpLocalization }}

- +
diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.ts index ffebe3ca2c..b9ff177e75 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-details/comment-details.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { CommonModule, DatePipe } from '@angular/common'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -18,6 +19,7 @@ import { eCmsKitAdminComponents } from '../../../enums'; import { CommentEntityService } from '../../../services'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-comment-details', templateUrl: './comment-details.component.html', providers: [ @@ -46,8 +48,8 @@ import { CommentEntityService } from '../../../services'; ], }) export class CommentDetailsComponent implements OnInit { - comment: CommentWithAuthorDto | null = null; - data: PagedResultDto = { items: [], totalCount: 0 }; + readonly comment = signal(null); + readonly commentId = signal(null); readonly list = inject(ListService); readonly commentEntityService = inject(CommentEntityService); @@ -57,19 +59,33 @@ export class CommentDetailsComponent implements OnInit { private router = inject(Router); private fb = inject(FormBuilder); + readonly data = toSignal( + this.list.hookToQuery(query => { + const filters = (this.list.filter as Partial) || {}; + const input: CommentGetListInput = { + repliedCommentId: this.commentId() ?? undefined, + ...query, + ...filters, + }; + return this.commentService.getList(input); + }), + { + initialValue: { items: [], totalCount: 0 } as PagedResultDto, + }, + ); + filterForm!: FormGroup; commentApproveStateOptions = commentApproveStateOptions; - commentId!: string; requireApprovement: boolean; ngOnInit() { this.route.params.subscribe(params => { const id = params['id']; if (id) { - this.commentId = id; + this.commentId.set(id); this.loadComment(id); this.createFilterForm(); - this.hookToQuery(); + this.list.get(); } }); this.requireApprovement = this.commentEntityService.requireApprovement; @@ -86,7 +102,7 @@ export class CommentDetailsComponent implements OnInit { private loadComment(id: string) { this.commentService.get(id).subscribe(comment => { - this.comment = comment; + this.comment.set(comment); }); } @@ -95,7 +111,7 @@ export class CommentDetailsComponent implements OnInit { const filters: Partial = { author: formValue.author || undefined, commentApproveState: formValue.commentApproveState, - repliedCommentId: this.commentId, + repliedCommentId: this.commentId() ?? undefined, creationStartDate: formValue.creationStartDate || undefined, creationEndDate: formValue.creationEndDate || undefined, }; @@ -104,20 +120,6 @@ export class CommentDetailsComponent implements OnInit { this.list.get(); } - private hookToQuery() { - this.list - .hookToQuery(query => { - const filters = (this.list.filter as Partial) || {}; - const input: CommentGetListInput = { - repliedCommentId: this.commentId, - ...query, - ...filters, - }; - return this.commentService.getList(input); - }) - .subscribe(res => (this.data = res)); - } - navigateToReply(id: string) { this.router.navigate(['/cms/comments', id]); } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.html index 5137014959..62d2eb5466 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.html @@ -87,6 +87,6 @@
- +
diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.ts b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.ts index dc7da58d78..5ab7e299d5 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/comments/comment-list/comment-list.component.ts @@ -1,4 +1,5 @@ -import { Component, OnInit, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { CommonModule } from '@angular/common'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { NgbDateAdapter, NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; @@ -17,6 +18,7 @@ import { eCmsKitAdminComponents } from '../../../enums'; import { CommentEntityService } from '../../../services'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'abp-comment-list', templateUrl: './comment-list.component.html', providers: [ @@ -44,21 +46,32 @@ import { CommentEntityService } from '../../../services'; ], }) export class CommentListComponent implements OnInit { - data: PagedResultDto = { items: [], totalCount: 0 }; - readonly list = inject(ListService); readonly commentEntityService = inject(CommentEntityService); private commentService = inject(CommentAdminService); private fb = inject(FormBuilder); + readonly data = toSignal( + this.list.hookToQuery(query => { + const filters = (this.list.filter as Partial) || {}; + const input: CommentGetListInput = { + ...query, + ...filters, + }; + return this.commentService.getList(input); + }), + { + initialValue: { items: [], totalCount: 0 } as PagedResultDto, + }, + ); + filterForm!: FormGroup; commentApproveStateOptions = commentApproveStateOptions; requireApprovement: boolean; ngOnInit() { this.createFilterForm(); - this.hookToQuery(); this.requireApprovement = this.commentEntityService.requireApprovement; } @@ -85,17 +98,4 @@ export class CommentListComponent implements OnInit { this.list.filter = filters as any; this.list.get(); } - - private hookToQuery() { - this.list - .hookToQuery(query => { - const filters = (this.list.filter as Partial) || {}; - const input: CommentGetListInput = { - ...query, - ...filters, - }; - return this.commentService.getList(input); - }) - .subscribe(res => (this.data = res)); - } } diff --git a/npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.html b/npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.html index 407c83dc26..5a132fea79 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.html +++ b/npm/ng-packs/packages/cms-kit/admin/src/components/global-resources/global-resources.component.html @@ -1,12 +1,12 @@ - +
@@ -29,9 +36,10 @@ id="select-all-in-all-tabs" name="select-all-in-all-tabs" class="form-check-input" - [(ngModel)]="selectAllTab" - (click)="onClickSelectAll()" - [disabled]="disabledSelectAllInAllTabs" + [checked]="selectAllTabState().checked" + [indeterminate]="selectAllTabState().indeterminate" + (change)="onSelectAllChange($event)" + [disabled]="disabledSelectAllInAllTabs()" />