Browse Source

Merge branch 'dev' into feat/#23930

pull/24050/head
erdemcaygor 10 months ago
parent
commit
e620712177
  1. 14
      Directory.Packages.props
  2. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  3. BIN
      docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/cover.png
  4. 122
      docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/images/sitemap-architecture.svg
  5. 475
      docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/post.md
  6. 1
      docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/summary.md
  7. 6
      docs/en/docs-nav.json
  8. 4
      docs/en/framework/infrastructure/artificial-intelligence/index.md
  9. 218
      docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md
  10. 1
      docs/en/framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md
  11. BIN
      docs/en/images/ai-management-widget.png
  12. 344
      docs/en/modules/ai-management/index.md
  13. 71
      docs/en/studio/release-notes.md
  14. 5
      docs/en/studio/version-mapping.md
  15. 4
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/AbpPermissionOptions.cs
  16. 2
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/ICanAddChildPermission.cs
  17. 14
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs
  18. 7
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionManager.cs
  19. 1
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionValueProvider.cs
  20. 35
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
  21. 83
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs
  22. 8
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IHasResourcePermissions.cs
  23. 33
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionChecker.cs
  24. 83
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionStore.cs
  25. 12
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProvider.cs
  26. 8
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProviderManager.cs
  27. 43
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/NullResourcePermissionStore.cs
  28. 34
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionCheckerExtensions.cs
  29. 21
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionGrantInfo.cs
  30. 75
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionStoreExtensions.cs
  31. 22
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueCheckContext.cs
  32. 20
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProvider.cs
  33. 38
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValuesCheckContext.cs
  34. 21
      framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/ResourcePermissionRequirement.cs
  35. 13
      framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/KeyedObjectResourcePermissionExtenstions.cs
  36. 5
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs
  37. 8
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationPolicyProvider.cs
  38. 2
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/MethodInvocationAuthorizationService.cs
  39. 8
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IDynamicPermissionDefinitionStore.cs
  40. 8
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IStaticPermissionDefinitionStore.cs
  41. 19
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/NullDynamicPermissionDefinitionStore.cs
  42. 44
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs
  43. 28
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionCheckerExtensions.cs
  44. 33
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionRequirementHandler.cs
  45. 49
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionStoreExtensions.cs
  46. 173
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs
  47. 65
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionPopulator.cs
  48. 43
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProviderManager.cs
  49. 79
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/RoleResourcePermissionValueProvider.cs
  50. 46
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/UserResourcePermissionValueProvider.cs
  51. 32
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
  52. 11
      framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs
  53. 26
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
  54. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs
  55. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs
  56. 14
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs
  57. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs
  58. 1
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs
  59. 8
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs
  60. 23
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  61. 7
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs
  62. 29
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs
  63. 39
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Telemetry/NullTelemetryService.cs
  64. 44
      framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Telemetry/TelemetryCliSessionProvider.cs
  65. 6
      framework/src/Volo.Abp.Core/Volo/Abp/IKeyedObject.cs
  66. 47
      framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs
  67. 26
      framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs
  68. 39
      framework/src/Volo.Abp.Core/Volo/Abp/KeyedObjectHelper.cs
  69. 45
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs
  70. 2
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs
  71. 5
      framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/EntityDto.cs
  72. 5
      framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs
  73. 2
      framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/IEntityDto.cs
  74. 13
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs
  75. 2
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/IEntity.cs
  76. 2
      framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj
  77. 1
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  78. 49
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptions.cs
  79. 4
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs
  80. 30
      framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs
  81. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json
  82. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json
  83. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json
  84. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json
  85. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json
  86. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json
  87. 5
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json
  88. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json
  89. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json
  90. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json
  91. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json
  92. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json
  93. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json
  94. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json
  95. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json
  96. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json
  97. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json
  98. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json
  99. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json
  100. 3
      framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json

14
Directory.Packages.props

@ -7,7 +7,7 @@
<PackageVersion Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
<PackageVersion Include="aliyun-net-sdk-sts" Version="3.1.3" />
<PackageVersion Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
<PackageVersion Include="AsyncKeyedLock" Version="7.1.6" />
<PackageVersion Include="AsyncKeyedLock" Version="7.1.8" />
<PackageVersion Include="Autofac" Version="8.4.0" />
<PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
@ -20,10 +20,10 @@
<PackageVersion Include="Azure.Identity" Version="1.14.2" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.25.0" />
<PackageVersion Include="Blazorise" Version="1.8.6" />
<PackageVersion Include="Blazorise.Components" Version="1.8.6" />
<PackageVersion Include="Blazorise.DataGrid" Version="1.8.6" />
<PackageVersion Include="Blazorise.Snackbar" Version="1.8.6" />
<PackageVersion Include="Blazorise" Version="1.8.8" />
<PackageVersion Include="Blazorise.Components" Version="1.8.8" />
<PackageVersion Include="Blazorise.DataGrid" Version="1.8.8" />
<PackageVersion Include="Blazorise.Snackbar" Version="1.8.8" />
<PackageVersion Include="Castle.Core" Version="5.2.1" />
<PackageVersion Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageVersion Include="CommonMark.NET" Version="0.15.1" />
@ -137,7 +137,7 @@
<PackageVersion Include="OpenIddict.Server.AspNetCore" Version="7.2.0" />
<PackageVersion Include="OpenIddict.Validation.AspNetCore" Version="7.2.0" />
<PackageVersion Include="OpenIddict.Validation.ServerIntegration" Version="7.2.0" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.90" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="10.23.26000" />
<PackageVersion Include="Polly" Version="8.6.3" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
@ -196,4 +196,4 @@
<PackageVersion Include="Fody" Version="6.9.3" />
<PackageVersion Include="System.Management" Version="10.0.0" />
</ItemGroup>
</Project>
</Project>

2
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -269,7 +269,7 @@
"Referral.CannotDeleteUsedLink": "You cannot delete a referral link that has already been used.",
"Referral.CannotReferYourself": "You cannot create a referral link for your own email address.",
"Referral:TargetEmail": "Target Email",
"Referral.CannotReferSameOrganizationMember": "You cannot create a referral link for a user who is already a member of your organization.",
"Referral.CannotReferSameOrganizationMember": "Referral links cannot be used for existing organization members.",
"LinkCopiedToClipboard": "Link copied to clipboard",
"AreYouSureToDeleteReferralLink": "Are you sure you want to delete this referral link?",
"DefaultErrorMessage": "An error occurred."

BIN
docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

122
docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/images/sitemap-architecture.svg

@ -0,0 +1,122 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 700">
<defs>
<style>
.box { fill: #ffffff; stroke: #1890ff; stroke-width: 2; }
.title-box { fill: #1890ff; stroke: #1890ff; stroke-width: 2; }
.text { font-family: Arial, sans-serif; font-size: 14px; fill: #333333; font-weight: 600; }
.title-text { font-family: Arial, sans-serif; font-size: 14px; fill: #ffffff; font-weight: 600; }
.small-text { font-family: Arial, sans-serif; font-size: 11px; fill: #666666; }
.arrow { stroke: #1890ff; stroke-width: 2; fill: none; marker-end: url(#arrowhead); }
.layer-label { font-family: Arial, sans-serif; font-size: 16px; fill: #1890ff; font-weight: bold; }
</style>
<marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<polygon points="0 0, 10 3, 0 6" fill="#1890ff" />
</marker>
</defs>
<!-- Title -->
<text x="500" y="30" text-anchor="middle" class="layer-label" style="font-size: 20px;">Sitemap Module Architecture</text>
<!-- Discovery Layer -->
<text x="50" y="80" class="layer-label">1. Discovery Layer</text>
<rect class="title-box" x="50" y="90" width="220" height="35" rx="5" />
<text class="title-text" x="160" y="112" text-anchor="middle">RazorPageDiscoveryService</text>
<rect class="box" x="50" y="125" width="220" height="60" rx="5" />
<text class="small-text" x="160" y="145" text-anchor="middle">• Scans assemblies</text>
<text class="small-text" x="160" y="163" text-anchor="middle">• Finds PageModel classes</text>
<text class="small-text" x="160" y="181" text-anchor="middle">• Extracts route metadata</text>
<!-- Arrow down -->
<path class="arrow" d="M 160 185 L 160 225" />
<!-- Source Layer -->
<text x="50" y="245" class="layer-label">2. Source Layer</text>
<!-- Static Source -->
<rect class="title-box" x="50" y="260" width="200" height="35" rx="5" />
<text class="title-text" x="150" y="282" text-anchor="middle">IStaticPageSitemapSource</text>
<rect class="box" x="50" y="295" width="200" height="80" rx="5" />
<text class="small-text" x="150" y="315" text-anchor="middle">Processes attributes:</text>
<text class="small-text" x="150" y="333" text-anchor="middle">[IncludeSitemapXml]</text>
<text class="small-text" x="150" y="351" text-anchor="middle">Returns static page</text>
<text class="small-text" x="150" y="369" text-anchor="middle">sitemap items</text>
<!-- Dynamic Source -->
<rect class="title-box" x="280" y="260" width="200" height="35" rx="5" />
<text class="title-text" x="380" y="282" text-anchor="middle">IGroupedSitemapSource</text>
<rect class="box" x="280" y="295" width="200" height="80" rx="5" />
<text class="small-text" x="380" y="315" text-anchor="middle">Queries repositories:</text>
<text class="small-text" x="380" y="333" text-anchor="middle">Books, Articles, Products</text>
<text class="small-text" x="380" y="351" text-anchor="middle">Returns dynamic</text>
<text class="small-text" x="380" y="369" text-anchor="middle">sitemap items</text>
<!-- Custom Source -->
<rect class="title-box" x="510" y="260" width="200" height="35" rx="5" />
<text class="title-text" x="610" y="282" text-anchor="middle">Custom Sources</text>
<rect class="box" x="510" y="295" width="200" height="80" rx="5" />
<text class="small-text" x="610" y="315" text-anchor="middle">Implement</text>
<text class="small-text" x="610" y="333" text-anchor="middle">ISitemapItemSource</text>
<text class="small-text" x="610" y="351" text-anchor="middle">for custom logic</text>
<!-- Arrows to Collection Layer -->
<path class="arrow" d="M 150 375 L 150 420 L 400 420 L 400 445" />
<path class="arrow" d="M 380 375 L 380 420 L 400 420 L 400 445" />
<path class="arrow" d="M 610 375 L 610 420 L 400 420 L 400 445" />
<!-- Collection Layer -->
<text x="50" y="465" class="layer-label">3. Collection Layer</text>
<rect class="title-box" x="250" y="480" width="300" height="35" rx="5" />
<text class="title-text" x="400" y="502" text-anchor="middle">SitemapItemCollector</text>
<rect class="box" x="250" y="515" width="300" height="80" rx="5" />
<text class="small-text" x="400" y="535" text-anchor="middle">• Aggregates items from all sources</text>
<text class="small-text" x="400" y="553" text-anchor="middle">• Groups by category (Main, Blog, Products)</text>
<text class="small-text" x="400" y="571" text-anchor="middle">• Removes duplicates</text>
<text class="small-text" x="400" y="589" text-anchor="middle">• Returns Dictionary&lt;Group, Items&gt;</text>
<!-- Arrow down -->
<path class="arrow" d="M 400 595 L 400 635" />
<!-- Generation Layer -->
<text x="650" y="465" class="layer-label">4. Generation Layer</text>
<rect class="title-box" x="640" y="480" width="280" height="35" rx="5" />
<text class="title-text" x="780" y="502" text-anchor="middle">SitemapXmlGenerator</text>
<rect class="box" x="640" y="515" width="280" height="80" rx="5" />
<text class="small-text" x="780" y="535" text-anchor="middle">• Converts items to XML format</text>
<text class="small-text" x="780" y="553" text-anchor="middle">• Adds &lt;loc&gt;, &lt;lastmod&gt;, &lt;priority&gt;</text>
<text class="small-text" x="780" y="571" text-anchor="middle">• Validates against sitemap protocol</text>
<text class="small-text" x="780" y="589" text-anchor="middle">• Returns XML string</text>
<!-- Arrow from Collection to Generation -->
<path class="arrow" d="M 550 555 L 640 555" />
<!-- Management Layer -->
<text x="50" y="655" class="layer-label">5. Management Layer</text>
<!-- File Generator -->
<rect class="title-box" x="50" y="470" width="180" height="35" rx="5" />
<text class="title-text" x="140" y="492" text-anchor="middle">SitemapFileGenerator</text>
<rect class="box" x="50" y="505" width="180" height="90" rx="5" />
<text class="small-text" x="140" y="525" text-anchor="middle">• Orchestrates collection</text>
<text class="small-text" x="140" y="543" text-anchor="middle">• Calls XML generator</text>
<text class="small-text" x="140" y="561" text-anchor="middle">• Writes files to disk:</text>
<text class="small-text" x="140" y="579" text-anchor="middle">Sitemaps/</text>
<!-- Background Worker -->
<rect class="title-box" x="250" y="640" width="300" height="35" rx="5" />
<text class="title-text" x="400" y="662" text-anchor="middle">SitemapRegenerationWorker</text>
<rect class="box" x="250" y="675" width="300" height="60" rx="5" />
<text class="small-text" x="400" y="695" text-anchor="middle">• Runs periodically (e.g., hourly)</text>
<text class="small-text" x="400" y="713" text-anchor="middle">• Triggers SitemapFileGenerator</text>
<text class="small-text" x="400" y="731" text-anchor="middle">• Non-blocking background execution</text>
<!-- Arrow from Worker to File Generator -->
<path class="arrow" d="M 250 705 L 230 550" />
<!-- Output Files -->
<text x="750" y="625" class="small-text" fill="#666666">📄 sitemap.xml</text>
<text x="750" y="643" class="small-text" fill="#666666">📄 sitemap-Blog.xml</text>
<text x="750" y="661" class="small-text" fill="#666666">📄 sitemap-Products.xml</text>
<!-- Arrow from Generation to Files -->
<path class="arrow" d="M 780 595 L 780 620" />
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

475
docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/post.md

@ -0,0 +1,475 @@
# Building Dynamic XML Sitemaps with ABP Framework
Search Engine Optimization (SEO) is crucial for any web application that wants to be discovered by users. One of the most fundamental SEO practices is providing a comprehensive XML sitemap that helps search engines crawl and index your website efficiently. In this article, we'll use a reusable ABP module that automatically generates dynamic XML sitemaps for both static Razor Pages and dynamic content from your database.
By the end of this tutorial, you'll have a production-ready sitemap solution that discovers your pages automatically, includes dynamic content like blog posts or products, and regenerates sitemaps in the background without impacting performance.
## What is an XML Sitemap?
An XML sitemap is a file that lists all important pages of your website in a structured format that search engines can easily read. It acts as a roadmap for crawlers like Google, Bing, and others, telling them which pages exist, when they were last updated, and how they relate to each other.
For modern web applications with dynamic content, manually maintaining sitemap files quickly becomes impractical. A dynamic sitemap solution that automatically discovers and updates URLs is essential for:
- **Large content sites** with frequently changing blog posts, articles, or products
- **Multi-tenant applications** where each tenant may have different content
- **Enterprise applications** with complex page hierarchies
- **E-commerce platforms** with thousands of product pages
## Why Build a Custom Sitemap Module?
While there are general-purpose sitemap libraries available, building a custom module for ABP Framework provides several advantages:
✅ **Deep ABP Integration**: Leverages ABP's dependency injection, background workers, and module system
✅ **Automatic Discovery**: Uses ASP.NET Core's Razor Page infrastructure to automatically find pages
✅ **Type-Safe Configuration**: Strongly-typed attributes and options for configuration
✅ **Multi-Group Support**: Organize sitemaps by logical groups (main, blog, products, etc.)
✅ **Background Generation**: Non-blocking sitemap regeneration using ABP's background worker system
✅ **Repository Integration**: Direct integration with ABP repositories for database entities
## Project Architecture Overview
Before using the module, let's understand its architecture:
![Architecture Diagram](./images/sitemap-architecture.svg)
The sitemap module consists of several key components:
1. **Discovery Layer**: Discovers Razor Pages and their metadata using reflection
2. **Source Layer**: Defines contracts for providing sitemap items (static pages and dynamic content)
3. **Collection Layer**: Collects items from all registered sources
4. **Generation Layer**: Transforms collected items into XML format
5. **Management Layer**: Orchestrates file generation and background workers
## Installation
To get started, clone the demo repository which includes the sitemap module:
```bash
git clone https://github.com/salihozkara/AbpSitemapDemo
cd AbpSitemapDemo
```
The repository contains the sitemap module in the `Modules/abp.sitemap/` directory. To use it in your own project, add a project reference:
```xml
<ProjectReference Include="../Modules/abp.sitemap/Abp.Sitemap.Web/Abp.Sitemap.Web.csproj" />
```
## Module Configuration
After installing the package, add the module to your ABP application's module class:
```csharp
using Abp.Sitemap.Web;
[DependsOn(
typeof(SitemapWebModule), // 👈 Add sitemap module
// ... other dependencies
)]
public class YourProjectWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Configure sitemap options
Configure<SitemapOptions>(options =>
{
options.BaseUrl = "https://yourdomain.com"; // 👈 Your website URL
options.FolderPath = "Sitemaps"; // 👈 Where XML files are stored
options.WorkerPeriod = 3600000; // 👈 Regenerate every hour (in milliseconds)
});
}
}
```
> **Note:** In ABP applications, BaseUrl can be resolved from AppUrlOptions to stay consistent with environment configuration.
That's it! The module is now integrated and will automatically:
- Discover your Razor Pages
- Generate sitemap XML files on application startup
- Regenerate sitemaps in the background every hour
## Usage Examples
Let's explore practical examples of using the sitemap module. You can see complete working examples in the [AbpSitemapDemo repository](https://github.com/salihozkara/AbpSitemapDemo).
### Example 1: Mark Static Pages
The simplest way to include pages in your sitemap is using attributes:
```csharp
using Abp.Sitemap.Web.Sitemap.Sources.Page.Attributes;
namespace YourProject.Pages;
[IncludeSitemapXml] // 👈 Include in default "Main" group
public class IndexModel : PageModel
{
public void OnGet()
{
// Your page logic
}
}
[IncludeSitemapXml(Group = "Help")]
public class FaqModel : PageModel
{
public void OnGet()
{
// Your page logic
}
}
```
These pages will be automatically discovered and included in the sitemap XML files.
### Example 2: Add Dynamic Content from Database
For dynamic content like blog posts, products, or articles, create a custom sitemap source. Here's a complete example using a Book entity:
```csharp
using Abp.Sitemap.Web.Sitemap.Core;
using Abp.Sitemap.Web.Sitemap.Sources.Group;
using Volo.Abp.DependencyInjection;
namespace YourProject.Sitemaps;
public class BookSitemapSource : GroupedSitemapItemSource<Book>, ITransientDependency
{
public BookSitemapSource(
IReadOnlyRepository<Book> repository,
IAsyncQueryableExecuter executer)
: base(repository, executer, group: "Books") // 👈 Creates sitemap-Books.xml
{
Filter = x => x.IsPublished; // 👈 Only published books
}
protected override Expression<Func<Book, SitemapItem>> Selector =>
book => new SitemapItem(
book.Id.ToString(), // 👈 Unique identifier
$"/Books/Detail/{book.Id}", // 👈 URL pattern matching your route
book.LastModificationTime ?? book.CreationTime // 👈 Last modified date
)
{
ChangeFrequency = "weekly",
Priority = 0.7
};
}
```
Key points:
- Inherits from `GroupedSitemapItemSource<TEntity>`
- Specifies the entity type (`Book`)
- Defines a group name ("Books") which creates `sitemap-Books.xml`
- Uses `Filter` to include only published books
- Maps entity properties to sitemap URLs using `Selector`
- Automatically registered via `ITransientDependency`
### Example 3: Category-Based Dynamic Content
For content with categories, you can build more complex URL patterns:
```csharp
using Abp.Sitemap.Web.Sitemap.Core;
using Abp.Sitemap.Web.Sitemap.Sources.Group;
namespace YourProject.Sitemaps;
public class ArticleSitemapSource : GroupedSitemapItemSource<Article>, ITransientDependency
{
public ArticleSitemapSource(
IReadOnlyRepository<Article> repository,
IAsyncQueryableExecuter executer)
: base(repository, executer, "Articles")
{
// Multiple filter conditions
Filter = x => x.IsPublished &&
!x.IsDeleted &&
x.PublishDate <= DateTime.Now;
}
protected override Expression<Func<Article, SitemapItem>> Selector =>
article => new SitemapItem(
article.Id.ToString(),
$"/blog/{article.Category.Slug}/{article.Slug}", // 👈 Category-based URL
article.LastModificationTime ?? article.CreationTime
);
}
```
This example demonstrates:
- Multiple filter conditions for complex business logic
- Building URLs with category slugs
## Testing Your Sitemaps
After configuring the module, test your sitemap generation:
### 1. Run Your Application
```bash
dotnet run
```
The sitemaps are automatically generated on application startup.
### 2. Check Generated Files
Navigate to `{WebProject}/Sitemaps/` directory (at the root of your web project):
```
{WebProject}
└── Sitemaps/
├── sitemap.xml # Main group (static pages)
├── sitemap-Books.xml # Books from database
├── sitemap-Articles.xml # Articles from database
└── sitemap-Help.xml # Help pages
```
### 3. Verify XML Content
Open `sitemap-Books.xml` and verify the structure:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://yourdomain.com/Books/Detail/3a071e39-12c9-48d7-8c1e-3b4f5c6d7e8f</loc>
<lastmod>2025-12-13</lastmod>
</url>
<url>
<loc>https://yourdomain.com/Books/Detail/7b8c9d0e-1f2a-3b4c-5d6e-7f8g9h0i1j2k</loc>
<lastmod>2025-12-10</lastmod>
</url>
</urlset>
```
### 4. Test in Browser
Visit the sitemap URLs directly (the module serves them from the root path):
- Main sitemap: `https://localhost:5001/sitemap.xml`
- Books sitemap: `https://localhost:5001/sitemap-Books.xml`
> **Note:** The sitemaps are stored in `{WebProject}/Sitemaps/` directory and served directly from the root URL.
## Advanced Configuration
### Custom Regeneration Schedule
Control when sitemaps are regenerated using cron expressions:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<SitemapOptions>(options =>
{
options.BaseUrl = "https://yourdomain.com";
options.WorkerCronExpression = "0 0 2 * * ?"; // 👈 Every day at 2 AM
// Or use period in milliseconds:
// options.WorkerPeriod = 7200000; // 2 hours
});
}
```
### Environment-Specific Configuration
Use different settings for development and production:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
Configure<SitemapOptions>(options =>
{
if (hostingEnvironment.IsDevelopment())
{
options.BaseUrl = "https://localhost:5001";
options.WorkerPeriod = 300000; // 5 minutes for testing
}
else
{
options.BaseUrl = configuration["App:SelfUrl"]!;
options.WorkerPeriod = 3600000; // 1 hour in production
}
options.FolderPath = "Sitemaps";
});
}
```
### Manual Sitemap Generation
Trigger sitemap generation manually (useful for admin panels):
```csharp
using Abp.Sitemap.Web.Sitemap.Management;
public class SitemapManagementService : ITransientDependency
{
private readonly SitemapFileGenerator _generator;
public SitemapManagementService(SitemapFileGenerator generator)
{
_generator = generator;
}
[Authorize("Admin")]
public async Task RegenerateSitemapsAsync()
{
await _generator.GenerateAsync(); // 👈 Manual regeneration
}
}
```
## Real-World Use Cases
Here are practical scenarios where the sitemap module excels:
### E-Commerce Platform
```csharp
// Products grouped by category
public class ProductSitemapSource : GroupedSitemapItemSource<Product>
{
// Automatically includes all active products with stock
}
// Separate sitemap for categories
public class CategorySitemapSource : GroupedSitemapItemSource<Category>
{
// All browsable categories
}
// Brand pages
public class BrandSitemapSource : GroupedSitemapItemSource<Brand>
{
// All active brands
}
```
Result: `sitemap-Products.xml`, `sitemap-Categories.xml`, `sitemap-Brands.xml`
### Content Management System
```csharp
// Blog posts by date
public class BlogPostSitemapSource : GroupedSitemapItemSource<BlogPost>
{
// Filter by published date, priority based on view count
}
// Static CMS pages
[IncludeSitemapXml]
public class AboutUsModel : PageModel { }
```
## Best Practices
### 1. Group Related Content
Organize your sitemaps logically:
```csharp
// ✅ Good: Logical grouping
"Products", "Categories", "Brands", "Blog", "Help"
// ❌ Bad: Everything in one group
"Main" // Contains 50,000 mixed URLs
```
### 2. Use Filters Wisely
```csharp
// ✅ Good: Only published, non-deleted content
Filter = x => x.IsPublished &&
!x.IsDeleted &&
x.PublishDate <= DateTime.Now
// ❌ Bad: Including draft content
Filter = x => true // Everything included
```
### 3. Keep URLs Clean
```csharp
// ✅ Good: SEO-friendly URLs
$"/products/{product.Slug}"
$"/blog/{year}/{month}/{article.Slug}"
// ❌ Bad: Technical IDs exposed
$"/product-detail?id={product.Id}"
```
## Troubleshooting
### Sitemap Not Generated
**Problem:** No XML files in `{WebProject}/Sitemaps/`
**Solutions:**
1. Check module is added to dependencies
2. Verify `SitemapOptions.BaseUrl` is configured
3. Check application logs for errors
4. Ensure the web project directory has write permissions
### Pages Not Appearing
**Problem:** Some pages missing from sitemap
**Solutions:**
1. Verify `[IncludeSitemapXml]` attribute is present
2. Check namespace imports: `using Abp.Sitemap.Web.Sitemap.Sources.Page.Attributes;`
3. Ensure PageModel classes are public
4. Check filter conditions in custom sources
### Background Worker Not Running
**Problem:** Sitemaps not regenerating automatically
**Solutions:**
1. Check `SitemapOptions.WorkerPeriod` is set
2. Verify background workers are enabled in ABP configuration
3. Check application logs for worker errors
## Performance Considerations
### Caching Strategy
Consider adding caching for frequently accessed sitemaps:
```csharp
public class CachedSitemapFileGenerator : ITransientDependency
{
private readonly SitemapFileGenerator _generator;
private readonly IDistributedCache _cache;
public async Task<string> GetOrGenerateAsync(string group)
{
var cacheKey = $"Sitemap:{group}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
return cached;
await _generator.GenerateAsync();
// Read and cache...
}
}
```
## Conclusion
The ABP Sitemap module provides a production-ready solution for dynamic sitemap generation in ABP Framework applications. By leveraging ABP's architecture—dependency injection, repository pattern, and background workers—the module automatically discovers pages, includes dynamic content, and regenerates sitemaps without manual intervention.
Key benefits:
✅ **Zero Configuration** for basic scenarios
✅ **Type-Safe** attribute-based configuration
✅ **Extensible** for complex business logic
✅ **Performance** optimized with background processing
✅ **SEO-Friendly** following XML sitemap standards
Whether you're building a blog, e-commerce platform, or enterprise application, this module provides a solid foundation for search engine optimization.
## Additional Resources
### Documentation
- [ABP Framework Documentation](https://abp.io/docs/latest/)
- [ABP Background Workers](https://abp.io/docs/latest/framework/infrastructure/background-workers)
- [ABP Repository Pattern](https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories)
- [ABP Dependency Injection](https://abp.io/docs/latest/framework/fundamentals/dependency-injection)
### Source Code
- [Complete Working Demo](https://github.com/salihozkara/AbpSitemapDemo) - Full implementation with examples
- [BookSitemapSource](https://github.com/salihozkara/AbpSitemapDemo/blob/master/AbpSitemapDemo/Pages/Books/Index.cshtml.cs#L23) - Entity-based source example
- [Index.cshtml](https://github.com/salihozkara/AbpSitemapDemo/blob/master/AbpSitemapDemo/Pages/Index.cshtml#L9) - Page attribute usage

1
docs/en/Community-Articles/2025-12-13-Building-Dynamic-XML-Sitemaps-With-ABP-Framework/summary.md

@ -0,0 +1 @@
Learn how to use the ABP Sitemap module for automatic XML sitemap generation in your ABP Framework applications.

6
docs/en/docs-nav.json

@ -563,7 +563,11 @@
{
"text": "Microsoft.Extensions.AI",
"path": "framework/infrastructure/artificial-intelligence/microsoft-extensions-ai.md"
},
},
{
"text": "Agent Framework",
"path": "framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md"
},
{
"text": "Semantic Kernel",
"path": "framework/infrastructure/artificial-intelligence/microsoft-semantic-kernel.md"

4
docs/en/framework/infrastructure/artificial-intelligence/index.md

@ -27,12 +27,14 @@ abp add-package Volo.Abp.AI
The `Volo.Abp.AI` package provides integration with the following libraries:
* [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/microsoft-extensions-ai)
* [Microsoft.Agents.AI (Agent Framework)](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
* [Microsoft.SemanticKernel](https://learn.microsoft.com/en-us/semantic-kernel/overview/)
The Microsoft.Extensions.AI library is suggested for library developers to keep the library dependency minimum and simple (since it provides basic abstractions and fundamental AI provider integrations), while Semantic Kernel is suggested for applications that need rich and advanced AI integration features.
The **Microsoft.Extensions.AI** library is suggested for library developers to keep the library dependency minimum and simple (since it provides basic abstractions and fundamental AI provider integrations). For applications, **Microsoft Agent Framework** is the recommended choice as it combines the best of both AutoGen and Semantic Kernel (it's direct successor of these two frameworks), offering simple abstractions for single- and multi-agent patterns along with advanced features like thread-based state management, type safety, filters, and telemetry. **Semantic Kernel** can still be used if you need its specific AI integration features.
Check the following documentation to learn how to use these libraries with the ABP integration:
- [ABP Microsoft.Extensions.AI integration](./microsoft-extensions-ai.md)
- [ABP Microsoft.Agents.AI (Agent Framework) integration](./microsoft-agent-framework.md)
- [ABP Microsoft.SemanticKernel integration](./microsoft-semantic-kernel.md)

218
docs/en/framework/infrastructure/artificial-intelligence/microsoft-agent-framework.md

@ -0,0 +1,218 @@
# Microsoft.Agents.AI (Agent Framework)
[Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview) is an open-source development kit for **building AI agents** and **multi-agent workflows**. It is the direct successor to both *AutoGen* and [*Semantic Kernel*](./microsoft-semantic-kernel.md), combining their strengths while adding new capabilities, and is the suggested framework for building AI agent applications. This documentation is about the usage of this library with ABP Framework. Make sure you have read the [Artificial Intelligence](./index.md) documentation before reading this documentation.
## Usage
**Microsoft Agent Framework** works on top of `IChatClient` from **Microsoft.Extensions.AI**. After obtaining an `IChatClient` instance, you can create an AI agent using the `CreateAIAgent` extension method:
```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
public class MyService
{
private readonly IChatClient _chatClient;
public MyService(IChatClient chatClient)
{
_chatClient = chatClient;
}
public async Task<string> GetResponseAsync(string userMessage)
{
AIAgent agent = _chatClient.CreateAIAgent(
instructions: "You are a helpful assistant that provides concise answers."
);
AgentRunResponse response = await agent.RunAsync(userMessage);
return response.Text;
}
}
```
You can also use `IChatClientAccessor` to access the `IChatClient` in scenarios where AI capabilities are **optional**, such as when developing a module or a service that may use AI capabilities optionally:
```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Volo.Abp.AI;
public class MyService
{
private readonly IChatClientAccessor _chatClientAccessor;
public MyService(IChatClientAccessor chatClientAccessor)
{
_chatClientAccessor = chatClientAccessor;
}
public async Task<string> GetResponseAsync(string userMessage)
{
var chatClient = _chatClientAccessor.ChatClient;
if (chatClient is null)
{
return "No chat client configured";
}
AIAgent agent = chatClient.CreateAIAgent(
instructions: "You are a helpful assistant that provides concise answers."
);
var response = await agent.RunAsync(userMessage);
return response.Text;
}
}
```
### Workspaces
Workspaces are a way to configure isolated AI configurations for a named scope. You can define a workspace by decorating a class with the `WorkspaceNameAttribute` attribute that carries the workspace name.
- Workspace names must be unique.
- Workspace names cannot contain spaces _(use underscores or camelCase)_.
- Workspace names are case-sensitive.
```csharp
using Volo.Abp.AI;
[WorkspaceName("CommentSummarization")]
public class CommentSummarization
{
}
```
> [!NOTE]
> If you don't specify the workspace name, the full name of the class will be used as the workspace name.
You can resolve generic versions of `IChatClient` and `IChatClientAccessor` services for a specific workspace as generic arguments. If Chat Client is not configured for a workspace, you will get `null` from the accessor services. You should check the accessor before using it. This applies only for specified workspaces. Another workspace may have a configured Chat Client.
`IChatClient<TWorkSpace>` or `IChatClientAccessor<TWorkSpace>` can be resolved to access a specific workspace's chat client. This is a typed chat client and can be configured separately from the default chat client.
Example of resolving a typed chat client for a workspace:
```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Volo.Abp.AI;
public class MyService
{
private readonly IChatClient<CustomerSupport> _chatClient;
public MyService(IChatClient<CustomerSupport> chatClient)
{
_chatClient = chatClient;
}
public async Task<string> GetResponseAsync(string userMessage)
{
AIAgent agent = _chatClient.CreateAIAgent(
instructions: "You are a customer support assistant. Be polite and helpful."
);
var response = await agent.RunAsync(userMessage);
return response.Text;
}
}
```
Example of resolving a typed chat client accessor for a workspace:
```csharp
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Volo.Abp.AI;
public class MyService
{
private readonly IChatClientAccessor<CustomerSupport> _chatClientAccessor;
public MyService(IChatClientAccessor<CustomerSupport> chatClientAccessor)
{
_chatClientAccessor = chatClientAccessor;
}
public async Task<string> GetResponseAsync(string userMessage)
{
var chatClient = _chatClientAccessor.ChatClient;
if (chatClient is null)
{
return "No chat client configured";
}
AIAgent agent = chatClient.CreateAIAgent(
instructions: "You are a customer support assistant. Be polite and helpful."
);
var response = await agent.RunAsync(userMessage);
return response.Text;
}
}
```
## Configuration
**Microsoft Agent Framework** uses `IChatClient` from **Microsoft.Extensions.AI** as its foundation. Therefore, the configuration process for workspaces is the same as described in the [Microsoft.Extensions.AI documentation](./microsoft-extensions-ai.md#configuration).
You need to configure the Chat Client for your workspace using `AbpAIWorkspaceOptions`, and then you can use the `CreateAIAgent` extension method to create AI agents from the configured chat client.
To configure a chat client, you'll need a LLM provider package such as [Microsoft.Extensions.AI.OpenAI](https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI) or [OllamaSharp](https://www.nuget.org/packages/OllamaSharp/).
_The following example requires [OllamaSharp](https://www.nuget.org/packages/OllamaSharp/) package to be installed._
Demonstration of the default workspace configuration:
```csharp
[DependsOn(typeof(AbpAIModule))]
public class MyProjectModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpAIWorkspaceOptions>(options =>
{
options.Workspaces.ConfigureDefault(configuration =>
{
configuration.ConfigureChatClient(chatClientConfiguration =>
{
chatClientConfiguration.Builder = new ChatClientBuilder(
sp => new OllamaApiClient("http://localhost:11434", "mistral")
);
});
});
});
}
}
```
Demonstration of the isolated workspace configuration:
```csharp
[DependsOn(typeof(AbpAIModule))]
public class MyProjectModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpAIWorkspaceOptions>(options =>
{
options.Workspaces.Configure<CustomerSupport>(configuration =>
{
configuration.ConfigureChatClient(chatClientConfiguration =>
{
chatClientConfiguration.Builder = new ChatClientBuilder(
sp => new OllamaApiClient("http://localhost:11434", "mistral")
);
});
});
});
}
}
```
## See Also
- [Usage of Microsoft.Extensions.AI](./microsoft-extensions-ai.md)
- [Usage of Semantic Kernel](./microsoft-semantic-kernel.md)
- [Microsoft Agent Framework Overview](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview)
- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/)

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

@ -172,5 +172,6 @@ public class MyProjectModule : AbpModule
## See Also
- [Usage of Agent Framework](./microsoft-agent-framework.md)
- [Usage of Semantic Kernel](./microsoft-semantic-kernel.md)
- [AI Samples for .NET](https://learn.microsoft.com/en-us/samples/dotnet/ai-samples/ai-samples/)

BIN
docs/en/images/ai-management-widget.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

344
docs/en/modules/ai-management/index.md

@ -10,8 +10,7 @@
> You must have an ABP Team or a higher license to use this module.
> **⚠️ Important Notice**
> The **AI Management Module** is currently in **preview** and not yet production-ready. The documentation and implementation are subject to change.
> We recommend using this module for **evaluation and experimentation** only, not in production environments for now.
> The **AI Management Module** is currently in **preview**. The documentation and implementation are subject to change.
This module implements AI (Artificial Intelligence) management capabilities on top of the [Artificial Intelligence Workspaces](../../framework/infrastructure/artificial-intelligence/index.md) feature of the ABP Framework and allows to manage workspaces dynamically from the application including UI components and API endpoints.
@ -29,7 +28,6 @@ abp add-module Volo.AIManagement
Open ABP Studio, navigate to your solution explorer, **Right Click** on the project and select **Import Module**. Choose `Volo.AIManagement` from `NuGet` tab and check the "Install this Module" checkbox. Click the "OK" button to install the module.
## Packages
This module follows the [module development best practices guide](../../framework/architecture/best-practices) and consists of several NuGet and NPM packages. See the guide if you want to understand the packages and relations between them.
@ -45,7 +43,7 @@ AI Management module packages are designed for various usage scenarios. Packages
AI Management module adds the following items to the "Main" menu:
* **AI Management**: Root menu item for AI Management module. (`AIManagement`)
* **Workspaces**: Workspace management page. (`AIManagement.Workspaces`)
* **Workspaces**: Workspace management page. (`AIManagement.Workspaces`)
`AIManagementMenus` class has the constants for the menu item names.
@ -88,21 +86,21 @@ Workspaces are the core concept of the AI Management module. A workspace represe
When creating or managing a workspace, you can configure the following properties:
| Property | Required | Description |
|----------|----------|-------------|
| `Name` | Yes | Unique workspace identifier (cannot contain spaces) |
| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") |
| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") |
| `ApiKey` | No | API authentication key (required by some providers) |
| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) |
| `SystemPrompt` | No | Default system prompt for all conversations |
| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) |
| `Description` | No | Workspace description |
| `IsActive` | No | Enable/disable the workspace (default: true) |
| `ApplicationName` | No | Associate workspace with specific application |
| `RequiredPermissionName` | No | Permission required to use this workspace |
| `IsSystem` | No | Whether it's a system workspace (read-only) |
| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings |
| Property | Required | Description |
| ----------------------------- | -------- | -------------------------------------------------------------- |
| `Name` | Yes | Unique workspace identifier (cannot contain spaces) |
| `Provider` | Yes* | AI provider name (e.g., "OpenAI", "Ollama") |
| `ModelName` | Yes* | Model identifier (e.g., "gpt-4", "mistral") |
| `ApiKey` | No | API authentication key (required by some providers) |
| `ApiBaseUrl` | No | Custom endpoint URL (defaults to provider's default) |
| `SystemPrompt` | No | Default system prompt for all conversations |
| `Temperature` | No | Response randomness (0.0-1.0, defaults to provider default) |
| `Description` | No | Workspace description |
| `IsActive` | No | Enable/disable the workspace (default: true) |
| `ApplicationName` | No | Associate workspace with specific application |
| `RequiredPermissionName` | No | Permission required to use this workspace |
| `IsSystem` | No | Whether it's a system workspace (read-only) |
| `OverrideSystemConfiguration` | No | Allow database configuration to override code-defined settings |
**\*Not required for system workspaces**
@ -137,7 +135,7 @@ PreConfigure<AbpAIWorkspaceOptions>(options =>
#### Dynamic Workspaces
* **Created through the UI** or programmatically via `IWorkspaceRepository`
* **Created through the UI** or programmatically via `ApplicationWorkspaceManager` and `IWorkspaceRepository`
* **Fully manageable** - can be created, updated, activated/deactivated, and deleted
* **Stored in database** with all configuration
* **Ideal for** user-customizable AI features
@ -145,15 +143,30 @@ PreConfigure<AbpAIWorkspaceOptions>(options =>
Example (data seeding):
```csharp
var workspace = new Workspace(
name: "CustomerSupportWorkspace",
provider: "OpenAI",
modelName: "gpt-4",
apiKey: "your-api-key"
);
workspace.ApplicationName = ApplicationInfoAccessor.ApplicationName;
workspace.SystemPrompt = "You are a helpful customer support assistant.";
await _workspaceRepository.InsertAsync(workspace);
public class WorkspaceDataSeederContributor : IDataSeedContributor, ITransientDependency
{
private readonly IWorkspaceRepository _workspaceRepository;
private readonly ApplicationWorkspaceManager _applicationWorkspaceManager;
public WorkspaceDataSeederContributor(
IWorkspaceRepository workspaceRepository,
ApplicationWorkspaceManager applicationWorkspaceManager)
{
_workspaceRepository = workspaceRepository;
_applicationWorkspaceManager = applicationWorkspaceManager;
}
public async Task SeedAsync(DataSeedContext context)
{
var workspace = await _applicationWorkspaceManager.CreateAsync(
name: "CustomerSupportWorkspace",
provider: "OpenAI",
modelName: "gpt-4");
workspace.ApiKey = "your-api-key";
workspace.SystemPrompt = "You are a helpful customer support assistant.";
await _workspaceRepository.InsertAsync(workspace);
}
```
### Workspace Naming Rules
@ -166,28 +179,29 @@ await _workspaceRepository.InsertAsync(workspace);
The AI Management module defines the following permissions:
| Permission | Description | Default Granted To |
|------------|-------------|-------------------|
| `AIManagement.Workspaces` | View workspaces | Admin role |
| `AIManagement.Workspaces.Create` | Create new workspaces | Admin role |
| `AIManagement.Workspaces.Update` | Edit existing workspaces | Admin role |
| `AIManagement.Workspaces.Delete` | Delete workspaces | Admin role |
| Permission | Description | Default Granted To |
| -------------------------------- | ------------------------ | ------------------ |
| `AIManagement.Workspaces` | View workspaces | Admin role |
| `AIManagement.Workspaces.Create` | Create new workspaces | Admin role |
| `AIManagement.Workspaces.Update` | Edit existing workspaces | Admin role |
| `AIManagement.Workspaces.Delete` | Delete workspaces | Admin role |
### Workspace-Level Permissions
In addition to module-level permissions, you can restrict access to individual workspaces by setting the `RequiredPermissionName` property:
```csharp
var workspace = new Workspace(
var workspace = await _applicationWorkspaceManager.CreateAsync(
name: "PremiumWorkspace",
provider: "OpenAI",
modelName: "gpt-4",
requiredPermissionName: "MyApp.PremiumFeatures"
modelName: "gpt-4"
);
// Set a specific permission for the workspace
workspace.RequiredPermissionName = MyAppPermissions.AccessPremiumWorkspaces;
```
When a workspace has a required permission:
* Only authorized users with that permission can access the workspace endpoints
* Users without the permission will receive an authorization error
@ -205,6 +219,7 @@ The AI Management module is designed to support various usage patterns, from sim
In this scenario, you only use the ABP Framework's AI features directly. You configure AI providers (like OpenAI) in your code and don't need any database or management UI.
**Required Packages:**
- `Volo.Abp.AI`
- Any Microsoft AI extensions (e.g., `Microsoft.Extensions.AI.OpenAI`)
@ -242,7 +257,7 @@ public class MyService
{
_chatClient = chatClient;
}
public async Task<string> GetResponseAsync(string prompt)
{
var response = await _chatClient.CompleteAsync(prompt);
@ -262,10 +277,12 @@ In this scenario, you install the AI Management module with its database layer,
**Required Packages:**
**Minimum (backend only):**
- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`)
- `Volo.AIManagement.OpenAI` (or another AI provider package)
**Full installation (with UI and API):**
- `Volo.AIManagement.EntityFrameworkCore` (or `Volo.AIManagement.MongoDB`)
- `Volo.AIManagement.Application`
- `Volo.AIManagement.HttpApi`
@ -300,6 +317,7 @@ public class YourModule : AbpModule
**Option 2 - Dynamic Workspace (UI-based):**
No code configuration needed. Define workspaces through:
- The AI Management UI (navigate to AI Management > Workspaces)
- Data seeding in your `DataSeeder` class
@ -324,6 +342,7 @@ public class MyService
In this scenario, your application communicates with a separate AI Management microservice that manages configurations and communicates with AI providers on your behalf. The AI Management service handles all AI provider interactions.
**Required Packages:**
- `Volo.AIManagement.Client.HttpApi.Client`
**Configuration:**
@ -384,7 +403,7 @@ public class MyService
var response = await _chatService.ChatCompletionsAsync(workspaceName, request);
return response.Content;
}
// For streaming responses
public async IAsyncEnumerable<string> StreamAIResponseAsync(string workspaceName, string prompt)
{
@ -411,6 +430,7 @@ public class MyService
This scenario builds on Scenario 3, but your application exposes its own HTTP endpoints that other applications can call. Your application then forwards these requests to the AI Management service.
**Required Packages:**
- `Volo.AIManagement.Client.HttpApi.Client` (to communicate with AI Management service)
- `Volo.AIManagement.Client.Application` (application services)
- `Volo.AIManagement.Client.HttpApi` (to expose HTTP endpoints)
@ -423,19 +443,196 @@ Same as Scenario 3, configure the remote AI Management service in `appsettings.j
**Usage:**
Once configured, other applications can call your application's endpoints:
- `POST /api/ai-management-client/chat-completion` for chat completions
- `POST /api/ai-management-client/stream-chat-completion` for streaming responses
Your application acts as a proxy, forwarding these requests to the AI Management microservice.
## Comparison Table
### Comparison Table
| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case |
| ------------------------- | ----------------- | -------------- | -------------- | ----------- | ----------------------------------------- |
| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed |
| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI |
| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally |
| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services |
## Client Usage (MVC UI)
AI Management uses different packages depending on the usage scenario:
- **`Volo.AIManagement.*` packages**: These contain the core AI functionality and are used when your application hosts and manages its own AI operations. These packages don't expose any application service and endpoints to be consumed by default.
- **`Volo.AIManagement.Client.*` packages**: These are designed for applications that need to consume AI services from a remote application. They provide both server and client side of remote access to the AI services.
**List of packages:**
- `Volo.AIManagement.Client.Application`
- `Volo.AIManagement.Client.Application.Contracts`
- `Volo.AIManagement.Client.HttpApi`
- `Volo.AIManagement.Client.HttpApi.Client`
- `Volo.AIManagement.Client.Web`
### The Chat Widget
The `Volo.AIManagement.Client.Web` package provides a chat widget to allow you to easily integrate a chat interface into your application that uses a specific AI workspace named `ChatClientChatViewComponent`.
#### Basic Usage
You can invoke the `ChatClientChatViewComponent` Widget in your razor page with the following code:
```csharp
@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
{
WorkspaceName = "mylama",
})
```
![ai-management-workspaces](../../images/ai-management-widget.png)
#### Properties
You can customize the chat widget with the following properties:
- `WorkspaceName`: The name of the workspace to use.
- `ComponentId`: Unique identifier for accessing the component via JavaScript API (stored in abp.chatComponents).
- `ConversationId`: The unique identifier for persisting and retrieving chat history from client-side storage.
- `Title`: The title of the chat widget.
- `ShowStreamCheckbox`: Whether to show the stream checkbox. Allows user to toggle streaming on and off. Default is `false`.
- `UseStreaming`: Default streaming behavior. Can be overridden by user when `ShowStreamCheckbox` is true.
```csharp
@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
{
WorkspaceName = "mylama",
ComponentId = "mylama-chat",
ConversationId = "mylama-conversation-" + @CurrentUser.Id,
Title = "My Custom Title",
ShowStreamCheckbox = true,
UseStreaming = true
})
```
#### Using the Conversation Id
You can use the `ConversationId` property to specify the id of the conversation to use. When the Conversation Id is provided, the chat will be stored at the client side and will be retrieved when the user revisits the page that contains the chat widget. If it's not provided or provided as **null**, the chat will be temporary and will not be saved, it'll be lost when the component lifetime ends.
```csharp
@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
{
WorkspaceName = "mylama",
ConversationId = "my-support-conversation-" + @CurrentUser.Id
})
```
| Scenario | Database Required | Manages Config | Executes AI | Exposes API | Use Case |
|----------|------------------|----------------|-------------|-------------|----------|
| **1. No AI Management** | No | Code | Local | Optional | Simple apps, no config management needed |
| **2. Full AI Management** | Yes | Database/UI | Local | Optional | Monoliths, services managing their own AI |
| **3. Client Remote** | No | Remote Service | Remote Service | No | Microservices consuming AI centrally |
| **4. Client Proxy** | No | Remote Service | Remote Service | Yes | API Gateway pattern, proxy services |
#### JavaScript API
The chat components are initialized automatically when the ViewComponent is rendered in the page. All the initialized components in the page are stored in the `abp.chatComponents` object. You can retrieve a specific component by its `ComponentId` which is defined while invoking the ViewComponent.
```csharp
@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
{
WorkspaceName = "mylama",
ComponentId = "mylama-chat"
})
```
You can then use the JavaScript API to interact with the component.
```js
var chatComponent = abp.chatComponents.get('mylama-chat');
```
Once you have the component, you can use the following functions to interact with it:
```js
// Switch to a different conversation
chatComponent.switchConversation(conversationId);
// Create a new conversation with a specific model
chatComponent.createConversation(conversationId, modelName);
// Clear the current conversation history
chatComponent.clearConversation();
// Get the current conversation ID (returns null for ephemeral conversations)
var currentId = chatComponent.getCurrentConversationId();
// Initialize with a specific conversation ID
chatComponent.initialize(conversationId);
// Send a message programmatically
chatComponent.sendMessage();
// Listen to events
chatComponent.on('messageSent', function(data) {
console.log('Message sent:', data.message);
console.log('Conversation ID:', data.conversationId);
console.log('Is first message:', data.isFirstMessage);
});
chatComponent.on('messageReceived', function(data) {
console.log('AI response:', data.message);
console.log('Conversation ID:', data.conversationId);
console.log('Is streaming:', data.isStreaming);
});
chatComponent.on('streamStarted', function(data) {
console.log('Streaming started for conversation:', data.conversationId);
});
// Remove event listeners
chatComponent.off('messageSent', callbackFunction);
```
**Best-practices:**
- Don't try to access the component at the page load time, it's not guaranteed to be initialized yet. Get the component whenever you need it to make sure it's **initialized** and the **latest state** is applied.
❌ Don't do this
```js
(function(){
var chatComponent = abp.chatComponents.get('mylama-chat');
$('#my-button').on('click', function() {
chatComponent.clearConversation();
});
});
```
✅ Do this
```js
(function(){
$('#my-button').on('click', function() {
var chatComponent = abp.chatComponents.get('mylama-chat');
chatComponent.clearConversation();
});
});
```
## Using Dynamic Workspace Configurations for custom requirements
The AI Management module allows you to access only configuration of a workspace without resolving pre-constructed chat client. This is useful when you want to use a workspace for your own purposes and you don't need to use the chat client.
The `IWorkspaceConfigurationStore` service is used to access the configuration of a workspace. It has multiple implementaations according to the usage scenario.
```csharp
public class MyService
{
private readonly IWorkspaceConfigurationStore _workspaceConfigurationStore;
public MyService(IWorkspaceConfigurationStore workspaceConfigurationStore)
{
_workspaceConfigurationStore = workspaceConfigurationStore;
}
public async Task DoSomethingAsync()
{
// Get the configuration of the workspace that can be managed dynamically.
var configuration = await _workspaceConfigurationStore.GetAsync("MyWorkspace");
// Do something with the configuration
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatClient(
config.ModelName!,
new Uri(config.ApiBaseUrl),
config.ApiKey
)
.Build();
}
}
```
## Implementing Custom AI Provider Factories
@ -504,24 +701,23 @@ public override void ConfigureServices(ServiceConfigurationContext context)
> [!TIP]
> For production scenarios, you may want to add validation for the factory configuration.
### Available Configuration Properties
The `ChatClientCreationConfiguration` object provides the following properties from the database:
| Property | Type | Description |
|----------|------|-------------|
| `Name` | string | Workspace name |
| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") |
| `ApiKey` | string? | API key for authentication |
| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") |
| `SystemPrompt` | string? | Default system prompt for the workspace |
| `Temperature` | float? | Temperature setting for response generation |
| `ApiBaseUrl` | string? | Custom API endpoint URL |
| `Description` | string? | Workspace description |
| `IsActive` | bool | Whether the workspace is active |
| `IsSystem` | bool | Whether it's a system workspace |
| `RequiredPermissionName` | string? | Permission required to use this workspace |
| Property | Type | Description |
| ------------------------ | ------- | ------------------------------------------- |
| `Name` | string | Workspace name |
| `Provider` | string | Provider name (e.g., "OpenAI", "Ollama") |
| `ApiKey` | string? | API key for authentication |
| `ModelName` | string | Model identifier (e.g., "gpt-4", "mistral") |
| `SystemPrompt` | string? | Default system prompt for the workspace |
| `Temperature` | float? | Temperature setting for response generation |
| `ApiBaseUrl` | string? | Custom API endpoint URL |
| `Description` | string? | Workspace description |
| `IsActive` | bool | Whether the workspace is active |
| `IsSystem` | bool | Whether it's a system workspace |
| `RequiredPermissionName` | string? | Permission required to use this workspace |
### Example: Azure OpenAI Factory
@ -558,6 +754,7 @@ public class AzureOpenAIChatClientFactory : IChatClientFactory, ITransientDepend
After implementing and registering your factory:
1. **Through UI**: Navigate to the AI Management workspaces page and create a new workspace:
- Select your provider name (e.g., "Ollama", "AzureOpenAI")
- Configure the API settings
- Set the model name
@ -565,14 +762,14 @@ After implementing and registering your factory:
2. **Through Code** (data seeding):
```csharp
await _workspaceRepository.InsertAsync(new Workspace(
GuidGenerator.Create(),
"MyOllamaWorkspace",
provider: "Ollama",
modelName: "mistral",
apiBaseUrl: "http://localhost:11434",
description: "Local Ollama workspace"
));
var workspace = await _applicationWorkspaceManager.CreateAsync(
name: "MyOllamaWorkspace",
provider: "Ollama",
modelName: "mistral"
);
workspace.ApiBaseUrl = "http://localhost:11434";
workspace.Description = "Local Ollama workspace";
await _workspaceRepository.InsertAsync(workspace);
```
> **Tip**: The provider name you use in `AddFactory<TFactory>("ProviderName")` must match the provider name stored in the workspace configuration in the database.
@ -596,7 +793,7 @@ The following custom repositories are defined:
#### Domain Services
- `ApplicationWorkspaceManager`: Manages workspace operations and validations.
- `WorkspaceConfigurationStore`: Retrieves workspace configuration with caching.
- `WorkspaceConfigurationStore`: Retrieves workspace configuration with caching. Implements `IWorkspaceConfigurationStore` interface.
- `ChatClientResolver`: Resolves the appropriate `IChatClient` implementation for a workspace.
#### Integration Services
@ -625,10 +822,15 @@ Workspace configurations are cached for performance. The cache key format:
WorkspaceConfiguration:{ApplicationName}:{WorkspaceName}
```
### HttpApi Client Layer
- `IntegrationWorkspaceConfigurationStore`: Integration service for remote workspace configuration retrieval. Implements `IWorkspaceConfigurationStore` interface.
The cache is automatically invalidated when workspaces are created, updated, or deleted.
## See Also
- [Artificial Intelligence Infrastructure](../../framework/infrastructure/artificial-intelligence/index.md): Learn about the underlying AI workspace infrastructure
- [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/): Microsoft's unified AI abstractions
- [Microsoft Agent Framework](https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview): Microsoft's Agent Framework
- [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/): Microsoft's Semantic Kernel integration

71
docs/en/studio/release-notes.md

@ -9,6 +9,77 @@
This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version.
## 2.1.3 (2025-12-15) Latest
* Updated `createCommand` and CLI help for multi-tenancy.
* Fixed `BookController` templating problem.
## 2.1.2 (2025-12-11)
* Fixed `SLNX` files in templates for macOS.
* Fixed `DbMigrator` problem on nolayers template.
## 2.1.1 (2025-12-11)
* Fixed duplicate workspace seeding issue.
* Fixed books sample problems when solution is tiered.
* Added AI Management module to `abpmdl` file.
* Improved skip running initial tasks text.
* Fixed unit test failures.
* Added `LanguageManagementDbContext` table creation in tests.
* Removed `ConfigureHttpClientProxies` method.
* Fixed issue with adding new services to existing Microservices.
* Fixed AI Management template issues.
* Reverted browser notification overlay fix.
## 2.1.0 (2025-12-08)
* Enhanced Module Installation UI with improved user experience.
* Added `AI Management` option to Startup Templates (app-nolayers, app).
* Added support for new `SLNX` solution file format.
* Enhanced modularity step in solution creation process.
* Fixed Swagger authorization issues when projects run via .NET Aspire.
* Fixed browser notification overlay problems.
* Added missing `Unit of Work` namespace in solution templates.
* Fixed JSON file formatting issues.
* Updated ABP Framework to `10.0.1` and LeptonX to `5.0.1`.
* Added MySQL compatibility warnings.
* Fixed initial tasks problems.
* Improved AI Assistant control UI with better margins and borders.
## 2.0.2 (2025-11-26)
* Fixed `.NET 10` installation problems.
* Added custom styles for code blocks in **Markdown** view.
* Fixed `OpenIddictCoreOptions` injection to use `IOptions`.
* Added IdentityModel package after KubernetesClient.
## 2.0.1 (2025-11-21)
* Added build step before adding EF Core migration.
* Updated `KubernetesClient` to version `18.0.5`.
## 2.0.0 (2025-11-20)
* Major upgrade to `.NET 10.0` and `ABP Framework 10.0`
* Replaced `IdentityModel` with `Duende.IdentityModel`.
* Added "Open on Start in Browser" option for .NET applications in Solution Runner.
* Added `Mapperly` configuration.
* Enabled user and tenant impersonation in Blazor client modules.
* Enhanced notification system to allow text copying.
* Added environment variable support for DesignTime DbContext.
* Used C# instead of JSON for Aspire AppHost project configuration.
* Fixed MongoDB image pulling problems.
* Improved AI Assistant with better code block visibility across themes.
* Added different cache paths for each browser instance.
* Fixed various UI issues including mouse pointer problems in trees and horizontal scrolling.
* Added `FileManagement` download URL configuration for tiered projects.
* Added chat SignalR configuration to Microservice Blazor apps.
* Updated `Blazorise` packages to version `1.8.6`.
* Fixed `BackToImpersonator` button in Microservice Template.
* Added log recording while crashing for better debugging.
* Enhanced tab headers for **Solution Runner** and **Kubernetes**.
## 1.4.2 (2025-10-30)
* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.3.6`)

5
docs/en/studio/version-mapping.md

@ -11,6 +11,8 @@ This document provides a general overview of the relationship between various ve
| **ABP Studio Version** | **ABP Version of Startup Template** |
|------------------------|---------------------------|
| 2.1.0 - 2.1.3 | 10.0.1 |
| 2.0.0 to 2.0.2 | 10.0.0 |
| 1.4.2 | 9.3.6 |
| 1.3.3 to 1.4.1 | 9.3.5 |
| 1.3.0 - 1.3.2 | 9.3.4 |
@ -19,8 +21,7 @@ This document provides a general overview of the relationship between various ve
| 1.1.2 | 9.2.3 |
| 1.1.0 - 1.1.1 | 9.2.2 |
| 1.0.2 | 9.2.1 |
| 1.0.1 | 9.2.0 |
| 1.0.0 | 9.2.0 |
| 1.0.0 - 1.0.1 | 9.2.0 |
| 0.9.26 | 9.1.1 |
| 0.9.24 - 0.9.25 | 9.1.0 |
| 0.9.22 - 0.9.23 | 9.0.4 |

4
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/AbpPermissionOptions.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Volo.Abp.Authorization.Permissions.Resources;
using Volo.Abp.Collections;
namespace Volo.Abp.Authorization.Permissions;
@ -9,6 +10,8 @@ public class AbpPermissionOptions
public ITypeList<IPermissionValueProvider> ValueProviders { get; }
public ITypeList<IResourcePermissionValueProvider> ResourceValueProviders { get; }
public HashSet<string> DeletedPermissions { get; }
public HashSet<string> DeletedPermissionGroups { get; }
@ -17,6 +20,7 @@ public class AbpPermissionOptions
{
DefinitionProviders = new TypeList<IPermissionDefinitionProvider>();
ValueProviders = new TypeList<IPermissionValueProvider>();
ResourceValueProviders = new TypeList<IResourcePermissionValueProvider>();
DeletedPermissions = new HashSet<string>();
DeletedPermissionGroups = new HashSet<string>();

2
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/ICanAddChildPermission.cs

@ -11,4 +11,4 @@ public interface ICanAddChildPermission
ILocalizableString? displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true);
}
}

14
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionContext.cs

@ -1,5 +1,7 @@
using System;
using JetBrains.Annotations;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Authorization.Permissions;
@ -46,4 +48,16 @@ public interface IPermissionDefinitionContext
/// <param name="name">Name of the permission</param>
/// </summary>
PermissionDefinition? GetPermissionOrNull(string name);
PermissionDefinition AddResourcePermission(
string name,
string resourceName,
string managementPermissionName,
ILocalizableString? displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true);
PermissionDefinition? GetResourcePermissionOrNull([NotNull] string resourceName, [NotNull] string name);
void RemoveResourcePermission([NotNull] string resourceName, [NotNull] string name);
}

7
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionDefinitionManager.cs

@ -11,7 +11,14 @@ public interface IPermissionDefinitionManager
Task<PermissionDefinition?> GetOrNullAsync([NotNull] string name);
[ItemNotNull]
Task<PermissionDefinition> GetResourcePermissionAsync([NotNull]string resourceName, [NotNull] string name);
Task<PermissionDefinition?> GetResourcePermissionOrNullAsync([NotNull]string resourceName, [NotNull] string name);
Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync();
Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync();
Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync();
}

1
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/IPermissionValueProvider.cs

@ -6,7 +6,6 @@ public interface IPermissionValueProvider
{
string Name { get; }
//TODO: Rename to GetResult? (CheckAsync throws exception by naming convention)
Task<PermissionGrantResult> CheckAsync(PermissionValueCheckContext context);
Task<MultiplePermissionGrantResult> CheckAsync(PermissionValuesCheckContext context);

35
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs

@ -7,7 +7,7 @@ using Volo.Abp.SimpleStateChecking;
namespace Volo.Abp.Authorization.Permissions;
public class PermissionDefinition :
public class PermissionDefinition :
IHasSimpleStateCheckers<PermissionDefinition>,
ICanAddChildPermission
{
@ -16,6 +16,16 @@ public class PermissionDefinition :
/// </summary>
public string Name { get; }
/// <summary>
/// Resource name of the permission.
/// </summary>
public string? ResourceName { get; set; }
/// <summary>
/// Management permission of the resource permission.
/// </summary>
public string? ManagementPermissionName { get; set; }
/// <summary>
/// Parent of this permission if one exists.
/// If set, this permission can be granted only if parent is granted.
@ -76,6 +86,19 @@ public class PermissionDefinition :
set => Properties[name] = value;
}
protected internal PermissionDefinition(
[NotNull] string name,
string resourceName,
string managementPermissionName,
ILocalizableString? displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
: this(name, displayName, multiTenancySide, isEnabled)
{
ResourceName = Check.NotNull(resourceName, nameof(resourceName));
ManagementPermissionName = Check.NotNull(managementPermissionName, nameof(managementPermissionName));
}
protected internal PermissionDefinition(
[NotNull] string name,
ILocalizableString? displayName = null,
@ -99,6 +122,11 @@ public class PermissionDefinition :
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
if (ResourceName != null)
{
throw new AbpException($"Resource permission cannot have child permissions. Resource: {ResourceName}");
}
var child = new PermissionDefinition(
name,
displayName,
@ -109,12 +137,12 @@ public class PermissionDefinition :
};
child[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName] = this[PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName];
_children.Add(child);
return child;
}
PermissionDefinition ICanAddChildPermission.AddPermission(
string name,
ILocalizableString? displayName = null,
@ -124,7 +152,6 @@ public class PermissionDefinition :
return this.AddChild(name, displayName, multiTenancySide, isEnabled);
}
/// <summary>
/// Sets a property in the <see cref="Properties"/> dictionary.
/// This is a shortcut for nested calls on this object.

83
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/PermissionDefinitionContext.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
namespace Volo.Abp.Authorization.Permissions;
@ -11,17 +13,20 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
public Dictionary<string, PermissionGroupDefinition> Groups { get; }
public List<PermissionDefinition> ResourcePermissions { get; }
internal IPermissionDefinitionProvider? CurrentProvider { get; set; }
public static class KnownPropertyNames
{
public const string CurrentProviderName = "_CurrentProviderName";
}
public PermissionDefinitionContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Groups = new Dictionary<string, PermissionGroupDefinition>();
ResourcePermissions = new List<PermissionDefinition>();
}
public virtual PermissionGroupDefinition AddGroup(
@ -43,7 +48,7 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
}
Groups[name] = group;
return group;
}
@ -51,37 +56,23 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
public virtual PermissionGroupDefinition GetGroup([NotNull] string name)
{
var group = GetGroupOrNull(name);
if (group == null)
{
throw new AbpException($"Could not find a permission definition group with the given name: {name}");
}
return group;
return group ?? throw new AbpException($"Could not find a permission definition group with the given name: {name}");
}
public virtual PermissionGroupDefinition? GetGroupOrNull([NotNull] string name)
{
Check.NotNull(name, nameof(name));
if (!Groups.ContainsKey(name))
{
return null;
}
return Groups[name];
return Groups.GetOrDefault(name);
}
public virtual void RemoveGroup(string name)
{
Check.NotNull(name, nameof(name));
if (!Groups.ContainsKey(name))
if (!Groups.Remove(name))
{
throw new AbpException($"Not found permission group with name: {name}");
}
Groups.Remove(name);
}
public virtual PermissionDefinition? GetPermissionOrNull([NotNull] string name)
@ -100,4 +91,58 @@ public class PermissionDefinitionContext : IPermissionDefinitionContext
return null;
}
public virtual PermissionDefinition AddResourcePermission(
string name,
string resourceName,
string managementPermissionName,
ILocalizableString? displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
Check.NotNull(name, nameof(name));
Check.NotNull(resourceName, nameof(resourceName));
Check.NotNull(managementPermissionName, nameof(managementPermissionName));
if (ResourcePermissions.Any(x => x.ResourceName == resourceName && x.Name == name))
{
throw new AbpException($"There is already an existing resource permission with name: {name} for resource: {resourceName}");
}
var permission = new PermissionDefinition(
name,
resourceName,
managementPermissionName,
displayName,
multiTenancySide,
isEnabled)
{
[KnownPropertyNames.CurrentProviderName] = CurrentProvider?.GetType().FullName
};
ResourcePermissions.Add(permission);
return permission;
}
public virtual PermissionDefinition? GetResourcePermissionOrNull([NotNull] string resourceName, [NotNull] string name)
{
Check.NotNull(resourceName, nameof(resourceName));
Check.NotNull(name, nameof(name));
return ResourcePermissions.FirstOrDefault(p => p.ResourceName == resourceName && p.Name == name);
}
public virtual void RemoveResourcePermission([NotNull] string resourceName, [NotNull] string name)
{
Check.NotNull(resourceName, nameof(resourceName));
Check.NotNull(name, nameof(name));
var resourcePermission = GetResourcePermissionOrNull(resourceName, name);
if (resourcePermission == null)
{
throw new AbpException($"Not found resource permission with name: {name} for resource: {resourceName}");
}
ResourcePermissions.Remove(resourcePermission);
}
}

8
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IHasResourcePermissions.cs

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Volo.Abp.Authorization.Permissions.Resources;
public interface IHasResourcePermissions : IKeyedObject
{
Dictionary<string, bool> ResourcePermissions { get; }
}

33
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionChecker.cs

@ -0,0 +1,33 @@
using System.Security.Claims;
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public interface IResourcePermissionChecker
{
Task<bool> IsGrantedAsync(
string name,
string resourceName,
string resourceKey
);
Task<bool> IsGrantedAsync(
ClaimsPrincipal? claimsPrincipal,
string name,
string resourceName,
string resourceKey
);
Task<MultiplePermissionGrantResult> IsGrantedAsync(
string[] names,
string resourceName,
string resourceKey
);
Task<MultiplePermissionGrantResult> IsGrantedAsync(
ClaimsPrincipal? claimsPrincipal,
string[] names,
string resourceName,
string resourceKey
);
}

83
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionStore.cs

@ -0,0 +1,83 @@
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public interface IResourcePermissionStore
{
/// <summary>
/// Checks if the given permission is granted for the given resource.
/// </summary>
/// <param name="name">The name of the permission.</param>
/// <param name="resourceName">The name of the resource.</param>
/// <param name="resourceKey">Resource key</param>
/// <param name="providerName">The name of the provider.</param>
/// <param name="providerKey">The key of the provider.</param>
/// <returns>
/// True if the permission is granted.
/// </returns>
Task<bool> IsGrantedAsync(
string name,
string resourceName,
string resourceKey,
string providerName,
string providerKey
);
/// <summary>
/// Checks if the given permissions are granted for the given resource.
/// </summary>
/// <param name="names">The name of the permissions.</param>
/// <param name="resourceName">The name of the resource.</param>
/// <param name="resourceKey">Resource key</param>
/// <param name="providerName">The name of the provider.</param>
/// <param name="providerKey">The key of the provider.</param>
/// <returns>
/// A <see cref="MultiplePermissionGrantResult"/> object containing the grant results for each permission.
/// </returns>
Task<MultiplePermissionGrantResult> IsGrantedAsync(
string[] names,
string resourceName,
string resourceKey,
string providerName,
string providerKey
);
/// <summary>
/// Gets all permissions for the given resource.
/// </summary>
/// <param name="resourceName">Resource name</param>
/// <param name="resourceKey">Resource key</param>
/// <returns>
/// A <see cref="MultiplePermissionGrantResult"/> object containing the grant results for each permission.
/// </returns>
Task<MultiplePermissionGrantResult> GetPermissionsAsync(
string resourceName,
string resourceKey
);
/// <summary>
/// Gets all granted permissions for the given resource.
/// </summary>
/// <param name="resourceName">Resource name</param>
/// <param name="resourceKey">Resource key</param>
/// <returns>
/// An array of granted permission names.
/// </returns>
Task<string[]> GetGrantedPermissionsAsync(
string resourceName,
string resourceKey
);
/// <summary>
/// Retrieves the keys of resources for which the specified permission is granted.
/// </summary>
/// <param name="resourceName">The name of the resource.</param>
/// <param name="name">The name of the permission.</param>
/// <returns>
/// An array of resource keys where the specified permission is granted.
/// </returns>
Task<string[]> GetGrantedResourceKeysAsync(
string resourceName,
string name
);
}

12
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProvider.cs

@ -0,0 +1,12 @@
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public interface IResourcePermissionValueProvider
{
string Name { get; }
Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context);
Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context);
}

8
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/IResourcePermissionValueProviderManager.cs

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Volo.Abp.Authorization.Permissions.Resources;
public interface IResourcePermissionValueProviderManager
{
IReadOnlyList<IResourcePermissionValueProvider> ValueProviders { get; }
}

43
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/NullResourcePermissionStore.cs

@ -0,0 +1,43 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class NullResourcePermissionStore : IResourcePermissionStore, ISingletonDependency
{
public ILogger<NullResourcePermissionStore> Logger { get; set; }
public NullResourcePermissionStore()
{
Logger = NullLogger<NullResourcePermissionStore>.Instance;
}
public Task<bool> IsGrantedAsync(string name, string resourceName, string resourceKey, string providerName, string providerKey)
{
return TaskCache.FalseResult;
}
public Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names, string resourceName, string resourceKey, string providerName, string providerKey)
{
return Task.FromResult(new MultiplePermissionGrantResult(names, PermissionGrantResult.Prohibited));
}
public Task<MultiplePermissionGrantResult> GetPermissionsAsync(string resourceName, string resourceKey)
{
return Task.FromResult(new MultiplePermissionGrantResult());
}
public Task<string[]> GetGrantedPermissionsAsync(string resourceName, string resourceKey)
{
return Task.FromResult(Array.Empty<string>());
}
public Task<string[]> GetGrantedResourceKeysAsync(string resourceName, string name)
{
return Task.FromResult(Array.Empty<string>());
}
}

34
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionCheckerExtensions.cs

@ -0,0 +1,34 @@
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public static class ResourcePermissionCheckerExtensions
{
/// <summary>
/// Checks if a specific permission is granted for a resource with a given key.
/// </summary>
/// <typeparam name="TResource">The type of the resource.</typeparam>
/// <param name="resourcePermissionChecker">The resource permission checker instance.</param>
/// <param name="permissionName">The name of the permission to check.</param>
/// <param name="resource">The resource instance to check permission for.</param>
/// <param name="resourceKey">The unique key identifying the resource instance.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a boolean value indicating whether the permission is granted.</returns>
public static Task<bool> IsGrantedAsync<TResource>(
this IResourcePermissionChecker resourcePermissionChecker,
string permissionName,
TResource resource,
object resourceKey
)
{
Check.NotNull(resourcePermissionChecker, nameof(resourcePermissionChecker));
Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName));
Check.NotNull(resource, nameof(resource));
Check.NotNull(resourceKey, nameof(resourceKey));
return resourcePermissionChecker.IsGrantedAsync(
permissionName,
typeof(TResource).FullName!,
resourceKey.ToString()!
);
}
}

21
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionGrantInfo.cs

@ -0,0 +1,21 @@
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ResourcePermissionGrantInfo : PermissionGrantInfo
{
public string ResourceName { get; }
public string ResourceKey { get; }
public ResourcePermissionGrantInfo(
string name,
bool isGranted,
string resourceName,
string resourceKey,
string? providerName = null,
string? providerKey = null)
: base(name, isGranted, providerName, providerKey)
{
ResourceName = resourceName;
ResourceKey = resourceKey;
}
}

75
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionStoreExtensions.cs

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public static class ResourcePermissionStoreExtensions
{
/// <summary>
/// Retrieves the list of granted permissions for a specific resource with a given key.
/// </summary>
/// <typeparam name="TResource">The type of the resource.</typeparam>
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
/// <param name="resource">The resource instance to retrieve permissions for.</param>
/// <param name="resourceKey">The unique key identifying the resource instance.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an array of strings representing the granted permissions.</returns>
public static async Task<string[]> GetGrantedPermissionsAsync<TResource>(
this IResourcePermissionStore resourcePermissionStore,
TResource resource,
object resourceKey
)
{
return (await GetPermissionsAsync(resourcePermissionStore, resource, resourceKey)).Where(x => x.Value).Select(x => x.Key).ToArray();
}
/// <summary>
/// Retrieves a dictionary of permissions and their granted status for the specified entity.
/// </summary>
/// <typeparam name="TResource">The type of the resource.</typeparam>
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
/// <param name="resource">The resource for which the permissions are being retrieved.</param>
/// <param name="resourceKey">The unique key identifying the resource instance.</param>
/// <returns>A dictionary where the keys are permission names and the values are booleans indicating whether the permission is granted.</returns>
public static async Task<IDictionary<string, bool>> GetPermissionsAsync<TResource>(
this IResourcePermissionStore resourcePermissionStore,
TResource resource,
object resourceKey
)
{
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
Check.NotNull(resource, nameof(resource));
Check.NotNull(resourceKey, nameof(resourceKey));
var result = await resourcePermissionStore.GetPermissionsAsync(
typeof(TResource).FullName!,
resourceKey.ToString()!
);
return result.Result.ToDictionary(x => x.Key, x => x.Value == PermissionGrantResult.Granted);
}
/// <summary>
/// Retrieves the keys of the resources granted a specific permission.
/// </summary>
/// <typeparam name="TResource">The type of the resource.</typeparam>
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
/// <param name="resource">The resource instance to check granted permissions for.</param>
/// <param name="permissionName">The name of the permission to check.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an array of strings representing the granted resource keys.</returns>
public static Task<string[]> GetGrantedResourceKeysAsync<TResource>(
this IResourcePermissionStore resourcePermissionStore,
TResource resource,
string permissionName
)
{
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
Check.NotNull(resource, nameof(resource));
Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName));
return resourcePermissionStore.GetGrantedResourceKeysAsync(
typeof(TResource).FullName!,
permissionName
);
}
}

22
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueCheckContext.cs

@ -0,0 +1,22 @@
using System.Security.Claims;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ResourcePermissionValueCheckContext : PermissionValueCheckContext
{
public string ResourceName { get; }
public string ResourceKey { get; }
public ResourcePermissionValueCheckContext(PermissionDefinition permission, string resourceName, string resourceKey)
: this(permission, null, resourceName, resourceKey)
{
}
public ResourcePermissionValueCheckContext(PermissionDefinition permission, ClaimsPrincipal? principal, string resourceName, string resourceKey)
: base(permission, principal)
{
ResourceName = resourceName;
ResourceKey = resourceKey;
}
}

20
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProvider.cs

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Authorization.Permissions.Resources;
public abstract class ResourcePermissionValueProvider : IResourcePermissionValueProvider, ITransientDependency
{
public abstract string Name { get; }
protected IResourcePermissionStore ResourcePermissionStore { get; }
protected ResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore)
{
ResourcePermissionStore = resourcePermissionStore;
}
public abstract Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context);
public abstract Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context);
}

38
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValuesCheckContext.cs

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Security.Claims;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ResourcePermissionValuesCheckContext : PermissionValuesCheckContext
{
public string ResourceName { get; }
public string ResourceKey { get; }
public ResourcePermissionValuesCheckContext(PermissionDefinition permission,string resourceName, string resourceKey)
: this([permission], null, resourceName, resourceKey)
{
}
public ResourcePermissionValuesCheckContext(PermissionDefinition permission, ClaimsPrincipal? principal, string resourceName, string resourceKey)
: this([permission], principal, resourceName, resourceKey)
{
}
public ResourcePermissionValuesCheckContext(List<PermissionDefinition> permissions, string resourceName, string resourceKey)
: this(permissions, null, resourceName, resourceKey)
{
ResourceName = resourceName;
ResourceKey = resourceKey;
}
public ResourcePermissionValuesCheckContext(List<PermissionDefinition> permissions, ClaimsPrincipal? principal, string resourceName, string resourceKey)
: base(permissions, principal)
{
ResourceName = resourceName;
ResourceKey = resourceKey;
}
}

21
framework/src/Volo.Abp.Authorization.Abstractions/Volo/Abp/Authorization/ResourcePermissionRequirement.cs

@ -0,0 +1,21 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Volo.Abp.Authorization;
public class ResourcePermissionRequirement : IAuthorizationRequirement
{
public string PermissionName { get; }
public ResourcePermissionRequirement([NotNull] string permissionName)
{
Check.NotNull(permissionName, nameof(permissionName));
PermissionName = permissionName;
}
public override string ToString()
{
return $"ResourcePermissionRequirement: {PermissionName}";
}
}

13
framework/src/Volo.Abp.Authorization/Microsoft/Extensions/DependencyInjection/KeyedObjectResourcePermissionExtenstions.cs

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Authorization.Permissions.Resources;
namespace Microsoft.Extensions.DependencyInjection;
public static class KeyedObjectResourcePermissionExtenstions
{
public static IServiceCollection AddKeyedObjectResourcePermissionAuthorization(this IServiceCollection services)
{
services.AddSingleton<IAuthorizationHandler, KeyedObjectResourcePermissionRequirementHandler>();
return services;
}
}

5
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationModule.cs

@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Authorization.Localization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Authorization.Permissions.Resources;
using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity;
@ -32,6 +33,7 @@ public class AbpAuthorizationModule : AbpModule
{
context.Services.AddAuthorizationCore();
context.Services.AddKeyedObjectResourcePermissionAuthorization();
context.Services.AddSingleton<IAuthorizationHandler, PermissionRequirementHandler>();
context.Services.AddSingleton<IAuthorizationHandler, PermissionsRequirementHandler>();
@ -42,6 +44,9 @@ public class AbpAuthorizationModule : AbpModule
options.ValueProviders.Add<UserPermissionValueProvider>();
options.ValueProviders.Add<RolePermissionValueProvider>();
options.ValueProviders.Add<ClientPermissionValueProvider>();
options.ResourceValueProviders.Add<UserResourcePermissionValueProvider>();
options.ResourceValueProviders.Add<RoleResourcePermissionValueProvider>();
});
Configure<AbpVirtualFileSystemOptions>(options =>

8
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/AbpAuthorizationPolicyProvider.cs

@ -40,6 +40,14 @@ public class AbpAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
return policyBuilder.Build();
}
if ((await _permissionDefinitionManager.GetResourcePermissionsAsync()).Any(x => x.Name == policyName))
{
//TODO: Optimize & Cache!
var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty<string>());
policyBuilder.Requirements.Add(new ResourcePermissionRequirement(policyName));
return policyBuilder.Build();
}
return null;
}

2
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/MethodInvocationAuthorizationService.cs

@ -20,7 +20,7 @@ public class MethodInvocationAuthorizationService : IMethodInvocationAuthorizati
_abpAuthorizationService = abpAuthorizationService;
}
public async Task CheckAsync(MethodInvocationAuthorizationContext context)
public virtual async Task CheckAsync(MethodInvocationAuthorizationContext context)
{
if (AllowAnonymous(context))
{

8
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IDynamicPermissionDefinitionStore.cs

@ -8,6 +8,10 @@ public interface IDynamicPermissionDefinitionStore
Task<PermissionDefinition?> GetOrNullAsync(string name);
Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync();
Task<PermissionDefinition?> GetResourcePermissionOrNullAsync(string resourceName, string name);
Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync();
Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync();
}
}

8
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/IStaticPermissionDefinitionStore.cs

@ -8,6 +8,10 @@ public interface IStaticPermissionDefinitionStore
Task<PermissionDefinition?> GetOrNullAsync(string name);
Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync();
Task<PermissionDefinition?> GetResourcePermissionOrNullAsync(string resourceName, string name);
Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync();
Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync();
}
}

19
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/NullDynamicPermissionDefinitionStore.cs

@ -9,10 +9,15 @@ namespace Volo.Abp.Authorization.Permissions;
public class NullDynamicPermissionDefinitionStore : IDynamicPermissionDefinitionStore, ISingletonDependency
{
private readonly static Task<PermissionDefinition?> CachedPermissionResult = Task.FromResult((PermissionDefinition?)null);
private readonly static Task<IReadOnlyList<PermissionDefinition>> CachedPermissionsResult =
Task.FromResult((IReadOnlyList<PermissionDefinition>)Array.Empty<PermissionDefinition>().ToImmutableList());
private readonly static Task<PermissionDefinition?> CachedResourcePermissionResult = Task.FromResult((PermissionDefinition?)null);
private readonly static Task<IReadOnlyList<PermissionDefinition>> CachedResourcePermissionsResult =
Task.FromResult((IReadOnlyList<PermissionDefinition>)Array.Empty<PermissionDefinition>().ToImmutableList());
private readonly static Task<IReadOnlyList<PermissionGroupDefinition>> CachedGroupsResult =
Task.FromResult((IReadOnlyList<PermissionGroupDefinition>)Array.Empty<PermissionGroupDefinition>().ToImmutableList());
@ -26,8 +31,18 @@ public class NullDynamicPermissionDefinitionStore : IDynamicPermissionDefinition
return CachedPermissionsResult;
}
public Task<PermissionDefinition?> GetResourcePermissionOrNullAsync(string resourceName, string name)
{
return CachedResourcePermissionResult;
}
public Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync()
{
return CachedResourcePermissionsResult;
}
public Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
return CachedGroupsResult;
}
}
}

44
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinitionManager.cs

@ -34,17 +34,36 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
{
Check.NotNull(name, nameof(name));
return await _staticStore.GetOrNullAsync(name) ??
return await _staticStore.GetOrNullAsync(name) ??
await _dynamicStore.GetOrNullAsync(name);
}
public virtual async Task<PermissionDefinition> GetResourcePermissionAsync(string resourceName, string name)
{
var permission = await GetResourcePermissionOrNullAsync(resourceName, name);
if (permission == null)
{
throw new AbpException($"Undefined resource permission: {name} for resource: {resourceName}");
}
return permission;
}
public virtual async Task<PermissionDefinition?> GetResourcePermissionOrNullAsync(string resourceName, string name)
{
Check.NotNull(name, nameof(name));
return await _staticStore.GetResourcePermissionOrNullAsync(resourceName, name) ??
await _dynamicStore.GetResourcePermissionOrNullAsync(resourceName, name);
}
public virtual async Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync()
{
var staticPermissions = await _staticStore.GetPermissionsAsync();
var staticPermissionNames = staticPermissions
.Select(p => p.Name)
.ToImmutableHashSet();
var dynamicPermissions = await _dynamicStore.GetPermissionsAsync();
/* We prefer static permissions over dynamics */
@ -53,13 +72,28 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
).ToImmutableList();
}
public async Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
public virtual async Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync()
{
var staticResourcePermissions = await _staticStore.GetResourcePermissionsAsync();
var staticResourcePermissionNames = staticResourcePermissions
.Select(p => p.Name)
.ToImmutableHashSet();
var dynamicResourcePermissions = await _dynamicStore.GetResourcePermissionsAsync();
/* We prefer static permissions over dynamics */
return staticResourcePermissions.Concat(
dynamicResourcePermissions.Where(d => !staticResourcePermissionNames.Contains(d.Name))
).ToImmutableList();
}
public virtual async Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
var staticGroups = await _staticStore.GetGroupsAsync();
var staticGroupNames = staticGroups
.Select(p => p.Name)
.ToImmutableHashSet();
var dynamicGroups = await _dynamicStore.GetGroupsAsync();
/* We prefer static groups over dynamics */
@ -67,4 +101,4 @@ public class PermissionDefinitionManager : IPermissionDefinitionManager, ITransi
dynamicGroups.Where(d => !staticGroupNames.Contains(d.Name))
).ToImmutableList();
}
}
}

28
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionCheckerExtensions.cs

@ -0,0 +1,28 @@
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public static class KeyedObjectResourcePermissionCheckerExtensions
{
/// <summary>
/// Checks if the specified permission is granted for the given resource.
/// </summary>
/// <typeparam name="TResource">The type of the object.</typeparam>
/// <param name="resourcePermissionChecker">The resource permission checker instance.</param>
/// <param name="permissionName">The name of the permission to check.</param>
/// <param name="resource">The resource for which the permission is being checked.</param>
/// <returns>A task that represents the asynchronous operation. The task result is a boolean indicating whether the permission is granted.</returns>
public static Task<bool> IsGrantedAsync<TResource>(this IResourcePermissionChecker resourcePermissionChecker, string permissionName, TResource resource)
where TResource : class, IKeyedObject
{
Check.NotNull(resourcePermissionChecker, nameof(resourcePermissionChecker));
Check.NotNullOrWhiteSpace(permissionName, nameof(permissionName));
Check.NotNull(resource, nameof(resource));
return resourcePermissionChecker.IsGrantedAsync(
permissionName,
resource,
resource.GetObjectKey() ?? throw new AbpException("The resource doesn't have a key.")
);
}
}

33
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionRequirementHandler.cs

@ -0,0 +1,33 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class KeyedObjectResourcePermissionRequirementHandler : AuthorizationHandler<ResourcePermissionRequirement, IKeyedObject>
{
protected readonly IResourcePermissionChecker PermissionChecker;
public KeyedObjectResourcePermissionRequirementHandler(IResourcePermissionChecker permissionChecker)
{
PermissionChecker = permissionChecker;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ResourcePermissionRequirement requirement,
IKeyedObject? resource)
{
if (resource == null)
{
return;
}
var resourceName = resource.GetType().FullName!;
var resourceKey = resource.GetObjectKey() ?? throw new AbpException("The resource doesn't have a key.");
if (await PermissionChecker.IsGrantedAsync(context.User, requirement.PermissionName, resourceName, resourceKey))
{
context.Succeed(requirement);
}
}
}

49
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/KeyedObjectResourcePermissionStoreExtensions.cs

@ -0,0 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Volo.Abp.Authorization.Permissions.Resources;
public static class KeyedObjectResourcePermissionStoreExtensions
{
/// <summary>
/// Retrieves an array of granted permissions for a specific entity.
/// </summary>
/// <typeparam name="TResource">The type of the resource.</typeparam>
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
/// <param name="resource">The resource for which the permissions are being checked.</param>
/// <returns>An array of granted permission names as strings.</returns>
public static async Task<string[]> GetGrantedPermissionsAsync<TResource>(
this IResourcePermissionStore resourcePermissionStore,
TResource resource
)
where TResource : class, IKeyedObject
{
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
Check.NotNull(resource, nameof(resource));
return (await GetPermissionsAsync(resourcePermissionStore, resource)).Where(x => x.Value).Select(x => x.Key).ToArray();
}
/// <summary>
/// Retrieves a dictionary of permissions and their granted status for the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="resourcePermissionStore">The resource permission store instance.</param>
/// <param name="entity">The entity for which the permissions are being retrieved.</param>
/// <returns>A dictionary where the keys are permission names and the values are booleans indicating whether the permission is granted.</returns>
public static async Task<IDictionary<string, bool>> GetPermissionsAsync<TEntity>(
this IResourcePermissionStore resourcePermissionStore,
TEntity entity
)
where TEntity : class, IKeyedObject
{
Check.NotNull(resourcePermissionStore, nameof(resourcePermissionStore));
Check.NotNull(entity, nameof(entity));
return await resourcePermissionStore.GetPermissionsAsync(
entity,
entity.GetObjectKey() ?? throw new AbpException("The entity doesn't have a key.")
);
}
}

173
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionChecker.cs

@ -0,0 +1,173 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Security.Claims;
using Volo.Abp.SimpleStateChecking;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ResourcePermissionChecker : IResourcePermissionChecker, ITransientDependency
{
protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
protected ICurrentPrincipalAccessor PrincipalAccessor { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IResourcePermissionValueProviderManager PermissionValueProviderManager { get; }
protected ISimpleStateCheckerManager<PermissionDefinition> StateCheckerManager { get; }
protected IPermissionChecker PermissionChecker { get; }
public ResourcePermissionChecker(
ICurrentPrincipalAccessor principalAccessor,
IPermissionDefinitionManager permissionDefinitionManager,
ICurrentTenant currentTenant,
IResourcePermissionValueProviderManager permissionValueProviderManager,
ISimpleStateCheckerManager<PermissionDefinition> stateCheckerManager,
IPermissionChecker permissionChecker)
{
PrincipalAccessor = principalAccessor;
PermissionDefinitionManager = permissionDefinitionManager;
CurrentTenant = currentTenant;
PermissionValueProviderManager = permissionValueProviderManager;
StateCheckerManager = stateCheckerManager;
PermissionChecker = permissionChecker;
}
public virtual async Task<bool> IsGrantedAsync(string name, string resourceName, string resourceKey)
{
return await IsGrantedAsync(PrincipalAccessor.Principal, name, resourceName, resourceKey);
}
public virtual async Task<bool> IsGrantedAsync(
ClaimsPrincipal? claimsPrincipal,
string name,
string resourceName,
string resourceKey)
{
Check.NotNull(name, nameof(name));
var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name);
if (permission == null)
{
return false;
}
if (!permission.IsEnabled)
{
return false;
}
if (!await StateCheckerManager.IsEnabledAsync(permission))
{
return false;
}
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide()
?? CurrentTenant.GetMultiTenancySide();
if (!permission.MultiTenancySide.HasFlag(multiTenancySide))
{
return false;
}
var isGranted = false;
var context = new ResourcePermissionValueCheckContext(permission, claimsPrincipal, resourceName, resourceKey);
foreach (var provider in PermissionValueProviderManager.ValueProviders)
{
if (context.Permission.Providers.Any() &&
!context.Permission.Providers.Contains(provider.Name))
{
continue;
}
var result = await provider.CheckAsync(context);
if (result == PermissionGrantResult.Granted)
{
isGranted = true;
}
else if (result == PermissionGrantResult.Prohibited)
{
return false;
}
}
return isGranted;
}
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names, string resourceName, string resourceKey)
{
return await IsGrantedAsync(PrincipalAccessor.Principal, names, resourceName, resourceKey);
}
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(ClaimsPrincipal? claimsPrincipal, string[] names, string resourceName, string resourceKey)
{
Check.NotNull(names, nameof(names));
var result = new MultiplePermissionGrantResult();
if (!names.Any())
{
return result;
}
var multiTenancySide = claimsPrincipal?.GetMultiTenancySide() ??
CurrentTenant.GetMultiTenancySide();
var permissionDefinitions = new List<PermissionDefinition>();
foreach (var name in names)
{
var permission = await PermissionDefinitionManager.GetResourcePermissionOrNullAsync(resourceName, name);
if (permission == null)
{
result.Result.Add(name, PermissionGrantResult.Prohibited);
continue;
}
result.Result.Add(name, PermissionGrantResult.Undefined);
if (permission.IsEnabled &&
await StateCheckerManager.IsEnabledAsync(permission) &&
permission.MultiTenancySide.HasFlag(multiTenancySide))
{
permissionDefinitions.Add(permission);
}
}
foreach (var provider in PermissionValueProviderManager.ValueProviders)
{
var permissions = permissionDefinitions
.Where(x => !x.Providers.Any() || x.Providers.Contains(provider.Name))
.ToList();
if (permissions.IsNullOrEmpty())
{
continue;
}
var context = new ResourcePermissionValuesCheckContext(
permissions,
claimsPrincipal,
resourceName,
resourceKey);
var multipleResult = await provider.CheckAsync(context);
foreach (var grantResult in multipleResult.Result.Where(grantResult =>
result.Result.ContainsKey(grantResult.Key) &&
result.Result[grantResult.Key] == PermissionGrantResult.Undefined &&
grantResult.Value != PermissionGrantResult.Undefined))
{
result.Result[grantResult.Key] = grantResult.Value;
permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key);
}
if (result.AllGranted || result.AllProhibited)
{
break;
}
}
return result;
}
}

65
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionPopulator.cs

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ResourcePermissionPopulator : ITransientDependency
{
protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
protected IResourcePermissionChecker ResourcePermissionChecker { get; }
protected IResourcePermissionStore ResourcePermissionStore { get; }
protected IPermissionChecker PermissionChecker { get; }
public ResourcePermissionPopulator(
IPermissionDefinitionManager permissionDefinitionManager,
IResourcePermissionChecker resourcePermissionChecker,
IResourcePermissionStore resourcePermissionStore,
IPermissionChecker permissionChecker)
{
PermissionDefinitionManager = permissionDefinitionManager;
ResourcePermissionChecker = resourcePermissionChecker;
ResourcePermissionStore = resourcePermissionStore;
PermissionChecker = permissionChecker;
}
public virtual async Task PopulateAsync<TResource>(TResource resource, string resourceName)
where TResource : IHasResourcePermissions
{
await PopulateAsync([resource], resourceName);
}
public virtual async Task PopulateAsync<TResource>(List<TResource> resources, string resourceName)
where TResource : IHasResourcePermissions
{
Check.NotNull(resources, nameof(resources));
Check.NotNullOrWhiteSpace(resourceName, nameof(resourceName));
var resopurcePermissions = (await PermissionDefinitionManager.GetResourcePermissionsAsync())
.Where(x => x.ResourceName == resourceName)
.ToArray();
foreach (var resource in resources)
{
var resourceKey = resource.GetObjectKey();
if (resourceKey.IsNullOrEmpty())
{
throw new AbpException("Resource key can not be null or empty.");
}
var results = await ResourcePermissionChecker.IsGrantedAsync(resopurcePermissions.Select(x => x.Name).ToArray(), resourceName, resourceKey);
foreach (var resopurcePermission in resopurcePermissions)
{
if (resource.ResourcePermissions == null)
{
ObjectHelper.TrySetProperty(resource, x => x.ResourcePermissions, () => new Dictionary<string, bool>());
}
var hasPermission = results.Result.TryGetValue(resopurcePermission.Name, out var granted) && granted == PermissionGrantResult.Granted;
resource.ResourcePermissions![resopurcePermission.Name] = hasPermission;
}
}
}
}

43
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/ResourcePermissionValueProviderManager.cs

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class ResourcePermissionValueProviderManager : IResourcePermissionValueProviderManager, ISingletonDependency
{
public IReadOnlyList<IResourcePermissionValueProvider> ValueProviders => _lazyProviders.Value;
private readonly Lazy<List<IResourcePermissionValueProvider>> _lazyProviders;
protected AbpPermissionOptions Options { get; }
protected IServiceProvider ServiceProvider { get; }
public ResourcePermissionValueProviderManager(
IServiceProvider serviceProvider,
IOptions<AbpPermissionOptions> options)
{
Options = options.Value;
ServiceProvider = serviceProvider;
_lazyProviders = new Lazy<List<IResourcePermissionValueProvider>>(GetProviders, true);
}
protected virtual List<IResourcePermissionValueProvider> GetProviders()
{
var providers = Options
.ResourceValueProviders
.Select(type => (ServiceProvider.GetRequiredService(type) as IResourcePermissionValueProvider)!)
.ToList();
var multipleProviders = providers.GroupBy(p => p.Name).FirstOrDefault(x => x.Count() > 1);
if(multipleProviders != null)
{
throw new AbpException($"Duplicate resource permission value provider name detected: {multipleProviders.Key}. Providers:{Environment.NewLine}{multipleProviders.Select(p => p.GetType().FullName!).JoinAsString(Environment.NewLine)}");
}
return providers;
}
}

79
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/RoleResourcePermissionValueProvider.cs

@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Security.Claims;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class RoleResourcePermissionValueProvider : ResourcePermissionValueProvider
{
public const string ProviderName = "R";
public override string Name => ProviderName;
public RoleResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore)
: base(resourcePermissionStore)
{
}
public override async Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context)
{
var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray();
if (roles == null || !roles.Any())
{
return PermissionGrantResult.Undefined;
}
foreach (var role in roles.Distinct())
{
if (await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, role))
{
return PermissionGrantResult.Granted;
}
}
return PermissionGrantResult.Undefined;
}
public override async Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context)
{
var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToList();
Check.NotNullOrEmpty(permissionNames, nameof(permissionNames));
var result = new MultiplePermissionGrantResult(permissionNames.ToArray());
var roles = context.Principal?.FindAll(AbpClaimTypes.Role).Select(c => c.Value).ToArray();
if (roles == null || !roles.Any())
{
return result;
}
foreach (var role in roles.Distinct())
{
var multipleResult = await ResourcePermissionStore.IsGrantedAsync(permissionNames.ToArray(), context.ResourceName, context.ResourceKey, Name, role);
foreach (var grantResult in multipleResult.Result.Where(grantResult =>
result.Result.ContainsKey(grantResult.Key) &&
result.Result[grantResult.Key] == PermissionGrantResult.Undefined &&
grantResult.Value != PermissionGrantResult.Undefined))
{
result.Result[grantResult.Key] = grantResult.Value;
permissionNames.RemoveAll(x => x == grantResult.Key);
}
if (result.AllGranted || result.AllProhibited)
{
break;
}
if (permissionNames.IsNullOrEmpty())
{
break;
}
}
return result;
}
}

46
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/Resources/UserResourcePermissionValueProvider.cs

@ -0,0 +1,46 @@
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Security.Claims;
namespace Volo.Abp.Authorization.Permissions.Resources;
public class UserResourcePermissionValueProvider : ResourcePermissionValueProvider
{
public const string ProviderName = "U";
public override string Name => ProviderName;
public UserResourcePermissionValueProvider(IResourcePermissionStore resourcePermissionStore)
: base(resourcePermissionStore)
{
}
public override async Task<PermissionGrantResult> CheckAsync(ResourcePermissionValueCheckContext context)
{
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
if (userId == null)
{
return PermissionGrantResult.Undefined;
}
return await ResourcePermissionStore.IsGrantedAsync(context.Permission.Name, context.ResourceName, context.ResourceKey, Name, userId)
? PermissionGrantResult.Granted
: PermissionGrantResult.Undefined;
}
public override async Task<MultiplePermissionGrantResult> CheckAsync(ResourcePermissionValuesCheckContext context)
{
var permissionNames = context.Permissions.Select(x => x.Name).Distinct().ToArray();
Check.NotNullOrEmpty(permissionNames, nameof(permissionNames));
var userId = context.Principal?.FindFirst(AbpClaimTypes.UserId)?.Value;
if (userId == null)
{
return new MultiplePermissionGrantResult(permissionNames);
}
return await ResourcePermissionStore.IsGrantedAsync(permissionNames, context.ResourceName, context.ResourceKey, Name, userId);
}
}

32
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs

@ -11,12 +11,14 @@ namespace Volo.Abp.Authorization.Permissions;
public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore, ISingletonDependency
{
protected IDictionary<string, PermissionGroupDefinition> PermissionGroupDefinitions => _lazyPermissionGroupDefinitions.Value;
private readonly Lazy<Dictionary<string, PermissionGroupDefinition>> _lazyPermissionGroupDefinitions;
protected IDictionary<string, PermissionGroupDefinition> PermissionGroupDefinitions => _lazyPermissionGroupDefinitions.Value.Item1;
private readonly Lazy<(Dictionary<string, PermissionGroupDefinition>, List<PermissionDefinition>)> _lazyPermissionGroupDefinitions;
protected IDictionary<string, PermissionDefinition> PermissionDefinitions => _lazyPermissionDefinitions.Value;
private readonly Lazy<Dictionary<string, PermissionDefinition>> _lazyPermissionDefinitions;
protected IList<PermissionDefinition> ResourcePermissionDefinitions => _lazyPermissionGroupDefinitions.Value.Item2;
protected AbpPermissionOptions Options { get; }
private readonly IServiceProvider _serviceProvider;
@ -33,12 +35,12 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
isThreadSafe: true
);
_lazyPermissionGroupDefinitions = new Lazy<Dictionary<string, PermissionGroupDefinition>>(
_lazyPermissionGroupDefinitions = new Lazy<(Dictionary<string, PermissionGroupDefinition>, List<PermissionDefinition>)>(
CreatePermissionGroupDefinitions,
isThreadSafe: true
);
}
protected virtual Dictionary<string, PermissionDefinition> CreatePermissionDefinitions()
{
var permissions = new Dictionary<string, PermissionDefinition>();
@ -71,7 +73,7 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
}
}
protected virtual Dictionary<string, PermissionGroupDefinition> CreatePermissionGroupDefinitions()
protected virtual (Dictionary<string, PermissionGroupDefinition>, List<PermissionDefinition>) CreatePermissionGroupDefinitions()
{
using (var scope = _serviceProvider.CreateScope())
{
@ -99,10 +101,10 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
context.CurrentProvider = provider;
provider.PostDefine(context);
}
context.CurrentProvider = null;
return context.Groups;
return (context.Groups, context.ResourcePermissions);
}
}
@ -110,7 +112,7 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
{
return Task.FromResult(PermissionDefinitions.GetOrDefault(name));
}
public virtual Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync()
{
return Task.FromResult<IReadOnlyList<PermissionDefinition>>(
@ -118,10 +120,22 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
);
}
public virtual Task<PermissionDefinition?> GetResourcePermissionOrNullAsync(string resourceName, string name)
{
return Task.FromResult<PermissionDefinition?>(ResourcePermissionDefinitions.FirstOrDefault(p => p.ResourceName == resourceName && p.Name == name));
}
public virtual Task<IReadOnlyList<PermissionDefinition>> GetResourcePermissionsAsync()
{
return Task.FromResult<IReadOnlyList<PermissionDefinition>>(
ResourcePermissionDefinitions.ToImmutableList()
);
}
public Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
return Task.FromResult<IReadOnlyList<PermissionGroupDefinition>>(
PermissionGroupDefinitions.Values.ToImmutableList()
);
}
}
}

11
framework/src/Volo.Abp.BlazoriseUI/Components/DataGridEntityActionsColumn.razor.cs

@ -23,7 +23,16 @@ public partial class DataGridEntityActionsColumn<TItem> : DataGridColumn<TItem>
Caption = UiLocalizer["Actions"];
Width = "150px";
Sortable = false;
Field = typeof(TItem).GetProperties().First().Name;
Field = ResolveFieldName();
return ValueTask.CompletedTask;
}
protected virtual string ResolveFieldName()
{
var props = typeof(TItem).GetProperties();
return props.Length > 0
? props[0].Name
: "Id";
}
}

26
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs

@ -1,5 +1,8 @@
using System;
using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Cli.Commands.Internal;
using Volo.Abp.Cli.Http;
@ -7,9 +10,12 @@ using Volo.Abp.Cli.ServiceProxying;
using Volo.Abp.Cli.ServiceProxying.Angular;
using Volo.Abp.Cli.ServiceProxying.CSharp;
using Volo.Abp.Cli.ServiceProxying.JavaScript;
using Volo.Abp.Cli.Telemetry;
using Volo.Abp.Domain;
using Volo.Abp.Http;
using Volo.Abp.IdentityModel;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Activity.Providers;
using Volo.Abp.Json;
using Volo.Abp.Localization;
using Volo.Abp.Minify;
@ -27,6 +33,8 @@ namespace Volo.Abp.Cli;
)]
public class AbpCliCoreModule : AbpModule
{
private const string EnableTelemetryVariableName = "ABP_STUDIO_ENABLE_TELEMETRY";
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddHttpClient(CliConsts.HttpClientName)
@ -82,5 +90,23 @@ public class AbpCliCoreModule : AbpModule
options.Generators[AngularServiceProxyGenerator.Name] = typeof(AngularServiceProxyGenerator);
options.Generators[CSharpServiceProxyGenerator.Name] = typeof(CSharpServiceProxyGenerator);
});
ConfigureTelemetry(context.Services);
}
private static void ConfigureTelemetry(IServiceCollection services)
{
var enableTelemetryEnvironmentVariable = Environment.GetEnvironmentVariable(EnableTelemetryVariableName , EnvironmentVariableTarget.Machine)
?? Environment.GetEnvironmentVariable(EnableTelemetryVariableName , EnvironmentVariableTarget.User)
?? Environment.GetEnvironmentVariable(EnableTelemetryVariableName , EnvironmentVariableTarget.Process);
if (enableTelemetryEnvironmentVariable.IsNullOrEmpty() || !enableTelemetryEnvironmentVariable.Equals("false", StringComparison.InvariantCultureIgnoreCase))
{
services.Remove(services.First(p => p.ImplementationType == typeof(TelemetrySessionInfoEnricher)));
}
else
{
services.Replace(ServiceDescriptor.Singleton<ITelemetryService, NullTelemetryService>());
}
}
}

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliService.cs

@ -14,12 +14,15 @@ using Volo.Abp.Cli.Memory;
using Volo.Abp.Cli.Version;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli;
public class CliService : ITransientDependency
{
private readonly MemoryService _memoryService;
private readonly ITelemetryService _telemetryService;
public ILogger<CliService> Logger { get; set; }
protected ICommandLineArgumentParser CommandLineArgumentParser { get; }
protected ICommandSelector CommandSelector { get; }
@ -35,7 +38,8 @@ public class CliService : ITransientDependency
PackageVersionCheckerService nugetService,
ICmdHelper cmdHelper,
MemoryService memoryService,
CliVersionService cliVersionService)
CliVersionService cliVersionService,
ITelemetryService telemetryService)
{
_memoryService = memoryService;
CommandLineArgumentParser = commandLineArgumentParser;
@ -44,6 +48,7 @@ public class CliService : ITransientDependency
PackageVersionCheckerService = nugetService;
CmdHelper = cmdHelper;
CliVersionService = cliVersionService;
_telemetryService = telemetryService;
Logger = NullLogger<CliService>.Instance;
}
@ -64,6 +69,7 @@ public class CliService : ITransientDependency
try
{
await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliRun);
if (commandLineArgs.IsCommand("prompt"))
{
await RunPromptAsync();
@ -84,9 +90,14 @@ public class CliService : ITransientDependency
}
catch (Exception ex)
{
await _telemetryService.AddErrorActivityAsync(ex.Message);
Logger.LogException(ex);
throw;
}
finally
{
await _telemetryService.AddActivityAsync(ActivityNameConsts.AbpCliExit);
}
}
private async Task RunPromptAsync()

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs

@ -11,6 +11,8 @@ using Volo.Abp.Cli.ProjectBuilding.Templates.MvcModule;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@ -20,6 +22,8 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency
public const string Name = "add-module";
private AddModuleInfoOutput _lastAddedModuleInfo;
private readonly ITelemetryService _telemetryService;
public ILogger<AddModuleCommand> Logger { get; set; }
protected SolutionModuleAdder SolutionModuleAdder { get; }
@ -39,11 +43,13 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency
public AddModuleCommand(
SolutionModuleAdder solutionModuleAdder,
SolutionPackageVersionFinder solutionPackageVersionFinder,
IOptions<AbpCliOptions> options)
IOptions<AbpCliOptions> options,
ITelemetryService telemetryService)
{
_options = options.Value;
SolutionModuleAdder = solutionModuleAdder;
SolutionPackageVersionFinder = solutionPackageVersionFinder;
_telemetryService = telemetryService;
Logger = NullLogger<AddModuleCommand>.Instance;
}
@ -66,6 +72,11 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency
}
var newTemplate = commandLineArgs.Options.ContainsKey(Options.NewTemplate.Long);
await using var _ = _telemetryService.TrackActivityAsync(newTemplate
? ActivityNameConsts.AbpCliCommandsInstallLocalModule
: ActivityNameConsts.AbpCliCommandsInstallModule);
var template = commandLineArgs.Options.GetOrNull(Options.Template.Short, Options.Template.Long);
var newProTemplate = !string.IsNullOrEmpty(template) && template == ModuleProTemplate.TemplateName;
var withSourceCode = newTemplate || newProTemplate || commandLineArgs.Options.ContainsKey(Options.SourceCode.Long);

14
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddPackageCommand.cs

@ -9,6 +9,8 @@ using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@ -16,16 +18,19 @@ public class AddPackageCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "add-package";
private readonly ITelemetryService _telemetryService;
public ILogger<AddPackageCommand> Logger { get; set; }
protected ProjectNugetPackageAdder ProjectNugetPackageAdder { get; }
public ProjectNpmPackageAdder ProjectNpmPackageAdder { get; }
public AddPackageCommand(ProjectNugetPackageAdder projectNugetPackageAdder, ProjectNpmPackageAdder projectNpmPackageAdder)
public AddPackageCommand(ProjectNugetPackageAdder projectNugetPackageAdder, ProjectNpmPackageAdder projectNpmPackageAdder, ITelemetryService telemetryService)
{
ProjectNugetPackageAdder = projectNugetPackageAdder;
ProjectNpmPackageAdder = projectNpmPackageAdder;
_telemetryService = telemetryService;
Logger = NullLogger<AddPackageCommand>.Instance;
}
@ -39,6 +44,9 @@ public class AddPackageCommand : IConsoleCommand, ITransientDependency
GetUsageInfo()
);
}
await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsNewPackage);
await using var __ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsAddPackage);
var isNpmPackage = false;
var isNugetPackage = true;
@ -51,11 +59,11 @@ public class AddPackageCommand : IConsoleCommand, ITransientDependency
var version = commandLineArgs.Options.GetOrNull(Options.Version.Short, Options.Version.Long);
var withSourceCode = commandLineArgs.Options.ContainsKey(Options.SourceCode.Long);
if (isNugetPackage)
{
var addSourceCodeToSolutionFile = withSourceCode && commandLineArgs.Options.ContainsKey("add-to-solution-file");
await ProjectNugetPackageAdder.AddAsync(
GetSolutionFile(commandLineArgs),
GetProjectFile(commandLineArgs),

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs

@ -8,6 +8,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@ -16,17 +18,21 @@ public class CleanCommand : IConsoleCommand, ITransientDependency
public const string Name = "clean";
public ILogger<CleanCommand> Logger { get; set; }
protected ICmdHelper CmdHelper { get; }
private readonly ITelemetryService _telemetryService;
public CleanCommand(ICmdHelper cmdHelper)
public CleanCommand(ICmdHelper cmdHelper, ITelemetryService telemetryService)
{
CmdHelper = cmdHelper;
_telemetryService = telemetryService;
Logger = NullLogger<CleanCommand>.Instance;
}
public Task ExecuteAsync(CommandLineArgs commandLineArgs)
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsClean);
var binEntries = Directory.EnumerateDirectories(Directory.GetCurrentDirectory(), "bin", SearchOption.AllDirectories);
var objEntries = Directory.EnumerateDirectories(Directory.GetCurrentDirectory(), "obj", SearchOption.AllDirectories);
@ -49,7 +55,6 @@ public class CleanCommand : IConsoleCommand, ITransientDependency
Logger.LogInformation($"'bin' and 'obj' folders removed successfully!");
Logger.LogInformation("Solution cleaned successfully!");
return Task.CompletedTask;
}
public string GetUsageInfo()

1
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/Internal/RecreateInitialMigrationCommand.cs

@ -47,6 +47,7 @@ public class RecreateInitialMigrationCommand : IConsoleCommand, ITransientDepend
Directory.Delete(Path.Combine(projectDir, "Migrations"), true);
}
CmdHelper.RunCmd($"dotnet build", workingDirectory: projectDir);
var separateDbContext = false;
if (Directory.Exists(Path.Combine(projectDir, "TenantMigrations")))
{

8
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ListModulesCommand.cs

@ -7,6 +7,8 @@ using System.Threading.Tasks;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@ -17,15 +19,19 @@ public class ListModulesCommand : IConsoleCommand, ITransientDependency
public ModuleInfoProvider ModuleInfoProvider { get; }
public ILogger<ListModulesCommand> Logger { get; set; }
private readonly ITelemetryService _telemetryService;
public ListModulesCommand(ModuleInfoProvider moduleInfoProvider)
public ListModulesCommand(ModuleInfoProvider moduleInfoProvider, ITelemetryService telemetryService)
{
ModuleInfoProvider = moduleInfoProvider;
_telemetryService = telemetryService;
Logger = NullLogger<ListModulesCommand>.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsListModules);
var modules = await ModuleInfoProvider.GetModuleListAsync();
var freeModules = modules.Where(m => !m.IsPro).ToList();
var proModules = modules.Where(m => m.IsPro).ToList();

23
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs

@ -11,11 +11,15 @@ using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectBuilding.Events;
using Volo.Abp.Cli.ProjectBuilding.Templates.Module;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
using Volo.Abp.Cli.Version;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
using Volo.Abp.Internal.Telemetry.Constants.Enums;
namespace Volo.Abp.Cli.Commands;
@ -26,6 +30,8 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
public ITemplateInfoProvider TemplateInfoProvider { get; }
private readonly ITelemetryService _telemetryService;
public NewCommand(
ConnectionStringProvider connectionStringProvider,
SolutionPackageVersionFinder solutionPackageVersionFinder,
@ -40,7 +46,8 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
ITemplateInfoProvider templateInfoProvider,
TemplateProjectBuilder templateProjectBuilder,
AngularThemeConfigurer angularThemeConfigurer,
CliVersionService cliVersionService) :
CliVersionService cliVersionService,
ITelemetryService telemetryService) :
base(connectionStringProvider,
solutionPackageVersionFinder,
cmdHelper,
@ -56,6 +63,7 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
{
TemplateInfoProvider = templateInfoProvider;
TemplateProjectBuilder = templateProjectBuilder;
_telemetryService = telemetryService;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
@ -94,6 +102,19 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
var result = await TemplateProjectBuilder.BuildAsync(
projectArgs
);
var activityName = ActivityNameConsts.AbpCliCommandsNewSolution;
if (ModuleTemplateBase.IsModuleTemplate(template))
{
activityName = ActivityNameConsts.AbpCliCommandsNewModule;
}
await _telemetryService.AddActivityAsync(activityName, o =>
{
o[ActivityPropertyNames.CreationTool] = AbpTool.OldCli;
o[ActivityPropertyNames.Template] = template;
});
ExtractProjectZip(result, projectArgs.OutputFolder);

7
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/UpdateCommand.cs

@ -9,6 +9,8 @@ using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Constants;
namespace Volo.Abp.Cli.Commands;
@ -20,18 +22,21 @@ public class UpdateCommand : IConsoleCommand, ITransientDependency
private readonly VoloNugetPackagesVersionUpdater _nugetPackagesVersionUpdater;
private readonly NpmPackagesUpdater _npmPackagesUpdater;
private readonly ITelemetryService _telemetryService;
public UpdateCommand(VoloNugetPackagesVersionUpdater nugetPackagesVersionUpdater,
NpmPackagesUpdater npmPackagesUpdater)
NpmPackagesUpdater npmPackagesUpdater, ITelemetryService telemetryService)
{
_nugetPackagesVersionUpdater = nugetPackagesVersionUpdater;
_npmPackagesUpdater = npmPackagesUpdater;
_telemetryService = telemetryService;
Logger = NullLogger<UpdateCommand>.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
await using var _ = _telemetryService.TrackActivityAsync(ActivityNameConsts.AbpCliCommandsUpdate);
var updateNpm = commandLineArgs.Options.ContainsKey(Options.Packages.Npm);
var updateNuget = commandLineArgs.Options.ContainsKey(Options.Packages.NuGet);

29
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/LocalReferenceConverter.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
@ -12,9 +13,9 @@ namespace Volo.Abp.Cli.ProjectModification;
public class LocalReferenceConverter : ITransientDependency
{
public ILogger<LocalReferenceConverter> Logger { get; set; }
public async Task ConvertAsync(
[NotNull] string directory,
[NotNull] List<string> localPaths)
@ -26,14 +27,14 @@ public class LocalReferenceConverter : ITransientDependency
var targetProjects = Directory.GetFiles(directory, "*.csproj", SearchOption.AllDirectories);
Logger.LogInformation($"Converting projects to local reference.");
foreach (var targetProject in targetProjects)
{
Logger.LogInformation($"Converting to local reference: {targetProject}");
await ConvertProjectToLocalReferences(targetProject, localProjects);
}
Logger.LogInformation($"Converted {targetProjects.Length} projects to local references.");
}
@ -41,14 +42,14 @@ public class LocalReferenceConverter : ITransientDependency
{
var xmlDocument = new XmlDocument() { PreserveWhitespace = true };
xmlDocument.Load(GenerateStreamFromString(File.ReadAllText(targetProject)));
var matchedNodes = xmlDocument.SelectNodes($"/Project/ItemGroup/PackageReference[@Include]");
if (matchedNodes == null || matchedNodes.Count == 0)
{
return;
}
foreach (XmlNode matchedNode in matchedNodes)
{
var packageName = matchedNode!.Attributes!["Include"].Value;
@ -62,7 +63,7 @@ public class LocalReferenceConverter : ITransientDependency
{
continue;
}
var parentNode = matchedNode.ParentNode;
parentNode!.RemoveChild(matchedNode);
@ -72,10 +73,10 @@ public class LocalReferenceConverter : ITransientDependency
newNode.Attributes.Append(includeAttr);
parentNode.AppendChild(newNode);
}
File.WriteAllText(targetProject, XDocument.Parse(xmlDocument.OuterXml).ToString());
}
private string CalculateRelativePath(string targetProject, string localProject)
{
return new Uri(targetProject).MakeRelativeUri(new Uri(localProject)).ToString();
@ -91,8 +92,12 @@ public class LocalReferenceConverter : ITransientDependency
{
continue;
}
list.AddRange(Directory.GetFiles(localPath, "*.csproj", SearchOption.AllDirectories));
var ignoreFolders = new[] { "bin", "obj", ".vs", ".idea", ".vscode", ".git" };
var csprojFiles = Directory.GetFiles(localPath, "*.csproj", SearchOption.AllDirectories)
.Where(x => !ignoreFolders.Any(x.Contains))
.ToList();
list.AddRange(csprojFiles);
}
return list;

39
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Telemetry/NullTelemetryService.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Internal.Telemetry;
using Volo.Abp.Internal.Telemetry.Activity;
namespace Volo.Abp.Cli.Telemetry;
public class NullTelemetryService : ITelemetryService
{
public IAsyncDisposable TrackActivity(ActivityEvent activityData)
{
return NullAsyncDisposable.Instance;
}
public IAsyncDisposable TrackActivityAsync(string activityName, Action<Dictionary<string, object>>? additionalProperties = null)
{
return NullAsyncDisposable.Instance;
}
public Task AddActivityAsync(string activityName, Action<Dictionary<string, object>>? additionalProperties = null)
{
return Task.CompletedTask;
}
public Task AddErrorActivityAsync(Action<Dictionary<string, object>> additionalProperties)
{
return Task.CompletedTask;
}
public Task AddErrorActivityAsync(string errorMessage)
{
return Task.CompletedTask;
}
public Task AddErrorForActivityAsync(string failingActivity, string errorMessage)
{
return Task.CompletedTask;
}
}

44
framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Telemetry/TelemetryCliSessionProvider.cs

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Internal.Telemetry.Activity;
using Volo.Abp.Internal.Telemetry.Activity.Contracts;
using Volo.Abp.Internal.Telemetry.Activity.Providers;
using Volo.Abp.Internal.Telemetry.Constants;
using Volo.Abp.Internal.Telemetry.Constants.Enums;
namespace Volo.Abp.Cli.Telemetry;
[ExposeServices(typeof(ITelemetryActivityEventEnricher))]
public class TelemetryCliSessionProvider : TelemetryActivityEventEnricher
{
public TelemetryCliSessionProvider(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
public override int ExecutionOrder { get; set; } = 10;
protected override Task ExecuteAsync(ActivityContext context)
{
context.Current[ActivityPropertyNames.SessionType] = SessionType.AbpCli;
context.Current[ActivityPropertyNames.SessionId] = Guid.NewGuid();
context.Current[ActivityPropertyNames.IsFirstSession] = !File.Exists(TelemetryPaths.ActivityStorage);
context.Current["OldCli"] = true;
if(context.Current.TryGetValue<Dictionary<string, object>>(ActivityPropertyNames.AdditionalProperties, out var additionalProperties))
{
additionalProperties["OldCli"] = true;
}
else
{
context.Current[ActivityPropertyNames.AdditionalProperties] = new Dictionary<string, object>
{
{ "OldCli", true }
};
}
return Task.CompletedTask;
}
}

6
framework/src/Volo.Abp.Core/Volo/Abp/IKeyedObject.cs

@ -0,0 +1,6 @@
namespace Volo.Abp;
public interface IKeyedObject
{
string? GetObjectKey();
}

47
framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs

@ -113,29 +113,42 @@ internal sealed class TelemetrySolutionInfoEnricher : TelemetryActivityEventEnri
continue;
}
var moduleJsonFileContent = File.ReadAllText(modulePath);
using var moduleDoc = JsonDocument.Parse(moduleJsonFileContent, new JsonDocumentOptions
{
AllowTrailingCommas = true
});
ExtractModuleImportsInfo(modulePath, modules);
}
context.Current[ActivityPropertyNames.InstalledModules] = modules;
}
private static void ExtractModuleImportsInfo(string modulePath, List<Dictionary<string, object?>> modules)
{
var moduleJsonFileContent = File.ReadAllText(modulePath);
using var moduleDoc = JsonDocument.Parse(moduleJsonFileContent, new JsonDocumentOptions
{
AllowTrailingCommas = true
});
if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports))
if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports))
{
return;
}
foreach (var import in imports.EnumerateObject())
{
var importPath = GetModuleFilePath(modulePath, import);
if (!importPath.IsNullOrEmpty())
{
ExtractModuleImportsInfo(importPath, modules);
continue;
}
foreach (var import in imports.EnumerateObject())
modules.Add(new Dictionary<string, object?>
{
modules.Add(new Dictionary<string, object?>
{
{ ActivityPropertyNames.ModuleName, import.Name },
{ ActivityPropertyNames.ModuleVersion, TelemetryJsonExtensions.GetStringOrNull(import.Value, "version") },
{ ActivityPropertyNames.ModuleInstallationTime, TelemetryJsonExtensions.GetDateTimeOffsetOrNull(import.Value, "creationTime") }
});
}
{ ActivityPropertyNames.ModuleName, import.Name },
{ ActivityPropertyNames.ModuleVersion, TelemetryJsonExtensions.GetStringOrNull(import.Value, "version") },
{ ActivityPropertyNames.ModuleInstallationTime, TelemetryJsonExtensions.GetDateTimeOffsetOrNull(import.Value, "creationTime") }
});
}
context.Current[ActivityPropertyNames.InstalledModules] = modules;
}
private static string? GetModuleFilePath(string solutionPath, JsonProperty module)

26
framework/src/Volo.Abp.Core/Volo/Abp/Internal/Telemetry/TelemetryService.cs

@ -67,7 +67,7 @@ public class TelemetryService : ITelemetryService, IScopedDependency
private Task AddActivityAsync(ActivityContext context)
{
_ = Task.Run(async () =>
var telemetryTask = Task.Run(async () =>
{
using var scope = _serviceScopeFactory.CreateScope();
@ -81,6 +81,30 @@ public class TelemetryService : ITelemetryService, IScopedDependency
telemetryActivitySender);
});
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
{
try
{
telemetryTask.Wait(TimeSpan.FromSeconds(10));
}
catch
{
// ignored
}
};
Console.CancelKeyPress += (_, _) =>
{
try
{
telemetryTask.Wait(TimeSpan.FromSeconds(10));
}
catch
{
// ignored
}
};
return Task.CompletedTask;
}

39
framework/src/Volo.Abp.Core/Volo/Abp/KeyedObjectHelper.cs

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Volo.Abp;
public static class KeyedObjectHelper
{
public static string EncodeCompositeKey(params object?[] keys)
{
var raw = keys.JoinAsString("||");
var bytes = Encoding.UTF8.GetBytes(raw);
var base64 = Convert.ToBase64String(bytes);
var base64Url = base64
.Replace("+", "-")
.Replace("/", "_")
.TrimEnd('=');
return base64Url;
}
public static string DecodeCompositeKey(string encoded)
{
var base64 = encoded
.Replace("-", "+")
.Replace("_", "/");
switch (encoded.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
var bytes = Convert.FromBase64String(base64);
var raw = Encoding.UTF8.GetString(bytes);
return raw;
}
}

45
framework/src/Volo.Abp.Core/Volo/Abp/Reflection/ReflectionHelper.cs

@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Volo.Abp.Reflection;
@ -229,4 +231,47 @@ public static class ReflectionHelper
return publicConstants.ToArray();
}
/// <summary>
/// Checks whether the property is nullable, including nullable reference types (NRT).
/// </summary>
/// <param name="propertyInfo">Property info to check</param>
public static bool IsNullable(PropertyInfo propertyInfo)
{
if (TypeHelper.IsNullable(propertyInfo.PropertyType))
{
return true;
}
#if NET6_0_OR_GREATER
var nullabilityInfoContext = new NullabilityInfoContext();
var nullabilityInfo = nullabilityInfoContext.Create(propertyInfo);
return nullabilityInfo.ReadState == NullabilityState.Nullable;
#else
var attr = propertyInfo.GetCustomAttributes().FirstOrDefault(a => a.GetType().FullName == "System.Runtime.CompilerServices.NullableAttribute");
if (attr != null)
{
var getter = NullableGetterCache.GetOrAdd(attr.GetType(), CreateNullableAccessor);
return getter(attr)?[0] == 2;
}
return false;
#endif
}
private static readonly ConcurrentDictionary<Type, Func<object, byte[]?>> NullableGetterCache = new ();
private static Func<object, byte[]?> CreateNullableAccessor(Type attrType)
{
var param = Expression.Parameter(typeof(object), "attr");
var casted = Expression.Convert(param, attrType);
var flagsField = attrType.GetField("NullableFlags");
if (flagsField == null)
{
return _ => null;
}
var access = Expression.Field(casted, flagsField);
return Expression.Lambda<Func<object, byte[]?>>(access, param).Compile();
}
}

2
framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs

@ -86,7 +86,7 @@ public static class TypeHelper
{
return default;
}
if (IsPrimitiveExtended(typeof(TProperty), includeEnums: true))
{
var conversionType = typeof(TProperty);

5
framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/EntityDto.cs

@ -23,4 +23,9 @@ public abstract class EntityDto<TKey> : EntityDto, IEntityDto<TKey>
{
return $"[DTO: {GetType().Name}] Id = {Id}";
}
public virtual string? GetObjectKey()
{
return Id?.ToString();
}
}

5
framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/ExtensibleEntityDto.cs

@ -27,6 +27,11 @@ public abstract class ExtensibleEntityDto<TKey> : ExtensibleObject, IEntityDto<T
{
return $"[DTO: {GetType().Name}] Id = {Id}";
}
public virtual string? GetObjectKey()
{
return Id?.ToString();
}
}
[Serializable]

2
framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/IEntityDto.cs

@ -5,7 +5,7 @@ public interface IEntityDto
}
public interface IEntityDto<TKey> : IEntityDto
public interface IEntityDto<TKey> : IEntityDto, IKeyedObject
{
TKey Id { get; set; }
}

13
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Entity.cs

@ -18,6 +18,17 @@ public abstract class Entity : IEntity
return $"[ENTITY: {GetType().Name}] Keys = {GetKeys().JoinAsString(", ")}";
}
public virtual string? GetObjectKey()
{
var keys = GetKeys();
return keys.Length switch
{
0 => null,
1 when keys[0] != null => keys[0]?.ToString(),
_ => KeyedObjectHelper.EncodeCompositeKey(keys)
};
}
public abstract object?[] GetKeys();
public bool EntityEquals(IEntity other)
@ -45,7 +56,7 @@ public abstract class Entity<TKey> : Entity, IEntity<TKey>
public override object?[] GetKeys()
{
return new object?[] { Id };
return [Id];
}
/// <inheritdoc/>

2
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/IEntity.cs

@ -4,7 +4,7 @@
/// Defines an entity. It's primary key may not be "Id" or it may have a composite primary key.
/// Use <see cref="IEntity{TKey}"/> where possible for better integration to repositories and other structures in the framework.
/// </summary>
public interface IEntity
public interface IEntity : IKeyedObject
{
/// <summary>
/// Returns an array of ordered keys for this entity.

2
framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo.Abp.EntityFrameworkCore.Oracle.csproj

@ -22,8 +22,6 @@
<ItemGroup>
<PackageReference Include="Oracle.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore" VersionOverride="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>
</Project>

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

@ -204,6 +204,7 @@ public abstract class AbpDbContext<TDbContext> : DbContext, IAbpEfCoreDbContext,
case "Npgsql.EntityFrameworkCore.PostgreSQL":
return EfCoreDatabaseProvider.PostgreSql;
case "Pomelo.EntityFrameworkCore.MySql":
case "MySql.Data.MySqlClient":
return EfCoreDatabaseProvider.MySql;
case "Oracle.EntityFrameworkCore":
case "Devart.Data.Oracle.Entity.EFCore":

49
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptions.cs

@ -26,14 +26,10 @@ public class AbpDbContextOptions
internal Dictionary<Type, List<object>> ConventionActions { get; }
internal Action<DbContext, ModelBuilder>? DefaultOnModelCreatingAction { get; set; }
internal Action<DbContext, DbContextOptionsBuilder>? DefaultOnConfiguringAction { get; set; }
internal Action<DbContext, DbContextOptionsBuilder>? DefaultOnConfiguringAction { get; set; }
internal Dictionary<Type, List<object>> OnModelCreatingActions { get; }
internal Dictionary<Type, List<object>> OnConfiguringActions { get; }
internal Action<DbContext, DbContextOptionsBuilder>? DefaultOnConfiguringAction { get; set; }
internal Dictionary<Type, List<object>> OnConfiguringActions { get; }
@ -108,27 +104,6 @@ public class AbpDbContextOptions
}
}
public void ConfigureDefaultOnConfiguring([NotNull] Action<DbContext, DbContextOptionsBuilder> action, bool overrideExisting = false)
{
Check.NotNull(action, nameof(action));
if (overrideExisting)
{
DefaultOnConfiguringAction = action;
}
else
{
DefaultOnConfiguringAction += action;
}
}
public void ConfigureDefaultOnConfiguring([NotNull] Action<DbContext, DbContextOptionsBuilder> action)
{
Check.NotNull(action, nameof(action));
DefaultOnConfiguringAction = action;
}
public void ConfigureOnModelCreating<TDbContext>([NotNull] Action<TDbContext, ModelBuilder> action)
where TDbContext : AbpDbContext<TDbContext>
{
@ -146,23 +121,19 @@ public class AbpDbContextOptions
actions.Add(action);
}
public void ConfigureOnConfiguring<TDbContext>([NotNull] Action<TDbContext, DbContextOptionsBuilder> action)
where TDbContext : AbpDbContext<TDbContext>
public void ConfigureDefaultOnConfiguring([NotNull] Action<DbContext, DbContextOptionsBuilder> action, bool overrideExisting = false)
{
Check.NotNull(action, nameof(action));
var actions = OnConfiguringActions.GetOrDefault(typeof(TDbContext));
if (actions == null)
if (overrideExisting)
{
OnConfiguringActions[typeof(TDbContext)] = new List<object>
{
new Action<DbContext, DbContextOptionsBuilder>((dbContext, builder) => action((TDbContext)dbContext, builder))
};
return;
DefaultOnConfiguringAction = action;
}
else
{
DefaultOnConfiguringAction += action;
}
actions.Add(action);
}
public void ConfigureOnConfiguring<TDbContext>([NotNull] Action<TDbContext, DbContextOptionsBuilder> action)

4
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs

@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Volo.Abp.Http.ProxyScripting.Configuration;
using Volo.Abp.Reflection;
namespace Volo.Abp.Http.Modeling;
@ -29,6 +30,8 @@ public class PropertyApiDescriptionModel
public string? Regex { get; set; }
public bool IsNullable { get; set; }
public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo)
{
var customAttributes = propertyInfo.GetCustomAttributes(true);
@ -39,6 +42,7 @@ public class PropertyApiDescriptionModel
Type = ApiTypeNameHelper.GetTypeName(propertyInfo.PropertyType),
TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(propertyInfo.PropertyType),
IsRequired = customAttributes.OfType<RequiredAttribute>().Any() || propertyInfo.GetCustomAttributesData().Any(attr => attr.AttributeType.Name == "RequiredMemberAttribute"),
IsNullable = ReflectionHelper.IsNullable(propertyInfo),
Minimum = customAttributes.OfType<RangeAttribute>().Select(x => x.Minimum).FirstOrDefault()?.ToString(),
Maximum = customAttributes.OfType<RangeAttribute>().Select(x => x.Maximum).FirstOrDefault()?.ToString(),
MinLength = customAttributes.OfType<MinLengthAttribute>().FirstOrDefault()?.Length ?? customAttributes.OfType<StringLengthAttribute>().FirstOrDefault()?.MinimumLength,

30
framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs

@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
@ -8,25 +10,31 @@ namespace Volo.Abp.MultiTenancy;
public class TenantResolver : ITenantResolver, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
private readonly AbpTenantResolveOptions _options;
public ILogger<TenantResolver> Logger { get; set; }
protected IServiceProvider ServiceProvider { get; }
protected AbpTenantResolveOptions Options { get; }
public TenantResolver(IOptions<AbpTenantResolveOptions> options, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_options = options.Value;
Logger = NullLogger<TenantResolver>.Instance;
ServiceProvider = serviceProvider;
Options = options.Value;
}
public virtual async Task<TenantResolveResult> ResolveTenantIdOrNameAsync()
{
var result = new TenantResolveResult();
using (var serviceScope = _serviceProvider.CreateScope())
Logger.LogDebug("Starting resolving tenant...");
using (var serviceScope = ServiceProvider.CreateScope())
{
var context = new TenantResolveContext(serviceScope.ServiceProvider);
foreach (var tenantResolver in _options.TenantResolvers)
foreach (var tenantResolver in Options.TenantResolvers)
{
Logger.LogDebug("Trying to resolve tenant through '{TenantResolverName}'...", tenantResolver.Name);
await tenantResolver.ResolveAsync(context);
result.AppliedResolvers.Add(tenantResolver.Name);
@ -34,15 +42,21 @@ public class TenantResolver : ITenantResolver, ITransientDependency
if (context.HasResolvedTenantOrHost())
{
result.TenantIdOrName = context.TenantIdOrName;
Logger.LogDebug("Tenant resolved by '{TenantResolverName}' as '{TenantIdOrName}'.", tenantResolver.Name, result.TenantIdOrName ?? "Host");
break;
}
}
}
if (result.TenantIdOrName.IsNullOrEmpty() && !string.IsNullOrWhiteSpace(_options.FallbackTenant))
if (result.TenantIdOrName.IsNullOrEmpty() && !string.IsNullOrWhiteSpace(Options.FallbackTenant))
{
result.TenantIdOrName = _options.FallbackTenant;
result.TenantIdOrName = Options.FallbackTenant;
result.AppliedResolvers.Add(TenantResolverNames.FallbackTenant);
Logger.LogDebug("No tenant resolved. Using fallback tenant as '{FallbackTenant}'.", result.TenantIdOrName);
}
else if (result.TenantIdOrName.IsNullOrEmpty())
{
Logger.LogDebug("No tenant resolved.");
}
return result;

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json

@ -61,6 +61,7 @@
"ProfilePicture": "الصوره الشخصيه",
"Theme": "سمة",
"NotAssigned": "غيرمعتمد",
"EntityActionsDisabledTooltip": "ليس لديك إذن لتنفيذ أي إجراء."
"EntityActionsDisabledTooltip": "ليس لديك إذن لتنفيذ أي إجراء.",
"ResourcePermissions": "أذونات"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profilový obrázek",
"Theme": "Téma",
"NotAssigned": "Nepřiřazena",
"EntityActionsDisabledTooltip": "Nemáte oprávnění provést žádnou akci."
"EntityActionsDisabledTooltip": "Nemáte oprávnění provést žádnou akci.",
"ResourcePermissions": "Oprávnění"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profilbild",
"Theme": "Thema",
"NotAssigned": "Nicht zugeordnet",
"EntityActionsDisabledTooltip": "Sie haben keine Berechtigung, Aktionen auszuführen."
"EntityActionsDisabledTooltip": "Sie haben keine Berechtigung, Aktionen auszuführen.",
"ResourcePermissions": "Berechtigungen"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json

@ -55,6 +55,7 @@
"OthersGroup": "άλλος",
"Today": "Σήμερα",
"Apply": "Ισχύουν",
"EntityActionsDisabledTooltip": "Δεν έχετε δικαίωμα να εκτελέσετε καμία ενέργεια."
"EntityActionsDisabledTooltip": "Δεν έχετε δικαίωμα να εκτελέσετε καμία ενέργεια.",
"ResourcePermissions": "Δικαιώματα",
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json

@ -56,6 +56,7 @@
"NotAssigned": "Not Assigned",
"Today": "Today",
"Apply": "Apply",
"EntityActionsDisabledTooltip": "You do not have permission to perform any action."
"EntityActionsDisabledTooltip": "You do not have permission to perform any action.",
"ResourcePermissions": "Permissions"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profile picture",
"Theme": "Theme",
"NotAssigned": "Not Assigned",
"EntityActionsDisabledTooltip": "You do not have permission to perform any action."
"EntityActionsDisabledTooltip": "You do not have permission to perform any action.",
"ResourcePermissions": "Permissions"
}
}

5
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json

@ -61,6 +61,7 @@
"ProfilePicture": "Foto de perfil",
"Theme": "Tema",
"NotAssigned": "No asignado",
"EntityActionsDisabledTooltip": "No tienes permisos para realizar ninguna acción."
"EntityActionsDisabledTooltip": "No tienes permisos para realizar ninguna acción.",
"ResourcePermissions": "Permisos"
}
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json

@ -55,6 +55,7 @@
"OthersGroup": "دیگر",
"Today": "امروز",
"Apply": "درخواست دادن",
"EntityActionsDisabledTooltip": "شما دسترسی به انجام هر گونه عملیات ندارید."
"EntityActionsDisabledTooltip": "شما دسترسی به انجام هر گونه عملیات ندارید.",
"ResourcePermissions": "مجوزها"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profiilikuva",
"Theme": "Teema",
"NotAssigned": "Ei määritetty",
"EntityActionsDisabledTooltip": "Sinulla ei ole oikeutta suorittaa mitään toimintoa."
"EntityActionsDisabledTooltip": "Sinulla ei ole oikeutta suorittaa mitään toimintoa.",
"ResourcePermissions": "Käyttöoikeudet"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json

@ -61,6 +61,7 @@
"ProfilePicture": "Image de profil",
"Theme": "Thème",
"NotAssigned": "Non attribué",
"EntityActionsDisabledTooltip": "Vous n&#39;avez pas les permissions pour effectuer une action."
"EntityActionsDisabledTooltip": "Vous n&#39;avez pas les permissions pour effectuer une action.",
"ResourcePermissions": "Autorisations"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json

@ -61,6 +61,7 @@
"ProfilePicture": "प्रोफ़ाइल फोटो",
"Theme": "विषय",
"NotAssigned": "सौंपा नहीं गया है",
"EntityActionsDisabledTooltip": "आपके पास कोई कार्रवाई नहीं है जो करने के लिए है।"
"EntityActionsDisabledTooltip": "आपके पास कोई कार्रवाई नहीं है जो करने के लिए है।",
"ResourcePermissions": "अनुमतियाँ"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profilna slika",
"Theme": "Tema",
"NotAssigned": "Nije dodijeljeno",
"EntityActionsDisabledTooltip": "Nemate dozvolu za izvođenje bilo kakve akcije."
"EntityActionsDisabledTooltip": "Nemate dozvolu za izvođenje bilo kakve akcije.",
"ResourcePermissions": "Dozvole"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profil kép",
"Theme": "Téma",
"NotAssigned": "Nem kijelölt",
"EntityActionsDisabledTooltip": "Nincs jogosultsága bármely művelethez."
"EntityActionsDisabledTooltip": "Nincs jogosultsága bármely művelethez.",
"ResourcePermissions": "Engedélyek"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json

@ -61,6 +61,7 @@
"ProfilePicture": "Forsíðumynd",
"Theme": "Þema",
"NotAssigned": "Ekki skráður",
"EntityActionsDisabledTooltip": "Þú hefur ekki aðgang að þessum aðgerðum."
"EntityActionsDisabledTooltip": "Þú hefur ekki aðgang að þessum aðgerðum.",
"ResourcePermissions": "Aðgangsheimildir"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json

@ -61,6 +61,7 @@
"ProfilePicture": "Immagine del profilo",
"Theme": "Tema",
"NotAssigned": "Non assegnato",
"EntityActionsDisabledTooltip": "Non hai i permessi per eseguire alcuna azione."
"EntityActionsDisabledTooltip": "Non hai i permessi per eseguire alcuna azione.",
"ResourcePermissions": "Autorizzazioni"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json

@ -61,6 +61,7 @@
"ProfilePicture": "Profielfoto",
"Theme": "Thema",
"NotAssigned": "Niet toegekend",
"EntityActionsDisabledTooltip": "U hebt geen toegang tot deze acties."
"EntityActionsDisabledTooltip": "U hebt geen toegang tot deze acties.",
"ResourcePermissions": "Machtigingen"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json

@ -61,6 +61,7 @@
"ProfilePicture": "Zdjęcie profilowe",
"Theme": "Temat",
"NotAssigned": "Nie przypisano",
"EntityActionsDisabledTooltip": "Nie masz uprawnień do wykonania żadnej akcji."
"EntityActionsDisabledTooltip": "Nie masz uprawnień do wykonania żadnej akcji.",
"ResourcePermissions": "Uprawnienia"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json

@ -61,6 +61,7 @@
"ProfilePicture": "Foto do perfil",
"Theme": "Tema",
"NotAssigned": "Não atribuído",
"EntityActionsDisabledTooltip": "Você não tem permissão para executar qualquer ação."
"EntityActionsDisabledTooltip": "Você não tem permissão para executar qualquer ação.",
"ResourcePermissions": "Permissões"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json

@ -61,6 +61,7 @@
"ProfilePicture": "Poză de profil",
"Theme": "Temă",
"NotAssigned": "Nealocat",
"EntityActionsDisabledTooltip": "Nu aveți permisiune să efectuați nicio acțiune."
"EntityActionsDisabledTooltip": "Nu aveți permisiune să efectuați nicio acțiune.",
"ResourcePermissions": "Permisiuni"
}
}

3
framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json

@ -61,6 +61,7 @@
"ProfilePicture": "Изображение профиля",
"Theme": "Тема",
"NotAssigned": "Не назначен",
"EntityActionsDisabledTooltip": "У вас нет прав на выполнение каких-либо действий."
"EntityActionsDisabledTooltip": "У вас нет прав на выполнение каких-либо действий.",
"ResourcePermissions": "Разрешения"
}
}

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

Loading…
Cancel
Save