diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index f916789293..80d0acd30f 100644 --- a/.github/workflows/auto-pr.yml +++ b/.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 }} diff --git a/Directory.Packages.props b/Directory.Packages.props index a568b976e1..453ddc618f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ - @@ -141,7 +140,7 @@ - + diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index ffcaf9b805..b59620e72b 100644 --- a/docs/en/docs-nav.json +++ b/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": [ diff --git a/docs/en/framework/ui/common/leptonx-css-variables.md b/docs/en/framework/ui/common/leptonx-css-variables.md new file mode 100644 index 0000000000..193246a420 --- /dev/null +++ b/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 `` or ``): + +```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; +} +``` \ No newline at end of file diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png index adebe653b5..94dfb63a56 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png and b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-opening.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png index a0ecbd469d..e3032dfbde 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png and b/docs/en/tutorials/book-store-with-abp-suite/images/abp-suite-solution-test-projects-mongo.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png index 687dca3593..5c4a377b4a 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-angular.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png index 5535659b45..e690e9b0e9 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-blazor.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png index 1bf3253fe7..961e7565a7 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mauiblazor.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png index 54860a5e9f..7a2850afc2 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png and b/docs/en/tutorials/book-store-with-abp-suite/images/book-store-studio-run-app-mvc.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png b/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png index 47a37bbd9b..bf1394a0a2 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png and b/docs/en/tutorials/book-store-with-abp-suite/images/studio-browser-suite.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png b/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png index dbb1890c14..b1e4a7d06e 100644 Binary files a/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png and b/docs/en/tutorials/book-store-with-abp-suite/images/suite-book-entity-1.png differ diff --git a/docs/en/tutorials/book-store-with-abp-suite/part-02.md b/docs/en/tutorials/book-store-with-abp-suite/part-02.md index 11725201cb..f9462b9130 100644 --- a/docs/en/tutorials/book-store-with-abp-suite/part-02.md +++ b/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. diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs index ad2f6ba983..26e51dbd38 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor/Volo/Abp/AspNetCore/Components/MauiBlazor/MauiCurrentApplicationConfigurationCacheResetService.cs +++ b/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(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs index 02de9d8bf7..144d975cb5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo/Abp/AspNetCore/Components/Server/Configuration/BlazorServerCurrentApplicationConfigurationCacheResetService.cs +++ b/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)); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs index c3e33a9e41..2d44399f9d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/ICurrentApplicationConfigurationCacheResetService.cs +++ b/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); } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs index bb91d70775..1cfaee3315 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web/Volo/Abp/AspNetCore/Components/Web/Configuration/NullCurrentApplicationConfigurationCacheResetService.cs +++ b/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; } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs index 40ac508030..359678daf4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs +++ b/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(); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs index cc1180fd20..ea0d778fad 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs +++ b/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 ApplicationVersionCache { get; } + + public MvcCachedApplicationConfigurationClientHelper(IDistributedCache applicationVersionCache) + { + ApplicationVersionCache = applicationVersionCache; + } + + public virtual async Task 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}"; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs new file mode 100644 index 0000000000..1cd9990a44 --- /dev/null +++ b/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; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs index 8d787bec65..ba42b55d18 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/RemoteDynamicClaimsPrincipalContributorCache.cs +++ b/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 ApplicationConfigurationDtoCache { get; } + protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; } protected ICurrentUser CurrentUser { get; } public RemoteDynamicClaimsPrincipalContributorCache( @@ -28,7 +29,8 @@ public class RemoteDynamicClaimsPrincipalContributorCache : RemoteDynamicClaimsP IOptions abpClaimsPrincipalFactoryOptions, IRemoteServiceHttpClientAuthenticator httpClientAuthenticator, IDistributedCache 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 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; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs index 0bff9d09b1..e3d3c12370 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationConfigurationClient.cs +++ b/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 Cache { get; } protected AbpAspNetCoreMvcClientCacheOptions Options { get; } public MvcCachedApplicationConfigurationClient( + MvcCachedApplicationConfigurationClientHelper cacheHelper, IDistributedCache 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 GetAsync() + public virtual async Task 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 CreateCacheKeyAsync() { - return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser); + return await CacheHelper.CreateCacheKeyAsync(CurrentUser.Id); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs index 8bd3971779..c32b63249c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcCurrentApplicationConfigurationCacheResetEventHandler.cs +++ b/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, ITransientDependency { - protected ICurrentUser CurrentUser { get; } protected IDistributedCache Cache { get; } + protected IDistributedCache ApplicationVersionCache { get; } + protected MvcCachedApplicationConfigurationClientHelper CacheHelper { get; } - public MvcCurrentApplicationConfigurationCacheResetEventHandler(ICurrentUser currentUser, - IDistributedCache cache) + public MvcCurrentApplicationConfigurationCacheResetEventHandler( + IDistributedCache cache, + IDistributedCache 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); + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs index a50cb7b136..fccc295429 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/CurrentApplicationConfigurationCacheResetEventData.cs +++ b/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; /// /// This event is used to invalidate current user's cached configuration. /// public class CurrentApplicationConfigurationCacheResetEventData { + public Guid? UserId { get; set; } + + public CurrentApplicationConfigurationCacheResetEventData() + { + + } + public CurrentApplicationConfigurationCacheResetEventData(Guid? userId) + { + UserId = userId; + } } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionChangedEvent.cs new file mode 100644 index 0000000000..6bc7b5f165 --- /dev/null +++ b/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 +{ + +} diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs index 4e6ff0d11c..8329fde12e 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/StaticPermissionDefinitionStore.cs +++ b/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 PermissionGroupDefinitions => _lazyPermissionGroupDefinitions.Value; - private readonly Lazy> _lazyPermissionGroupDefinitions; - - protected IDictionary PermissionDefinitions => _lazyPermissionDefinitions.Value; - private readonly Lazy> _lazyPermissionDefinitions; - + protected IServiceProvider ServiceProvider { get; } protected AbpPermissionOptions Options { get; } - - private readonly IServiceProvider _serviceProvider; + protected IStaticDefinitionCache> GroupCache { get; } + protected IStaticDefinitionCache> DefinitionCache { get; } public StaticPermissionDefinitionStore( IServiceProvider serviceProvider, - IOptions options) + IOptions options, + IStaticDefinitionCache> groupCache, + IStaticDefinitionCache> definitionCache) { - _serviceProvider = serviceProvider; + ServiceProvider = serviceProvider; Options = options.Value; + GroupCache = groupCache; + DefinitionCache = definitionCache; + } - _lazyPermissionDefinitions = new Lazy>( - CreatePermissionDefinitions, - isThreadSafe: true - ); + public async Task GetOrNullAsync(string name) + { + var defs = await GetPermissionDefinitionsAsync(); + return defs.GetOrDefault(name); + } - _lazyPermissionGroupDefinitions = new Lazy>( - CreatePermissionGroupDefinitions, - isThreadSafe: true - ); + public virtual async Task> GetPermissionsAsync() + { + var defs = await GetPermissionDefinitionsAsync(); + return defs.Values.ToImmutableList(); + } + + public async Task> GetGroupsAsync() + { + var groups = await GetPermissionGroupDefinitionsAsync(); + return groups.Values.ToImmutableList(); + } + + protected virtual async Task> GetPermissionDefinitionsAsync() + { + return await DefinitionCache.GetOrCreateAsync(CreatePermissionDefinitionsAsync); } - - protected virtual Dictionary CreatePermissionDefinitions() + + protected virtual async Task> CreatePermissionDefinitionsAsync() { var permissions = new Dictionary(); - 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 CreatePermissionGroupDefinitions() + protected virtual async Task> GetPermissionGroupDefinitionsAsync() + { + return await GroupCache.GetOrCreateAsync(CreatePermissionGroupDefinitionsAsync); + } + + protected virtual Task> 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 GetOrNullAsync(string name) - { - return Task.FromResult(PermissionDefinitions.GetOrDefault(name)); - } - - public virtual Task> GetPermissionsAsync() - { - return Task.FromResult>( - PermissionDefinitions.Values.ToImmutableList() - ); - } - - public Task> GetGroupsAsync() - { - return Task.FromResult>( - PermissionGroupDefinitions.Values.ToImmutableList() - ); - } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs index be28f569ea..5ed5561e0e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Internal/InternalServiceCollectionExtensions.cs +++ b/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(); services.AddTransient(typeof(ISimpleStateCheckerManager<>), typeof(SimpleStateCheckerManager<>)); - + services.AddSingleton(typeof(IStaticDefinitionCache<,>), typeof(StaticDefinitionCache<,>)); services.Configure(options => { options.Contributors.Add(); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/IStaticDefinitionCache.cs b/framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/IStaticDefinitionCache.cs new file mode 100644 index 0000000000..0f4bb5b5a8 --- /dev/null +++ b/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 +{ + Task GetOrCreateAsync(Func> factory); + + Task ClearAsync(); +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/StaticDefinitionCache.cs b/framework/src/Volo.Abp.Core/Volo/Abp/StaticDefinitions/StaticDefinitionCache.cs new file mode 100644 index 0000000000..4f6a9bebe4 --- /dev/null +++ b/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 : IStaticDefinitionCache +{ + private Lazy>? _lazy; + + public virtual async Task GetOrCreateAsync(Func> factory) + { + var lazy = _lazy; + if (lazy != null) + { + return await lazy.Value; + } + + var newLazy = new Lazy>(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; + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs new file mode 100644 index 0000000000..7cab60c8d7 --- /dev/null +++ b/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; + +/// +/// Per-key asynchronous lock for coordinating concurrent flows. +/// +/// +/// Based on the pattern described in https://stackoverflow.com/a/31194647. +/// Use within a using scope to ensure the lock is released via IDisposable.Dispose(). +/// +public static class KeyedLock +{ + private static readonly Dictionary> SemaphoreSlims = new(); + + /// + /// Acquires an exclusive asynchronous lock for the specified . + /// This method waits until the lock becomes available. + /// + /// A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock. + /// An handle that must be disposed to release the lock. + /// Thrown when is . + /// + /// + /// var key = "my-critical-section"; + /// using (await KeyedLock.LockAsync(key)) + /// { + /// // protected work + /// } + /// + /// + public static async Task LockAsync(object key) + { + Check.NotNull(key, nameof(key)); + return await LockAsync(key, CancellationToken.None); + } + + /// + /// Acquires an exclusive asynchronous lock for the specified , observing a . + /// + /// A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock. + /// A token to cancel the wait for the lock. + /// An handle that must be disposed to release the lock. + /// Thrown when is . + /// Thrown if the wait is canceled via . + /// + /// + /// var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + /// using (await KeyedLock.LockAsync("db-update", cts.Token)) + /// { + /// // protected work + /// } + /// + /// + public static async Task 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); + } + + /// + /// Attempts to acquire an exclusive lock for the specified without waiting. + /// + /// A non-null object that identifies the lock. + /// + /// An handle if the lock was immediately acquired; otherwise . + /// + /// Thrown when is . + /// + /// + /// var handle = await KeyedLock.TryLockAsync("cache-key"); + /// if (handle != null) + /// { + /// using (handle) + /// { + /// // protected work + /// } + /// } + /// + /// + public static async Task TryLockAsync(object key) + { + Check.NotNull(key, nameof(key)); + return await TryLockAsync(key, default, CancellationToken.None); + } + + /// + /// Attempts to acquire an exclusive lock for the specified , waiting up to . + /// + /// A non-null object that identifies the lock. + /// Maximum time to wait for the lock. If set to , the method performs an immediate, non-blocking attempt. + /// A token to cancel the wait. + /// + /// An handle if the lock was acquired within the timeout; otherwise . + /// + /// Thrown when is . + /// Thrown if the wait is canceled via . + /// + /// + /// var handle = await KeyedLock.TryLockAsync("send-mail", TimeSpan.FromSeconds(1)); + /// if (handle != null) + /// { + /// using (handle) + /// { + /// // protected work + /// } + /// } + /// else + /// { + /// // lock not acquired within timeout + /// } + /// + /// + public static async Task 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 item; + lock (SemaphoreSlims) + { + if (SemaphoreSlims.TryGetValue(key, out item!)) + { + ++item.RefCount; + } + else + { + item = new RefCounted(new SemaphoreSlim(1, 1)); + SemaphoreSlims[key] = item; + } + } + return item.Value; + } + + private sealed class RefCounted(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 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? 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; + } +} diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj index 83f8f0076b..773c954051 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo.Abp.DistributedLocking.Abstractions.csproj @@ -18,7 +18,6 @@ - diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs index 15956b159e..5d12d0deeb 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs +++ b/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 _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 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); } } diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs new file mode 100644 index 0000000000..165aebc64a --- /dev/null +++ b/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; + +/// +/// This implementation of does not provide any distributed locking functionality. +/// Useful in scenarios where distributed locking is not required or during testing. +/// +public class NullAbpDistributedLock : IAbpDistributedLock +{ + public Task TryAcquireAsync(string name, TimeSpan timeout = default, CancellationToken cancellationToken = default) + { + return Task.FromResult(new LocalAbpDistributedLockHandle(NullDisposable.Instance)); + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs index 04eef96e6c..39f1cf5be8 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs @@ -28,7 +28,12 @@ public class FeatureChecker : FeatureCheckerBase public override async Task 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(); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionChangedEvent.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionChangedEvent.cs new file mode 100644 index 0000000000..e654516d98 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionChangedEvent.cs @@ -0,0 +1,9 @@ +using System; + +namespace Volo.Abp.Features; + +[Serializable] +public class StaticFeatureDefinitionChangedEvent +{ + +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs index d0647168d3..12c21b3987 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs +++ b/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 FeatureGroupDefinitions => _lazyFeatureGroupDefinitions.Value; - private readonly Lazy> _lazyFeatureGroupDefinitions; - - protected IDictionary FeatureDefinitions => _lazyFeatureDefinitions.Value; - private readonly Lazy> _lazyFeatureDefinitions; - + protected IServiceProvider ServiceProvider { get; } protected AbpFeatureOptions Options { get; } - - private readonly IServiceProvider _serviceProvider; + protected IStaticDefinitionCache> GroupCache { get; } + protected IStaticDefinitionCache> DefinitionCache { get; } public StaticFeatureDefinitionStore( + IServiceProvider serviceProvider, IOptions options, - IServiceProvider serviceProvider) + IStaticDefinitionCache> groupCache, + IStaticDefinitionCache> definitionCache) { - _serviceProvider = serviceProvider; + ServiceProvider = serviceProvider; Options = options.Value; - - _lazyFeatureDefinitions = new Lazy>( - CreateFeatureDefinitions, - isThreadSafe: true - ); - - _lazyFeatureGroupDefinitions = new Lazy>( - CreateFeatureGroupDefinitions, - isThreadSafe: true - ); + GroupCache = groupCache; + DefinitionCache = definitionCache; } public virtual async Task GetAsync(string name) @@ -52,43 +42,39 @@ public class StaticFeatureDefinitionStore: IStaticFeatureDefinitionStore, ISingl return feature; } - protected virtual Dictionary CreateFeatureDefinitions() + public virtual async Task GetOrNullAsync(string name) { - var features = new Dictionary(); - - 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> GetFeaturesAsync() + { + var defs = await GetFeatureDefinitionsAsync(); + return defs.Values.ToList(); } - protected virtual void AddFeatureToDictionaryRecursively( - Dictionary features, - FeatureDefinition feature) + public virtual async Task> 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> GetFeatureGroupDefinitionsAsync() + { + return await GroupCache.GetOrCreateAsync(CreateFeatureGroupDefinitionsAsync); + } - foreach (var child in feature.Children) - { - AddFeatureToDictionaryRecursively(features, child); - } + protected virtual async Task> GetFeatureDefinitionsAsync() + { + return await DefinitionCache.GetOrCreateAsync(CreateFeatureDefinitionsAsync); } - protected virtual Dictionary CreateFeatureGroupDefinitions() + protected virtual Task> 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 GetOrNullAsync(string name) + protected virtual async Task> CreateFeatureDefinitionsAsync() { - return Task.FromResult(FeatureDefinitions.GetOrDefault(name)); - } + var features = new Dictionary(); - public virtual Task> GetFeaturesAsync() - { - return Task.FromResult>(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> GetGroupsAsync() + protected virtual void AddFeatureToDictionaryRecursively( + Dictionary features, + FeatureDefinition feature) { - return Task.FromResult>(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); + } } } diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs index 2b0bdfcf9f..90c0527067 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs @@ -23,7 +23,12 @@ public class SettingProvider : ISettingProvider, ITransientDependency public virtual async Task 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); diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionChangedEvent.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionChangedEvent.cs new file mode 100644 index 0000000000..591ff88890 --- /dev/null +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionChangedEvent.cs @@ -0,0 +1,9 @@ +using System; + +namespace Volo.Abp.Settings; + +[Serializable] +public class StaticSettingDefinitionChangedEvent +{ + +} diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionStore.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionStore.cs index 653ee935f7..a35ed31b75 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/StaticSettingDefinitionStore.cs +++ b/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> SettingDefinitions { get; } - - protected AbpSettingOptions Options { get; } - protected IServiceProvider ServiceProvider { get; } + protected AbpSettingOptions Options { get; } + protected IStaticDefinitionCache> DefinitionCache { get; } - public StaticSettingDefinitionStore(IOptions options, IServiceProvider serviceProvider) + public StaticSettingDefinitionStore( + IServiceProvider serviceProvider, + IOptions options, + IStaticDefinitionCache> definitionCache) { ServiceProvider = serviceProvider; Options = options.Value; - - SettingDefinitions = new Lazy>(CreateSettingDefinitions, true); + DefinitionCache = definitionCache; } public virtual async Task GetAsync(string name) @@ -39,17 +40,24 @@ public class StaticSettingDefinitionStore : IStaticSettingDefinitionStore, ISing return setting; } - public virtual Task> GetAllAsync() + public virtual async Task> GetAllAsync() + { + var defs = await GetSettingDefinitionsAsync(); + return defs.Values.ToImmutableList(); + } + + public virtual async Task GetOrNullAsync(string name) { - return Task.FromResult>(SettingDefinitions.Value.Values.ToImmutableList()); + var defs = await GetSettingDefinitionsAsync(); + return defs.GetOrDefault(name); } - public virtual Task GetOrNullAsync(string name) + protected virtual async Task> GetSettingDefinitionsAsync() { - return Task.FromResult(SettingDefinitions.Value.GetOrDefault(name)); + return await DefinitionCache.GetOrCreateAsync(CreateSettingDefinitionsAsync); } - protected virtual IDictionary CreateSettingDefinitions() + protected virtual Task> CreateSettingDefinitionsAsync() { var settings = new Dictionary(); @@ -66,6 +74,6 @@ public class StaticSettingDefinitionStore : IStaticSettingDefinitionStore, ISing } } - return settings; + return Task.FromResult(settings); } } diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionChangedEvent.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionChangedEvent.cs new file mode 100644 index 0000000000..dd862b9c69 --- /dev/null +++ b/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 +{ + +} diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionStore.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionStore.cs index 4231f39b4d..083a111f87 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/StaticTemplateDefinitionStore.cs +++ b/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> TemplateDefinitions { get; } - - protected AbpTextTemplatingOptions Options { get; } - protected IServiceProvider ServiceProvider { get; } + protected AbpTextTemplatingOptions Options { get; } + protected IStaticDefinitionCache> DefinitionCache { get; } - public StaticTemplateDefinitionStore(IOptions options, IServiceProvider serviceProvider) + public StaticTemplateDefinitionStore( + IServiceProvider serviceProvider, + IOptions options, + IStaticDefinitionCache> definitionCache) { ServiceProvider = serviceProvider; Options = options.Value; - - TemplateDefinitions = new Lazy>(CreateTextTemplateDefinitions, true); + DefinitionCache = definitionCache; } - public virtual Task GetAsync(string name) + public virtual async Task 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> GetAllAsync() + { + var defs = await GetTemplateDefinitionsAsync(); + return defs.Values.ToImmutableList(); } - public virtual Task> GetAllAsync() + public virtual async Task GetOrNullAsync(string name) { - return Task.FromResult>(TemplateDefinitions.Value.Values.ToImmutableList()); + var defs = await GetTemplateDefinitionsAsync(); + return defs.GetOrDefault(name); } - public virtual Task GetOrNullAsync(string name) + protected virtual async Task> GetTemplateDefinitionsAsync() { - return Task.FromResult(TemplateDefinitions.Value.GetOrDefault(name)); + return await DefinitionCache.GetOrCreateAsync(CreateTextTemplateDefinitionsAsync); } - protected virtual IDictionary CreateTextTemplateDefinitions() + protected virtual Task> CreateTextTemplateDefinitionsAsync() { var templates = new Dictionary(); @@ -78,6 +86,6 @@ public class StaticTemplateDefinitionStore : IStaticTemplateDefinitionStore, ISi } } - return templates; + return Task.FromResult(templates); } } diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/StaticDefinitions/StaticDefinitionCache_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/StaticDefinitions/StaticDefinitionCache_Tests.cs new file mode 100644 index 0000000000..21ab9b8c59 --- /dev/null +++ b/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 +{ + protected readonly IStaticDefinitionCache> _staticDefinitionCache1; + protected readonly IStaticDefinitionCache> _staticDefinitionCache2; + + public StaticDefinitionCache_Tests() + { + _staticDefinitionCache1 = GetRequiredService>>(); + _staticDefinitionCache2 = GetRequiredService>>(); + } + + [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 { 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 { new StaticDefinition1 {Name = "Definition1", Value = 1} }); + }); + var definitions2 = await _staticDefinitionCache2.GetOrCreateAsync(() => + { + return Task.FromResult(new List { 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 { new StaticDefinition1 {Name = "Definition1", Value = 1} }); + }); + var definitions2 = await _staticDefinitionCache2.GetOrCreateAsync(() => + { + return Task.FromResult(new List { 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 { new StaticDefinition1 {Name = "DefinitionNew", Value = 10} }); + }); + var definitions2AfterClear = await _staticDefinitionCache2.GetOrCreateAsync(() => + { + return Task.FromResult(new List {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; } + } +} diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs new file mode 100644 index 0000000000..1477bd7dd5 --- /dev/null +++ b/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(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(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(); + } +} diff --git a/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/CmsKitMongoDbTestModule.cs b/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/CmsKitMongoDbTestModule.cs index 80ae993732..480d055cec 100644 --- a/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/MongoDB/CmsKitMongoDbTestModule.cs +++ b/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 { diff --git a/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/Volo.CmsKit.MongoDB.Tests.csproj b/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/Volo.CmsKit.MongoDB.Tests.csproj index dfa0e199b5..15222835c2 100644 --- a/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/Volo.CmsKit.MongoDB.Tests.csproj +++ b/modules/cms-kit/test/Volo.CmsKit.MongoDB.Tests/Volo.CmsKit.MongoDB.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs index 42ad413f3f..574cb43f73 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/AbpFeatureManagementDomainModule.cs +++ b/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(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(); + var initializer = rootServiceProvider.GetRequiredService(); + 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>() - .Value; - - if (!options.SaveStaticFeaturesToDatabase && !options.IsDynamicFeatureStoreEnabled) - { - return; - } - - var rootServiceProvider = context.ServiceProvider.GetRequiredService(); - - _initializeDynamicFeaturesTask = Task.Run(async () => - { - using var scope = rootServiceProvider.CreateScope(); - var applicationLifetime = scope.ServiceProvider.GetService(); - var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService(); - 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() - .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() - .SaveAsync(); - } - catch (Exception ex) - { - // ReSharper disable once AccessToDisposedClosure - scope.ServiceProvider - .GetService>()? - .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() - .GetGroupsAsync(); - } - catch (Exception ex) - { - // ReSharper disable once AccessToDisposedClosure - scope - .ServiceProvider - .GetService>()? - .LogException(ex); - - throw; // It will be cached in InitializeDynamicFeatures - } - } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDynamicInitializer.cs new file mode 100644 index 0000000000..32ed4cb646 --- /dev/null +++ b/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 Logger { get; set; } + + protected IServiceProvider ServiceProvider { get; } + protected IOptions Options { get; } + [CanBeNull] + protected IHostApplicationLifetime ApplicationLifetime { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IDynamicFeatureDefinitionStore DynamicFeatureDefinitionStore { get; } + protected IStaticFeatureSaver StaticFeatureSaver { get; } + + public FeatureDynamicInitializer( + IServiceProvider serviceProvider, + IOptions options, + ICancellationTokenProvider cancellationTokenProvider, + IDynamicFeatureDefinitionStore dynamicFeatureDefinitionStore, + IStaticFeatureSaver staticFeatureSaver) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + Options = options; + ApplicationLifetime = ServiceProvider.GetService(); + 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() + .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() + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureDefinitionChangedEventHandler.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureDefinitionChangedEventHandler.cs new file mode 100644 index 0000000000..b26b50d9b5 --- /dev/null +++ b/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, ITransientDependency +{ + protected IStaticDefinitionCache> GroupCache { get; } + protected IStaticDefinitionCache> DefinitionCache { get; } + protected FeatureDynamicInitializer FeatureDynamicInitializer { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + public StaticFeatureDefinitionChangedEventHandler( + IStaticDefinitionCache> groupCache, + IStaticDefinitionCache> 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); + } +} diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs index c370e5f558..71254e009f 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.EntityFrameworkCore.Tests/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreTestModule.cs +++ b/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().GetInitializeDynamicFeaturesTask(); - if (!task.IsCompleted) - { - AsyncHelper.RunSync(() => Awaited(task)); - } - } - - private async static Task Awaited(Task task) - { - await task; + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var initializer = rootServiceProvider.GetRequiredService(); + AsyncHelper.RunSync(() => initializer.InitializeAsync(false)); } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs index 2a618b9d4b..b3c917177b 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Components/PermissionManagementModal.razor.cs +++ b/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"]); diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs index 0f40e3c039..3029625ffb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs +++ b/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()); + if (context.Services.IsDataMigrationEnvironment()) { Configure(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(); + var initializer = rootServiceProvider.GetRequiredService(); + 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>() - .Value; - - if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled) - { - return; - } - - var rootServiceProvider = context.ServiceProvider.GetRequiredService(); - - _initializeDynamicPermissionsTask = Task.Run(async () => - { - using var scope = rootServiceProvider.CreateScope(); - var applicationLifetime = scope.ServiceProvider.GetService(); - var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService(); - 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() - .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() - .SaveAsync(); - } - catch (Exception ex) - { - // ReSharper disable once AccessToDisposedClosure - scope.ServiceProvider - .GetService>()? - .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() - .GetGroupsAsync(); - } - catch (Exception ex) - { - // ReSharper disable once AccessToDisposedClosure - scope - .ServiceProvider - .GetService>()? - .LogException(ex); - - throw; // It will be cached in InitializeDynamicPermissions - } - } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDynamicInitializer.cs new file mode 100644 index 0000000000..74bdf1821b --- /dev/null +++ b/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 Logger { get; set; } + + protected IServiceProvider ServiceProvider { get; } + protected IOptions Options { get; } + [CanBeNull] + protected IHostApplicationLifetime ApplicationLifetime { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IDynamicPermissionDefinitionStore DynamicPermissionDefinitionStore { get; } + protected IStaticPermissionSaver StaticPermissionSaver { get; } + + public PermissionDynamicInitializer( + IServiceProvider serviceProvider, + IOptions options, + ICancellationTokenProvider cancellationTokenProvider, + IDynamicPermissionDefinitionStore dynamicPermissionDefinitionStore, + IStaticPermissionSaver staticPermissionSaver) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + Options = options; + ApplicationLifetime = ServiceProvider.GetService(); + 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() + .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() + } + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/StaticPermissionDefinitionChangedEventHandler.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/StaticPermissionDefinitionChangedEventHandler.cs new file mode 100644 index 0000000000..ceff978a96 --- /dev/null +++ b/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, ITransientDependency +{ + protected IStaticDefinitionCache> GroupCache { get; } + protected IStaticDefinitionCache> DefinitionCache { get; } + protected PermissionDynamicInitializer PermissionDynamicInitializer { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + public StaticPermissionDefinitionChangedEventHandler( + IStaticDefinitionCache> groupCache, + IStaticDefinitionCache> 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); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs index 2c9d2dcc1c..6f6d6113e9 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs +++ b/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); } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs index 1e483fe1aa..7db8f8fe7e 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.EntityFrameworkCore.Tests/Volo/Abp/PermissionManagement/EntityFrameworkCore/AbpPermissionManagementEntityFrameworkCoreTestModule.cs +++ b/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().GetInitializeDynamicPermissionsTask(); - if (!task.IsCompleted) - { - AsyncHelper.RunSync(() => Awaited(task)); - } - } - - private async static Task Awaited(Task task) - { - await task; + var rootServiceProvider = context.ServiceProvider.GetRequiredService(); + var initializer = rootServiceProvider.GetRequiredService(); + AsyncHelper.RunSync(() => initializer.InitializeAsync(false)); } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/AbpSettingManagementDomainModule.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/AbpSettingManagementDomainModule.cs index 3f73342d0a..a5d2ba832e 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/AbpSettingManagementDomainModule.cs +++ b/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(); + var initializer = rootServiceProvider.GetRequiredService(); + 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>() - .Value; - - if (!options.SaveStaticSettingsToDatabase && !options.IsDynamicSettingStoreEnabled) - { - return; - } - - var rootServiceProvider = context.ServiceProvider.GetRequiredService(); - - _initializeDynamicSettingsTask = Task.Run(async () => - { - using var scope = rootServiceProvider.CreateScope(); - var applicationLifetime = scope.ServiceProvider.GetService(); - var cancellationTokenProvider = scope.ServiceProvider.GetRequiredService(); - 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() - .WaitAndRetryAsync(8, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt) * 10)) - .ExecuteAsync(async _ => - { - try - { - // ReSharper disable once AccessToDisposedClosure - await scope - .ServiceProvider - .GetRequiredService() - .SaveAsync(); - } - catch (Exception ex) - { - // ReSharper disable once AccessToDisposedClosure - scope.ServiceProvider - .GetService>()? - .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() - .GetAllAsync(); - } - catch (Exception ex) - { - // ReSharper disable once AccessToDisposedClosure - scope - .ServiceProvider - .GetService>()? - .LogException(ex); - - throw; // It will be cached in InitializeDynamicSettings - } - } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingDynamicInitializer.cs new file mode 100644 index 0000000000..41db669d9b --- /dev/null +++ b/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 Logger { get; set; } + + protected IServiceProvider ServiceProvider { get; } + protected IOptions Options { get; } + [CanBeNull] + protected IHostApplicationLifetime ApplicationLifetime { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected IDynamicSettingDefinitionStore DynamicSettingDefinitionStore { get; } + protected IStaticSettingSaver StaticSettingSaver { get; } + + public SettingDynamicInitializer( + IServiceProvider serviceProvider, + IOptions options, + ICancellationTokenProvider cancellationTokenProvider, + IDynamicSettingDefinitionStore dynamicSettingDefinitionStore, + IStaticSettingSaver staticSettingSaver) + { + Logger = NullLogger.Instance; + + ServiceProvider = serviceProvider; + Options = options; + ApplicationLifetime = ServiceProvider.GetService(); + 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() + .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() + } + } +} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/StaticSettingDefinitionChangedEventHandler.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/StaticSettingDefinitionChangedEventHandler.cs new file mode 100644 index 0000000000..cfc1058bb1 --- /dev/null +++ b/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, ITransientDependency +{ + protected IStaticDefinitionCache> DefinitionCache { get; } + protected SettingDynamicInitializer SettingDynamicInitializer { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + public StaticSettingDefinitionChangedEventHandler( + IStaticDefinitionCache> 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); + } +} diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs index ca9db64c70..95691f53a9 100644 --- a/modules/setting-management/test/Volo.Abp.SettingManagement.Tests/Volo/Abp/SettingManagement/SettingManager_Basic_Tests.cs +++ b/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( - 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"); } -} \ No newline at end of file +} diff --git a/text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs b/text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs new file mode 100644 index 0000000000..86b498ed90 --- /dev/null +++ b/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, ITransientDependency +{ + protected IStaticDefinitionCache> DefinitionCache { get; } + protected TextTemplateDynamicInitializer TextTemplateDynamicInitializer { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + + public StaticTemplateDefinitionChangedEventHandler( + IStaticDefinitionCache> 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); + } +}