@ -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: |
|||
|
|||
 |
|||
|
|||
### 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. |
|||
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 16 KiB |
@ -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) |
|||
@ -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: |
|||
|
|||
 |
|||
|
|||
## 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<AbpBackgroundJobWorkerOptions>(options => |
|||
{ |
|||
options.StoreSuccessfulJobs = true; |
|||
options.SuccessfulJobRetentionTime = TimeSpan.FromDays(30); |
|||
|
|||
options.AddDedicatedWorker<EmailJobArgs, SmsJobArgs>("NotificationWorkerLock"); |
|||
options.AddDedicatedWorker<ReportJobArgs>("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<AbpAspNetCoreMvcOptions>(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 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! |
|||
|
After Width: | Height: | Size: 470 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 94 KiB |
@ -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\! |
|||
|
|||
@ -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) |
|||
@ -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` |
|||
|
Before Width: | Height: | Size: 930 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 975 KiB |
|
Before Width: | Height: | Size: 822 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 474 KiB |
|
Before Width: | Height: | Size: 906 KiB |
|
Before Width: | Height: | Size: 823 KiB |
|
Before Width: | Height: | Size: 838 KiB |
|
Before Width: | Height: | Size: 936 KiB |
|
Before Width: | Height: | Size: 922 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 679 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 962 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
@ -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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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 |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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. |
|||
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1005 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 897 KiB |
@ -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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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<TCacheItem>` |
|||
- `IDistributedCache<TCacheItem, TCacheKey>` |
|||
|
|||
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<ProductSummaryCacheItem, Guid> _cache; |
|||
private readonly IRepository<Product, Guid> _productRepository; |
|||
|
|||
public ProductAppService( |
|||
IDistributedCache<ProductSummaryCacheItem, Guid> cache, |
|||
IRepository<Product, Guid> productRepository) |
|||
{ |
|||
_cache = cache; |
|||
_productRepository = productRepository; |
|||
} |
|||
|
|||
public async Task<ProductSummaryCacheItem> 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 |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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<AbpDistributedCacheOptions>(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<IReadOnlyList<ProductSummaryCacheItem>> 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 |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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<AbpDistributedCacheOptions>(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. |
|||
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 996 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@ -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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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<Guid> |
|||
{ |
|||
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<OrderPlacedDomainEvent>, |
|||
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<OrderPlacedEto>, |
|||
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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## Using AddDistributedEvent directly on aggregates |
|||
|
|||
ABP also allows aggregates and domain services to add distributed events directly. |
|||
|
|||
```csharp |
|||
public class Product : AggregateRoot<Guid> |
|||
{ |
|||
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. |
|||
|
|||
|
|||
|
|||
 |
|||
|
|||
## 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<OrderingDbContext>, IHasEventOutbox |
|||
{ |
|||
public DbSet<OutgoingEventRecord> OutgoingEvents { get; set; } |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
builder.ConfigureEventOutbox(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Then configure the outbox: |
|||
|
|||
```csharp |
|||
Configure<AbpDistributedEventBusOptions>(options => |
|||
{ |
|||
options.Outboxes.Configure(config => |
|||
{ |
|||
config.UseDbContext<OrderingDbContext>(); |
|||
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<InventoryDbContext>, IHasEventInbox |
|||
{ |
|||
public DbSet<IncomingEventRecord> IncomingEvents { get; set; } |
|||
|
|||
protected override void OnModelCreating(ModelBuilder builder) |
|||
{ |
|||
base.OnModelCreating(builder); |
|||
|
|||
builder.ConfigureEventInbox(); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
Then wire it up: |
|||
|
|||
```csharp |
|||
Configure<AbpDistributedEventBusOptions>(options => |
|||
{ |
|||
options.Inboxes.Configure(config => |
|||
{ |
|||
config.UseDbContext<InventoryDbContext>(); |
|||
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<T>` |
|||
- `EntityUpdatedEto<T>` |
|||
- `EntityDeletedEto<T>` |
|||
|
|||
These are useful when another service needs basic CRUD-oriented synchronization rather than a rich business workflow event. |
|||
|
|||
### Enabling auto entity events |
|||
|
|||
```csharp |
|||
Configure<AbpDistributedEntityEventOptions>(options => |
|||
{ |
|||
options.AutoEventSelectors.Add<Product>(); |
|||
options.EtoMappings.Add<Product, ProductEto>(); |
|||
}); |
|||
``` |
|||
|
|||
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<TDomainEvent>` |
|||
- 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<TEto>` |
|||
- 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. |
|||
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 955 KiB |
|
After Width: | Height: | Size: 950 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
@ -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. |
|||
|
|||
 |
|||
|
|||
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<T>()` 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. |
|||
|
|||
 |
|||
|
|||
> **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<TInput, TOutput>` and `WorkflowActivity<TInput, TOutput>` 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<TInput, TOutput>` 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<OrderPayload, OrderResult> |
|||
{ |
|||
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order) |
|||
{ |
|||
var logger = context.CreateReplaySafeLogger<OrderProcessingWorkflow>(); |
|||
logger.LogInformation("Starting order {OrderId}: {Quantity} x {ProductName}", |
|||
order.OrderId, order.Quantity, order.ProductName); |
|||
|
|||
// 1. Check inventory |
|||
var inventory = await context.CallActivityAsync<InventoryResult>( |
|||
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<T>()` 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<TInput, TOutput>` 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<OrderPayload, InventoryResult> |
|||
{ |
|||
private readonly ILogger<CheckInventoryActivity> _logger; |
|||
|
|||
public CheckInventoryActivity(ILogger<CheckInventoryActivity> logger) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
public override Task<InventoryResult> 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<OrderPayload, object?> |
|||
{ |
|||
private readonly ILogger<ProcessPaymentActivity> _logger; |
|||
|
|||
public ProcessPaymentActivity(ILogger<ProcessPaymentActivity> logger) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
public override Task<object?> 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<object?>(null); |
|||
} |
|||
} |
|||
|
|||
public class NotifyCustomerActivity : WorkflowActivity<OrderPayload, object?> |
|||
{ |
|||
private readonly ILogger<NotifyCustomerActivity> _logger; |
|||
|
|||
public NotifyCustomerActivity(ILogger<NotifyCustomerActivity> logger) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
public override Task<object?> 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<object?>(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: |
|||
|
|||
 |
|||
|
|||
## 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<OrderProcessingWorkflow>(); |
|||
|
|||
options.RegisterActivity<CheckInventoryActivity>(); |
|||
options.RegisterActivity<ProcessPaymentActivity>(); |
|||
options.RegisterActivity<NotifyCustomerActivity>(); |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
> `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: |
|||
|
|||
 |
|||
|
|||
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<IActionResult> 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<IActionResult> 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<OrderResult>() |
|||
}); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
`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! |
|||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@ -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 `<ng-component>` tag instead of a structurally neutral `<ng-container>` 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 `<ng-container>` instead of `<ng-component>`. |
|||
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: '<abp-dynamic-layout />' |
|||
}) |
|||
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<br><small>(Local Source Code)</small>"]:::react |
|||
C2["TanStack Router & Query<br><small>(Type-Safe Client Routes)</small>"]:::react |
|||
|
|||
T1["Vite Dev Server & Bundling<br><small>(Fast HMR, Vitest)</small>"]:::tool |
|||
T2["Tailwind CSS / shadcn/ui<br><small>(Accessible UI Components)</small>"]:::tool |
|||
|
|||
C1 --> C2 |
|||
T1 --> T2 |
|||
end |
|||
|
|||
%% Backend Box |
|||
subgraph NetCore ["ASP.NET Core Web API Host"] |
|||
P1["Dynamic API Client Proxies<br><small>(Auto-Generated Endpoints)</small>"]:::proxy |
|||
A1["ABP Admin Console<br><small>(Delivered via NuGet)</small>"]:::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. |
|||
@ -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()) { |
|||
<span>Please enter a valid email.</span> |
|||
} |
|||
``` |
|||
|
|||
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<User>(() => { |
|||
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. |
|||
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 910 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1001 KiB |
@ -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<AbpBackgroundJobWorkerOptions>(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<AbpAntiForgeryOptions>(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. |
|||
@ -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) |
|||
@ -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; |
|||
|
|||
/// <summary>
|
|||
/// Periodically deletes retained successfully completed jobs older than
|
|||
/// <see cref="AbpBackgroundJobWorkerOptions.SuccessfulJobRetentionTime"/>.
|
|||
/// Only relevant when <see cref="AbpBackgroundJobWorkerOptions.StoreSuccessfulJobs"/> is enabled.
|
|||
/// </summary>
|
|||
public class BackgroundJobCleanupWorker : AsyncPeriodicBackgroundWorkerBase |
|||
{ |
|||
protected AbpBackgroundJobOptions JobOptions { get; } |
|||
|
|||
protected AbpBackgroundJobWorkerOptions WorkerOptions { get; } |
|||
|
|||
protected IAbpDistributedLock DistributedLock { get; } |
|||
|
|||
public BackgroundJobCleanupWorker( |
|||
AbpAsyncTimer timer, |
|||
IServiceScopeFactory serviceScopeFactory, |
|||
IOptions<AbpBackgroundJobOptions> jobOptions, |
|||
IOptions<AbpBackgroundJobWorkerOptions> 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<IBackgroundJobStore>(); |
|||
var clock = workerContext.ServiceProvider.GetRequiredService<IClock>(); |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Volo.Abp.BackgroundJobs; |
|||
|
|||
/// <summary>
|
|||
/// Filters the waiting jobs of a background job worker by job name.
|
|||
/// A worker is exactly one of: no filter (<see cref="None"/>), include-only (a dedicated worker) or
|
|||
/// exclude-only (the default worker in a multi-worker setup) — the two can never be combined.
|
|||
/// </summary>
|
|||
public class BackgroundJobNameFilter |
|||
{ |
|||
/// <summary>
|
|||
/// A filter that matches every job name.
|
|||
/// </summary>
|
|||
public static BackgroundJobNameFilter None { get; } = new(BackgroundJobNameFilterMode.None); |
|||
|
|||
public BackgroundJobNameFilterMode Mode { get; } |
|||
|
|||
public IReadOnlyList<string> JobNames { get; } |
|||
|
|||
public BackgroundJobNameFilter(BackgroundJobNameFilterMode mode, IReadOnlyList<string>? 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<string>(); |
|||
|
|||
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<string> jobNames) |
|||
{ |
|||
return new BackgroundJobNameFilter(BackgroundJobNameFilterMode.Include, jobNames); |
|||
} |
|||
|
|||
public static BackgroundJobNameFilter Exclude(IReadOnlyList<string> jobNames) |
|||
{ |
|||
return new BackgroundJobNameFilter(BackgroundJobNameFilterMode.Exclude, jobNames); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 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 <see cref="Mode"/> and
|
|||
/// <see cref="JobNames"/> 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).
|
|||
/// </summary>
|
|||
public virtual bool IsMatch(string jobName) |
|||
{ |
|||
return Mode switch |
|||
{ |
|||
BackgroundJobNameFilterMode.Include => JobNames.Contains(jobName, StringComparer.Ordinal), |
|||
BackgroundJobNameFilterMode.Exclude => !JobNames.Contains(jobName, StringComparer.Ordinal), |
|||
_ => true |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
namespace Volo.Abp.BackgroundJobs; |
|||
|
|||
public enum BackgroundJobNameFilterMode : byte |
|||
{ |
|||
/// <summary>
|
|||
/// No filter; all job names match.
|
|||
/// </summary>
|
|||
None = 0, |
|||
|
|||
/// <summary>
|
|||
/// Only the job names in the filter match.
|
|||
/// </summary>
|
|||
Include = 1, |
|||
|
|||
/// <summary>
|
|||
/// All job names except those in the filter match.
|
|||
/// </summary>
|
|||
Exclude = 2 |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Volo.Abp.BackgroundJobs; |
|||
|
|||
/// <summary>
|
|||
/// Configuration of a dedicated <see cref="BackgroundJobWorker"/> that processes only specific job types.
|
|||
/// </summary>
|
|||
public class BackgroundJobWorkerConfiguration |
|||
{ |
|||
/// <summary>
|
|||
/// 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
|
|||
/// <see cref="AbpBackgroundJobWorkerOptions.MaxParallelJobExecutionCount"/> is 1; in parallel mode
|
|||
/// (greater than 1) jobs are claimed with per-job locks instead and this lock is not acquired.
|
|||
/// </summary>
|
|||
public string LockName { get; } |
|||
|
|||
/// <summary>
|
|||
/// The job argument types that are processed exclusively by this worker.
|
|||
/// </summary>
|
|||
public IReadOnlyList<Type> 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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
/// <summary>
|
|||
/// Owns and controls the background job workers.
|
|||
/// When no <see cref="AbpBackgroundJobWorkerOptions.WorkerConfigurations"/> 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 <see cref="IBackgroundJobWorker"/> is respected.
|
|||
/// </summary>
|
|||
public class BackgroundJobWorkerManager : IBackgroundWorker |
|||
{ |
|||
protected AbpBackgroundJobOptions JobOptions { get; } |
|||
|
|||
protected AbpBackgroundJobWorkerOptions WorkerOptions { get; } |
|||
|
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
protected List<IBackgroundJobWorker> Workers { get; } |
|||
|
|||
public BackgroundJobWorkerManager( |
|||
IOptions<AbpBackgroundJobOptions> jobOptions, |
|||
IOptions<AbpBackgroundJobWorkerOptions> workerOptions, |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
JobOptions = jobOptions.Value; |
|||
WorkerOptions = workerOptions.Value; |
|||
ServiceProvider = serviceProvider; |
|||
Workers = new List<IBackgroundJobWorker>(); |
|||
} |
|||
|
|||
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<DedicatedWorkerDefinition>(); |
|||
var allDedicatedJobNames = new List<string>(); |
|||
|
|||
// The default worker uses WorkerOptions.DistributedLockName, so dedicated workers must not reuse it.
|
|||
var lockNames = new List<string> { 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<IBackgroundJobWorker>(); |
|||
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(); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Volo.Abp.BackgroundJobs; |
|||
|
|||
/// <summary>
|
|||
/// 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 <see cref="BackgroundJobWorkerManager"/> from a
|
|||
/// <see cref="BackgroundJobWorkerConfiguration"/> after all configurations have been validated.
|
|||
/// </summary>
|
|||
public class DedicatedWorkerDefinition |
|||
{ |
|||
/// <summary>
|
|||
/// The distributed lock name this worker runs under.
|
|||
/// </summary>
|
|||
public string LockName { get; } |
|||
|
|||
/// <summary>
|
|||
/// The resolved job names this worker is responsible for.
|
|||
/// </summary>
|
|||
public IReadOnlyList<string> JobNames { get; } |
|||
|
|||
public DedicatedWorkerDefinition(string lockName, IReadOnlyList<string> jobNames) |
|||
{ |
|||
LockName = lockName; |
|||
JobNames = jobNames; |
|||
} |
|||
} |
|||
@ -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 |
|||
/// <summary>
|
|||
/// A background job worker that polls and executes waiting jobs.
|
|||
/// Instances are created, configured and started by <see cref="BackgroundJobWorkerManager"/>.
|
|||
/// </summary>
|
|||
public interface IBackgroundJobWorker |
|||
{ |
|||
/// <summary>
|
|||
/// Starts this worker.
|
|||
/// </summary>
|
|||
/// <param name="distributedLockName">
|
|||
/// Distributed lock name for this worker. When null, <see cref="AbpBackgroundJobWorkerOptions.DistributedLockName"/> is used.
|
|||
/// </param>
|
|||
/// <param name="jobNameFilter">Filters the jobs this worker processes by name. When null, all jobs are processed.</param>
|
|||
/// <param name="cancellationToken">Cancellation token.</param>
|
|||
Task StartAsync( |
|||
string? distributedLockName = null, |
|||
BackgroundJobNameFilter? jobNameFilter = null, |
|||
CancellationToken cancellationToken = default); |
|||
|
|||
Task StopAsync(CancellationToken cancellationToken = default); |
|||
} |
|||
|
|||