Browse Source

Merge branch 'dev' into issue-24673

pull/24766/head
Fahri Gedik 2 days ago
parent
commit
d63fa90149
  1. 1
      delete-bin-obj.ps1
  2. 50
      docs/en/Community-Articles/2026-02-02-ndc-london-article/post.md
  3. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/0.png
  4. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/1.png
  5. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/2.png
  6. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/3.png
  7. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4.png
  8. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_1.png
  9. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/4_2.png
  10. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/5.png
  11. 239
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/Post.md
  12. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/cover.png
  13. BIN
      docs/en/Community-Articles/2026-02-03-Impressions-of-NDC-London-2026/steve-sanderson-talk.png
  14. 5
      framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpBlazorClientHttpMessageHandler.cs
  15. 6
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs
  16. 37
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  17. 62
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs
  18. 47
      framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerUIOptionsExtensions.cs
  19. 6
      framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.js
  20. 2
      framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js
  21. 1
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs
  22. 37
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs
  23. 23
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs
  24. 162
      framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs
  25. 156
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs
  26. 8
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs
  27. 8
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs
  28. 32
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs
  29. 11
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs
  30. 4
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs
  31. 56
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs
  32. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json
  33. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json
  34. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json
  35. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json
  36. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json
  37. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json
  38. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json
  39. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json
  40. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json
  41. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json
  42. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json
  43. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json
  44. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json
  45. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json
  46. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json
  47. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json
  48. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json
  49. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json
  50. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json
  51. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json
  52. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json
  53. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json
  54. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json
  55. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json
  56. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json
  57. 4
      npm/ng-packs/apps/dev-app/src/app/app.config.ts
  58. 4
      npm/ng-packs/apps/dev-app/src/app/app.routes.ts
  59. 8
      npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts
  60. 1
      npm/ng-packs/apps/dev-app/src/app/home/home.component.html
  61. 58
      npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts
  62. 10
      npm/ng-packs/apps/dev-app/src/assets/localization/en.json
  63. 10
      npm/ng-packs/apps/dev-app/src/assets/localization/tr.json
  64. 2
      npm/ng-packs/package.json
  65. 8
      npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts
  66. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts
  67. 14
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  68. 6
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts
  69. 21
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  70. 6
      npm/ng-packs/packages/components/page/src/page.component.html
  71. 15
      npm/ng-packs/packages/components/page/src/page.component.ts
  72. 8
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html
  73. 9
      npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts
  74. 20
      npm/ng-packs/packages/core/src/lib/models/common.ts
  75. 12
      npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts
  76. 4
      npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts
  77. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  78. 5
      npm/ng-packs/packages/core/src/lib/services/localization.service.ts
  79. 119
      npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts
  80. 160
      npm/ng-packs/packages/core/src/lib/tests/ui-localization.service.spec.ts
  81. 2
      npm/ng-packs/packages/feature-management/package.json
  82. 2
      npm/ng-packs/packages/identity/package.json
  83. 5
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  84. 2
      npm/ng-packs/packages/permission-management/package.json
  85. 25
      npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts
  86. 44
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template
  87. 7
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test-setup.ts.template
  88. 26
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template
  89. 2
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template
  90. 12
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template
  91. 44
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template
  92. 7
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test-setup.ts.template
  93. 26
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test.ts.template
  94. 2
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template
  95. 12
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.spec.json.template
  96. 6
      npm/ng-packs/packages/schematics/src/utils/angular-schematic/generate-lib.ts
  97. 1
      npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts
  98. 2
      npm/ng-packs/packages/setting-management/package.json
  99. 7
      npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts
  100. 9
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

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 Write-Host "BIN and OBJ folders have been successfully deleted." -ForegroundColor Green

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

5
framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/AbpBlazorClientHttpMessageHandler.cs → framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/AbpBlazorClientHttpMessageHandler.cs

@ -4,13 +4,15 @@ using System.Net.Http.Headers;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Volo.Abp.AspNetCore.Components.Progression; using Volo.Abp.AspNetCore.Components.Progression;
using Volo.Abp.AspNetCore.Components.Web;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Timing; using Volo.Abp.Timing;
namespace Volo.Abp.AspNetCore.Components.Web; namespace Volo.Abp.AspNetCore.Components.WebAssembly;
public class AbpBlazorClientHttpMessageHandler : DelegatingHandler, ITransientDependency public class AbpBlazorClientHttpMessageHandler : DelegatingHandler, ITransientDependency
{ {
@ -51,6 +53,7 @@ public class AbpBlazorClientHttpMessageHandler : DelegatingHandler, ITransientDe
options.Type = UiPageProgressType.Info; options.Type = UiPageProgressType.Info;
}); });
request.SetBrowserRequestStreamingEnabled(true);
await SetLanguageAsync(request, cancellationToken); await SetLanguageAsync(request, cancellationToken);
await SetAntiForgeryTokenAsync(request); await SetAntiForgeryTokenAsync(request);
await SetTimeZoneAsync(request); await SetTimeZoneAsync(request);

6
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs

@ -20,7 +20,7 @@ public static class EntityCacheServiceCollectionExtensions
services.Configure<AbpDistributedCacheOptions>(options => services.Configure<AbpDistributedCacheOptions>(options =>
{ {
options.ConfigureCache<TEntity>(cacheOptions ?? GetDefaultCacheOptions()); options.ConfigureCache<EntityCacheItemWrapper<TEntity>>(cacheOptions ?? GetDefaultCacheOptions());
}); });
services.Configure<AbpSystemTextJsonSerializerModifiersOptions>(options => services.Configure<AbpSystemTextJsonSerializerModifiersOptions>(options =>
@ -42,7 +42,7 @@ public static class EntityCacheServiceCollectionExtensions
services.Configure<AbpDistributedCacheOptions>(options => services.Configure<AbpDistributedCacheOptions>(options =>
{ {
options.ConfigureCache<TEntityCacheItem>(cacheOptions ?? GetDefaultCacheOptions()); options.ConfigureCache<EntityCacheItemWrapper<TEntityCacheItem>>(cacheOptions ?? GetDefaultCacheOptions());
}); });
return services; return services;
@ -59,7 +59,7 @@ public static class EntityCacheServiceCollectionExtensions
services.Configure<AbpDistributedCacheOptions>(options => services.Configure<AbpDistributedCacheOptions>(options =>
{ {
options.ConfigureCache<TEntityCacheItem>(cacheOptions ?? GetDefaultCacheOptions()); options.ConfigureCache<EntityCacheItemWrapper<TEntityCacheItem>>(cacheOptions ?? GetDefaultCacheOptions());
}); });
return services; return services;

37
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -310,7 +310,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity);
} }
} }
else if (entityEntry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) else if (GetAllPropertyEntries(entityEntry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
{ {
if (IsOnlyForeignKeysModified(entityEntry)) if (IsOnlyForeignKeysModified(entityEntry))
{ {
@ -446,7 +446,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
break; break;
case EntityState.Modified: case EntityState.Modified:
if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) if (GetAllPropertyEntries(entry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
{ {
if (IsOnlyForeignKeysModified(entry)) if (IsOnlyForeignKeysModified(entry))
{ {
@ -454,7 +454,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
break; break;
} }
var modifiedProperties = entry.Properties.Where(x => x.IsModified).ToList(); var modifiedProperties = GetAllPropertyEntries(entry).Where(x => x.IsModified).ToList();
var disableAuditingAttributes = modifiedProperties.Select(x => x.Metadata.PropertyInfo?.GetCustomAttribute<DisableAuditingAttribute>()).ToList(); var disableAuditingAttributes = modifiedProperties.Select(x => x.Metadata.PropertyInfo?.GetCustomAttribute<DisableAuditingAttribute>()).ToList();
if (disableAuditingAttributes.Any(x => x == null || x.UpdateModificationProps)) if (disableAuditingAttributes.Any(x => x == null || x.UpdateModificationProps))
{ {
@ -501,9 +501,36 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
} }
} }
protected virtual IEnumerable<PropertyEntry> GetAllPropertyEntries(EntityEntry entry)
{
return entry.Properties.Concat(GetAllComplexPropertyEntries(entry.ComplexProperties));
}
protected virtual IEnumerable<PropertyEntry> GetAllComplexPropertyEntries(IEnumerable<ComplexPropertyEntry> complexPropertyEntries)
{
foreach (var complexPropertyEntry in complexPropertyEntries)
{
var complexPropertyInfo = complexPropertyEntry.Metadata.PropertyInfo;
if (complexPropertyInfo != null && complexPropertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
{
continue;
}
foreach (var propertyEntry in complexPropertyEntry.Properties)
{
yield return propertyEntry;
}
foreach (var nestedPropertyEntry in GetAllComplexPropertyEntries(complexPropertyEntry.ComplexProperties))
{
yield return nestedPropertyEntry;
}
}
}
protected virtual bool IsOnlyForeignKeysModified(EntityEntry entry) protected virtual bool IsOnlyForeignKeysModified(EntityEntry entry)
{ {
return entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && return GetAllPropertyEntries(entry).Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() &&
(x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString())); (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString()));
} }
@ -662,7 +689,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, bool forceApply = false) protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, bool forceApply = false)
{ {
if (forceApply || if (forceApply ||
entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) GetAllPropertyEntries(entry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)))
{ {
IncrementEntityVersionProperty(entry); IncrementEntityVersionProperty(entry);
SetModificationAuditProperties(entry); SetModificationAuditProperties(entry);

62
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs

@ -184,6 +184,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
var properties = entityEntry.Metadata.GetProperties(); var properties = entityEntry.Metadata.GetProperties();
var isCreated = IsCreated(entityEntry); var isCreated = IsCreated(entityEntry);
var isDeleted = IsDeleted(entityEntry); var isDeleted = IsDeleted(entityEntry);
var isSoftDeleted = IsSoftDeleted(entityEntry);
foreach (var property in properties) foreach (var property in properties)
{ {
@ -193,7 +194,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
} }
var propertyEntry = entityEntry.Property(property.Name); var propertyEntry = entityEntry.Property(property.Name);
if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry)) if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !isSoftDeleted)
{ {
var propertyType = DeterminePropertyTypeFromEntry(property, propertyEntry); var propertyType = DeterminePropertyTypeFromEntry(property, propertyEntry);
@ -207,6 +208,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
} }
} }
foreach (var complexPropertyEntry in entityEntry.ComplexProperties)
{
AddComplexPropertyChanges(
complexPropertyEntry,
propertyChanges,
isCreated,
isDeleted,
isSoftDeleted,
parentPath: null);
}
if (AbpEfCoreNavigationHelper == null) if (AbpEfCoreNavigationHelper == null)
{ {
return propertyChanges; return propertyChanges;
@ -250,6 +262,52 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
return propertyChanges; return propertyChanges;
} }
protected virtual void AddComplexPropertyChanges(
ComplexPropertyEntry complexPropertyEntry,
List<EntityPropertyChangeInfo> propertyChanges,
bool isCreated,
bool isDeleted,
bool isSoftDeleted,
string? parentPath)
{
var complexPropertyInfo = complexPropertyEntry.Metadata.PropertyInfo;
if (complexPropertyInfo != null && complexPropertyInfo.IsDefined(typeof(DisableAuditingAttribute), true))
{
return;
}
var complexPropertyPath = parentPath == null
? complexPropertyEntry.Metadata.Name
: $"{parentPath}.{complexPropertyEntry.Metadata.Name}";
foreach (var propertyEntry in complexPropertyEntry.Properties)
{
if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !isSoftDeleted)
{
var propertyType = DeterminePropertyTypeFromEntry(propertyEntry.Metadata, propertyEntry);
propertyChanges.Add(new EntityPropertyChangeInfo
{
NewValue = isDeleted ? null : JsonSerializer.Serialize(propertyEntry.CurrentValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength),
OriginalValue = isCreated ? null : JsonSerializer.Serialize(propertyEntry.OriginalValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength),
PropertyName = $"{complexPropertyPath}.{propertyEntry.Metadata.Name}",
PropertyTypeFullName = propertyType.FullName!
});
}
}
foreach (var nestedComplexPropertyEntry in complexPropertyEntry.ComplexProperties)
{
AddComplexPropertyChanges(
nestedComplexPropertyEntry,
propertyChanges,
isCreated,
isDeleted,
isSoftDeleted,
complexPropertyPath);
}
}
/// <summary> /// <summary>
/// Determines the CLR type of a property based on its EF Core metadata and the values in the given <see cref="PropertyEntry"/>. /// Determines the CLR type of a property based on its EF Core metadata and the values in the given <see cref="PropertyEntry"/>.
/// </summary> /// </summary>
@ -262,7 +320,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
/// <see cref="PropertyEntry.OriginalValue"/>. If both values are <c>null</c>, the declared CLR type /// <see cref="PropertyEntry.OriginalValue"/>. If both values are <c>null</c>, the declared CLR type
/// (which may remain <see cref="object"/>) is returned. /// (which may remain <see cref="object"/>) is returned.
/// </returns> /// </returns>
protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) protected virtual Type DeterminePropertyTypeFromEntry(IReadOnlyPropertyBase property, PropertyEntry propertyEntry)
{ {
var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable();

47
framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerUIOptionsExtensions.cs

@ -0,0 +1,47 @@
using System;
using System.Text;
using System.Text.Json;
using JetBrains.Annotations;
using Swashbuckle.AspNetCore.SwaggerUI;
using Volo.Abp;
namespace Microsoft.Extensions.DependencyInjection;
public static class AbpSwaggerUIOptionsExtensions
{
/// <summary>
/// Sets the abp.appPath used by the Swagger UI scripts.
/// </summary>
/// <param name="options">The Swagger UI options.</param>
/// <param name="appPath">The application base path.</param>
public static void AbpAppPath([NotNull] this SwaggerUIOptions options, [NotNull] string appPath)
{
Check.NotNull(options, nameof(options));
Check.NotNull(appPath, nameof(appPath));
var normalizedAppPath = NormalizeAppPath(appPath);
options.HeadContent = BuildAppPathScript(normalizedAppPath, options.HeadContent ?? string.Empty);
}
private static string NormalizeAppPath(string appPath)
{
return string.IsNullOrWhiteSpace(appPath)
? "/"
: appPath.Trim().EnsureStartsWith('/').EnsureEndsWith('/');
}
private static string BuildAppPathScript(string normalizedAppPath, string headContent)
{
var builder = new StringBuilder(headContent);
if (builder.Length > 0)
{
builder.AppendLine();
}
builder.AppendLine("<script>");
builder.AppendLine(" var abp = abp || {};");
builder.AppendLine($" abp.appPath = {JsonSerializer.Serialize(normalizedAppPath)};");
builder.AppendLine("</script>");
return builder.ToString();
}
}

6
framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.js

@ -2,11 +2,7 @@ var abp = abp || {};
(function () { (function () {
/* Application paths *****************************************/ /* Application paths *****************************************/
abp.appPath = abp.appPath || '/';
//Current application root path (including virtual directory if exists).
var baseElement = document.querySelector('base');
var baseHref = baseElement ? baseElement.getAttribute('href') : null;
abp.appPath = baseHref || abp.appPath || '/';
/* UTILS ***************************************************/ /* UTILS ***************************************************/

2
framework/src/Volo.Abp.Swashbuckle/wwwroot/swagger/ui/abp.swagger.js

@ -11,7 +11,7 @@ var abp = abp || {};
var oidcSupportedScopes = configObject.oidcSupportedScopes || []; var oidcSupportedScopes = configObject.oidcSupportedScopes || [];
var oidcDiscoveryEndpoint = configObject.oidcDiscoveryEndpoint || []; var oidcDiscoveryEndpoint = configObject.oidcDiscoveryEndpoint || [];
var tenantPlaceHolders = ["{{tenantId}}", "{{tenantName}}", "{0}"] var tenantPlaceHolders = ["{{tenantId}}", "{{tenantName}}", "{0}"]
abp.appPath = configObject.baseUrl || abp.appPath; abp.appPath = abp.appPath || "/";
var requestInterceptor = configObject.requestInterceptor; var requestInterceptor = configObject.requestInterceptor;
var responseInterceptor = configObject.responseInterceptor; var responseInterceptor = configObject.responseInterceptor;

1
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs

@ -61,6 +61,7 @@ public class AbpAuditingTestModule : AbpModule
); );
options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty))); options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty)));
options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithComplexProperty), type => type == typeof(AppEntityWithComplexProperty)));
}); });
context.Services.AddType<Auditing_Tests.MyAuditedObject1>(); context.Services.AddType<Auditing_Tests.MyAuditedObject1>();

37
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs

@ -0,0 +1,37 @@
using System;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities.Auditing;
namespace Volo.Abp.Auditing.App.Entities;
public class AppEntityWithComplexProperty : FullAuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public AppEntityContactInformation ContactInformation { get; set; }
[DisableAuditing]
public AppEntityContactInformation DisabledContactInformation { get; set; }
public AppEntityWithComplexProperty()
{
}
public AppEntityWithComplexProperty(Guid id, string name)
: base(id)
{
Name = name;
}
}
public class AppEntityContactInformation
{
public string Street { get; set; } = string.Empty;
public AppEntityContactLocation Location { get; set; } = new();
}
public class AppEntityContactLocation
{
public string City { get; set; } = string.Empty;
}

23
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs

@ -31,6 +31,7 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
public DbSet<AppEntityWithNavigationChildOneToMany> AppEntityWithNavigationChildOneToMany { get; set; } public DbSet<AppEntityWithNavigationChildOneToMany> AppEntityWithNavigationChildOneToMany { get; set; }
public DbSet<AppEntityWithNavigationsAndDisableAuditing> AppEntityWithNavigationsAndDisableAuditing { get; set; } public DbSet<AppEntityWithNavigationsAndDisableAuditing> AppEntityWithNavigationsAndDisableAuditing { get; set; }
public DbSet<AppEntityWithJsonProperty> EntitiesWithObjectProperty { get; set; } public DbSet<AppEntityWithJsonProperty> EntitiesWithObjectProperty { get; set; }
public DbSet<AppEntityWithComplexProperty> AppEntitiesWithComplexProperty { get; set; }
public AbpAuditingTestDbContext(DbContextOptions<AbpAuditingTestDbContext> options) public AbpAuditingTestDbContext(DbContextOptions<AbpAuditingTestDbContext> options)
: base(options) : base(options)
@ -77,5 +78,27 @@ public class AbpAuditingTestDbContext : AbpDbContext<AbpAuditingTestDbContext>
); );
}); });
}); });
modelBuilder.Entity<AppEntityWithComplexProperty>(b =>
{
b.ConfigureByConvention();
b.ComplexProperty(x => x.ContactInformation, cb =>
{
cb.Property(x => x.Street).IsRequired();
cb.ComplexProperty(x => x.Location, lb =>
{
lb.Property(x => x.City).IsRequired();
});
});
b.ComplexProperty(x => x.DisabledContactInformation, cb =>
{
cb.Property(x => x.Street).IsRequired();
cb.ComplexProperty(x => x.Location, lb =>
{
lb.Property(x => x.City).IsRequired();
});
});
});
} }
} }

162
framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute; using NSubstitute;
using Shouldly;
using Volo.Abp.Auditing.App.Entities; using Volo.Abp.Auditing.App.Entities;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
@ -820,6 +821,167 @@ public class Auditing_Tests : AbpAuditingTestBase
AuditingStore.ClearReceivedCalls(); AuditingStore.ClearReceivedCalls();
#pragma warning restore 4014 #pragma warning restore 4014
} }
[Fact]
public async Task Should_Write_AuditLog_For_Complex_Property_Changes()
{
var entityId = Guid.NewGuid();
var repository = ServiceProvider.GetRequiredService<IBasicRepository<AppEntityWithComplexProperty, Guid>>();
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = new AppEntityWithComplexProperty(entityId, "Test Entity")
{
ContactInformation = new AppEntityContactInformation
{
Street = "First Street",
Location = new AppEntityContactLocation
{
City = "First City"
}
},
DisabledContactInformation = new AppEntityContactInformation
{
Street = "Disabled Street",
Location = new AppEntityContactLocation
{
City = "Disabled City"
}
}
};
await repository.InsertAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 1 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Created &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithComplexProperty).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 3 &&
x.EntityChanges[0].PropertyChanges.Any(pc =>
pc.PropertyName == nameof(AppEntityWithComplexProperty.Name) &&
pc.OriginalValue == null &&
pc.NewValue == "\"Test Entity\"" &&
pc.PropertyTypeFullName == typeof(string).FullName) &&
x.EntityChanges[0].PropertyChanges.Any(pc =>
pc.PropertyName == "ContactInformation.Street" &&
pc.OriginalValue == null &&
pc.NewValue == "\"First Street\"" &&
pc.PropertyTypeFullName == typeof(string).FullName) &&
x.EntityChanges[0].PropertyChanges.Any(pc =>
pc.PropertyName == "ContactInformation.Location.City" &&
pc.OriginalValue == null &&
pc.NewValue == "\"First City\"" &&
pc.PropertyTypeFullName == typeof(string).FullName) &&
x.EntityChanges[0].PropertyChanges.All(pc =>
!pc.PropertyName.StartsWith(nameof(AppEntityWithComplexProperty.DisabledContactInformation)))));
AuditingStore.ClearReceivedCalls();
#pragma warning restore 4014
using (var scope = _auditingManager.BeginScope())
{
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.ContactInformation.Location.City = "Updated City";
entity.DisabledContactInformation.Street = "Updated Disabled Street";
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
await scope.SaveAsync();
}
}
#pragma warning disable 4014
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 1 &&
x.EntityChanges[0].ChangeType == EntityChangeType.Updated &&
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithComplexProperty).FullName &&
x.EntityChanges[0].PropertyChanges.Count == 1 &&
x.EntityChanges[0].PropertyChanges[0].PropertyName == "ContactInformation.Location.City" &&
x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"First City\"" &&
x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Updated City\"" &&
x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName));
AuditingStore.ClearReceivedCalls();
#pragma warning restore 4014
}
[Fact]
public async Task Should_Not_Update_Modification_Audit_Properties_When_Only_Disabled_Complex_Property_Changes()
{
var entityId = Guid.NewGuid();
var repository = ServiceProvider.GetRequiredService<IBasicRepository<AppEntityWithComplexProperty, Guid>>();
using (var uow = _unitOfWorkManager.Begin())
{
var entity = new AppEntityWithComplexProperty(entityId, "Test Entity")
{
ContactInformation = new AppEntityContactInformation
{
Street = "First Street",
Location = new AppEntityContactLocation
{
City = "First City"
}
},
DisabledContactInformation = new AppEntityContactInformation
{
Street = "Disabled Street",
Location = new AppEntityContactLocation
{
City = "Disabled City"
}
}
};
await repository.InsertAsync(entity);
await uow.CompleteAsync();
}
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.Name = "Updated Test Entity";
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
}
DateTime? lastModificationTime;
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
lastModificationTime = entity.LastModificationTime;
lastModificationTime.ShouldNotBeNull();
await uow.CompleteAsync();
}
await Task.Delay(10);
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.DisabledContactInformation.Street = "Updated Disabled Street";
await repository.UpdateAsync(entity);
await uow.CompleteAsync();
}
using (var uow = _unitOfWorkManager.Begin())
{
var entity = await repository.GetAsync(entityId);
entity.LastModificationTime.ShouldBe(lastModificationTime);
await uow.CompleteAsync();
}
}
} }
public class Auditing_DisableLogActionInfo_Tests : Auditing_Tests public class Auditing_DisableLogActionInfo_Tests : Auditing_Tests

156
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs

@ -6,6 +6,7 @@ using NSubstitute;
using Shouldly; using Shouldly;
using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.TestApp; using Volo.Abp.TestApp;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.TestApp.Testing; using Volo.Abp.TestApp.Testing;
using Xunit; using Xunit;
@ -85,6 +86,33 @@ public class Auditing_Tests : Auditing_Tests<AbpEntityFrameworkCoreTestModule>
})); }));
} }
[Fact]
public async Task Should_Set_Modification_If_Complex_Properties_Changed()
{
var city = Guid.NewGuid().ToString();
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.Location.City = city;
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.ContactInformation.ShouldNotBeNull();
douglas.ContactInformation!.Location.City.ShouldBe(city);
douglas.LastModificationTime.ShouldNotBeNull();
douglas.LastModificationTime.Value.ShouldBeLessThanOrEqualTo(Clock.Now);
douglas.LastModifierId.ShouldBe(CurrentUserId);
}));
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
}
[Fact] [Fact]
public async Task Should_Not_Set_Modification_If_Properties_HasDisableAuditing_UpdateModificationProps() public async Task Should_Not_Set_Modification_If_Properties_HasDisableAuditing_UpdateModificationProps()
{ {
@ -106,6 +134,50 @@ public class Auditing_Tests : Auditing_Tests<AbpEntityFrameworkCoreTestModule>
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>()); EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
} }
[Fact]
public async Task Should_Not_Set_Modification_If_ComplexProperties_HasDisableAuditing_UpdateModificationProps()
{
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.DisableAuditingUpdateModificationPropsProperty = Guid.NewGuid().ToString();
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.LastModificationTime.ShouldBeNull();
douglas.LastModifierId.ShouldBeNull();
}));
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
}
[Fact]
public async Task Should_Not_Set_Modification_If_Nested_ComplexProperties_HasDisableAuditing_UpdateModificationProps()
{
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.Location.DisableAuditingUpdateModificationPropsProperty = Guid.NewGuid().ToString();
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.LastModificationTime.ShouldBeNull();
douglas.LastModifierId.ShouldBeNull();
}));
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
}
[Fact] [Fact]
public async Task Should_Not_PublishEntityEvent_If_Properties_HasDisableAuditing_PublishEntityEventProperty() public async Task Should_Not_PublishEntityEvent_If_Properties_HasDisableAuditing_PublishEntityEventProperty()
{ {
@ -126,6 +198,48 @@ public class Auditing_Tests : Auditing_Tests<AbpEntityFrameworkCoreTestModule>
EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any<object>()); EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any<object>());
} }
[Fact]
public async Task Should_Not_PublishEntityEvent_If_ComplexProperties_HasDisableAuditing_PublishEntityEventProperty()
{
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.DisableAuditingPublishEntityEventProperty = Guid.NewGuid().ToString();
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.LastModificationTime.ShouldNotBeNull();
}));
EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any<object>());
}
[Fact]
public async Task Should_Not_PublishEntityEvent_If_Nested_ComplexProperties_HasDisableAuditing_PublishEntityEventProperty()
{
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.Location.DisableAuditingPublishEntityEventProperty = Guid.NewGuid().ToString();
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.LastModificationTime.ShouldNotBeNull();
}));
EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any<object>());
}
[Fact] [Fact]
public async Task Should_Set_Modification_And_PublishEntityEvent_If_Properties_HasDisableAuditing() public async Task Should_Set_Modification_And_PublishEntityEvent_If_Properties_HasDisableAuditing()
@ -146,4 +260,46 @@ public class Auditing_Tests : Auditing_Tests<AbpEntityFrameworkCoreTestModule>
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>()); EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
} }
[Fact]
public async Task Should_Set_Modification_And_PublishEntityEvent_If_ComplexProperties_HasDisableAuditing()
{
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.DisableAuditingProperty = Guid.NewGuid().ToString();
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.LastModificationTime.ShouldNotBeNull();
}));
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
}
[Fact]
public async Task Should_Set_Modification_And_PublishEntityEvent_If_Nested_ComplexProperties_HasDisableAuditing()
{
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ContactInformation ??= new PersonContactInformation();
douglas.ContactInformation.Location.DisableAuditingProperty = Guid.NewGuid().ToString();
}));
await WithUnitOfWorkAsync((async () =>
{
var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId);
douglas.ShouldNotBeNull();
douglas.LastModificationTime.ShouldNotBeNull();
}));
EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any<object>());
}
} }

8
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs

@ -78,6 +78,14 @@ public class TestMigrationsDbContext : AbpDbContext<TestMigrationsDbContext>
b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now); b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now);
b.Property(x => x.TenantId).HasColumnName("Tenant_Id"); b.Property(x => x.TenantId).HasColumnName("Tenant_Id");
b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted"); b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted");
b.ComplexProperty(x => x.ContactInformation, cb =>
{
cb.Property(x => x.Street).IsRequired();
cb.ComplexProperty(x => x.Location, locationBuilder =>
{
locationBuilder.Property(x => x.City).IsRequired();
});
});
}); });
modelBuilder.Entity<City>(b => modelBuilder.Entity<City>(b =>

8
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs

@ -92,6 +92,14 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now); b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now);
b.Property(x => x.TenantId).HasColumnName("Tenant_Id"); b.Property(x => x.TenantId).HasColumnName("Tenant_Id");
b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted"); b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted");
b.ComplexProperty(x => x.ContactInformation, cb =>
{
cb.Property(x => x.Street).IsRequired();
cb.ComplexProperty(x => x.Location, locationBuilder =>
{
locationBuilder.Property(x => x.City).IsRequired();
});
});
}); });
modelBuilder modelBuilder

32
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs

@ -32,6 +32,8 @@ public class Person : FullAuditedAggregateRoot<Guid>, IMultiTenant, IHasEntityVe
public virtual DateTime HasDefaultValue { get; set; } public virtual DateTime HasDefaultValue { get; set; }
public virtual PersonContactInformation? ContactInformation { get; set; }
public int EntityVersion { get; set; } public int EntityVersion { get; set; }
[DisableAuditing(UpdateModificationProps = false)] [DisableAuditing(UpdateModificationProps = false)]
@ -84,3 +86,33 @@ public class Person : FullAuditedAggregateRoot<Guid>, IMultiTenant, IHasEntityVe
); );
} }
} }
public class PersonContactInformation
{
public string Street { get; set; } = string.Empty;
public PersonContactLocation Location { get; set; } = new();
[DisableAuditing(UpdateModificationProps = false)]
public string? DisableAuditingUpdateModificationPropsProperty { get; set; }
[DisableAuditing(PublishEntityEvent = false)]
public string? DisableAuditingPublishEntityEventProperty { get; set; }
[DisableAuditing]
public string? DisableAuditingProperty { get; set; }
}
public class PersonContactLocation
{
public string City { get; set; } = string.Empty;
[DisableAuditing(UpdateModificationProps = false)]
public string? DisableAuditingUpdateModificationPropsProperty { get; set; }
[DisableAuditing(PublishEntityEvent = false)]
public string? DisableAuditingPublishEntityEventProperty { get; set; }
[DisableAuditing]
public string? DisableAuditingProperty { get; set; }
}

11
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs

@ -30,8 +30,15 @@ public class TestAppModule : AbpModule
context.Services.AddHttpContextAccessor(); context.Services.AddHttpContextAccessor();
context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, TestMemoryDistributedCache>()); context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, TestMemoryDistributedCache>());
context.Services.AddEntityCache<Product, Guid>(); context.Services.AddEntityCache<Product, Guid>(new DistributedCacheEntryOptions
context.Services.AddEntityCache<Product, ProductCacheItem, Guid>(); {
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(7)
});
context.Services.AddEntityCache<Product, ProductCacheItem, Guid>(new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(9)
});
context.Services.AddEntityCache<Product, ProductCacheItem2, Guid>();
} }
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)

4
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs

@ -76,6 +76,10 @@ public class TestDataBuilder : ITransientDependency
private async Task AddPeople() private async Task AddPeople()
{ {
var douglas = new Person(UserDouglasId, "Douglas", 42, cityId: LondonCityId); var douglas = new Person(UserDouglasId, "Douglas", 42, cityId: LondonCityId);
douglas.ContactInformation = new PersonContactInformation
{
Street = "Test Street"
};
douglas.Phones.Add(new Phone(douglas.Id, "123456789")); douglas.Phones.Add(new Phone(douglas.Id, "123456789"));
douglas.Phones.Add(new Phone(douglas.Id, "123456780", PhoneType.Home)); douglas.Phones.Add(new Phone(douglas.Id, "123456780", PhoneType.Home));

56
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Shouldly; using Shouldly;
using Volo.Abp.Caching; using Volo.Abp.Caching;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
@ -116,6 +118,49 @@ public abstract class EntityCache_Tests<TStartupModule> : TestAppTestBase<TStart
productCacheItem.Name.ShouldBe("Product2"); productCacheItem.Name.ShouldBe("Product2");
productCacheItem.Price.ShouldBe(decimal.Zero); productCacheItem.Price.ShouldBe(decimal.Zero);
} }
[Fact]
public void EntityCache_Default_Options_Should_Be_2_Minutes()
{
var productCache = GetRequiredService<IDistributedCache<EntityCacheItemWrapper<ProductCacheItem2>, Guid>>();
var productOptions = GetDefaultCachingOptions(productCache);
productOptions.AbsoluteExpirationRelativeToNow.ShouldBe(TimeSpan.FromMinutes(2));
productOptions.SlidingExpiration.ShouldBeNull();
}
[Fact]
public void EntityCache_Configured_Options_Should_Be_Applied()
{
var productCache = GetRequiredService<IDistributedCache<EntityCacheItemWrapper<Product>, Guid>>();
var productCacheItemCache = GetRequiredService<IDistributedCache<EntityCacheItemWrapper<ProductCacheItem>, Guid>>();
var productOptions = GetDefaultCachingOptions(productCache);
productOptions.AbsoluteExpirationRelativeToNow.ShouldBe(TimeSpan.FromMinutes(7));
productOptions.SlidingExpiration.ShouldBeNull();
var productCacheItemOptions = GetDefaultCachingOptions(productCacheItemCache);
productCacheItemOptions.AbsoluteExpirationRelativeToNow.ShouldBe(TimeSpan.FromMinutes(9));
productCacheItemOptions.SlidingExpiration.ShouldBeNull();
}
private static DistributedCacheEntryOptions GetDefaultCachingOptions(object instance)
{
var internalCacheProperty = instance
.GetType()
.GetProperty("InternalCache", BindingFlags.Instance | BindingFlags.Public);
if (internalCacheProperty != null)
{
instance = internalCacheProperty.GetValue(instance);
}
var defaultOptionsField = instance
.GetType()
.GetField("DefaultCacheOptions", BindingFlags.Instance | BindingFlags.NonPublic);
return (DistributedCacheEntryOptions)defaultOptionsField.GetValue(instance);
}
} }
[Serializable] [Serializable]
@ -148,3 +193,14 @@ public class ProductCacheItem
public decimal Price { get; set; } public decimal Price { get; set; }
} }
[Serializable]
[CacheName("ProductCacheItem2")]
public class ProductCacheItem2
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "إدارة الإعداد", "Feature:SettingManagementGroup": "إدارة الإعداد",
"Feature:SettingManagementEnable": "تمكين إدارة الإعداد", "Feature:SettingManagementEnable": "تمكين إدارة الإعداد",
"Feature:SettingManagementEnableDescription": "تفعيل إعداد نظام الإدارة في التطبيق.", "Feature:SettingManagementEnableDescription": "تفعيل إعداد نظام الإدارة في التطبيق.",
"Feature:AllowChangingEmailSettings": "السماح لتغيير إعدادات البريد الإلكتروني.", "Feature:AllowChangingEmailSettings": "السماح لتغيير إعدادات البريد الإلكتروني",
"Feature:AllowChangingEmailSettingsDescription": "السماح لتغيير إعدادات البريد الإلكتروني.", "Feature:AllowChangingEmailSettingsDescription": "السماح لتغيير إعدادات البريد الإلكتروني.",
"SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور" "SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Správa nastavení", "Feature:SettingManagementGroup": "Správa nastavení",
"Feature:SettingManagementEnable": "Povolit správu nastavení", "Feature:SettingManagementEnable": "Povolit správu nastavení",
"Feature:SettingManagementEnableDescription": "Povolit systém správy nastavení v aplikaci.", "Feature:SettingManagementEnableDescription": "Povolit systém správy nastavení v aplikaci.",
"Feature:AllowChangingEmailSettings": "Povolit změnu nastavení e-mailu.", "Feature:AllowChangingEmailSettings": "Povolit změnu nastavení e-mailu",
"Feature:AllowChangingEmailSettingsDescription": "Povolit změnu nastavení e-mailu.", "Feature:AllowChangingEmailSettingsDescription": "Povolit změnu nastavení e-mailu.",
"SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla" "SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Einstellungsverwaltung", "Feature:SettingManagementGroup": "Einstellungsverwaltung",
"Feature:SettingManagementEnable": "Einstellungsverwaltung aktivieren", "Feature:SettingManagementEnable": "Einstellungsverwaltung aktivieren",
"Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.", "Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.",
"Feature:AllowChangingEmailSettings": "Änderung der E-Mail-Einstellungen zulassen.", "Feature:AllowChangingEmailSettings": "Änderung der E-Mail-Einstellungen zulassen",
"Feature:AllowChangingEmailSettingsDescription": "Änderung der E-Mail-Einstellungen zulassen.", "Feature:AllowChangingEmailSettingsDescription": "Änderung der E-Mail-Einstellungen zulassen.",
"SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren" "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json

@ -36,7 +36,7 @@
"Feature:SettingManagementGroup": "Einstellungsmanagement", "Feature:SettingManagementGroup": "Einstellungsmanagement",
"Feature:SettingManagementEnable": "Aktivieren Sie die Einstellungsverwaltung", "Feature:SettingManagementEnable": "Aktivieren Sie die Einstellungsverwaltung",
"Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.", "Feature:SettingManagementEnableDescription": "Aktivieren Sie das Einstellungsverwaltungssystem in der Anwendung.",
"Feature:AllowChangingEmailSettings": "Erlauben Sie das Ändern der E-Mail-Einstellungen.", "Feature:AllowChangingEmailSettings": "Erlauben Sie das Ändern der E-Mail-Einstellungen",
"Feature:AllowChangingEmailSettingsDescription": "Erlauben Sie das Ändern der E-Mail-Einstellungen.", "Feature:AllowChangingEmailSettingsDescription": "Erlauben Sie das Ändern der E-Mail-Einstellungen.",
"SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren" "SmtpPasswordPlaceholder": "Geben Sie einen Wert ein, um das Passwort zu aktualisieren"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json

@ -31,7 +31,7 @@
"Feature:SettingManagementGroup": "Διαχείριση ρυθμίσεων", "Feature:SettingManagementGroup": "Διαχείριση ρυθμίσεων",
"Feature:SettingManagementEnable": "Ενεργοποίηση διαχείρισης ρυθμίσεων", "Feature:SettingManagementEnable": "Ενεργοποίηση διαχείρισης ρυθμίσεων",
"Feature:SettingManagementEnableDescription": "Ενεργοποίηση συστήματος διαχείρισης ρυθμίσεων στην εφαρμογή.", "Feature:SettingManagementEnableDescription": "Ενεργοποίηση συστήματος διαχείρισης ρυθμίσεων στην εφαρμογή.",
"Feature:AllowChangingEmailSettings": "Επιτρέψτε την αλλαγή των ρυθμίσεων email.", "Feature:AllowChangingEmailSettings": "Επιτρέψτε την αλλαγή των ρυθμίσεων email",
"Feature:AllowChangingEmailSettingsDescription": "Επιτρέψτε την αλλαγή των ρυθμίσεων email.", "Feature:AllowChangingEmailSettingsDescription": "Επιτρέψτε την αλλαγή των ρυθμίσεων email.",
"SmtpPasswordPlaceholder": "Εισαγάγετε μια τιμή για ενημέρωση κωδικού πρόσβασης" "SmtpPasswordPlaceholder": "Εισαγάγετε μια τιμή για ενημέρωση κωδικού πρόσβασης"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Setting management", "Feature:SettingManagementGroup": "Setting management",
"Feature:SettingManagementEnable": "Enable setting management", "Feature:SettingManagementEnable": "Enable setting management",
"Feature:SettingManagementEnableDescription": "Enable setting management system in the application.", "Feature:SettingManagementEnableDescription": "Enable setting management system in the application.",
"Feature:AllowChangingEmailSettings": "Allow changing email settings.", "Feature:AllowChangingEmailSettings": "Allow changing email settings",
"Feature:AllowChangingEmailSettingsDescription": "Allow changing email settings.", "Feature:AllowChangingEmailSettingsDescription": "Allow changing email settings.",
"SmtpPasswordPlaceholder": "Enter a value to update password" "SmtpPasswordPlaceholder": "Enter a value to update password"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Gestión de la configuración", "Feature:SettingManagementGroup": "Gestión de la configuración",
"Feature:SettingManagementEnable": "Habilitar la gestión de la configuración", "Feature:SettingManagementEnable": "Habilitar la gestión de la configuración",
"Feature:SettingManagementEnableDescription": "Habilite el sistema de gestión de la configuración en la aplicación.", "Feature:SettingManagementEnableDescription": "Habilite el sistema de gestión de la configuración en la aplicación.",
"Feature:AllowChangingEmailSettings": "Permitir cambiar la configuración de correo electrónico.", "Feature:AllowChangingEmailSettings": "Permitir cambiar la configuración de correo electrónico",
"Feature:AllowChangingEmailSettingsDescription": "Permitir cambiar la configuración de correo electrónico.", "Feature:AllowChangingEmailSettingsDescription": "Permitir cambiar la configuración de correo electrónico.",
"SmtpPasswordPlaceholder": "Ingrese un valor para actualizar la contraseña" "SmtpPasswordPlaceholder": "Ingrese un valor para actualizar la contraseña"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Asetusten hallinta", "Feature:SettingManagementGroup": "Asetusten hallinta",
"Feature:SettingManagementEnable": "Ota asetusten hallinta käyttöön", "Feature:SettingManagementEnable": "Ota asetusten hallinta käyttöön",
"Feature:SettingManagementEnableDescription": "Ota asetustenhallintajärjestelmä käyttöön sovelluksessa.", "Feature:SettingManagementEnableDescription": "Ota asetustenhallintajärjestelmä käyttöön sovelluksessa.",
"Feature:AllowChangingEmailSettings": "Salli sähköpostiasetusten muuttaminen.", "Feature:AllowChangingEmailSettings": "Salli sähköpostiasetusten muuttaminen",
"Feature:AllowChangingEmailSettingsDescription": "Salli sähköpostiasetusten muuttaminen.", "Feature:AllowChangingEmailSettingsDescription": "Salli sähköpostiasetusten muuttaminen.",
"SmtpPasswordPlaceholder": "Syötä arvo päivittääksesi salasana" "SmtpPasswordPlaceholder": "Syötä arvo päivittääksesi salasana"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Gestion des paramètres", "Feature:SettingManagementGroup": "Gestion des paramètres",
"Feature:SettingManagementEnable": "Activer la gestion des paramètres", "Feature:SettingManagementEnable": "Activer la gestion des paramètres",
"Feature:SettingManagementEnableDescription": "Activer le système de gestion des paramètres dans l'application.", "Feature:SettingManagementEnableDescription": "Activer le système de gestion des paramètres dans l'application.",
"Feature:AllowChangingEmailSettings": "Autoriser la modification des paramètres de messagerie.", "Feature:AllowChangingEmailSettings": "Autoriser la modification des paramètres de messagerie",
"Feature:AllowChangingEmailSettingsDescription": "Autoriser la modification des paramètres de messagerie.", "Feature:AllowChangingEmailSettingsDescription": "Autoriser la modification des paramètres de messagerie.",
"SmtpPasswordPlaceholder": "Entrez une valeur pour mettre à jour le mot de passe" "SmtpPasswordPlaceholder": "Entrez une valeur pour mettre à jour le mot de passe"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json

@ -36,7 +36,7 @@
"Feature:SettingManagementGroup": "Upravljanje postavkama", "Feature:SettingManagementGroup": "Upravljanje postavkama",
"Feature:SettingManagementEnable": "Omogući upravljanje postavkama", "Feature:SettingManagementEnable": "Omogući upravljanje postavkama",
"Feature:SettingManagementEnableDescription": "Omogućite sustav upravljanja postavkama u aplikaciji.", "Feature:SettingManagementEnableDescription": "Omogućite sustav upravljanja postavkama u aplikaciji.",
"Feature:AllowChangingEmailSettings": "Dopusti promjenu postavki e-pošte.", "Feature:AllowChangingEmailSettings": "Dopusti promjenu postavki e-pošte",
"Feature:AllowChangingEmailSettingsDescription": "Dopusti promjenu postavki e-pošte.", "Feature:AllowChangingEmailSettingsDescription": "Dopusti promjenu postavki e-pošte.",
"SmtpPasswordPlaceholder": "Unesite vrijednost za ažuriranje lozinke" "SmtpPasswordPlaceholder": "Unesite vrijednost za ažuriranje lozinke"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Beállításkezelés", "Feature:SettingManagementGroup": "Beállításkezelés",
"Feature:SettingManagementEnable": "Beállításkezelés engedélyezése", "Feature:SettingManagementEnable": "Beállításkezelés engedélyezése",
"Feature:SettingManagementEnableDescription": "A beállításkezelő rendszer engedélyezése az alkalmazásban.", "Feature:SettingManagementEnableDescription": "A beállításkezelő rendszer engedélyezése az alkalmazásban.",
"Feature:AllowChangingEmailSettings": "Az e-mail beállítások módosításának engedélyezése.", "Feature:AllowChangingEmailSettings": "Az e-mail beállítások módosításának engedélyezése",
"Feature:AllowChangingEmailSettingsDescription": "Az e-mail beállítások módosításának engedélyezése.", "Feature:AllowChangingEmailSettingsDescription": "Az e-mail beállítások módosításának engedélyezése.",
"SmtpPasswordPlaceholder": "Adjon meg egy értéket a jelszó frissítéséhez" "SmtpPasswordPlaceholder": "Adjon meg egy értéket a jelszó frissítéséhez"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Stillingar", "Feature:SettingManagementGroup": "Stillingar",
"Feature:SettingManagementEnable": "Virkja stillingar", "Feature:SettingManagementEnable": "Virkja stillingar",
"Feature:SettingManagementEnableDescription": "Virkja stillingar í forritinu.", "Feature:SettingManagementEnableDescription": "Virkja stillingar í forritinu.",
"Feature:AllowChangingEmailSettings": "Leyfa að breyta stillingum tölvupósts.", "Feature:AllowChangingEmailSettings": "Leyfa að breyta stillingum tölvupósts",
"Feature:AllowChangingEmailSettingsDescription": "Leyfa að breyta stillingum tölvupósts.", "Feature:AllowChangingEmailSettingsDescription": "Leyfa að breyta stillingum tölvupósts.",
"SmtpPasswordPlaceholder": "Sláðu inn gildi til að uppfæra lykilorð" "SmtpPasswordPlaceholder": "Sláðu inn gildi til að uppfæra lykilorð"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Gestione Impostazioni", "Feature:SettingManagementGroup": "Gestione Impostazioni",
"Feature:SettingManagementEnable": "Abilita gestione impostazioni", "Feature:SettingManagementEnable": "Abilita gestione impostazioni",
"Feature:SettingManagementEnableDescription": "Abilita sistema gestione impostazioni nell'applicazione", "Feature:SettingManagementEnableDescription": "Abilita sistema gestione impostazioni nell'applicazione",
"Feature:AllowChangingEmailSettings": "Consenti di modificare le loro impostazioni e-mail.", "Feature:AllowChangingEmailSettings": "Consenti di modificare le loro impostazioni e-mail",
"Feature:AllowChangingEmailSettingsDescription": "Consenti di modificare le loro impostazioni e-mail.", "Feature:AllowChangingEmailSettingsDescription": "Consenti di modificare le loro impostazioni e-mail.",
"SmtpPasswordPlaceholder": "Inserisci un valore per aggiornare la password" "SmtpPasswordPlaceholder": "Inserisci un valore per aggiornare la password"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Instellingsbeheer", "Feature:SettingManagementGroup": "Instellingsbeheer",
"Feature:SettingManagementEnable": "Instellingenbeheer inschakelen", "Feature:SettingManagementEnable": "Instellingenbeheer inschakelen",
"Feature:SettingManagementEnableDescription": "Schakel het instellingsbeheersysteem in de toepassing in.", "Feature:SettingManagementEnableDescription": "Schakel het instellingsbeheersysteem in de toepassing in.",
"Feature:AllowChangingEmailSettings": "Toestaan om e-mailinstellingen te wijzigen.", "Feature:AllowChangingEmailSettings": "Toestaan om e-mailinstellingen te wijzigen",
"Feature:AllowChangingEmailSettingsDescription": "Toestaan om e-mailinstellingen te wijzigen.", "Feature:AllowChangingEmailSettingsDescription": "Toestaan om e-mailinstellingen te wijzigen.",
"SmtpPasswordPlaceholder": "Voer een waarde in om het wachtwoord bij te werken" "SmtpPasswordPlaceholder": "Voer een waarde in om het wachtwoord bij te werken"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Zarządzanie ustawieniami", "Feature:SettingManagementGroup": "Zarządzanie ustawieniami",
"Feature:SettingManagementEnable": "Włącz zarządzanie ustawieniami", "Feature:SettingManagementEnable": "Włącz zarządzanie ustawieniami",
"Feature:SettingManagementEnableDescription": "Włącz system zarządzania ustawieniami w aplikacji.", "Feature:SettingManagementEnableDescription": "Włącz system zarządzania ustawieniami w aplikacji.",
"Feature:AllowChangingEmailSettings": "Zezwól na zmianę ustawień poczty e-mail.", "Feature:AllowChangingEmailSettings": "Zezwól na zmianę ustawień poczty e-mail",
"Feature:AllowChangingEmailSettingsDescription": "Zezwól na zmianę ustawień poczty e-mail.", "Feature:AllowChangingEmailSettingsDescription": "Zezwól na zmianę ustawień poczty e-mail.",
"SmtpPasswordPlaceholder": "Wprowadź wartość, aby zaktualizować hasło" "SmtpPasswordPlaceholder": "Wprowadź wartość, aby zaktualizować hasło"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Gestão de Cenários", "Feature:SettingManagementGroup": "Gestão de Cenários",
"Feature:SettingManagementEnable": "Habilitar gerenciamento de configuração", "Feature:SettingManagementEnable": "Habilitar gerenciamento de configuração",
"Feature:SettingManagementEnableDescription": "Habilite o sistema de gerenciamento de configuração no aplicativo.", "Feature:SettingManagementEnableDescription": "Habilite o sistema de gerenciamento de configuração no aplicativo.",
"Feature:AllowChangingEmailSettings": "Permitir alterar as configurações de e-mail.", "Feature:AllowChangingEmailSettings": "Permitir alterar as configurações de e-mail",
"Feature:AllowChangingEmailSettingsDescription": "Permitir alterar as configurações de e-mail.", "Feature:AllowChangingEmailSettingsDescription": "Permitir alterar as configurações de e-mail.",
"SmtpPasswordPlaceholder": "Digite um valor para atualizar a senha" "SmtpPasswordPlaceholder": "Digite um valor para atualizar a senha"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Administrarea setărilor", "Feature:SettingManagementGroup": "Administrarea setărilor",
"Feature:SettingManagementEnable": "Activează administrarea setărilor", "Feature:SettingManagementEnable": "Activează administrarea setărilor",
"Feature:SettingManagementEnableDescription": "Activează sistemul de administrare a setărilor în aplicaţie.", "Feature:SettingManagementEnableDescription": "Activează sistemul de administrare a setărilor în aplicaţie.",
"Feature:AllowChangingEmailSettings": "Permiteți modificarea setărilor de e-mail.", "Feature:AllowChangingEmailSettings": "Permiteți modificarea setărilor de e-mail",
"Feature:AllowChangingEmailSettingsDescription": "Permiteți modificarea setărilor de e-mail.", "Feature:AllowChangingEmailSettingsDescription": "Permiteți modificarea setărilor de e-mail.",
"SmtpPasswordPlaceholder": "Introduceți o valoare pentru a actualiza parola" "SmtpPasswordPlaceholder": "Introduceți o valoare pentru a actualiza parola"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Управление настройками", "Feature:SettingManagementGroup": "Управление настройками",
"Feature:SettingManagementEnable": "Включить управление настройками", "Feature:SettingManagementEnable": "Включить управление настройками",
"Feature:SettingManagementEnableDescription": "Включите систему управления настройками в приложении.", "Feature:SettingManagementEnableDescription": "Включите систему управления настройками в приложении.",
"Feature:AllowChangingEmailSettings": "Разрешить изменение настроек электронной почты.", "Feature:AllowChangingEmailSettings": "Разрешить изменение настроек электронной почты",
"Feature:AllowChangingEmailSettingsDescription": "Разрешить изменение настроек электронной почты.", "Feature:AllowChangingEmailSettingsDescription": "Разрешить изменение настроек электронной почты.",
"SmtpPasswordPlaceholder": "Введите значение для обновления пароля" "SmtpPasswordPlaceholder": "Введите значение для обновления пароля"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Správa nastavení", "Feature:SettingManagementGroup": "Správa nastavení",
"Feature:SettingManagementEnable": "Povoliť správu nastavení", "Feature:SettingManagementEnable": "Povoliť správu nastavení",
"Feature:SettingManagementEnableDescription": "Povoliť systém správy nastavení v aplikácii.", "Feature:SettingManagementEnableDescription": "Povoliť systém správy nastavení v aplikácii.",
"Feature:AllowChangingEmailSettings": "Povoliť zmenu nastavení e-mailu.", "Feature:AllowChangingEmailSettings": "Povoliť zmenu nastavení e-mailu",
"Feature:AllowChangingEmailSettingsDescription": "Povoliť zmenu nastavení e-mailu.", "Feature:AllowChangingEmailSettingsDescription": "Povoliť zmenu nastavení e-mailu.",
"SmtpPasswordPlaceholder": "Zadajte hodnotu pre aktualizáciu hesla" "SmtpPasswordPlaceholder": "Zadajte hodnotu pre aktualizáciu hesla"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Upravljanje nastavitev", "Feature:SettingManagementGroup": "Upravljanje nastavitev",
"Feature:SettingManagementEnable": "Omogoči upravljanje nastavitev", "Feature:SettingManagementEnable": "Omogoči upravljanje nastavitev",
"Feature:SettingManagementEnableDescription": "Omogočite nastavitev sistema upravljanja v aplikaciji.", "Feature:SettingManagementEnableDescription": "Omogočite nastavitev sistema upravljanja v aplikaciji.",
"Feature:AllowChangingEmailSettings": "Dovoli spreminjanje e-poštnih nastavitev.", "Feature:AllowChangingEmailSettings": "Dovoli spreminjanje e-poštnih nastavitev",
"Feature:AllowChangingEmailSettingsDescription": "Dovoli spreminjanje e-poštnih nastavitev.", "Feature:AllowChangingEmailSettingsDescription": "Dovoli spreminjanje e-poštnih nastavitev.",
"SmtpPasswordPlaceholder": "Vnesite vrednost za posodobitev gesla" "SmtpPasswordPlaceholder": "Vnesite vrednost za posodobitev gesla"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json

@ -34,7 +34,7 @@
"Feature:SettingManagementGroup": "Hantering av inställningar", "Feature:SettingManagementGroup": "Hantering av inställningar",
"Feature:SettingManagementEnable": "Aktivera hantering av inställningar", "Feature:SettingManagementEnable": "Aktivera hantering av inställningar",
"Feature:SettingManagementEnableDescription": "Aktivera inställningshanteringssystem i applikationen.", "Feature:SettingManagementEnableDescription": "Aktivera inställningshanteringssystem i applikationen.",
"Feature:AllowChangingEmailSettings": "Tillåt ändring av e-postinställningar.", "Feature:AllowChangingEmailSettings": "Tillåt ändring av e-postinställningar",
"Feature:AllowChangingEmailSettingsDescription": "Tillåt ändring av e-postinställningar.", "Feature:AllowChangingEmailSettingsDescription": "Tillåt ändring av e-postinställningar.",
"SmtpPasswordPlaceholder": "Ange ett värde för att uppdatera lösenordet" "SmtpPasswordPlaceholder": "Ange ett värde för att uppdatera lösenordet"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Ayar yönetimi", "Feature:SettingManagementGroup": "Ayar yönetimi",
"Feature:SettingManagementEnable": "Ayar yönetimini etkinleştir", "Feature:SettingManagementEnable": "Ayar yönetimini etkinleştir",
"Feature:SettingManagementEnableDescription": "Uygulamada ayar yönetim sistemini etkinleştirin.", "Feature:SettingManagementEnableDescription": "Uygulamada ayar yönetim sistemini etkinleştirin.",
"Feature:AllowChangingEmailSettings": "E-posta ayarlarını değiştirmeye izin verin.", "Feature:AllowChangingEmailSettings": "E-posta ayarlarını değiştirmeye izin verin",
"Feature:AllowChangingEmailSettingsDescription": "E-posta ayarlarını değiştirmeye izin verin.", "Feature:AllowChangingEmailSettingsDescription": "E-posta ayarlarını değiştirmeye izin verin.",
"SmtpPasswordPlaceholder": "Şifreyi güncellemek için bir değer girin" "SmtpPasswordPlaceholder": "Şifreyi güncellemek için bir değer girin"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "Cài đặt quản lý", "Feature:SettingManagementGroup": "Cài đặt quản lý",
"Feature:SettingManagementEnable": "Bật quản lý cài đặt", "Feature:SettingManagementEnable": "Bật quản lý cài đặt",
"Feature:SettingManagementEnableDescription": "Bật cài đặt hệ thống quản lý trong ứng dụng.", "Feature:SettingManagementEnableDescription": "Bật cài đặt hệ thống quản lý trong ứng dụng.",
"Feature:AllowChangingEmailSettings": "Cho phép thay đổi cài đặt email.", "Feature:AllowChangingEmailSettings": "Cho phép thay đổi cài đặt email",
"Feature:AllowChangingEmailSettingsDescription": "Cho phép thay đổi cài đặt email.", "Feature:AllowChangingEmailSettingsDescription": "Cho phép thay đổi cài đặt email.",
"SmtpPasswordPlaceholder": "Nhập một giá trị để cập nhật mật khẩu" "SmtpPasswordPlaceholder": "Nhập một giá trị để cập nhật mật khẩu"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "设置管理", "Feature:SettingManagementGroup": "设置管理",
"Feature:SettingManagementEnable": "启用设置管理", "Feature:SettingManagementEnable": "启用设置管理",
"Feature:SettingManagementEnableDescription": "在应用程序中启用设置管理系统。", "Feature:SettingManagementEnableDescription": "在应用程序中启用设置管理系统。",
"Feature:AllowChangingEmailSettings": "允许更改邮件设置", "Feature:AllowChangingEmailSettings": "允许更改邮件设置",
"Feature:AllowChangingEmailSettingsDescription": "允许更改邮件设置。", "Feature:AllowChangingEmailSettingsDescription": "允许更改邮件设置。",
"SmtpPasswordPlaceholder": "输入一个值以更新密码" "SmtpPasswordPlaceholder": "输入一个值以更新密码"
} }

2
modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json

@ -35,7 +35,7 @@
"Feature:SettingManagementGroup": "設定管理", "Feature:SettingManagementGroup": "設定管理",
"Feature:SettingManagementEnable": "啟用設定管理", "Feature:SettingManagementEnable": "啟用設定管理",
"Feature:SettingManagementEnableDescription": "在應用程序中啟用設定管理系統.", "Feature:SettingManagementEnableDescription": "在應用程序中啟用設定管理系統.",
"Feature:AllowChangingEmailSettings": "允許更改電子郵件設置", "Feature:AllowChangingEmailSettings": "允許更改電子郵件設置",
"Feature:AllowChangingEmailSettingsDescription": "允許更改電子郵件設置。", "Feature:AllowChangingEmailSettingsDescription": "允許更改電子郵件設置。",
"SmtpPasswordPlaceholder": "輸入一個值以更新密碼" "SmtpPasswordPlaceholder": "輸入一個值以更新密碼"
} }

4
npm/ng-packs/apps/dev-app/src/app/app.config.ts

@ -31,6 +31,10 @@ export const appConfig: ApplicationConfig = {
registerLocaleFn: registerLocaleForEsBuild(), registerLocaleFn: registerLocaleForEsBuild(),
sendNullsAsQueryParam: false, sendNullsAsQueryParam: false,
skipGetAppConfiguration: false, skipGetAppConfiguration: false,
uiLocalization: {
enabled: true,
basePath: '/assets/localization',
},
}), }),
), ),
provideAbpOAuth(), provideAbpOAuth(),

4
npm/ng-packs/apps/dev-app/src/app/app.routes.ts

@ -10,6 +10,10 @@ export const appRoutes: Routes = [
path: 'dynamic-form', path: 'dynamic-form',
loadComponent: () => import('./dynamic-form-page/dynamic-form-page.component').then(m => m.DynamicFormPageComponent), loadComponent: () => import('./dynamic-form-page/dynamic-form-page.component').then(m => m.DynamicFormPageComponent),
}, },
{
path: 'localization-test',
loadComponent: () => import('./localization-test/localization-test.component').then(m => m.LocalizationTestComponent),
},
{ {
path: 'account', path: 'account',
loadChildren: () => import('@abp/ng.account').then(m => m.createRoutes()), loadChildren: () => import('@abp/ng.account').then(m => m.createRoutes()),

8
npm/ng-packs/apps/dev-app/src/app/dynamic-form-page/dynamic-form-page.component.ts

@ -1,4 +1,4 @@
import { Component, inject, OnInit, ViewChild } from '@angular/core'; import { Component, inject, OnInit, viewChild } from '@angular/core';
import { DynamicFormComponent, FormFieldConfig } from '@abp/ng.components/dynamic-form'; import { DynamicFormComponent, FormFieldConfig } from '@abp/ng.components/dynamic-form';
import { FormConfigService } from './form-config.service'; import { FormConfigService } from './form-config.service';
@ -8,7 +8,7 @@ import { FormConfigService } from './form-config.service';
imports: [DynamicFormComponent], imports: [DynamicFormComponent],
}) })
export class DynamicFormPageComponent implements OnInit { export class DynamicFormPageComponent implements OnInit {
@ViewChild(DynamicFormComponent, { static: false }) dynamicFormComponent: DynamicFormComponent; readonly dynamicFormComponent = viewChild(DynamicFormComponent);
protected readonly formConfigService = inject(FormConfigService); protected readonly formConfigService = inject(FormConfigService);
formFields: FormFieldConfig[] = []; formFields: FormFieldConfig[] = [];
@ -27,12 +27,12 @@ export class DynamicFormPageComponent implements OnInit {
alert('✅ Form submitted successfully! Check the console for details.'); alert('✅ Form submitted successfully! Check the console for details.');
// Reset form after submission // Reset form after submission
this.dynamicFormComponent.resetForm(); this.dynamicFormComponent().resetForm();
} }
cancel() { cancel() {
console.log('❌ Form Cancelled'); console.log('❌ Form Cancelled');
alert('Form cancelled'); alert('Form cancelled');
this.dynamicFormComponent.resetForm(); this.dynamicFormComponent().resetForm();
} }
} }

1
npm/ng-packs/apps/dev-app/src/app/home/home.component.html

@ -1,6 +1,7 @@
<div class="container"> <div class="container">
<div class="text-center mb-4"> <div class="text-center mb-4">
<a routerLink="/dynamic-form" class="btn btn-primary">Go to Dynamic Form</a> <a routerLink="/dynamic-form" class="btn btn-primary">Go to Dynamic Form</a>
<a routerLink="/localization-test" class="btn btn-secondary ms-2">Test Hybrid Localization</a>
</div> </div>
<div class="p-5 text-center"> <div class="p-5 text-center">
<div class="d-inline-block bg-success text-white p-1 h5 rounded mb-4" role="alert"> <div class="d-inline-block bg-success text-white p-1 h5 rounded mb-4" role="alert">

58
npm/ng-packs/apps/dev-app/src/app/localization-test/localization-test.component.ts

@ -0,0 +1,58 @@
import { Component, inject, OnInit } from '@angular/core';
import { LocalizationPipe, UILocalizationService, SessionStateService } from '@abp/ng.core';
import { CommonModule } from '@angular/common';
import { CardComponent, CardBodyComponent } from '@abp/ng.theme.shared';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'app-localization-test',
standalone: true,
imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent, AsyncPipe],
template: `
<div class="container mt-5">
<h2>Hybrid Localization Test</h2>
<abp-card cardClass="mt-4">
<abp-card-body>
<h5>Backend Localization (if available)</h5>
<p><strong>MyProjectName::Welcome:</strong> {{ 'MyProjectName::Welcome' | abpLocalization }}</p>
<p><strong>AbpAccount::Login:</strong> {{ 'AbpAccount::Login' | abpLocalization }}</p>
</abp-card-body>
</abp-card>
<abp-card cardClass="mt-4">
<abp-card-body>
<h5>UI Localization (from /assets/localization/{{ currentLanguage$ | async }}.json)</h5>
<p><strong>MyProjectName::CustomKey:</strong> {{ 'MyProjectName::CustomKey' | abpLocalization }}</p>
<p><strong>MyProjectName::TestMessage:</strong> {{ 'MyProjectName::TestMessage' | abpLocalization }}</p>
</abp-card-body>
</abp-card>
<abp-card cardClass="mt-4">
<abp-card-body>
<h5>UI Override (UI > Backend Priority)</h5>
<p><strong>AbpAccount::Login:</strong> {{ 'AbpAccount::Login' | abpLocalization }}</p>
<p class="text-muted">If backend has "Login", UI version should override it</p>
</abp-card-body>
</abp-card>
<abp-card cardClass="mt-4">
<abp-card-body>
<h5>Loaded UI Localizations</h5>
<pre>{{ loadedLocalizations | json }}</pre>
</abp-card-body>
</abp-card>
</div>
`,
})
export class LocalizationTestComponent implements OnInit {
private uiLocalizationService = inject(UILocalizationService);
private sessionState = inject(SessionStateService);
loadedLocalizations: any = {};
currentLanguage$ = this.sessionState.getLanguage$();
ngOnInit() {
this.loadedLocalizations = this.uiLocalizationService.getLoadedLocalizations();
}
}

10
npm/ng-packs/apps/dev-app/src/assets/localization/en.json

@ -0,0 +1,10 @@
{
"MyProjectName": {
"Welcome": "Welcome from UI (en.json)",
"CustomKey": "This is a UI-only localization",
"TestMessage": "UI localization is working!"
},
"AbpAccount": {
"Login": "Sign In (UI Override)"
}
}

10
npm/ng-packs/apps/dev-app/src/assets/localization/tr.json

@ -0,0 +1,10 @@
{
"MyProjectName": {
"Welcome": "UI'dan Hoş Geldiniz (tr.json)",
"CustomKey": "Bu sadece UI'da olan bir localization",
"TestMessage": "UI localization çalışıyor!"
},
"AbpAccount": {
"Login": "Giriş Yap (UI Override)"
}
}

2
npm/ng-packs/package.json

@ -58,7 +58,7 @@
"@angular-eslint/eslint-plugin-template": "~21.0.0", "@angular-eslint/eslint-plugin-template": "~21.0.0",
"@angular-eslint/template-parser": "~21.0.0", "@angular-eslint/template-parser": "~21.0.0",
"@angular/animations": "21.0.0", "@angular/animations": "21.0.0",
"@angular/aria": "21.0.0", "@angular/aria": "~21.1.0",
"@angular/build": "~21.0.0", "@angular/build": "~21.0.0",
"@angular/cli": "~21.0.0", "@angular/cli": "~21.0.0",
"@angular/common": "~21.0.0", "@angular/common": "~21.0.0",

8
npm/ng-packs/packages/components/dynamic-form/src/dynamic-form-field/dynamic-form-field-host.component.ts

@ -1,6 +1,5 @@
import { import {
Component, Component,
ViewChild,
ViewContainerRef, ViewContainerRef,
ChangeDetectionStrategy, ChangeDetectionStrategy,
forwardRef, forwardRef,
@ -9,6 +8,7 @@ import {
DestroyRef, DestroyRef,
inject, inject,
input, input,
viewChild
} from '@angular/core'; } from '@angular/core';
import { import {
ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl, ReactiveFormsModule
@ -34,7 +34,7 @@ export class DynamicFieldHostComponent implements ControlValueAccessor {
component = input<Type<ControlValueAccessor>>(); component = input<Type<ControlValueAccessor>>();
inputs = input<Record<string, any>>({}); inputs = input<Record<string, any>>({});
@ViewChild('vcRef', { read: ViewContainerRef, static: true }) viewContainerRef!: ViewContainerRef; readonly viewContainerRef = viewChild.required('vcRef', { read: ViewContainerRef });
private componentRef?: any; private componentRef?: any;
private value: any; private value: any;
@ -55,10 +55,10 @@ export class DynamicFieldHostComponent implements ControlValueAccessor {
} }
private createChild() { private createChild() {
this.viewContainerRef.clear(); this.viewContainerRef().clear();
if (!this.component()) return; if (!this.component()) return;
this.componentRef = this.viewContainerRef.createComponent(this.component()); this.componentRef = this.viewContainerRef().createComponent(this.component());
this.applyInputs(); this.applyInputs();
const instance: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl; const instance: any = this.componentRef.instance as controlValueAccessorLike & acceptsFormControl;

10
npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts

@ -6,7 +6,7 @@ import {
input, input,
Optional, Optional,
SkipSelf, SkipSelf,
ViewChild, viewChild
} from '@angular/core'; } from '@angular/core';
import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms';
import { import {
@ -76,14 +76,14 @@ export class ExtensibleDateTimePickerComponent {
meridian = input<boolean>(false); meridian = input<boolean>(false);
placement = input<Placement>('bottom-left'); placement = input<Placement>('bottom-left');
@ViewChild(NgbInputDatepicker) date!: NgbInputDatepicker; readonly date = viewChild.required(NgbInputDatepicker);
@ViewChild(NgbTimepicker) time!: NgbTimepicker; readonly time = viewChild.required(NgbTimepicker);
setDate(dateStr: string) { setDate(dateStr: string) {
this.date.writeValue(dateStr); this.date().writeValue(dateStr);
} }
setTime(dateStr: string) { setTime(dateStr: string) {
this.time.writeValue(dateStr); this.time().writeValue(dateStr);
} }
} }

14
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -19,9 +19,7 @@ import {
Optional, Optional,
SimpleChanges, SimpleChanges,
SkipSelf, SkipSelf,
ViewChild, viewChild,
signal,
effect,
} from '@angular/core'; } from '@angular/core';
import { import {
ControlContainer, ControlContainer,
@ -72,8 +70,8 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f
AsyncPipe, AsyncPipe,
NgComponentOutlet, NgComponentOutlet,
NgTemplateOutlet, NgTemplateOutlet,
FormsModule FormsModule,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ExtensibleFormPropService], providers: [ExtensibleFormPropService],
viewProviders: [ viewProviders: [
@ -98,7 +96,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
@Input() prop!: FormProp; @Input() prop!: FormProp;
@Input() first?: boolean; @Input() first?: boolean;
@Input() isFirstGroup?: boolean; @Input() isFirstGroup?: boolean;
@ViewChild('field') private fieldRef!: ElementRef<HTMLElement>; private readonly fieldRef = viewChild.required<ElementRef<HTMLElement>>('field');
injectorForCustomComponent?: Injector; injectorForCustomComponent?: Injector;
asterisk = ''; asterisk = '';
@ -158,9 +156,9 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
} }
ngAfterViewInit() { ngAfterViewInit() {
if (this.isFirstGroup && this.first && this.fieldRef) { if (this.isFirstGroup && this.first && this.fieldRef()) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.fieldRef.nativeElement.focus(); this.fieldRef().nativeElement.focus();
}); });
} }
} }

6
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts

@ -6,9 +6,8 @@ import {
inject, inject,
Input, Input,
Optional, Optional,
QueryList,
SkipSelf, SkipSelf,
ViewChildren, viewChildren
} from '@angular/core'; } from '@angular/core';
import { ControlContainer, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { ControlContainer, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
@ -41,8 +40,7 @@ export class ExtensibleFormComponent<R = any> {
private readonly extensions = inject(ExtensionsService); private readonly extensions = inject(ExtensionsService);
private readonly identifier = inject(EXTENSIONS_IDENTIFIER); private readonly identifier = inject(EXTENSIONS_IDENTIFIER);
@ViewChildren(ExtensibleFormPropComponent) readonly formProps = viewChildren(ExtensibleFormPropComponent);
formProps!: QueryList<ExtensibleFormPropComponent>;
@Input() @Input()
set selectedRecord(record: R) { set selectedRecord(record: R) {

21
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

@ -4,7 +4,7 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed, computed,
ContentChild, EventEmitter,
inject, inject,
Injector, Injector,
Input, Input,
@ -16,8 +16,9 @@ import {
SimpleChanges, SimpleChanges,
TemplateRef, TemplateRef,
TrackByFunction, TrackByFunction,
ViewChild, output,
output contentChild,
viewChild
} from '@angular/core'; } from '@angular/core';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
@ -140,17 +141,16 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
@Input() rowDetailHeight: string | number = '100%'; @Input() rowDetailHeight: string | number = '100%';
readonly rowDetailToggle = output<R>(); readonly rowDetailToggle = output<R>();
@ContentChild(ExtensibleTableRowDetailComponent) readonly rowDetailComponent = contentChild(ExtensibleTableRowDetailComponent);
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
@ViewChild('table', { static: false }) table!: DatatableComponent; readonly table = viewChild.required<DatatableComponent>('table');
protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined { protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined {
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate; return this.rowDetailComponent()?.template() ?? this.rowDetailTemplate;
} }
protected get effectiveRowDetailHeight(): string | number { protected get effectiveRowDetailHeight(): string | number {
return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight; return this.rowDetailComponent()?.rowHeight() ?? this.rowDetailHeight;
} }
hasAtLeastOnePermittedAction: boolean; hasAtLeastOnePermittedAction: boolean;
@ -317,8 +317,9 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
} }
toggleExpandRow(row: R): void { toggleExpandRow(row: R): void {
if (this.table && this.table.rowDetail) { const table = this.table();
this.table.rowDetail.toggleExpandRow(row); if (table && table.rowDetail) {
table.rowDetail.toggleExpandRow(row);
} }
this.rowDetailToggle.emit(row); this.rowDetailToggle.emit(row);
} }

6
npm/ng-packs/packages/components/page/src/page.component.html

@ -1,6 +1,6 @@
@if (shouldRenderRow) { @if (shouldRenderRow) {
<div class="row entry-row"> <div class="row entry-row">
@if (customTitle) { @if (customTitle()) {
<ng-content select="abp-page-title-container"></ng-content> <ng-content select="abp-page-title-container"></ng-content>
} @else { } @else {
@if (title) { @if (title) {
@ -12,7 +12,7 @@
} }
} }
@if (customBreadcrumb) { @if (customBreadcrumb()) {
<ng-content select="abp-page-breadcrumb-container"></ng-content> <ng-content select="abp-page-breadcrumb-container"></ng-content>
} @else { } @else {
@if (breadcrumb) { @if (breadcrumb) {
@ -22,7 +22,7 @@
} }
} }
@if (customToolbar) { @if (customToolbar()) {
<ng-content select="abp-page-toolbar-container"></ng-content> <ng-content select="abp-page-toolbar-container"></ng-content>
} @else { } @else {
@if (toolbarVisible) { @if (toolbarVisible) {

15
npm/ng-packs/packages/components/page/src/page.component.ts

@ -1,4 +1,4 @@
import { Component, Input, ViewEncapsulation, ContentChild } from '@angular/core'; import { Component, Input, ViewEncapsulation, contentChild } from '@angular/core';
import { import {
PageTitleContainerComponent, PageTitleContainerComponent,
PageBreadcrumbContainerComponent, PageBreadcrumbContainerComponent,
@ -37,19 +37,18 @@ export class PageComponent {
toolbar: PageParts.toolbar, toolbar: PageParts.toolbar,
}; };
@ContentChild(PageTitleContainerComponent) customTitle?: PageTitleContainerComponent; readonly customTitle = contentChild(PageTitleContainerComponent);
@ContentChild(PageBreadcrumbContainerComponent) readonly customBreadcrumb = contentChild(PageBreadcrumbContainerComponent);
customBreadcrumb?: PageBreadcrumbContainerComponent; readonly customToolbar = contentChild(PageToolbarContainerComponent);
@ContentChild(PageToolbarContainerComponent) customToolbar?: PageToolbarContainerComponent;
get shouldRenderRow() { get shouldRenderRow() {
return !!( return !!(
this.title || this.title ||
this.toolbarVisible || this.toolbarVisible ||
this.breadcrumb || this.breadcrumb ||
this.customTitle || this.customTitle() ||
this.customBreadcrumb || this.customBreadcrumb() ||
this.customToolbar || this.customToolbar() ||
this.pageParts this.pageParts
); );
} }

8
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html

@ -7,7 +7,7 @@
[nzData]="nodes" [nzData]="nodes"
[nzTreeTemplate]="treeTemplate" [nzTreeTemplate]="treeTemplate"
[nzExpandedKeys]="expandedKeys" [nzExpandedKeys]="expandedKeys"
[nzExpandedIcon]="expandedIconTemplate?.template || defaultIconTemplate" [nzExpandedIcon]="expandedIconTemplate()?.template || defaultIconTemplate"
(nzExpandChange)="onExpandedKeysChange($event)" (nzExpandChange)="onExpandedKeysChange($event)"
(nzCheckboxChange)="onCheckboxChange($event)" (nzCheckboxChange)="onCheckboxChange($event)"
(nzOnDrop)="onDrop($event)" (nzOnDrop)="onDrop($event)"
@ -26,13 +26,13 @@
<div class="d-inline-flex align-items-center abp-ellipsis-inline"> <div class="d-inline-flex align-items-center abp-ellipsis-inline">
<ng-container <ng-container
*ngTemplateOutlet=" *ngTemplateOutlet="
customNodeTemplate ? customNodeTemplate?.template : defaultNodeTemplate; customNodeTemplate() ? customNodeTemplate()?.template : defaultNodeTemplate;
context: { $implicit: node } context: { $implicit: node }
" "
/> />
</div> </div>
@if (menu) { @if (menu()) {
<div <div
#dropdown="ngbDropdown" #dropdown="ngbDropdown"
class="d-inline-block ms-1" class="d-inline-block ms-1"
@ -48,7 +48,7 @@
aria-hidden="true" aria-hidden="true"
></i> ></i>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<ng-template *ngTemplateOutlet="menu; context: { $implicit: node }" /> <ng-template *ngTemplateOutlet="menu(); context: { $implicit: node }" />
</div> </div>
</div> </div>
} }

9
npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts

@ -2,7 +2,8 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ContentChild, contentChild,
EventEmitter,
inject, inject,
Input, Input,
OnInit, OnInit,
@ -59,9 +60,9 @@ export class TreeComponent implements OnInit {
dropdowns = {} as { [key: string]: NgbDropdown }; dropdowns = {} as { [key: string]: NgbDropdown };
@ContentChild('menu') menu: TemplateRef<any>; readonly menu = contentChild<TemplateRef<any>>('menu');
@ContentChild(TreeNodeTemplateDirective) customNodeTemplate: TreeNodeTemplateDirective; readonly customNodeTemplate = contentChild(TreeNodeTemplateDirective);
@ContentChild(ExpandedIconTemplateDirective) expandedIconTemplate: ExpandedIconTemplateDirective; readonly expandedIconTemplate = contentChild(ExpandedIconTemplateDirective);
readonly checkedKeysChange = output<any>(); readonly checkedKeysChange = output<any>();
readonly expandedKeysChange = output<string[]>(); readonly expandedKeysChange = output<string[]>();
readonly selectedNodeChange = output<any>(); readonly selectedNodeChange = output<any>();

20
npm/ng-packs/packages/core/src/lib/models/common.ts

@ -16,6 +16,26 @@ export namespace ABP {
othersGroup?: string; othersGroup?: string;
dynamicLayouts?: Map<string, string>; dynamicLayouts?: Map<string, string>;
disableProjectNameInTitle?: boolean; disableProjectNameInTitle?: boolean;
uiLocalization?: UILocalizationOptions;
}
export interface UILocalizationOptions {
/**
* Enable UI localization feature
* When enabled, localization files are automatically loaded based on selected language
* Files should be located at: {basePath}/{culture}.json
* Example: /assets/localization/en.json
* JSON format: { "ResourceName": { "Key": "Value" } }
* Merges with backend localizations (UI > Backend priority)
*/
enabled?: boolean;
/**
* Base path for localization JSON files
* Default: '/assets/localization'
* Files should be located at: {basePath}/{culture}.json
* Example: /assets/localization/en.json
*/
basePath?: string;
} }
export interface Child { export interface Child {

12
npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts

@ -23,7 +23,12 @@ import { RoutesHandler } from '../handlers';
import { ABP, SortableItem } from '../models'; import { ABP, SortableItem } from '../models';
import { AuthErrorFilterService } from '../abstracts'; import { AuthErrorFilterService } from '../abstracts';
import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants'; import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants';
import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services'; import {
LocalizationService,
LocalStorageListenerService,
AbpTitleStrategy,
UILocalizationService,
} from '../services';
import { DefaultQueueManager, getInitialData } from '../utils'; import { DefaultQueueManager, getInitialData } from '../utils';
import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './'; import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './';
import { timezoneInterceptor, transferStateInterceptor } from '../interceptors'; import { timezoneInterceptor, transferStateInterceptor } from '../interceptors';
@ -113,6 +118,11 @@ export function provideAbpCore(...features: CoreFeature<CoreFeatureKind>[]) {
inject(LocalizationService); inject(LocalizationService);
inject(LocalStorageListenerService); inject(LocalStorageListenerService);
inject(RoutesHandler); inject(RoutesHandler);
// Initialize UILocalizationService if UI-only mode is enabled
const options = inject(CORE_OPTIONS);
if (options?.uiLocalization?.enabled) {
inject(UILocalizationService);
}
await getInitialData(); await getInitialData();
}), }),
LocaleProvider, LocaleProvider,

4
npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts

@ -6,8 +6,8 @@ import { Injectable, inject } from '@angular/core';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class AbpApplicationConfigurationService { export class AbpApplicationConfigurationService {
private restService = inject(RestService); private restService = inject(RestService);
apiName = 'abp'; apiName = 'abp';

1
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -8,6 +8,7 @@ export * from './http-wait.service';
export * from './lazy-load.service'; export * from './lazy-load.service';
export * from './list.service'; export * from './list.service';
export * from './localization.service'; export * from './localization.service';
export * from './ui-localization.service';
export * from './multi-tenancy.service'; export * from './multi-tenancy.service';
export * from './permission.service'; export * from './permission.service';
export * from './replaceable-components.service'; export * from './replaceable-components.service';

5
npm/ng-packs/packages/core/src/lib/services/localization.service.ts

@ -60,6 +60,8 @@ export class LocalizationService {
private initLocalizationValues() { private initLocalizationValues() {
localizations$.subscribe(val => this.addLocalization(val)); localizations$.subscribe(val => this.addLocalization(val));
// Backend-based localization loading (always enabled)
// UI localizations are merged via addLocalization() (UI > Backend priority)
const legacyResources$ = this.configState.getDeep$('localization.values') as Observable< const legacyResources$ = this.configState.getDeep$('localization.values') as Observable<
Record<string, Record<string, string>> Record<string, Record<string, string>>
>; >;
@ -90,7 +92,8 @@ export class LocalizationService {
const resourceName = entry[0]; const resourceName = entry[0];
const remoteTexts = entry[1]; const remoteTexts = entry[1];
let resource = local?.get(resourceName) || {}; let resource = local?.get(resourceName) || {};
resource = { ...resource, ...remoteTexts }; // UI > Backend priority: local texts override remote texts
resource = { ...remoteTexts, ...resource };
local?.set(resourceName, resource); local?.set(resourceName, resource);
}); });

119
npm/ng-packs/packages/core/src/lib/services/ui-localization.service.ts

@ -0,0 +1,119 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, distinctUntilChanged, switchMap, of } from 'rxjs';
import { catchError, shareReplay, tap } from 'rxjs/operators';
import { ABP } from '../models/common';
import { LocalizationService } from './localization.service';
import { SessionStateService } from './session-state.service';
import { CORE_OPTIONS } from '../tokens/options.token';
export interface UILocalizationResource {
[resourceName: string]: Record<string, string>;
}
/**
* Service for managing UI localizations in ABP Angular applications.
* Automatically loads localization files based on selected language
* Merges with backend localizations (UI > Backend priority)
*/
@Injectable({ providedIn: 'root' })
export class UILocalizationService {
private http = inject(HttpClient);
private localizationService = inject(LocalizationService);
private sessionState = inject(SessionStateService);
private options = inject(CORE_OPTIONS);
private loadedLocalizations$ = new BehaviorSubject<Record<string, UILocalizationResource>>({});
private currentLanguage$ = this.sessionState.getLanguage$();
constructor() {
const uiLocalization = this.options.uiLocalization;
if (uiLocalization?.enabled) {
this.subscribeToLanguageChanges();
}
}
private subscribeToLanguageChanges() {
this.currentLanguage$
.pipe(
distinctUntilChanged(),
switchMap(culture => this.loadLocalizationFile(culture))
)
.subscribe();
}
private loadLocalizationFile(culture: string) {
const config = this.options.uiLocalization;
if (!config?.enabled) return of(null);
const basePath = config.basePath || '/assets/localization';
const url = `${basePath}/${culture}.json`;
return this.http.get<UILocalizationResource>(url).pipe(
catchError(() => {
// If file not found or error occurs, return null
return of(null);
}),
tap(data => {
if (data) {
this.processLocalizationData(culture, data);
}
}),
);
}
private processLocalizationData(culture: string, data: UILocalizationResource) {
const abpFormat: ABP.Localization[] = [
{
culture,
resources: Object.entries(data).map(([resourceName, texts]) => ({
resourceName,
texts,
})),
},
];
this.localizationService.addLocalization(abpFormat);
const current = this.loadedLocalizations$.value;
current[culture] = data;
this.loadedLocalizations$.next(current);
}
addAngularLocalizeLocalization(
culture: string,
resourceName: string,
translations: Record<string, string>,
): void {
const abpFormat: ABP.Localization[] = [
{
culture,
resources: [
{
resourceName,
texts: translations,
},
],
},
];
this.localizationService.addLocalization(abpFormat);
const current = this.loadedLocalizations$.value;
if (!current[culture]) {
current[culture] = {};
}
if (!current[culture][resourceName]) {
current[culture][resourceName] = {};
}
current[culture][resourceName] = {
...current[culture][resourceName],
...translations,
};
this.loadedLocalizations$.next(current);
}
getLoadedLocalizations(culture?: string): UILocalizationResource {
const lang = culture || this.sessionState.getLanguage();
return this.loadedLocalizations$.value[lang] || {};
}
}

160
npm/ng-packs/packages/core/src/lib/tests/ui-localization.service.spec.ts

@ -0,0 +1,160 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest';
import { of, Subject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { UILocalizationService } from '../services/ui-localization.service';
import { LocalizationService } from '../services/localization.service';
import { SessionStateService } from '../services/session-state.service';
import { CORE_OPTIONS } from '../tokens/options.token';
describe('UILocalizationService', () => {
let spectator: SpectatorService<UILocalizationService>;
let service: UILocalizationService;
let language$: Subject<string>;
let httpGet: ReturnType<typeof vi.fn>;
let addLocalizationSpy: ReturnType<typeof vi.fn>;
const createService = createServiceFactory({
service: UILocalizationService,
mocks: [HttpClient, LocalizationService],
providers: [
{
provide: SessionStateService,
useFactory: () => {
let currentLanguage = 'en';
language$ = new Subject<string>();
language$.subscribe(lang => {
currentLanguage = lang;
});
return {
getLanguage: vi.fn(() => currentLanguage),
getLanguage$: vi.fn(() => language$.asObservable()),
};
},
},
{
provide: CORE_OPTIONS,
useValue: {
uiLocalization: {
enabled: true,
basePath: '/assets/localization',
},
},
},
],
});
beforeEach(() => {
spectator = createService();
service = spectator.service;
const http = spectator.inject(HttpClient);
const localizationService = spectator.inject(LocalizationService);
httpGet = vi.fn();
(http as any).get = httpGet;
addLocalizationSpy = vi.fn();
(localizationService as any).addLocalization = addLocalizationSpy;
});
describe('when uiLocalization is enabled', () => {
it('should load localization file when language changes', () => {
const uiData = { MyApp: { Welcome: 'Welcome from UI' } };
httpGet.mockReturnValue(of(uiData));
language$.next('en');
expect(httpGet).toHaveBeenCalledWith('/assets/localization/en.json');
expect(addLocalizationSpy).toHaveBeenCalledWith([
{
culture: 'en',
resources: [{ resourceName: 'MyApp', texts: { Welcome: 'Welcome from UI' } }],
},
]);
});
it('should use default basePath when not provided', () => {
expect(httpGet).not.toHaveBeenCalled();
httpGet.mockReturnValue(of({}));
language$.next('en');
expect(httpGet).toHaveBeenCalledWith('/assets/localization/en.json');
});
it('should not call addLocalization when file is missing (HTTP error)', () => {
httpGet.mockReturnValue(throwError(() => new Error('404')));
language$.next('fr');
expect(httpGet).toHaveBeenCalledWith('/assets/localization/fr.json');
expect(addLocalizationSpy).not.toHaveBeenCalled();
});
it('should cache loaded data in getLoadedLocalizations', () => {
const uiData = { AbpAccount: { Login: 'Sign In (UI)' } };
httpGet.mockReturnValue(of(uiData));
language$.next('en');
const loaded = service.getLoadedLocalizations('en');
expect(loaded).toEqual(uiData);
});
it('should load again when language changes to another culture', () => {
httpGet.mockReturnValue(of({}));
language$.next('en');
expect(httpGet).toHaveBeenCalledTimes(1);
httpGet.mockClear();
httpGet.mockReturnValue(of({ MyApp: { Title: 'Titre' } }));
language$.next('fr');
expect(httpGet).toHaveBeenCalledWith('/assets/localization/fr.json');
expect(addLocalizationSpy).toHaveBeenCalledWith([
{
culture: 'fr',
resources: [{ resourceName: 'MyApp', texts: { Title: 'Titre' } }],
},
]);
});
});
describe('addAngularLocalizeLocalization', () => {
it('should add localization via LocalizationService (UI data in merge pipeline)', () => {
service.addAngularLocalizeLocalization('en', 'MyApp', {
CustomKey: 'UI Override',
});
expect(addLocalizationSpy).toHaveBeenCalledWith([
{
culture: 'en',
resources: [
{
resourceName: 'MyApp',
texts: { CustomKey: 'UI Override' },
},
],
},
]);
});
it('should merge into getLoadedLocalizations cache', () => {
service.addAngularLocalizeLocalization('en', 'MyApp', { Key1: 'Val1' });
service.addAngularLocalizeLocalization('en', 'MyApp', { Key2: 'Val2' });
const loaded = service.getLoadedLocalizations('en');
expect(loaded.MyApp).toEqual({ Key1: 'Val1', Key2: 'Val2' });
});
});
describe('getLoadedLocalizations', () => {
it('should return empty object when no culture loaded', () => {
expect(service.getLoadedLocalizations('de')).toEqual({});
});
it('should return current language when culture not passed', () => {
const uiData = { R: { K: 'V' } };
httpGet.mockReturnValue(of(uiData));
language$.next('tr');
const loaded = service.getLoadedLocalizations();
expect(loaded).toEqual(uiData);
});
});
});

2
npm/ng-packs/packages/feature-management/package.json

@ -11,7 +11,7 @@
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/aria": "~21.0.0" "@angular/aria": "~21.1.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

2
npm/ng-packs/packages/identity/package.json

@ -13,7 +13,7 @@
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/aria": "~21.0.0" "@angular/aria": "~21.1.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

5
npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

@ -39,7 +39,7 @@ import {
OnInit, OnInit,
TemplateRef, TemplateRef,
TrackByFunction, TrackByFunction,
ViewChild, viewChild
} from '@angular/core'; } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
@ -99,8 +99,7 @@ export class UsersComponent implements OnInit {
data: PagedResultDto<IdentityUserDto> = { items: [], totalCount: 0 }; data: PagedResultDto<IdentityUserDto> = { items: [], totalCount: 0 };
@ViewChild('modalContent', { static: false }) readonly modalContent = viewChild.required<TemplateRef<any>>('modalContent');
modalContent!: TemplateRef<any>;
form!: UntypedFormGroup; form!: UntypedFormGroup;

2
npm/ng-packs/packages/permission-management/package.json

@ -11,7 +11,7 @@
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/aria": "~21.0.0" "@angular/aria": "~21.1.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

25
npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts

@ -15,20 +15,22 @@ import {
UpdatePermissionDto, UpdatePermissionDto,
} from '@abp/ng.permission-management/proxy'; } from '@abp/ng.permission-management/proxy';
import { import {
afterNextRender,
Component, Component,
computed, computed,
DOCUMENT, DOCUMENT,
ElementRef, ElementRef,
inject, inject,
Injector,
Input, Input,
QueryList, QueryList,
signal, signal,
TrackByFunction, TrackByFunction,
ViewChildren, output,
output viewChildren
} from '@angular/core'; } from '@angular/core';
import { concat, of } from 'rxjs'; import { of } from 'rxjs';
import { finalize, switchMap, take, tap } from 'rxjs/operators'; import { finalize, switchMap, tap } from 'rxjs/operators';
import { PermissionManagement } from '../models'; import { PermissionManagement } from '../models';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@ -115,6 +117,7 @@ export class PermissionManagementComponent
protected readonly service = inject(PermissionsService); protected readonly service = inject(PermissionsService);
protected readonly configState = inject(ConfigStateService); protected readonly configState = inject(ConfigStateService);
protected readonly toasterService = inject(ToasterService); protected readonly toasterService = inject(ToasterService);
private readonly injector = inject(Injector);
private document = inject(DOCUMENT); private document = inject(DOCUMENT);
@Input() @Input()
@ -145,11 +148,9 @@ export class PermissionManagementComponent
this.openModal().subscribe(() => { this.openModal().subscribe(() => {
this._visible = true; this._visible = true;
this.visibleChange.emit(true); this.visibleChange.emit(true);
concat(this.selectAllInAllTabsRef.changes, this.selectAllInThisTabsRef.changes) afterNextRender(() => {
.pipe(take(1)) this.initModal();
.subscribe(() => { }, { injector: this.injector });
this.initModal();
});
}); });
} else { } else {
this.setSelectedGroup(null); this.setSelectedGroup(null);
@ -161,10 +162,8 @@ export class PermissionManagementComponent
readonly visibleChange = output<boolean>(); readonly visibleChange = output<boolean>();
@ViewChildren('selectAllInThisTabsRef') selectAllInThisTabsRef = viewChildren<ElementRef<HTMLInputElement>>('selectAllInThisTabsRef');
selectAllInThisTabsRef!: QueryList<ElementRef<HTMLInputElement>>; selectAllInAllTabsRef = viewChildren<ElementRef<HTMLInputElement>>('selectAllInAllTabsRef');
@ViewChildren('selectAllInAllTabsRef')
selectAllInAllTabsRef!: QueryList<ElementRef<HTMLInputElement>>;
data: GetPermissionListResultDto = { groups: [], entityDisplayName: '' }; data: GetPermissionListResultDto = { groups: [], entityDisplayName: '' };

44
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template

@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

7
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test-setup.ts.template

@ -0,0 +1,7 @@
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
// Initialize Angular testing environment
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());

26
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template

@ -1,26 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

2
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template

@ -14,7 +14,7 @@
] ]
}, },
"exclude": [ "exclude": [
"src/test.ts", "src/test-setup.ts",
"**/*.spec.ts" "**/*.spec.ts"
] ]
} }

12
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template

@ -3,15 +3,7 @@
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../out-tsc/spec", "outDir": "../../out-tsc/spec",
"types": [ "types": ["vitest/globals"]
"jasmine"
]
}, },
"files": [ "include": ["**/*.spec.ts", "**/*.d.ts"]
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
} }

44
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/karma.conf.js.template

@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

7
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test-setup.ts.template

@ -0,0 +1,7 @@
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
// Initialize Angular testing environment
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());

26
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/src/test.ts.template

@ -1,26 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

2
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template

@ -14,7 +14,7 @@
] ]
}, },
"exclude": [ "exclude": [
"src/test.ts", "src/test-setup.ts",
"**/*.spec.ts" "**/*.spec.ts"
] ]
} }

12
npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.spec.json.template

@ -3,15 +3,7 @@
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "../../out-tsc/spec", "outDir": "../../out-tsc/spec",
"types": [ "types": ["vitest/globals"]
"jasmine"
]
}, },
"files": [ "include": ["**/*.spec.ts", "**/*.d.ts"]
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
} }

6
npm/ng-packs/packages/schematics/src/utils/angular-schematic/generate-lib.ts

@ -39,11 +39,11 @@ export function addLibToWorkspaceFile(projectRoot: string, projectName: string):
}, },
}, },
test: { test: {
builder: Builders.Karma, builder: Builders.UnitTest,
options: { options: {
main: `${projectRoot}/src/test.ts`,
tsConfig: `${projectRoot}/tsconfig.spec.json`, tsConfig: `${projectRoot}/tsconfig.spec.json`,
karmaConfig: `${projectRoot}/karma.conf.js`, buildTarget: `${projectName}:build`,
runner: 'vitest',
}, },
}, },
}, },

1
npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts

@ -36,6 +36,7 @@ export enum Builders {
BuildExtractI18n = '@angular/build:extract-i18n', BuildExtractI18n = '@angular/build:extract-i18n',
Protractor = '@angular-devkit/build-angular:private-protractor', Protractor = '@angular-devkit/build-angular:private-protractor',
BuildApplication = '@angular/build:application', BuildApplication = '@angular/build:application',
UnitTest = '@angular/build:unit-test',
} }
export interface FileReplacements { export interface FileReplacements {

2
npm/ng-packs/packages/setting-management/package.json

@ -12,7 +12,7 @@
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/aria": "~21.0.0" "@angular/aria": "~21.1.0"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

7
npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts

@ -11,10 +11,9 @@ import {
ElementRef, ElementRef,
inject, inject,
Input, Input,
QueryList,
Renderer2, Renderer2,
TrackByFunction, TrackByFunction,
ViewChildren, viewChildren
} from '@angular/core'; } from '@angular/core';
import { NgTemplateOutlet, AsyncPipe } from '@angular/common'; import { NgTemplateOutlet, AsyncPipe } from '@angular/common';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
@ -41,7 +40,7 @@ export class RoutesComponent {
@Input() smallScreen?: boolean; @Input() smallScreen?: boolean;
@ViewChildren('childrenContainer') childrenContainers!: QueryList<ElementRef<HTMLDivElement>>; readonly childrenContainers = viewChildren<ElementRef<HTMLDivElement>>('childrenContainer');
rootDropdownExpand = {} as { [key: string]: boolean }; rootDropdownExpand = {} as { [key: string]: boolean };
@ -52,7 +51,7 @@ export class RoutesComponent {
} }
closeDropdown() { closeDropdown() {
this.childrenContainers.forEach(({ nativeElement }) => { this.childrenContainers().forEach(({ nativeElement }) => {
this.renderer.addClass(nativeElement, 'd-none'); this.renderer.addClass(nativeElement, 'd-none');
setTimeout(() => this.renderer.removeClass(nativeElement, 'd-none'), 0); setTimeout(() => this.renderer.removeClass(nativeElement, 'd-none'), 0);
}); });

9
npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

@ -5,9 +5,9 @@ import {
Input, Input,
OnInit, OnInit,
Renderer2, Renderer2,
ViewChild,
inject, inject,
output output,
viewChild
} from '@angular/core'; } from '@angular/core';
import { ABP, StopPropagationDirective } from '@abp/ng.core'; import { ABP, StopPropagationDirective } from '@abp/ng.core';
@ -69,8 +69,7 @@ export class ButtonComponent implements OnInit {
readonly abpBlur = output<FocusEvent>(); readonly abpBlur = output<FocusEvent>();
@ViewChild('button', { static: true }) readonly buttonRef = viewChild.required<ElementRef<HTMLButtonElement>>('button');
buttonRef!: ElementRef<HTMLButtonElement>;
get icon(): string { get icon(): string {
return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`; return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`;
@ -80,7 +79,7 @@ export class ButtonComponent implements OnInit {
if (this.attributes) { if (this.attributes) {
Object.keys(this.attributes).forEach(key => { Object.keys(this.attributes).forEach(key => {
if (this.attributes?.[key]) { if (this.attributes?.[key]) {
this.renderer.setAttribute(this.buttonRef.nativeElement, key, this.attributes[key]); this.renderer.setAttribute(this.buttonRef().nativeElement, key, this.attributes[key]);
} }
}); });
} }

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

Loading…
Cancel
Save