Browse Source

Merge branch 'dev' into feat/mudblazor

feat/mudblazor-mix
maliming 6 months ago
parent
commit
b69ef774d9
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 8
      .claude/settings.local.json
  2. 1
      .github/workflows/auto-pr.yml
  3. 2
      .gitignore
  4. 123
      Directory.Packages.props
  5. 151
      ai-rules/README.md
  6. 182
      ai-rules/common/abp-core.mdc
  7. 236
      ai-rules/common/application-layer.mdc
  8. 186
      ai-rules/common/authorization.mdc
  9. 92
      ai-rules/common/cli-commands.mdc
  10. 244
      ai-rules/common/ddd-patterns.mdc
  11. 153
      ai-rules/common/dependency-rules.mdc
  12. 299
      ai-rules/common/development-flow.mdc
  13. 249
      ai-rules/common/infrastructure.mdc
  14. 165
      ai-rules/common/multi-tenancy.mdc
  15. 257
      ai-rules/data/ef-core.mdc
  16. 206
      ai-rules/data/mongodb.mdc
  17. 83
      ai-rules/template-specific/app-nolayers.mdc
  18. 209
      ai-rules/template-specific/microservice.mdc
  19. 234
      ai-rules/template-specific/module.mdc
  20. 274
      ai-rules/testing/patterns.mdc
  21. 224
      ai-rules/ui/angular.mdc
  22. 210
      ai-rules/ui/blazor.mdc
  23. 262
      ai-rules/ui/mvc.mdc
  24. 1
      delete-bin-obj.ps1
  25. 17
      docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md
  26. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png
  27. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png
  28. 153
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md
  29. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png
  30. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg
  31. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png
  32. BIN
      docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png
  33. 167
      docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/POST.md
  34. BIN
      docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/image.png
  35. 50
      docs/en/Community-Articles/2026-02-02-ndc-london-article/post.md
  36. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/0.png
  37. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/1.png
  38. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/2.png
  39. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/3.png
  40. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4.png
  41. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_1.png
  42. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_2.png
  43. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/5.png
  44. 239
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/Post.md
  45. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/cover.png
  46. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/steve-sanderson-talk.png
  47. 2
      docs/en/deployment/configuring-production.md
  48. 14
      docs/en/docs-nav.json
  49. 2
      docs/en/framework/api-development/standard-apis/configuration.md
  50. 2
      docs/en/framework/architecture/domain-driven-design/application-services.md
  51. 23
      docs/en/framework/architecture/domain-driven-design/entities.md
  52. 2
      docs/en/framework/architecture/modularity/extending/customizing-application-modules-guide.md
  53. 97
      docs/en/framework/fundamentals/authorization/index.md
  54. 241
      docs/en/framework/fundamentals/authorization/resource-based-authorization.md
  55. 2
      docs/en/framework/fundamentals/dynamic-claims.md
  56. 2
      docs/en/framework/fundamentals/exception-handling.md
  57. 2
      docs/en/framework/fundamentals/index.md
  58. 2
      docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md
  59. 2
      docs/en/framework/infrastructure/background-jobs/hangfire.md
  60. 2
      docs/en/framework/infrastructure/interceptors.md
  61. 29
      docs/en/framework/infrastructure/object-to-object-mapping.md
  62. 1
      docs/en/framework/ui/angular/data-table-column-extensions.md
  63. 189
      docs/en/framework/ui/angular/extensible-table-row-detail.md
  64. BIN
      docs/en/framework/ui/angular/images/row-detail-image.png
  65. 2
      docs/en/framework/ui/angular/localization.md
  66. 2
      docs/en/framework/ui/angular/permission-management.md
  67. 4
      docs/en/framework/ui/blazor/authorization.md
  68. 2
      docs/en/framework/ui/blazor/page-toolbar-extensions.md
  69. 2
      docs/en/framework/ui/mvc-razor-pages/auto-complete-select.md
  70. 2
      docs/en/framework/ui/mvc-razor-pages/javascript-api/ajax.md
  71. 2
      docs/en/framework/ui/mvc-razor-pages/javascript-api/auth.md
  72. 2
      docs/en/framework/ui/mvc-razor-pages/navigation-menu.md
  73. 2
      docs/en/framework/ui/mvc-razor-pages/page-toolbar-extensions.md
  74. 2
      docs/en/framework/ui/mvc-razor-pages/toolbars.md
  75. 4
      docs/en/framework/ui/mvc-razor-pages/widgets.md
  76. BIN
      docs/en/get-started/images/abp-studio-background-tasks.png
  77. BIN
      docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png
  78. BIN
      docs/en/get-started/images/abp-studio-created-new-microservice-solution.png
  79. BIN
      docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png
  80. BIN
      docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png
  81. BIN
      docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png
  82. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png
  83. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png
  84. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png
  85. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png
  86. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png
  87. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png
  88. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png
  89. BIN
      docs/en/get-started/images/abp-studio-microservice-solution-runner.png
  90. BIN
      docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png
  91. BIN
      docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png
  92. BIN
      docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png
  93. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png
  94. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png
  95. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png
  96. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png
  97. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png
  98. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png
  99. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png
  100. BIN
      docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png

8
.claude/settings.local.json

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(yarn nx g:*)",
"Bash(npx vitest:*)"
]
}
}

1
.github/workflows/auto-pr.yml

@ -26,7 +26,6 @@ jobs:
branch: auto-merge/rel-10-1/${{github.run_number}}
title: Merge branch dev with rel-10.1
body: This PR generated automatically to merge dev with rel-10.1. Please review the changed files before merging to prevent any errors that may occur.
reviewers: maliming
draft: true
token: ${{ github.token }}
- name: Merge Pull Request

2
.gitignore

@ -328,4 +328,4 @@ deploy/_run_all_log.txt
# No commit yarn.lock files in the subfolders of templates directory
templates/**/yarn.lock
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Logs/logs.txt
templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json
templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/Properties/launchSettings.json

123
Directory.Packages.props

@ -31,8 +31,9 @@
<PackageVersion Include="Dapper" Version="2.1.66" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.16.0" />
<PackageVersion Include="Dapr.Client" Version="1.16.0" />
<PackageVersion Include="ModelContextProtocol" Version="0.5.0-preview.1" />
<PackageVersion Include="MyCSharp.HttpUserAgentParser" Version="3.0.28" />
<PackageVersion Include="Devart.Data.Oracle.EFCore" Version="10.4.290.9" />
<PackageVersion Include="Devart.Data.Oracle.EFCore" Version="11.0.0.9" />
<PackageVersion Include="DistributedLock.Core" Version="1.0.8" />
<PackageVersion Include="DistributedLock.Redis" Version="1.1.0" />
<PackageVersion Include="DeepL.net" Version="1.15.0" />
@ -57,63 +58,63 @@
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="14.9.1" />
<PackageVersion Include="MailKit" Version="4.13.0" />
<PackageVersion Include="Markdig.Signed" Version="0.42.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.0" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.20" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="10.0.20" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.2" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
<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.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.67.1" />
<PackageVersion Include="Microsoft.SemanticKernel.Abstractions" Version="1.67.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.9.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Composite" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Composite" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.2" />
<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" />
@ -122,7 +123,7 @@
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.14.0" />
<PackageVersion Include="Minio" Version="6.0.5" />
<PackageVersion Include="MongoDB.Driver" Version="3.5.2" />
<PackageVersion Include="MongoDB.Driver" Version="3.6.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" />
@ -141,7 +142,7 @@
<PackageVersion Include="Polly" Version="8.6.3" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.0-rc" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.1" />
<PackageVersion Include="Quartz" Version="3.15.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.15.0" />
<PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="3.15.0" />
@ -169,17 +170,17 @@
<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.0" />
<PackageVersion Include="System.Collections.Immutable" Version="10.0.2" />
<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.0" />
<PackageVersion Include="System.Security.Permissions" Version="10.0.0" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.2" />
<PackageVersion Include="System.Security.Permissions" Version="10.0.2" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.0" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.2" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.2" />
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
<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" />
@ -194,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.0" />
<PackageVersion Include="System.Management" Version="10.0.2"/>
</ItemGroup>
</Project>
</Project>

151
ai-rules/README.md

@ -0,0 +1,151 @@
# ABP AI Rules
This folder contains AI rules (Cursor `.mdc` format) for ABP based solutions. These rules help AI assistants understand ABP-specific patterns, conventions, and best practices when working with ABP-based applications.
## Purpose
This folder serves as a central repository for ABP-specific AI rules. The community can contribute, improve, and maintain these rules collaboratively.
When you create a new ABP solution, these rules are included in your project based on your configuration. This provides AI assistants with ABP-specific context, helping them generate code that follows ABP conventions.
> **Important**: These rules are ABP-specific. They don't cover general .NET or ASP.NET Core patterns—AI assistants already know those. Instead, they focus on ABP's unique architecture, module system, and conventions.
## How Rules Work
Large language models don't retain memory between completions. Rules provide persistent, reusable context at the prompt level.
When applied, rule contents are included at the start of the model context. This gives the AI consistent guidance for generating code, interpreting edits, or helping with workflows.
## Mini Glossary (ABP Terms)
- **Application service**: Use-case orchestration (ABP’s primary “business API” surface). Usually exposed remotely via Auto API Controllers or explicit controllers.
- **Auto API Controllers**: ABP can auto-generate HTTP endpoints from `IApplicationService` contracts.
- **Client proxy**: Generated client-side code (Angular/JS/C#) to call remote application services.
- **Integration service (microservices)**: Application-service-like contract intended for **service-to-service** communication; typically exposed separately and consumed via generated C# proxies.
- **Domain vs Application**: Domain holds business rules/invariants; Application coordinates domain + infrastructure and returns DTOs.
## File Structure
```
ai-rules/
├── README.md
├── common/ # Rules for all ABP projects
│ ├── abp-core.mdc # Core ABP conventions (alwaysApply: true)
│ ├── ddd-patterns.mdc # DDD patterns (Entity, AggregateRoot, Repository)
│ ├── application-layer.mdc # Application services, DTOs, validation
│ ├── authorization.mdc # Permissions and authorization
│ ├── multi-tenancy.mdc # Multi-tenant entities and data isolation
│ ├── infrastructure.mdc # Settings, Features, Caching, Events, Jobs
│ ├── dependency-rules.mdc # Layer dependencies and guardrails
│ ├── development-flow.mdc # Development workflow
│ └── cli-commands.mdc # ABP CLI commands reference
├── ui/ # UI-specific rules (applied by globs)
│ ├── blazor.mdc # Blazor UI patterns
│ ├── angular.mdc # Angular UI patterns
│ └── mvc.mdc # MVC/Razor Pages patterns
├── data/ # Data layer rules (applied by globs)
│ ├── ef-core.mdc # Entity Framework Core patterns
│ └── mongodb.mdc # MongoDB patterns
├── testing/ # Testing rules
│ └── patterns.mdc # Unit and integration test patterns
└── template-specific/ # Template-specific rules
├── app-nolayers.mdc # Single-layer app template
├── module.mdc # Module template
└── microservice.mdc # Microservice template
```
### Rule Format
Each rule is a markdown file with frontmatter metadata:
```markdown
---
description: "Describes when this rule should apply - used by AI to decide relevance"
globs: "src/**/*.cs"
alwaysApply: false
---
# Rule Title
Your rule content here...
```
### Frontmatter Properties
| Property | Description |
|----------|-------------|
| `description` | Brief description of what the rule covers. Used by AI to determine relevance. |
| `globs` | File patterns that trigger this rule (e.g., `**/*.cs`, `*.Domain/**`). |
| `alwaysApply` | If `true`, rule is always included. If `false`, AI decides based on context. |
### Rule Types
| Type | When Applied |
|------|--------------|
| **Always Apply** | Every chat session (`alwaysApply: true`) |
| **Apply Intelligently** | When AI decides it's relevant based on `description` |
| **Apply to Specific Files** | When file matches `globs` pattern |
| **Apply Manually** | When @-mentioned in chat (e.g., `@my-rule`) |
## Rule Categories
### Common Rules
Core ABP patterns that apply to all DDD-based templates (app, module, microservice):
- `abp-core.mdc` - Always applied, covers module system, DI conventions, base classes
- `ddd-patterns.mdc` - Entity, AggregateRoot, Repository, Domain Services
- `application-layer.mdc` - Application services, DTOs, validation, error handling
- `authorization.mdc` - Permission system and authorization
- `infrastructure.mdc` - Settings, Features, Caching, Events, Background Jobs
- `dependency-rules.mdc` - Layer dependencies and project structure
- `development-flow.mdc` - Development workflow for adding features
### UI Rules (Applied by Globs)
- `blazor.mdc` - Applied to `**/*.razor`, `**/Blazor/**/*.cs`
- `angular.mdc` - Applied to `**/angular/**/*.ts`
- `mvc.mdc` - Applied to `**/*.cshtml`, `**/Pages/**/*.cs`
### Data Rules (Applied by Globs)
- `ef-core.mdc` - Applied to `**/*.EntityFrameworkCore/**/*.cs`
- `mongodb.mdc` - Applied to `**/*.MongoDB/**/*.cs`
### Template-Specific Rules
- `app-nolayers.mdc` - For single-layer web application template
- `module.mdc` - For reusable module template
- `microservice.mdc` - For microservice template
## Best Practices
Good rules are focused, actionable, and scoped:
- **Keep rules under 500 lines** - Split large rules into multiple, composable rules
- **Provide concrete examples** - Reference actual files or include code snippets
- **Be specific, not vague** - Write rules like clear internal documentation
- **Reference files instead of copying** - This keeps rules short and prevents staleness
- **Start simple** - Add rules only when you notice AI making the same mistake repeatedly
## What to Avoid
- **Copying entire style guides**: Use a linter instead. AI already knows common style conventions.
- **Documenting every possible command**: AI knows common tools like `dotnet` and `npm`.
- **Adding instructions for edge cases that rarely apply**: Keep rules focused on patterns you use frequently.
- **Duplicating what's already in your codebase**: Point to canonical examples instead of copying code.
- **Including non-ABP patterns**: Don't add generic .NET/ASP.NET Core guidance—focus on ABP-specific conventions.
## Contributing
We welcome community contributions to improve these rules! You can open a PR to add new rules or improve existing ones.
Please review our [Contribution Guide](../CONTRIBUTING.md) and [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing.
### Contribution Guidelines
- Each rule should focus on a single ABP concept or pattern
- Use clear, actionable language
- Include examples where helpful
- Test your rules by using them in a real ABP project
- Keep ABP-specific focus—don't add general .NET patterns
## Related Resources
- [Cursor Rules Documentation](https://cursor.com/docs/context/rules)
- [ABP Framework Documentation](https://abp.io/docs)

182
ai-rules/common/abp-core.mdc

@ -0,0 +1,182 @@
---
description: "Core ABP Framework conventions - module system, dependency injection, and base classes"
alwaysApply: true
---
# ABP Core Conventions
> **Documentation**: https://abp.io/docs/latest
> **API Reference**: https://abp.io/docs/api/
## Module System
Every ABP application/module has a module class that configures services:
```csharp
[DependsOn(
typeof(AbpDddDomainModule),
typeof(AbpEntityFrameworkCoreModule)
)]
public class MyAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Service registration and configuration
}
}
```
> **Note**: Middleware configuration (`OnApplicationInitialization`) should only be done in the final host application, not in reusable modules.
## Dependency Injection Conventions
### Automatic Registration
ABP automatically registers services implementing marker interfaces:
- `ITransientDependency` → Transient lifetime
- `ISingletonDependency` → Singleton lifetime
- `IScopedDependency` → Scoped lifetime
Classes inheriting from `ApplicationService`, `DomainService`, `AbpController` are also auto-registered.
### Repository Usage
You can use the generic `IRepository<TEntity, TKey>` for simple CRUD operations. Define custom repository interfaces only when you need custom query methods:
```csharp
// Simple CRUD - Generic repository is fine
public class BookAppService : ApplicationService
{
private readonly IRepository<Book, Guid> _bookRepository; // ✅ OK for simple operations
}
// Custom queries needed - Define custom interface
public interface IBookRepository : IRepository<Book, Guid>
{
Task<Book> FindByNameAsync(string name); // Custom query
}
public class BookAppService : ApplicationService
{
private readonly IBookRepository _bookRepository; // ✅ Use custom when needed
}
```
### Exposing Services
```csharp
[ExposeServices(typeof(IMyService))]
public class MyService : IMyService, ITransientDependency { }
```
## Important Base Classes
| Base Class | Purpose |
|------------|---------|
| `Entity<TKey>` | Basic entity with ID |
| `AggregateRoot<TKey>` | DDD aggregate root |
| `DomainService` | Domain business logic |
| `ApplicationService` | Use case orchestration |
| `AbpController` | REST API controller |
ABP base classes already inject commonly used services as properties. Before injecting a service, check if it's already available:
| Property | Available In | Description |
|----------|--------------|-------------|
| `GuidGenerator` | All base classes | Generate GUIDs |
| `Clock` | All base classes | Current time (use instead of `DateTime`) |
| `CurrentUser` | All base classes | Authenticated user info |
| `CurrentTenant` | All base classes | Multi-tenancy context |
| `L` (StringLocalizer) | `ApplicationService`, `AbpController` | Localization |
| `AuthorizationService` | `ApplicationService`, `AbpController` | Permission checks |
| `FeatureChecker` | `ApplicationService`, `AbpController` | Feature availability |
| `DataFilter` | All base classes | Data filtering (soft-delete, tenant) |
| `UnitOfWorkManager` | `ApplicationService`, `DomainService` | Unit of work management |
| `LoggerFactory` | All base classes | Create loggers |
| `Logger` | All base classes | Logging (auto-created) |
| `LazyServiceProvider` | All base classes | Lazy service resolution |
**Useful methods from base classes:**
- `CheckPolicyAsync()` - Check permission and throw if not granted
- `IsGrantedAsync()` - Check permission without throwing
## Async Best Practices
- Use async all the way - never use `.Result` or `.Wait()`
- All async methods should end with `Async` suffix
- ABP automatically handles `CancellationToken` in most cases (e.g., from `HttpContext.RequestAborted`)
- Only pass `CancellationToken` explicitly when implementing custom cancellation logic
## Time Handling
Never use `DateTime.Now` or `DateTime.UtcNow` directly. Use ABP's `IClock` service:
```csharp
// In classes inheriting from base classes (ApplicationService, DomainService, etc.)
public class BookAppService : ApplicationService
{
public void DoSomething()
{
var now = Clock.Now; // ✅ Already available as property
}
}
// In other services - inject IClock
public class MyService : ITransientDependency
{
private readonly IClock _clock;
public MyService(IClock clock) => _clock = clock;
public void DoSomething()
{
var now = _clock.Now; // ✅ Correct
// var now = DateTime.Now; // ❌ Wrong - not testable, ignores timezone settings
}
}
```
> **Tip**: Before injecting a service, check if it's already available as a property in your base classes.
## Business Exceptions
Use `BusinessException` for domain rule violations with namespaced error codes:
```csharp
throw new BusinessException("MyModule:BookNameAlreadyExists")
.WithData("Name", bookName);
```
Configure localization mapping:
```csharp
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("MyModule", typeof(MyModuleResource));
});
```
## Localization
- In base classes (`ApplicationService`, `AbpController`, etc.): Use `L["Key"]` - this is the `IStringLocalizer` property
- In other services: Inject `IStringLocalizer<TResource>`
- Always localize user-facing messages and exceptions
**Localization file location**: `*.Domain.Shared/Localization/{ResourceName}/{lang}.json`
```json
// Example: MyProject.Domain.Shared/Localization/MyProject/en.json
{
"culture": "en",
"texts": {
"Menu:Home": "Home",
"Welcome": "Welcome",
"BookName": "Book Name"
}
}
```
## ❌ Never Use (ABP Anti-Patterns)
| Don't Use | Use Instead |
|-----------|-------------|
| Minimal APIs | ABP Controllers or Auto API Controllers |
| MediatR | Application Services |
| `DbContext` directly in App Services | `IRepository<T>` |
| `AddScoped/AddTransient/AddSingleton` | `ITransientDependency`, `ISingletonDependency` |
| `DateTime.Now` | `IClock` / `Clock.Now` |
| Custom UnitOfWork | ABP's `IUnitOfWorkManager` |
| Manual HTTP calls from UI | ABP client proxies (`generate-proxy`) |
| Hardcoded role checks | Permission-based authorization |
| Business logic in Controllers | Application Services |

236
ai-rules/common/application-layer.mdc

@ -0,0 +1,236 @@
---
description: "ABP Application Services, DTOs, validation, and error handling patterns"
globs:
- "**/*.Application/**/*.cs"
- "**/Application/**/*.cs"
- "**/*AppService*.cs"
- "**/*Dto*.cs"
alwaysApply: false
---
# ABP Application Layer Patterns
> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services
## Application Service Structure
### Interface (Application.Contracts)
```csharp
public interface IBookAppService : IApplicationService
{
Task<BookDto> GetAsync(Guid id);
Task<PagedResultDto<BookListItemDto>> GetListAsync(GetBookListInput input);
Task<BookDto> CreateAsync(CreateBookDto input);
Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input);
Task DeleteAsync(Guid id);
}
```
### Implementation (Application)
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IBookRepository _bookRepository;
private readonly BookManager _bookManager;
private readonly BookMapper _bookMapper;
public BookAppService(
IBookRepository bookRepository,
BookManager bookManager,
BookMapper bookMapper)
{
_bookRepository = bookRepository;
_bookManager = bookManager;
_bookMapper = bookMapper;
}
public async Task<BookDto> GetAsync(Guid id)
{
var book = await _bookRepository.GetAsync(id);
return _bookMapper.MapToDto(book);
}
[Authorize(BookStorePermissions.Books.Create)]
public async Task<BookDto> CreateAsync(CreateBookDto input)
{
var book = await _bookManager.CreateAsync(input.Name, input.Price);
await _bookRepository.InsertAsync(book);
return _bookMapper.MapToDto(book);
}
[Authorize(BookStorePermissions.Books.Edit)]
public async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await _bookRepository.GetAsync(id);
await _bookManager.ChangeNameAsync(book, input.Name);
book.SetPrice(input.Price);
await _bookRepository.UpdateAsync(book);
return _bookMapper.MapToDto(book);
}
}
```
## Application Service Best Practices
- Don't repeat entity name in method names (`GetAsync` not `GetBookAsync`)
- Accept/return DTOs only, never entities
- ID not inside UpdateDto - pass separately
- Use custom repositories when you need custom queries, generic repository is fine for simple CRUD
- Call `UpdateAsync` explicitly (don't assume change tracking)
- Don't call other app services in same module
- Don't use `IFormFile`/`Stream` - pass `byte[]` from controllers
- Use base class properties (`Clock`, `CurrentUser`, `GuidGenerator`, `L`) instead of injecting these services
## DTO Naming Conventions
| Purpose | Convention | Example |
|---------|------------|---------|
| Query input | `Get{Entity}Input` | `GetBookInput` |
| List query input | `Get{Entity}ListInput` | `GetBookListInput` |
| Create input | `Create{Entity}Dto` | `CreateBookDto` |
| Update input | `Update{Entity}Dto` | `UpdateBookDto` |
| Single entity output | `{Entity}Dto` | `BookDto` |
| List item output | `{Entity}ListItemDto` | `BookListItemDto` |
## DTO Location
- Define DTOs in `*.Application.Contracts` project
- This allows sharing with clients (Blazor, HttpApi.Client)
## Validation
### Data Annotations
```csharp
public class CreateBookDto
{
[Required]
[StringLength(100, MinimumLength = 3)]
public string Name { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
}
```
### Custom Validation with IValidatableObject
Before adding custom validation, decide if it's a **domain rule** or **application rule**:
- **Domain rule**: Put validation in entity constructor or domain service (enforces business invariants)
- **Application rule**: Use DTO validation (input format, required fields)
Only use `IValidatableObject` for application-level validation that can't be expressed with data annotations:
```csharp
public class CreateBookDto : IValidatableObject
{
public string Name { get; set; }
public string Description { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Name == Description)
{
yield return new ValidationResult(
"Name and Description cannot be the same!",
new[] { nameof(Name), nameof(Description) }
);
}
}
}
```
### FluentValidation
```csharp
public class CreateBookDtoValidator : AbstractValidator<CreateBookDto>
{
public CreateBookDtoValidator()
{
RuleFor(x => x.Name).NotEmpty().Length(3, 100);
RuleFor(x => x.Price).GreaterThan(0);
}
}
```
## Error Handling
### Business Exceptions
```csharp
throw new BusinessException("BookStore:010001")
.WithData("BookName", name);
```
### Entity Not Found
```csharp
var book = await _bookRepository.FindAsync(id);
if (book == null)
{
throw new EntityNotFoundException(typeof(Book), id);
}
```
### User-Friendly Exceptions
```csharp
throw new UserFriendlyException(L["BookNotAvailable"]);
```
### HTTP Status Code Mapping
Status code mapping is **configurable** in ABP (do not rely on a fixed mapping in business logic).
| Exception | Typical HTTP Status |
|-----------|-------------|
| `AbpValidationException` | 400 |
| `AbpAuthorizationException` | 401/403 |
| `EntityNotFoundException` | 404 |
| `BusinessException` | 403 (but configurable) |
| Other exceptions | 500 |
## Auto API Controllers
ABP automatically generates API controllers for application services:
- Interface must inherit `IApplicationService` (which already has `[RemoteService]` attribute)
- HTTP methods determined by method name prefix (Get, Create, Update, Delete)
- Use `[RemoteService(false)]` to disable auto API generation for specific methods
## Object Mapping (Mapperly / AutoMapper)
ABP supports **both Mapperly and AutoMapper** integrations. But the default mapping library is Mapperly. You need to first check the project's active mapping library.
- Prefer the mapping provider already used in the solution (check existing mapping files / loaded modules).
- In mixed solutions, explicitly setting the default provider may be required (see `docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md`).
### Mapperly (compile-time)
Define mappers as partial classes:
```csharp
[Mapper]
public partial class BookMapper
{
public partial BookDto MapToDto(Book book);
public partial List<BookDto> MapToDtoList(List<Book> books);
}
```
Register in module:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<BookMapper>();
}
```
Usage in application service:
```csharp
public class BookAppService : ApplicationService
{
private readonly BookMapper _bookMapper;
public BookAppService(BookMapper bookMapper)
{
_bookMapper = bookMapper;
}
public BookDto GetBook(Book book)
{
return _bookMapper.MapToDto(book);
}
}
```
> **Note**: Mapperly generates mapping code at compile-time, providing better performance than runtime mappers.
### AutoMapper (runtime)
If the solution uses AutoMapper, mappings are typically defined in `Profile` classes and registered via ABP's AutoMapper integration.

186
ai-rules/common/authorization.mdc

@ -0,0 +1,186 @@
---
description: "ABP permission system and authorization patterns"
globs:
- "**/*Permission*.cs"
- "**/*AppService*.cs"
- "**/*Controller*.cs"
alwaysApply: false
---
# ABP Authorization
> **Docs**: https://abp.io/docs/latest/framework/fundamentals/authorization
## Permission Definition
Define permissions in `*.Application.Contracts` project:
```csharp
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
```
Register in provider:
```csharp
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
var booksPermission = bookStoreGroup.AddPermission(
BookStorePermissions.Books.Default,
L("Permission:Books"));
booksPermission.AddChild(
BookStorePermissions.Books.Create,
L("Permission:Books.Create"));
booksPermission.AddChild(
BookStorePermissions.Books.Edit,
L("Permission:Books.Edit"));
booksPermission.AddChild(
BookStorePermissions.Books.Delete,
L("Permission:Books.Delete"));
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
}
```
## Using Permissions
### Declarative (Attribute)
```csharp
[Authorize(BookStorePermissions.Books.Create)]
public virtual async Task<BookDto> CreateAsync(CreateBookDto input)
{
// Only users with Books.Create permission can execute
}
```
### Programmatic Check
```csharp
public class BookAppService : ApplicationService
{
public async Task DoSomethingAsync()
{
// Check and throw if not granted
await CheckPolicyAsync(BookStorePermissions.Books.Edit);
// Or check without throwing
if (await IsGrantedAsync(BookStorePermissions.Books.Delete))
{
// Has permission
}
}
}
```
### Allow Anonymous Access
```csharp
[AllowAnonymous]
public virtual async Task<BookDto> GetPublicBookAsync(Guid id)
{
// No authentication required
}
```
## Current User
Access authenticated user info via `CurrentUser` property (available in base classes like `ApplicationService`, `DomainService`, `AbpController`):
```csharp
public class BookAppService : ApplicationService
{
public async Task DoSomethingAsync()
{
// CurrentUser is available from base class - no injection needed
var userId = CurrentUser.Id;
var userName = CurrentUser.UserName;
var email = CurrentUser.Email;
var isAuthenticated = CurrentUser.IsAuthenticated;
var roles = CurrentUser.Roles;
var tenantId = CurrentUser.TenantId;
}
}
// In other services, inject ICurrentUser
public class MyService : ITransientDependency
{
private readonly ICurrentUser _currentUser;
public MyService(ICurrentUser currentUser) => _currentUser = currentUser;
}
```
### Ownership Validation
```csharp
public async Task UpdateMyBookAsync(Guid bookId, UpdateBookDto input)
{
var book = await _bookRepository.GetAsync(bookId);
if (book.CreatorId != CurrentUser.Id)
{
throw new AbpAuthorizationException();
}
// Update book...
}
```
## Multi-Tenancy Permissions
Control permission availability per tenant side:
```csharp
bookStoreGroup.AddPermission(
BookStorePermissions.Books.Default,
L("Permission:Books"),
multiTenancySide: MultiTenancySides.Tenant // Only for tenants
);
```
Options: `MultiTenancySides.Host`, `Tenant`, or `Both`
## Feature-Dependent Permissions
```csharp
booksPermission.RequireFeatures("BookStore.PremiumFeature");
```
## Permission Management
Grant/revoke permissions programmatically:
```csharp
public class MyService : ITransientDependency
{
private readonly IPermissionManager _permissionManager;
public async Task GrantPermissionToUserAsync(Guid userId, string permissionName)
{
await _permissionManager.SetForUserAsync(userId, permissionName, true);
}
public async Task GrantPermissionToRoleAsync(string roleName, string permissionName)
{
await _permissionManager.SetForRoleAsync(roleName, permissionName, true);
}
}
```
## Security Best Practices
- Never trust client input for user identity
- Use `CurrentUser` property (from base class) or inject `ICurrentUser`
- Validate ownership in application service methods
- Filter queries by current user when appropriate
- Don't expose sensitive fields in DTOs

92
ai-rules/common/cli-commands.mdc

@ -0,0 +1,92 @@
---
description: "ABP CLI commands: generate-proxy, install-libs, add-package-ref, new-module, install-module, update, clean, suite generate (CRUD pages)"
globs:
- "**/*.csproj"
- "**/appsettings*.json"
alwaysApply: false
---
# ABP CLI Commands
> **Full documentation**: https://abp.io/docs/latest/cli
> Use `abp help [command]` for detailed options.
## Generate Client Proxies
```bash
# URL flag: `-u` (short) or `--url` (long). Use whichever your team prefers, but keep it consistent.
#
# Angular (host must be running)
abp generate-proxy -t ng
# C# client proxies
abp generate-proxy -t csharp -u https://localhost:44300
# Integration services only (microservices)
abp generate-proxy -t csharp -u https://localhost:44300 -st integration
# JavaScript
abp generate-proxy -t js -u https://localhost:44300
```
## Install Client-Side Libraries
```bash
# Install NPM packages for MVC/Blazor Server
abp install-libs
```
## Add Package Reference
```bash
# Add project reference with module dependency
abp add-package-ref Acme.BookStore.Domain
abp add-package-ref Acme.BookStore.Domain -t Acme.BookStore.Application
```
## Module Operations
```bash
# Create new module in solution
abp new-module Acme.OrderManagement -t module:ddd
# Install published module
abp install-module Volo.Blogging
# Add ABP NuGet package
abp add-package Volo.Abp.Caching.StackExchangeRedis
```
## Update & Clean
```bash
abp update # Update all ABP packages
abp update --version 8.0.0 # Specific version
abp clean # Delete bin/obj folders
```
## ABP Suite (CRUD Generation)
Generate CRUD pages from entity JSON (created via Suite UI):
```bash
abp suite generate --entity .suite/entities/Book.json --solution ./Acme.BookStore.sln
```
> **Note**: Entity JSON files are created when you generate an entity via ABP Suite UI. They are stored in `.suite/entities/` folder.
> **Suite docs**: https://abp.io/docs/latest/suite
## Quick Reference
| Task | Command |
|------|---------|
| Angular proxies | `abp generate-proxy -t ng` |
| C# proxies | `abp generate-proxy -t csharp -u URL` |
| Install JS libs | `abp install-libs` |
| Add reference | `abp add-package-ref PackageName` |
| Create module | `abp new-module ModuleName` |
| Install module | `abp install-module ModuleName` |
| Update packages | `abp update` |
| Clean solution | `abp clean` |
| Suite CRUD | `abp suite generate -e entity.json -s solution.sln` |
| Get help | `abp help [command]` |

244
ai-rules/common/ddd-patterns.mdc

@ -0,0 +1,244 @@
---
description: "ABP DDD patterns - Entities, Aggregate Roots, Repositories, Domain Services"
globs:
- "**/*.Domain/**/*.cs"
- "**/Domain/**/*.cs"
- "**/Entities/**/*.cs"
alwaysApply: false
---
# ABP DDD Patterns
> **Docs**: https://abp.io/docs/latest/framework/architecture/domain-driven-design
## Rich Domain Model vs Anemic Domain Model
ABP promotes **Rich Domain Model** pattern where entities contain both data AND behavior:
| Anemic (Anti-pattern) | Rich (Recommended) |
|----------------------|-------------------|
| Entity = data only | Entity = data + behavior |
| Logic in services | Logic in entity methods |
| Public setters | Private setters with methods |
| No validation in entity | Entity enforces invariants |
**Encapsulation is key**: Protect entity state by using private setters and exposing behavior through methods.
## Entities
### Entity Example (Rich Model)
```csharp
public class OrderLine : Entity<Guid>
{
public Guid ProductId { get; private set; }
public int Count { get; private set; }
public decimal Price { get; private set; }
protected OrderLine() { } // For ORM
internal OrderLine(Guid id, Guid productId, int count, decimal price) : base(id)
{
ProductId = productId;
SetCount(count); // Validates through method
Price = price;
}
public void SetCount(int count)
{
if (count <= 0)
throw new BusinessException("Orders:InvalidCount");
Count = count;
}
}
```
## Aggregate Roots
Aggregate roots are consistency boundaries that:
- Own their child entities
- Enforce business rules
- Publish domain events
```csharp
public class Order : AggregateRoot<Guid>
{
public string OrderNumber { get; private set; }
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public ICollection<OrderLine> Lines { get; private set; }
protected Order() { } // For ORM
public Order(Guid id, string orderNumber, Guid customerId) : base(id)
{
OrderNumber = Check.NotNullOrWhiteSpace(orderNumber, nameof(orderNumber));
CustomerId = customerId;
Status = OrderStatus.Created;
Lines = new List<OrderLine>();
}
public void AddLine(Guid lineId, Guid productId, int count, decimal price)
{
// Business rule: Can only add lines to created orders
if (Status != OrderStatus.Created)
throw new BusinessException("Orders:CannotModifyOrder");
Lines.Add(new OrderLine(lineId, productId, count, price));
}
public void Complete()
{
if (Status != OrderStatus.Created)
throw new BusinessException("Orders:CannotCompleteOrder");
Status = OrderStatus.Completed;
// Publish events for side effects
AddLocalEvent(new OrderCompletedEvent(Id)); // Same transaction
AddDistributedEvent(new OrderCompletedEto { OrderId = Id }); // Cross-service
}
}
```
### Domain Events
- `AddLocalEvent()` - Handled within same transaction, can access full entity
- `AddDistributedEvent()` - Handled asynchronously, use ETOs (Event Transfer Objects)
### Entity Best Practices
- **Encapsulation**: Private setters, public methods that enforce rules
- **Primary constructor**: Enforce invariants, accept `id` parameter
- **Protected parameterless constructor**: Required for ORM
- **Initialize collections**: In primary constructor
- **Virtual members**: For ORM proxy compatibility
- **Reference by Id**: Don't add navigation properties to other aggregates
- **Don't generate GUID in constructor**: Use `IGuidGenerator` externally
## Repository Pattern
### When to Use Custom Repository
- **Generic repository** (`IRepository<T, TKey>`): Sufficient for simple CRUD operations
- **Custom repository**: Only when you need custom query methods
### Interface (Domain Layer)
```csharp
// Define custom interface only when custom queries are needed
public interface IOrderRepository : IRepository<Order, Guid>
{
Task<Order> FindByOrderNumberAsync(string orderNumber, bool includeDetails = false);
Task<List<Order>> GetListByCustomerAsync(Guid customerId, bool includeDetails = false);
}
```
### Repository Best Practices
- **One repository per aggregate root only** - Never create repositories for child entities
- Child entities must be accessed/modified only through their aggregate root
- Creating repositories for child entities breaks data consistency (bypasses aggregate root's business rules)
- In ABP, use `AddDefaultRepositories()` without `includeAllEntities: true` to enforce this
- Define custom repository only when custom queries are needed
- ABP handles `CancellationToken` automatically; add parameter only for explicit cancellation control
- Single entity methods: `includeDetails = true` by default
- List methods: `includeDetails = false` by default
- Don't return projection classes
- Interface in Domain, implementation in data layer
```csharp
// ✅ Correct: Repository for aggregate root (Order)
public interface IOrderRepository : IRepository<Order, Guid> { }
// ❌ Wrong: Repository for child entity (OrderLine)
// OrderLine should only be accessed through Order aggregate
public interface IOrderLineRepository : IRepository<OrderLine, Guid> { } // Don't do this!
```
## Domain Services
Use domain services for business logic that:
- Spans multiple aggregates
- Requires repository queries to enforce rules
```csharp
public class OrderManager : DomainService
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
public OrderManager(
IOrderRepository orderRepository,
IProductRepository productRepository)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
}
public async Task<Order> CreateAsync(string orderNumber, Guid customerId)
{
// Business rule: Order number must be unique
var existing = await _orderRepository.FindByOrderNumberAsync(orderNumber);
if (existing != null)
{
throw new BusinessException("Orders:OrderNumberAlreadyExists")
.WithData("OrderNumber", orderNumber);
}
return new Order(GuidGenerator.Create(), orderNumber, customerId);
}
public async Task AddProductAsync(Order order, Guid productId, int count)
{
var product = await _productRepository.GetAsync(productId);
order.AddLine(productId, count, product.Price);
}
}
```
### Domain Service Best Practices
- Use `*Manager` suffix naming
- No interface by default (create only if needed)
- Accept/return domain objects, not DTOs
- Don't depend on authenticated user - pass values from application layer
- Use base class properties (`GuidGenerator`, `Clock`) instead of injecting these services
## Domain Events
### Local Events
```csharp
// In aggregate
AddLocalEvent(new OrderCompletedEvent(Id));
// Handler
public class OrderCompletedEventHandler : ILocalEventHandler<OrderCompletedEvent>, ITransientDependency
{
public async Task HandleEventAsync(OrderCompletedEvent eventData)
{
// Handle within same transaction
}
}
```
### Distributed Events (ETO)
For inter-module/microservice communication:
```csharp
// In Domain.Shared
[EventName("Orders.OrderCompleted")]
public class OrderCompletedEto
{
public Guid OrderId { get; set; }
public string OrderNumber { get; set; }
}
```
## Specifications
Reusable query conditions:
```csharp
public class CompletedOrdersSpec : Specification<Order>
{
public override Expression<Func<Order, bool>> ToExpression()
{
return o => o.Status == OrderStatus.Completed;
}
}
// Usage
var orders = await _orderRepository.GetListAsync(new CompletedOrdersSpec());
```

153
ai-rules/common/dependency-rules.mdc

@ -0,0 +1,153 @@
---
description: "ABP layer dependency rules and project structure guardrails"
globs:
- "**/*.csproj"
- "**/*Module*.cs"
alwaysApply: false
---
# ABP Dependency Rules
## Core Principles (All Templates)
These principles apply regardless of solution structure:
1. **Domain logic never depends on infrastructure** (no DbContext in domain/application)
2. **Use abstractions** (interfaces) for dependencies
3. **Higher layers depend on lower layers**, never the reverse
4. **Data access through repositories**, not direct DbContext
## Layered Template Structure
> **Note**: This section applies to layered templates (app, module). Single-layer and microservice templates have different structures.
```
Domain.Shared → Constants, enums, localization keys
Domain → Entities, repository interfaces, domain services
Application.Contracts → App service interfaces, DTOs
Application → App service implementations
HttpApi → REST controllers (optional)
Host → Final application with DI and middleware
```
### Layered Dependency Direction
| Project | Can Reference | Referenced By |
|---------|---------------|---------------|
| Domain.Shared | Nothing | All |
| Domain | Domain.Shared | Application, Data layer |
| Application.Contracts | Domain.Shared | Application, HttpApi, Clients |
| Application | Domain, Contracts | Host |
| EntityFrameworkCore/MongoDB | Domain | Host only |
| HttpApi | Contracts only | Host |
## Critical Rules
### ❌ Never Do
```csharp
// Application layer accessing DbContext directly
public class BookAppService : ApplicationService
{
private readonly MyDbContext _dbContext; // ❌ WRONG
}
// Domain depending on application layer
public class BookManager : DomainService
{
private readonly IBookAppService _appService; // ❌ WRONG
}
// HttpApi depending on Application implementation
public class BookController : AbpController
{
private readonly BookAppService _bookAppService; // ❌ WRONG - Use interface
}
```
### ✅ Always Do
```csharp
// Application layer using repository abstraction
public class BookAppService : ApplicationService
{
private readonly IBookRepository _bookRepository; // ✅ CORRECT
}
// Domain service using domain abstractions
public class BookManager : DomainService
{
private readonly IBookRepository _bookRepository; // ✅ CORRECT
}
// HttpApi depending on contracts only
public class BookController : AbpController
{
private readonly IBookAppService _bookAppService; // ✅ CORRECT
}
```
## Repository Pattern Enforcement
### Interface Location
```csharp
// In Domain project
public interface IBookRepository : IRepository<Book, Guid>
{
Task<Book> FindByNameAsync(string name);
}
```
### Implementation Location
```csharp
// In EntityFrameworkCore project
public class BookRepository : EfCoreRepository<MyDbContext, Book, Guid>, IBookRepository
{
// Implementation
}
// In MongoDB project
public class BookRepository : MongoDbRepository<MyDbContext, Book, Guid>, IBookRepository
{
// Implementation
}
```
## Multi-Application Scenarios
When you have multiple applications (e.g., Admin + Public API):
### Vertical Separation
```
MyProject.Admin.Application - Admin-specific services
MyProject.Public.Application - Public-specific services
MyProject.Domain - Shared domain (both reference this)
```
### Rules
- Admin and Public application layers **MUST NOT** reference each other
- Share domain logic, not application logic
- Each vertical can have its own DTOs even if similar
## Enforcement Checklist (Layered Templates)
When adding a new feature:
1. **Entity changes?** → Domain project
2. **Constants/enums?** → Domain.Shared project
3. **Repository interface?** → Domain project (only if custom queries needed)
4. **Repository implementation?** → EntityFrameworkCore/MongoDB project
5. **DTOs and service interface?** → Application.Contracts project
6. **Service implementation?** → Application project
7. **API endpoint?** → HttpApi project (if not using auto API controllers)
## Common Violations to Watch
| Violation | Impact | Fix |
|-----------|--------|-----|
| DbContext in Application | Breaks DB independence | Use repository |
| Entity in DTO | Exposes internals | Map to DTO |
| IQueryable in interface | Breaks abstraction | Return concrete types |
| Cross-module app service call | Tight coupling | Use events or domain |

299
ai-rules/common/development-flow.mdc

@ -0,0 +1,299 @@
---
description: "ABP development workflow - adding features, entities, and migrations"
globs:
- "**/*AppService*.cs"
- "**/*Application*/**/*.cs"
- "**/*Application.Contracts*/**/*.cs"
- "**/*Dto*.cs"
- "**/*DbContext*.cs"
- "**/*.EntityFrameworkCore/**/*.cs"
- "**/*.MongoDB/**/*.cs"
- "**/*Permission*.cs"
alwaysApply: false
---
# ABP Development Workflow
> **Tutorials**: https://abp.io/docs/latest/tutorials
## Adding a New Entity (Full Flow)
### 1. Domain Layer
Create entity (location varies by template: `*.Domain/Entities/` for layered, `Entities/` for single-layer/microservice):
```csharp
public class Book : AggregateRoot<Guid>
{
public string Name { get; private set; }
public decimal Price { get; private set; }
public Guid AuthorId { get; private set; }
protected Book() { }
public Book(Guid id, string name, decimal price, Guid authorId) : base(id)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
SetPrice(price);
AuthorId = authorId;
}
public void SetPrice(decimal price)
{
Price = Check.Range(price, nameof(price), 0, 9999);
}
}
```
### 2. Domain.Shared
Add constants and enums in `*.Domain.Shared/`:
```csharp
public static class BookConsts
{
public const int MaxNameLength = 128;
}
public enum BookType
{
Novel,
Science,
Biography
}
```
### 3. Repository Interface (Optional)
Define custom repository in `*.Domain/` only if you need custom query methods. For simple CRUD, use generic `IRepository<Book, Guid>` directly:
```csharp
// Only if custom queries are needed
public interface IBookRepository : IRepository<Book, Guid>
{
Task<Book> FindByNameAsync(string name);
}
```
### 4. EF Core Configuration
In `*.EntityFrameworkCore/`:
**DbContext:**
```csharp
public DbSet<Book> Books { get; set; }
```
**OnModelCreating:**
```csharp
builder.Entity<Book>(b =>
{
b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(BookConsts.MaxNameLength);
b.HasIndex(x => x.Name);
});
```
**Repository Implementation (only if custom interface defined):**
```csharp
public class BookRepository : EfCoreRepository<MyDbContext, Book, Guid>, IBookRepository
{
public BookRepository(IDbContextProvider<MyDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Book> FindByNameAsync(string name)
{
return await (await GetDbSetAsync())
.FirstOrDefaultAsync(b => b.Name == name);
}
}
```
### 5. Run Migration
```bash
cd src/MyProject.EntityFrameworkCore
# Add migration
dotnet ef migrations add Added_Book
# Apply migration (choose one):
dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data
# OR
dotnet ef database update # EF Core command only
```
### 6. Application.Contracts
Create DTOs and service interface:
```csharp
// DTOs
public class BookDto : EntityDto<Guid>
{
public string Name { get; set; }
public decimal Price { get; set; }
public Guid AuthorId { get; set; }
}
public class CreateBookDto
{
[Required]
[StringLength(BookConsts.MaxNameLength)]
public string Name { get; set; }
[Range(0, 9999)]
public decimal Price { get; set; }
[Required]
public Guid AuthorId { get; set; }
}
// Service Interface
public interface IBookAppService : IApplicationService
{
Task<BookDto> GetAsync(Guid id);
Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input);
Task<BookDto> CreateAsync(CreateBookDto input);
}
```
### 7. Object Mapping (Mapperly / AutoMapper)
ABP supports both Mapperly and AutoMapper. Prefer the provider already used in the solution.
If the solution uses **Mapperly**, create a mapper in the Application project:
```csharp
[Mapper]
public partial class BookMapper
{
public partial BookDto MapToDto(Book book);
public partial List<BookDto> MapToDtoList(List<Book> books);
}
```
Register in module:
```csharp
context.Services.AddSingleton<BookMapper>();
```
### 8. Application Service
Implement service (using generic repository - use `IBookRepository` if you defined custom interface in step 3):
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _bookRepository; // Or IBookRepository
private readonly BookMapper _bookMapper;
public BookAppService(
IRepository<Book, Guid> bookRepository,
BookMapper bookMapper)
{
_bookRepository = bookRepository;
_bookMapper = bookMapper;
}
public async Task<BookDto> GetAsync(Guid id)
{
var book = await _bookRepository.GetAsync(id);
return _bookMapper.MapToDto(book);
}
[Authorize(MyProjectPermissions.Books.Create)]
public async Task<BookDto> CreateAsync(CreateBookDto input)
{
var book = new Book(
GuidGenerator.Create(),
input.Name,
input.Price,
input.AuthorId
);
await _bookRepository.InsertAsync(book);
return _bookMapper.MapToDto(book);
}
}
```
### 9. Add Localization
In `*.Domain.Shared/Localization/*/en.json`:
```json
{
"Book": "Book",
"Books": "Books",
"BookName": "Name",
"BookPrice": "Price"
}
```
### 10. Add Permissions (if needed)
```csharp
public static class MyProjectPermissions
{
public static class Books
{
public const string Default = "MyProject.Books";
public const string Create = Default + ".Create";
}
}
```
### 11. Add Tests
```csharp
public class BookAppService_Tests : MyProjectApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Create_Book()
{
var result = await _bookAppService.CreateAsync(new CreateBookDto
{
Name = "Test Book",
Price = 19.99m
});
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("Test Book");
}
}
```
## Quick Reference Commands
### Build Solution
```bash
dotnet build
```
### Run Migrations
```bash
cd src/MyProject.EntityFrameworkCore
dotnet ef migrations add MigrationName
dotnet run --project ../MyProject.DbMigrator # Apply migration + seed data
```
### Generate Angular Proxies
```bash
abp generate-proxy -t ng
```
## Checklist for New Features
- [ ] Entity created with proper constructors
- [ ] Constants in Domain.Shared
- [ ] Custom repository interface in Domain (only if custom queries needed)
- [ ] EF Core configuration added
- [ ] Custom repository implementation (only if interface defined)
- [ ] Migration generated and applied (use DbMigrator)
- [ ] Mapperly mapper created and registered
- [ ] DTOs created in Application.Contracts
- [ ] Service interface defined
- [ ] Service implementation with authorization
- [ ] Localization keys added
- [ ] Permissions defined (if applicable)
- [ ] Tests written

249
ai-rules/common/infrastructure.mdc

@ -0,0 +1,249 @@
---
description: "ABP infrastructure services - Settings, Features, Caching, Events, Background Jobs"
globs:
- "**/*Setting*.cs"
- "**/*Feature*.cs"
- "**/*Cache*.cs"
- "**/*Event*.cs"
- "**/*Job*.cs"
alwaysApply: false
---
# ABP Infrastructure Services
> **Docs**: https://abp.io/docs/latest/framework/infrastructure
## Settings
### Define Settings
```csharp
public class MySettingDefinitionProvider : SettingDefinitionProvider
{
public override void Define(ISettingDefinitionContext context)
{
context.Add(
new SettingDefinition("MyApp.MaxItemCount", "10"),
new SettingDefinition("MyApp.EnableFeature", "false"),
new SettingDefinition("MyApp.SecretKey", isEncrypted: true)
);
}
}
```
### Read Settings
```csharp
public class MyService : ITransientDependency
{
private readonly ISettingProvider _settingProvider;
public async Task DoSomethingAsync()
{
var maxCount = await _settingProvider.GetAsync<int>("MyApp.MaxItemCount");
var isEnabled = await _settingProvider.IsTrueAsync("MyApp.EnableFeature");
}
}
```
### Setting Value Providers (Priority Order)
1. User settings (highest)
2. Tenant settings
3. Global settings
4. Configuration (appsettings.json)
5. Default value (lowest)
## Features
### Define Features
```csharp
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature(
"MyApp.PdfReporting",
defaultValue: "false",
valueType: new ToggleStringValueType()
);
myGroup.AddFeature(
"MyApp.MaxProductCount",
defaultValue: "10",
valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000))
);
}
}
```
### Check Features
```csharp
[RequiresFeature("MyApp.PdfReporting")]
public async Task<PdfReportDto> GetPdfReportAsync()
{
// Only executes if feature is enabled
}
// Or programmatically
if (await _featureChecker.IsEnabledAsync("MyApp.PdfReporting"))
{
// Feature is enabled for current tenant
}
var maxCount = await _featureChecker.GetAsync<int>("MyApp.MaxProductCount");
```
## Distributed Caching
### Typed Cache
```csharp
public class BookService : ITransientDependency
{
private readonly IDistributedCache<BookCacheItem> _cache;
private readonly IClock _clock;
public BookService(IDistributedCache<BookCacheItem> cache, IClock clock)
{
_cache = cache;
_clock = clock;
}
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId.ToString(),
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = _clock.Now.AddHours(1)
}
);
}
}
[CacheName("Books")]
public class BookCacheItem
{
public string Name { get; set; }
public decimal Price { get; set; }
}
```
## Event Bus
### Local Events (Same Process)
```csharp
// Event class
public class OrderCreatedEvent
{
public Order Order { get; set; }
}
// Handler
public class OrderCreatedEventHandler : ILocalEventHandler<OrderCreatedEvent>, ITransientDependency
{
public async Task HandleEventAsync(OrderCreatedEvent eventData)
{
// Handle within same transaction
}
}
// Publish
await _localEventBus.PublishAsync(new OrderCreatedEvent { Order = order });
```
### Distributed Events (Cross-Service)
```csharp
// Event Transfer Object (in Domain.Shared)
[EventName("MyApp.Order.Created")]
public class OrderCreatedEto
{
public Guid OrderId { get; set; }
public string OrderNumber { get; set; }
}
// Handler
public class OrderCreatedEtoHandler : IDistributedEventHandler<OrderCreatedEto>, ITransientDependency
{
public async Task HandleEventAsync(OrderCreatedEto eventData)
{
// Handle distributed event
}
}
// Publish
await _distributedEventBus.PublishAsync(new OrderCreatedEto { ... });
```
### When to Use Which
- **Local**: Within same module/bounded context
- **Distributed**: Cross-module or microservice communication
## Background Jobs
### Define Job
```csharp
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
public class EmailSendingJob : AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency
{
private readonly IEmailSender _emailSender;
public EmailSendingJob(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public override async Task ExecuteAsync(EmailSendingArgs args)
{
await _emailSender.SendAsync(args.EmailAddress, args.Subject, args.Body);
}
}
```
### Enqueue Job
```csharp
await _backgroundJobManager.EnqueueAsync(
new EmailSendingArgs
{
EmailAddress = "user@example.com",
Subject = "Hello",
Body = "..."
},
delay: TimeSpan.FromMinutes(5) // Optional delay
);
```
## Localization
### Define Resource
```csharp
[LocalizationResourceName("MyModule")]
public class MyModuleResource { }
```
### JSON Structure
```json
{
"culture": "en",
"texts": {
"HelloWorld": "Hello World!",
"Menu:Books": "Books"
}
}
```
### Usage
- In `ApplicationService`: Use `L["Key"]` property (already available from base class)
- In other services: Inject `IStringLocalizer<MyResource>`
> **Tip**: ABP base classes already provide commonly used services as properties. Check before injecting:
> - `StringLocalizer` (L), `Clock`, `CurrentUser`, `CurrentTenant`, `GuidGenerator`
> - `AuthorizationService`, `FeatureChecker`, `DataFilter`
> - `LoggerFactory`, `Logger`
> - Methods like `CheckPolicyAsync()` for authorization checks

165
ai-rules/common/multi-tenancy.mdc

@ -0,0 +1,165 @@
---
description: "ABP Multi-Tenancy patterns - tenant-aware entities, data isolation, and tenant switching"
globs:
- "**/*Tenant*.cs"
- "**/*MultiTenant*.cs"
- "**/Entities/**/*.cs"
alwaysApply: false
---
# ABP Multi-Tenancy
> **Docs**: https://abp.io/docs/latest/framework/architecture/multi-tenancy
## Making Entities Multi-Tenant
Implement `IMultiTenant` interface to make entities tenant-aware:
```csharp
public class Product : AggregateRoot<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; } // Required by IMultiTenant
public string Name { get; private set; }
public decimal Price { get; private set; }
protected Product() { }
public Product(Guid id, string name, decimal price) : base(id)
{
Name = name;
Price = price;
// TenantId is automatically set from CurrentTenant.Id
}
}
```
**Key points:**
- `TenantId` is **nullable** - `null` means entity belongs to Host
- ABP **automatically filters** queries by current tenant
- ABP **automatically sets** `TenantId` when creating entities
## Accessing Current Tenant
Use `CurrentTenant` property (available in base classes) or inject `ICurrentTenant`:
```csharp
public class ProductAppService : ApplicationService
{
public async Task DoSomethingAsync()
{
// Available from base class
var tenantId = CurrentTenant.Id; // Guid? - null for host
var tenantName = CurrentTenant.Name; // string?
var isAvailable = CurrentTenant.IsAvailable; // true if Id is not null
}
}
// In other services
public class MyService : ITransientDependency
{
private readonly ICurrentTenant _currentTenant;
public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant;
}
```
## Switching Tenant Context
Use `CurrentTenant.Change()` to temporarily switch tenant (useful in host context):
```csharp
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public async Task<long> GetProductCountAsync(Guid? tenantId)
{
// Switch to specific tenant
using (CurrentTenant.Change(tenantId))
{
return await _productRepository.GetCountAsync();
}
// Automatically restored to previous tenant after using block
}
public async Task DoHostOperationAsync()
{
// Switch to host context
using (CurrentTenant.Change(null))
{
// Operations here are in host context
}
}
}
```
> **Important**: Always use `Change()` with a `using` statement.
## Disabling Multi-Tenant Filter
To query all tenants' data (only works with single database):
```csharp
public class ProductManager : DomainService
{
public async Task<long> GetAllProductCountAsync()
{
// DataFilter is available from base class
using (DataFilter.Disable<IMultiTenant>())
{
return await _productRepository.GetCountAsync();
// Returns count from ALL tenants
}
}
}
```
> **Note**: This doesn't work with separate databases per tenant.
## Database Architecture Options
| Approach | Description | Use Case |
|----------|-------------|----------|
| Single Database | All tenants share one database | Simple, cost-effective |
| Database per Tenant | Each tenant has dedicated database | Data isolation, compliance |
| Hybrid | Mix of shared and dedicated | Flexible, premium tenants |
Connection strings are configured per tenant in Tenant Management module.
## Best Practices
1. **Always implement `IMultiTenant`** for tenant-specific entities
2. **Never manually filter by `TenantId`** - ABP does it automatically
3. **Don't change `TenantId` after creation** - it moves entity between tenants
4. **Use `Change()` scope carefully** - nested scopes are supported
5. **Test both host and tenant contexts** - ensure proper data isolation
6. **Consider nullable `TenantId`** - entity may be host-only or shared
## Enabling Multi-Tenancy
```csharp
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true; // Enabled by default in ABP templates
});
```
Check `MultiTenancyConsts.IsEnabled` in your solution for centralized control.
## Tenant Resolution
ABP resolves current tenant from (in order):
1. Current user's claims
2. Query string (`?__tenant=...`)
3. Route (`/{__tenant}/...`)
4. HTTP header (`__tenant`)
5. Cookie (`__tenant`)
6. Domain/subdomain (if configured)
For subdomain-based resolution:
```csharp
Configure<AbpTenantResolveOptions>(options =>
{
options.AddDomainTenantResolver("{0}.mydomain.com");
});
```

257
ai-rules/data/ef-core.mdc

@ -0,0 +1,257 @@
---
description: "ABP Entity Framework Core patterns - DbContext, migrations, repositories"
globs:
- "**/*.EntityFrameworkCore/**/*.cs"
- "**/EntityFrameworkCore/**/*.cs"
- "**/*DbContext*.cs"
alwaysApply: false
---
# ABP Entity Framework Core
> **Docs**: https://abp.io/docs/latest/framework/data/entity-framework-core
## DbContext Configuration
```csharp
[ConnectionStringName("Default")]
public class MyProjectDbContext : AbpDbContext<MyProjectDbContext>
{
public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
public MyProjectDbContext(DbContextOptions<MyProjectDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Configure all entities
builder.ConfigureMyProject();
}
}
```
## Entity Configuration
```csharp
public static class MyProjectDbContextModelCreatingExtensions
{
public static void ConfigureMyProject(this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<Book>(b =>
{
b.ToTable(MyProjectConsts.DbTablePrefix + "Books", MyProjectConsts.DbSchema);
b.ConfigureByConvention(); // ABP conventions (audit, soft-delete, etc.)
// Property configurations
b.Property(x => x.Name)
.IsRequired()
.HasMaxLength(BookConsts.MaxNameLength);
b.Property(x => x.Price)
.HasColumnType("decimal(18,2)");
// Indexes
b.HasIndex(x => x.Name);
// Relationships
b.HasOne<Author>()
.WithMany()
.HasForeignKey(x => x.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
});
}
}
```
## Repository Implementation
```csharp
public class BookRepository : EfCoreRepository<MyProjectDbContext, Book, Guid>, IBookRepository
{
public BookRepository(IDbContextProvider<MyProjectDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Book> FindByNameAsync(
string name,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.IncludeDetails(includeDetails)
.FirstOrDefaultAsync(
b => b.Name == name,
GetCancellationToken(cancellationToken));
}
public async Task<List<Book>> GetListByAuthorAsync(
Guid authorId,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var dbSet = await GetDbSetAsync();
return await dbSet
.IncludeDetails(includeDetails)
.Where(b => b.AuthorId == authorId)
.ToListAsync(GetCancellationToken(cancellationToken));
}
public override async Task<IQueryable<Book>> WithDetailsAsync()
{
return (await GetQueryableAsync())
.Include(b => b.Reviews);
}
}
```
## Extension Method for Include
```csharp
public static class BookEfCoreQueryableExtensions
{
public static IQueryable<Book> IncludeDetails(
this IQueryable<Book> queryable,
bool include = true)
{
if (!include)
{
return queryable;
}
return queryable
.Include(b => b.Reviews);
}
}
```
## Migration Commands
```bash
# Navigate to EF Core project
cd src/MyProject.EntityFrameworkCore
# Add migration
dotnet ef migrations add MigrationName
# Apply migration (choose one):
dotnet run --project ../MyProject.DbMigrator # Recommended - also seeds data
dotnet ef database update # EF Core command only
# Remove last migration (if not applied)
dotnet ef migrations remove
# Generate SQL script
dotnet ef migrations script
```
> **Note**: ABP templates include `IDesignTimeDbContextFactory` in the EF Core project, so `-s` (startup project) parameter is not needed.
## Module Configuration
```csharp
[DependsOn(typeof(AbpEntityFrameworkCoreModule))]
public class MyProjectEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAbpDbContext<MyProjectDbContext>(options =>
{
// Add default repositories for aggregate roots only (DDD best practice)
options.AddDefaultRepositories();
// ⚠️ Avoid includeAllEntities: true - it creates repositories for child entities,
// allowing them to be modified without going through the aggregate root,
// which breaks data consistency
});
Configure<AbpDbContextOptions>(options =>
{
options.UseSqlServer(); // or UseNpgsql(), UseMySql(), etc.
});
}
}
```
## Best Practices
### Repositories for Aggregate Roots Only
Don't use `includeAllEntities: true` in `AddDefaultRepositories()`. This creates repositories for child entities, allowing direct modification without going through the aggregate root - breaking DDD data consistency rules.
```csharp
// ✅ Correct - Only aggregate roots get repositories
options.AddDefaultRepositories();
// ❌ Avoid - Creates repositories for ALL entities including child entities
options.AddDefaultRepositories(includeAllEntities: true);
```
### Always Call ConfigureByConvention
```csharp
builder.Entity<MyEntity>(b =>
{
b.ConfigureByConvention(); // Don't forget this!
// Other configurations...
});
```
### Use Table Prefix
```csharp
public static class MyProjectConsts
{
public const string DbTablePrefix = "App";
public const string DbSchema = null; // Or "myschema"
}
```
### Performance Tips
- Add explicit indexes for frequently queried fields
- Use `AsNoTracking()` for read-only queries
- Avoid N+1 queries with `.Include()` or specifications
- ABP handles cancellation automatically; use `GetCancellationToken(cancellationToken)` only in custom repository methods
- Consider query splitting for complex queries with multiple collections
### Accessing Raw DbContext
```csharp
public async Task CustomOperationAsync()
{
var dbContext = await GetDbContextAsync();
// Raw SQL
await dbContext.Database.ExecuteSqlRawAsync(
"UPDATE Books SET IsPublished = 1 WHERE AuthorId = {0}",
authorId
);
}
```
## Data Seeding
```csharp
public class MyProjectDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() > 0)
{
return;
}
await _bookRepository.InsertAsync(
new Book(_guidGenerator.Create(), "Sample Book", 19.99m, Guid.Empty),
autoSave: true
);
}
}
```

206
ai-rules/data/mongodb.mdc

@ -0,0 +1,206 @@
---
description: "ABP MongoDB patterns - MongoDbContext and repositories"
globs:
- "**/*.MongoDB/**/*.cs"
- "**/MongoDB/**/*.cs"
- "**/*MongoDb*.cs"
alwaysApply: false
---
# ABP MongoDB
> **Docs**: https://abp.io/docs/latest/framework/data/mongodb
## MongoDbContext Configuration
```csharp
[ConnectionStringName("Default")]
public class MyProjectMongoDbContext : AbpMongoDbContext
{
public IMongoCollection<Book> Books => Collection<Book>();
public IMongoCollection<Author> Authors => Collection<Author>();
protected override void CreateModel(IMongoModelBuilder modelBuilder)
{
base.CreateModel(modelBuilder);
modelBuilder.ConfigureMyProject();
}
}
```
## Entity Configuration
```csharp
public static class MyProjectMongoDbContextExtensions
{
public static void ConfigureMyProject(this IMongoModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<Book>(b =>
{
b.CollectionName = MyProjectConsts.DbTablePrefix + "Books";
});
builder.Entity<Author>(b =>
{
b.CollectionName = MyProjectConsts.DbTablePrefix + "Authors";
});
}
}
```
## Repository Implementation
```csharp
public class BookRepository : MongoDbRepository<MyProjectMongoDbContext, Book, Guid>, IBookRepository
{
public BookRepository(IMongoDbContextProvider<MyProjectMongoDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task<Book> FindByNameAsync(
string name,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.FirstOrDefaultAsync(
b => b.Name == name,
GetCancellationToken(cancellationToken));
}
public async Task<List<Book>> GetListByAuthorAsync(
Guid authorId,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
return await (await GetQueryableAsync())
.Where(b => b.AuthorId == authorId)
.ToListAsync(GetCancellationToken(cancellationToken));
}
}
```
## Module Configuration
```csharp
[DependsOn(typeof(AbpMongoDbModule))]
public class MyProjectMongoDbModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddMongoDbContext<MyProjectMongoDbContext>(options =>
{
// Add default repositories for aggregate roots only (DDD best practice)
options.AddDefaultRepositories();
// ⚠️ Avoid includeAllEntities: true - breaks DDD data consistency
});
}
}
```
## Connection String
In `appsettings.json`:
```json
{
"ConnectionStrings": {
"Default": "mongodb://localhost:27017/MyProjectDb"
}
}
```
## Key Differences from EF Core
### No Migrations
MongoDB is schema-less; no migrations needed. Changes to entity structure are handled automatically.
### includeDetails Parameter
Often ignored in MongoDB because documents typically embed related data:
```csharp
public async Task<List<Book>> GetListAsync(
bool includeDetails = false, // Usually ignored
CancellationToken cancellationToken = default)
{
// MongoDB documents already include nested data
return await (await GetQueryableAsync())
.ToListAsync(GetCancellationToken(cancellationToken));
}
```
### Embedded Documents vs References
```csharp
// Embedded (stored in same document)
public class Order : AggregateRoot<Guid>
{
public List<OrderLine> Lines { get; set; } // Embedded
}
// Reference (separate collection, store ID only)
public class Order : AggregateRoot<Guid>
{
public Guid CustomerId { get; set; } // Reference by ID
}
```
### No Change Tracking
MongoDB doesn't track entity changes automatically:
```csharp
public async Task UpdateBookAsync(Guid id, string newName)
{
var book = await _bookRepository.GetAsync(id);
book.SetName(newName);
// Must explicitly update
await _bookRepository.UpdateAsync(book);
}
```
## Direct Collection Access
```csharp
public async Task CustomOperationAsync()
{
var collection = await GetCollectionAsync();
// Use MongoDB driver directly
var filter = Builders<Book>.Filter.Eq(b => b.AuthorId, authorId);
var update = Builders<Book>.Update.Set(b => b.IsPublished, true);
await collection.UpdateManyAsync(filter, update);
}
```
## Indexing
Configure indexes in repository or via MongoDB driver:
```csharp
public class BookRepository : MongoDbRepository<MyProjectMongoDbContext, Book, Guid>, IBookRepository
{
public override async Task<IQueryable<Book>> GetQueryableAsync()
{
var collection = await GetCollectionAsync();
// Ensure index exists
var indexKeys = Builders<Book>.IndexKeys.Ascending(b => b.Name);
await collection.Indexes.CreateOneAsync(new CreateIndexModel<Book>(indexKeys));
return await base.GetQueryableAsync();
}
}
```
## Best Practices
- Design documents for query patterns (denormalize when needed)
- Use references for frequently changing data
- Use embedding for data that's always accessed together
- Add indexes for frequently queried fields
- Use `GetCancellationToken(cancellationToken)` for proper cancellation
- Remember: ABP data filters (soft-delete, multi-tenancy) work with MongoDB too

83
ai-rules/template-specific/app-nolayers.mdc

@ -0,0 +1,83 @@
---
description: "ABP Single-Layer (No-Layers) application template specific patterns"
globs:
- "**/src/*/*Module.cs"
- "**/src/*/Entities/**/*.cs"
- "**/src/*/Services/**/*.cs"
- "**/src/*/Data/**/*.cs"
alwaysApply: false
---
# ABP Single-Layer Application Template
> **Docs**: https://abp.io/docs/latest/solution-templates/single-layer-web-application
## Solution Structure
Single project containing everything:
```
MyProject/
├── src/
│ └── MyProject/
│ ├── Data/ # DbContext, migrations
│ ├── Entities/ # Domain entities
│ ├── Services/ # Application services + DTOs
│ ├── Pages/ # Razor pages / Blazor components
│ └── MyProjectModule.cs
└── test/
└── MyProject.Tests/
```
## Key Differences from Layered
| Layered Template | Single-Layer Template |
|------------------|----------------------|
| DTOs in Application.Contracts | DTOs in Services folder (same project) |
| Repository interfaces in Domain | Use generic `IRepository<T, TKey>` directly |
| Separate Domain.Shared for constants | Constants in same project |
| Multiple module classes | Single module class |
## File Organization
Group related files by feature:
```
Services/
├── Books/
│ ├── BookAppService.cs
│ ├── BookDto.cs
│ ├── CreateBookDto.cs
│ └── IBookAppService.cs
└── Authors/
├── AuthorAppService.cs
└── ...
```
## Simplified Entity (Still keep invariants)
Single-layer templates are structurally simpler, but you may still have real business invariants.
- For **trivial CRUD** entities, public setters can be acceptable.
- For **non-trivial business rules**, still prefer encapsulation (private setters + methods) to prevent invalid states.
```csharp
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; } // OK for trivial CRUD only
public decimal Price { get; set; }
}
```
## No Custom Repository Needed
Use generic repository directly - no need to define custom interfaces:
```csharp
public class BookAppService : ApplicationService
{
private readonly IRepository<Book, Guid> _bookRepository;
// Generic repository is sufficient for single-layer apps
}
```

209
ai-rules/template-specific/microservice.mdc

@ -0,0 +1,209 @@
---
description: "ABP Microservice solution template specific patterns"
alwaysApply: false
---
# ABP Microservice Solution Template
> **Docs**: https://abp.io/docs/latest/solution-templates/microservice
## Solution Structure
```
MyMicroservice/
├── apps/ # UI applications
│ ├── web/ # Web application
│ ├── public-web/ # Public website
│ └── auth-server/ # Authentication server (OpenIddict)
├── gateways/ # BFF pattern - one gateway per UI
│ └── web-gateway/ # YARP reverse proxy
├── services/ # Microservices
│ ├── administration/ # Permissions, settings, features
│ ├── identity/ # Users, roles
│ └── [your-services]/ # Your business services
└── etc/
├── docker/ # Docker compose for local infra
└── helm/ # Kubernetes deployment
```
## Microservice Structure (NOT Layered!)
Each microservice has simplified structure - everything in one project:
```
services/ordering/
├── OrderingService/ # Main project
│ ├── Entities/
│ ├── Services/
│ ├── IntegrationServices/ # For inter-service communication
│ ├── Data/ # DbContext (implements IHasEventInbox, IHasEventOutbox)
│ └── OrderingServiceModule.cs
├── OrderingService.Contracts/ # Interfaces, DTOs, ETOs (shared)
└── OrderingService.Tests/
```
## Inter-Service Communication
### 1. Integration Services (Synchronous HTTP)
For synchronous calls, use **Integration Services** - NOT regular application services.
#### Step 1: Provider Service - Create Integration Service
```csharp
// In CatalogService.Contracts project
[IntegrationService]
public interface IProductIntegrationService : IApplicationService
{
Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids);
}
// In CatalogService project
[IntegrationService]
public class ProductIntegrationService : ApplicationService, IProductIntegrationService
{
public async Task<List<ProductDto>> GetProductsByIdsAsync(List<Guid> ids)
{
var products = await _productRepository.GetListAsync(p => ids.Contains(p.Id));
return ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
}
}
```
#### Step 2: Provider Service - Expose Integration Services
```csharp
// In CatalogServiceModule.cs
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ExposeIntegrationServices = true;
});
```
#### Step 3: Consumer Service - Add Package Reference
Add reference to provider's Contracts project (via ABP Studio or manually):
- Right-click OrderingService → Add Package Reference → Select `CatalogService.Contracts`
#### Step 4: Consumer Service - Generate Proxies
```bash
# Run ABP CLI in consumer service folder
abp generate-proxy -t csharp -u http://localhost:44361 -m catalog --without-contracts
```
Or use ABP Studio: Right-click service → ABP CLI → Generate Proxy → C#
#### Step 5: Consumer Service - Register HTTP Client Proxies
```csharp
// In OrderingServiceModule.cs
[DependsOn(typeof(CatalogServiceContractsModule))] // Add module dependency
public class OrderingServiceModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Register static HTTP client proxies
context.Services.AddStaticHttpClientProxies(
typeof(CatalogServiceContractsModule).Assembly,
"CatalogService");
}
}
```
#### Step 6: Consumer Service - Configure Remote Service URL
```json
// appsettings.json
"RemoteServices": {
"CatalogService": {
"BaseUrl": "http://localhost:44361"
}
}
```
#### Step 7: Use Integration Service
```csharp
public class OrderAppService : ApplicationService
{
private readonly IProductIntegrationService _productIntegrationService;
public async Task<List<OrderDto>> GetListAsync()
{
var orders = await _orderRepository.GetListAsync();
var productIds = orders.Select(o => o.ProductId).Distinct().ToList();
// Call remote service via generated proxy
var products = await _productIntegrationService.GetProductsByIdsAsync(productIds);
// ...
}
}
```
> **Why Integration Services?** Application services are for UI - they have different authorization, validation, and optimization needs. Integration services are designed specifically for inter-service communication.
**When to use:** Need immediate response, data required to complete current operation (e.g., get product details to display in order list).
### 2. Distributed Events (Asynchronous)
Use RabbitMQ-based events for loose coupling.
**When to use:**
- Notifying other services about state changes (e.g., "order placed", "stock updated")
- Operations that don't need immediate response
- When services should remain independent and decoupled
```csharp
// Define ETO in Contracts project
[EventName("Product.StockChanged")]
public class StockCountChangedEto
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
// Publish
await _distributedEventBus.PublishAsync(new StockCountChangedEto { ... });
// Subscribe in another service
public class StockChangedHandler : IDistributedEventHandler<StockCountChangedEto>, ITransientDependency
{
public async Task HandleEventAsync(StockCountChangedEto eventData) { ... }
}
```
DbContext must implement `IHasEventInbox`, `IHasEventOutbox` for Outbox/Inbox pattern.
## Performance: Entity Cache
For frequently accessed data from other services, use Entity Cache:
```csharp
// Register
context.Services.AddEntityCache<Product, ProductDto, Guid>();
// Use - auto-invalidates on entity changes
private readonly IEntityCache<ProductDto, Guid> _productCache;
public async Task<ProductDto> GetProductAsync(Guid id)
{
return await _productCache.GetAsync(id);
}
```
## Pre-Configured Infrastructure
- **RabbitMQ** - Distributed events with Outbox/Inbox
- **Redis** - Distributed cache and locking
- **YARP** - API Gateway
- **OpenIddict** - Auth server
## Best Practices
- **Choose communication wisely** - Synchronous for queries needing immediate data, asynchronous for notifications and state changes
- **Use Integration Services** - Not application services for inter-service calls
- **Cache remote data** - Use Entity Cache or IDistributedCache for frequently accessed data
- **Share only Contracts** - Never share implementations
- **Idempotent handlers** - Events may be delivered multiple times
- **Database per service** - Each service owns its database

234
ai-rules/template-specific/module.mdc

@ -0,0 +1,234 @@
---
description: "ABP Module solution template specific patterns"
alwaysApply: false
---
# ABP Module Solution Template
> **Docs**: https://abp.io/docs/latest/solution-templates/application-module
This template is for developing reusable ABP modules. Key requirement: **extensibility** - consumers must be able to override and customize module behavior.
## Solution Structure
```
MyModule/
├── src/
│ ├── MyModule.Domain.Shared/ # Constants, enums, localization
│ ├── MyModule.Domain/ # Entities, repository interfaces, domain services
│ ├── MyModule.Application.Contracts/ # DTOs, service interfaces
│ ├── MyModule.Application/ # Service implementations
│ ├── MyModule.EntityFrameworkCore/ # EF Core implementation
│ ├── MyModule.MongoDB/ # MongoDB implementation
│ ├── MyModule.HttpApi/ # REST controllers
│ ├── MyModule.HttpApi.Client/ # Client proxies
│ ├── MyModule.Web/ # MVC/Razor Pages UI
│ └── MyModule.Blazor/ # Blazor UI
├── test/
│ └── MyModule.Tests/
└── host/
└── MyModule.HttpApi.Host/ # Test host application
```
## Database Independence
Support both EF Core and MongoDB:
### Repository Interface (Domain)
```csharp
public interface IBookRepository : IRepository<Book, Guid>
{
Task<Book> FindByNameAsync(string name);
Task<List<Book>> GetListByAuthorAsync(Guid authorId);
}
```
### EF Core Implementation
```csharp
public class BookRepository : EfCoreRepository<MyModuleDbContext, Book, Guid>, IBookRepository
{
public async Task<Book> FindByNameAsync(string name)
{
var dbSet = await GetDbSetAsync();
return await dbSet.FirstOrDefaultAsync(b => b.Name == name);
}
}
```
### MongoDB Implementation
```csharp
public class BookRepository : MongoDbRepository<MyModuleMongoDbContext, Book, Guid>, IBookRepository
{
public async Task<Book> FindByNameAsync(string name)
{
var queryable = await GetQueryableAsync();
return await queryable.FirstOrDefaultAsync(b => b.Name == name);
}
}
```
## Table/Collection Prefix
Allow customization to avoid naming conflicts:
```csharp
// Domain.Shared
public static class MyModuleDbProperties
{
public static string DbTablePrefix { get; set; } = "MyModule";
public static string DbSchema { get; set; } = null;
public const string ConnectionStringName = "MyModule";
}
```
Usage:
```csharp
builder.Entity<Book>(b =>
{
b.ToTable(MyModuleDbProperties.DbTablePrefix + "Books", MyModuleDbProperties.DbSchema);
});
```
## Module Options
Provide configuration options:
```csharp
// Domain
public class MyModuleOptions
{
public bool EnableFeatureX { get; set; } = true;
public int MaxItemCount { get; set; } = 100;
}
```
Usage in module:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<MyModuleOptions>(options =>
{
options.EnableFeatureX = true;
});
}
```
Usage in service:
```csharp
public class MyService : ITransientDependency
{
private readonly MyModuleOptions _options;
public MyService(IOptions<MyModuleOptions> options)
{
_options = options.Value;
}
}
```
## Extensibility Points
### Virtual Methods (Critical for Modules!)
When developing a reusable module, **all public and protected methods must be virtual** to allow consumers to override behavior:
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
// ✅ Public methods MUST be virtual
public virtual async Task<BookDto> CreateAsync(CreateBookDto input)
{
var book = await CreateBookEntityAsync(input);
await _bookRepository.InsertAsync(book);
return _bookMapper.MapToDto(book);
}
// ✅ Use protected virtual for helper methods (not private)
protected virtual Task<Book> CreateBookEntityAsync(CreateBookDto input)
{
return Task.FromResult(new Book(
GuidGenerator.Create(),
input.Name,
input.Price
));
}
// ❌ WRONG for modules - private methods cannot be overridden
// private Book CreateBook(CreateBookDto input) { ... }
}
```
This allows module consumers to:
- Override specific methods without copying entire class
- Extend functionality while preserving base behavior
- Customize module behavior for their needs
### Entity Extension
Support object extension system:
```csharp
public class MyModuleModuleExtensionConfigurator
{
public static void Configure()
{
OneTimeRunner.Run(() =>
{
ObjectExtensionManager.Instance.Modules()
.ConfigureMyModule(module =>
{
module.ConfigureBook(book =>
{
book.AddOrUpdateProperty<string>("CustomProperty");
});
});
});
}
}
```
## Localization
```csharp
// Domain.Shared
[LocalizationResourceName("MyModule")]
public class MyModuleResource
{
}
// Module configuration
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<MyModuleResource>("en")
.AddVirtualJson("/Localization/MyModule");
});
```
## Permission Definition
```csharp
public class MyModulePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup(
MyModulePermissions.GroupName,
L("Permission:MyModule"));
myGroup.AddPermission(
MyModulePermissions.Books.Default,
L("Permission:Books"));
}
}
```
## Best Practices
1. **Virtual methods** - All public/protected methods must be `virtual` for extensibility
2. **Protected virtual helpers** - Use `protected virtual` instead of `private` for helper methods
3. **Database agnostic** - Support both EF Core and MongoDB
4. **Configurable** - Use options pattern for customization
5. **Localizable** - Use localization for all user-facing text
6. **Table prefix** - Allow customization to avoid conflicts
7. **Separate connection string** - Support dedicated database
8. **No dependencies on host** - Module should be self-contained
9. **Test with host app** - Include a host application for testing

274
ai-rules/testing/patterns.mdc

@ -0,0 +1,274 @@
---
description: "ABP testing patterns - unit tests and integration tests"
globs:
- "test/**/*.cs"
- "tests/**/*.cs"
- "**/*Tests*/**/*.cs"
- "**/*Test*.cs"
alwaysApply: false
---
# ABP Testing Patterns
> **Docs**: https://abp.io/docs/latest/testing
## Test Project Structure
| Project | Purpose | Base Class |
|---------|---------|------------|
| `*.Domain.Tests` | Domain logic, entities, domain services | `*DomainTestBase` |
| `*.Application.Tests` | Application services | `*ApplicationTestBase` |
| `*.EntityFrameworkCore.Tests` | Repository implementations | `*EntityFrameworkCoreTestBase` |
## Integration Test Approach
ABP recommends integration tests over unit tests:
- Tests run with real services and database (SQLite in-memory)
- No mocking of internal services
- Each test gets a fresh database instance
## Application Service Test
```csharp
public class BookAppService_Tests : MyProjectApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
// Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
// Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "Test Book");
}
[Fact]
public async Task Should_Create_Book()
{
// Arrange
var input = new CreateBookDto
{
Name = "New Book",
Price = 19.99m
};
// Act
var result = await _bookAppService.CreateAsync(input);
// Assert
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New Book");
result.Price.ShouldBe(19.99m);
}
[Fact]
public async Task Should_Not_Create_Book_With_Invalid_Name()
{
// Arrange
var input = new CreateBookDto
{
Name = "", // Invalid
Price = 10m
};
// Act & Assert
await Should.ThrowAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(input);
});
}
}
```
## Domain Service Test
```csharp
public class BookManager_Tests : MyProjectDomainTestBase
{
private readonly BookManager _bookManager;
private readonly IBookRepository _bookRepository;
public BookManager_Tests()
{
_bookManager = GetRequiredService<BookManager>();
_bookRepository = GetRequiredService<IBookRepository>();
}
[Fact]
public async Task Should_Create_Book()
{
// Act
var book = await _bookManager.CreateAsync("Test Book", 29.99m);
// Assert
book.ShouldNotBeNull();
book.Name.ShouldBe("Test Book");
book.Price.ShouldBe(29.99m);
}
[Fact]
public async Task Should_Not_Allow_Duplicate_Book_Name()
{
// Arrange
await _bookManager.CreateAsync("Existing Book", 10m);
// Act & Assert
var exception = await Should.ThrowAsync<BusinessException>(async () =>
{
await _bookManager.CreateAsync("Existing Book", 20m);
});
exception.Code.ShouldBe("MyProject:BookNameAlreadyExists");
}
}
```
## Test Naming Convention
Use descriptive names:
```csharp
// Pattern: Should_ExpectedBehavior_When_Condition
public async Task Should_Create_Book_When_Input_Is_Valid()
public async Task Should_Throw_BusinessException_When_Name_Already_Exists()
public async Task Should_Return_Empty_List_When_No_Books_Exist()
```
## Arrange-Act-Assert (AAA)
```csharp
[Fact]
public async Task Should_Update_Book_Price()
{
// Arrange
var bookId = await CreateTestBookAsync();
var newPrice = 39.99m;
// Act
var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto
{
Price = newPrice
});
// Assert
result.Price.ShouldBe(newPrice);
}
```
## Assertions with Shouldly
ABP uses Shouldly library:
```csharp
result.ShouldNotBeNull();
result.Name.ShouldBe("Expected Name");
result.Price.ShouldBeGreaterThan(0);
result.Items.ShouldContain(x => x.Id == expectedId);
result.Items.ShouldBeEmpty();
result.Items.Count.ShouldBe(5);
// Exception assertions
await Should.ThrowAsync<BusinessException>(async () =>
{
await _service.DoSomethingAsync();
});
var ex = await Should.ThrowAsync<BusinessException>(async () =>
{
await _service.DoSomethingAsync();
});
ex.Code.ShouldBe("MyProject:ErrorCode");
```
## Test Data Seeding
```csharp
public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
public static readonly Guid TestBookId = Guid.Parse("...");
private readonly IBookRepository _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public async Task SeedAsync(DataSeedContext context)
{
await _bookRepository.InsertAsync(
new Book(TestBookId, "Test Book", 19.99m, Guid.Empty),
autoSave: true
);
}
}
```
## Disabling Authorization in Tests
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAlwaysAllowAuthorization();
}
```
## Mocking External Services
Use NSubstitute when needed:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var emailSender = Substitute.For<IEmailSender>();
emailSender.SendAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.CompletedTask);
context.Services.AddSingleton(emailSender);
}
```
## Testing with Specific User
```csharp
[Fact]
public async Task Should_Get_Current_User_Books()
{
// Login as specific user
await WithUnitOfWorkAsync(async () =>
{
using (CurrentUser.Change(TestData.UserId))
{
var result = await _bookAppService.GetMyBooksAsync();
result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId);
}
});
}
```
## Testing Multi-Tenancy
```csharp
[Fact]
public async Task Should_Filter_Books_By_Tenant()
{
using (CurrentTenant.Change(TestData.TenantId))
{
var result = await _bookAppService.GetListAsync(new GetBookListDto());
// Results should be filtered by tenant
}
}
```
## Best Practices
- Each test should be independent
- Don't share state between tests
- Use meaningful test data
- Test edge cases and error conditions
- Keep tests focused on single behavior
- Use test data seeders for common data
- Avoid testing framework internals

224
ai-rules/ui/angular.mdc

@ -0,0 +1,224 @@
---
description: "ABP Angular UI patterns and best practices"
globs:
- "**/angular/**/*.ts"
- "**/angular/**/*.html"
- "**/*.component.ts"
alwaysApply: false
---
# ABP Angular UI
> **Docs**: https://abp.io/docs/latest/framework/ui/angular/overview
## Project Structure
```
src/app/
├── proxy/ # Auto-generated service proxies
├── shared/ # Shared components, pipes, directives
├── book/ # Feature module
│ ├── book.module.ts
│ ├── book-routing.module.ts
│ ├── book-list/
│ │ ├── book-list.component.ts
│ │ ├── book-list.component.html
│ │ └── book-list.component.scss
│ └── book-detail/
```
## Generate Service Proxies
```bash
abp generate-proxy -t ng
```
This generates typed service classes in `src/app/proxy/`.
## List Component Pattern
```typescript
@Component({
selector: 'app-book-list',
templateUrl: './book-list.component.html'
})
export class BookListComponent implements OnInit {
books = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;
constructor(
public readonly list: ListService,
private bookService: BookService,
private confirmation: ConfirmationService
) {}
ngOnInit(): void {
this.hookToQuery();
}
private hookToQuery(): void {
this.list.hookToQuery(query =>
this.bookService.getList(query)
).subscribe(response => {
this.books = response;
});
}
create(): void {
// Open create modal
}
delete(book: BookDto): void {
this.confirmation
.warn('::AreYouSureToDelete', '::AreYouSure')
.subscribe(status => {
if (status === Confirmation.Status.confirm) {
this.bookService.delete(book.id).subscribe(() => this.list.get());
}
});
}
}
```
## Localization
```typescript
// In component
constructor(private localizationService: LocalizationService) {}
getText(): string {
return this.localizationService.instant('::Books');
}
```
```html
<!-- In template -->
<h1>{{ '::Books' | abpLocalization }}</h1>
<!-- With parameters -->
<p>{{ '::WelcomeMessage' | abpLocalization: userName }}</p>
```
## Authorization
### Permission Directive
```html
<button *abpPermission="'BookStore.Books.Create'">Create</button>
```
### Permission Guard
```typescript
const routes: Routes = [
{
path: '',
component: BookListComponent,
canActivate: [PermissionGuard],
data: {
requiredPolicy: 'BookStore.Books'
}
}
];
```
### Programmatic Check
```typescript
constructor(private permissionService: PermissionService) {}
canCreate(): boolean {
return this.permissionService.getGrantedPolicy('BookStore.Books.Create');
}
```
## Forms with Validation
```typescript
@Component({...})
export class BookFormComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.buildForm();
}
buildForm(): void {
this.form = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(128)]],
price: [0, [Validators.required, Validators.min(0)]]
});
}
save(): void {
if (this.form.invalid) return;
this.bookService.create(this.form.value).subscribe(() => {
// Handle success
});
}
}
```
```html
<form [formGroup]="form" (ngSubmit)="save()">
<div class="form-group">
<label for="name">{{ '::Name' | abpLocalization }}</label>
<input type="text" id="name" formControlName="name" class="form-control" />
</div>
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
{{ '::Save' | abpLocalization }}
</button>
</form>
```
## Configuration API
```typescript
constructor(private configService: ConfigStateService) {}
getCurrentUser(): CurrentUserDto {
return this.configService.getOne('currentUser');
}
getSettings(): void {
const setting = this.configService.getSetting('MyApp.MaxItemCount');
}
```
## Modal Service
```typescript
constructor(private modalService: ModalService) {}
openCreateModal(): void {
const modalRef = this.modalService.open(BookFormComponent, {
size: 'lg'
});
modalRef.result.then(result => {
if (result) {
this.list.get();
}
});
}
```
## Toast Notifications
```typescript
constructor(private toaster: ToasterService) {}
showSuccess(): void {
this.toaster.success('::BookCreatedSuccessfully', '::Success');
}
showError(error: string): void {
this.toaster.error(error, '::Error');
}
```
## Lazy Loading Modules
```typescript
// app-routing.module.ts
const routes: Routes = [
{
path: 'books',
loadChildren: () => import('./book/book.module').then(m => m.BookModule)
}
];
```
## Theme & Styling
- Use Bootstrap classes
- ABP provides theme variables via CSS custom properties
- Component-specific styles in `.component.scss`

210
ai-rules/ui/blazor.mdc

@ -0,0 +1,210 @@
---
description: "ABP Blazor UI patterns and components"
globs:
- "**/*.razor"
- "**/Blazor/**/*.cs"
- "**/*.Blazor*/**/*.cs"
alwaysApply: false
---
# ABP Blazor UI
> **Docs**: https://abp.io/docs/latest/framework/ui/blazor/overall
## Component Base Classes
### Basic Component
```razor
@inherits AbpComponentBase
<h1>@L["Books"]</h1>
```
### CRUD Page
```razor
@page "/books"
@inherits AbpCrudPageBase<IBookAppService, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
<Card>
<CardHeader>
<Row>
<Column>
<h2>@L["Books"]</h2>
</Column>
<Column TextAlignment="TextAlignment.End">
@if (HasCreatePermission)
{
<Button Color="Color.Primary" Clicked="OpenCreateModalAsync">
@L["NewBook"]
</Button>
}
</Column>
</Row>
</CardHeader>
<CardBody>
<DataGrid TItem="BookDto"
Data="Entities"
ReadData="OnDataGridReadAsync"
TotalItems="TotalCount"
ShowPager="true"
PageSize="PageSize">
<DataGridColumns>
<DataGridColumn Field="@nameof(BookDto.Name)" Caption="@L["Name"]" />
<DataGridColumn Field="@nameof(BookDto.Price)" Caption="@L["Price"]" />
<DataGridEntityActionsColumn TItem="BookDto">
<DisplayTemplate>
<EntityActions TItem="BookDto">
<EntityAction TItem="BookDto"
Text="@L["Edit"]"
Visible="HasUpdatePermission"
Clicked="() => OpenEditModalAsync(context)" />
<EntityAction TItem="BookDto"
Text="@L["Delete"]"
Visible="HasDeletePermission"
Clicked="() => DeleteEntityAsync(context)"
ConfirmationMessage="() => GetDeleteConfirmationMessage(context)" />
</EntityActions>
</DisplayTemplate>
</DataGridEntityActionsColumn>
</DataGridColumns>
</DataGrid>
</CardBody>
</Card>
```
## Localization
```razor
@* Using L property from base class *@
<h1>@L["PageTitle"]</h1>
@* With parameters *@
<p>@L["WelcomeMessage", CurrentUser.UserName]</p>
```
## Authorization
```razor
@* Check permission before rendering *@
@if (await AuthorizationService.IsGrantedAsync("MyPermission"))
{
<Button>Admin Action</Button>
}
@* Using policy-based authorization *@
<AuthorizeView Policy="MyPolicy">
<Authorized>
<p>You have access!</p>
</Authorized>
</AuthorizeView>
```
## Navigation & Menu
Configure in `*MenuContributor.cs`:
```csharp
public class MyMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
var bookMenu = new ApplicationMenuItem(
"Books",
l["Menu:Books"],
"/books",
icon: "fa fa-book"
);
if (await context.IsGrantedAsync(MyPermissions.Books.Default))
{
context.Menu.AddItem(bookMenu);
}
}
}
}
```
## Notifications & Messages
```csharp
// Success message
await Message.Success(L["BookCreatedSuccessfully"]);
// Confirmation dialog
if (await Message.Confirm(L["AreYouSure"]))
{
// User confirmed
}
// Toast notification
await Notify.Success(L["OperationCompleted"]);
```
## Forms & Validation
```razor
<Form @ref="CreateForm">
<Validations @ref="CreateValidationsRef" Model="@NewEntity" ValidateOnLoad="false">
<Validation MessageLocalizer="@LH.Localize">
<Field>
<FieldLabel>@L["Name"]</FieldLabel>
<TextEdit @bind-Text="@NewEntity.Name">
<Feedback>
<ValidationError />
</Feedback>
</TextEdit>
</Field>
</Validation>
</Validations>
</Form>
```
## JavaScript Interop
```csharp
@inject IJSRuntime JsRuntime
@code {
private async Task CallJavaScript()
{
await JsRuntime.InvokeVoidAsync("myFunction", arg1, arg2);
var result = await JsRuntime.InvokeAsync<string>("myFunctionWithReturn");
}
}
```
## State Management
```csharp
// Inject service proxy from HttpApi.Client
@inject IBookAppService BookAppService
@code {
private List<BookDto> Books { get; set; }
protected override async Task OnInitializedAsync()
{
var result = await BookAppService.GetListAsync(new PagedAndSortedResultRequestDto());
Books = result.Items.ToList();
}
}
```
## Code-Behind Pattern
**Books.razor:**
```razor
@page "/books"
@inherits BooksBase
```
**Books.razor.cs:**
```csharp
public partial class Books : BooksBase
{
// Component logic here
}
```
**BooksBase.cs:**
```csharp
public abstract class BooksBase : AbpComponentBase
{
[Inject]
protected IBookAppService BookAppService { get; set; }
}
```

262
ai-rules/ui/mvc.mdc

@ -0,0 +1,262 @@
---
description: "ABP MVC and Razor Pages UI patterns"
globs:
- "**/*.cshtml"
- "**/Pages/**/*.cs"
- "**/Views/**/*.cs"
- "**/Controllers/**/*.cs"
alwaysApply: false
---
# ABP MVC / Razor Pages UI
> **Docs**: https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall
## Razor Page Model
```csharp
public class IndexModel : AbpPageModel
{
private readonly IBookAppService _bookAppService;
public List<BookDto> Books { get; set; }
public IndexModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync()
{
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
Books = result.Items.ToList();
}
}
```
## Razor Page View
```html
@page
@model IndexModel
<abp-card>
<abp-card-header>
<abp-row>
<abp-column size-md="_6">
<h2>@L["Books"]</h2>
</abp-column>
<abp-column size-md="_6" class="text-end">
<abp-button button-type="Primary"
id="NewBookButton"
text="@L["NewBook"].Value" />
</abp-column>
</abp-row>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable">
<thead>
<tr>
<th>@L["Name"]</th>
<th>@L["Price"]</th>
<th>@L["Actions"]</th>
</tr>
</thead>
<tbody>
@foreach (var book in Model.Books)
{
<tr>
<td>@book.Name</td>
<td>@book.Price</td>
<td>
<abp-button button-type="Primary" size="Small"
text="@L["Edit"].Value" />
</td>
</tr>
}
</tbody>
</abp-table>
</abp-card-body>
</abp-card>
```
## ABP Tag Helpers
### Cards
```html
<abp-card>
<abp-card-header>Header</abp-card-header>
<abp-card-body>Content</abp-card-body>
<abp-card-footer>Footer</abp-card-footer>
</abp-card>
```
### Buttons
```html
<abp-button button-type="Primary" text="@L["Save"].Value" />
<abp-button button-type="Danger" icon="fa fa-trash" />
```
### Forms
```html
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
<abp-modal>
<abp-modal-header title="@L["NewBook"].Value" />
<abp-modal-body>
<abp-form-content />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Save | AbpModalButtons.Cancel)" />
</abp-modal>
</abp-dynamic-form>
```
### Tables
```html
<abp-table striped-rows="true" hoverable-rows="true">
<!-- content -->
</abp-table>
```
## Localization
```html
@* In Razor views/pages *@
<h1>@L["Books"]</h1>
@* With parameters *@
<p>@L["WelcomeMessage", Model.UserName]</p>
```
## JavaScript API
```javascript
// Localization
var text = abp.localization.getResource('BookStore')('Books');
// Authorization
if (abp.auth.isGranted('BookStore.Books.Create')) {
// Show create button
}
// Settings
var maxCount = abp.setting.get('BookStore.MaxItemCount');
// Ajax with automatic error handling
abp.ajax({
url: '/api/app/book',
type: 'POST',
data: JSON.stringify(bookData)
}).then(function(result) {
// Success
});
// Notifications
abp.notify.success('Book created successfully!');
abp.notify.error('An error occurred!');
// Confirmation
abp.message.confirm('Are you sure?').then(function(confirmed) {
if (confirmed) {
// User confirmed
}
});
```
## DataTables Integration
```javascript
var dataTable = $('#BooksTable').DataTable(
abp.libs.datatables.normalizeConfiguration({
serverSide: true,
paging: true,
ajax: abp.libs.datatables.createAjax(bookService.getList),
columnDefs: [
{
title: l('Name'),
data: 'name'
},
{
title: l('Price'),
data: 'price',
render: function(data) {
return data.toFixed(2);
}
},
{
title: l('Actions'),
rowAction: {
items: [
{
text: l('Edit'),
visible: abp.auth.isGranted('BookStore.Books.Edit'),
action: function(data) {
editModal.open({ id: data.record.id });
}
},
{
text: l('Delete'),
visible: abp.auth.isGranted('BookStore.Books.Delete'),
confirmMessage: function(data) {
return l('BookDeletionConfirmationMessage', data.record.name);
},
action: function(data) {
bookService.delete(data.record.id).then(function() {
abp.notify.success(l('SuccessfullyDeleted'));
dataTable.ajax.reload();
});
}
}
]
}
}
]
})
);
```
## Modal Pages
**CreateModal.cshtml:**
```html
@page
@model CreateModalModel
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
<abp-modal>
<abp-modal-header title="@L["NewBook"].Value" />
<abp-modal-body>
<abp-form-content />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Save | AbpModalButtons.Cancel)" />
</abp-modal>
</abp-dynamic-form>
```
**CreateModal.cshtml.cs:**
```csharp
public class CreateModalModel : AbpPageModel
{
[BindProperty]
public CreateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public CreateModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.CreateAsync(Book);
return NoContent();
}
}
```
## Bundle & Minification
```csharp
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(
StandardBundles.Styles.Global,
bundle => bundle.AddFiles("/styles/my-styles.css")
);
});
```

1
delete-bin-obj.ps1

@ -10,4 +10,3 @@ Get-ChildItem -Path . -Include bin,obj -Recurse -Directory | ForEach-Object {
}
Write-Host "BIN and OBJ folders have been successfully deleted." -ForegroundColor Green

17
docs/en/Community-Articles/2026-01-16-meet-abio-at-ndc-london/post.md

@ -0,0 +1,17 @@
We are thrilled to announce that **ABP.IO will be sponsoring [NDC London 2026](https://ndclondon.com/),** making the start of 2026 a very exciting time for us\!
NDC London is going to take place from **26th-30th January 2026 at Queen Elizabeth II Center.** This 5-Day event for software developers will have over 90 speakers and 100 sessions. We are excited to be a part of this amazing event once more as devoted supporters of the software development community\!
## Conference Tracks, Topics, and What Developers Can Expect
Developers attending **NDC London 2026** can expect five focused tracks packed with practical, real-world sessions. The conference covers the modern development stack, including **.NET, JavaScript, Cloud, DevOps, Security, Testing, UX, Web**, and emerging technologies, delivered by industry experts with actionable insights developers can apply immediately.
## Discover Previous NDC Events
We have shared **our takeaways from past NDC events** and other conferences [**here**](https://abp.io/community/events/sponsored#gsc.tab=0). You can check them out to learn what we discovered along the way\!
## Stop By Our Booth and Say Hello
We can’t wait to meet fellow developers at NDC London 2026, have meaningful conversations and connect in person. **If you are stopping by our booth, don’t miss our raffle\!** We will be giving away a nice surprise during the event\!
We are looking forward to meeting you there and sharing a few great days focused on software development. See you there\!

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/PuppeteerSharp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/QuestPDF.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

153
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/article.md

@ -0,0 +1,153 @@
# Which Open-Source PDF Libraries Are Recently Popular ? A Data-Driven Look At PDF Topic
So you're looking for a PDF library in .NET, right? Here's the thing - just because something has a million downloads doesn't mean it's what you should use *today*. I'm looking at **recent download momentum** (how many people are actually using it NOW via NuGet) and **GitHub activity** (are they still maintaining this thing or did they abandon it?).
I pulled data from the last ~90 days for the main players in the .NET PDF space. Here's what's actually happening:
## Popularity Comparison of .NET PDF Libraries (*ordered by score*)
| Library | GitHub Stars | Avg Daily NuGet Downloads | Total NuGet Downloads | **Popularity Score** |
|---------|---------------|-----------------------------|----------------------------|---------------------|
| **[Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet)** | [2.9k](https://github.com/microsoft/playwright-dotnet) | [23k](https://www.nuget.org/packages/Microsoft.Playwright) | 39M | **71/100** |
| **[QuestPDF](https://github.com/QuestPDF/QuestPDF)** | [13.7k](https://github.com/QuestPDF/QuestPDF) | [8.2k](https://www.nuget.org/packages/QuestPDF) | 15M | **54/100** |
| **[PDFsharp](https://github.com/empira/PDFsharp)** | [862](https://github.com/empira/PDFsharp) | [9k](https://www.nuget.org/packages/PdfSharp) | 47M | **48/100** |
| **[iText](https://github.com/itext/itext-dotnet)** | [1.9k](https://github.com/itext/itext-dotnet) | [17.2k](https://www.nuget.org/packages/itext) | 16M | **44/100** |
| **[PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)** | [3.8k](https://github.com/hardkoded/puppeteer-sharp) | [8.7k](https://www.nuget.org/packages/PuppeteerSharp) | 26M | **40/100** |
**How I calculated the score:** I weighted GitHub Stars (30%), Daily Downloads (40% - because that's what matters NOW), and Total Downloads (30% - for historical context). Everything normalized to 0-100 before weighting. Higher = better momentum overall.
## The Breakdown - What You Actually Need to Know
### [PDFsharp](https://docs.pdfsharp.net/)
![pdfsharp](pdfsharp.png)
**NuGet:** [PdfSharp](https://www.nuget.org/packages/PdfSharp) | **GitHub:** [empira/PDFsharp](https://github.com/empira/PDFsharp)
**What it does:** Code-first PDF stuff - drawing, manipulating, merging, that kind of thing. Not for HTML/browser rendering though, so don't try to convert your React app to PDF with this.
**What's the vibe?** **Stable, but kinda old school.** It's got the biggest total download count (47M!) but only pulling ~9k/day now. They updated it 2 weeks ago (Jan 6) so it's alive, and it supports .NET 8-10 which is nice. The GitHub stars (862) are pretty low compared to the shiny new kids, but honestly? It's been around forever and people still use it. It's the reliable old workhorse.
**Pick this if:**
- You need to build PDFs from scratch with code (not HTML)
- You want to draw graphics, manipulate existing PDFs, merge files
- You don't want browser engines anywhere near your project
---
### [iText](https://itextpdf.com/)
![iText Logo](itext.jpg)
**NuGet:** [itext](https://www.nuget.org/packages/itext/) | **GitHub:** [itext/itext-dotnet](https://github.com/itext/itext-dotnet)
**What it does:** The enterprise beast. Digital signatures, PDF compliance (PDF/A, PDF/UA), forms, all that fancy stuff. Can do HTML-to-PDF too if you need it.
**What's the vibe?** **Actually doing pretty well!** ~17.2k downloads/day (highest for code-first libs), updated literally yesterday (Jan 18). They're moving fast. 1.9k stars isn't huge but the community seems active. The catch? This is the enterprise option - check the licensing before you commit if you're doing commercial work.
**Pick this if:**
- You need digital signatures, PDF compliance, or advanced form stuff
- Your company is cool with licensing fees (or you're doing open source)
- You need serious PDF manipulation features
- You want HTML-to-PDF AND code-based generation in one package
---
### [Microsoft.Playwright](https://playwright.dev/dotnet/)
![Playwright Logo](playwright.png)
**NuGet:** [Microsoft.Playwright](https://www.nuget.org/packages/Microsoft.Playwright) | **GitHub:** [microsoft/playwright-dotnet](https://github.com/microsoft/playwright-dotnet)
**What it does:** Browser automation that can turn HTML/CSS/JS into PDFs. Uses real browser engines (Chromium, WebKit, Firefox) so your PDFs look exactly like they would in a browser.
**What's the vibe?** **Killing it.** ~23k downloads/day (highest in this whole list!). It's Microsoft-backed so you know they're not gonna abandon it anytime soon. Last commit was December 3rd but honestly that's fine, they're actively maintaining. 2.9k stars and climbing. If you need to turn web pages into PDFs, this is probably your best bet right now.
**Pick this if:**
- You need to convert HTML/CSS/JS to PDF and want it to look EXACTLY like the browser
- You're working with SPAs, dynamic content, or web templates
- You also need browser automation/testing (bonus!)
- Layout accuracy is critical (forms, dashboards, etc.)
---
### [PuppeteerSharp](https://www.puppeteersharp.com/)
![PuppeteerSharp Logo](PuppeteerSharp.png)
**NuGet:** [PuppeteerSharp](https://www.nuget.org/packages/PuppeteerSharp) | **GitHub:** [hardkoded/puppeteer-sharp](https://github.com/hardkoded/puppeteer-sharp)
**What it does:** Basically Playwright's older sibling. Uses headless Chromium to turn HTML into PDFs. Same idea, different API.
**What's the vibe?** **Stable but losing ground.** Got updated last week (Jan 12) so it's maintained, but ~8.7k/day is way less than Playwright's ~23k. 3.8k stars is decent though. It works fine, but Playwright is eating its lunch. Still, if you know Puppeteer already or only need Chromium, this might be fine.
**Pick this if:**
- You already know Puppeteer from Node.js and want the same vibe in .NET
- You only need Chromium (don't care about Firefox/WebKit)
- You have existing Puppeteer code you're porting
---
### [QuestPDF](https://github.com/QuestPDF/QuestPDF)
![QuestPDF Logo](QuestPDF.png)
**NuGet:** [QuestPDF](https://www.nuget.org/packages/QuestPDF) | **GitHub:** [QuestPDF/QuestPDF](https://github.com/QuestPDF/QuestPDF)
**What it does:** Build PDFs with fluent C# APIs. Think of it like building a UI layout, but for PDFs. No HTML needed - it's all code, all .NET.
**What's the vibe?** **The community favorite.** 13.7k stars (most by far!), updated yesterday (Jan 18). ~8.2k downloads/day isn't the highest but the community is clearly excited about it. Modern API, active dev, people seem to actually enjoy using it. If you're building reports/invoices from code and want something that feels modern, this is it.
**Pick this if:**
- You want to build PDFs with code (not HTML) and you like fluent APIs
- You're generating reports, invoices, structured documents
- You want zero browser dependencies
- You care about type safety and maintainable code
- You want something that feels modern and well-designed
## Who's Winning Right Now?
Here's what the numbers are telling us:
### Code-First Libraries (Building PDFs with Code)
**[QuestPDF](https://github.com/QuestPDF/QuestPDF)** - Score: 54/100
The people's choice. Most GitHub stars (13.7k), updated yesterday, community loves it. Downloads aren't the highest but the engagement is real. This is what people are excited about.
**[iText](https://github.com/itext/itext-dotnet)** - Score: 44/100
Actually pulling the most daily downloads (~17.2k/day) for code-first libs, also updated yesterday. The enterprise crowd is still using this heavily. Just watch that licensing.
**[PDFsharp](https://github.com/empira/PDFsharp)** - Score: 48/100
The old reliable. 47M total downloads but only ~9k/day now. It works, it's stable, but it's not where the momentum is. Still a solid choice if you need something battle-tested.
### HTML/Browser-Based Libraries (Turning Web Pages into PDFs)
**[Microsoft.Playwright](https://github.com/microsoft/playwright-dotnet)** - Score: 71/100
Winner winner. ~23k downloads/day (highest overall), Microsoft backing, actively maintained. If you need HTML-to-PDF, this is probably the move.
**[PuppeteerSharp](https://github.com/hardkoded/puppeteer-sharp)** - Score: 40/100
Still kicking around at ~8.7k/day but Playwright is clearly the future. Updated last week so it's not dead, just... less popular.
## TL;DR - What Should You Actually Use?
**Building PDFs from code (not HTML):**
- **QuestPDF** - If you want something modern and the community is raving about it (13.7k stars!)
- **iText** - If you need enterprise features and can handle the licensing
- **PDFsharp** - If you want the battle-tested option that's been around forever
**Converting HTML/web pages to PDF:**
- **Playwright** - Just use this. It's winning right now (~23k/day), Microsoft-backed, actively maintained. Game over.
- **PuppeteerSharp** - Only if you really need Chromium-only or you're migrating from Node.js Puppeteer
**Bottom line:** For HTML-to-PDF, Playwright is dominating. For code-first, QuestPDF has the hype but iText has the downloads. Choose your fighter.
---
*Numbers from GitHub and NuGet as of January 19, 2026. Daily downloads are from the last 90 days.*

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/itext.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/pdfsharp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
docs/en/Community-Articles/2026-01-19-Trend-PDF-Libraries-For-CSharp/playwright.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

167
docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/POST.md

@ -0,0 +1,167 @@
# How AI Is Changing Developers
In the last few years, AI has moved from “nice to have” to “hard to live without” for developers. At first it was just code completion and smart hints. Now it’s getting deep into how we build software: the methods, the toolchain, and even the job itself.
Here are some structured thoughts on how AI is affecting developers, based on trends and personal experience.
## Every library will have AI-first docs
Future libraries and frameworks won’t just have docs for humans. They’ll also have a manual for AI:
- How to use
- Why it is designed this way
- What NOT to do
- Conventions & Best Practices
Once these rules are written in a structured way, AI can onboard to a library faster and more consistently than a junior developer.
Docs won’t just be knowledge anymore. They’ll be instructions AI can execute.
## AI will be a must-have for developers
Soon, “writing code without AI” will feel as strange as “writing code without an IDE.”
- It won’t be about whether you use AI
- It’ll be about how well you use it and where
AI will become:
- A standard productivity tool
- An extension of a developer’s thinking
- A second brain
Developers who don’t use AI will fall behind in both speed and understanding.
## As AI gets smarter, it replaces “time”
AI isn’t replacing developers right away. It’s replacing:
- Lots of repetitive time
- Basic development costs
- Higher output per hour
Boilerplate, CRUD, basic validation, simple logic — all of that will get swallowed fast.
It’s not people being replaced. It’s waste.
## Orchestrating multiple AIs becomes real
The future isn’t “one AI does everything.” It’s more like:
- Claude writes core code
- Copilot generates and maintains unit tests
- Codex and similar tools write docs and examples
- Other AIs handle refactoring, performance analysis, security checks
The dev process itself becomes an AI orchestration system.
The developer’s role looks more like:
Architect + conductor + quality gatekeeper
## Only great infrastructure gets amplified by AI
Even if AI can teach you “how to use it correctly,” it still can’t invent mature infrastructure for you.
We still rely on:
- Stable base frameworks (like [ABP](https://abp.io))
- Engineering capability proven by many projects
- Long-term maintenance and evolution
AI is an accelerator, not the foundation.
For open source, AI is actually a better companion:
- Helps understand the source code
- Helps learn design thinking
- Helps ship faster
The stronger the infrastructure, the more value AI can amplify.
## Frontend feels mature; backend still evolving
From personal experience:
- AI is already very strong in frontend work (Bootstrap / UI components, layout, styling, interaction)
- Backend is still learning and improving (business boundaries, architecture trade-offs, implicit constraints)
This shows: the clearer the rules and the faster the feedback, the faster AI improves.
## Writing rules for AI is productivity itself
In the ABP libraries, we’ve already written lots of rules for AI:
- Conventions
- Usage limits
- Recommended patterns
As rules grow:
- AI becomes more stable
- More predictable
- Base development work can be largely automated
Future engineering skill will be, in large part: how to design a rules system for AI.
## The real advantage is better feedback loops
AI gets much stronger when there’s clear feedback:
- Tests that run fast and fail loudly
- Logs and metrics that explain behavior
- Code review that checks for edge cases and security
The teams that win are the ones who can quickly verify, correct, and learn.
## About a developer’s career
Sometimes I think: I’m glad I didn’t enter the software industry just in the last few years.
If you’re just starting out, you really feel:
- The barrier is lower
- The competition is tougher
But whenever I see AI generate confident but wrong code, I’m reminded:
- The industry still has a future
- It still needs judgment, taste, and experience
There will always be people who love coding. If AI does it and we watch, that’s fine too.
## Chaos everywhere, but the experience is moving fast
Big companies, platforms, tools:
- GitHub
- OpenAI
- Claude
- All kinds of IDEs / agents
New AI tools, apps, and platforms keep popping up. New concepts show up almost every week. It’s noisy, but the big picture is clear: AI keeps getting better, and the overall developer experience is improving fast.
## Get ready for the AI revolution
Looking back at personal experience:
- Before: Google
- Now: ChatGPT
- Before: manual translation
- Now: fully automatic
- Before: writing unit tests by hand
- Now: AI does it all
- Before: human replies to customers
- Now: AI-assisted or even AI-led
From code completion to agents running tasks, and now deep IDE integration — the pace is shocking.
## Closing
AI is not the end of software engineering. It is:
- A leap in cognition
- A restructure of how work gets done
- An upgrade of roles
What matters most isn’t how much code AI can write, but how we redefine the value of “developers” in the AI era.

BIN
docs/en/Community-Articles/2026-01-24-How-AI-Is-Changing-Developers/image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

50
docs/en/Community-Articles/2026-02-02-ndc-london-article/post.md

@ -0,0 +1,50 @@
The software development world converged on the **Queen Elizabeth II Centre** in Westminster from **January 26-30** for **NDC London 2026**. As one of the most anticipated tech conferences in Europe, this year’s event delivered a masterclass in the future of the stack.
We have spent five days immersed in workshops and sessions. Here is our comprehensive recap of the highlights and the technical shifts that will define 2026\.
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BBjsk292Ejh%2b5X2yeS2pD9uibmq8qxh50b9eOg5U5Ib2jAFaeCHItbTyOpajIeaUzNKg/p0WHohjf1iac2%2bVL6kT/Y3ORSKpRQrdE22QJTwAxBMUryUgTQJ989hYtsvF%2bkReDR03k0gIl4ApUaji6Tg)
## **1\. High-Performance .NET and C\# Evolution**
A major focus this year was the continued evolution of the .NET ecosystem. Experts delivered standout sessions on high-performance coding patterns, it’s clear that efficiency and "Native AOT" (Ahead-of-Time compilation) are no longer niche topics, they are becoming industry standards.
## **1\. Moving Beyond the AI Hype**
If 2025 was about experimenting with LLMs, NDC London 2026 was about AI integration. Sessions from experts showcased how developers are moving past simple chatbots and integrating AI directly into the CI/CD pipeline and automated testing suites.
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BDxx%2FqqZ08tgIxCPsAnDDD2w5yJPjVXwUJrbGHpSln3npfpJEBQ78chKoSlZS1cz1nbigNQtRq60dlbyMLwnAgE52tBwUJz481PcBgNtyFMW7rm7oKhFV9c7tK8bEcK%2FscRudaV8w7%2FPO8U5KJv%2BQal)
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BBdNXgjnu7HIGgX//VJrh3XzjPns4ODHMUhZ%2bDQCcZa2Nc0%2b%2bshyt2UXqaIKEJMPHh6JIDGBtUrdQZ1EzmGn3pingGKiw7YTbh0Z%2bLRZSmcY6pEXkd1S/7VVncmICIHrQgjg%2b7eb2uO28qadIWGbD99)
## **3\. The "Hallway Track" and Community Networking**
One of the biggest draws of **NDC London** is the community. Between the 100+ sessions, the exhibitor hall was buzzing with live demos and networking.
Watch the video:
[![Watch the Hallway Track video](https://img.youtube.com/vi/yb-FILkqL7U/hqdefault.jpg)](https://www.youtube.com/watch?v=yb-FILkqL7U)
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BCLbkSK3YZDZZhBGi/IBZOCXgcWHwTyS/s5v6U%2bSeQnY5yCTzMJFTu/mA4xX%2bL5tjbMPfEI8gvCwmVEfSymGFIiJLtAbP8T2zFZev%2bm74sTsQ%2b4sdsLKbdijiae3G%2b45ijWep7yFJx9BWMgV263zzvI)
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BCrCACVWDlDjOgl9ASMeZNMVBGye%2bfya4aO6UW5Kyg9MCVLswzckRWS%2bT71AcQuWMGfiousZlSCrKNAGrosPXzuWAsxnNai3xBcj061TWjGAGX4u1AtrD0eknRxuKe2ba%2bVO7r0sZqle%2bUyZa305hhO)
## **4\. The Big Giveaway: Our Xbox Series S Raffle**
One of our favorite moments of the week was our Raffle Session. We love giving back to the community that inspires us, and this year, the energy at our booth was higher than ever.
We were thrilled to give away a brand-new Xbox Series S to one lucky winner\! It was fantastic to meet so many of you who stopped by to enter, chat about your current projects, and share your thoughts on the future of the industry.
**Congratulations again to our 2026 winner\!** We hope you enjoy some well-deserved gaming time after a long week of learning.
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BBozHxXhCL7qMtx5LAxvafvPOKaZJepGlR7tgHVvw6wGpuR4Ervipym%2busZ7eMl3uook15K1874RYEwUenBfoZSJBm33MdaHFduha9iJ7tnfTmW12QbdYM77yqfVJ7EonuJsRrNySdYrQuRI0H2RkZr)
Watch the video:
[![Watch the Xbox Series S giveaway](https://img.youtube.com/vi/W5HRwys8dpE/hqdefault.jpg)](https://www.youtube.com/watch?v=W5HRwys8dpE)
## **Final Thoughts: See You at NDC London 2027\!**
NDC London 2026 proved once again why it is a cornerstone event for the global developer community. We are returning to our projects with a refreshed roadmap and a deeper understanding of the tools shaping our industry.
![enter image description here](https://abp.io/api/file-management/file-descriptor/share?shareToken=CfDJ8NqaJZr2oLpIuRyHVjJk1BDJq%2bG7yg1jtoY3gGH8mFMZen%2bncuL%2bKrQHY4/FPOF2KXcLyEjJymhk0JAVwJ76lPeqBchrfsAK3TOUTKY15tC7jm3uwgcH9IWRxCM2ouqxVGqGPd8YIRdG7H7QgyuknBkS4wsdYI9gl1EGqgPtTXJd)

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/0.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/5.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

239
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/Post.md

@ -0,0 +1,239 @@
# What NDC London 2026 Looked Like From a Developer’s Perspective
![](0.png)
This year we attended NDC London as a sponsor for [ABP](https://abp.io). The conference was held at the same place [Queen Elizabeth II](https://qeiicentre.london/). I guess this is the best conf for .NET developers around the world (thanks to the NDC team). It was 3 full days started from 28 to 30 January 2026. As exhibitor we talked a lot with the attendees who stopped by our booth or while we were eating or in the conf rooms.
This is the best opportunity to know what everyone is doing in software society. While I was explaining ABP to the people who first time heard, I also ask about what they do in their work. Developers mostly work on web platforms. And as you know there's an AI transformation in our sector. That's why I wonder if other people also stick to the latest AI trend! Well... not as I expected. In Volosoft, we are tightly following AI trends, using in our daily development, injecting this new technology to our product and trying to benefit this as much as possible.
![1](1.png)
This new AI trend is same as the invention of printing (by Johannes Gutenberg in 1450) or it's similar to invention of calculators (by William S. Burroughs in 1886). The countries who benefit these inventions got a huge increase in their welfare level. So, we welcome this new AI invention in software development, design, devops and testing. I also see this as a big wave in the ocean, if you are prepared and developed your skills, you can play with it 🌊 and it's called surfing or you'll die in the ocean. But not all the companies react this transformation quickly. Many developers use it like ChatGpt conversation (copy-paste from it) or using GitHub Co-Pilot in a limited manner. But as I heard from Steven Sanderson's session and other Microsoft employees, they are already using it to reproduce the bugs reported in the issues or creating even feature PRs via Co-Pilot. That's a good news for me!
Here're some pictures from the conf and that's me on the left side with brown shoes :)
![2](2.png)
Another thing I see, there's a decrease in the number of attendees'. I don't know the real reason but probably the IT companies cut the budget for conferences. As you also hear, many companies layoff because of the AI replaces some of the positions.
The food was great during the conference. It was more like eating sessions for me. Lots of good meals from different countries' kitchen. In the second day, there was a party. People grabbed their beers, wines, beverages and made some more networking.
I was expecting more AI oriented sessions but it was less then my expectations. Even though I was an exhibitor, I tried to attend some of the session. I'll tell you my notes about these.
## Sessions / Talks
### The dangers of probably-working software | Damian Brady
The first session and keynote was from Damian Brady. He's part of Developer Advocacy team at GitHub. And the topic was "The dangers of probably-working software". He started with some negative impact of how generative AI is killing software, and he ended like this a not so bad, we can benefit from the AI transformation. First time I hear "sleepwalking" term for the development. He was telling when we generate code via AI, and we don't review well-enough, we're sleepwalkers. And that's correct! and good analogy for that case. This talk centers on a powerful lesson: *“**Don’t ship code you don’t truly understand.**”*
Damian tells a personal story from his early .NET days when he implemented a **Huffman compression algorithm** based largely on Wikipedia. The code **“worked” in small tests** but **failed in production**. The experience forced him to deeply understand the algorithm rather than relying on copied solutions. Through this story, he explores themes of trust, complexity, testing, and mental models in software engineering.
#### What I learnt from this session
- “It seems to work” is not the same as “I understand it.”
- Code copied from Wikipedia or StackOverflow is inherently risky in production.
- Passing tests on small datasets does not guarantee real-world reliability.
- Performance issues often surface only in edge cases.
- Delivery pressure can discourage deep understanding — to the detriment of quality.
- Always ask: “**When does this fail?**” — not just “**Why does this work?**”
![3](3.png)
### Playing the long game | Sheena O'Connell
Sheena is a former software engineer who now trains and supports tech educators. She talks about AI tools...
AI tools are everywhere but poorly understood; there’s hype, risks, and mixed results. The key question is how individuals and organisations should play the long game so skilled human engineers—especially juniors—can still grow and thrive.
She showed some statistics about how job postings on Indeed platform dramatically decreasing for software developers. About AI generated-code, she tells, it's less secure, there might be logical problems or interesting bugs, human might not read code very well and understanding/debugging code might sometimes take much longer time.
![4](4.png)
Being an engineer is about much more than a job title — it requires systems thinking, clear communication, dealing with uncertainty, continuous learning, discipline, and good knowledge management. The job market is shifting: demand for AI-skilled workers is rising quickly and paying premiums, and required skills are changing faster in AI-exposed roles. There’s strength in using a diversity of models instead of locking into one provider, and guardrails improve reliability.
AI is creating new roles (like AI security, observability, and operations) and new kinds of work, while routine attrition also opens opportunities. At the same time, heavy AI use can have negative cognitive effects: people may think less, feel lonelier, and prefer talking to AI over humans.
Organizations are becoming more dynamic and project-based, with shorter planning cycles, higher trust, and more experimentation — but also risk of “shiny new toy” syndrome. Research shows AI can boost productivity by 15–20% in many cases, especially in simpler, greenfield projects and popular languages, but it can actually reduce productivity on very complex work. Overall, the recommendation is to focus on using AI well (not just the newest model), add monitoring and guardrails, keep flexibility, and build tools that allow safe experimentation.
![4_1](4_1.png)
We’re in a messy, fast-moving AI era where LLM tools are everywhere but poorly understood. There’s a lot of hype and marketing noise, making it hard even for technical people to separate reality from fantasy. Different archetypes have emerged — from AI-optimists to skeptics — and both extremes have risks. AI is great for quick prototyping but unreliable for complex work, so teams need guardrails, better practices, and a focus on learning rather than “writing more code faster.” The key question is how individuals and organizations can play the long game so strong human engineers — especially juniors — can still grow and thrive in an AI-driven world.
![4_2](4_2.png)
### Crafting Intelligent Agents with Context Engineering | Carly Richmond
Carly is a Developer Advocate Lead at Elastic in London with deep experience in web development and agile delivery from her years in investment banking. A practical UI engineer. She brings a clear, hands-on perspective to building real-world AI systems. In her talk on **“Crafting Intelligent Agents with Context Engineering,”** she argues that prompt engineering isn’t enough — and shows how carefully shaping context across data, tools, and systems is key to creating reliable, useful AI agents. She mentioned about the context of an AI process. The context consists of Instructions, Short Memory, Long Memory, RAG, User Prompts, Tools, Structured Output.
![5](5.png)
### Modular Monoliths | Kevlin Henney
Kevlin frames the “microservices vs monolith” debate as a false dichotomy. His core argument is simple but powerful: problems rarely come from *being a monolith* — they come from being a **poorly structured one**. Modularity is not a deployment choice; it is an architectural discipline.
## **Notes from the Talk**
- A monolith is not inherently bad; a tangled monolith is.
- Architecture is mostly about **boundaries**, not boxes.
- If you cannot draw clean internal boundaries, you are not ready for microservices.
- Dependencies reveal your real architecture better than diagrams.
- Teams shape systems more than tools do (a modern reading of Conway’s Law).
- Splitting systems prematurely increases complexity without increasing clarity.
- Good modular design makes systems **easier to change, not just easier to scale**.
## **Lessons for Developers**
- Start with a well-structured modular monolith before considering microservices.
- Treat modules as real first-class citizens: clear ownership, clear contracts.
- Make dependency direction explicit — no circular graphs.
- Use internal architectural tests to prevent boundary violations.
- Organize code by *capability*, not by technical layer.
- Optimize for **cognitive load**, not deployment topology.
- If your team structure is messy, your architecture will be messy — fix people, not tech.
---
### AI Coding Agents & Skills | Steve Sanderson
![steve-sanderson-talk](D:\github\volosoft\abp\docs\en\Community-Articles\2026-02-03-Impressions-of-NDC-London-2026\steve-sanderson-talk.png)
In this session, Steve started how Microsoft is excessively using AI tools for PRs, reproducing bug reports etc... He says, we use brains and hands less then anytime. And he summarized the AI assisted development into 10 outlines. These are Subagents, Plan Mode, Skills, Delegate, Memories, Hooks, MCP, Infinite Sessions, Plugins and Git Workflow. Let's see his ideas for each of these headings:
## **1. Subagents**
- Break big problems into smaller, specialized agents.
- Each subagent should have a clear responsibility and limited scope.
- Parallel work is better than one “smart but slow” agent.
- Reduces hallucination by narrowing context per agent.
- Easier to debug: you can inspect each agent’s output separately.
------
## **2. Plan Mode**
- Always start with a plan before generating code.
- The plan should be explicit, human-readable, and reviewable.
- Helps align expectations between you and the AI.
- Prevents wasted effort on wrong directions.
- Encourages structured thinking instead of trial-and-error coding.
------
## **3. Skills**
- Skills are reusable capabilities for AI agents.
- Treat skills like APIs: versioned, documented, and shareable.
- Prefer many small skills over one monolithic skill.
- Store skills in Git, not in chat history.
- Skills should integrate with real tools (CI, GitHub, browsers, etc.).
------
## **4. Delegate**
- Don’t micromanage — delegate well-defined tasks.
- Give clear inputs, constraints, and success criteria.
- Let the AI own the implementation details.
- Review outcomes instead of every intermediate step.
- Use delegation for repetitive or mechanical work.
------
## **5. Memories**
- Long-term memory should capture decisions, not chat noise.
- Store *why* something was done, not every detail of *how*.
- Keep memory sparse and structured.
- Treat memory like documentation that evolves over time.
- Be careful about leaking sensitive data into persistent memory.
------
## **6. Hooks**
- Hooks connect AI actions to your real workflow.
- Examples: pre-commit checks, PR reviews, test triggers.
- Hooks make AI proactive instead of reactive.
- They reduce manual context switching for developers.
- Best hooks are lightweight and predictable.
------
## **7. MCP (Model Context Protocol)**
- Standard way for models to talk to external tools.
- Enables safe, controlled access to systems (files, APIs, databases).
- Prevents random tool usage; everything is explicit.
- Encourages ecosystem of interoperable tools.
- Critical for production-grade AI assistants.
------
## **8. Infinite Sessions**
- AI should remember the “project context,” not just the last message.
- Reduces repetition and re-explaining.
- Enables deeper reasoning over time.
- Works best when combined with structured memory.
- Still requires periodic cleanup to avoid context bloat.
------
## **9. Plugins**
- Extend AI capabilities beyond core model features.
- Plugins should solve real workflow problems, not demos.
- Prefer composable plugins over custom hacks.
- Security matters — don’t give plugins unlimited access.
- Treat plugins like dependencies: review and maintain them.
------
## **10. Git Workflow**
- AI should operate inside your existing Git process.
- Generate small, focused commits — not giant changes.
- Use AI for PR descriptions and code reviews.
- Keep humans in the loop for design decisions.
- Branching strategy still matters; AI doesn’t replace it.
**Lessons for Developers from Steve's Talk**
- Coding agents work best when you treat them like programmable teammates, not autocomplete tools.
- “Skills” are the right abstraction for scaling AI assistants across a team.
- A skill is fundamentally a structured Markdown file + metadata + optional scripts/tools.
- Load **descriptions first, details later** — this keeps LLM context small and reliable.
- Treat skills like shared APIs: version them, review them, and store them in source control.
- Skills can be installed from Git repos (marketplaces), not just created locally.
- Slash commands make skills fast, explicit, and reproducible in daily workflow.
- Use skills to bridge AI ↔ real systems (e.g., GitHub Actions, Playwright, build status).
- Automation skills are most valuable when they handle end-to-end flows (browser + app + data).
- Let the agent *discover* the right skill rather than hard-coding every step.
- Prefer small, composable skills over one “god skill.”
- Skills reduce hallucination risk by constraining what the agent is allowed to do.
---
### My Personal Notes about AI
- This is your code tech stack for a basic .NET project:
- Assembly > MSIL > C# > ASP.NET Core > NuGet + NPM > Your Handmade Business Code
When we ask a development to an AI assisted IDE, AI never starts from Assembly or even it's not writing an existing NPM package. It basically uses what's there on the market. So we know frameworks like ASP.NET Core, ABP will always be there after AI evolution.
- Software engineer is not just writing correct syntax code to explain a program to computer. As an engineer you need to understand the requirements, design the problem, make proper decisions and fix the uncertainty. Asking AI the right questions is very critical these days.
- Tesla cars already started to go autonomous. As a driver, you don't need to care about how the car is driven. You need to choose the right way to go in the shortest time without hussle.
- Nowadays, **developers big new issue is Reviewing the AI generated-code.** In the future, developers who use AI, who inspect AI generated code well and who tells the AI exactly what's needed will be the most important topics. Others (who's typing only code) will be naturally eliminated. Invest your time for these topics.
- We see that our brain is getting lazier, our coding muscles gets weaker day by day. Just like after calculator invention, we stopped calculate big numbers. We'll eventually forget coding. But maybe that's what it needs to be!
- Also I don't think AI will replace developers. Think about washing machines. Since they came out, they still need humans to put the clothes in the machine, pick the best program, take out from the machine and iron. From now on, AI is our assistance in every aspect of our life from shopping, medical issues, learning to coding. Let's benefit from it.

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/steve-sanderson-talk.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

2
docs/en/deployment/configuring-production.md

@ -113,6 +113,6 @@ ABP uses .NET's standard [Logging services](../framework/fundamentals/logging.md
ABP's startup solution templates come with [Swagger UI](https://swagger.io/) pre-installed. Swagger is a pretty standard and useful tool to discover and test your HTTP APIs on a built-in UI that is embedded into your application or service. It is typically used in development environment, but you may want to enable it on staging or production environments too.
While you will always secure your HTTP APIs with other techniques (like the [Authorization](../framework/fundamentals/authorization.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment.
While you will always secure your HTTP APIs with other techniques (like the [Authorization](../framework/fundamentals/authorization/index.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment.
> You may also want to see the [ABP Swagger integration](../framework/api-development/swagger.md) document.

14
docs/en/docs-nav.json

@ -333,7 +333,7 @@
"path": "studio/solution-explorer.md"
},
{
"text": "Running Applications",
"text": "Solution Runner",
"path": "studio/running-applications.md"
},
{
@ -347,6 +347,10 @@
{
"text": "Working with ABP Suite",
"path": "studio/working-with-suite.md"
},
{
"text": "Custom Commands",
"path": "studio/custom-commands.md"
}
]
},
@ -458,12 +462,16 @@
"items": [
{
"text": "Overview",
"path": "framework/fundamentals/authorization.md",
"path": "framework/fundamentals/authorization/index.md",
"isIndex": true
},
{
"text": "Dynamic Claims",
"path": "framework/fundamentals/dynamic-claims.md"
},
{
"text": "Resource Based Authorization",
"path": "framework/fundamentals/authorization/resource-based-authorization.md"
}
]
},
@ -1284,7 +1292,7 @@
},
{
"text": "LeptonX Lite",
"path": "ui-themes/lepton-x-lite/mvc.md"
"path": "ui-themes/lepton-x-lite/asp-net-core.md"
},
{
"text": "LeptonX",

2
docs/en/framework/api-development/standard-apis/configuration.md

@ -9,7 +9,7 @@
ABP provides a pre-built and standard endpoint that contains some useful information about the application/service. Here, is the list of some fundamental information at this endpoint:
* Granted [policies](../../fundamentals/authorization.md) (permissions) for the current user.
* Granted [policies](../../fundamentals/authorization/index.md) (permissions) for the current user.
* [Setting](../../infrastructure/settings.md) values for the current user.
* Info about the [current user](../../infrastructure/current-user.md) (like id and user name).
* Info about the current [tenant](../../architecture/multi-tenancy) (like id and name).

2
docs/en/framework/architecture/domain-driven-design/application-services.md

@ -218,7 +218,7 @@ See the [validation document](../../fundamentals/validation.md) for more.
It's possible to use declarative and imperative authorization for application service methods.
See the [authorization document](../../fundamentals/authorization.md) for more.
See the [authorization document](../../fundamentals/authorization/index.md) for more.
## CRUD Application Services

23
docs/en/framework/architecture/domain-driven-design/entities.md

@ -135,6 +135,29 @@ if (book1.EntityEquals(book2)) //Check equality
}
```
### `IKeyedObject` Interface
ABP entities implement the `IKeyedObject` interface, which provides a way to get the entity's primary key as a string:
```csharp
public interface IKeyedObject
{
string? GetObjectKey();
}
```
The `GetObjectKey()` method returns a string representation of the entity's primary key. For entities with a single key (like `Entity<Guid>` or `Entity<int>`), it returns the `Id` property converted to a string. For entities with composite keys, it returns the keys combined with a comma separator.
This interface is particularly useful for scenarios where you need to identify an entity by its key in a type-agnostic way, such as:
* **Resource-based authorization**: When checking or granting permissions for specific entity instances
* **Caching**: When creating cache keys based on entity identifiers
* **Logging and auditing**: When recording entity identifiers in a consistent format
Since all ABP entities implement this interface through the `IEntity` interface, you can use `GetObjectKey()` on any entity without additional implementation.
> See the [Resource-Based Authorization](../../fundamentals/authorization/resource-based-authorization.md) documentation for a practical example of using `IKeyedObject` with the permission system.
## AggregateRoot Class
"*Aggregate is a pattern in Domain-Driven Design. A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An example may be an order and its line-items, these will be separate objects, but it's useful to treat the order (together with its line items) as a single aggregate.*" (see the [full description](http://martinfowler.com/bliki/DDD_Aggregate.html))

2
docs/en/framework/architecture/modularity/extending/customizing-application-modules-guide.md

@ -112,4 +112,4 @@ Also, see the following documents:
* See [the localization document](../../../fundamentals/localization.md) to learn how to extend existing localization resources.
* See [the settings document](../../../infrastructure/settings.md) to learn how to change setting definitions of a depended module.
* See [the authorization document](../../../fundamentals/authorization.md) to learn how to change permission definitions of a depended module.
* See [the authorization document](../../../fundamentals/authorization/index.md) to learn how to change permission definitions of a depended module.

97
docs/en/framework/fundamentals/authorization.md → docs/en/framework/fundamentals/authorization/index.md

@ -9,13 +9,15 @@
Authorization is used to check if a user is allowed to perform some specific operations in the application.
ABP extends [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing authorization system to be usable in the **[application services](../architecture/domain-driven-design/application-services.md)** too.
ABP extends [ASP.NET Core Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction) by adding **permissions** as auto [policies](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) and allowing authorization system to be usable in the **[application services](../../architecture/domain-driven-design/application-services.md)** too.
So, all the ASP.NET Core authorization features and the documentation are valid in an ABP based application. This document focuses on the features that are added on top of ASP.NET Core authorization features.
ABP supports two types of permissions: **Standard permissions** apply globally (e.g., "can create documents"), while **resource-based permissions** target specific instances (e.g., "can edit Document #123"). This document covers standard permissions; see [Resource-Based Authorization](./resource-based-authorization.md) for fine-grained, per-resource access control.
## Authorize Attribute
ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/simple) attribute that can be used for an action, a controller or a page. ABP allows you to use the same attribute for an [application service](../architecture/domain-driven-design/application-services.md) too.
ASP.NET Core defines the [**Authorize**](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/simple) attribute that can be used for an action, a controller or a page. ABP allows you to use the same attribute for an [application service](../../architecture/domain-driven-design/application-services.md) too.
Example:
@ -87,9 +89,11 @@ namespace Acme.BookStore.Permissions
> ABP automatically discovers this class. No additional configuration required!
> You typically define this class inside the `Application.Contracts` project of your [application](../../solution-templates/layered-web-application). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with.
> You typically define this class inside the `Application.Contracts` project of your [application](../../../solution-templates/layered-web-application/index.md). The startup template already comes with an empty class named *YourProjectNamePermissionDefinitionProvider* that you can start with.
In the `Define` method, you first need to add a **permission group** (or get an existing group), then add **permissions** to this group using the `AddPermission` method.
In the `Define` method, you first need to add a **permission group** or get an existing group then add **permissions** to this group.
> For resource-specific fine-grained permissions, use the `AddResourcePermission` method instead. See [Resource-Based Authorization](./resource-based-authorization.md) for details.
When you define a permission, it becomes usable in the ASP.NET Core authorization system as a **policy** name. It also becomes visible in the UI. See permissions dialog for a role:
@ -100,6 +104,8 @@ When you define a permission, it becomes usable in the ASP.NET Core authorizatio
When you save the dialog, it is saved to the database and used in the authorization system.
> **Note:** Only standard (global) permissions are shown in this dialog. Resource-based permissions are managed through the [Resource Permission Management Dialog](../../../modules/permission-management.md#resource-permission-management-dialog) on individual resource instances.
> The screen above is available when you have installed the identity module, which is basically used for user and role management. Startup templates come with the identity module pre-installed.
#### Localizing the Permission Name
@ -125,15 +131,15 @@ Then you can define texts for "BookStore" and "Permission:BookStore_Author_Creat
"Permission:BookStore_Author_Create": "Creating a new author"
```
> For more information, see the [localization document](./localization.md) on the localization system.
> For more information, see the [localization document](../localization.md) on the localization system.
The localized UI will be as seen below:
![authorization-new-permission-ui-localized](../../images/authorization-new-permission-ui-localized.png)
![authorization-new-permission-ui-localized](../../../images/authorization-new-permission-ui-localized.png)
#### Multi-Tenancy
ABP supports [multi-tenancy](../architecture/multi-tenancy) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below:
ABP supports [multi-tenancy](../../architecture/multi-tenancy/index.md) as a first class citizen. You can define multi-tenancy side option while defining a new permission. It gets one of the three values defined below:
- **Host**: The permission is available only for the host side.
- **Tenant**: The permission is available only for the tenant side.
@ -180,7 +186,7 @@ authorManagement.AddChild("Author_Management_Delete_Books");
The result on the UI is shown below (you probably want to localize permissions for your application):
![authorization-new-permission-ui-hierarcy](../../images/authorization-new-permission-ui-hierarcy.png)
![authorization-new-permission-ui-hierarcy](../../../images/authorization-new-permission-ui-hierarcy.png)
For the example code, it is assumed that a role/user with "Author_Management" permission granted may have additional permissions. Then a typical application service that checks permissions can be defined as shown below:
@ -229,7 +235,7 @@ See [policy based authorization](https://docs.microsoft.com/en-us/aspnet/core/se
### Changing Permission Definitions of a Depended Module
A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](../architecture/modularity/basics.md)) and change their definitions.
A class deriving from the `PermissionDefinitionProvider` (just like the example above) can also get existing permission definitions (defined by the depended [modules](../../architecture/modularity/basics.md)) and change their definitions.
Example:
@ -247,12 +253,12 @@ When you write this code inside your permission definition provider, it finds th
You may want to disable a permission based on a condition. Disabled permissions are not visible on the UI and always returns `prohibited` when you check them. There are two built-in conditional dependencies for a permission definition;
* A permission can be automatically disabled if a [Feature](../infrastructure/features.md) was disabled.
* A permission can be automatically disabled if a [Global Feature](../infrastructure/global-features.md) was disabled.
* A permission can be automatically disabled if a [Feature](../../infrastructure/features.md) was disabled.
* A permission can be automatically disabled if a [Global Feature](../../infrastructure/global-features.md) was disabled.
In addition, you can create your custom extensions.
#### Depending on a Features
#### Depending on Features
Use the `RequireFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled:
@ -261,7 +267,7 @@ myGroup.AddPermission("Book_Creation")
.RequireFeatures("BookManagement");
````
#### Depending on a Global Feature
#### Depending on Global Features
Use the `RequireGlobalFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled:
@ -272,13 +278,13 @@ myGroup.AddPermission("Book_Creation")
#### Creating a Custom Permission Dependency
`PermissionDefinition` supports state check, Please refer to [Simple State Checker's documentation](../infrastructure/simple-state-checker.md)
`PermissionDefinition` supports state check, please refer to [Simple State Checker's documentation](../../infrastructure/simple-state-checker.md)
## IAuthorizationService
ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject, you can use it in your code to conditionally control the authorization.
ASP.NET Core provides the `IAuthorizationService` that can be used to check for authorization. Once you inject it, you can use it in your code to conditionally control the authorization.
Example:
**Example:**
```csharp
public async Task CreateAsync(CreateAuthorDto input)
@ -295,7 +301,7 @@ public async Task CreateAsync(CreateAuthorDto input)
}
```
> `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](./dependency-injection.md) it into your class.
> `AuthorizationService` is available as a property when you derive from ABP's `ApplicationService` base class. Since it is widely used in application services, `ApplicationService` pre-injects it for you. Otherwise, you can directly [inject](../dependency-injection.md) it into your class.
Since this is a typical code block, ABP provides extension methods to simplify it.
@ -320,15 +326,15 @@ public async Task CreateAsync(CreateAuthorDto input)
See the following documents to learn how to re-use the authorization system on the client side:
* [ASP.NET Core MVC / Razor Pages UI: Authorization](../ui/mvc-razor-pages/javascript-api/auth.md)
* [Angular UI Authorization](../ui/angular/authorization.md)
* [Blazor UI Authorization](../ui/blazor/authorization.md)
* [ASP.NET Core MVC / Razor Pages UI: Authorization](../../ui/mvc-razor-pages/javascript-api/auth.md)
* [Angular UI Authorization](../../ui/angular/authorization.md)
* [Blazor UI Authorization](../../ui/blazor/authorization.md)
## Permission Management
Permission management is normally done by an admin user using the permission management modal:
![authorization-new-permission-ui-localized](../../images/authorization-new-permission-ui-localized.png)
![authorization-new-permission-ui-localized](../../../images/authorization-new-permission-ui-localized.png)
If you need to manage permissions by code, inject the `IPermissionManager` and use as shown below:
@ -356,13 +362,13 @@ public class MyService : ITransientDependency
`SetForUserAsync` sets the value (true/false) for a permission of a user. There are more extension methods like `SetForRoleAsync` and `SetForClientAsync`.
`IPermissionManager` is defined by the permission management module. See the [permission management module documentation](../../modules/permission-management.md) for more information.
`IPermissionManager` is defined by the Permission Management module. For resource-based permissions, use `IResourcePermissionManager` instead. See the [Permission Management Module documentation](../../../modules/permission-management.md) for more information.
## Advanced Topics
### Permission Value Providers
Permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers:
The permission checking system is extensible. Any class derived from `PermissionValueProvider` (or implements `IPermissionValueProvider`) can contribute to the permission check. There are three pre-defined value providers:
- `UserPermissionValueProvider` checks if the current user is granted for the given permission. It gets user id from the current claims. User claim name is defined with the `AbpClaimTypes.UserId` static property.
- `RolePermissionValueProvider` checks if any of the roles of the current user is granted for the given permission. It gets role names from the current claims. Role claims name is defined with the `AbpClaimTypes.Role` static property.
@ -412,15 +418,35 @@ Configure<AbpPermissionOptions>(options =>
});
```
### Resource Permission Value Providers
Similar to standard permission value providers, you can extend the resource permission checking system by creating custom **resource permission value providers**. ABP provides two built-in resource permission value providers:
* `UserResourcePermissionValueProvider`: Checks permissions granted directly to users for a specific resource.
* `RoleResourcePermissionValueProvider`: Checks permissions granted to roles for a specific resource.
You can create custom providers by implementing `IResourcePermissionValueProvider` or inheriting from `ResourcePermissionValueProvider`. Register them using:
```csharp
Configure<AbpPermissionOptions>(options =>
{
options.ResourceValueProviders.Add<YourCustomResourcePermissionValueProvider>();
});
```
> See the [Permission Management Module](../../../modules/permission-management.md#resource-permission-value-providers) documentation for detailed examples.
### Permission Store
`IPermissionStore` is the only interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and pre-installed in the application startup template. See the [permission management module documentation](../../modules/permission-management.md) for more information
`IPermissionStore` is the interface that needs to be implemented to read the value of permissions from a persistence source, generally a database system. The Permission Management module implements it and is pre-installed in the application startup template. See the [Permission Management Module documentation](../../../modules/permission-management.md) for more information.
For resource-based permissions, `IResourcePermissionStore` serves the same purpose, storing and retrieving permissions for specific resource instances.
### AlwaysAllowAuthorizationService
`AlwaysAllowAuthorizationService` is a class that is used to bypass the authorization service. It is generally used in integration tests where you may want to disable the authorization system.
Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](./dependency-injection.md) system:
Use `IServiceCollection.AddAlwaysAllowAuthorization()` extension method to register the `AlwaysAllowAuthorizationService` to the [dependency injection](../../dependency-injection.md) system:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
@ -466,11 +492,24 @@ public static class CurrentUserExtensions
}
```
> If you use OpenIddict please see [Updating Claims in Access Token and ID Token](../../modules/openiddict#updating-claims-in-access_token-and-id_token).
> If you use OpenIddict please see [Updating Claims in Access Token and ID Token](../../../modules/openiddict#updating-claims-in-access_token-and-id_token).
## Resource-Based Authorization
While this document covers standard (global) permissions, ABP also supports **resource-based authorization** for fine-grained access control on specific resource instances. Resource-based authorization allows you to grant permissions for a specific document, project, or any other entity rather than granting a permission for all resources of that type.
**Example scenarios:**
* Allow users to edit **only their own** blog posts or documents
* Grant access to **specific projects** based on team membership
* Implement document sharing where **different users have different access levels** to the same document
> See the [Resource-Based Authorization](./resource-based-authorization.md) document for implementation details.
## See Also
* [Permission Management Module](../../modules/permission-management.md)
* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](../ui/mvc-razor-pages/javascript-api/auth.md)
* [Permission Management in Angular UI](../ui/angular/Permission-Management.md)
* [Resource-Based Authorization](./resource-based-authorization.md)
* [Permission Management Module](../../../modules/permission-management.md)
* [ASP.NET Core MVC / Razor Pages JavaScript Auth API](../../ui/mvc-razor-pages/javascript-api/auth.md)
* [Permission Management in Angular UI](../../ui/angular/Permission-Management.md)
* [Video tutorial](https://abp.io/video-courses/essentials/authorization)

241
docs/en/framework/fundamentals/authorization/resource-based-authorization.md

@ -0,0 +1,241 @@
```json
//[doc-seo]
{
"Description": "Learn how to implement resource-based authorization in ABP Framework for fine-grained access control on specific resource instances like documents, projects, or any entity."
}
```
# Resource-Based Authorization
**Resource-Based Authorization** is a powerful feature that enables fine-grained access control based on specific resource instances. While the standard [authorization system](./index.md) grants permissions at a general level (e.g., "can edit documents"), resource-based authorization allows you to grant permissions for a **specific** document, project, or any other entity rather than granting a permission for all of them.
## When to Use Resource-Based Authorization?
Consider resource-based authorization when you need to:
* Allow users to edit **only their own blog posts or documents**
* Grant access to **specific projects** based on team membership
* Implement document sharing **where different users have different access levels to the same document**
* Control access to resources based on ownership or custom sharing rules
**Example Scenarios:**
Imagine a document management system where:
- User A can view and edit Document 1
- User B can only view Document 1
- User A has no access to Document 2
- User C can manage permissions for Document 2
This level of granular control is what resource-based authorization provides.
## Usage
Implementing resource-based authorization involves three main steps:
1. **Define** resource permissions in your `PermissionDefinitionProvider`
2. **Check** permissions using `IResourcePermissionChecker`
3. **Manage** permissions via UI or using `IResourcePermissionManager` for programmatic usages
### Defining Resource Permissions
Define resource permissions in your `PermissionDefinitionProvider` class using the `AddResourcePermission` method:
```csharp
namespace Acme.BookStore.Permissions;
public static class BookStorePermissions
{
public const string GroupName = "BookStore";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string ManagePermissions = Default + ".ManagePermissions";
public static class Resources
{
public const string Name = "Acme.BookStore.Books.Book";
public const string View = Name + ".View";
public const string Edit = Name + ".Edit";
public const string Delete = Name + ".Delete";
}
}
}
```
```csharp
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
namespace Acme.BookStore.Permissions
{
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup("BookStore");
// Standard permissions
myGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
// Permission to manage resource permissions (required)
myGroup.AddPermission(BookStorePermissions.Books.ManagePermissions, L("Permission:Books:ManagePermissions"));
// Resource-based permissions
context.AddResourcePermission(
name: BookStorePermissions.Books.Resources.View,
resourceName: BookStorePermissions.Books.Resources.Name,
managementPermissionName: BookStorePermissions.Books.ManagePermissions,
displayName: L("Permission:Books:View")
);
context.AddResourcePermission(
name: BookStorePermissions.Books.Resources.Edit,
resourceName: BookStorePermissions.Books.Resources.Name,
managementPermissionName: BookStorePermissions.Books.ManagePermissions,
displayName: L("Permission:Books:Edit")
);
context.AddResourcePermission(
name: BookStorePermissions.Books.Resources.Delete,
resourceName: BookStorePermissions.Books.Resources.Name,
managementPermissionName: BookStorePermissions.Books.ManagePermissions,
displayName: L("Permission:Books:Delete"),
multiTenancySide: MultiTenancySides.Host
);
}
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<BookStoreResource>(name);
}
}
```
The `AddResourcePermission` method requires the following parameters:
* `name`: A unique name for the resource permission.
* `resourceName`: An identifier for the resource type. This is typically the full name of the entity class (e.g., `Acme.BookStore.Books.Book`).
* `managementPermissionName`: A standard permission that controls who can manage resource permissions. Users with this permission can grant/revoke resource permissions for specific resources.
* `displayName`: (Optional) A localized display name shown in the UI.
* `multiTenancySide`: (Optional) Specifies on which side of a multi-tenant application this permission can be used. Accepts `MultiTenancySides.Host` (only for the host side), `MultiTenancySides.Tenant` (only for tenants), or `MultiTenancySides.Both` (default, available on both sides).
### Checking Resource Permissions
Use the `IAuthorizationService` service to check if a user/role/client has a specific permission for a resource:
```csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization.Permissions.Resources;
namespace Acme.BookStore.Books
{
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IBookRepository _bookRepository;
public BookAppService(IBookRepository bookRepository)
{
_bookRepository = bookRepository;
}
public virtual async Task<BookDto> GetAsync(Guid id)
{
var book = await _bookRepository.GetAsync(id);
// Check if the current user can view this specific book
var isGranted = await AuthorizationService.IsGrantedAsync(book, BookStorePermissions.Books.Resources.View); // AuthorizationService is a property of the ApplicationService class and will be automatically injected.
if (!isGranted)
{
throw new AbpAuthorizationException("You don't have permission to view this book.");
}
return ObjectMapper.Map<Book, BookDto>(book);
}
public virtual async Task UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await _bookRepository.GetAsync(id);
// Check if the current user can edit this specific book
var isGranted = await AuthorizationService.IsGrantedAsync(book, BookStorePermissions.Books.Resources.Edit); // AuthorizationService is a property of the ApplicationService class and will be automatically injected.
if (!isGranted)
{
throw new AbpAuthorizationException("You don't have permission to edit this book.");
}
book.Title = input.Title;
book.Content = input.Content;
await _bookRepository.UpdateAsync(book);
}
}
}
```
In this example, the `BookAppService` uses `IAuthorizationService` to check if the current user has the required permission for a specific book before performing the operation. The method takes the `Book` entity object and resource permission name as parameters.
#### IKeyedObject
The `IAuthorizationService` internally uses `IResourcePermissionChecker` to check resource permissions, and gets the resource key by calling the `GetObjectKey()` method of the `IKeyedObject` interface. All ABP entities implement the `IKeyedObject` interface, so you can directly pass entity objects to the `IsGrantedAsync` method.
> See the [Entities documentation](../../architecture/domain-driven-design/entities.md) for more information about the `IKeyedObject` interface.
#### IResourcePermissionChecker
You can also directly use the `IResourcePermissionChecker` service to check resource permissions which provides more advanced features, such as checking multiple permissions at once:
> You have to pass the resource key (obtained via `GetObjectKey()`) explicitly when using `IResourcePermissionChecker`.
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IBookRepository _bookRepository;
private readonly IResourcePermissionChecker _resourcePermissionChecker;
public BookAppService(IBookRepository bookRepository, IResourcePermissionChecker resourcePermissionChecker)
{
_bookRepository = bookRepository;
_resourcePermissionChecker = resourcePermissionChecker;
}
public async Task<BookPermissionsDto> GetPermissionsAsync(Guid id)
{
var book = await _bookRepository.GetAsync(id);
var result = await _resourcePermissionChecker.IsGrantedAsync(new[]
{
BookStorePermissions.Books.Resources.View,
BookStorePermissions.Books.Resources.Edit,
BookStorePermissions.Books.Resources.Delete
},
BookStorePermissions.Books.Resources.Name,
book.GetObjectKey()!);
return new BookPermissionsDto
{
CanView = result.Result[BookStorePermissions.Books.Resources.View] == PermissionGrantResult.Granted,
CanEdit = result.Result[BookStorePermissions.Books.Resources.Edit] == PermissionGrantResult.Granted,
CanDelete = result.Result[BookStorePermissions.Books.Resources.Delete] == PermissionGrantResult.Granted
};
}
}
```
### Managing Resource Permissions
Once you have defined resource permissions, you need a way to grant or revoke them for specific users, roles, or clients. The [Permission Management Module](../../../modules/permission-management.md) provides the infrastructure for managing resource permissions:
- **UI Components**: Built-in modal dialogs for managing resource permissions on all supported UI frameworks (MVC/Razor Pages, Blazor, and Angular). These components allow administrators to grant or revoke permissions for users and roles on specific resource instances through a user-friendly interface.
- **`IResourcePermissionManager` Service**: A service for programmatically granting, revoking, and querying resource permissions at runtime. This is useful for scenarios like automatically granting permissions when a resource is created, implementing sharing functionality, or integrating with external systems.
> See the [Permission Management Module](../../../modules/permission-management.md#resource-permission-management-dialog) documentation for detailed information on using the UI components and the `IResourcePermissionManager` service.
## See Also
* [Authorization](./index.md)
* [Permission Management Module](../../../modules/permission-management.md)
* [Entities](../../architecture/domain-driven-design/entities.md)

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

@ -94,6 +94,6 @@ If you want to add your own dynamic claims contributor, you can create a class t
## See Also
* [Authorization](./authorization.md)
* [Authorization](./authorization/index.md)
* [Claims-based authorization in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims)
* [Mapping, customizing, and transforming claims in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims)

2
docs/en/framework/fundamentals/exception-handling.md

@ -322,7 +322,7 @@ The `context` object contains necessary information about the exception occurred
Some exception types are automatically thrown by the framework:
- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](./authorization.md) for more.
- `AbpAuthorizationException` is thrown if the current user has no permission to perform the requested operation. See [authorization](./authorization/index.md) for more.
- `AbpValidationException` is thrown if the input of the current request is not valid. See [validation](./validation.md) for more.
- `EntityNotFoundException` is thrown if the requested entity is not available. This is mostly thrown by [repositories](../architecture/domain-driven-design/repositories.md).

2
docs/en/framework/fundamentals/index.md

@ -10,7 +10,7 @@
The following documents explains the fundamental building blocks to create ABP solutions:
* [Application Startup](./application-startup.md)
* [Authorization](./authorization.md)
* [Authorization](./authorization/index.md)
* [Caching](./caching.md)
* [Configuration](./configuration.md)
* [Connection Strings](./connection-strings.md)

2
docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md

@ -71,7 +71,7 @@ public class CommentSummarization
> [!NOTE]
> If you don't specify the workspace name, the full name of the class will be used as the workspace name.
You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, you will get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client.
You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, the default workspace's chat client is returned. Only if both the workspace-specific and default chat clients are not configured will you get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client.
`IChatClient<TWorkSpace>` or `IChatClientAccessor<TWorkSpace>` can be resolved to access a specific workspace's chat client. This is a typed chat client and can be configured separately from the default chat client.

2
docs/en/framework/infrastructure/background-jobs/hangfire.md

@ -149,7 +149,7 @@ namespace MyProject
Hangfire Dashboard provides information about your background jobs, including method names and serialized arguments as well as gives you an opportunity to manage them by performing different actions – retry, delete, trigger, etc. So it is important to restrict access to the Dashboard.
To make it secure by default, only local requests are allowed, however you can change this by following the [official documentation](http://docs.hangfire.io/en/latest/configuration/using-dashboard.html) of Hangfire.
You can integrate the Hangfire dashboard to [ABP authorization system](../../fundamentals/authorization.md) using the **AbpHangfireAuthorizationFilter**
You can integrate the Hangfire dashboard to [ABP authorization system](../../fundamentals/authorization/index.md) using the **AbpHangfireAuthorizationFilter**
class. This class is defined in the `Volo.Abp.Hangfire` package. The following example, checks if the current user is logged in to the application:
```csharp

2
docs/en/framework/infrastructure/interceptors.md

@ -42,7 +42,7 @@ Automatically begins and commits/rolls back a database transaction when entering
Input DTOs are automatically validated against data annotation attributes and custom validation rules before executing the service logic, providing consistent validation behavior across all services.
### [Authorization](../fundamentals/authorization.md)
### [Authorization](../fundamentals/authorization/index.md)
Checks user permissions before allowing the execution of application service methods, ensuring security policies are enforced consistently.

29
docs/en/framework/infrastructure/object-to-object-mapping.md

@ -313,6 +313,34 @@ It is suggested to use the `MapExtraPropertiesAttribute` attribute if both class
Mapperly requires that properties of both source and destination objects have `setter` methods. Otherwise, the property will be ignored. You can use `protected set` or `private set` to control the visibility of the `setter` method, but each property must have a `setter` method.
### Nullable Reference Types
Mapperly respects C# nullable reference types (NRT). If your project enables NRT via `<Nullable>enable</Nullable>` in the project file, Mapperly will treat reference type properties as **non-nullable by default**.
That means:
- If a property can be `null`, declare it as nullable so Mapperly (and the compiler) understands it can be missing.
- If you declare a property as non-nullable, Mapperly assumes it is not `null`.
Otherwise, the generated mapping code may throw runtime exceptions (e.g., `NullReferenceException`) if a value is actually `null` during the mapping process.
Example:
````xml
<!-- .csproj -->
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
````
````csharp
public class PersonDto
{
public Country? Country { get; set; } // Nullable (can be null)
public City City { get; set; } = default!; // Non-nullable (cannot be null)
}
````
### Deep Cloning
By default, Mapperly does not create deep copies of objects to improve performance. If an object can be directly assigned to the target, it will do so (e.g., if the source and target type are both `List<T>`, the list and its entries will not be cloned). To create deep copies, set the `UseDeepCloning` property on the `MapperAttribute` to `true`.
@ -505,6 +533,7 @@ Each solution has its own advantages:
Choose the approach that best aligns with your application's architecture and maintainability requirements.
### More Mapperly Features
Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details.

1
docs/en/framework/ui/angular/data-table-column-extensions.md

@ -342,4 +342,5 @@ export const identityEntityPropContributors = {
## See Also
- [Extensible Table Row Detail](extensible-table-row-detail.md)
- [Customizing Application Modules Guide](../../architecture/modularity/extending/customizing-application-modules-guide.md)

189
docs/en/framework/ui/angular/extensible-table-row-detail.md

@ -0,0 +1,189 @@
```json
//[doc-seo]
{
"Description": "Learn how to add expandable row details to data tables using the Extensible Table Row Detail component in ABP Framework Angular UI."
}
```
# Extensible Table Row Detail for Angular UI
## Introduction
The `<abp-extensible-table-row-detail>` component allows you to add expandable row details to any `<abp-extensible-table>`. When users click the expand icon, additional content is revealed below the row.
<img alt="Extensible Table Row Detail Example" src="./images/row-detail-image.png" width="800px" style="max-width:100%">
## Quick Start
### Step 1. Import the Component
Import `ExtensibleTableRowDetailComponent` in your component:
```typescript
import {
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent
} from '@abp/ng.components/extensible';
@Component({
// ...
imports: [
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent,
],
})
export class MyComponent { }
```
### Step 2. Add Row Detail Template
Place `<abp-extensible-table-row-detail>` inside `<abp-extensible-table>` with an `ng-template`:
```html
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list">
<abp-extensible-table-row-detail>
<ng-template let-row="row" let-expanded="expanded">
<div class="p-3">
<h5>{%{{{ row.name }}}%}</h5>
<p>ID: {%{{{ row.id }}}%}</p>
<p>Status: {%{{{ row.isActive ? 'Active' : 'Inactive' }}}%}</p>
</div>
</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
An expand/collapse chevron icon will automatically appear in the first column of each row.
## API
### ExtensibleTableRowDetailComponent
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `rowHeight` | `string` &#124; `number` | `'100%'` | Height of the expanded row detail area |
### Template Context Variables
| Variable | Type | Description |
|----------|------|-------------|
| `row` | `R` | The current row data object |
| `expanded` | `boolean` | Whether the row is currently expanded |
## Usage Examples
### Basic Example
Display additional information when a row is expanded:
```html
<abp-extensible-table [data]="data.items" [list]="list">
<abp-extensible-table-row-detail>
<ng-template let-row="row">
<div class="p-3 border rounded m-2">
<strong>Details for: {%{{{ row.name }}}%}</strong>
<pre>{%{{{ row | json }}}%}</pre>
</div>
</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
### With Custom Row Height
Specify a fixed height for the detail area:
```html
<abp-extensible-table-row-detail [rowHeight]="200">
<ng-template let-row="row">
<div class="p-3">Fixed 200px height content</div>
</ng-template>
</abp-extensible-table-row-detail>
```
### Using Expanded State
Apply conditional styling based on expansion state:
```html
<abp-extensible-table-row-detail>
<ng-template let-row="row" let-expanded="expanded">
<div class="p-3" [class.fade-in]="expanded">
<p>This row is {%{{{ expanded ? 'expanded' : 'collapsed' }}}%}</p>
</div>
</ng-template>
</abp-extensible-table-row-detail>
```
### With Badges and Localization
```html
<abp-extensible-table-row-detail>
<ng-template let-row="row">
<div class="p-3 bg-light border rounded m-2">
<div class="row">
<div class="col-md-6">
<p class="mb-1"><strong>{%{{{ 'MyModule::Name' | abpLocalization }}}%}</strong></p>
<p class="text-muted">{%{{{ row.name }}}%}</p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>{%{{{ 'MyModule::Status' | abpLocalization }}}%}</strong></p>
<p>
@if (row.isActive) {
<span class="badge bg-success">{%{{{ 'AbpUi::Yes' | abpLocalization }}}%}</span>
} @else {
<span class="badge bg-secondary">{%{{{ 'AbpUi::No' | abpLocalization }}}%}</span>
}
</p>
</div>
</div>
</div>
</ng-template>
</abp-extensible-table-row-detail>
```
## Alternative: Direct Template Input
For simpler use cases, you can use the `rowDetailTemplate` input on `<abp-extensible-table>` directly:
```html
<abp-extensible-table
[data]="data.items"
[list]="list"
[rowDetailTemplate]="detailTemplate"
/>
<ng-template #detailTemplate let-row="row">
<div class="p-3">{%{{{ row.name }}}%}</div>
</ng-template>
```
## Events
### rowDetailToggle
The `rowDetailToggle` output emits when a row is expanded or collapsed:
```html
<abp-extensible-table
[data]="data.items"
[list]="list"
(rowDetailToggle)="onRowToggle($event)"
>
<abp-extensible-table-row-detail>
<ng-template let-row="row">...</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
```typescript
onRowToggle(row: MyDto) {
console.log('Row toggled:', row);
}
```
## See Also
- [Data Table Column Extensions](data-table-column-extensions.md)
- [Entity Action Extensions](entity-action-extensions.md)
- [Extensions Overview](extensions-overall.md)

BIN
docs/en/framework/ui/angular/images/row-detail-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

2
docs/en/framework/ui/angular/localization.md

@ -12,7 +12,7 @@ Before exploring _the localization pipe_ and _the localization service_, you sho
The localization key format consists of two sections which are **Resource Name** and **Key**.
`ResourceName::Key`
> If you do not specify the resource name, the `defaultResourceName` which is declared in `environment.ts` will be considered as default.
> If you do not specify the resource name, the `defaultResourceName` will be used. The value is first retrieved from the backend API response. If the backend does not provide a `defaultResourceName`, the value declared in `environment.ts` will be used as a fallback.
```ts
const environment = {

2
docs/en/framework/ui/angular/permission-management.md

@ -7,7 +7,7 @@
# Permission Management
A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../fundamentals/authorization.md) document.
A permission is a simple policy that is granted or prohibited for a particular user, role or client. You can read more about [authorization in ABP](../../fundamentals/authorization/index.md) document.
You can get permission of authenticated user using `getGrantedPolicy` or `getGrantedPolicy$` method of `PermissionService`.

4
docs/en/framework/ui/blazor/authorization.md

@ -9,7 +9,7 @@
Blazor applications can use the same authorization system and permissions defined in the server side.
> This document is only for authorizing on the Blazor UI. See the [Server Side Authorization](../../fundamentals/authorization.md) to learn how to define permissions and control the authorization system.
> This document is only for authorizing on the Blazor UI. See the [Server Side Authorization](../../fundamentals/authorization/index.md) to learn how to define permissions and control the authorization system.
## Basic Usage
@ -76,7 +76,7 @@ There are some useful extension methods for the `IAuthorizationService`:
## See Also
* [Authorization](../../fundamentals/authorization.md) (server side)
* [Authorization](../../fundamentals/authorization/index.md) (server side)
* [Blazor Security](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/) (Microsoft documentation)
* [ICurrentUser Service](../../infrastructure/current-user.md)
* [Video tutorial](https://abp.io/video-courses/essentials/authorization)

2
docs/en/framework/ui/blazor/page-toolbar-extensions.md

@ -102,7 +102,7 @@ protected override async ValueTask SetToolbarItemsAsync()
#### Permissions
If your button/component should be available based on a [permission/policy](../../fundamentals/authorization.md), you can pass the permission/policy name as the `RequiredPolicyName` parameter to the `AddButton` and `AddComponent` methods.
If your button/component should be available based on a [permission/policy](../../fundamentals/authorization/index.md), you can pass the permission/policy name as the `RequiredPolicyName` parameter to the `AddButton` and `AddComponent` methods.
### Add a Page Toolbar Contributor

2
docs/en/framework/ui/mvc-razor-pages/auto-complete-select.md

@ -78,4 +78,4 @@ It'll be automatically bound to a collection of defined value type.
## Notices
If the authenticated user doesn't have permission on the given URL, the user will get an authorization error. Be careful while designing this kind of UIs.
You can create a specific, [unauthorized](../../fundamentals/authorization.md) endpoint/method to get the list of items, so the page can retrieve lookup data of dependent entity without giving the entire read permission to users.
You can create a specific, [unauthorized](../../fundamentals/authorization/index.md) endpoint/method to get the list of items, so the page can retrieve lookup data of dependent entity without giving the entire read permission to users.

2
docs/en/framework/ui/mvc-razor-pages/javascript-api/ajax.md

@ -32,7 +32,7 @@ abp.ajax({
});
````
This command logs the list of users to the console, if you've **logged in** to the application and have [permission](../../../fundamentals/authorization.md) for the user management page of the [Identity Module](../../../../modules/identity.md).
This command logs the list of users to the console, if you've **logged in** to the application and have [permission](../../../fundamentals/authorization/index.md) for the user management page of the [Identity Module](../../../../modules/identity.md).
## Error Handling

2
docs/en/framework/ui/mvc-razor-pages/javascript-api/auth.md

@ -9,7 +9,7 @@
Auth API allows you to check permissions (policies) for the current user in the client side. In this way, you can conditionally show/hide UI parts or perform your client side logic based on the current permissions.
> This document only explains the JavaScript API. See the [authorization document](../../../fundamentals/authorization.md) to understand the ABP authorization & permission system.
> This document only explains the JavaScript API. See the [authorization document](../../../fundamentals/authorization/index.md) to understand the ABP authorization & permission system.
## Basic Usage

2
docs/en/framework/ui/mvc-razor-pages/navigation-menu.md

@ -117,7 +117,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
As seen above, a menu contributor contributes to the menu dynamically. So, you can perform any custom logic or get menu items from any source.
One use case is the [authorization](../../fundamentals/authorization.md). You typically want to add menu items by checking a permission.
One use case is the [authorization](../../fundamentals/authorization/index.md). You typically want to add menu items by checking a permission.
**Example: Check if the current user has a permission**

2
docs/en/framework/ui/mvc-razor-pages/page-toolbar-extensions.md

@ -134,7 +134,7 @@ Configure<AbpPageToolbarOptions>(options =>
#### Permissions
If your button/component should be available based on a [permission/policy](../../fundamentals/authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods.
If your button/component should be available based on a [permission/policy](../../fundamentals/authorization/index.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods.
### Add a Page Toolbar Contributor

2
docs/en/framework/ui/mvc-razor-pages/toolbars.md

@ -79,7 +79,7 @@ public class MyToolbarContributor : IToolbarContributor
}
````
You can use the [authorization](../../fundamentals/authorization.md) to decide whether to add a `ToolbarItem`.
You can use the [authorization](../../fundamentals/authorization/index.md) to decide whether to add a `ToolbarItem`.
````csharp
if (await context.IsGrantedAsync("MyPermissionName"))

4
docs/en/framework/ui/mvc-razor-pages/widgets.md

@ -12,7 +12,7 @@ ABP provides a model and infrastructure to create **reusable widgets**. Widget s
* Have **scripts & styles** dependencies for your widget.
* Create **dashboards** with widgets used inside.
* Define widgets in reusable **[modules](../../architecture/modularity/basics.md)**.
* Co-operate widgets with **[authorization](../../fundamentals/authorization.md)** and **[bundling](bundling-minification.md)** systems.
* Co-operate widgets with **[authorization](../../fundamentals/authorization/index.md)** and **[bundling](bundling-minification.md)** systems.
## Basic Widget Definition
@ -482,7 +482,7 @@ Used to refresh the widget when needed. It has a filter argument that can be use
Some widgets may need to be available only for authenticated or authorized users. In this case, use the following properties of the `Widget` attribute:
* `RequiresAuthentication` (`bool`): Set to true to make this widget usable only for authentication users (user have logged in to the application).
* `RequiredPolicies` (`List<string>`): A list of policy names to authorize the user. See [the authorization document](../../fundamentals/authorization.md) for more info about policies.
* `RequiredPolicies` (`List<string>`): A list of policy names to authorize the user. See [the authorization document](../../fundamentals/authorization/index.md) for more info about policies.
Example:

BIN
docs/en/get-started/images/abp-studio-background-tasks.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
docs/en/get-started/images/abp-studio-created-microservice-solution-explorer.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
docs/en/get-started/images/abp-studio-created-new-microservice-solution.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-kubernetes-build-docker-images.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-kubernetes-install-helm-chart.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-kubernetes-tab.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-browse-microservice.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-browse.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-enable-watch-2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-external-service.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner-watch-enabled-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
docs/en/get-started/images/abp-studio-microservice-solution-runner.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
docs/en/get-started/images/abp-studio-new-microservice-helm-charts.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-optional-modules.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/en/get-started/images/abp-studio-new-microservice-solution-dialog-properties.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options-microservice.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-additional-services.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-admin-password.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-aspire-configuration-microservice.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-microservice.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-microservice.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-dynamic-localization.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/en/get-started/images/abp-studio-new-solution-dialog-languages-microservice.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 31 KiB

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

Loading…
Cancel
Save