Browse Source

Merge remote-tracking branch 'origin/rel-10.6' into issue/ng-v22-upgrade-docs

pull/25691/head
sumeyye 3 months ago
parent
commit
8b04a482de
  1. 20
      .github/workflows/auto-pr.yml
  2. 126
      Directory.Packages.props
  3. 4
      common.props
  4. 71
      docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/POST.md
  5. BIN
      docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/cover-image.png
  6. BIN
      docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/upgrade-abp-packages.png
  7. 55
      docs/en/Blog-Posts/2026-07-06-ABP-Summer-Campaign/post.md
  8. 180
      docs/en/Blog-Posts/2026-07-07 v10_6_Preview/POST.md
  9. BIN
      docs/en/Blog-Posts/2026-07-07 v10_6_Preview/cover-image.png
  10. BIN
      docs/en/Blog-Posts/2026-07-07 v10_6_Preview/studio-switch-to-preview.png
  11. BIN
      docs/en/Blog-Posts/2026-07-07 v10_6_Preview/summer-sale.png
  12. 22
      docs/en/Community-Articles/2026-06-24-Meet-ABP-at-WeAreDevelopers-World-Congress-2026/post.md
  13. 86
      docs/en/Community-Articles/2026-06-25-ABP-Bootcamp-AI-Assisted-Application-Development-with-ABP/post.md
  14. 207
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post-v2.md
  15. 190
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post.md
  16. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20888.jpg
  17. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20890.jpg
  18. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20891.jpg
  19. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20927.jpg
  20. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20935.jpg
  21. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20970.jpg
  22. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20974.jpg
  23. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20976.jpg
  24. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20977.jpg
  25. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20980.jpg
  26. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/convex-ambiance.jpg
  27. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/cover.jpg
  28. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/hand-made-coding.png
  29. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-1.jpg
  30. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-2.jpg
  31. BIN
      docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/volosoft-presentation.pptx
  32. 349
      docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/Post.md
  33. BIN
      docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/cover.png
  34. BIN
      docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-1.png
  35. BIN
      docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-2.png
  36. BIN
      docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-3.png
  37. BIN
      docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-4.png
  38. 631
      docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/Post.md
  39. BIN
      docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/cover.png
  40. BIN
      docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-1.png
  41. BIN
      docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-2.png
  42. BIN
      docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-3.png
  43. 693
      docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/Post.md
  44. BIN
      docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/cover.png
  45. BIN
      docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-1.png
  46. BIN
      docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-2.png
  47. BIN
      docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-3.png
  48. 419
      docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/POST.md
  49. BIN
      docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/cover-image.png
  50. BIN
      docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-init-run-result.png
  51. BIN
      docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-workflow-response.png
  52. BIN
      docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid1.png
  53. BIN
      docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid2.png
  54. 216
      docs/en/Community-Articles/2026-06-29-customizing-the-abp-framework/POST.md
  55. 344
      docs/en/Community-Articles/2026-06-30-state-management-for-angular/POST.md
  56. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/cover.png
  57. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png
  58. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-identity-users.png
  59. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-dashboard.png
  60. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png
  61. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-permission-management.png
  62. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-saas-tenants.png
  63. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png
  64. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-first-run.png
  65. 0
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png
  66. 10
      docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/post.md
  67. 1330
      docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/Post.md
  68. BIN
      docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/cover.png
  69. BIN
      docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-1.png
  70. BIN
      docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-2.png
  71. BIN
      docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-3.png
  72. BIN
      docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-4.png
  73. 4
      docs/en/docs-nav.json
  74. 7
      docs/en/framework/fundamentals/dynamic-claims.md
  75. 69
      docs/en/framework/infrastructure/background-jobs/index.md
  76. 2
      docs/en/get-started/index.md
  77. 2
      docs/en/modules/background-jobs.md
  78. 73
      docs/en/package-version-changes.md
  79. 2
      docs/en/release-info/migration-guides/abp-10-5.md
  80. 237
      docs/en/release-info/migration-guides/abp-10-6.md
  81. 1
      docs/en/release-info/migration-guides/index.md
  82. 27
      docs/en/release-info/release-notes.md
  83. 70
      docs/en/release-info/road-map.md
  84. 2
      docs/en/solution-templates/index.md
  85. 55
      docs/en/solution-templates/modern-vs-classic.md
  86. 2
      framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Security/AbpComponentsClaimsCache.cs
  87. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs
  88. 155
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs
  89. 12
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs
  90. 66
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker.cs
  91. 8
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs
  92. 71
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilter.cs
  93. 19
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilterMode.cs
  94. 353
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs
  95. 37
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerConfiguration.cs
  96. 131
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerManager.cs
  97. 27
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DedicatedWorkerDefinition.cs
  98. 26
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs
  99. 23
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobWorker.cs
  100. 36
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/InMemoryBackgroundJobStore.cs

20
.github/workflows/auto-pr.yml

@ -1,13 +1,13 @@
name: Merge branch dev with rel-10.5
name: Merge branch dev with rel-10.6
on:
push:
branches:
- rel-10.5
- rel-10.6
permissions:
contents: read
jobs:
merge-dev-with-rel-10-5:
merge-dev-with-rel-10-6:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
@ -18,14 +18,14 @@ jobs:
ref: dev
- name: Reset promotion branch
run: |
git fetch origin rel-10.5:rel-10.5
git reset --hard rel-10.5
git fetch origin rel-10.6:rel-10.6
git reset --hard rel-10.6
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
branch: auto-merge/rel-10-5/${{github.run_number}}
title: Merge branch dev with rel-10.5
body: This PR generated automatically to merge dev with rel-10.5. Please review the changed files before merging to prevent any errors that may occur.
branch: auto-merge/rel-10-6/${{github.run_number}}
title: Merge branch dev with rel-10.6
body: This PR generated automatically to merge dev with rel-10.6. Please review the changed files before merging to prevent any errors that may occur.
draft: true
token: ${{ github.token }}
- name: Merge Pull Request
@ -33,5 +33,5 @@ jobs:
GH_TOKEN: ${{ secrets.BOT_SECRET }}
run: |
gh pr ready
gh pr review auto-merge/rel-10-5/${{github.run_number}} --approve
gh pr merge auto-merge/rel-10-5/${{github.run_number}} --merge --auto --delete-branch
gh pr review auto-merge/rel-10-6/${{github.run_number}} --approve
gh pr merge auto-merge/rel-10-6/${{github.run_number}} --merge --auto --delete-branch

126
Directory.Packages.props

@ -58,72 +58,72 @@
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.13.0" />
<PackageVersion Include="MailKit" Version="4.13.0" />
<PackageVersion Include="Markdig.Signed" Version="0.42.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.51" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.51" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.7" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.9" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.9" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="7.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.9" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.71.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Abstractions" Version="1.71.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Composite" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.7.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Composite" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.16.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.16.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.1" />
<PackageVersion Include="Minio" Version="6.0.5" />
<PackageVersion Include="MongoDB.Driver" Version="3.9.0" />
<PackageVersion Include="MongoDB.Driver" Version="3.10.0" />
<PackageVersion Include="NEST" Version="7.17.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Nito.AsyncEx.Context" Version="5.1.2" />
@ -169,18 +169,18 @@
<PackageVersion Include="Slugify.Core" Version="5.1.1" />
<PackageVersion Include="Spectre.Console" Version="0.51.1" />
<PackageVersion Include="StackExchange.Redis" Version="2.9.17" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.7" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.9" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.6.7" />
<PackageVersion Include="System.Linq.Queryable" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.7" />
<PackageVersion Include="System.Security.Permissions" Version="10.0.7" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.9" />
<PackageVersion Include="System.Security.Permissions" Version="10.0.9" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.7" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.7" />
<PackageVersion Include="System.Text.Json" Version="10.0.7" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.9" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.9" />
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="TencentCloudSDK.Sms" Version="3.0.1273" />
<PackageVersion Include="TimeZoneConverter" Version="7.2.0" />
@ -195,6 +195,6 @@
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" />
<PackageVersion Include="Fody" Version="6.9.3" />
<PackageVersion Include="System.Management" Version="10.0.7" />
<PackageVersion Include="System.Management" Version="10.0.9" />
</ItemGroup>
</Project>

4
common.props

@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>10.6.0-preview</Version>
<LeptonXVersion>5.6.0-preview</LeptonXVersion>
<Version>10.6.0-rc.1</Version>
<LeptonXVersion>5.6.0-rc.1</LeptonXVersion>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

71
docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/POST.md

@ -0,0 +1,71 @@
# ABP.IO Platform 10.5 Final Has Been Released!
We are glad to announce that [ABP](https://abp.io/) 10.5 stable version has been released.
## What's New With Version 10.5?
All the new features were explained in detail in the [10.5 RC Announcement Post](https://abp.io/community/announcements/announcing-abp-10-5-release-candidate-k6oxdfle), so there is no need to review them again. You can check it out for more details.
## Getting Started with 10.5
### How to Upgrade an Existing Solution
You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained:
### Upgrading via ABP Studio
If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See [the documentation](https://abp.io/docs/latest/studio/installation#upgrading) for more info.
After upgrading the ABP Studio, then you can open your solution in the application, and simply click the **Upgrade ABP Packages** action button to instantly upgrade your solution:
![](upgrade-abp-packages.png)
### Upgrading via ABP CLI
Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version.
If you haven't installed it yet, you can run the following command:
```bash
dotnet tool install -g Volo.Abp.Studio.Cli
```
Or to update the existing CLI, you can run the following command:
```bash
dotnet tool update -g Volo.Abp.Studio.Cli
```
After installing/updating the ABP CLI, you can use the [`update` command](https://abp.io/docs/latest/CLI#update) to update all the ABP related NuGet and NPM packages in your solution as follows:
```bash
abp update
```
You can run this command in the root folder of your solution to update all ABP related packages.
## Migration Guides
There are no explicitly marked breaking changes in this version. However, there are still some important migration notes for specific scenarios. Please read the migration guide carefully, if you are upgrading from v10.4 or earlier versions: [ABP Version 10.5 Migration Guide](https://abp.io/docs/10.5/release-info/migration-guides/abp-10-5)
## Community News
### New ABP Community Articles
As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:
- [Sumeyye Kurtulus](https://abp.io/community/members/sumeyye.kurtulus) has published 2 new articles:
- [Angular 22 State Management: Signals, SignalStore, or NgRx?](https://abp.io/community/articles/angular-22-state-management-signals-signalstore-or-ngrx-yq8zg0nw)
- [Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI](https://abp.io/community/articles/customizing-the-abp-framework-a-developers-guide-to-nklweri3)
- [Working with Dapr Workflows in the ABP Framework](https://abp.io/community/articles/working-with-dapr-workflows-in-the-abp-framework-6476or18) by [Engincan Veske](https://abp.io/community/members/EngincanV)
- [Alper Ebicoglu](https://abp.io/community/members/alper) has published 2 new articles:
- [My Speaker's View of CONVEX Summit 2026](https://abp.io/community/articles/my-speakers-view-of-convex-summit-2026-ai-net-conference-3uk6ln1l)
- [AI Isn't Replacing Developers - It's Changing What Good Developers Spend Time On](https://abp.io/community/articles/ai-isnt-replacing-developers-its-changing-what-good-2016q6ng)
- [Deep Dive on ABP AI Agent: The Complete Series](https://abp.io/community/articles/deep-dive-on-abp-ai-agent-the-complete-series-f7jute7n) by [Berkan Sasmaz](https://abp.io/community/members/berkansasmaz)
- We have created a deep-dive series for ABP Studio's AI Coding Agent. You can read this series to learn the main features of the AI Coding Agent and how it can help you while developing ABP-based solutions.
Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community.
## About the Next Version
The next feature version will be 10.6. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version.

BIN
docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

BIN
docs/en/Blog-Posts/2026-06-30 v10_5_Release_Stable/upgrade-abp-packages.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

55
docs/en/Blog-Posts/2026-07-06-ABP-Summer-Campaign/post.md

@ -0,0 +1,55 @@
Summer is here, and so is one of the best times to start building with ABP.
From **July 6 to July 20**, we're offering exclusive summer savings on **ABP licenses and renewals**. Save **20% on new licenses** or **10% on license renewals**, and receive **up to $300 in AI credits** to power the **ABP AI Agent** in **ABP Studio**.
Whether you're starting a new project or upgrading your development workflow, this campaign helps you save on your license while accelerating development with AI.
### **What's Included?**
**During the campaign period, you'll receive:**
* **20% off new ABP licenses**
* **10% off license renewals**
* **Up to $300 in AI credits** for the **ABP AI Agent**
The AI credits can be used with the **ABP AI Agent** in **ABP Studio**, allowing you to automate repetitive development tasks and build applications faster.
### **Build Faster with the ABP AI Agent**
The ABP AI Agent is designed specifically for ABP developers. Rather than acting as a generic coding assistant, it understands your ABP solution and helps automate common development workflows.
With the included AI credits, you can:
* Generate application features with AI assistance
* Create entities, services, and UI components faster
* Run automated development workflows
* Generate database migrations and update projects
* Inspect exceptions and troubleshoot issues
* Execute development tasks directly from ABP Studio
The result is less time spent on repetitive work and more time focused on building your application's business value.
### **Why Choose ABP?**
ABP is a complete application development platform for building modern, maintainable, and scalable .NET applications.
With ABP, you can:
* Build enterprise-grade ASP.NET Core applications faster
* Follow Domain-Driven Design (DDD) and clean architecture principles
* Develop modular, reusable, and maintainable application modules
* Leverage built-in capabilities such as multi-tenancy, authentication, authorization, localization, auditing, and more
* Scale from modular monoliths to microservice architectures
* Boost developer productivity with ABP Studio and the ABP AI Agent
Instead of spending weeks building common infrastructure, your team can focus on delivering business value and shipping features faster.
## **Don't Miss This Limited-Time Offer**
This campaign is available **only between July 6 and July 20**.
Whether you're purchasing your first ABP license or renewing your existing one, now is the perfect time to save. Get **20% off new licenses** or **10% off renewals**, plus receive **up to $300 in AI credits** to accelerate development with the **ABP AI Agent**.
**Claim your summer discount before July 20 and start building faster with ABP.**
**Get your discount now:** [https://abp.io/pricing](https://abp.io/pricing)

180
docs/en/Blog-Posts/2026-07-07 v10_6_Preview/POST.md

@ -0,0 +1,180 @@
# ABP Platform 10.6 RC Has Been Released
We are happy to release [ABP](https://abp.io) version **10.6 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version.
Try this version and provide feedback for a more stable version of ABP v10.6! Thanks to you in advance.
## Get Started with the 10.6 RC
You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli).
By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI:
![studio-switch-to-preview](studio-switch-to-preview.png)
## Migration Guide
You can check the migration guide if you are upgrading from v10.5 or earlier: [ABP Version 10.6 Migration Guide](https://abp.io/docs/10.6/release-info/migration-guides/abp-10-6).
## What's New with ABP v10.6?
In this section, I will introduce some major features released in this version.
Here is a brief list of titles explained in the next sections:
- Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention
- API Definition and Proxy Improvements for Content Types and Multipart Uploads
- Angular UI: Upgrade to Angular 22
- Antiforgery and OpenIddict Security Improvements
- OpenIddict: Generate Access Token from the UI
- Dependency Updates
### Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention
ABP v10.6 adds three opt-in enhancements to the default background job worker. All of them are disabled by default, so existing applications keep the current behavior unless you enable them explicitly.
**Storing successful jobs**
By default, a job is deleted as soon as it runs successfully. You can now set `StoreSuccessfulJobs = true` to keep completed jobs in the store. A new `CompletionTime` column marks completed jobs, and a cleanup worker prunes them after `SuccessfulJobRetentionTime` (default: 7 days).
**Dedicated workers per job type**
`AddDedicatedWorker(...)` registers a worker that processes only the configured job argument types, each with its own distributed lock. The default worker continues handling all remaining job types.
**Parallel job execution**
Set `MaxParallelJobExecutionCount` greater than 1 to execute multiple jobs in the same poll cycle. In this mode, each job is claimed with its own distributed lock so different application instances can process different jobs concurrently without running the same job twice.
Example configuration:
```csharp
Configure<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-sale](summer-sale.png)
Summer is a great time to start building with ABP. From **July 6 to July 20**, we're offering exclusive summer savings on **ABP licenses and renewals**: **20% off new licenses**, **10% off renewals**, and **up to $300 in AI credits** for the **ABP AI Agent** in **ABP Studio**. Whether you're starting a new project or upgrading your development workflow, this limited-time offer helps you save on your license while accelerating development with AI.
> You can read the announcement here: [ABP Summer Campaign: Get Up To 20% Off + $300 in AI Credits](https://abp.io/community/announcements/abp-summer-campaign-get-up-to-20-off-300-in-ai-credits-r5lqtpg9).
## Conclusion
This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/10.6/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.6 RC and provide feedback to help us release a more stable version.
Thanks for being a part of this community!

BIN
docs/en/Blog-Posts/2026-07-07 v10_6_Preview/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

BIN
docs/en/Blog-Posts/2026-07-07 v10_6_Preview/studio-switch-to-preview.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
docs/en/Blog-Posts/2026-07-07 v10_6_Preview/summer-sale.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

22
docs/en/Community-Articles/2026-06-24-Meet-ABP-at-WeAreDevelopers-World-Congress-2026/post.md

@ -0,0 +1,22 @@
We are happy to announce that the ABP team will be heading to Berlin for WeAreDevelopers World Congress 2026, one of the largest gatherings of software developers and technology professionals in Europe.
Taking place from **8-10 July 2026**, the event brings together thousands of developers, architects, engineering leaders, startups, and technology companies to explore the latest trends, tools, and ideas shaping the future of software development.
We're excited to be part of this global community once again and look forward to connecting with developers from around the world.
## **Visit Us at the Event\!**
If you're attending WeAreDevelopers World Congress, make sure to stop by **Hall A, Booth A-41** and meet the ABP team.
We'll be showcasing the latest developments across the ABP ecosystem, including ABP Framework, ABP Studio, and our newest AI-powered development capabilities. Whether you're building enterprise applications, modernizing existing systems, or exploring new approaches to software development, we'd love to hear about your projects and challenges.
Our team will be available throughout the event for product demos, technical discussions, and conversations about modern .NET development, modular architecture, microservices, and AI-assisted software development.
## **See You in Berlin**
Nothing replaces meeting developers face-to-face\!
Whether you're already using ABP, evaluating it for a future project, or simply curious about what we're building, we'd be happy to meet you.
See you in **Hall A, Booth A-41** at WeAreDevelopers World Congress 2026\!

86
docs/en/Community-Articles/2026-06-25-ABP-Bootcamp-AI-Assisted-Application-Development-with-ABP/post.md

@ -0,0 +1,86 @@
AI is changing how software is built. Today, developers can generate features, services, tests, and even entire applications in minutes. Tasks that once took hours can now be completed with a single prompt.
But speed is no longer the biggest challenge.Reliability is.
AI generates probabilistic answers. Production software requires deterministic behavior. When developers receive different implementations for the same problem, applications become harder to maintain, harder to scale, and more difficult to evolve over time.
Building software with AI is a lot like constructing a building with power tools.The tools make construction faster. They do not make poor foundations safer.
In fact, they allow mistakes to spread much faster.
The architectural decisions made during the first few months of a project often determine its long-term success. Security, modularity, authorization, maintainability, and development conventions become part of the foundation that everything else depends on.
This is where ABP comes in.
For more than a decade, we've been helping development teams build enterprise-grade .NET applications on solid architectural foundations. Today, companies around the world continue to build and maintain production systems with ABP, even as AI becomes an increasingly important part of the software development process.
We didn't start thinking about AI yesterday.
We've integrated AI into our own development workflows, evolved our startup templates, created AI-specific development rules, and built ABP Studio AI Agent to help developers work more effectively with ABP-based applications.
To help developers learn these practices, we're excited to announce our latest bootcamp:
## **AI-Assisted Application Development with ABP**
This live, instructor-led bootcamp is not about generating code faster.
It's about learning how to build applications faster while maintaining architectural consistency, code quality, and long-term maintainability.
Over three days of hands-on sessions, you'll learn practical AI-assisted engineering workflows using ABP Studio AI Agent and discover how to combine AI productivity with proven software engineering practices.
## **Bootcamp Details**
**Dates:** August 25-27, 2026
**Time:** 17:00-19:00 UTC each day
**Duration:** 6 hours total
**Format:** Live online sessions via Google Meet
**Price:** $399 (discounted from $799)
*\*Participants who do not already have access to ABP Studio AI Agent will receive **complimentary trial access** **for the duration of the bootcamp**. Additional AI credits will be provided when needed.*
## **What You'll Learn**
Throughout the bootcamp, you'll explore real-world AI-assisted software development workflows, including:
* Using AI to accelerate application development with ABP
* Working effectively with the ABP Studio AI Agent
* Generating features, services, and application components faster
* Understanding how AI can assist with implementation, debugging, and code exploration
* Applying AI-assisted engineering practices in real ABP projects
* Combining developer expertise with AI capabilities to improve productivity
The focus will be on practical examples, live demonstrations, and hands-on exercises that you can immediately apply in your own projects.
## **Why Learn From the ABP Team?**
Many AI development courses teach how to generate code.
This bootcamp focuses on something more important: how to generate code that remains maintainable, scalable, and consistent as your application grows.
The ABP team has spent more than 10 years building and evolving one of the most widely used application frameworks in the .NET ecosystem.
We've worked closely with development teams across industries, helped companies build production systems, and recently invested heavily in AI-powered development tools such as ABP Studio AI Agent.
The lessons shared in this bootcamp come directly from our own experience building software with AI, not from theoretical examples or isolated experiments.
You'll learn the same principles, workflows, and practices we use to combine AI-assisted development with real-world software engineering.
## **Who It's For**
This bootcamp is ideal for:
* ABP developers who want to increase productivity with AI
* Software developers interested in AI-assisted software development
* Teams exploring how AI can improve their development workflows
* Technical leaders evaluating AI-powered development practices
* Anyone looking to stay ahead as software engineering continues to evolve
## **Reserve Your Spot**
AI-assisted software development is quickly becoming an essential skill for modern development teams.
This bootcamp is designed to help you understand not only how AI tools work, but how to use them effectively within a real-world application development framework.
Join us and learn how to build applications faster with ABP and AI.
Registration is now open, fill the form: [https://docs.google.com/forms/d/e/1FAIpQLSdREtytTXXEfnOrwuMeTnXs7O10LcVXo-dlyhUNVTX\_dMZriw/viewform?usp=publish-editor](https://docs.google.com/forms/d/e/1FAIpQLSdREtytTXXEfnOrwuMeTnXs7O10LcVXo-dlyhUNVTX_dMZriw/viewform?usp=publish-editor)

207
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post-v2.md

@ -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`

190
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/Post.md

@ -2,106 +2,66 @@
# My Speaker's View of CONVEX Summit 2026 Madrid
# My Speaker's View of CONVEX Summit 2026 Madrid
## Where AI Hype Met Enterprise Reality
Hi, I'm here for another conference review. This time I attended [Convex Summit 2026 Developer](https://www.convexsummit.com/) conference, held in the capital city of Spain, Madrid. It was my first talk in Madrid. The organizer [Plain Concepts](https://www.plainconcepts.com/) arranged a 2 days conf with 3 parallel sessions. 2 of the sessions were in Spanish and one of them was English. The conf dates were 17, 18 June 2026. It was in a very big cinema. In Lithuania I also spoke in a cinema, I guess it's better to arrange a conf in a cinema because of asthenosphere, acoustic and state visibility for visitors.
**Meta description:** My personal speaker’s view of CONVEX Summit 2026 in Madrid, where AI, .NET, software architecture, and enterprise product thinking came together around real-world technology challenges.
There is a small moment before every conference talk when the room becomes quiet, the slides are ready, and you suddenly remember why the topic matters. For me, that moment happened at Convex. My topic was “Turn any database into a conversational reporting engine.” I can admit it was a cool talk. But I was also there as a listener, a software architect, and someone trying to understand where enterprise AI is really going after the first wave of demos and experiments. CONVEX was interesting because it brought software development, architecture, and AI into the same conversation. These topics are often discussed separately, but in real companies they are tightly connected. AI features do not live in isolation. They live inside applications, databases, identity systems, permission models, workflows, dashboards, and business expectations. That was the real theme I felt throughout the event.
**SEO slug:** `convex-2026-ai-enterprise-reality-speaker-experience`
[Convex Summit 2026 Developer](https://www.convexsummit.com/) conference held in the capital city of Spain, Madrid for the first time. [Plain Concepts](https://www.plainconcepts.com/) company organized this event. 2 days with 3 parallel sessions. 2 of the sessions were Spanish and one of them was English. It was organized on 17, 18 June 2026.
There is a small moment before every conference talk when the room becomes quiet, the slides are ready, and you suddenly remember why the topic matters.
For me, that moment happened at CONVEX Summit 2026 in Madrid.
I was there as a speaker, presenting my session **“Chat with Your Data: Turn any database into a conversational reporting engine.”** But I was also there as a listener, a software architect, and someone trying to understand where enterprise AI is really going after the first wave of demos and experiments.
CONVEX was interesting because it brought software development, architecture, and AI into the same conversation. These topics are often discussed separately, but in real companies they are tightly connected. AI features do not live in isolation. They live inside applications, databases, identity systems, permission models, workflows, dashboards, and business expectations.
That was the real theme I felt throughout the event.
## Speaking at CONVEX
![My CONVEX 2026 speaker badge before the sessions started](images/IMG_20927.jpg)
*The speaker badge made the event feel real before the session even started.*
![The CONVEX 2026 main stage in Madrid](images/IMG_20891.jpg)
*The stage setup reflected the scale of the event: three communities, one shared conversation.*
## Speaking at CONVEX
My talk focused on a question that is becoming more important for enterprise software teams:
**What if users could ask their business data questions in natural language—and receive safe, useful, validated answers?**
> What if users could ask their business data questions in natural language and receive safe, useful, validated answers?
I started my slide with Sobrino de Botin restaurant. It's the oldest restaurant in the world according to the Guinness Book of World Records. Sobrino de Botín **has been open since 1725**. The taste never changed, the restaurant **is keeping the same classic taste with the same cooking techniques**, but in the age of AI; we developers should **use new techniques for reporting**. We are not running a restaurant and this is how the development works. Adjust to the tech standards, new techniques and best practises.
I started my slide with **Sobrino de Botin restaurant**. It's the oldest restaurant in the world according to the Guinness Book of World Records. Sobrino de Botín **has been open since 1725**. The taste never changed, the restaurant **is keeping the same classic taste with the same cooking techniques** but in the age of AI; we developers should **use new techniques for reporting**. We are not running a restaurant and this is how the development works. Adjust to the tech standards, new techniques and recent practices.
![image-20260624200005106](images/the-oldest-restaurant-world.png)
And in one part of my talk, I need to ask questions like “Show me the top customers by revenue this quarter.” An AI model generates SQL. The system runs it. The user gets a result. And I learnt some Spanish sentences before the conf. Even for this I used AI. So I translated 10 diffferent English sentences to Spanish. Later I asked ChatGPT: "Can you score my pronunciation for these sentences.". ChatGPT gave the most ratings to 3 of my sentences' prouncations. And I talked those during my interview with my reporting AI tool. And that was fantastic eye-catching moment of my talk.
And in one part of my talk, I need to ask questions like “Show me the top customers by revenue this quarter.” So the LLM answers in my language. And I learnt some Spanish sentences before the conf. Even for this I used AI. So I translated 10 different English sentences to Spanish. Later I asked ChatGPT: "*Can you score my pronunciation for these sentences.*". ChatGPT gave the highest ratings to 3 of my sentences' pronunciations. And I talked those during my interview with my reporting AI tool. And that was fantastic eye-catching moment of my talk.
![Opening slide for “Chat with Your Data” at CONVEX 2026](images/IMG_20976.jpg)
*My session was about building an AI-powered reporting tool that can understand a database and turn questions into controlled reporting workflows.*
![Presenting my “Chat with Your Data” session at CONVEX 2026](images/IMG_20977.jpg)
![Some photos from my talk 1](images/my-pictures-1.jpg)
*My session focused on building conversational reporting experiences without giving up control, validation, or security.*
I asked the attendees how many of you have written SQL, created reporting screen, and %80 of people wrote SQL and created reporting UIs. And same amount of people also use .NET.
One of my slides showed the reporting problem in a very familiar way. Traditional reporting tools are powerful, but they can be rigid. In many companies, a new report still means writing SQL, designing a page, testing the output, deploying a change, and waiting for a developer to become available. The result is that business users wait, developers become the bottleneck, and valuable data stays trapped behind technical complexity.
The approach I shared has several layers.
First, the system needs **schema awareness**. The AI should understand tables, columns, relationships, keys, data types, and enum values. Depending on the system, it may also need safe sample values or extra DDL information. Without that structured context, natural-language reporting becomes fragile.
![A slide from my session about teaching the AI the database schema](images/IMG_20974.jpg)
*One of the most important parts of the talk was schema and system-prompt design: tables, columns, relationships, keys, data types, enum values, and controlled rules for query generation.*
I asked the attendees how many of you have written SQL and created reporting screens, and %80 of people wrote SQL and created reporting UIs. And the same number of people also use .NET.
Second, it needs **intent interpretation**. Users do not always speak in database terms. They ask business questions. A good reporting engine should translate business language into technical meaning without forcing users to know the data model.
![Some photos from my talk 2](images/my-pictures-2.jpg)
Third, it needs **safe SQL generation**. Letting an LLM freely generate and execute SQL is risky. The model should operate under strict rules: read-only access, no destructive operations, tenant-aware filtering, query limits, validation before execution, and clear logging.
> **AI can make data more accessible, but architecture must define the boundaries.**
Fourth, it needs **useful output**. A raw result table is not always enough. Sometimes the user needs an Excel file, a chart, a summary, or a clarification question.
![A session slide about turning data access into a reporting engine](images/IMG_20980.jpg)
Most business users do not want to “build a report.” They want an answer. But as developers and architects, our job is to make that experience safe, explainable, and reliable.
AI can make data more accessible, but architecture must define the boundaries.
---
## What I Learned From the English Sessions
## What I Learned From the Other Sessions
There were 3 parallel session tracks. One of them was completely English sessions. One of the reasons I enjoyed CONVEX was that the English sessions did not treat AI as magic.
There was a track with completely English sessions. One of the reasons I enjoyed Convex was that the English sessions did not treat AI as magic.
The strongest message I heard across different sessions was this:
**AI is becoming part of real systems, and real systems have constraints.**
> **AI is becoming part of real systems and real systems have constraints.**
And I can see, software development is rapidly evolving with AI agentic tools.
> We cannot say development is dead, but hand-made development is dead.
> **We cannot say development is dead, but hand-made development is dead.**
From now on we'll use our time less on typing and more on thinking about features, user experiences and robust infrastructure.
![hand-made-coding](images/hand-made-coding.png)
From now on, we'll use our time less on typing and more on thinking about features, user experiences and robust infrastructure.
**In the age of AI, writing code by hand is the software equivalent of sending a fax to prove commitment.**
![hand-made-coding](images/hand-made-coding.png)
For a while, many AI discussions were focused on what AI could generate: code, tests, text, SQL, documentation, designs. At CONVEX, the more interesting question was:
For a while, many AI discussions were focused on what AI could generate: code, tests, text, SQL, documentation, designs.
At CONVEX, the more interesting question was:
**What happens after AI generates something?**
- Who validates it?
- Who owns the decision?
- Can we say I don't know to a question about how that works!?
- If something blows up or happens a data leakage, who is accountable for it?
- How does it fit into the architecture?
- How do we prevent data leakage?
- How do we make it useful for the business instead of impressive for five minutes?
### Architecture is also a people problem
@ -112,56 +72,86 @@ One session that stayed with me used the idea of the **Prisoner’s Dilemma** to
*The architecture sessions connected technical decisions with incentives, collaboration, and long-term system health.*
Another slide suggested practical ways forward: learn the business, understand the competition, avoid “big bang” changes, work incrementally, create options, make trade-off decisions with the business, and **become a business value creator** rather than someone who only responds to requests.
Another slide suggested practical ways forward: learn the business, understand the competition, avoid “big bang” changes, work incrementally, create options, make trade-off decisions with the business and **become a business value creator** rather than someone who only responds to requests.
I liked that message. Architecture is much more valuable when it helps the business create options, not when it only explains why something is risky.
### Power, knowledge, and decision-making
---
### Power, knowledge and decision-making
Another interesting thread was about power in organizations. One talk referenced **Power-With**, an idea I found useful because it shifts the conversation away from control and toward collaboration. The slides connected power with access to knowledge, authority, charisma, and the way decisions move through an organization.
Another interesting thread was about power in organizations. One talk referenced **Power-With**, I found it useful because it shifts the conversation away from control and toward collaboration. The slides connected power with access to knowledge, authority, charisma and the way decisions move through an organization.
![A session slide discussing Power-With and organizational dynamics](images/IMG_20914.jpg)
*Some of the most interesting moments connected architecture with people, incentives, and organizational reality.*
This may sound less technical than a database, a framework, or a deployment pipeline. But in practice, many technical decisions fail or succeed because of organizational dynamics. A clean architecture can still fail if teams are not aligned. A promising AI feature can still fail if no one trusts the output.
There are 2 powers:
1. **Power-Over**: Conquer other people's mind, control, force and order for a work output.
2. **Power-With**: Co-operate with others, use everyone's power into a big power, move together for a work output.
This may sound less technical than a database, a framework, or a deployment pipeline. But in practice, many technical decisions fail or succeed because of organizational dynamics. A clean architecture can still fail if teams are not aligned.
> A promising AI feature can still fail if no one trusts the output.
### So Let's Think What's Charisma at Work
- **What's charisma actually?** Let me tell you my opinion; charisma is experience, wisdom, grace, listening more and speaking less, way of looking to life, trustability (reliability), being a role model and an inspiration to others.
- **Why charisma is important?** It makes your words to be listened by other people **naturally** (without any need of dictation).
One slide quoted the idea that knowledge workers “think for a living.” Another referenced Peter Naur’s **Programming as Theory Building**, where program text and documentation are not always enough to carry the most important design ideas. That felt very relevant in the age of AI-generated code. If code becomes easier to produce, shared understanding becomes even more valuable.
### AI agents need more than task execution
As a summary; AI can often understand the architecture from the code. What it usually can't know reliably is **why**
- the architecture ended up that way; the design decisions
- trade-offs
- historical context
- assumptions that exist mostly in the team's shared understanding rather than in the code itself.
---
### What AI Agents can do and cannot do?!
One of the most thought-provoking slides at the conf explored the current boundaries of AI agents. Instead of asking whether AI will replace humans, it asked a more interesting question: **What can AI agents actually do today, what can't they do and which limitations might disappear over time?**
The first column listed the things AI agents already do well. They can execute tasks reliably without getting tired, maintain context across long-running work, report progress, detect problems, follow established processes, generate alternatives, document their work, and scale by running thousands of instances simultaneously. In short, AI excels at **execution**.
The second column focused on capabilities that are fundamentally human today. AI can perform the role of a manager, mentor, or teammate, but it cannot truly *be* one. It cannot be held accountable for its decisions, earn trust through years of shared experience, genuinely care about an outcome, feel the weight of failure, or belong to a team. It can disagree or refuse a request, but not from genuine conviction or personal values. These qualities come from human relationships, responsibility, and lived experience, not from generating the next token.
The AI-related sessions also made a useful distinction between what AI agents can do today and what humans still bring to teams.
The third column added an important nuance. It wasn't titled "Impossible," but rather "Cannot, but maybe will one day." Some capabilities are already beginning to emerge. Multi-agent systems can delegate work to other agents. Better memory and continuity may allow AI to build trust over time. Multimodal models are becoming better at reading context, and reinforcement learning is an early form of learning from mistakes. The point wasn't that these problems are solved, but that some of today's limitations may become tomorrow's capabilities.
The overall message was refreshingly balanced. AI agents should not be viewed as human employees, nor should they be underestimated. They are exceptional at executing work at scale, but organizations are built on more than execution. Trust, accountability, judgment, conviction, and belonging remain deeply human qualities, for now.
One slide compared capabilities such as being assigned a task, executing work, holding context, reporting progress, flagging anomalies, following processes, generating options, reviewing work, documenting outputs, working asynchronously, and scaling across many instances.
But the same slide also pointed to harder human qualities: accountability, trust earned over time, judgment under competing priorities, reading the room, feeling the weight of failure, belonging to a team, and genuinely caring about the outcome.
![A slide comparing what AI agents can and cannot do](images/IMG_20935.jpg)
*The AI agent discussion was interesting because it did not only focus on automation; it also highlighted accountability, trust, judgment, and team dynamics.*
That is a healthy way to talk about AI. Not “AI will replace everything,” and not “AI is useless.” The real question is where AI can help a team, and where humans still need to own the decision.
### Charisma at Work
That is a healthy way to talk about AI. Not “*AI will replace everything*,” and not “*AI is useless.*”
- **What's Charisma actually?** Let me tell my opinion; The charisma is the experience, the knowledge, wisdom, elegance, listening more and talking less, way of looking to life, performing good on the responsibilities, trustability, being inspiring.
- **Why Charisma is important?** It makes your words to be listened by other people.
> **The real question is where AI can help a team and where humans still need to own the decision.**
### Better decisions need better records
I also followed sessions about architecture principles and decision records. One slide explained principles as priorities, beliefs, guardrails, and a way to connect requirements to architectural decisions. Another used a simple architectural decision example: **Data Store per Service**, where each service owns its data and other services access it through APIs or events instead of direct database queries.
I also followed sessions about architecture principles and decision records. One slide explained principles as priorities, beliefs, guardrails and a way to connect requirements to architectural decisions. Another used a simple architectural decision example: **Data Store per Service**, where each service owns its data and other services access it through APIs or events instead of direct database queries.
![A slide about architecture principles and decisions](images/IMG_20952.jpg)
*Architecture principles were presented as guardrails that connect requirements, trade-offs, and decisions.*
This connected nicely with another slide about ADRs. A minimal ADR, based on Michael Nygard’s format, includes a name, status, context, decision, and consequences. A more comprehensive ADR can also include related requirements, assumptions, constraints, options, reasoning, and trade-offs.
Let me first define what's ADR:.. An **ADR (Architecture Decision Record)** is a short document that explains **an important technical decision, why it was made, and what the consequences are**. See the [Microsoft Document about ADR](https://learn.microsoft.com/en-us/azure/well-architected/architect-role/architecture-decision-record).
> **Code tells you *what* the system does. An ADR tells you *why* it was designed that way.**
This connected nicely with another slide about ADRs. A minimal ADR, based on Michael Nygard’s format, includes a name, status, context, decision, and consequences. A more comprehensive ADR can also include related requirements, assumptions, constraints, options, reasoning and trade-offs.
![A slide explaining the minimal ADR structure](images/IMG_20960.jpg)
*The ADR discussions were a reminder that good architecture is not only about making decisions, but also about preserving the reasoning behind them.*
For .NET teams, these ideas are practical. AI can sit on top of strong foundations around backend services, identity, data access, cloud integration, and enterprise applications—but it should not bypass them.
For .NET teams, these ideas are practical. AI can sit on top of strong foundations around backend services, identity, data access, cloud integration, and enterprise applications, but it should not bypass them.
If anything, AI makes good engineering discipline more important.
@ -169,45 +159,33 @@ If anything, AI makes good engineering discipline more important.
The venue, Kinépolis Ciudad de la Imagen in Madrid, gave the conference a different feeling from a typical hotel-based event. The rooms, stage, and screens made the sessions feel cinematic. Outside the session rooms, the networking areas were active throughout the day. People were not only exchanging LinkedIn profiles; they were continuing the technical debates from the talks.
![Participants networking at CONVEX Summit 2026](images/IMG_20888.jpg)
![Participants networking at CONVEX Summit 2026](images/convex-ambiance.jpg)
*The networking areas were busy between sessions, creating space for conversations beyond the formal agenda.*
![Attendees moving between sessions at the venue](images/IMG_20890.jpg)
*The event had a steady flow between talks, networking areas, and informal conversations.*
For me, the most valuable conversations happened after the talk. Several people came with practical questions about AI-assisted reporting: How much schema information should be given to the model? Should generated SQL be shown to users? How do we prevent dangerous queries? Can this work with multi-tenant applications? How should we evaluate the quality of AI-generated reports? What should be logged for auditing?
These questions matter because they show that teams are moving from curiosity to implementation. They are no longer asking only, “Can we do this?” They are asking, “How can we do this safely inside our product?”
That is a much better question.
![A personal moment from the event floor at CONVEX 2026](images/IMG_20970.jpg)
*I'm preparing my setup and waiting the attendees from lunch :) One of those small personal moments that makes a conference feel memorable, not only useful.*
A good conference gives you two things: ideas from the stage and better questions from the people you meet. CONVEX did both.
I met people from my country as well from Bosch and Aselsan companies...For me, the most valuable conversations happened after the talk. I made new friends and learn what other people do.
## My Key Takeaways
1. **AI features need data boundaries.** The more natural the interface becomes, the more important permissions, context, and allowed actions become.
2. **Natural language is becoming a product interface.** Users increasingly want answers, not navigation paths.
3. **Architecture is becoming more important, not less.** AI can accelerate delivery, but it cannot remove product constraints, security requirements, or organizational complexity.
4. **Decisions need memory.** Principles, ADRs, trade-offs, and exceptions help teams preserve reasoning.
5. **Conferences still matter.** A hallway discussion after a session can sometimes teach you more than a full article or video. It's a way of motivation, a way of socializing for developers. You see what other do, you discuss with them, you know your customers...
2. **Natural language is becoming a product interface.** Users increasingly want answers, not get lost in the UI and not navigation paths.
3. **Architecture is becoming more important, not less.** AI can accelerate delivery, but it cannot remove product constraints, security requirements, or organizational complexity.
> When the calculator was first invented, they didn't think problem solving finished with this invention; people spent more time solving problems rather than calculating....
4. **Decisions need memory.** Principles, ADRs, trade-offs and exceptions help teams preserve reasoning.
5. **Conferences still matter.** A hallway discussion after a session can sometimes teach you more than a full article or video. It's **a way of motivation**, a way of socializing for developers. You see what others do, you discuss with them, you know your customers, and you know where you're at in development.
🖼 All photos of Convex Summit 2026 are available 👉 https://www.flickr.com/photos/204742998@N04/albums/72177720334500733/with/55369833471
## My Cultural Visits
I visited Toledo and Madrid's most important tourist attractions and museums. Now I know way much better then I knew before about Spanish culture and lifestyle. But this is the most impressive moment for me. As you may know I'm Turkish. My grand grandfathers were Ottomans living in Anatolia. In 1571 they were in a war with Spain, Genoa, Malta and Italy. The battle was in the sea near Greece. It's called Sea Battle of Lepanto. In the below pictures, you can see the Ottoman's highest level sea commander (we call him Kaptan-ı Derya -the captain of seas-) personal items when he was died in this war. For those who wants to see it; it's Royal Palace of Madrid and the items are in Royal Armoury department.
I visited Toledo and Madrid's most important tourist attractions and museums. Now I know way more than I knew before about Spanish culture and lifestyle. But this is the most impressive moment for me. As you may know I'm Turkish. My grand grandfathers were Ottomans coming from Mongolia to Anatolia. In 1571 the Ottomans were in a war with Spain, Genoa, Malta and Italy. The battle was in the sea near Greece. It's called Sea Battle of Lepanto. In the pictures below, you can see the Ottoman's highest-level sea commander's personal items. We call him **Kaptan-ı Derya** -the captain of the seas-. He died in this war. For those who want to see it, it's in the Royal Palace of Madrid, and the items are in the Royal Armory department.
If you're interested in this war, I'll little bit tell you about it.
If you're interested in this war, you can also read this part:
### The Battle of Lepanto ⚔
The Ottoman army was invading Cyprus. Angered by this, European countries asked Spain—one of the strongest kingdoms of the period—for help. To retake the island, a Holy Christian fleet was assembled under Spanish leadership. On 7 October 1571, the forces arrived at the Gulf of Patras in Greece for what would become known as the Battle of Lepanto. On one side was the Ottoman army, commanded by Ali Pasha, known as Kaptan-ı Derya (the Grand Admiral). On the opposing side were the major European powers: Spain, Venice, the Papacy, Genoa, the Knights of Malta, and Italy. It would become the last major naval battle fought with oared warships. The Ottomans lost the battle, and the sultan of that time Sokollu Mehmed Pasha said the following famous saying: “*You have cut off our beard, but we have cut off your arm. A beard grows back.*” More than 200 Ottoman ships were lost. Tens of thousands of soldiers were killed or captured. In the below photograph, you will see war trophies taken from Ali Pasha. In Spain, this battle would be called “*La Defensa de la Cristiandad*” means “*The Defense of Christianity*” and would be used for propaganda for years. These trophies were used as symbols. The first time I saw the exhibit in a museum, I stood in front of it for 15–20 minutes, simply looking at it.
The Ottoman army was invading Cyprus. Angered by this, European countries asked Spain, one of the strongest kingdoms of the period for help. To retake the island, a Holy Christian fleet was assembled under Spanish leadership. On 7 October 1571, the forces arrived at the Gulf of Patras in Greece for what would become known as the Battle of Lepanto. On one side was the Ottoman army, commanded by Ali Pasha (the Grand Admiral). On the opposing side were the major European powers: Spain, Venice, the Papacy, Genoa, the Knights of Malta, and Italy. It would become the last major naval battle fought with oared warships. The Ottomans lost the battle and Sokollu Mehmed Pasha (he's normally Serbian Turk and he's the most powerful manager after Sultan) said the following famous saying: “*You have cut off our beard, but we have cut off your arm. A beard grows back.*” More than 200 Ottoman ships were lost. Tens of thousands of soldiers were killed or captured. In the below photograph, you will see war trophies taken from Ali Pasha. In Spain, this battle would be called “*La Defensa de la Cristiandad*” means “*The Defense of Christianity*” and would be used for propaganda for years. These trophies were used as symbols. The first time I saw the exhibit in a museum, I stood in front of it for 15–20 minutes, simply looking at it.
![royal-palace-0](images/royal-palace-0.jpg)
@ -230,21 +208,19 @@ From the outside, it looks like nothing more than a “brutal spectacle,” but
- **Bulls don’t react to the color red:** They are actually color-blind. What triggers them is the movement of the cape, not its color. Red is purely for visual aesthetics.
- **The selected bull is a special, wild breed:** The animal has almost never seen a human before entering the arena. It sees the matador as an enemy and wants to destroy him.
- **The greatest honor is to survive:** If a bull shows exceptional nobility and courage, the audience waves white handkerchiefs to ask for its pardon. That bull never enters the arena again and spends the rest of its life like a king on a farm.
- **A dance with death:** Matadors do not see this as a sport, but as a way of confronting death. When making the most critical strike, the matador must also put his own life at risk—he cannot simply stab the bull from behind. He has to face the bull head-on, bravely. In that sense, there is a strange bond of respect between them. The bull is powerful, but the matador is intelligent. He cannot defeat it through strength, only through skill, timing, and agility.
- **A dance with death:** Matadors do not see this as a sport, but as a way of confronting death. When making the most critical strike, the matador must also put his own life at risk, he cannot simply stab the bull from behind. He has to face the bull head-on, bravely. In that sense, there is a strange bond of respect between them. The bull is powerful, but the matador is intelligent. He cannot defeat it through strength, only through skill, timing, and agility.
- **It is a controversial subject**, but hearing it firsthand in its own context changed my perspective considerably. At a time when animal rights are more important than ever, this tradition still continues. And I have now learned that bullfighting has a much deeper philosophy behind it than I had realized.
![bull-fight](images/bull-fight.jpg)
## Closing
I left CONVEX 2026 with new ideas, useful feedback, and a stronger belief that the next phase of enterprise AI will be less about impressive demos and more about trusted systems.
I left Convex 2026 and Spain with new ideas, useful feedback, and a stronger belief that the next phase of enterprise AI will be less about impressive demos and more about trusted systems.
For me, the most interesting AI features are not the ones that look magical. They are the ones that quietly solve a real problem, respect the architecture around them, and help users make better decisions.
Thank you to the CONVEX organizers, the speakers, and everyone who joined my session or continued the conversation afterward.
Thank you to the Convex organizers, the speakers, and everyone who joined my session or continued the conversation afterward.
Madrid was a great place to talk about AI, .NET, architecture, and the future of enterprise software. I hope to see many of you again at the next event.
---

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20888.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 930 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20890.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20891.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20927.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 KiB

After

Width:  |  Height:  |  Size: 294 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20935.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 474 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20970.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 906 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20974.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 823 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20976.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20977.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/IMG_20980.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 922 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/convex-ambiance.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/cover.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/hand-made-coding.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 962 KiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/images/my-pictures-2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
docs/en/Community-Articles/2026-06-25-Convex-Summit-2026-Recap/volosoft-presentation.pptx

Binary file not shown.

349
docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/Post.md

@ -0,0 +1,349 @@
A lot of the current AI discussion in software development swings between two extremes: either AI will write everything, or it is just autocomplete with better marketing.
Neither view is especially useful.
What the evidence shows is more practical: AI coding tools can improve developer throughput on certain tasks, especially repetitive work, scaffolding, and first drafts. But they do not remove the need for developers. In many teams, they actually create a new category of work around review, verification, security, and long-term maintainability.
That is the real story. AI is not replacing developers. It is changing what developers do, what teams optimize for, and where engineering judgment matters most.
## The productivity gains are real, but they are not magic
There is enough data now to move beyond hot takes.
Across multiple studies and industry reports, AI coding assistants show measurable productivity gains, but those gains are usually modest rather than transformational:
- A BlueOptima analysis across 30,000 developers in 18 enterprises reported an average productivity uplift of 5.4%, with the most active users seeing gains closer to 20%.
- An open source study found roughly a 6.5% project-level productivity increase after Copilot adoption.
- GitHub survey data from more than 2,000 developers showed strong perceived benefits: improved flow, less mental drain on repetitive tasks, and greater job satisfaction.
- A longitudinal study from a large public-sector engineering organization found that developers using Copilot were already highly active, and while they reported productivity improvements, commit-based metrics did not show a statistically significant post-adoption jump.
That last point matters.
Perceived productivity and actual output are not always the same thing. Developers may feel faster because they spend less time on boilerplate, search, or syntax recall. That feeling is valuable. Less friction often means better focus. But it does not automatically translate into dramatically more shipped business value.
In other words, AI helps, but it does not suspend the usual constraints of software delivery:
- unclear requirements still slow teams down
- poor architecture still creates drag
- bad testing practices still leak defects
- messy codebases are still messy codebases
If your delivery bottleneck is typing, AI looks revolutionary. If your bottleneck is product ambiguity, compliance, integration complexity, or production risk, AI helps less than the marketing suggests.
## What AI coding tools are actually good at
The strongest use case for AI in development is not autonomous software engineering. It is acceleration of narrow, well-bounded tasks.
AI coding assistants are usually good at:
- generating boilerplate
- filling in repetitive CRUD patterns
- writing simple tests and test skeletons
- suggesting refactors
- producing documentation drafts
- translating between languages or frameworks
- helping developers recall APIs and syntax
- creating a first pass for routine utility code
This is why many developers genuinely like these tools. They reduce low-value friction.
A practical example:
A developer building an ABP-based application might use AI to:
- scaffold DTO mappings
- draft validation rules
- generate basic unit test cases
- create repository query examples
- summarize a service class before refactoring
Those are useful accelerators. But the same tool is much less reliable when asked to decide:
- whether a module boundary is correct
- how to model a permission system
- what tradeoff to make between consistency and performance
- how multi-tenancy affects data access rules
- which abstraction will still be maintainable a year later
That is the dividing line. AI handles local code generation better than system-level reasoning.
![Generated illustration](inline-1.png)
## Where developers are still irreplaceable
The most valuable parts of software development were never just typing code.
Developers are still responsible for the parts AI consistently struggles with:
### Understanding the problem behind the ticket
Business requirements are often incomplete, contradictory, or politically constrained. A human developer can ask the uncomfortable question, spot hidden assumptions, and translate vague intent into a workable implementation.
AI can generate an answer. It cannot reliably challenge the question.
### Making architecture tradeoffs
Real systems involve tradeoffs, not ideal answers.
Should this feature live in an existing module or a new service? Is eventual consistency acceptable here? Are we optimizing for onboarding speed, runtime performance, auditability, or cost control?
These decisions depend on context that usually lives outside the prompt window.
### Working safely in large, imperfect codebases
Most production systems are not greenfield demos. They include legacy code, weird integrations, undocumented conventions, and historical constraints.
This is where experienced developers earn their keep. They know that the technically correct change is not always the operationally safe change.
### Taking responsibility for outcomes
An AI assistant does not get paged at 2 a.m. It does not own the incident review. It does not explain a data leak to legal, security, or customers.
Software engineering is not just generation. It is accountability.
## The hidden cost: verification debt
One of the most important ideas in the current AI coding debate is verification debt.
AI can generate code quickly, but that speed often shifts effort downstream. Instead of spending time writing code, teams spend time validating whether the generated code is correct, secure, idiomatic, and maintainable.
That creates a new form of debt:
- code is produced faster than it is reviewed properly
- weak suggestions slip into the codebase because they look plausible
- reviewers must inspect more generated code with lower trust
- maintenance costs rise later because low-context code ages badly
Recent survey data points in the same direction:
- 72% of developers reported using AI tools daily
- AI contributes a substantial share of committed code in some teams
- 96% of developers do not fully trust AI-generated code
- less than half consistently review AI-generated code before committing
- 38% say reviewing AI code can take longer than reviewing human-written code
That combination should worry engineering leaders.
If teams accept more machine-generated code while also trusting it less, the result is not full automation. The result is a fragile review pipeline.
This is why senior engineers are not becoming obsolete. Their work is shifting toward validation, standards, and system integrity.
![Generated illustration](inline-2.png)
## Security is the clearest reason AI won’t replace developers
If you want one hard reality check, it is security.
AI-generated code often looks polished. That makes insecure output more dangerous, not less dangerous.
Research and industry testing have found recurring problems such as:
- flawed input validation
- weak authentication or authorization logic
- unsafe serialization patterns
- insecure defaults
- cross-site scripting exposure
- log injection issues
- dependency and configuration mistakes
A Veracode study covering 100 LLMs across 80 coding tasks found that about 45% of AI-generated code samples contained security flaws. Reported failure rates were especially high in some languages and security-sensitive tasks.
That aligns with what many teams see in practice: AI can produce code that appears complete while quietly missing the exact defensive details that matter in production.
There is a second security problem too: the tools themselves.
Recent research into AI-enabled IDE workflows has highlighted risks such as:
- prompt injection through project content
- data exfiltration from workspace context
- misuse of tool permissions
- remote code execution paths via compromised assistant workflows
So the risk surface is now two-layered:
1. the generated code may be unsafe
2. the coding assistant environment may itself introduce supply-chain and data exposure risks
That is not a path to replacing developers. It is a path to needing more disciplined developers.
![Generated illustration](inline-3.png)
## Why junior and senior developers benefit differently
AI does not help every developer in the same way.
Less experienced developers often benefit the most from:
- faster onboarding
- easier exploration of unfamiliar APIs
- reduced time spent on repetitive syntax work
- quick examples to unblock momentum
That is a good thing. Used well, AI can shorten the distance between "I know the concept" and "I can build the first version."
But there is a catch.
If juniors over-rely on generated solutions they do not understand, they can ship code without building judgment. That creates a team with higher output but thinner engineering depth.
Senior developers usually get less value from raw generation and more value from targeted acceleration. Their role shifts toward:
- architectural direction
- code review and design review
- defining guardrails
- mentoring developers on when not to trust the tool
- shaping prompts and workflows around quality
That is not replacement. It is role redistribution.
## The best teams treat AI like a power tool, not a developer
The most productive framing is simple: AI is a power tool.
A power tool can make a skilled worker much faster. It can also let an unskilled worker make bigger mistakes faster.
Teams getting real value from AI coding assistants usually do a few things consistently.
### They define where AI is allowed to help
For example:
- okay for scaffolding and test drafts
- okay for documentation summaries
- okay for refactoring suggestions in low-risk modules
- not okay for auth flows without explicit review
- not okay for security-sensitive changes without human design approval
- not okay for direct commits to critical paths
### They keep human review non-negotiable
AI-generated code should be reviewed like code from a new team member who is fast, confident, and occasionally wrong in subtle ways.
That means checking:
- correctness
- security
- consistency with project conventions
- operational impact
- maintainability six months from now
### They invest in guardrails
Useful guardrails include:
- secure coding standards
- mandatory tests for generated code
- SAST and dependency scanning
- branch protection and review policies
- secret scanning
- documented AI usage rules
- prompt hygiene, especially around sensitive data
### They optimize for maintainability, not just speed
The wrong metric is lines generated.
Better metrics include:
- cycle time without increased incident rate
- review burden
- escaped defects
- time to understand generated code later
- security findings per change
![Generated illustration](inline-4.png)
## When AI helps most — and when it helps least
A balanced view is more useful than either fear or hype.
### When to use AI-assisted coding
AI is a strong fit when:
- the task is repetitive or pattern-based
- the scope is narrow and easy to verify
- the code is low-risk and well-tested
- you need a first draft, not a final answer
- developers understand the output well enough to challenge it
- the team has solid review and security practices
Examples:
- generating DTOs, mappings, and validation stubs
- producing test cases for straightforward services
- drafting migration scripts that will be reviewed carefully
- summarizing unfamiliar code before manual refactoring
### When not to rely on AI-assisted coding
AI is a poor fit when:
- business rules are complex or ambiguous
- security is central to the change
- architecture decisions are still in flux
- the code touches compliance-heavy or highly regulated paths
- the surrounding codebase has lots of undocumented behavior
- the team is unlikely to review the output carefully
Examples:
- permission and tenancy boundaries
- payment or identity workflows
- critical infrastructure automation
- cross-service consistency logic
- sensitive data handling and audit trails
## What this means for the future of software teams
AI is changing software development, but not in the simplistic way people often describe.
The likely outcome is not fewer developers because code writes itself. The more plausible outcome is a different distribution of engineering work:
- more generated code
- more review and verification work
- more emphasis on architecture and systems thinking
- more value placed on security awareness
- more leverage for developers who can guide tools effectively
There are also organizational effects.
If AI tools can remove some low-level friction, teams may ship faster. Some studies and industry analyses even project large macroeconomic gains from AI-augmented software work. But inside engineering organizations, those gains depend on whether speed is paired with discipline.
Without discipline, AI increases noise.
With discipline, AI increases leverage.
That is the distinction leaders should care about.
## The developer job is not disappearing — it is getting more judgment-heavy
The strongest developers in the AI era will not be the ones who generate the most code. They will be the ones who can:
- define the problem clearly
- evaluate tradeoffs
- spot incorrect assumptions
- review machine output efficiently
- protect quality under delivery pressure
- turn generated fragments into coherent systems
That is a more senior version of software engineering, not a smaller one.
Typing code was never the whole profession. It was just the most visible part. AI is making that easier, which means the less visible parts now matter even more.
And those parts are deeply human: judgment, context, responsibility, and taste.
## TL;DR
- AI coding tools improve productivity on repetitive, bounded tasks, but the gains are usually incremental, not total automation.
- Developers are still needed for architecture, business logic, tradeoffs, security, and accountability.
- AI-generated code often adds verification debt, increasing review and maintenance work later.
- Security remains a major limitation, both in generated code and in AI-assisted development workflows.
- The winning teams use AI as a power tool with strong guardrails, not as a replacement for engineering judgment.

BIN
docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 KiB

BIN
docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/en/Community-Articles/2026-06-25-ai-isnt-replacing-developers-its-changing-what-good/inline-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 KiB

631
docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/Post.md

@ -0,0 +1,631 @@
Caching is one of those topics that looks simple until an application starts scaling. The first version works fine with direct database reads. Then traffic grows, page loads become inconsistent, and suddenly the team is debating Redis, stale data, invalidation, and why one node sees fresh data while another still serves old results.
ABP Framework gives you a solid caching foundation, but the important part is choosing the right caching strategy for the job. Not everything should be cached the same way. A read-only lookup list, a tenant-specific settings object, and an entity that changes every minute do not have the same caching needs.
This article walks through the practical caching strategies in ABP Framework, what each one is good at, how to configure them, and the mistakes that usually show up in production.
## Understand ABP's caching model first
ABP builds its caching support on top of `Microsoft.Extensions.Caching.Distributed.IDistributedCache`. That matters because ABP does not invent a completely separate caching universe. Instead, it adds practical features developers actually need in real systems:
- typed cache abstractions
- automatic serialization and deserialization
- tenant-aware cache keys
- configurable key prefixes
- batch operations
- optional Unit of Work awareness
- safer error handling defaults
Out of the box, the default distributed cache implementation is `MemoryDistributedCache`. Despite the name, this is still wired through the distributed cache abstraction, but the storage is in-memory for the current app instance.
That is fine for:
- local development
- demos
- single-node monoliths
- low-risk cached reads
It is not enough for:
- load-balanced deployments
- Kubernetes or App Service scale-out
- background workers sharing cached data with web apps
- any scenario where multiple instances must see the same cache state
In those cases, you should move to a real distributed provider such as Redis.
![Generated illustration](inline-1.png)
## Strategy 1: Use typed distributed cache for application data
For most ABP applications, the default and most useful strategy is the generic typed distributed cache.
ABP provides:
- `IDistributedCache<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
![Generated illustration](inline-2.png)
## Strategy 3: Prefer Redis for real distributed deployments
A lot of caching problems are not about API design. They are deployment problems.
If you run multiple application instances and still use the default in-memory distributed cache implementation, each node will maintain its own private cache state. That means:
- node A may have fresh data
- node B may have stale data
- invalidation on one node does not magically update the others
- behavior becomes inconsistent under load balancing
For production scale-out, Redis is usually the practical answer.
ABP provides Redis integration through `Volo.Abp.Caching.StackExchangeRedis`.
### Basic setup idea
Install the Redis caching package and configure distributed caching as your backing provider. ABP then continues to use its caching abstractions, while Redis stores the actual cache entries.
A typical module configuration looks like this:
```csharp
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
[DependsOn(typeof(AbpCachingStackExchangeRedisModule))]
public class MyProjectModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration["Redis:Configuration"];
});
Configure<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
![Generated illustration](inline-3.png)
## Strategy 6: Treat multi-tenancy as a cache design concern, not a detail
ABP automatically includes the current tenant ID in cache keys for typed distributed cache scenarios unless multi-tenancy is explicitly ignored.
This is one of those features that quietly prevents serious data leaks.
Without tenant-aware cache keys, this can happen:
- tenant A requests a settings object
- it gets cached under a generic key
- tenant B requests the same logical object
- tenant B receives tenant A's cached data
That is not just a bug. In many systems, it is a security incident.
### Practical guidance
For multi-tenant systems:
- keep tenant-aware caching enabled by default
- only ignore multi-tenancy for truly global shared data
- review custom key-building logic carefully
- test cache behavior with at least two tenants in integration tests
If a cache item is intentionally global, make that decision explicit and document it.
## Strategy 7: Be deliberate about expiration policy
A lot of bad caching behavior comes from expiration values chosen almost randomly.
ABP lets you define expiration using `DistributedCacheEntryOptions`, including:
- `AbsoluteExpiration`
- `AbsoluteExpirationRelativeToNow`
- `SlidingExpiration`
ABP also supports global defaults through `AbpDistributedCacheOptions`. If you do not specify item-level options, a default sliding expiration is commonly configured as 20 minutes.
### A simple rule of thumb
- Use sliding expiration for frequently accessed, low-volatility items.
- Use absolute expiration when freshness has a hard upper bound.
- Use both when you want hot items to stay warm, but not forever.
### Example global configuration
```csharp
Configure<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.

BIN
docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 KiB

BIN
docs/en/Community-Articles/2026-06-25-caching-strategies-in-abp-framework/inline-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

693
docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/Post.md

@ -0,0 +1,693 @@
Domain events look simple on paper: something happened, react to it. In a real ABP microservices solution, the hard part is not raising the event. The hard part is deciding which event belongs inside the service, which one should cross service boundaries, and how to publish it without losing data or coupling your modules into a distributed monolith.
ABP gives you the primitives to do this well: local events, distributed events, aggregate-root support, and built-in outbox/inbox infrastructure. Used correctly, they let you keep your domain model clean while still coordinating work across microservices.
This article walks through a practical way to implement domain events in ABP microservices, including the boundary between domain and integration events, the transactional flow, outbox/inbox configuration, and the pitfalls that usually show up after the first production incident.
## Start with the right event boundary
The most important design choice is this:
- **Domain events** are internal to a bounded context.
- **Integration events** are for other microservices.
These are not interchangeable, even if the payload looks similar.
### Domain events
A domain event represents something meaningful that happened inside your domain model.
Examples:
- `OrderPlacedDomainEvent`
- `PaymentCapturedDomainEvent`
- `ProductStockDecreasedDomainEvent`
These events are typically handled **in-process**. In ABP, that usually means the **local event bus** or ABP's domain event dispatching from aggregates tracked by the ORM.
Use domain events when you want to:
- trigger side effects inside the same microservice
- keep aggregate logic focused
- avoid bloated application services
- coordinate rules across domain services without hard references
### Integration events
An integration event is a contract for communication between microservices.
Examples:
- `OrderPlacedEto`
- `StockCountChangedEto`
- `CustomerDeletedEto`
In ABP, these go through the **distributed event bus**. With a real provider like RabbitMQ, Kafka, or Azure Service Bus, they leave the current process and get consumed elsewhere.
Use integration events when you want to:
- notify another microservice
- update a local projection in another service
- drive eventual consistency across bounded contexts
### The rule that keeps systems healthy
A good practical rule is:
1. Raise a **domain event** from the aggregate or domain layer.
2. Handle it inside the same service.
3. From that handler, publish a **distributed event** if another microservice needs to know.
That separation prevents leaking internal domain details into your external contracts.
![Generated illustration](inline-1.png)
## What ABP gives you out of the box
ABP already supports the eventing model most microservices need.
### Local event bus
The local event bus is in-process. It is appropriate for:
- domain events
- module-to-module communication inside the same app
- internal side effects that should not leave the service boundary
### Distributed event bus
The distributed event bus is for cross-process communication.
A few practical notes matter here:
- Without a real provider configured, it behaves effectively in-process.
- With RabbitMQ, Kafka, or another provider, it becomes actual inter-service messaging.
- It works best with **ETOs** instead of domain entities.
### Aggregate roots and generated events
ABP aggregate roots can generate events directly. In practice, if your entity inherits from `AggregateRoot`, you can use methods like:
- `AddDomainEvent(...)`
- `AddDistributedEvent(...)`
ABP collects these events and dispatches them during persistence, typically around `SaveChanges` in EF Core-based applications.
That means your aggregate can say, "this happened," without knowing who will react.
## A practical implementation flow
Let's use a simple example: an Ordering microservice places an order, and an Inventory microservice needs to update its local stock view.
### Step 1: Raise a domain event in the aggregate
The aggregate should express business meaning, not infrastructure concerns.
```csharp
public class Order : AggregateRoot<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.
![Generated illustration](inline-2.png)
## Using AddDistributedEvent directly on aggregates
ABP also allows aggregates and domain services to add distributed events directly.
```csharp
public class Product : AggregateRoot<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.
![Generated illustration](inline-3.png)
## The outbox pattern: the part that saves you in production
Without outbox, the classic failure is simple:
1. Save business data to the database.
2. Try publishing to the broker.
3. App crashes between the two.
4. Your data is committed, but the event is gone.
Now one microservice thinks the operation happened, and the others never hear about it.
Outbox exists to remove that gap.
### How outbox works in ABP
With outbox enabled:
1. Your business data is saved.
2. The outgoing distributed event is also stored in the same database transaction.
3. A background worker reads pending outbox records.
4. It publishes them to the message broker.
5. Published records are marked processed and later cleaned up.
That gives you transactional safety between your local state change and the fact that an event must be published.
### EF Core outbox configuration
Your DbContext needs to participate in event outbox support.
```csharp
public class OrderingDbContext : AbpDbContext<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.

BIN
docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

BIN
docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

BIN
docs/en/Community-Articles/2026-06-25-implementing-domain-events-in-abp-microservices/inline-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

419
docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/POST.md

@ -0,0 +1,419 @@
# Working with Dapr Workflows in the ABP Framework
Most real business processes don't finish in a single request.
An order gets placed, inventory gets checked, a payment gets charged, and the customer gets notified. Each step can fail, time out, or need a retry. And the whole thing has to survive a process restart without losing its place or charging someone twice.
We usually solve this with a pile of queues, a state table, and a lot of defensive code to track where each process is. It works, but the business logic ends up scattered across handlers and database rows, and nobody can read the flow top to bottom anymore.
[I covered **Elsa** in two earlier articles](https://abp.io/community/search?tag=elsa) as one way to handle workflows in ABP. **Dapr Workflow** takes a different path: instead of an in-app engine, the workflow engine runs in the [**Dapr sidecar**](https://docs.dapr.io/concepts/dapr-services/sidecar/), and you write the process as ordinary C# code that Dapr makes durable. If the host crashes halfway through, the workflow picks up right where it left off.
In this article, we'll build a small Dapr Workflow inside a fresh ABP project and run it end to end. By the time you reach the bottom, you should be able to copy the code, run it, and watch a workflow march through its steps.
> **Note:** Versions matter here, because both ABP and Dapr move fast. This article is written in June 2026 against **ABP 10.4** (.NET 10), **Dapr 1.18**, and the **`Dapr.Workflow` 1.18.x** package. The `Dapr.Workflow` package was rewritten in Dapr 1.17, so older tutorials you find online may use a different API.
## What Dapr Workflow Actually Is?
You define a [**workflow**](https://docs.dapr.io/developing-applications/building-blocks/workflow/) that orchestrates a process, and [**activities**](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/#workflows-and-activities) that do the actual work (call a database, hit an API, send an email).
> **This is orchestration rather than choreography:** one place drives the process, instead of services reacting to each other's events. The definitions live in your app, but the engine that executes them runs in the Dapr sidecar next to it.
![Dapr workflow execution architecture diagram](./mermaid1.png)
The key idea is **durable execution**. Dapr records every step to a state store, so the workflow can be replayed from history at any time. A crash, a deployment, or a scale-out event doesn't lose progress, and a workflow can run for seconds or for months.
> ⚠️ One rule follows from this: **workflow code must be deterministic**. No `DateTime.Now`, no random values, no direct I/O. Anything non-deterministic goes into an activity. Even logging is affected, so inside a workflow you use `context.CreateReplaySafeLogger<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.
![dapr-init-run-result](./dapr-init-run-result.png)
> **Pro Tip:** If you ever swap the default Redis store for your own component, double-check that it sets `actorStateStore: "true"`. Without it, workflows silently fail to start, and it's the line people forget most often.
## Create the Project
In this article I'll create a new layered solution with **EF Core** as the database provider, using the ABP CLI.
> If you already have an ABP project, you don't need a new one. You can apply the following steps to your existing solution and skip this section.
Create a new solution named `DaprWorkflowDemo` (or whatever you want):
```bash
abp new DaprWorkflowDemo
```
Once the download finishes, your project boilerplate is ready. Open the solution in your IDE and run the `DaprWorkflowDemo.Web` project once to confirm the app starts and the UI works.
> Since, we have created the solution via ABP Studio CLI, it automatically runs the initial-tasks, which init database, seed initial data and run `abp install-libs` command, so, no need run the **DbMigrator* project.
> Default admin username is **admin** and the password is **1q2w3E***. You can use these credentials to login...
We'll do all the workflow work inside the `DaprWorkflowDemo.Web` project, since that's the running host where the workflow engine connects to the sidecar.
## Install the Dapr.Workflow Package
Open a terminal in the `DaprWorkflowDemo.Web` project folder and add the package:
```bash
dotnet add package Dapr.Workflow
```
-> **This single package gives you everything:** the base `Workflow<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:
![Order processing workflow flowchart](./mermaid2.png)
## Register the Workflow
Workflows and activities need to be registered so the engine knows about them. Open your `DaprWorkflowDemoWebModule` class and register them in `ConfigureServices`. Most of the existing code is abbreviated for simplicity:
```csharp
using DaprWorkflowDemo.Web.Workflows;
using Dapr.Workflow;
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
// ... existing ABP configuration ...
//Configure Dapr Workflows...
context.Services.AddDaprWorkflow(options =>
{
options.RegisterWorkflow<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:
![dapr-workflow-response](./dapr-workflow-response.png)
The same management API also lets you `terminate`, `pause`, `resume`, and `purge` instances, and `raiseEvent` to send external events into a waiting workflow. For example:
```bash
# Permanently delete a finished workflow's state
curl -X POST "http://localhost:3500/v1.0/workflows/dapr/order-001/purge"
```
The `DaprWorkflowClient` exposes the same operations in code (terminating, suspending and resuming, purging, and raising external events on an instance), which is the way to go for anything beyond a quick manual test.
## Triggering Workflows From Your ABP Code
Hitting the sidecar API by hand is great for a quick check, but in a real app you'll start workflows from your own code, and this is the recommended path. That's what the `DaprWorkflowClient` is for, and `AddDaprWorkflow` already registered it for you.
You can inject it anywhere, for example into a controller or an application service. Here's a minimal controller in the `DaprWorkflowDemo.Web` project that starts an order and reads its status:
```csharp
using System.Threading.Tasks;
using DaprWorkflowDemo.Web.Workflows;
using Dapr.Workflow;
using Microsoft.AspNetCore.Mvc;
namespace DaprWorkflowDemo.Web.Controllers;
[ApiController]
[Route("api/orders")]
public class OrderController : ControllerBase
{
private readonly DaprWorkflowClient _workflowClient;
public OrderController(DaprWorkflowClient workflowClient)
{
_workflowClient = workflowClient;
}
[HttpPost]
public async Task<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!

BIN
docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/cover-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-init-run-result.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/dapr-workflow-response.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/en/Community-Articles/2026-06-28-working-with-dapr-workflows/mermaid2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

216
docs/en/Community-Articles/2026-06-29-customizing-the-abp-framework/POST.md

@ -0,0 +1,216 @@
# Customizing the ABP Framework: A Developer's Guide to LeptonX Theme Overrides in Angular and the Transition to React UI
Enterprise ASP.NET Boilerplate (ABP) projects rarely stay with default theme behavior for long. At some point, teams need stricter brand alignment, user experience (UX) consistency across modules, or product-specific shell behavior that goes beyond palette and typography tweaks.
This article explains a practical way to customize the LeptonX theme in Angular projects through two primary layers :
1. **Style Overriding:** Utilizing design tokens, global CSS custom properties (variables), and component-level styling.
2. **Element Overriding:** Replacing or extending UI fragments and layout pieces using ABP's built-in services.
Finally, we connect this customization mindset to ABP’s new React direction, where application development teams own more of the user interface (UI) implementation directly from day one.
## Why Overriding Matters in Real ABP Solutions
In enterprise software engineering, frontend customization is not a cosmetic task. Instead, it directly supports core technical and architectural goals :
- **Brand System Compliance:** Enforcing strict color palettes, layouts, and typography across tenant-facing portals and internal back-office administration pages.
- **Accessibility (a11y) Improvements:** Optimizing focus states, color contrast ratios, screen reader compatibility, and keyboard navigation to meet WCAG standards.
- **Product Differentiation:** Structuring distinct top-level layouts, sidebar behavior, and navigation elements to separate multiple products within the same suite.
- **Operational Usability:** Reorganizing application spaces to match domain-specific workflows and simplify intensive data-entry tasks.
To avoid building fragile CSS overrides that break during framework updates, development teams must follow a strict, highly structured hierarchy of customization :
| Level | Customization Type | Technical Mechanism | Strategic Role |
| :---: | :--- | :--- | :--- |
| **1** | **Token-Level Variables** | CSS Custom Properties | 🛡️ *First Line of Defense* |
| **2** | **Component-Style Patch** | Class-Based Overrides | 🎨 *Moderate Visual Tweaks* |
| **3** | **Element Replacement** | ReplaceableComponents | 🏗️ *Deep Structural Overrides* |
Adhering to this hierarchy reduces "style debt" and ensures that theme upgrades remain manageable throughout the application lifecycle.
### Layer 1: Style Overriding in LeptonX (Angular)
Style overriding is the safest and most maintainable way to alter your application's presentation layer. The LeptonX engine relies heavily on CSS custom properties (variables) defined at the `:root` level.
### Customizing Brand Colors and Typography Tokens
To modify the default colors and branding assets, developers can define custom properties within the global `src/styles.scss` file :
```scss
:root {
/* Set the primary brand color used on active elements, buttons, and focuses */
--lpx-brand: #1e3a8a;
/* Set the physical paths for the application logos */
--lpx-logo: url('/assets/images/logo.png');
--lpx-logo-icon: url('/assets/images/logo-icon.png'); /* Displayed when sidebar is collapsed */
}
```
For applications utilizing multi-theme layouts (such as LeptonX Pro's Light, Dark, or Dim modes), variables can be scoped under individual theme classes to dynamically swap brand colors or assets :
```scss
/* Scoping theme-specific logos to prevent visibility issues on dark backgrounds */
:root.lpx-theme-dark, :root.lpx-theme-dim {
--lpx-logo: url('/assets/images/logo-light.png');
--lpx-logo-icon: url('/assets/images/logo-icon-light.png');
}
```
#### Solving the "Visual Branding Blink" on Initial Page Load
A common issue in production occurs when the default LeptonX logo is briefly displayed on screen before the client browser parses the custom stylesheet. This latency creates a noticeable "blink" or flicker.
To eliminate this rendering gap, bypass the CSS variable load phase by replacing the physical logo assets inside the web host project's public directory. Write your custom branding files directly to `/images/logo/leptonx/logo-light.png` inside the server's public folder. Because the fallback variable defaults directly to this location, the client browser displays the custom logo asset immediately without waiting to parse the custom CSS rules.
Additionally, note that styles registered solely in the application's global `styles.scss` may fail to apply to the **Account Layout** (such as the standard login page) because it compiles within an isolated module lifecycle. To ensure your styling overrides apply globally, register the assets and styles in the Virtual File System (VFS) of the.NET backend host, making them universally accessible across all client routing contexts.
### Layer 2: Element Overriding in LeptonX (Angular)
When CSS modifications cannot support your required user experience (such as adding search interfaces, custom profile controls, or custom action layouts), teams must override the underlying UI elements.
ABP provides the `ReplaceableComponentsService` to dynamically replace pre-built layout pieces with custom, project-owned Angular components without breaking core module logic.
### Troubleshooting the Mobile User Profile Freeze
In compiled editions of the LeptonX Lite layout library (specifically versions 3.1.x through 4.3.1), developers have identified a rendering bug affecting mobile layouts. When a user logs in via a mobile device and taps the profile dropdown menu, the page freezes. Instead of displaying the profile options, the sidebar area recursively renders a duplicate copy of the active route page. This layout loop completely breaks navigation until the page is refreshed.
The root cause is a layout bug inside the compiled LeptonX library template (`mn-user-profile.component.html`), where the template markup is wrapped inside an `<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.

344
docs/en/Community-Articles/2026-06-30-state-management-for-angular/POST.md

@ -0,0 +1,344 @@
# Angular 22 State Management: Signals, SignalStore, or NgRx?
Angular has been steadily moving toward a signal-first architecture since the introduction of Signals in Angular 16. With Angular 22, that transition reaches another milestone. Signals are now at the center of Angular's reactive programming model, while APIs such as Resource and Signal Forms have matured into production-ready solutions. Combined with the framework's continued investment in zoneless change detection, these improvements significantly influence how Angular applications should manage state.
This shift also changes the role of NgRx. While the classic NgRx Store remains a powerful solution for large, event-driven applications, many scenarios that previously required reducers, selectors, and effects can now be implemented with much simpler, feature-scoped signal stores. Rather than replacing NgRx, Angular 22 encourages developers to choose the right state management strategy based on the scope and complexity of the problem.
In this article, we'll explore how Angular 22 changes the state management landscape, compare the classic NgRx Store with NgRx SignalStore, and demonstrate best practices for building modern Angular applications. We'll also discuss how Angular's new reactive APIs fit into enterprise applications and what these changes mean for projects built with the ABP Framework.
## Why Angular 22 Changes State Management
Angular Signals introduced a fundamentally different approach by providing fine-grained reactivity built directly into the framework. Instead of propagating changes through Observable streams, Signals allow Angular to track exactly which pieces of state are consumed and update only the affected parts of the UI. This results in more predictable rendering, less boilerplate, and improved runtime performance.
Angular 22 builds on this foundation by making Signals the preferred reactive primitive throughout the framework. New APIs such as **Resource** for asynchronous data loading and **Signal Forms** for reactive forms integrate naturally with Signals, reducing the need for custom RxJS pipelines in many common scenarios.
For developers using NgRx, this doesn't mean abandoning existing applications or rewriting every store. Instead, it changes how state management should be approached. Component-local state can often be managed with plain Signals, feature-level state fits naturally into SignalStore, and the classic NgRx Store continues to excel for large-scale applications that benefit from centralized event streams, auditing, and global state synchronization.
Understanding these changing responsibilities is the key to designing maintainable Angular applications in the Angular 22 era. A practical way to think about state management is to start with the simplest solution and introduce additional abstractions only when the application's complexity requires them.
## Use Signals for Local Component State
Plain Angular Signals are ideal for state that belongs exclusively to a single component. Examples include dialog visibility, selected tabs, loading indicators, filter values, or temporary form data.
Signals provide a straightforward API with minimal overhead and integrate seamlessly with Angular's change detection. For state that never needs to be shared outside a component or its immediate children, introducing a dedicated store often adds unnecessary complexity.
A settings page often contains UI state that doesn't need to be shared with the rest of the application. Using a dedicated store for this would introduce unnecessary complexity.
```ts
@Component({...})
export class UserListComponent {
readonly search = signal('');
readonly showInactive = signal(false);
readonly filteredUsers = computed(() =>
this.users().filter(user =>
user.name.includes(this.search()) &&
(this.showInactive() || user.active)
)
);
}
```
This state is entirely local to the component and doesn't justify introducing a SignalStore.
## Use NgRx SignalStore for Feature State
As applications grow, state often needs to be shared across multiple components within the same feature. Examples include user profiles, shopping carts, administration screens, dashboards, or settings pages.
NgRx SignalStore is designed specifically for these scenarios. It combines Angular Signals with a lightweight, feature-oriented architecture where state, computed values, and business logic are defined together. Instead of scattering logic across reducers, selectors, effects, and services, developers can keep everything related to a feature inside a single store.
SignalStore also integrates naturally with Angular's signal-based APIs, making it an excellent choice for modern Angular applications built around Resources and Signal Forms.
A User Management module is shared by multiple pages. The selected user, filters, and loaded entities should remain synchronized across those pages.
```ts
export const UserStore = signalStore(
withState({
users: [] as User[],
selectedUserId: null as number | null,
loading: false,
}),
withComputed(({ users, selectedUserId }) => ({
selectedUser: computed(() =>
users().find(x => x.id === selectedUserId())
),
})),
withMethods((store) => ({
selectUser(id: number) {
patchState(store, { selectedUserId: id });
},
})),
);
```
Everything related to the feature lives in one place: state, derived values, and business operations.
## NgRx Store vs. NgRx SignalStore
Although both solutions belong to the NgRx ecosystem, they are designed to solve different architectural problems.
The classic NgRx Store follows the Redux pattern, where every state change is represented by an action that flows through reducers before producing a new immutable state. This explicit, event-driven architecture provides excellent traceability and scales well for applications with extensive global interactions.
SignalStore takes a different approach. Instead of centering the application around dispatched actions, it treats state as a reactive service built with Angular Signals. A SignalStore typically contains three core building blocks:
- **State**, which represents the application's reactive data.
- **Computed signals**, which derive values from existing state.
- **Methods**, which encapsulate business logic and state updates.
This functional model significantly reduces boilerplate while remaining predictable and testable. Since it builds directly on Angular Signals, it also integrates naturally with Angular's fine-grained change detection without requiring selectors or `async` pipes for many common scenarios.
The following comparison summarizes the strengths of each approach.
| Feature | Classic NgRx Store | NgRx SignalStore |
| ---------------- | ------------------------------- | --------------------------------- |
| Architecture | Redux-based global store | Feature-oriented reactive store |
| Reactivity | RxJS Observables | Angular Signals |
| Boilerplate | Higher | Lower |
| State Scope | Global application state | Feature or route state |
| Side Effects | Effects | Store methods or `rxMethod` |
| Change Detection | Observable subscriptions | Native signal reactivity |
| Best For | Large event-driven applications | Modern feature-based applications |
For most new Angular 22 applications, SignalStore is an excellent default choice for feature-level state management because it embraces the framework's signal-first architecture while keeping code concise and maintainable. The classic NgRx Store remains indispensable for applications that rely heavily on centralized event processing, global synchronization, or advanced debugging capabilities.
Instead of asking *"Which one should I use?"*, the better question is *"Which scope of state am I trying to manage?"* The answer usually determines the appropriate solution.
## Angular 22 Features That Improve State Management
Angular 22 introduces several framework APIs that naturally complement modern state management patterns. Rather than replacing NgRx, these APIs reduce the amount of custom infrastructure developers previously had to build around it.
### Resource API
One of the most significant additions is the **Resource API**, which provides a signal-based approach to asynchronous data loading.
Historically, fetching remote data in Angular involved coordinating `HttpClient`, RxJS operators, subscriptions, loading flags, and error handling. While these patterns remain valid, they often require considerable boilerplate even for straightforward scenarios.
Resources encapsulate these concerns into a single reactive abstraction. A Resource automatically tracks the signals it depends on, performs requests when those dependencies change, cancels obsolete requests, and exposes its lifecycle through reactive state such as the current value, loading status, and errors.
This makes Resources particularly well suited for read-oriented operations where data should stay synchronized with application state.
For example, changing a selected user ID can automatically trigger a new request without manually wiring `switchMap` or managing subscription lifecycles.
```tsx
const userResource = httpResource(() => ({
url: `/api/users/${selectedUserId()}`
}));
```
### Signal Forms
Another major improvement is the stabilization of **Signal Forms**.
Traditional Reactive Forms expose their state through `FormControl` and `FormGroup` instances, requiring developers to query validation status, dirty state, touched state, and values through an imperative API.
Signal Forms expose these properties as signals instead. Every field becomes reactive by default, making templates easier to read while eliminating much of the manual state synchronization commonly found in form-heavy applications.
```html
@if (profileForm.email.invalid() && profileForm.email.touched()) {
<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.

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/cover.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/cover.png

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-basic-theme-dashboard.png

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-identity-users.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-identity-users.png

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-dashboard.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-dashboard.png

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-leptonx-lite-dashboard.png

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-permission-management.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-permission-management.png

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-saas-tenants.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-saas-tenants.png

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-blazor-ui-library-dropdown.png

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-studio-first-run.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-studio-first-run.png

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

0
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/mud-vs-blazorise-leptonx.png

Before

Width:  |  Height:  |  Size: 379 KiB

After

Width:  |  Height:  |  Size: 379 KiB

10
docs/en/Community-Articles/2026-06-09-mudblazor-in-abp-framework/post.md → docs/en/Community-Articles/2026-07-01-mudblazor-in-abp-framework/post.md

@ -1,10 +1,10 @@
# ABP 10.4.2 Expands Blazor UI Options with MudBlazor Support
# ABP 10.5.0 Expands Blazor UI Options with MudBlazor Support
With ABP 10.4.2, new Blazor projects can now use **MudBlazor** (Material Design) as an alternative to the long-standing default, **Blazorise** (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.4.2 packages are live on nuget.org.
With ABP 10.5.0, new Blazor projects can now use **MudBlazor** (Material Design) as an alternative to the long-standing default, **Blazorise** (Bootstrap 5). Framework, themes (LeptonX / LeptonX Lite / Basic), modules, solution templates, ABP Studio, and ABP Suite all support both libraries side by side. The 10.5.0 packages are live on nuget.org.
## Why add another Blazor UI library?
Blazorise has been ABP's default Blazor UI library for years and **remains the default and is fully supported** — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.4 does not change anything for them.
Blazorise has been ABP's default Blazor UI library for years and **remains the default and is fully supported** — existing Blazorise projects can keep moving at their own pace, and upgrading to 10.5.0 does not change anything for them.
We added MudBlazor because one Blazor UI choice cannot fit every team:
@ -190,7 +190,7 @@ Documentation:
## FAQ
**I'm already using Blazorise — will upgrading to 10.4 / 10.4.2 break my project?**
**I'm already using Blazorise — will upgrading to 10.5.0 break my project?**
No. Blazorise stays the default, and package paths, type names, and namespaces are fully compatible. Follow the standard ABP upgrade flow.
**Can I use Blazorise and MudBlazor in the same project?**
@ -201,7 +201,7 @@ Your custom Razor pages are tied to the UI library they were built with, so swit
## Wrapping up
MudBlazor is now a first-class Blazor UI library in ABP. With 10.4.2 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single `abp new` command.
MudBlazor is now a first-class Blazor UI library in ABP. With 10.5.0 released, every related package, theme, template, Studio integration, and Suite generator is in place — you can try it out with a single `abp new` command.
If you hit a bug, have a suggestion, or want a particular module's MudBlazor UX prioritized, let us know via [GitHub Issues](https://github.com/abpframework/abp/issues) or [abp.io support](https://abp.io/support).

1330
docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/Post.md

File diff suppressed because it is too large

BIN
docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

BIN
docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
docs/en/Community-Articles/2026-07-03-building-scalable-enterprise-applications-with-abp/inline-4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 KiB

4
docs/en/docs-nav.json

@ -2113,6 +2113,10 @@
"text": "Template Guide",
"path": "solution-templates/guide.md"
},
{
"text": "Modern vs Classic Templates",
"path": "solution-templates/modern-vs-classic.md"
},
{
"text": "Single-Layer Solution",
"isLazyExpandable": true,

7
docs/en/framework/fundamentals/dynamic-claims.md

@ -70,7 +70,7 @@ There are three pre-built implementations of `IAbpDynamicClaimsPrincipalContribu
* `IdentityDynamicClaimsPrincipalContributor`: Provided by the [Identity module](../../modules/identity.md) and generates and overrides the actual dynamic claims, and writes to the distributed cache. Typically works in the authentication server in a distributed system.
* `RemoteDynamicClaimsPrincipalContributor`: For distributed scenarios, this implementation works in the UI application. It tries to get dynamic claim values in the distributed cache. If not found in the distributed cache, it makes an HTTP call to the authentication server and requests filling it by the authentication server. `AbpClaimsPrincipalFactoryOptions.RemoteRefreshUrl` should be properly configure to make it running.
* `WebRemoteDynamicClaimsPrincipalContributor`: Similar to the `RemoteDynamicClaimsPrincipalContributor` but works in the microservice applications.
* `WebRemoteDynamicClaimsPrincipalContributor`: Similar to the `RemoteDynamicClaimsPrincipalContributor` but works in the microservice applications. Both remote contributors run on the UI/API (resource-server) side that authenticates against a remote authentication server, not on the authentication server itself.
### IAbpDynamicClaimsPrincipalContributor
@ -82,7 +82,8 @@ If you want to add your own dynamic claims contributor, you can create a class t
* `IsDynamicClaimsEnabled`: Enable or disable the dynamic claims feature.
* `RemoteRefreshUrl`: The `url ` of the Auth Server to refresh the cache. It will be used by the `RemoteDynamicClaimsPrincipalContributor`. The default value is `/api/account/dynamic-claims/refresh ` and you should provide the full URL in the authentication server, like `http://my-account-server/api/account/dynamic-claims/refresh `.
* `DynamicClaims`: A list of dynamic claim types. Only the claims in that list will be overridden by the dynamic claims system.
* `IsRemoteRefreshEnabled`: Controls whether the remote contributors (`RemoteDynamicClaimsPrincipalContributor` and `WebRemoteDynamicClaimsPrincipalContributor`) are registered. `true` by default, but the Identity module sets it to `false`. So an application that includes the Identity module builds the dynamic claims locally and does not register the remote contributors, even if `WebRemoteDynamicClaimsPrincipalContributorOptions.IsEnabled` is set to `true`.
* `DynamicClaims`: A list of dynamic claim types. Only the claims in that list will be overridden by the dynamic claims system. Adding a claim type here makes the dynamic claims system authoritative for that type, so the source that fills the cache (the Identity-side claims principal factory in the local case) must actually produce it; otherwise the claim is cached with a null value and removed from the principal on each refresh.
* `ClaimsMap`: A dictionary to map the claim types. This is used when the claim types are different between the Auth Server and the client. Already set up for common claim types by default.
## WebRemoteDynamicClaimsPrincipalContributorOptions
@ -91,6 +92,8 @@ If you want to add your own dynamic claims contributor, you can create a class t
* `IsEnabled`: Enable or disable the `WebRemoteDynamicClaimsPrincipalContributor`. `false` by default.
* `AuthenticationScheme`: The authentication scheme to authenticate the HTTP call to the authentication server.
> Setting `IsEnabled = true` registers the contributor only when `AbpClaimsPrincipalFactoryOptions.IsRemoteRefreshEnabled` is also `true`. Because the Identity module disables `IsRemoteRefreshEnabled`, this contributor is not registered in applications that include the Identity module; it is intended for the resource-server/microservice side of a tiered solution.
## See Also

69
docs/en/framework/infrastructure/background-jobs/index.md

@ -225,7 +225,7 @@ ABP includes a simple `IBackgroundJobManager` implementation that;
- **Retries** job execution until the job **successfully runs** or **timeouts**. Default timeout is 2 days for a job. Logs all exceptions.
- **Deletes** a job from the store (database) when it's successfully executed. If it's timed out, it sets it as **abandoned** and leaves it in the database.
- **Increasingly waits between retries** for a job. It waits 1 minute for the first retry, 2 minutes for the second retry, 4 minutes for the third retry and so on.
- **Polls** the store for jobs in fixed intervals. It queries jobs, ordering by priority (asc) and then by try count (asc).
- **Polls** the store for jobs in fixed intervals. It queries jobs, ordering by priority (desc) and then by try count (asc).
> `Volo.Abp.BackgroundJobs` nuget package contains the default background job manager and it is installed to the startup templates by default.
@ -248,11 +248,76 @@ public class MyModule : AbpModule
````
* `JobPollPeriod` is used to determine the interval between two job polling operations. Default is 5000 ms (5 seconds).
* `MaxJobFetchCount` is used to determine the maximum job count to fetch in a single polling operation. Default is 1000.
* `MaxJobFetchCount` is used to determine the maximum job count to fetch in a single polling operation. It is also used as the batch size for the retention cleanup deletions. Default is 1000.
* `DefaultFirstWaitDuration` is used to determine the duration to wait before the first retry. Default is 60 seconds.
* `DefaultTimeout` is used to determine the timeout duration for a job. Default is 172800 seconds (2 days).
* `DefaultWaitFactor` is used to determine the factor to increase the wait duration between retries. Default is 2.0.
* `DistributedLockName` is used to determine the distributed lock name to use. Default is `AbpBackgroundJobWorker`.
* `StoreSuccessfulJobs` is used to determine whether to keep successfully completed jobs in the store instead of deleting them. Default is `false`. See the *Storing Successful Jobs* section.
* `SuccessfulJobRetentionTime` is used to determine how long a kept job is retained before the cleanup deletes it. Default is 7 days. Set to `null` to keep completed jobs forever. Only relevant when `StoreSuccessfulJobs` is enabled.
* `CleanSuccessfulJobsPeriod` is used to determine the interval between cleanup runs that delete expired completed jobs. Default is 3600000 ms (1 hour).
* `CleanupDistributedLockName` is used to determine the distributed lock name for the cleanup worker. Default is `AbpBackgroundJobCleanup`.
* `MaxParallelJobExecutionCount` is used to determine the maximum number of jobs a worker executes in parallel within one poll cycle. Default is 1. See the *Parallel Job Execution* section.
* `PerJobDistributedLockPrefix` is used to determine the prefix of the per-job distributed lock name used when `MaxParallelJobExecutionCount` is greater than 1. Default is `AbpBackgroundJob:`.
### Storing Successful Jobs
By default, the background job manager deletes a job from the store as soon as it runs successfully. If you want to keep completed jobs (for auditing or history), enable `StoreSuccessfulJobs`:
````csharp
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.StoreSuccessfulJobs = true;
options.SuccessfulJobRetentionTime = TimeSpan.FromDays(30); //null to keep forever
});
````
When enabled, a successful job is not deleted; instead its `CompletionTime` is set and it stays in the store. Completed jobs are excluded from the waiting jobs query, so they are not executed again. A cleanup worker periodically deletes completed jobs older than `SuccessfulJobRetentionTime`.
> **Note:** The `IBackgroundJobStore` interface has new overloads (a `GetWaitingJobsAsync` overload that takes a job name filter and a `DeleteAsync` overload for cleanup). If you have a custom `IBackgroundJobStore` implementation, you must implement them for your code to compile. The built-in stores already implement them.
### Dedicated Workers per Job Type
By default, a single worker processes all job types. If you want to process certain job types separately (for example, slow or high-volume jobs), you can register dedicated workers, each handling only the specified job argument types with its own distributed lock:
````csharp
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.AddDedicatedWorker<EmailJobArgs, SmsJobArgs>("NotificationWorkerLock");
options.AddDedicatedWorker<ReportJobArgs>("ReportWorkerLock");
});
````
Each dedicated worker processes only its configured job types. An additional default worker is automatically started to process all the remaining job types. In sequential mode, each worker (including the default one) runs independently under its own distributed lock (see *Parallel Job Execution* for how this changes when running jobs in parallel).
If you don't want to specify a lock name, use the overloads without the `lockName` parameter; a stable, length-bounded lock name is then derived from the job argument types:
````csharp
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.AddDedicatedWorker<EmailJobArgs, SmsJobArgs>();
options.AddDedicatedWorker<ReportJobArgs>();
});
````
> **Note:** Each job type can be handled by only one dedicated worker, and each worker must have a unique lock name; `AddDedicatedWorker` throws if this is violated. Dedicated workers require an `IBackgroundJobStore` that can filter jobs by name (the built-in stores can).
### Parallel Job Execution
By default, a worker executes waiting jobs one by one under a single worker-level distributed lock, so only one job runs at a time across all application instances. If you want to execute multiple jobs concurrently, set `MaxParallelJobExecutionCount` to a value greater than 1:
````csharp
Configure<AbpBackgroundJobWorkerOptions>(options =>
{
options.MaxParallelJobExecutionCount = 4;
});
````
When it is greater than 1, the worker-level lock is not used. Instead, each job is claimed with its own distributed lock, so multiple application instances can execute different jobs at the same time. With a properly configured distributed lock provider, a job is not executed by more than one instance at a time.
`MaxParallelJobExecutionCount` is a per-worker, per-poll-cycle limit — it is not a cluster-wide limit. A worker first fetches up to `MaxJobFetchCount` waiting jobs, then executes up to `MaxParallelJobExecutionCount` of them in parallel, so a single worker runs up to `min(MaxJobFetchCount, MaxParallelJobExecutionCount)` jobs per cycle; keep `MaxJobFetchCount` at least as large as `MaxParallelJobExecutionCount` to avoid capping the parallelism. When you also configure dedicated workers, each worker runs its own timer and claims up to `MaxParallelJobExecutionCount` jobs, so the effective concurrency is up to (number of workers) × `MaxParallelJobExecutionCount` per application instance, and up to (number of application instances) × (number of workers) × `MaxParallelJobExecutionCount` across the whole cluster.
> **Important:** Configure `MaxParallelJobExecutionCount` and `PerJobDistributedLockPrefix` consistently across all application instances. Mixing sequential (worker lock) and parallel (per-job lock) instances removes the common mutual exclusion, and a different prefix produces a different per-job lock name for the same job — either case may let the same job run on more than one instance. As with the sequential mode, configure a real [distributed lock](../distributed-locking.md) provider for clustered deployments.
### Data Store

2
docs/en/get-started/index.md

@ -20,7 +20,7 @@ Please select one of the following documents best fits for your application:
- [WPF Application](wpf.md)
- [Console Application](console.md)
If you seek a React-based web UI, use the **modern template system** with ABP Studio or `abp new --modern`. See [UI options](../framework/ui/index.md) for the full list of officially supported UI frameworks.
If you are choosing between the **Modern** and **Classic** ABP Studio template families, see [Modern vs Classic Templates](../solution-templates/modern-vs-classic.md). If you seek a React-based web UI, use the **modern template system** with ABP Studio or `abp new --modern`. See [UI options](../framework/ui/index.md) for the full list of officially supported UI frameworks.
## Which Startup Template is Suitable for Me?

2
docs/en/modules/background-jobs.md

@ -33,6 +33,8 @@ Following custom repositories are defined for this module:
- `IBackgroundJobRepository`
> `IBackgroundJobRepository` supports filtering the waiting jobs for dedicated workers and cleaning up retained completed jobs. See the *Dedicated Workers per Job Type* and *Storing Successful Jobs* sections of the [background jobs](../framework/infrastructure/background-jobs) document.
### Database providers
#### Common

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

@ -7,6 +7,79 @@
# Package Version Changes
## 10.6.0-rc.1
| Package | Old Version | New Version | PR |
|---------|-------------|-------------|-----|
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Authentication.OpenIdConnect | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Authorization | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components.Authorization | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components.Web | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components.WebAssembly | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components.WebAssembly.Authentication | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components.WebAssembly.DevServer | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Components.WebAssembly.Server | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.DataProtection.StackExchangeRedis | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Mvc.NewtonsoftJson | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.Mvc.Testing | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.TestHost | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.AspNetCore.WebUtilities | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Bcl.AsyncInterfaces | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Data.SqlClient | 6.1.1 | 7.0.2 | #25706 |
| Microsoft.Data.Sqlite | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.Design | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.InMemory | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.Proxies | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.Relational | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.SqlServer | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.Sqlite | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.EntityFrameworkCore.Tools | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Caching.Hybrid | 9.9.0 | 10.7.0 | #25706 |
| Microsoft.Extensions.Caching.Memory | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Caching.StackExchangeRedis | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Configuration.Binder | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Configuration.CommandLine | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Configuration.EnvironmentVariables | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Configuration.UserSecrets | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.DependencyInjection | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.FileProviders.Composite | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.FileProviders.Embedded | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.FileProviders.Physical | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.FileSystemGlobbing | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Hosting | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Hosting.Abstractions | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Http | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Identity.Core | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Localization | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Logging | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Logging.Abstractions | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Logging.Console | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Options | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.Extensions.Options.ConfigurationExtensions | 10.0.7 | 10.0.9 | #25706 |
| Microsoft.IdentityModel.JsonWebTokens | 8.16.0 | 8.19.1 | #25706 |
| Microsoft.IdentityModel.Protocols.OpenIdConnect | 8.16.0 | 8.19.1 | #25706 |
| Microsoft.IdentityModel.Tokens | 8.16.0 | 8.19.1 | #25706 |
| MongoDB.Driver | 3.9.0 | 3.10.0 | #25773 |
| System.Collections.Immutable | 10.0.7 | 10.0.9 | #25706 |
| System.IdentityModel.Tokens.Jwt | 8.16.0 | 8.19.1 | #25706 |
| System.Management | 10.0.7 | 10.0.9 | #25706 |
| System.Security.Cryptography.Xml | 10.0.7 | 10.0.9 | #25706 |
| System.Security.Permissions | 10.0.7 | 10.0.9 | #25706 |
| System.Text.Encoding.CodePages | 10.0.7 | 10.0.9 | #25706 |
| System.Text.Encodings.Web | 10.0.7 | 10.0.9 | #25706 |
| System.Text.Json | 10.0.7 | 10.0.9 | #25706 |
## 10.5.1
| Package | Old Version | New Version | PR |
|---------|-------------|-------------|-----|
| Swashbuckle.AspNetCore | 10.0.1 | 10.2.3 | #25759 |
## 10.5.0-rc.4
| Package | Old Version | New Version | PR |

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

@ -171,7 +171,7 @@ Keep `DisablePayloadSigning` disabled for providers that support the default AWS
**What changed**
- Blazorise packages were upgraded to **2.1.3**.
- Blazorise packages were upgraded to **2.2.1**.
- MongoDB.Driver was upgraded to **3.9.0**.
- `@abp/codemirror` was updated to CodeMirror **6.0.2**.

237
docs/en/release-info/migration-guides/abp-10-6.md

@ -0,0 +1,237 @@
```json
//[doc-seo]
{
"Description": "Upgrade your ABP solutions from v10.5 to v10.6 with this migration guide covering important behavior and integration changes."
}
```
# ABP Version 10.6 Migration Guide
This document is a guide for upgrading ABP v10.5 solutions to ABP v10.6. There are some important changes that may require action in specific application scenarios.
> **Package Version Changes:** Before upgrading, review the [Package Version Changes](../../package-version-changes.md) document to see version changes on dependent NuGet and NPM packages and align your project with ABP's internal package versions.
## Open-Source (Framework)
This version contains the following changes on the open-source side:
### Background Jobs Infrastructure Extensions
**Who is affected**
- Applications using the default background job worker and wanting dedicated workers, parallel execution, or successful job retention.
- Applications with a custom `IBackgroundJobStore` implementation.
- Applications using the Background Jobs module with EF Core and enabling successful job retention.
**What changed**
- ABP adds opt-in support for:
- storing successfully completed jobs (`StoreSuccessfulJobs`)
- dedicated workers per job argument type (`AddDedicatedWorker(...)`)
- parallel job execution (`MaxParallelJobExecutionCount`)
- `IBackgroundJobStore`, `IBackgroundJobWorker`, and related infrastructure gained new members.
- EF Core stores add a `CompletionTime` column to background job records for retention scenarios.
- All new runtime features are disabled by default.
**What to do**
No action is required if you do not enable the new options and do not maintain a custom background job store.
If you maintain a custom `IBackgroundJobStore`, implement the new interface members so your solution compiles.
If you enable `StoreSuccessfulJobs`, add/review the EF Core migration for the `CompletionTime` column and configure retention options explicitly:
```csharp
Configure<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.

1
docs/en/release-info/migration-guides/index.md

@ -9,6 +9,7 @@
The following documents explain how to migrate your existing ABP applications. We write migration documents only if you need to take an action while upgrading your solution. Otherwise, you can easily upgrade your solution using the [abp update command](../upgrading.md).
- [10.5 to 10.6](abp-10-6.md)
- [10.4 to 10.5](abp-10-5.md)
- [10.3 to 10.4](abp-10-4.md)
- [10.x to 10.3](abp-10-3.md)

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

@ -1,7 +1,7 @@
```json
//[doc-seo]
{
"Description": "Explore the latest ABP Framework release notes, highlighting major features and enhancements for each version, including migration guidance."
"Description": "Explore the latest ABP Framework release notes, highlighting major features and enhancements for each version, including migration guidance."
}
```
@ -14,19 +14,19 @@ Also see the following notes about ABP releases:
- [ABP Studio release notes](../studio/release-notes.md)
- [Change logs for ABP pro packages](https://abp.io/pro-releases)
## 10.6 ()
## 10.6 (2026-07-07)
See the detailed **[blog post / announcement]()** for the v10.6 release.
See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-6-release-candidate-reoq6kzw)** for the v10.6 release.
- Angular has been upgraded to version 22.0.x.
- Background Jobs: Dedicated Workers, Parallel Execution, and Successful Job Retention
- API Definition and Proxy Improvements for Content Types and Multipart Uploads
- Angular UI: Upgrade to Angular 22
- Antiforgery and OpenIddict Security Improvements
- OpenIddict: Generate Access Token from the UI
For a complete list of changes, including breaking changes, migration steps, and package updates, see the **[Angular Release Notes for v10.6](./../framework/ui/angular/release-notes/angular-22-typescript-6.md)**.
## 10.5 (2026-06-30)
## 10.5 (2026-06-02)
See the detailed **[blog post / announcement](https://abp.io/community/articles/announcing-abp-10-5-release-candidate-k6oxdfle)** for the v10.5 release.
See the detailed **[blog post / announcement](https://abp.io/community/announcements/announcing-abp-10-5-stable-release-2u589bsc)** for the v10.5 release.
- S3-Compatible Blob Storage Support
- OpenIddict: Default Scope Fallback Options
@ -56,7 +56,7 @@ See the detailed **[blog post / announcement](https://abp.io/community/announcem
- Event Bus: String-Based Event Publishing with Dynamic Payload
- Background Jobs/Workers: String-Based Publishing with Dynamic Payload
- API Definition Endpoint: Descriptions and Documentation Support
- Entity Cache: New Batch APIs (`FindMany`* / `GetMany*`)
- Entity Cache: New Batch APIs (`FindMany`_ / `GetMany_`)
- Angular: User/Tenant Sharing and Tenant Switch Experience
- Angular: Upgrade to 21.2 + TypeScript 5.9
- Introducing the `Volo.Abp.LuckyPenny.AutoMapper` Provider
@ -467,7 +467,7 @@ See the detailed **blog post / announcement** for the v2.8 release: [https://abp
## 2.7 (2020-05-07)
See the detailed **blog post / announcement** for the v2.7 release: [https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released](https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released)
See the detailed **blog post / announcement** for the v2.7 release: [https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released](https://abp.io/blog/ABP-Framework-v2_7_0-Has-Been-Released)
- New module: **Text template management** (with angular and mvc UI - document is [coming](../modules/text-template-management.md)).
- **Dynamically add properties** to current entities of the depended modules (see [module entity extensions](../framework/architecture/modularity/extending/module-entity-extensions.md))
@ -478,9 +478,8 @@ See the detailed **blog post / announcement** for the v2.7 release: [https://ab
- **Optimize database migrations** & seed code for multi-tenant multi-database systems.
- ABP Suite: Make **menu item active** on navigation menu when selected.
- ABP Suite: Improve **enum usage** while creating new entities.
- Bug fixes in the [Lepton Theme](https://abp.io/themes), [ABP Suite](https://abp.io/tools/suite) and other modules.
- Bug fixes in the [Lepton Theme](https://abp.io/themes), [ABP Suite](https://abp.io/tools/suite) and other modules.
## See Also
- [Road map](road-map.md)

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

@ -1,7 +1,7 @@
```json
//[doc-seo]
{
"Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.5, planned for June 2026."
"Description": "Explore the ABP Platform Road Map for insights on upcoming features, release schedules, and improvements in version 10.7, planned for August 2026."
}
```
@ -11,28 +11,29 @@ This document provides a road map, release schedule, and planned features for th
## Next Versions
### v10.6
### v10.7
The next planned version will be 10.6, which is scheduled to be released as a stable version in July 2026. We will be mostly working on the following topics:
The next planned version will be 10.7, which is scheduled to be released as a stable version in August 2026. Based on the currently open issues and pull requests across the ABP ecosystem, we will be mostly working on the following topics:
* Framework
* Token verification improvements with refresh token support and distributed locking
* Angular UI fixes and proxy generation improvements
* Better handling for extra properties, object mapping and auditing edge cases
* Hybrid UI / page embedding infrastructure
* Upgrading 3rd-party dependencies and evaluating replacements where needed
* General bug fixing and improvements in core framework packages
* Cookie authentication: refresh token support and distributed locking ([#25011](https://github.com/abpframework/abp/issues/25011))
* jQuery 4.x upgrade, or removing jQuery as a dependency ([#25123](https://github.com/abpframework/abp/issues/25123))
* AI agent skills distributed as versioned plugins ([#25712](https://github.com/abpframework/abp/issues/25712))
* Hybrid UI / page embedding infrastructure ([#23102](https://github.com/abpframework/abp/issues/23102), [#23161](https://github.com/abpframework/abp/issues/23161))
* Microsoft Agent Framework migration and native agent skills support ([#24310](https://github.com/abpframework/abp/issues/24310), [#25194](https://github.com/abpframework/abp/issues/25194))
* Better ExtraProperties mapping for EF Core ([#23546](https://github.com/abpframework/abp/issues/23546))
* Upgrading 3rd-party dependencies and general bug fixing in core framework packages
* ABP Suite
* Improvements on generated codes for nullability
* Improvements on master-detail page design (making it more compact)
* Replace the templating system with Scriban while preserving backward compatibility
* Support for additional property types like `DateTimeOffset`, `TimeSpan` and numeric enums
* Display names and ordering for properties and navigation properties
* Filter on inherited properties and namespace-based UI foldering
* Low-Code system integration
* Better support for additional property types like `DateTimeOffset`, `TimeSpan` and numeric enum scenarios
* Improvements for generated file upload, navigation property and display-name experiences
* Improvements on generated code nullability, master-detail pages and file upload experiences
* ABP Studio
* AI Coding Agent and MCP integration
* Modern solution wizard improvements and low-code support
* Low-Code platform integration
* Theme Builder: live preview, project integration and import/export
* Linux support and packaging improvements
* Better React / React Native / Thin UI template experience
@ -41,11 +42,12 @@ The next planned version will be 10.6, which is scheduled to be released as a st
* Terminal, browser and built-in developer productivity enhancements
* Application Modules
* AI Management: chat history, multi-tenancy and tenant-scoped workspace capabilities
* AI Management: chat history
* AI Management: multi-tenancy and tenant-scoped workspace capabilities
* RAG: Cloudflare `/crawl` endpoint as a data source
* New module: Chat with your data
* Low-Code designer and low-code platform integrations
* CMS Kit and public website improvements
* Payment module e-mail notification improvements
* CMS Kit and public website improvements
* UI/UX improvements on existing application modules
* Updating existing tutorials & documents (with other UI & DB options)
@ -61,42 +63,46 @@ The *Next Versions* section above shows the main focus of the planned versions.
The ABP framework is [open source](https://github.com/abpframework/abp) and free for everyone. You can see its [public backlog](https://github.com/abpframework/abp/milestone/2). Here are some of the selected backlog items and longer-term topics:
* [#23102](https://github.com/abpframework/abp/issues/23102) / ABP Hybrid UI System: Re-using module UIs in different technologies
* [#23161](https://github.com/abpframework/abp/issues/23161) / Page Embedding Feature (aka Hybrid UI)
* [#25123](https://github.com/abpframework/abp/issues/25123) / Upgrade jQuery to 4.x, or consider removing it as dependency
* [#24742](https://github.com/abpframework/abp/issues/24742) / Add Support for LiteDB as a Database Provider
* [#24442](https://github.com/abpframework/abp/issues/24442) / Add Couchbase EF Core Provider Integration
* [#17093](https://github.com/abpframework/abp/issues/17093) / MVC UI: decouple jQuery
* [#24310](https://github.com/abpframework/abp/issues/24310) / Migrate Volo.Abp.AI Semantic Kernel to Microsoft Agent Framework
* [#25194](https://github.com/abpframework/abp/issues/25194) / Integrate Microsoft.Agents.AI for native Agent Skills support
* [#23575](https://github.com/abpframework/abp/issues/23575) / Support list/enumerable of complex types for ABP dynamic/static C# proxies on GET requests
* [#23546](https://github.com/abpframework/abp/issues/23546) / Better ExtraProperties mapping for EF Core
* [#23935](https://github.com/abpframework/abp/issues/23935) / Hybrid Cache Support for EntityCache
* [#22931](https://github.com/abpframework/abp/issues/22931) / Angular - Support dynamic URLs for breadcrumbs
* [#25032](https://github.com/abpframework/abp/issues/25032) / Guidance and infrastructure considerations for gRPC-based scenarios
* [#2882](https://github.com/abpframework/abp/issues/2882) / Providing a gRPC integration infrastructure
* [#57](https://github.com/abpframework/abp/issues/57) / Built-in CQRS infrastructure
* [#24742](https://github.com/abpframework/abp/issues/24742) / Add Support for LiteDB as a Database Provider
* [#24442](https://github.com/abpframework/abp/issues/24442) / Add Couchbase EF Core Provider Integration
* [#2882](https://github.com/abpframework/abp/issues/2882) / ABP gRPC Integration
* [#57](https://github.com/abpframework/abp/issues/57) / CQRS infrastructure
* [#58](https://github.com/abpframework/abp/issues/58) / Content localization system (multilingual entities)
* [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook system
* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB integration for multitenancy
* [#2296](https://github.com/abpframework/abp/issues/2296) / Feature toggling infrastructure
* [#4223](https://github.com/abpframework/abp/issues/4223) / WebHook System
* [#162](https://github.com/abpframework/abp/issues/162) / Azure ElasticDB Integration for multitenancy
* [#2296](https://github.com/abpframework/abp/issues/2296) / Implementing Feature Toggle
* [#15932](https://github.com/abpframework/abp/issues/15932) / Introduce ABP Diagnostics Module
* [#16744](https://github.com/abpframework/abp/issues/16744) / State Management API
* [#119](https://github.com/abpframework/abp/issues/119) / REST API versioning improvements
* [#2087](https://github.com/abpframework/abp/issues/2087) / RavenDB database support
* [#119](https://github.com/abpframework/abp/issues/119) / REST API Versioning Improvements
* [#2087](https://github.com/abpframework/abp/issues/2087) / Add RavenDB Database support
### Application Modules / UI Themes
ABP Platform provides many (free and commercial) [pre-built application modules](../modules/index.md) and modern [UI themes](../ui-themes/index.md). In every release, many enhancements and bugfixes are delivered for these modules and themes. Important backlog topics currently include:
* AI Management module: chat history, multi-tenancy, tenant workspaces and operational hardening
* CMS Kit module: media gallery and richer public website capabilities
* New module: Chat with your data
* RAG with external data sources such as website crawling
* Payment module: richer notifications and invoice-oriented scenarios
* CMS Kit: Meta information for SEO
* Audit logging UI: filter redesign and UX improvements
* Identity Pro: richer filtering and organization-unit UX improvements
* LeptonX and existing UIs: new layouts, styles and usability refinements
* New module ideas: Chat with your data, AI Search, user notification and dynamic dashboard
### ABP Studio
[ABP Studio](../studio/index.md) is a cross-platform desktop application for ABP and .NET developers to simplify and automate daily tasks of developers. It has a community (free) edition as well as commercial capabilities. Here are some of the important planned features and active backlog topics for the next ABP Studio versions:
* Low-Code: ABP Studio Integration
* Theme builder for LeptonX, including live preview, management UI and project integration
* Analyze user solutions to explore entities, domain services, application services, pages and other fundamental objects
* AI agent/browser capabilities and developer-assistant experiences
@ -107,16 +113,14 @@ ABP Platform provides many (free and commercial) [pre-built application modules]
* More options while creating new solutions, modules and services
* Better environment-variable, deployment and Kubernetes experiences
* Compare changes on startup templates when a new ABP version is published
* Rapid application development and low-code oriented features
* ABP support integration and better diagnostics/error experiences
### ABP Suite
[ABP Suite](../suite/index.md) is a GUI application that is mainly used to generate CRUD-style pages in your application. You define your entity and it can generate all the code from the database to the UI. Here are some of the important planned features for the next ABP Suite versions:
* Replace the current templating system with Scriban while preserving backward compatibility
* Better nullability support in generated code
* MudBlazor support
* Replacing the current templating system with a text engine while preserving backward compatibility
* Support for additional property types like `DateTimeOffset` and `TimeSpan`
* Handle image properties for entities (in addition to file properties, which are already supported)
* Allow to define display names and better ordering for properties and navigation properties

2
docs/en/solution-templates/index.md

@ -9,7 +9,7 @@
ABP provides pre-architected and production-ready templates to jump start a new solution.
> **You can see the [Solution Template Selection Guide](guide.md) if you are not sure which solution template is suitable for you.**
> **You can see the [Solution Template Selection Guide](guide.md) if you are not sure which solution template is suitable for you. See [Modern vs Classic Templates](modern-vs-classic.md) if you are choosing between ABP Studio's Modern and Classic template families.**
The reference pages below cover both classic and modern ABP Studio template families. The Single-Layer and Layered pages document the classic templates. The Modular Monolith and Microservice pages call out the current modern structure where it differs.

55
docs/en/solution-templates/modern-vs-classic.md

@ -0,0 +1,55 @@
```json
//[doc-seo]
{
"Description": "Compare Modern and Classic ABP Studio templates, including architecture mapping, UI choices, mobile options, and when to choose each template family."
}
```
# Modern vs Classic Templates
ABP Studio provides two solution template families: **Modern** and **Classic**. Both families create production-ready ABP solutions and share the same ABP backend concepts, such as Entity Framework Core and MongoDB database options, OpenIddict authentication, multi-tenancy, optional modules, test projects, language selection and deployment-related configuration where they are supported.
The main difference is how you choose and shape the solution. **Classic templates** are the traditional template-first flow. You first choose a concrete template, such as Single-Layer, Layered or Microservice, and then select UI, database, mobile and module options. **Modern templates** are the newer architecture-first flow. You first choose the backend architecture, then ABP Studio maps your selection to the proper modern template.
## Template Families
| Template family | Template names | Main idea |
| --- | --- | --- |
| Classic | `app-nolayers`, `app`, `microservice` | The traditional ABP Studio solution templates with the broadest UI framework choices. |
| Modern | `app-nolayers-modern`, `app-modern`, `microservice-modern` | The newer ABP Studio templates focused on React-based web applications and an architecture-first creation flow. |
## Modern Architecture Mapping
When you use the Modern solution creation flow in ABP Studio, your selected architecture maps to a modern template:
| Modern architecture | Generated template | Notes |
| --- | --- | --- |
| Simple Monolith | `app-nolayers-modern` | A simpler application structure with the main backend code in one host project. |
| Layered Monolith | `app-modern` | A layered solution based on Domain Driven Design practices. |
| Modular Monolith | `app-nolayers-modern` | Uses the modern single-layer template with modular solution options enabled. |
| Microservice | `microservice-modern` | A distributed solution with dedicated services, gateways and applications. |
## Practical Differences
| Area | Classic templates | Modern templates |
| --- | --- | --- |
| Creation flow | Template-first: select Single-Layer, Layered or Microservice first. | Architecture-first: select Simple Monolith, Layered Monolith, Modular Monolith or Microservice first. |
| Web UI choices | Supports MVC / Razor Pages, Angular, Blazor WebAssembly, Blazor Server, Blazor Web App, MAUI Blazor and No UI depending on the selected template. | Supports React or No UI. |
| Mobile choices | Keeps broader mobile choices, including MAUI and React Native in templates that support mobile applications. | Supports React Native or no mobile application. |
| Public website | Uses the classic public website structure where the selected template supports it. | Uses React-based public web assets where the selected modern template supports a public website. |
| Frontend assets | Uses the established ABP UI framework integrations and theme options for MVC, Angular and Blazor applications. | Uses newer React and `shadcn`-oriented frontend assets. |
| Best fit | Existing projects, teams using MVC / Angular / Blazor / MAUI, and scenarios that need the broadest UI framework choices. | New React-focused solutions and teams that prefer the newer Studio creation experience. |
## Which One Should I Choose?
Choose **Modern** if you are starting a new solution with React UI, want a React Native mobile application, prefer the newer architecture-first ABP Studio flow, or plan to use AI-assisted programming for an AI-driven project.
Choose **Classic** if you want MVC / Razor Pages, Angular, Blazor, MAUI Blazor or MAUI mobile options, or if your team is following existing Classic-template tutorials, conventions or project structure.
ABP Studio may show or hide some templates and options based on your license. For example, Microservice templates require a higher license level than regular application templates, and Modern application templates are not shown in the Community Edition.
## See Also
* [Solution Template Selection Guide](guide.md)
* [Startup Solution Templates](index.md)
* [Get Started](../get-started/index.md)

2
framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Security/AbpComponentsClaimsCache.cs

@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Components.Web.Security;
public class AbpComponentsClaimsCache : IScopedDependency
{
public ClaimsPrincipal Principal { get; private set; } = default!;
public ClaimsPrincipal Principal { get; private set; } = new ClaimsPrincipal(new ClaimsIdentity());
private readonly AuthenticationStateProvider? _authenticationStateProvider;

6
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs

@ -42,14 +42,14 @@ public class AbpDateTimeModelBinder : IModelBinder
_clock.SupportsMultipleTimezone &&
!_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace())
{
var timeZone = _currentTimezoneProvider.TimeZone;
try
{
var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone);
dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime;
dateTime = _timezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone);
}
catch
{
_logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone);
_logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone);
}
}

155
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobWorkerOptions.cs

@ -1,4 +1,8 @@
namespace Volo.Abp.BackgroundJobs;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.BackgroundJobs;
public class AbpBackgroundJobWorkerOptions
{
@ -15,6 +19,7 @@ public class AbpBackgroundJobWorkerOptions
/// <summary>
/// Maximum count of jobs to fetch from data store in one loop.
/// Also used as the batch size for the retention cleanup deletions (see <see cref="BackgroundJobCleanupWorker"/>).
/// Default: 1000.
/// </summary>
public int MaxJobFetchCount { get; set; }
@ -42,7 +47,61 @@ public class AbpBackgroundJobWorkerOptions
/// Distributed lock name for the worker.
/// Default value: "AbpBackgroundJobWorker".
/// </summary>
public string DistributedLockName { get; set; }
public string DistributedLockName { get; set; }
/// <summary>
/// When set to true, a successfully completed job is kept (its <see cref="BackgroundJobInfo.CompletionTime"/> is set)
/// instead of being deleted, so it can be used for auditing/history. Completed jobs are excluded from the
/// waiting jobs query and are removed by the retention cleanup (see <see cref="SuccessfulJobRetentionTime"/>).
/// Default value: false (successful jobs are deleted).
/// </summary>
public bool StoreSuccessfulJobs { get; set; }
/// <summary>
/// How long a kept (successfully completed) job is retained before the cleanup deletes it.
/// Only relevant when <see cref="StoreSuccessfulJobs"/> is true. Set to null to keep them forever (no automatic cleanup).
/// Default value: 7 days.
/// </summary>
public TimeSpan? SuccessfulJobRetentionTime { get; set; }
/// <summary>
/// Interval (as milliseconds) between cleanup runs that delete retained successful jobs older than <see cref="SuccessfulJobRetentionTime"/>.
/// Default value: 3,600,000 (1 hour).
/// </summary>
public int CleanSuccessfulJobsPeriod { get; set; }
/// <summary>
/// Distributed lock name for the cleanup worker.
/// Default value: "AbpBackgroundJobCleanup".
/// </summary>
public string CleanupDistributedLockName { get; set; }
/// <summary>
/// Dedicated workers, each processing only the configured job types with its own distributed lock.
/// When this list is not empty, an additional default worker is started that processes all
/// other job types. When it is empty, a single default worker processes all job types (the default behavior).
/// Use <see cref="AddDedicatedWorker(string, Type[])"/> to add a dedicated worker.
/// </summary>
public List<BackgroundJobWorkerConfiguration> WorkerConfigurations { get; }
/// <summary>
/// Maximum number of jobs that a single worker executes in parallel within one poll cycle.
/// When it is 1 (default), jobs are executed sequentially under a single worker distributed lock
/// (the default behavior). When greater than 1, the worker distributed lock is not used; instead
/// each job is claimed with its own distributed lock so multiple application instances can execute
/// different jobs concurrently.
/// This value should be configured consistently across all application instances: mixing sequential
/// (worker lock) and parallel (per-job lock) instances removes the common mutual exclusion and may
/// let the same job run on more than one instance.
/// Default value: 1.
/// </summary>
public int MaxParallelJobExecutionCount { get; set; }
/// <summary>
/// Prefix of the per-job distributed lock name used when <see cref="MaxParallelJobExecutionCount"/> is greater than 1.
/// Default value: "AbpBackgroundJob:".
/// </summary>
public string PerJobDistributedLockPrefix { get; set; }
public AbpBackgroundJobWorkerOptions()
{
@ -52,5 +111,97 @@ public class AbpBackgroundJobWorkerOptions
DefaultTimeout = 172800;
DefaultWaitFactor = 2.0;
DistributedLockName = "AbpBackgroundJobWorker";
WorkerConfigurations = new List<BackgroundJobWorkerConfiguration>();
MaxParallelJobExecutionCount = 1;
PerJobDistributedLockPrefix = "AbpBackgroundJob:";
SuccessfulJobRetentionTime = TimeSpan.FromDays(7);
CleanSuccessfulJobsPeriod = 3600000;
CleanupDistributedLockName = "AbpBackgroundJobCleanup";
}
/// <summary>
/// Adds a dedicated worker that processes only the given job argument types.
/// </summary>
/// <param name="lockName">A unique distributed lock name for this worker.</param>
/// <param name="jobArgsTypes">The job argument types handled exclusively by this worker.</param>
public AbpBackgroundJobWorkerOptions AddDedicatedWorker(string lockName, params Type[] jobArgsTypes)
{
Check.NotNullOrEmpty(jobArgsTypes, nameof(jobArgsTypes));
var configuration = new BackgroundJobWorkerConfiguration(lockName, jobArgsTypes.Distinct().ToArray());
var duplicateType = configuration.JobArgsTypes.FirstOrDefault(
type => WorkerConfigurations.Any(c => c.JobArgsTypes.Contains(type)));
if (duplicateType != null)
{
throw new AbpException(
$"The background job args type '{duplicateType.FullName}' is already assigned to a dedicated worker. " +
$"Each job type can be handled by only one dedicated worker.");
}
if (lockName == DistributedLockName || WorkerConfigurations.Any(c => c.LockName == lockName))
{
throw new AbpException(
$"The distributed lock name '{lockName}' is already used by another background job worker. " +
$"Each worker must have a unique lock name.");
}
WorkerConfigurations.Add(configuration);
return this;
}
public AbpBackgroundJobWorkerOptions AddDedicatedWorker<TArgs>(string lockName)
{
return AddDedicatedWorker(lockName, typeof(TArgs));
}
public AbpBackgroundJobWorkerOptions AddDedicatedWorker<TArgs1, TArgs2>(string lockName)
{
return AddDedicatedWorker(lockName, typeof(TArgs1), typeof(TArgs2));
}
public AbpBackgroundJobWorkerOptions AddDedicatedWorker<TArgs1, TArgs2, TArgs3>(string lockName)
{
return AddDedicatedWorker(lockName, typeof(TArgs1), typeof(TArgs2), typeof(TArgs3));
}
/// <summary>
/// Adds a dedicated worker with a lock name derived from the given job argument type names.
/// Use the <c>AddDedicatedWorker(string lockName, ...)</c> overloads to set an explicit lock name.
/// </summary>
/// <param name="jobArgsTypes">The job argument types handled exclusively by this worker.</param>
public AbpBackgroundJobWorkerOptions AddDedicatedWorker(params Type[] jobArgsTypes)
{
return AddDedicatedWorker(GetDedicatedWorkerLockName(jobArgsTypes), jobArgsTypes);
}
public AbpBackgroundJobWorkerOptions AddDedicatedWorker<TArgs>()
{
return AddDedicatedWorker(typeof(TArgs));
}
public AbpBackgroundJobWorkerOptions AddDedicatedWorker<TArgs1, TArgs2>()
{
return AddDedicatedWorker(typeof(TArgs1), typeof(TArgs2));
}
public AbpBackgroundJobWorkerOptions AddDedicatedWorker<TArgs1, TArgs2, TArgs3>()
{
return AddDedicatedWorker(typeof(TArgs1), typeof(TArgs2), typeof(TArgs3));
}
protected virtual string GetDedicatedWorkerLockName(Type[] jobArgsTypes)
{
Check.NotNullOrEmpty(jobArgsTypes, nameof(jobArgsTypes));
if (jobArgsTypes.Any(t => t == null))
{
throw new ArgumentException("Job args types cannot contain null.", nameof(jobArgsTypes));
}
// Hash the (stable, sorted) full type names so the derived lock name stays short and within the
// length limits of distributed lock providers (e.g. SQL Server sp_getapplock is limited to 255 chars).
var key = string.Join(",", jobArgsTypes.Select(t => t.FullName).Distinct().OrderBy(n => n, StringComparer.Ordinal));
return "AbpBackgroundJobDedicatedWorker:" + key.ToMd5();
}
}

12
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/AbpBackgroundJobsModule.cs

@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
@ -23,9 +23,15 @@ public class AbpBackgroundJobsModule : AbpModule
{
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
if (context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundJobOptions>>().Value.IsJobExecutionEnabled)
// The manager decides (based on options) whether and which workers to run.
await context.AddBackgroundWorkerAsync<BackgroundJobWorkerManager>();
// Only register the cleanup worker when it has something to do (retained successful jobs).
var jobOptions = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundJobOptions>>().Value;
var workerOptions = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundJobWorkerOptions>>().Value;
if (jobOptions.IsJobExecutionEnabled && workerOptions.StoreSuccessfulJobs && workerOptions.SuccessfulJobRetentionTime != null)
{
await context.AddBackgroundWorkerAsync<IBackgroundJobWorker>();
await context.AddBackgroundWorkerAsync<BackgroundJobCleanupWorker>();
}
}

66
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobCleanupWorker.cs

@ -0,0 +1,66 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DistributedLocking;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
namespace Volo.Abp.BackgroundJobs;
/// <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);
}
}
}

8
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobInfo.cs

@ -50,6 +50,14 @@ public class BackgroundJobInfo
/// </summary>
public virtual bool IsAbandoned { get; set; }
/// <summary>
/// The time this job was completed successfully.
/// When set, the job is kept as history and excluded from the waiting jobs query.
/// It is only set when <see cref="AbpBackgroundJobWorkerOptions.StoreSuccessfulJobs"/> is enabled;
/// otherwise successfully completed jobs are deleted.
/// </summary>
public virtual DateTime? CompletionTime { get; set; }
/// <summary>
/// Priority of this job.
/// </summary>

71
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilter.cs

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.BackgroundJobs;
/// <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
};
}
}

19
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobNameFilterMode.cs

@ -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
}

353
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorker.cs

@ -1,17 +1,22 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
namespace Volo.Abp.BackgroundJobs;
public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroundJobWorker
public class BackgroundJobWorker : IBackgroundJobWorker, ITransientDependency
{
protected AbpBackgroundJobOptions JobOptions { get; }
@ -19,95 +24,321 @@ public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroun
protected IAbpDistributedLock DistributedLock { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected AbpAsyncTimer Timer { get; }
public ILogger<BackgroundJobWorker> Logger { get; set; }
protected string DistributedLockName { get; set; } = default!;
protected BackgroundJobNameFilter JobNameFilter { get; set; } = BackgroundJobNameFilter.None;
protected CancellationTokenSource StoppingTokenSource { get; }
protected CancellationToken StoppingToken { get; }
public BackgroundJobWorker(
AbpAsyncTimer timer,
IOptions<AbpBackgroundJobOptions> jobOptions,
IOptions<AbpBackgroundJobWorkerOptions> workerOptions,
IServiceScopeFactory serviceScopeFactory,
IAbpDistributedLock distributedLock)
: base(
timer,
serviceScopeFactory)
{
Timer = timer;
DistributedLock = distributedLock;
ServiceScopeFactory = serviceScopeFactory;
WorkerOptions = workerOptions.Value;
JobOptions = jobOptions.Value;
Logger = NullLogger<BackgroundJobWorker>.Instance;
Timer.Period = WorkerOptions.JobPollPeriod;
Timer.Elapsed = TimerOnElapsed;
StoppingTokenSource = new CancellationTokenSource();
StoppingToken = StoppingTokenSource.Token;
}
public virtual Task StartAsync(
string? distributedLockName = null,
BackgroundJobNameFilter? jobNameFilter = null,
CancellationToken cancellationToken = default)
{
DistributedLockName = distributedLockName ?? WorkerOptions.DistributedLockName;
JobNameFilter = jobNameFilter ?? BackgroundJobNameFilter.None;
Timer.Start(cancellationToken);
return Task.CompletedTask;
}
public virtual Task StopAsync(CancellationToken cancellationToken = default)
{
StoppingTokenSource.Cancel();
Timer.Stop(cancellationToken);
StoppingTokenSource.Dispose();
return Task.CompletedTask;
}
private async Task TimerOnElapsed(AbpAsyncTimer timer)
{
await RunAsync();
}
protected virtual async Task RunAsync()
{
using var scope = ServiceScopeFactory.CreateScope();
try
{
var workerContext = new PeriodicBackgroundWorkerContext(scope.ServiceProvider, StoppingToken);
if (WorkerOptions.MaxParallelJobExecutionCount > 1)
{
await ExecuteJobsInParallelAsync(workerContext);
}
else
{
await ExecuteJobsWithWorkerLockAsync(workerContext);
}
}
catch (Exception ex)
{
await scope.ServiceProvider
.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
Logger.LogException(ex);
}
}
protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext)
protected virtual async Task ExecuteJobsWithWorkerLockAsync(PeriodicBackgroundWorkerContext workerContext)
{
await using (var handler = await DistributedLock.TryAcquireAsync(WorkerOptions.DistributedLockName, cancellationToken: StoppingToken))
await using (var handler = await DistributedLock.TryAcquireAsync(DistributedLockName, cancellationToken: StoppingToken))
{
if (handler != null)
{
var store = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
await ExecuteWaitingJobsAsync(workerContext);
}
else
{
await WaitForNextTryAsync();
}
}
}
protected virtual async Task ExecuteWaitingJobsAsync(PeriodicBackgroundWorkerContext workerContext)
{
var store = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
var waitingJobs = await GetWaitingJobsAsync(workerContext, store);
var waitingJobs = await store.GetWaitingJobsAsync(WorkerOptions.ApplicationName, WorkerOptions.MaxJobFetchCount);
if (!waitingJobs.Any())
{
return;
}
if (!waitingJobs.Any())
var jobExecuter = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobExecuter>();
var clock = workerContext.ServiceProvider.GetRequiredService<IClock>();
var serializer = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobSerializer>();
foreach (var jobInfo in waitingJobs)
{
await TryExecuteJobAsync(workerContext, store, jobInfo, jobExecuter, clock, serializer);
}
}
/// <summary>
/// Executes waiting jobs in parallel across application instances, up to
/// <see cref="AbpBackgroundJobWorkerOptions.MaxParallelJobExecutionCount"/> jobs per cycle.
/// </summary>
protected virtual async Task ExecuteJobsInParallelAsync(PeriodicBackgroundWorkerContext workerContext)
{
var store = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
var waitingJobs = await GetWaitingJobsAsync(workerContext, store);
if (!waitingJobs.Any())
{
return;
}
var runningTasks = new List<Task>();
// Await already-started jobs even if acquiring a lock for a later job throws,
// so no claimed job is left running detached from this cycle.
try
{
foreach (var jobInfo in waitingJobs)
{
if (runningTasks.Count >= WorkerOptions.MaxParallelJobExecutionCount || StoppingToken.IsCancellationRequested)
{
return;
break;
}
var jobExecuter = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobExecuter>();
var clock = workerContext.ServiceProvider.GetRequiredService<IClock>();
var serializer = workerContext.ServiceProvider.GetRequiredService<IBackgroundJobSerializer>();
foreach (var jobInfo in waitingJobs)
var handle = await DistributedLock.TryAcquireAsync(GetPerJobDistributedLockName(jobInfo), cancellationToken: StoppingToken);
if (handle == null)
{
jobInfo.TryCount++;
jobInfo.LastTryTime = clock.Now;
try
{
var jobConfiguration = JobOptions.GetJob(jobInfo.JobName);
var jobArgs = serializer.Deserialize(jobInfo.JobArgs, jobConfiguration.ArgsType);
var context = new JobExecutionContext(
workerContext.ServiceProvider,
jobConfiguration.JobType,
jobArgs,
workerContext.CancellationToken);
try
{
await jobExecuter.ExecuteAsync(context);
await store.DeleteAsync(jobInfo.Id);
}
catch (BackgroundJobExecutionException)
{
var nextTryTime = CalculateNextTryTime(jobInfo, clock);
if (nextTryTime.HasValue)
{
jobInfo.NextTryTime = nextTryTime.Value;
}
else
{
jobInfo.IsAbandoned = true;
}
await TryUpdateAsync(store, jobInfo);
}
}
catch (Exception ex)
{
Logger.LogException(ex);
jobInfo.IsAbandoned = true;
await TryUpdateAsync(store, jobInfo);
}
// Another instance is already processing this job.
continue;
}
runningTasks.Add(ExecuteClaimedJobAsync(jobInfo, handle));
}
else
}
finally
{
await Task.WhenAll(runningTasks);
}
}
protected virtual async Task ExecuteClaimedJobAsync(BackgroundJobInfo jobInfo, IAbpDistributedLockHandle handle)
{
await using (handle)
{
// Each concurrently executed job runs in its own service scope so that scoped services
// (e.g. the DbContext and the unit of work) are not shared across parallel jobs.
using var scope = ServiceScopeFactory.CreateScope();
try
{
try
var workerContext = new PeriodicBackgroundWorkerContext(scope.ServiceProvider, StoppingToken);
var store = scope.ServiceProvider.GetRequiredService<IBackgroundJobStore>();
var clock = scope.ServiceProvider.GetRequiredService<IClock>();
// Re-read under the lock: another instance may have completed, abandoned or rescheduled this job
// between fetching the waiting list and acquiring the per-job lock.
var currentJobInfo = await store.FindAsync(jobInfo.Id);
if (!IsJobEligible(currentJobInfo, clock))
{
await Task.Delay(WorkerOptions.JobPollPeriod * 12, StoppingToken);
return;
}
catch (TaskCanceledException) { }
var jobExecuter = scope.ServiceProvider.GetRequiredService<IBackgroundJobExecuter>();
var serializer = scope.ServiceProvider.GetRequiredService<IBackgroundJobSerializer>();
await TryExecuteJobAsync(workerContext, store, currentJobInfo, jobExecuter, clock, serializer);
}
catch (Exception ex)
{
await scope.ServiceProvider
.GetRequiredService<IExceptionNotifier>()
.NotifyAsync(new ExceptionNotificationContext(ex));
Logger.LogException(ex);
}
}
}
protected virtual bool IsJobEligible(BackgroundJobInfo? jobInfo, IClock clock)
{
return jobInfo != null &&
jobInfo.ApplicationName == WorkerOptions.ApplicationName &&
!jobInfo.IsAbandoned &&
jobInfo.CompletionTime == null &&
jobInfo.NextTryTime <= clock.Now &&
JobNameFilter.IsMatch(jobInfo.JobName);
}
protected virtual string GetPerJobDistributedLockName(BackgroundJobInfo jobInfo)
{
return WorkerOptions.PerJobDistributedLockPrefix + jobInfo.Id;
}
protected virtual async Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(
PeriodicBackgroundWorkerContext workerContext,
IBackgroundJobStore store)
{
return await store.GetWaitingJobsAsync(
WorkerOptions.ApplicationName,
WorkerOptions.MaxJobFetchCount,
JobNameFilter);
}
protected virtual async Task TryExecuteJobAsync(
PeriodicBackgroundWorkerContext workerContext,
IBackgroundJobStore store,
BackgroundJobInfo jobInfo,
IBackgroundJobExecuter jobExecuter,
IClock clock,
IBackgroundJobSerializer serializer)
{
jobInfo.TryCount++;
jobInfo.LastTryTime = clock.Now;
try
{
var jobConfiguration = JobOptions.GetJob(jobInfo.JobName);
var jobArgs = serializer.Deserialize(jobInfo.JobArgs, jobConfiguration.ArgsType);
var context = new JobExecutionContext(
workerContext.ServiceProvider,
jobConfiguration.JobType,
jobArgs,
workerContext.CancellationToken);
try
{
await jobExecuter.ExecuteAsync(context);
await HandleJobSuccessAsync(store, jobInfo, clock);
}
catch (BackgroundJobExecutionException)
{
await HandleJobFailureAsync(store, jobInfo, clock);
}
}
catch (Exception ex)
{
await HandleJobErrorAsync(store, jobInfo, ex);
}
}
protected virtual async Task HandleJobSuccessAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, IClock clock)
{
if (WorkerOptions.StoreSuccessfulJobs)
{
// Keep the job as history: mark it completed instead of deleting. It is then excluded from the
// waiting jobs query and removed later by the retention cleanup.
jobInfo.CompletionTime = clock.Now;
await store.UpdateAsync(jobInfo);
}
else
{
await store.DeleteAsync(jobInfo.Id);
}
}
protected virtual async Task HandleJobFailureAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, IClock clock)
{
var nextTryTime = CalculateNextTryTime(jobInfo, clock);
if (nextTryTime.HasValue)
{
jobInfo.NextTryTime = nextTryTime.Value;
}
else
{
jobInfo.IsAbandoned = true;
}
await TryUpdateAsync(store, jobInfo);
}
protected virtual async Task HandleJobErrorAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo, Exception ex)
{
Logger.LogException(ex);
jobInfo.IsAbandoned = true;
await TryUpdateAsync(store, jobInfo);
}
protected virtual async Task WaitForNextTryAsync()
{
try
{
await Task.Delay(WorkerOptions.JobPollPeriod * 12, StoppingToken);
}
catch (TaskCanceledException) { }
}
protected virtual async Task TryUpdateAsync(IBackgroundJobStore store, BackgroundJobInfo jobInfo)

37
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerConfiguration.cs

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.BackgroundJobs;
/// <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();
}
}

131
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/BackgroundJobWorkerManager.cs

@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
namespace Volo.Abp.BackgroundJobs;
/// <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();
}
}

27
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DedicatedWorkerDefinition.cs

@ -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;
}
}

26
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobStore.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;
@ -24,7 +25,7 @@ public interface IBackgroundJobStore
/// <summary>
/// Gets waiting jobs. It should get jobs based on these:
/// Conditions: ApplicationName is applicationName And !IsAbandoned And NextTryTime &lt;= Clock.Now.
/// Conditions: ApplicationName is applicationName And !IsAbandoned And CompletionTime == null And NextTryTime &lt;= Clock.Now.
/// Order by: Priority DESC, TryCount ASC, NextTryTime ASC.
/// Maximum result: <paramref name="maxResultCount"/>.
/// </summary>
@ -32,12 +33,35 @@ public interface IBackgroundJobStore
/// <param name="maxResultCount">Maximum result count.</param>
Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(string? applicationName, int maxResultCount);
/// <summary>
/// Gets waiting jobs (see <see cref="GetWaitingJobsAsync(string, int)"/>), additionally filtered by job name
/// according to <paramref name="jobNameFilter"/>.
/// </summary>
/// <param name="applicationName">Application name.</param>
/// <param name="maxResultCount">Maximum result count.</param>
/// <param name="jobNameFilter">Job name filter. When null, no job name filter is applied.</param>
Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(
string? applicationName,
int maxResultCount,
BackgroundJobNameFilter? jobNameFilter);
/// <summary>
/// Deletes a job.
/// </summary>
/// <param name="jobId">The Job Unique Identifier.</param>
Task DeleteAsync(Guid jobId);
/// <summary>
/// Deletes successfully completed jobs (<see cref="BackgroundJobInfo.CompletionTime"/> is set) of the given
/// application that completed before <paramref name="completedBefore"/>. Used by the retention cleanup.
/// </summary>
/// <returns>The number of deleted jobs.</returns>
Task<int> DeleteAsync(
string? applicationName,
DateTime completedBefore,
int maxResultCount,
CancellationToken cancellationToken = default);
/// <summary>
/// Updates a job.
/// </summary>

23
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/IBackgroundJobWorker.cs

@ -1,8 +1,27 @@
using Volo.Abp.BackgroundWorkers;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;
public interface IBackgroundJobWorker : IBackgroundWorker
/// <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);
}

36
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/InMemoryBackgroundJobStore.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Timing;
@ -37,9 +38,20 @@ public class InMemoryBackgroundJobStore : IBackgroundJobStore, ISingletonDepende
public virtual Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(string? applicationName, int maxResultCount)
{
return GetWaitingJobsAsync(applicationName, maxResultCount, null);
}
public virtual Task<List<BackgroundJobInfo>> GetWaitingJobsAsync(
string? applicationName,
int maxResultCount,
BackgroundJobNameFilter? jobNameFilter)
{
var filter = jobNameFilter ?? BackgroundJobNameFilter.None;
var waitingJobs = _jobs.Values
.Where(t => t.ApplicationName == applicationName)
.Where(t => !t.IsAbandoned && t.NextTryTime <= Clock.Now)
.Where(t => !t.IsAbandoned && t.CompletionTime == null && t.NextTryTime <= Clock.Now)
.Where(t => filter.IsMatch(t.JobName))
.OrderByDescending(t => t.Priority)
.ThenBy(t => t.TryCount)
.ThenBy(t => t.NextTryTime)
@ -57,6 +69,28 @@ public class InMemoryBackgroundJobStore : IBackgroundJobStore, ISingletonDepende
return Task.CompletedTask;
}
public virtual Task<int> DeleteAsync(
string? applicationName,
DateTime completedBefore,
int maxResultCount,
CancellationToken cancellationToken = default)
{
var idsToDelete = _jobs.Values
.Where(t => t.ApplicationName == applicationName)
.Where(t => t.CompletionTime != null && t.CompletionTime < completedBefore)
.OrderBy(t => t.CompletionTime)
.Take(maxResultCount)
.Select(t => t.Id)
.ToList();
foreach (var id in idsToDelete)
{
_jobs.TryRemove(id, out _);
}
return Task.FromResult(idsToDelete.Count);
}
public virtual Task UpdateAsync(BackgroundJobInfo jobInfo)
{
if (jobInfo.IsAbandoned)

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save