Browse Source

Merge branch 'rel-10.0' into HTML-encode-TagHelper

pull/24488/head
maliming 1 month ago
parent
commit
b7dfc7840e
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 10
      .github/workflows/auto-pr.yml
  2. 3
      Directory.Packages.props
  3. 4
      docs/en/docs-nav.json
  4. 119
      docs/en/framework/ui/common/leptonx-css-variables.md
  5. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png
  6. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png
  7. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png
  8. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png
  9. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png
  10. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png
  11. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png
  12. BIN
      docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png
  13. 2
      docs/en/tutorials/book-store-with-abp-suite/part-02.md
  14. 3
      framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs
  15. 9
      framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs
  16. 5
      framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs
  17. 3
      framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs
  18. 3
      framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs
  19. 25
      framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs
  20. 13
      framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs
  21. 7
      framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs
  22. 42
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs
  23. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs
  24. 14
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs
  25. 9
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs
  26. 90
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs
  27. 3
      framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs
  28. 11
      framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/IStaticDefinitionCache.cs
  29. 30
      framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/StaticDefinitionCache.cs
  30. 242
      framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs
  31. 1
      framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj
  32. 18
      framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs
  33. 17
      framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs
  34. 7
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs
  35. 9
      framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionChangedEvent.cs
  36. 112
      framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs
  37. 7
      framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs
  38. 9
      framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionChangedEvent.cs
  39. 34
      framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionStore.cs
  40. 9
      framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionChangedEvent.cs
  41. 40
      framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionStore.cs
  42. 122
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/StaticDefinitions/StaticDefinitionCache_Tests.cs
  43. 179
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs
  44. 4
      modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/CmsKitMongoDbTestModule.cs
  45. 1
      modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/Volo.CmsKit.MongoDB.Tests.csproj
  46. 131
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs
  47. 149
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs
  48. 36
      modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureDefinitionChangedEventHandler.cs
  49. 14
      modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs
  50. 9
      modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs
  51. 132
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs
  52. 151
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs
  53. 36
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/StaticPermissionDefinitionChangedEventHandler.cs
  54. 14
      modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs
  55. 15
      modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs
  56. 120
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/AbpSettingManagementDomainModule.cs
  57. 150
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs
  58. 32
      modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/StaticSettingDefinitionChangedEventHandler.cs
  59. 13
      modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs
  60. 32
      text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs

10
.github/workflows/auto-pr.yml

@ -1,4 +1,4 @@
name: Merge branch dev with rel-10.0
name: Merge branch rel-10.1 with rel-10.0
on:
push:
branches:
@ -7,7 +7,7 @@ permissions:
contents: read
jobs:
merge-dev-with-rel-10-0:
merge-rel-10-1-with-rel-10-0:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
ref: dev
ref: rel-10.1
- name: Reset promotion branch
run: |
git fetch origin rel-10.0:rel-10.0
@ -24,8 +24,8 @@ jobs:
uses: peter-evans/create-pull-request@v3
with:
branch: auto-merge/rel-10-0/${{github.run_number}}
title: Merge branch dev with rel-10.0
body: This PR generated automatically to merge dev with rel-10.0. Please review the changed files before merging to prevent any errors that may occur.
title: Merge branch rel-10.1 with rel-10.0
body: This PR generated automatically to merge rel-10.1 with rel-10.0. Please review the changed files before merging to prevent any errors that may occur.
reviewers: maliming
draft: true
token: ${{ github.token }}

3
Directory.Packages.props

@ -7,7 +7,6 @@
<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="Autofac" Version="8.4.0" />
<PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
@ -141,7 +140,7 @@
<PackageVersion Include="Polly" Version="8.6.3" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.0-preview" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.0-rc" />
<PackageVersion Include="Quartz" Version="3.15.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.15.0" />
<PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="3.15.0" />

4
docs/en/docs-nav.json

@ -1857,6 +1857,10 @@
"text": "Overriding the User Interface",
"path": "framework/architecture/modularity/extending/overriding-user-interface.md"
},
{
"text": "How to Override LeptonX CSS Variables",
"path": "framework/ui/common/leptonx-css-variables.md"
},
{
"text": "Utilities",
"items": [

119
docs/en/framework/ui/common/leptonx-css-variables.md

@ -0,0 +1,119 @@
# LeptonX CSS Variables Documentation
LeptonX uses CSS custom properties (variables) prefixed with `--lpx-*` to provide a flexible theming system. These variables control colors, spacing, shadows, and component-specific styles throughout the application.
## Brand & Semantic Colors
| Variable | Description |
|----------|-------------|
| `--lpx-brand` | Brand-specific accent color |
| `--lpx-brand-text` | Text color used on brand-colored backgrounds |
## Base Colors
| Variable | Description |
|----------|-------------|
| `--lpx-light` | Light shade for subtle backgrounds or text |
| `--lpx-dark` | Dark shade for contrasting elements |
## Layout & Surface Colors
| Variable | Description |
|----------|-------------|
| `--lpx-content-bg` | Main content area background color |
| `--lpx-content-text` | Default text color for content areas |
| `--lpx-card-bg` | Card component background color |
| `--lpx-card-title-text-color` | Card title text color |
| `--lpx-border-color` | Default border color for dividers and outlines |
| `--lpx-shadow` | Box shadow definition for elevated elements |
## Navigation
| Variable | Description |
|----------|-------------|
| `--lpx-navbar-color` | Navbar background color |
| `--lpx-navbar-text-color` | Navbar default text/icon color |
| `--lpx-navbar-active-text-color` | Navbar active/hover text color |
| `--lpx-navbar-active-bg-color` | Navbar active item background color |
## Utility
| Variable | Description |
|----------|-------------|
| `--lpx-radius` | Global border-radius value for rounded corners |
## Global Override
Applies to all themes and pages:
```css
:root {
/* Brand & Semantic */
--lpx-brand: #f72585;
/* Base Colors */
--lpx-light: #f5f7fb;
--lpx-dark: #0b0f19;
/* Layout & Surface */
--lpx-content-bg: #101018;
--lpx-content-text: #cfd6e4;
--lpx-card-bg: #151a2b;
--lpx-card-title-text-color: #ffffff;
--lpx-border-color: #242836;
--lpx-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
/* Navigation */
--lpx-navbar-color: #0d1020;
--lpx-navbar-text-color: #aab2c8;
--lpx-navbar-active-text-color: #ffffff;
--lpx-navbar-active-bg-color: rgba(247, 37, 133, 0.15);
/* Utility */
--lpx-radius: 10px;
}
```
## Theme-Scoped Override
Applies only when a specific theme class is active (e.g., `.lpx-theme-dark` on `<html>` or `<body>`):
```css
:root .lpx-theme-dark {
/* Brand & Semantic */
--lpx-brand: #4dd0e1;
/* Base Colors */
--lpx-light: #e0f7fa;
--lpx-dark: #020617;
/* Layout & Surface */
--lpx-content-bg: #0b1118;
--lpx-content-text: #c7d0e0;
--lpx-card-bg: #111a24;
--lpx-card-title-text-color: #e6f1ff;
--lpx-border-color: #1e2a3a;
--lpx-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
/* Navigation */
--lpx-navbar-color: #0f1a22;
--lpx-navbar-text-color: #9fb3c8;
--lpx-navbar-active-text-color: #ffffff;
--lpx-navbar-active-bg-color: rgba(77, 208, 225, 0.18);
/* Utility */
--lpx-radius: 12px;
}
```
## Component/Page-Specific Override
For targeted customizations that should only affect a specific section:
```css
.my-custom-page {
--lpx-brand: #e91e63;
--lpx-card-bg: #1a1a2e;
}
```

BIN
docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 29 KiB

2
docs/en/tutorials/book-store-with-abp-suite/part-02.md

@ -96,7 +96,7 @@ Here is the all details for the `Book` entity:
* `Name` is **required**, it's a **string** property and maximum length is **128**.
* `Type` is an **enum** and the enum file path is *\Acme.BookStore.Domain.Shared\Books\BookType.cs*.
* `PublishDate` is a **DateTime** property and **not nullable**.
* `Price` is a **float** property and **required**.
* `Price` is a **float** property.
You can leave the other configurations as default.

3
framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.DependencyInjection;
@ -16,7 +17,7 @@ public class MauiCurrentApplicationConfigurationCacheResetService :
_mauiBlazorCachedApplicationConfigurationClient = mauiBlazorCachedApplicationConfigurationClient;
}
public async Task ResetAsync()
public async Task ResetAsync(Guid? userId = null)
{
await _mauiBlazorCachedApplicationConfigurationClient.InitializeAsync();
}

9
framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.DependencyInjection;
@ -19,10 +20,8 @@ public class BlazorServerCurrentApplicationConfigurationCacheResetService :
_localEventBus = localEventBus;
}
public async Task ResetAsync()
public async Task ResetAsync(Guid? userId = null)
{
await _localEventBus.PublishAsync(
new CurrentApplicationConfigurationCacheResetEventData()
);
await _localEventBus.PublishAsync(new CurrentApplicationConfigurationCacheResetEventData(userId));
}
}

5
framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs

@ -1,8 +1,9 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.AspNetCore.Components.Web.Configuration;
public interface ICurrentApplicationConfigurationCacheResetService
{
Task ResetAsync();
Task ResetAsync(Guid? userId = null);
}

3
framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
@ -5,7 +6,7 @@ namespace Volo.Abp.AspNetCore.Components.Web.Configuration;
public class NullCurrentApplicationConfigurationCacheResetService : ICurrentApplicationConfigurationCacheResetService, ISingletonDependency
{
public Task ResetAsync()
public Task ResetAsync(Guid? userId = null)
{
return Task.CompletedTask;
}

3
framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.DependencyInjection;
@ -16,7 +17,7 @@ public class BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService :
_webAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient;
}
public async Task ResetAsync()
public async Task ResetAsync(Guid? userId = null)
{
await _webAssemblyCachedApplicationConfigurationClient.InitializeAsync();
}

25
framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs

@ -1,13 +1,26 @@
using System.Globalization;
using Volo.Abp.Users;
using System;
using System.Globalization;
using System.Threading.Tasks;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Client;
public static class MvcCachedApplicationConfigurationClientHelper
public class MvcCachedApplicationConfigurationClientHelper : ITransientDependency
{
public static string CreateCacheKey(ICurrentUser currentUser)
protected IDistributedCache<MvcCachedApplicationVersionCacheItem> ApplicationVersionCache { get; }
public MvcCachedApplicationConfigurationClientHelper(IDistributedCache<MvcCachedApplicationVersionCacheItem> applicationVersionCache)
{
ApplicationVersionCache = applicationVersionCache;
}
public virtual async Task<string> CreateCacheKeyAsync(Guid? userId)
{
var userKey = currentUser.Id?.ToString("N") ?? "Anonymous";
return $"ApplicationConfiguration_{userKey}_{CultureInfo.CurrentUICulture.Name}";
var appVersion = await ApplicationVersionCache.GetOrAddAsync(MvcCachedApplicationVersionCacheItem.CacheKey,
() => Task.FromResult(new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N")))) ??
new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N"));
var userKey = userId?.ToString("N") ?? "Anonymous";
return $"ApplicationConfiguration_{appVersion.Version}_{userKey}_{CultureInfo.CurrentUICulture.Name}";
}
}

13
framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs

@ -0,0 +1,13 @@
namespace Volo.Abp.AspNetCore.Mvc.Client;
public class MvcCachedApplicationVersionCacheItem
{
public const string CacheKey = "Mvc_Application_Version";
public string Version { get; set; }
public MvcCachedApplicationVersionCacheItem(string version)
{
Version = version;
}
}

7
framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs

@ -20,6 +20,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
protected IHttpClientFactory HttpClientFactory { get; }
protected IRemoteServiceHttpClientAuthenticator HttpClientAuthenticator { get; }
protected IDistributedCache<ApplicationConfigurationDto> ApplicationConfigurationDtoCache { get; }
protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; }
protected ICurrentUser CurrentUser { get; }
public RemoteDynamicClaimsPrincipalContributorCache(
@ -28,7 +29,8 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
IOptions<AbpClaimsPrincipalFactoryOptions> abpClaimsPrincipalFactoryOptions,
IRemoteServiceHttpClientAuthenticator httpClientAuthenticator,
IDistributedCache<ApplicationConfigurationDto> applicationConfigurationDtoCache,
ICurrentUser currentUser)
ICurrentUser currentUser,
MvcCachedApplicationConfigurationClientHelper cacheHelper)
: base(abpClaimsPrincipalFactoryOptions)
{
Cache = cache;
@ -36,6 +38,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
HttpClientAuthenticator = httpClientAuthenticator;
ApplicationConfigurationDtoCache = applicationConfigurationDtoCache;
CurrentUser = currentUser;
CacheHelper = cacheHelper;
}
protected async override Task<AbpDynamicClaimCacheItem?> GetCacheAsync(Guid userId, Guid? tenantId = null)
@ -56,7 +59,7 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP
catch (Exception e)
{
Logger.LogWarning(e, $"Failed to refresh remote claims for user: {userId}");
await ApplicationConfigurationDtoCache.RemoveAsync(MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser));
await ApplicationConfigurationDtoCache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id));
throw;
}
}

42
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs

@ -1,3 +1,4 @@
using System;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
@ -13,14 +14,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Client;
public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigurationClient, ITransientDependency
{
private const string ApplicationConfigurationDtoCacheKey = "ApplicationConfigurationDto_CacheKey";
protected IHttpContextAccessor HttpContextAccessor { get; }
protected AbpApplicationConfigurationClientProxy ApplicationConfigurationAppService { get; }
protected AbpApplicationLocalizationClientProxy ApplicationLocalizationClientProxy { get; }
protected ICurrentUser CurrentUser { get; }
protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; }
protected IDistributedCache<ApplicationConfigurationDto> Cache { get; }
protected AbpAspNetCoreMvcClientCacheOptions Options { get; }
public MvcCachedApplicationConfigurationClient(
MvcCachedApplicationConfigurationClientHelper cacheHelper,
IDistributedCache<ApplicationConfigurationDto> cache,
AbpApplicationConfigurationClientProxy applicationConfigurationAppService,
ICurrentUser currentUser,
@ -33,13 +38,27 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
HttpContextAccessor = httpContextAccessor;
ApplicationLocalizationClientProxy = applicationLocalizationClientProxy;
Options = options.Value;
CacheHelper = cacheHelper;
Cache = cache;
}
public async Task<ApplicationConfigurationDto> GetAsync()
public virtual async Task<ApplicationConfigurationDto> GetAsync()
{
var cacheKey = CreateCacheKey();
string? cacheKey = null;
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key)
{
cacheKey = key;
}
if (cacheKey.IsNullOrWhiteSpace())
{
cacheKey = await CreateCacheKeyAsync();
if (httpContext != null)
{
httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey;
}
}
if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
{
@ -86,8 +105,21 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
public ApplicationConfigurationDto Get()
{
var cacheKey = CreateCacheKey();
string? cacheKey = null;
var httpContext = HttpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key)
{
cacheKey = key;
}
if (cacheKey.IsNullOrWhiteSpace())
{
cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync);
if (httpContext != null)
{
httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey;
}
}
if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration)
{
@ -97,8 +129,8 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu
return AsyncHelper.RunSync(GetAsync);
}
protected virtual string CreateCacheKey()
protected virtual async Task<string> CreateCacheKeyAsync()
{
return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser);
return await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id);
}
}

27
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs

@ -3,7 +3,6 @@ using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.Users;
namespace Volo.Abp.AspNetCore.Mvc.Client;
@ -11,23 +10,29 @@ public class MvcCurrentApplicationConfigurationCacheResetEventHandler :
ILocalEventHandler<CurrentApplicationConfigurationCacheResetEventData>,
ITransientDependency
{
protected ICurrentUser CurrentUser { get; }
protected IDistributedCache<ApplicationConfigurationDto> Cache { get; }
protected IDistributedCache<MvcCachedApplicationVersionCacheItem> ApplicationVersionCache { get; }
protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; }
public MvcCurrentApplicationConfigurationCacheResetEventHandler(ICurrentUser currentUser,
IDistributedCache<ApplicationConfigurationDto> cache)
public MvcCurrentApplicationConfigurationCacheResetEventHandler(
IDistributedCache<ApplicationConfigurationDto> cache,
IDistributedCache<MvcCachedApplicationVersionCacheItem> applicationVersionCache,
MvcCachedApplicationConfigurationClientHelper cacheHelper)
{
CurrentUser = currentUser;
Cache = cache;
ApplicationVersionCache = applicationVersionCache;
CacheHelper = cacheHelper;
}
public virtual async Task HandleEventAsync(CurrentApplicationConfigurationCacheResetEventData eventData)
{
await Cache.RemoveAsync(CreateCacheKey());
}
protected virtual string CreateCacheKey()
{
return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser);
if (eventData.UserId.HasValue)
{
await Cache.RemoveAsync(await CacheHelper.CreateCacheKeyAsync(eventData.UserId));
}
else
{
await ApplicationVersionCache.RemoveAsync(MvcCachedApplicationVersionCacheItem.CacheKey);
}
}
}

14
framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs

@ -1,9 +1,21 @@
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using System;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
/// <summary>
/// This event is used to invalidate current user's cached configuration.
/// </summary>
public class CurrentApplicationConfigurationCacheResetEventData
{
public Guid? UserId { get; set; }
public CurrentApplicationConfigurationCacheResetEventData()
{
}
public CurrentApplicationConfigurationCacheResetEventData(Guid? userId)
{
UserId = userId;
}
}

9
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs

@ -0,0 +1,9 @@
using System;
namespace Volo.Abp.Authorization.Permissions;
[Serializable]
public class StaticPermissionDefinitionChangedEvent
{
}

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

@ -6,44 +6,58 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.StaticDefinitions;
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, PermissionDefinition> PermissionDefinitions => _lazyPermissionDefinitions.Value;
private readonly Lazy<Dictionary<string, PermissionDefinition>> _lazyPermissionDefinitions;
protected IServiceProvider ServiceProvider { get; }
protected AbpPermissionOptions Options { get; }
private readonly IServiceProvider _serviceProvider;
protected IStaticDefinitionCache<PermissionGroupDefinition, Dictionary<string, PermissionGroupDefinition>> GroupCache { get; }
protected IStaticDefinitionCache<PermissionDefinition, Dictionary<string, PermissionDefinition>> DefinitionCache { get; }
public StaticPermissionDefinitionStore(
IServiceProvider serviceProvider,
IOptions<AbpPermissionOptions> options)
IOptions<AbpPermissionOptions> options,
IStaticDefinitionCache<PermissionGroupDefinition, Dictionary<string, PermissionGroupDefinition>> groupCache,
IStaticDefinitionCache<PermissionDefinition, Dictionary<string, PermissionDefinition>> definitionCache)
{
_serviceProvider = serviceProvider;
ServiceProvider = serviceProvider;
Options = options.Value;
GroupCache = groupCache;
DefinitionCache = definitionCache;
}
_lazyPermissionDefinitions = new Lazy<Dictionary<string, PermissionDefinition>>(
CreatePermissionDefinitions,
isThreadSafe: true
);
public async Task<PermissionDefinition?> GetOrNullAsync(string name)
{
var defs = await GetPermissionDefinitionsAsync();
return defs.GetOrDefault(name);
}
_lazyPermissionGroupDefinitions = new Lazy<Dictionary<string, PermissionGroupDefinition>>(
CreatePermissionGroupDefinitions,
isThreadSafe: true
);
public virtual async Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync()
{
var defs = await GetPermissionDefinitionsAsync();
return defs.Values.ToImmutableList();
}
public async Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
var groups = await GetPermissionGroupDefinitionsAsync();
return groups.Values.ToImmutableList();
}
protected virtual async Task<Dictionary<string, PermissionDefinition>> GetPermissionDefinitionsAsync()
{
return await DefinitionCache.GetOrCreateAsync(CreatePermissionDefinitionsAsync);
}
protected virtual Dictionary<string, PermissionDefinition> CreatePermissionDefinitions()
protected virtual async Task<Dictionary<string, PermissionDefinition>> CreatePermissionDefinitionsAsync()
{
var permissions = new Dictionary<string, PermissionDefinition>();
foreach (var groupDefinition in PermissionGroupDefinitions.Values)
var groups = await GetPermissionGroupDefinitionsAsync();
foreach (var groupDefinition in groups.Values)
{
foreach (var permission in groupDefinition.Permissions)
{
@ -71,9 +85,14 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
}
}
protected virtual Dictionary<string, PermissionGroupDefinition> CreatePermissionGroupDefinitions()
protected virtual async Task<Dictionary<string, PermissionGroupDefinition>> GetPermissionGroupDefinitionsAsync()
{
return await GroupCache.GetOrCreateAsync(CreatePermissionGroupDefinitionsAsync);
}
protected virtual Task<Dictionary<string, PermissionGroupDefinition>> CreatePermissionGroupDefinitionsAsync()
{
using (var scope = _serviceProvider.CreateScope())
using (var scope = ServiceProvider.CreateScope())
{
var context = new PermissionDefinitionContext(scope.ServiceProvider);
@ -99,29 +118,10 @@ public class StaticPermissionDefinitionStore : IStaticPermissionDefinitionStore,
context.CurrentProvider = provider;
provider.PostDefine(context);
}
context.CurrentProvider = null;
return context.Groups;
return Task.FromResult(context.Groups);
}
}
public Task<PermissionDefinition?> GetOrNullAsync(string name)
{
return Task.FromResult(PermissionDefinitions.GetOrDefault(name));
}
public virtual Task<IReadOnlyList<PermissionDefinition>> GetPermissionsAsync()
{
return Task.FromResult<IReadOnlyList<PermissionDefinition>>(
PermissionDefinitions.Values.ToImmutableList()
);
}
public Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
return Task.FromResult<IReadOnlyList<PermissionGroupDefinition>>(
PermissionGroupDefinitions.Values.ToImmutableList()
);
}
}
}

3
framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs

@ -5,6 +5,7 @@ using Volo.Abp.Logging;
using Volo.Abp.Modularity;
using Volo.Abp.Reflection;
using Volo.Abp.SimpleStateChecking;
using Volo.Abp.StaticDefinitions;
namespace Volo.Abp.Internal;
@ -42,7 +43,7 @@ internal static class InternalServiceCollectionExtensions
services.AddAssemblyOf<IAbpApplication>();
services.AddTransient(typeof(ISimpleStateCheckerManager<>), typeof(SimpleStateCheckerManager<>));
services.AddSingleton(typeof(IStaticDefinitionCache<,>), typeof(StaticDefinitionCache<,>));
services.Configure<AbpModuleLifecycleOptions>(options =>
{
options.Contributors.Add<OnPreApplicationInitializationModuleLifecycleContributor>();

11
framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/IStaticDefinitionCache.cs

@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp.StaticDefinitions;
public interface IStaticDefinitionCache<TKey, TValue>
{
Task<TValue> GetOrCreateAsync(Func<Task<TValue>> factory);
Task ClearAsync();
}

30
framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/StaticDefinitionCache.cs

@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.StaticDefinitions;
public class StaticDefinitionCache<TKey, TValue> : IStaticDefinitionCache<TKey, TValue>
{
private Lazy<Task<TValue>>? _lazy;
public virtual async Task<TValue> GetOrCreateAsync(Func<Task<TValue>> factory)
{
var lazy = _lazy;
if (lazy != null)
{
return await lazy.Value;
}
var newLazy = new Lazy<Task<TValue>>(factory, LazyThreadSafetyMode.ExecutionAndPublication);
lazy = Interlocked.CompareExchange(ref _lazy, newLazy, null) ?? newLazy;
return await lazy.Value;
}
public virtual Task ClearAsync()
{
Interlocked.Exchange(ref _lazy, null);
return Task.CompletedTask;
}
}

242
framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs

@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.Threading;
/// <summary>
/// Per-key asynchronous lock for coordinating concurrent flows.
/// </summary>
/// <remarks>
/// Based on the pattern described in https://stackoverflow.com/a/31194647.
/// Use within a <c>using</c> scope to ensure the lock is released via <c>IDisposable.Dispose()</c>.
/// </remarks>
public static class KeyedLock
{
private static readonly Dictionary<object, RefCounted<SemaphoreSlim>> SemaphoreSlims = new();
/// <summary>
/// Acquires an exclusive asynchronous lock for the specified <paramref name="key"/>.
/// This method waits until the lock becomes available.
/// </summary>
/// <param name="key">A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock.</param>
/// <returns>An <see cref="IDisposable"/> handle that must be disposed to release the lock.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <example>
/// <code>
/// var key = "my-critical-section";
/// using (await KeyedLock.LockAsync(key))
/// {
/// // protected work
/// }
/// </code>
/// </example>
public static async Task<IDisposable> LockAsync(object key)
{
Check.NotNull(key, nameof(key));
return await LockAsync(key, CancellationToken.None);
}
/// <summary>
/// Acquires an exclusive asynchronous lock for the specified <paramref name="key"/>, observing a <paramref name="cancellationToken"/>.
/// </summary>
/// <param name="key">A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock.</param>
/// <param name="cancellationToken">A token to cancel the wait for the lock.</param>
/// <returns>An <see cref="IDisposable"/> handle that must be disposed to release the lock.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="OperationCanceledException">Thrown if the wait is canceled via <paramref name="cancellationToken"/>.</exception>
/// <example>
/// <code>
/// var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
/// using (await KeyedLock.LockAsync("db-update", cts.Token))
/// {
/// // protected work
/// }
/// </code>
/// </example>
public static async Task<IDisposable> LockAsync(object key, CancellationToken cancellationToken)
{
Check.NotNull(key, nameof(key));
var semaphore = GetOrCreate(key);
try
{
await semaphore.WaitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
var toDispose = DecrementRefAndMaybeRemove(key);
toDispose?.Dispose();
throw;
}
return new Releaser(key);
}
/// <summary>
/// Attempts to acquire an exclusive lock for the specified <paramref name="key"/> without waiting.
/// </summary>
/// <param name="key">A non-null object that identifies the lock.</param>
/// <returns>
/// An <see cref="IDisposable"/> handle if the lock was immediately acquired; otherwise <see langword="null"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <example>
/// <code>
/// var handle = await KeyedLock.TryLockAsync("cache-key");
/// if (handle != null)
/// {
/// using (handle)
/// {
/// // protected work
/// }
/// }
/// </code>
/// </example>
public static async Task<IDisposable?> TryLockAsync(object key)
{
Check.NotNull(key, nameof(key));
return await TryLockAsync(key, default, CancellationToken.None);
}
/// <summary>
/// Attempts to acquire an exclusive lock for the specified <paramref name="key"/>, waiting up to <paramref name="timeout"/>.
/// </summary>
/// <param name="key">A non-null object that identifies the lock.</param>
/// <param name="timeout">Maximum time to wait for the lock. If set to <see cref="TimeSpan.Zero"/>, the method performs an immediate, non-blocking attempt.</param>
/// <param name="cancellationToken">A token to cancel the wait.</param>
/// <returns>
/// An <see cref="IDisposable"/> handle if the lock was acquired within the timeout; otherwise <see langword="null"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="OperationCanceledException">Thrown if the wait is canceled via <paramref name="cancellationToken"/>.</exception>
/// <example>
/// <code>
/// var handle = await KeyedLock.TryLockAsync("send-mail", TimeSpan.FromSeconds(1));
/// if (handle != null)
/// {
/// using (handle)
/// {
/// // protected work
/// }
/// }
/// else
/// {
/// // lock not acquired within timeout
/// }
/// </code>
/// </example>
public static async Task<IDisposable?> TryLockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken = default)
{
Check.NotNull(key, nameof(key));
var semaphore = GetOrCreate(key);
bool acquired;
try
{
if (timeout == default)
{
acquired = await semaphore.WaitAsync(0, cancellationToken);
}
else
{
acquired = await semaphore.WaitAsync(timeout, cancellationToken);
}
}
catch (OperationCanceledException)
{
var toDispose = DecrementRefAndMaybeRemove(key);
toDispose?.Dispose();
throw;
}
if (acquired)
{
return new Releaser(key);
}
var toDisposeOnFail = DecrementRefAndMaybeRemove(key);
toDisposeOnFail?.Dispose();
return null;
}
private static SemaphoreSlim GetOrCreate(object key)
{
RefCounted<SemaphoreSlim> item;
lock (SemaphoreSlims)
{
if (SemaphoreSlims.TryGetValue(key, out item!))
{
++item.RefCount;
}
else
{
item = new RefCounted<SemaphoreSlim>(new SemaphoreSlim(1, 1));
SemaphoreSlims[key] = item;
}
}
return item.Value;
}
private sealed class RefCounted<T>(T value)
{
public int RefCount { get; set; } = 1;
public T Value { get; } = value;
}
private sealed class Releaser(object key) : IDisposable
{
private int _disposed;
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 1)
{
return;
}
RefCounted<SemaphoreSlim> item;
var shouldDispose = false;
lock (SemaphoreSlims)
{
if (!SemaphoreSlims.TryGetValue(key, out item!))
{
return;
}
--item.RefCount;
if (item.RefCount == 0)
{
SemaphoreSlims.Remove(key);
shouldDispose = true;
}
}
if (shouldDispose)
{
item.Value.Dispose();
}
else
{
item.Value.Release();
}
}
}
private static SemaphoreSlim? DecrementRefAndMaybeRemove(object key)
{
RefCounted<SemaphoreSlim>? itemToDispose = null;
lock (SemaphoreSlims)
{
if (SemaphoreSlims.TryGetValue(key, out var item))
{
--item.RefCount;
if (item.RefCount == 0)
{
SemaphoreSlims.Remove(key);
itemToDispose = item;
}
}
}
return itemToDispose?.Value;
}
}

1
framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj

@ -18,7 +18,6 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Core\Volo.Abp.Core.csproj" />
<PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>

18
framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs

@ -1,19 +1,13 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.DistributedLocking;
public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency
{
private readonly AsyncKeyedLocker<string> _localSyncObjects = new(o =>
{
o.PoolSize = 20;
o.PoolInitialFill = 1;
});
protected IDistributedLockKeyNormalizer DistributedLockKeyNormalizer { get; }
public LocalAbpDistributedLock(IDistributedLockKeyNormalizer distributedLockKeyNormalizer)
@ -21,7 +15,6 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency
DistributedLockKeyNormalizer = distributedLockKeyNormalizer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public async Task<IAbpDistributedLockHandle?> TryAcquireAsync(
string name,
TimeSpan timeout = default,
@ -29,12 +22,11 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency
{
Check.NotNullOrWhiteSpace(name, nameof(name));
var key = DistributedLockKeyNormalizer.NormalizeKey(name);
var timeoutReleaser = await _localSyncObjects.LockOrNullAsync(key, timeout, cancellationToken);
if (timeoutReleaser is not null)
var disposable = await KeyedLock.TryLockAsync(key, timeout, cancellationToken);
if (disposable == null)
{
return new LocalAbpDistributedLockHandle(timeoutReleaser);
return null;
}
return null;
return new LocalAbpDistributedLockHandle(disposable);
}
}

17
framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs

@ -0,0 +1,17 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.DistributedLocking;
/// <summary>
/// This implementation of <see cref="IAbpDistributedLock"/> does not provide any distributed locking functionality.
/// Useful in scenarios where distributed locking is not required or during testing.
/// </summary>
public class NullAbpDistributedLock : IAbpDistributedLock
{
public Task<IAbpDistributedLockHandle?> TryAcquireAsync(string name, TimeSpan timeout = default, CancellationToken cancellationToken = default)
{
return Task.FromResult<IAbpDistributedLockHandle?>(new LocalAbpDistributedLockHandle(NullDisposable.Instance));
}
}

7
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs

@ -28,7 +28,12 @@ public class FeatureChecker : FeatureCheckerBase
public override async Task<string?> GetOrNullAsync(string name)
{
var featureDefinition = await FeatureDefinitionManager.GetAsync(name);
var featureDefinition = await FeatureDefinitionManager.GetOrNullAsync(name);
if (featureDefinition == null)
{
return null;
}
var providers = FeatureValueProviderManager.ValueProviders
.Reverse();

9
framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionChangedEvent.cs

@ -0,0 +1,9 @@
using System;
namespace Volo.Abp.Features;
[Serializable]
public class StaticFeatureDefinitionChangedEvent
{
}

112
framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs

@ -5,37 +5,27 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.StaticDefinitions;
namespace Volo.Abp.Features;
public class StaticFeatureDefinitionStore: IStaticFeatureDefinitionStore, ISingletonDependency
{
protected IDictionary<string, FeatureGroupDefinition> FeatureGroupDefinitions => _lazyFeatureGroupDefinitions.Value;
private readonly Lazy<Dictionary<string, FeatureGroupDefinition>> _lazyFeatureGroupDefinitions;
protected IDictionary<string, FeatureDefinition> FeatureDefinitions => _lazyFeatureDefinitions.Value;
private readonly Lazy<Dictionary<string, FeatureDefinition>> _lazyFeatureDefinitions;
protected IServiceProvider ServiceProvider { get; }
protected AbpFeatureOptions Options { get; }
private readonly IServiceProvider _serviceProvider;
protected IStaticDefinitionCache<FeatureGroupDefinition, Dictionary<string, FeatureGroupDefinition>> GroupCache { get; }
protected IStaticDefinitionCache<FeatureDefinition, Dictionary<string, FeatureDefinition>> DefinitionCache { get; }
public StaticFeatureDefinitionStore(
IServiceProvider serviceProvider,
IOptions<AbpFeatureOptions> options,
IServiceProvider serviceProvider)
IStaticDefinitionCache<FeatureGroupDefinition, Dictionary<string, FeatureGroupDefinition>> groupCache,
IStaticDefinitionCache<FeatureDefinition, Dictionary<string, FeatureDefinition>> definitionCache)
{
_serviceProvider = serviceProvider;
ServiceProvider = serviceProvider;
Options = options.Value;
_lazyFeatureDefinitions = new Lazy<Dictionary<string, FeatureDefinition>>(
CreateFeatureDefinitions,
isThreadSafe: true
);
_lazyFeatureGroupDefinitions = new Lazy<Dictionary<string, FeatureGroupDefinition>>(
CreateFeatureGroupDefinitions,
isThreadSafe: true
);
GroupCache = groupCache;
DefinitionCache = definitionCache;
}
public virtual async Task<FeatureDefinition> GetAsync(string name)
@ -52,43 +42,39 @@ public class StaticFeatureDefinitionStore: IStaticFeatureDefinitionStore, ISingl
return feature;
}
protected virtual Dictionary<string, FeatureDefinition> CreateFeatureDefinitions()
public virtual async Task<FeatureDefinition?> GetOrNullAsync(string name)
{
var features = new Dictionary<string, FeatureDefinition>();
foreach (var groupDefinition in FeatureGroupDefinitions.Values)
{
foreach (var feature in groupDefinition.Features)
{
AddFeatureToDictionaryRecursively(features, feature);
}
}
var defs = await GetFeatureDefinitionsAsync();
return defs.GetOrDefault(name);
}
return features;
public virtual async Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync()
{
var defs = await GetFeatureDefinitionsAsync();
return defs.Values.ToList();
}
protected virtual void AddFeatureToDictionaryRecursively(
Dictionary<string, FeatureDefinition> features,
FeatureDefinition feature)
public virtual async Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync()
{
if (features.ContainsKey(feature.Name))
{
throw new AbpException("Duplicate feature name: " + feature.Name);
}
var groups = await GetFeatureGroupDefinitionsAsync();
return groups.Values.ToList();
}
features[feature.Name] = feature;
protected virtual async Task<Dictionary<string, FeatureGroupDefinition>> GetFeatureGroupDefinitionsAsync()
{
return await GroupCache.GetOrCreateAsync(CreateFeatureGroupDefinitionsAsync);
}
foreach (var child in feature.Children)
{
AddFeatureToDictionaryRecursively(features, child);
}
protected virtual async Task<Dictionary<string, FeatureDefinition>> GetFeatureDefinitionsAsync()
{
return await DefinitionCache.GetOrCreateAsync(CreateFeatureDefinitionsAsync);
}
protected virtual Dictionary<string, FeatureGroupDefinition> CreateFeatureGroupDefinitions()
protected virtual Task<Dictionary<string, FeatureGroupDefinition>> CreateFeatureGroupDefinitionsAsync()
{
var context = new FeatureDefinitionContext();
using (var scope = _serviceProvider.CreateScope())
using (var scope = ServiceProvider.CreateScope())
{
var providers = Options
.DefinitionProviders
@ -101,21 +87,39 @@ public class StaticFeatureDefinitionStore: IStaticFeatureDefinitionStore, ISingl
}
}
return context.Groups;
return Task.FromResult(context.Groups);
}
public virtual Task<FeatureDefinition?> GetOrNullAsync(string name)
protected virtual async Task<Dictionary<string, FeatureDefinition>> CreateFeatureDefinitionsAsync()
{
return Task.FromResult(FeatureDefinitions.GetOrDefault(name));
}
var features = new Dictionary<string, FeatureDefinition>();
public virtual Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync()
{
return Task.FromResult<IReadOnlyList<FeatureDefinition>>(FeatureDefinitions.Values.ToList());
var groups = await GetFeatureGroupDefinitionsAsync();
foreach (var groupDefinition in groups.Values)
{
foreach (var feature in groupDefinition.Features)
{
AddFeatureToDictionaryRecursively(features, feature);
}
}
return features;
}
public virtual Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync()
protected virtual void AddFeatureToDictionaryRecursively(
Dictionary<string, FeatureDefinition> features,
FeatureDefinition feature)
{
return Task.FromResult<IReadOnlyList<FeatureGroupDefinition>>(FeatureGroupDefinitions.Values.ToList());
if (features.ContainsKey(feature.Name))
{
throw new AbpException("Duplicate feature name: " + feature.Name);
}
features[feature.Name] = feature;
foreach (var child in feature.Children)
{
AddFeatureToDictionaryRecursively(features, child);
}
}
}

7
framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs

@ -23,7 +23,12 @@ public class SettingProvider : ISettingProvider, ITransientDependency
public virtual async Task<string?> GetOrNullAsync(string name)
{
var setting = await SettingDefinitionManager.GetAsync(name);
var setting = await SettingDefinitionManager.GetOrNullAsync(name);
if (setting == null)
{
return null;
}
var providers = Enumerable
.Reverse(SettingValueProviderManager.Providers);

9
framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionChangedEvent.cs

@ -0,0 +1,9 @@
using System;
namespace Volo.Abp.Settings;
[Serializable]
public class StaticSettingDefinitionChangedEvent
{
}

34
framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionStore.cs

@ -6,23 +6,24 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.StaticDefinitions;
namespace Volo.Abp.Settings;
public class StaticSettingDefinitionStore : IStaticSettingDefinitionStore, ISingletonDependency
{
protected Lazy<IDictionary<string, SettingDefinition>> SettingDefinitions { get; }
protected AbpSettingOptions Options { get; }
protected IServiceProvider ServiceProvider { get; }
protected AbpSettingOptions Options { get; }
protected IStaticDefinitionCache<SettingDefinition, Dictionary<string, SettingDefinition>> DefinitionCache { get; }
public StaticSettingDefinitionStore(IOptions<AbpSettingOptions> options, IServiceProvider serviceProvider)
public StaticSettingDefinitionStore(
IServiceProvider serviceProvider,
IOptions<AbpSettingOptions> options,
IStaticDefinitionCache<SettingDefinition, Dictionary<string, SettingDefinition>> definitionCache)
{
ServiceProvider = serviceProvider;
Options = options.Value;
SettingDefinitions = new Lazy<IDictionary<string, SettingDefinition>>(CreateSettingDefinitions, true);
DefinitionCache = definitionCache;
}
public virtual async Task<SettingDefinition> GetAsync(string name)
@ -39,17 +40,24 @@ public class StaticSettingDefinitionStore : IStaticSettingDefinitionStore, ISing
return setting;
}
public virtual Task<IReadOnlyList<SettingDefinition>> GetAllAsync()
public virtual async Task<IReadOnlyList<SettingDefinition>> GetAllAsync()
{
var defs = await GetSettingDefinitionsAsync();
return defs.Values.ToImmutableList();
}
public virtual async Task<SettingDefinition?> GetOrNullAsync(string name)
{
return Task.FromResult<IReadOnlyList<SettingDefinition>>(SettingDefinitions.Value.Values.ToImmutableList());
var defs = await GetSettingDefinitionsAsync();
return defs.GetOrDefault(name);
}
public virtual Task<SettingDefinition?> GetOrNullAsync(string name)
protected virtual async Task<Dictionary<string, SettingDefinition>> GetSettingDefinitionsAsync()
{
return Task.FromResult(SettingDefinitions.Value.GetOrDefault(name));
return await DefinitionCache.GetOrCreateAsync(CreateSettingDefinitionsAsync);
}
protected virtual IDictionary<string, SettingDefinition> CreateSettingDefinitions()
protected virtual Task<Dictionary<string, SettingDefinition>> CreateSettingDefinitionsAsync()
{
var settings = new Dictionary<string, SettingDefinition>();
@ -66,6 +74,6 @@ public class StaticSettingDefinitionStore : IStaticSettingDefinitionStore, ISing
}
}
return settings;
return Task.FromResult(settings);
}
}

9
framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionChangedEvent.cs

@ -0,0 +1,9 @@
using System;
namespace Volo.Abp.TextTemplating;
[Serializable]
public class StaticTemplateDefinitionChangedEvent
{
}

40
framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionStore.cs

@ -6,50 +6,58 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.StaticDefinitions;
namespace Volo.Abp.TextTemplating;
public class StaticTemplateDefinitionStore : IStaticTemplateDefinitionStore, ISingletonDependency
{
protected Lazy<IDictionary<string, TemplateDefinition>> TemplateDefinitions { get; }
protected AbpTextTemplatingOptions Options { get; }
protected IServiceProvider ServiceProvider { get; }
protected AbpTextTemplatingOptions Options { get; }
protected IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> DefinitionCache { get; }
public StaticTemplateDefinitionStore(IOptions<AbpTextTemplatingOptions> options, IServiceProvider serviceProvider)
public StaticTemplateDefinitionStore(
IServiceProvider serviceProvider,
IOptions<AbpTextTemplatingOptions> options,
IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> definitionCache)
{
ServiceProvider = serviceProvider;
Options = options.Value;
TemplateDefinitions = new Lazy<IDictionary<string, TemplateDefinition>>(CreateTextTemplateDefinitions, true);
DefinitionCache = definitionCache;
}
public virtual Task<TemplateDefinition> GetAsync(string name)
public virtual async Task<TemplateDefinition> GetAsync(string name)
{
Check.NotNull(name, nameof(name));
var template = GetOrNullAsync(name);
var template = await GetOrNullAsync(name);
if (template == null)
{
throw new AbpException("Undefined template: " + name);
}
return template!;
return template;
}
public virtual async Task<IReadOnlyList<TemplateDefinition>> GetAllAsync()
{
var defs = await GetTemplateDefinitionsAsync();
return defs.Values.ToImmutableList();
}
public virtual Task<IReadOnlyList<TemplateDefinition>> GetAllAsync()
public virtual async Task<TemplateDefinition?> GetOrNullAsync(string name)
{
return Task.FromResult<IReadOnlyList<TemplateDefinition>>(TemplateDefinitions.Value.Values.ToImmutableList());
var defs = await GetTemplateDefinitionsAsync();
return defs.GetOrDefault(name);
}
public virtual Task<TemplateDefinition?> GetOrNullAsync(string name)
protected virtual async Task<Dictionary<string, TemplateDefinition>> GetTemplateDefinitionsAsync()
{
return Task.FromResult(TemplateDefinitions.Value.GetOrDefault(name));
return await DefinitionCache.GetOrCreateAsync(CreateTextTemplateDefinitionsAsync);
}
protected virtual IDictionary<string, TemplateDefinition> CreateTextTemplateDefinitions()
protected virtual Task<Dictionary<string, TemplateDefinition>> CreateTextTemplateDefinitionsAsync()
{
var templates = new Dictionary<string, TemplateDefinition>();
@ -78,6 +86,6 @@ public class StaticTemplateDefinitionStore : IStaticTemplateDefinitionStore, ISi
}
}
return templates;
return Task.FromResult(templates);
}
}

122
framework/test/Volo.Abp.Core.Tests/Volo/Abp/StaticDefinitions/StaticDefinitionCache_Tests.cs

@ -0,0 +1,122 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.StaticDefinitions;
public class StaticDefinitionCache_Tests : AbpIntegratedTest<AbpTestModule>
{
protected readonly IStaticDefinitionCache<StaticDefinition1, List<StaticDefinition1>> _staticDefinitionCache1;
protected readonly IStaticDefinitionCache<StaticDefinition2, List<StaticDefinition2>> _staticDefinitionCache2;
public StaticDefinitionCache_Tests()
{
_staticDefinitionCache1 = GetRequiredService<IStaticDefinitionCache<StaticDefinition1, List<StaticDefinition1>>>();
_staticDefinitionCache2 = GetRequiredService<IStaticDefinitionCache<StaticDefinition2, List<StaticDefinition2>>>();
}
[Fact]
public async Task GetOrCreate_Test()
{
var definition1 = new StaticDefinition1 { Name = "Definition1", Value = 1 };
var definition2 = new StaticDefinition1 { Name = "Definition2", Value = 2 };
var definitionsFirstRetrieval = await _staticDefinitionCache1.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition1> { definition1, definition2 });
});
var definitionsSecondRetrieval = await _staticDefinitionCache1.GetOrCreateAsync(() =>
{
throw new AbpException("Factory should not be called on second retrieval");
});
definitionsFirstRetrieval.ShouldBe(definitionsSecondRetrieval);
definitionsSecondRetrieval.Count.ShouldBe(2);
definitionsSecondRetrieval[0].Name.ShouldBe("Definition1");
definitionsSecondRetrieval[0].Value.ShouldBe(1);
definitionsSecondRetrieval[1].Name.ShouldBe("Definition2");
definitionsSecondRetrieval[1].Value.ShouldBe(2);
}
[Fact]
public async Task Separate_Caches_For_Different_Types_Test()
{
var definitions1 = await _staticDefinitionCache1.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition1> { new StaticDefinition1 {Name = "Definition1", Value = 1} });
});
var definitions2 = await _staticDefinitionCache2.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition2> { new StaticDefinition2 {Name = "DefinitionA", Value = 100} });
});
definitions1.Count.ShouldBe(1);
definitions1[0].Name.ShouldBe("Definition1");
definitions1[0].Value.ShouldBe(1);
definitions2.Count.ShouldBe(1);
definitions2[0].Name.ShouldBe("DefinitionA");
definitions2[0].Value.ShouldBe(100);
}
[Fact]
public async Task Clear_Test()
{
var definitions1 = await _staticDefinitionCache1.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition1> { new StaticDefinition1 {Name = "Definition1", Value = 1} });
});
var definitions2 = await _staticDefinitionCache2.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition2> { new StaticDefinition2 {Name = "DefinitionA", Value = 100} });
});
definitions1.Count.ShouldBe(1);
definitions1[0].Name.ShouldBe("Definition1");
definitions1[0].Value.ShouldBe(1);
definitions2.Count.ShouldBe(1);
definitions2[0].Name.ShouldBe("DefinitionA");
definitions2[0].Value.ShouldBe(100);
await _staticDefinitionCache1.ClearAsync();
await _staticDefinitionCache2.ClearAsync();
var definitions1AfterClear = await _staticDefinitionCache1.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition1> { new StaticDefinition1 {Name = "DefinitionNew", Value = 10} });
});
var definitions2AfterClear = await _staticDefinitionCache2.GetOrCreateAsync(() =>
{
return Task.FromResult(new List<StaticDefinition2> {new StaticDefinition2 {Name = "DefinitionNewA", Value = 200}});
});
definitions1AfterClear.Count.ShouldBe(1);
definitions1AfterClear[0].Name.ShouldBe("DefinitionNew");
definitions1AfterClear[0].Value.ShouldBe(10);
definitions2AfterClear.Count.ShouldBe(1);
definitions2AfterClear[0].Name.ShouldBe("DefinitionNewA");
definitions2AfterClear[0].Value.ShouldBe(200);
}
public class StaticDefinition1
{
public string Name { get; set; }
public int Value { get; set; }
}
public class StaticDefinition2
{
public string Name { get; set; }
public int Value { get; set; }
}
}

179
framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs

@ -0,0 +1,179 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
namespace Volo.Abp.Threading;
public class KeyedLock_Tests
{
[Fact]
public async Task TryLock_Should_Acquire_Immediately_When_Free()
{
var key = "key-try-1";
var handle = await KeyedLock.TryLockAsync(key);
handle.ShouldNotBeNull();
handle!.Dispose();
var handle2 = await KeyedLock.TryLockAsync(key);
handle2.ShouldNotBeNull();
handle2!.Dispose();
}
[Fact]
public async Task TryLock_Should_Return_Null_When_Already_Locked()
{
var key = "key-try-2";
using (await KeyedLock.LockAsync(key))
{
var handle2 = await KeyedLock.TryLockAsync(key);
handle2.ShouldBeNull();
}
var handle3 = await KeyedLock.TryLockAsync(key);
handle3.ShouldNotBeNull();
handle3!.Dispose();
}
[Fact]
public async Task LockAsync_Should_Block_Until_Released()
{
var key = "key-block-1";
var sw = Stopwatch.StartNew();
Task inner;
using (await KeyedLock.LockAsync(key))
{
inner = Task.Run(async () =>
{
using (await KeyedLock.LockAsync(key))
{
// Acquired only after outer lock is released
}
});
// While holding the outer lock, inner waiter should not complete
await Task.Delay(200);
inner.IsCompleted.ShouldBeFalse();
}
// After releasing, inner should complete; elapsed >= hold time
await inner;
sw.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(180);
}
[Fact]
public async Task TryLock_With_Timeout_Should_Return_Null_When_Busy()
{
var key = "key-timeout-1";
using (await KeyedLock.LockAsync(key))
{
var handle = await KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(50));
handle.ShouldBeNull();
}
}
[Fact]
public async Task TryLock_With_Timeout_Should_Succeed_If_Released_In_Time()
{
var key = "key-timeout-2";
// Hold the lock manually
var outer = await KeyedLock.LockAsync(key);
var tryTask = KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(200));
await Task.Delay(50);
// Release within the timeout window
outer.Dispose();
var handle2 = await tryTask;
handle2.ShouldNotBeNull();
handle2!.Dispose();
}
[Fact]
public async Task LockAsync_With_Cancellation_Should_Rollback_RefCount()
{
var key = "key-cancel-1";
var cts = new CancellationTokenSource();
await cts.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(async () =>
{
await KeyedLock.LockAsync(key, cts.Token);
});
// After cancellation, we should still be able to acquire the key
var handle = await KeyedLock.TryLockAsync(key);
handle.ShouldNotBeNull();
handle!.Dispose();
}
[Fact]
public async Task TryLock_With_Cancellation_Should_Rollback()
{
var key = "key-cancel-2";
// Ensure it's initially free
var h0 = await KeyedLock.TryLockAsync(key);
h0?.Dispose();
var cts = new CancellationTokenSource();
await cts.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(async () =>
{
await KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(200), cts.Token);
});
// After cancellation, the key should be acquirable
var handle = await KeyedLock.TryLockAsync(key);
handle.ShouldNotBeNull();
handle!.Dispose();
}
[Fact]
public async Task Serializes_Access_For_Same_Key()
{
var key = "key-serial-1";
int counter = 0;
var tasks = Enumerable.Range(0, 10).Select(async _ =>
{
using (await KeyedLock.LockAsync(key))
{
var current = counter;
await Task.Delay(10);
counter = current + 1;
}
});
await Task.WhenAll(tasks);
counter.ShouldBe(10);
}
[Fact]
public async Task Multiple_Keys_Should_Not_Block_Each_Other()
{
var key1 = "key-multi-1";
var key2 = "key-multi-2";
using (await KeyedLock.LockAsync(key1))
{
var handle2 = await KeyedLock.TryLockAsync(key2);
handle2.ShouldNotBeNull();
handle2!.Dispose();
}
}
[Fact]
public async Task TryLock_Default_Overload_Delegates_To_Full_Overload()
{
var key = "key-default-1";
using (await KeyedLock.LockAsync(key))
{
var h1 = await KeyedLock.TryLockAsync(key);
h1.ShouldBeNull();
}
var h2 = await KeyedLock.TryLockAsync(key);
h2.ShouldNotBeNull();
h2!.Dispose();
}
}

4
modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/CmsKitMongoDbTestModule.cs

@ -1,13 +1,15 @@
using System;
using Volo.Abp.Data;
using Volo.Abp.Modularity;
using Volo.Abp.SettingManagement.MongoDB;
using Volo.Abp.Uow;
namespace Volo.CmsKit.MongoDB;
[DependsOn(
typeof(CmsKitTestBaseModule),
typeof(CmsKitMongoDbModule)
typeof(CmsKitMongoDbModule),
typeof(AbpSettingManagementMongoDbModule)
)]
public class CmsKitMongoDbTestModule : AbpModule
{

1
modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/Volo.CmsKit.MongoDB.Tests.csproj

@ -11,6 +11,7 @@
<PackageReference Include="MongoSandbox8.runtime.linux-x64" Condition="$([MSBuild]::IsOSPlatform('Linux'))" />
<PackageReference Include="MongoSandbox8.runtime.osx-arm64" Condition="$([MSBuild]::IsOSPlatform('OSX'))" />
<PackageReference Include="MongoSandbox8.runtime.win-x64" Condition="$([MSBuild]::IsOSPlatform('Windows'))" />
<ProjectReference Include="..\..\..\..\modules\setting-management\src\Volo.Abp.SettingManagement.MongoDB/Volo.Abp.SettingManagement.MongoDB.csproj" />
<ProjectReference Include="..\..\src\Volo.CmsKit.MongoDB\Volo.CmsKit.MongoDB.csproj" />
<ProjectReference Include="..\Volo.CmsKit.TestBase\Volo.CmsKit.TestBase.csproj" />
</ItemGroup>

131
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs

@ -24,6 +24,8 @@ namespace Volo.Abp.FeatureManagement;
)]
public class AbpFeatureManagementDomainModule : AbpModule
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<FeatureManagementOptions>(options =>
@ -51,18 +53,16 @@ public class AbpFeatureManagementDomainModule : AbpModule
}
}
private readonly CancellationTokenSource _cancellationTokenSource = new();
private Task _initializeDynamicFeaturesTask;
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context));
}
public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
InitializeDynamicFeatures(context);
return Task.CompletedTask;
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
var initializer = rootServiceProvider.GetRequiredService<FeatureDynamicInitializer>();
await initializer.InitializeAsync(true, _cancellationTokenSource.Token);
}
public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
@ -70,123 +70,4 @@ public class AbpFeatureManagementDomainModule : AbpModule
_cancellationTokenSource.Cancel();
return Task.CompletedTask;
}
public Task GetInitializeDynamicFeaturesTask()
{
return _initializeDynamicFeaturesTask ?? Task.CompletedTask;
}
private void InitializeDynamicFeatures(ApplicationInitializationContext context)
{
var options = context
.ServiceProvider
.GetRequiredService<IOptions<FeatureManagementOptions>>()
.Value;
if (!options.SaveStaticFeaturesToDatabase && !options.IsDynamicFeatureStoreEnabled)
{
return;
}
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
_initializeDynamicFeaturesTask = Task.Run(async () =>
{
using var scope = rootServiceProvider.CreateScope();
var applicationLifetime = scope.ServiceProvider.GetService<IHostApplicationLifetime>();
var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
var cancellationToken = applicationLifetime?.ApplicationStopping ?? _cancellationTokenSource.Token;
try
{
using (cancellationTokenProvider.Use(cancellationToken))
{
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticFeaturesToDatabaseAsync(options, scope, cancellationTokenProvider);
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicFeaturesAsync(options, scope);
}
}
// ReSharper disable once EmptyGeneralCatchClause (No need to log since it is logged above)
catch { }
});
}
private static async Task SaveStaticFeaturesToDatabaseAsync(
FeatureManagementOptions options,
IServiceScope scope,
ICancellationTokenProvider cancellationTokenProvider)
{
if (!options.SaveStaticFeaturesToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(
8,
retryAttempt => TimeSpan.FromSeconds(
RandomHelper.GetRandom(
(int)Math.Pow(2, retryAttempt) * 8,
(int)Math.Pow(2, retryAttempt) * 12)
)
)
.ExecuteAsync(async _ =>
{
try
{
// ReSharper disable once AccessToDisposedClosure
await scope
.ServiceProvider
.GetRequiredService<IStaticFeatureSaver>()
.SaveAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope.ServiceProvider
.GetService<ILogger<AbpFeatureManagementDomainModule>>()?
.LogException(ex);
throw; // Polly will catch it
}
}, cancellationTokenProvider.Token);
}
private static async Task PreCacheDynamicFeaturesAsync(FeatureManagementOptions options, IServiceScope scope)
{
if (!options.IsDynamicFeatureStoreEnabled)
{
return;
}
try
{
// Pre-cache features, so first request doesn't wait
await scope
.ServiceProvider
.GetRequiredService<IDynamicFeatureDefinitionStore>()
.GetGroupsAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope
.ServiceProvider
.GetService<ILogger<AbpFeatureManagementDomainModule>>()?
.LogException(ex);
throw; // It will be cached in InitializeDynamicFeatures
}
}
}

149
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs

@ -0,0 +1,149 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Polly;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Features;
using Volo.Abp.Threading;
namespace Volo.Abp.FeatureManagement;
public class FeatureDynamicInitializer : ITransientDependency
{
public ILogger<FeatureDynamicInitializer> Logger { get; set; }
protected IServiceProvider ServiceProvider { get; }
protected IOptions<FeatureManagementOptions> Options { get; }
[CanBeNull]
protected IHostApplicationLifetime ApplicationLifetime { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IDynamicFeatureDefinitionStore DynamicFeatureDefinitionStore { get; }
protected IStaticFeatureSaver StaticFeatureSaver { get; }
public FeatureDynamicInitializer(
IServiceProvider serviceProvider,
IOptions<FeatureManagementOptions> options,
ICancellationTokenProvider cancellationTokenProvider,
IDynamicFeatureDefinitionStore dynamicFeatureDefinitionStore,
IStaticFeatureSaver staticFeatureSaver)
{
Logger = NullLogger<FeatureDynamicInitializer>.Instance;
ServiceProvider = serviceProvider;
Options = options;
ApplicationLifetime = ServiceProvider.GetService<IHostApplicationLifetime>();
CancellationTokenProvider = cancellationTokenProvider;
DynamicFeatureDefinitionStore = dynamicFeatureDefinitionStore;
StaticFeatureSaver = staticFeatureSaver;
}
public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default)
{
var options = Options.Value;
if (!options.SaveStaticFeaturesToDatabase && !options.IsDynamicFeatureStoreEnabled)
{
return Task.CompletedTask;
}
if (runInBackground)
{
Task.Run(async () =>
{
if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null)
{
cancellationToken = ApplicationLifetime.ApplicationStopping;
}
await ExecuteInitializationAsync(options, cancellationToken);
}, cancellationToken);
return Task.CompletedTask;
}
return ExecuteInitializationAsync(options, cancellationToken);
}
protected virtual async Task ExecuteInitializationAsync(FeatureManagementOptions options, CancellationToken cancellationToken)
{
try
{
using (CancellationTokenProvider.Use(cancellationToken))
{
if (CancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticFeaturesToDatabaseAsync(options, cancellationToken);
if (CancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicFeaturesAsync(options);
}
}
catch
{
// No need to log here since inner calls log
}
}
protected virtual async Task SaveStaticFeaturesToDatabaseAsync(
FeatureManagementOptions options,
CancellationToken cancellationToken)
{
if (!options.SaveStaticFeaturesToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(
8,
retryAttempt => TimeSpan.FromSeconds(
Volo.Abp.RandomHelper.GetRandom(
(int)Math.Pow(2, retryAttempt) * 8,
(int)Math.Pow(2, retryAttempt) * 12)
)
)
.ExecuteAsync(async _ =>
{
try
{
await StaticFeatureSaver.SaveAsync();
}
catch (Exception ex)
{
Logger.LogException(ex);
throw; // Polly will catch it
}
}, cancellationToken);
}
protected virtual async Task PreCacheDynamicFeaturesAsync(FeatureManagementOptions options)
{
if (!options.IsDynamicFeatureStoreEnabled)
{
return;
}
try
{
// Pre-cache features, so first request doesn't wait
await DynamicFeatureDefinitionStore.GetGroupsAsync();
}
catch (Exception ex)
{
Logger.LogException(ex);
throw; // It will be cached in Initialize()
}
}
}

36
modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureDefinitionChangedEventHandler.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.Features;
using Volo.Abp.StaticDefinitions;
using Volo.Abp.Threading;
namespace Volo.Abp.FeatureManagement;
public class StaticFeatureDefinitionChangedEventHandler : ILocalEventHandler<StaticFeatureDefinitionChangedEvent>, ITransientDependency
{
protected IStaticDefinitionCache<FeatureGroupDefinition, Dictionary<string, FeatureGroupDefinition>> GroupCache { get; }
protected IStaticDefinitionCache<FeatureDefinition, Dictionary<string, FeatureDefinition>> DefinitionCache { get; }
protected FeatureDynamicInitializer FeatureDynamicInitializer { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public StaticFeatureDefinitionChangedEventHandler(
IStaticDefinitionCache<FeatureGroupDefinition, Dictionary<string, FeatureGroupDefinition>> groupCache,
IStaticDefinitionCache<FeatureDefinition, Dictionary<string, FeatureDefinition>> definitionCache,
FeatureDynamicInitializer featureDynamicInitializer,
ICancellationTokenProvider cancellationTokenProvider)
{
GroupCache = groupCache;
DefinitionCache = definitionCache;
FeatureDynamicInitializer = featureDynamicInitializer;
CancellationTokenProvider = cancellationTokenProvider;
}
public virtual async Task HandleEventAsync(StaticFeatureDefinitionChangedEvent eventData)
{
await GroupCache.ClearAsync();
await DefinitionCache.ClearAsync();
await FeatureDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token);
}
}

14
modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs

@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
using Volo.Abp.Modularity;
@ -53,15 +54,8 @@ public class AbpFeatureManagementEntityFrameworkCoreTestModule : AbpModule
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var task = context.ServiceProvider.GetRequiredService<AbpFeatureManagementDomainModule>().GetInitializeDynamicFeaturesTask();
if (!task.IsCompleted)
{
AsyncHelper.RunSync(() => Awaited(task));
}
}
private async static Task Awaited(Task task)
{
await task;
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
var initializer = rootServiceProvider.GetRequiredService<FeatureDynamicInitializer>();
AsyncHelper.RunSync(() => initializer.InitializeAsync(false));
}
}

9
modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs

@ -6,6 +6,7 @@ using Blazorise;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization;
using Volo.Abp.PermissionManagement.Localization;
@ -153,7 +154,13 @@ public partial class PermissionManagementModal
await PermissionAppService.UpdateAsync(_providerName, _providerKey, updateDto);
await CurrentApplicationConfigurationCacheResetService.ResetAsync();
Guid? userId = null;
if (_providerName == UserPermissionValueProvider.ProviderName && Guid.TryParse(_providerKey, out var parsedUserId))
{
userId = parsedUserId;
}
await CurrentApplicationConfigurationCacheResetService.ResetAsync(userId);
await InvokeAsync(_modal.Hide);
await Notify.Success(L["SavedSuccessfully"]);

132
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs

@ -2,6 +2,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -11,6 +12,7 @@ using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Caching;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.Domain;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
@ -26,9 +28,11 @@ namespace Volo.Abp.PermissionManagement;
public class AbpPermissionManagementDomainModule : AbpModule
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private Task _initializeDynamicPermissionsTask;
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Replace(ServiceDescriptor.Singleton<IAbpDistributedLock, NullAbpDistributedLock>());
if (context.Services.IsDataMigrationEnvironment())
{
Configure<PermissionManagementOptions>(options =>
@ -44,10 +48,11 @@ public class AbpPermissionManagementDomainModule : AbpModule
AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context));
}
public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
InitializeDynamicPermissions(context);
return Task.CompletedTask;
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
var initializer = rootServiceProvider.GetRequiredService<PermissionDynamicInitializer>();
await initializer.InitializeAsync(true, _cancellationTokenSource.Token);
}
public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
@ -55,123 +60,4 @@ public class AbpPermissionManagementDomainModule : AbpModule
_cancellationTokenSource.Cancel();
return Task.CompletedTask;
}
public Task GetInitializeDynamicPermissionsTask()
{
return _initializeDynamicPermissionsTask ?? Task.CompletedTask;
}
private void InitializeDynamicPermissions(ApplicationInitializationContext context)
{
var options = context
.ServiceProvider
.GetRequiredService<IOptions<PermissionManagementOptions>>()
.Value;
if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled)
{
return;
}
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
_initializeDynamicPermissionsTask = Task.Run(async () =>
{
using var scope = rootServiceProvider.CreateScope();
var applicationLifetime = scope.ServiceProvider.GetService<IHostApplicationLifetime>();
var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
var cancellationToken = applicationLifetime?.ApplicationStopping ?? _cancellationTokenSource.Token;
try
{
using (cancellationTokenProvider.Use(cancellationToken))
{
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticPermissionsToDatabaseAsync(options, scope, cancellationTokenProvider);
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicPermissionsAsync(options, scope);
}
}
// ReSharper disable once EmptyGeneralCatchClause (No need to log since it is logged above)
catch { }
});
}
private async static Task SaveStaticPermissionsToDatabaseAsync(
PermissionManagementOptions options,
IServiceScope scope,
ICancellationTokenProvider cancellationTokenProvider)
{
if (!options.SaveStaticPermissionsToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(
8,
retryAttempt => TimeSpan.FromSeconds(
RandomHelper.GetRandom(
(int)Math.Pow(2, retryAttempt) * 8,
(int)Math.Pow(2, retryAttempt) * 12)
)
)
.ExecuteAsync(async _ =>
{
try
{
// ReSharper disable once AccessToDisposedClosure
await scope
.ServiceProvider
.GetRequiredService<IStaticPermissionSaver>()
.SaveAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope.ServiceProvider
.GetService<ILogger<AbpPermissionManagementDomainModule>>()?
.LogException(ex);
throw; // Polly will catch it
}
}, cancellationTokenProvider.Token);
}
private async static Task PreCacheDynamicPermissionsAsync(PermissionManagementOptions options, IServiceScope scope)
{
if (!options.IsDynamicPermissionStoreEnabled)
{
return;
}
try
{
// Pre-cache permissions, so first request doesn't wait
await scope
.ServiceProvider
.GetRequiredService<IDynamicPermissionDefinitionStore>()
.GetGroupsAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope
.ServiceProvider
.GetService<ILogger<AbpPermissionManagementDomainModule>>()?
.LogException(ex);
throw; // It will be cached in InitializeDynamicPermissions
}
}
}

151
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs

@ -0,0 +1,151 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Polly;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.PermissionManagement;
public class PermissionDynamicInitializer : ITransientDependency
{
public ILogger<PermissionDynamicInitializer> Logger { get; set; }
protected IServiceProvider ServiceProvider { get; }
protected IOptions<PermissionManagementOptions> Options { get; }
[CanBeNull]
protected IHostApplicationLifetime ApplicationLifetime { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IDynamicPermissionDefinitionStore DynamicPermissionDefinitionStore { get; }
protected IStaticPermissionSaver StaticPermissionSaver { get; }
public PermissionDynamicInitializer(
IServiceProvider serviceProvider,
IOptions<PermissionManagementOptions> options,
ICancellationTokenProvider cancellationTokenProvider,
IDynamicPermissionDefinitionStore dynamicPermissionDefinitionStore,
IStaticPermissionSaver staticPermissionSaver)
{
Logger = NullLogger<PermissionDynamicInitializer>.Instance;
ServiceProvider = serviceProvider;
Options = options;
ApplicationLifetime = ServiceProvider.GetService<IHostApplicationLifetime>();
CancellationTokenProvider = cancellationTokenProvider;
DynamicPermissionDefinitionStore = dynamicPermissionDefinitionStore;
StaticPermissionSaver = staticPermissionSaver;
}
public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default)
{
var options = Options.Value;
if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled)
{
return Task.CompletedTask;
}
if (runInBackground)
{
Task.Run(async () =>
{
if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null)
{
cancellationToken = ApplicationLifetime.ApplicationStopping;
}
await ExecuteInitializationAsync(options, cancellationToken);
}, cancellationToken);
return Task.CompletedTask;
}
return ExecuteInitializationAsync(options, cancellationToken);
}
protected virtual async Task ExecuteInitializationAsync(PermissionManagementOptions options, CancellationToken cancellationToken)
{
try
{
using (CancellationTokenProvider.Use(cancellationToken))
{
if (CancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticPermissionsToDatabaseAsync(options, cancellationToken);
if (CancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicPermissionsAsync(options);
}
}
catch
{
// No need to log here since inner calls log
}
}
protected virtual async Task SaveStaticPermissionsToDatabaseAsync(
PermissionManagementOptions options,
CancellationToken cancellationToken)
{
if (!options.SaveStaticPermissionsToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(
8,
retryAttempt => TimeSpan.FromSeconds(
Volo.Abp.RandomHelper.GetRandom(
(int)Math.Pow(2, retryAttempt) * 8,
(int)Math.Pow(2, retryAttempt) * 12)
)
)
.ExecuteAsync(async _ =>
{
try
{
await StaticPermissionSaver.SaveAsync();
}
catch (Exception ex)
{
Logger.LogException(ex);
throw; // Polly will catch it
}
}, cancellationToken);
}
protected virtual async Task PreCacheDynamicPermissionsAsync(PermissionManagementOptions options)
{
if (!options.IsDynamicPermissionStoreEnabled)
{
return;
}
try
{
// Pre-cache permissions, so first request doesn't wait
await DynamicPermissionDefinitionStore.GetGroupsAsync();
}
catch (Exception ex)
{
Logger.LogException(ex);
throw; // It will be cached in Initialize()
}
}
}

36
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/StaticPermissionDefinitionChangedEventHandler.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.StaticDefinitions;
using Volo.Abp.Threading;
namespace Volo.Abp.PermissionManagement;
public class StaticPermissionDefinitionChangedEventHandler : ILocalEventHandler<StaticPermissionDefinitionChangedEvent>, ITransientDependency
{
protected IStaticDefinitionCache<PermissionGroupDefinition, Dictionary<string, PermissionGroupDefinition>> GroupCache { get; }
protected IStaticDefinitionCache<PermissionDefinition, Dictionary<string, PermissionDefinition>> DefinitionCache { get; }
protected PermissionDynamicInitializer PermissionDynamicInitializer { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public StaticPermissionDefinitionChangedEventHandler(
IStaticDefinitionCache<PermissionGroupDefinition, Dictionary<string, PermissionGroupDefinition>> groupCache,
IStaticDefinitionCache<PermissionDefinition, Dictionary<string, PermissionDefinition>> definitionCache,
PermissionDynamicInitializer permissionDynamicInitializer,
ICancellationTokenProvider cancellationTokenProvider)
{
GroupCache = groupCache;
DefinitionCache = definitionCache;
PermissionDynamicInitializer = permissionDynamicInitializer;
CancellationTokenProvider = cancellationTokenProvider;
}
public virtual async Task HandleEventAsync(StaticPermissionDefinitionChangedEvent eventData)
{
await GroupCache.ClearAsync();
await DefinitionCache.ClearAsync();
await PermissionDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token);
}
}

14
modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -6,6 +7,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Localization;
using Volo.Abp.PermissionManagement.Web.Utils;
@ -105,9 +107,13 @@ public class PermissionManagementModal : AbpPageModel
}
);
await LocalEventBus.PublishAsync(
new CurrentApplicationConfigurationCacheResetEventData()
);
Guid? userId = null;
if (ProviderName == UserPermissionValueProvider.ProviderName && Guid.TryParse(ProviderKey, out var parsedUserId))
{
userId = parsedUserId;
}
await LocalEventBus.PublishAsync(new CurrentApplicationConfigurationCacheResetEventData(userId));
return NoContent();
}
@ -130,7 +136,7 @@ public class PermissionManagementModal : AbpPageModel
public bool IsDisabled(string currentProviderName)
{
var grantedProviders = Permissions.SelectMany(x => x.GrantedProviders);
return Permissions.All(x => x.IsGranted) && grantedProviders.All(p => p.ProviderName != currentProviderName);
}
}

15
modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs

@ -9,6 +9,7 @@ using Volo.Abp.Modularity;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
using Microsoft.Data.Sqlite;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.PermissionManagement.EntityFrameworkCore;
@ -56,18 +57,10 @@ public class AbpPermissionManagementEntityFrameworkCoreTestModule : AbpModule
return connection;
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var task = context.ServiceProvider.GetRequiredService<AbpPermissionManagementDomainModule>().GetInitializeDynamicPermissionsTask();
if (!task.IsCompleted)
{
AsyncHelper.RunSync(() => Awaited(task));
}
}
private async static Task Awaited(Task task)
{
await task;
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
var initializer = rootServiceProvider.GetRequiredService<PermissionDynamicInitializer>();
AsyncHelper.RunSync(() => initializer.InitializeAsync(false));
}
}

120
modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/AbpSettingManagementDomainModule.cs

@ -25,7 +25,6 @@ namespace Volo.Abp.SettingManagement;
public class AbpSettingManagementDomainModule : AbpModule
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private Task _initializeDynamicSettingsTask;
public override void ConfigureServices(ServiceConfigurationContext context)
{
@ -53,10 +52,11 @@ public class AbpSettingManagementDomainModule : AbpModule
AsyncHelper.RunSync(() => OnApplicationInitializationAsync(context));
}
public override Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
InitializeDynamicSettings(context);
return Task.CompletedTask;
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
var initializer = rootServiceProvider.GetRequiredService<SettingDynamicInitializer>();
await initializer.InitializeAsync(true, _cancellationTokenSource.Token);
}
public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
@ -64,116 +64,4 @@ public class AbpSettingManagementDomainModule : AbpModule
_cancellationTokenSource.Cancel();
return Task.CompletedTask;
}
public Task GetInitializeDynamicSettingsTask()
{
return _initializeDynamicSettingsTask ?? Task.CompletedTask;
}
private void InitializeDynamicSettings(ApplicationInitializationContext context)
{
var options = context
.ServiceProvider
.GetRequiredService<IOptions<SettingManagementOptions>>()
.Value;
if (!options.SaveStaticSettingsToDatabase && !options.IsDynamicSettingStoreEnabled)
{
return;
}
var rootServiceProvider = context.ServiceProvider.GetRequiredService<IRootServiceProvider>();
_initializeDynamicSettingsTask = Task.Run(async () =>
{
using var scope = rootServiceProvider.CreateScope();
var applicationLifetime = scope.ServiceProvider.GetService<IHostApplicationLifetime>();
var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
var cancellationToken = applicationLifetime?.ApplicationStopping ?? _cancellationTokenSource.Token;
try
{
using (cancellationTokenProvider.Use(cancellationToken))
{
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticSettingsToDatabaseAsync(options, scope, cancellationTokenProvider);
if (cancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicSettingsAsync(options, scope);
}
}
// ReSharper disable once EmptyGeneralCatchClause (No need to log since it is logged above)
catch { }
});
}
private async static Task SaveStaticSettingsToDatabaseAsync(
SettingManagementOptions options,
IServiceScope scope,
ICancellationTokenProvider cancellationTokenProvider)
{
if (!options.SaveStaticSettingsToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(8, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt) * 10))
.ExecuteAsync(async _ =>
{
try
{
// ReSharper disable once AccessToDisposedClosure
await scope
.ServiceProvider
.GetRequiredService<IStaticSettingSaver>()
.SaveAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope.ServiceProvider
.GetService<ILogger<AbpSettingManagementDomainModule>>()?
.LogException(ex);
throw; // Polly will catch it
}
}, cancellationTokenProvider.Token);
}
private async static Task PreCacheDynamicSettingsAsync(SettingManagementOptions options, IServiceScope scope)
{
if (!options.IsDynamicSettingStoreEnabled)
{
return;
}
try
{
// Pre-cache settings, so first request doesn't wait
await scope
.ServiceProvider
.GetRequiredService<IDynamicSettingDefinitionStore>()
.GetAllAsync();
}
catch (Exception ex)
{
// ReSharper disable once AccessToDisposedClosure
scope
.ServiceProvider
.GetService<ILogger<AbpSettingManagementDomainModule>>()?
.LogException(ex);
throw; // It will be cached in InitializeDynamicSettings
}
}
}

150
modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs

@ -0,0 +1,150 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Polly;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Settings;
using Volo.Abp.Threading;
namespace Volo.Abp.SettingManagement;
public class SettingDynamicInitializer : ITransientDependency
{
public ILogger<SettingDynamicInitializer> Logger { get; set; }
protected IServiceProvider ServiceProvider { get; }
protected IOptions<SettingManagementOptions> Options { get; }
[CanBeNull]
protected IHostApplicationLifetime ApplicationLifetime { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected IDynamicSettingDefinitionStore DynamicSettingDefinitionStore { get; }
protected IStaticSettingSaver StaticSettingSaver { get; }
public SettingDynamicInitializer(
IServiceProvider serviceProvider,
IOptions<SettingManagementOptions> options,
ICancellationTokenProvider cancellationTokenProvider,
IDynamicSettingDefinitionStore dynamicSettingDefinitionStore,
IStaticSettingSaver staticSettingSaver)
{
Logger = NullLogger<SettingDynamicInitializer>.Instance;
ServiceProvider = serviceProvider;
Options = options;
ApplicationLifetime = ServiceProvider.GetService<IHostApplicationLifetime>();
CancellationTokenProvider = cancellationTokenProvider;
DynamicSettingDefinitionStore = dynamicSettingDefinitionStore;
StaticSettingSaver = staticSettingSaver;
}
public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default)
{
var options = Options.Value;
if (!options.SaveStaticSettingsToDatabase && !options.IsDynamicSettingStoreEnabled)
{
return Task.CompletedTask;
}
if (runInBackground)
{
Task.Run(async () =>
{
if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null)
{
cancellationToken = ApplicationLifetime.ApplicationStopping;
}
await ExecuteInitializationAsync(options, cancellationToken);
}, cancellationToken);
return Task.CompletedTask;
}
return ExecuteInitializationAsync(options, cancellationToken);
}
protected virtual async Task ExecuteInitializationAsync(SettingManagementOptions options, CancellationToken cancellationToken)
{
try
{
using (CancellationTokenProvider.Use(cancellationToken))
{
if (CancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await SaveStaticSettingsToDatabaseAsync(options, cancellationToken);
if (CancellationTokenProvider.Token.IsCancellationRequested)
{
return;
}
await PreCacheDynamicSettingsAsync(options);
}
}
catch
{
// No need to log here since inner calls log
}
}
protected virtual async Task SaveStaticSettingsToDatabaseAsync(
SettingManagementOptions options,
CancellationToken cancellationToken)
{
if (!options.SaveStaticSettingsToDatabase)
{
return;
}
await Policy
.Handle<Exception>()
.WaitAndRetryAsync(
8,
retryAttempt => TimeSpan.FromSeconds(
Volo.Abp.RandomHelper.GetRandom(
(int)Math.Pow(2, retryAttempt) * 8,
(int)Math.Pow(2, retryAttempt) * 12)
)
)
.ExecuteAsync(async _ =>
{
try
{
await StaticSettingSaver.SaveAsync();
}
catch (Exception ex)
{
Logger.LogException(ex);
throw; // Polly will catch it
}
}, cancellationToken);
}
protected virtual async Task PreCacheDynamicSettingsAsync(SettingManagementOptions options)
{
if (!options.IsDynamicSettingStoreEnabled)
{
return;
}
try
{
// Pre-cache settings, so first request doesn't wait
await DynamicSettingDefinitionStore.GetAllAsync();
}
catch (Exception ex)
{
Logger.LogException(ex);
throw; // It will be cached in Initialize()
}
}
}

32
modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/StaticSettingDefinitionChangedEventHandler.cs

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.Settings;
using Volo.Abp.StaticDefinitions;
using Volo.Abp.Threading;
namespace Volo.Abp.SettingManagement;
public class StaticSettingDefinitionChangedEventHandler : ILocalEventHandler<StaticSettingDefinitionChangedEvent>, ITransientDependency
{
protected IStaticDefinitionCache<SettingDefinition, Dictionary<string, SettingDefinition>> DefinitionCache { get; }
protected SettingDynamicInitializer SettingDynamicInitializer { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public StaticSettingDefinitionChangedEventHandler(
IStaticDefinitionCache<SettingDefinition, Dictionary<string, SettingDefinition>> definitionCache,
SettingDynamicInitializer settingDynamicInitializer,
ICancellationTokenProvider cancellationTokenProvider)
{
DefinitionCache = definitionCache;
SettingDynamicInitializer = settingDynamicInitializer;
CancellationTokenProvider = cancellationTokenProvider;
}
public virtual async Task HandleEventAsync(StaticSettingDefinitionChangedEvent eventData)
{
await DefinitionCache.ClearAsync();
await SettingDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token);
}
}

13
modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs

@ -18,11 +18,10 @@ public class SettingManager_Basic_Tests : SettingsTestBase
}
[Fact]
public async Task Should_Throw_Exception_When_Try_To_Get_An_Undefined_Setting()
public async Task Should_Return_Null_When_Try_To_Get_An_Undefined_Setting()
{
await Assert.ThrowsAsync<AbpException>(
async () => await _settingProvider.GetOrNullAsync("UndefinedSetting")
);
var value = await _settingProvider.GetOrNullAsync("UndefinedSetting");
value.ShouldBeNull();
}
[Fact]
@ -64,7 +63,7 @@ public class SettingManager_Basic_Tests : SettingsTestBase
(await _settingManager.GetOrNullGlobalAsync("MySetting1")).ShouldBe("43");
(await _settingProvider.GetOrNullAsync("MySetting1")).ShouldBe("43");
}
[Fact]
public async Task Set_Should_Throw_Exception_If_Provider_Not_Found()
{
@ -72,7 +71,7 @@ public class SettingManager_Basic_Tests : SettingsTestBase
{
await _settingManager.SetAsync("MySetting1", "43", "UndefinedProvider", "Test");
});
exception.Message.ShouldBe("Unknown setting value provider: UndefinedProvider");
}
}
}

32
text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Abp.StaticDefinitions;
using Volo.Abp.TextTemplating;
using Volo.Abp.Threading;
namespace Volo.Abp.TextTemplateManagement;
public class StaticTemplateDefinitionChangedEventHandler : ILocalEventHandler<StaticTemplateDefinitionChangedEvent>, ITransientDependency
{
protected IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> DefinitionCache { get; }
protected TextTemplateDynamicInitializer TextTemplateDynamicInitializer { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public StaticTemplateDefinitionChangedEventHandler(
IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> definitionCache,
TextTemplateDynamicInitializer textTemplateDynamicInitializer,
ICancellationTokenProvider cancellationTokenProvider)
{
DefinitionCache = definitionCache;
TextTemplateDynamicInitializer = textTemplateDynamicInitializer;
CancellationTokenProvider = cancellationTokenProvider;
}
public virtual async Task HandleEventAsync(StaticTemplateDefinitionChangedEvent eventData)
{
await DefinitionCache.ClearAsync();
await TextTemplateDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token);
}
}
Loading…
Cancel
Save