From fcf835a8af7f3ef67281080206b2f4526a870622 Mon Sep 17 00:00:00 2001 From: Mark Cilia Vincenti Date: Mon, 8 Dec 2025 20:02:26 +0100 Subject: [PATCH 01/35] Use frozen collections where suitable --- .../AbpDatePickerBaseTagHelperService.cs | 31 ++++++++++--------- ...UiObjectExtensionPropertyInfoExtensions.cs | 5 +-- .../AbpBackgroundWorkersTickerQOptions.cs | 8 ++--- ...UiObjectExtensionPropertyInfoExtensions.cs | 9 +++--- .../ProjectModification/NpmPackagesUpdater.cs | 7 +---- .../Abp/Logging/DefaultInitLoggerFactory.cs | 4 +-- .../Volo/Abp/Reflection/TypeHelper.cs | 9 +++--- ...PostConfigureAbpRabbitMqEventBusOptions.cs | 13 ++++---- .../Generators/ProxyScriptingJsFuncHelper.cs | 5 +-- .../LocalizationResourceDictionary.cs | 2 +- .../MemoryDb/MemoryDatabaseCollection.cs | 2 +- .../Volo/Abp/MongoDB/MongoModelBuilder.cs | 2 +- .../Abp/Specifications/ParameterRebinder.cs | 2 +- .../MultiLingualObjectManager_Tests.cs | 5 +-- .../app/VoloDocs.Web/Pages/Error.cshtml.cs | 5 +-- 15 files changed, 57 insertions(+), 52 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs index 5088c08293..3f7b6b0afa 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/DatePicker/AbpDatePickerBaseTagHelperService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -23,7 +24,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form.DatePicker; public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelperService where TTagHelper : AbpDatePickerBaseTagHelper { - protected readonly Dictionary> SupportedInputTypes; + protected readonly FrozenDictionary> SupportedInputTypes; protected readonly IJsonSerializer JsonSerializer; protected readonly IHtmlGenerator Generator; @@ -103,7 +104,7 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp return string.Empty; } } - }; + }.ToFrozenDictionary(); } protected virtual T? GetAttribute() where T : Attribute @@ -136,7 +137,7 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp ? await ProcessButtonAndGetContentAsync(context, output, "calendar", "open") : ""; var clearButtonContent = TagHelper.ClearButton == true || (!TagHelper.ClearButton.HasValue && TagHelper.AutoUpdateInput != true) - ? await ProcessButtonAndGetContentAsync(context, output, "times", "clear", visible:!TagHelper.SingleOpenAndClearButton) + ? await ProcessButtonAndGetContentAsync(context, output, "times", "clear", visible: !TagHelper.SingleOpenAndClearButton) : ""; var labelContent = await GetLabelAsHtmlAsync(context, output, TagHelperOutput); @@ -269,7 +270,7 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp { var attrList = new TagHelperAttributeList(); - if(options == null) + if (options == null) { return attrList; } @@ -401,29 +402,29 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp attrList.Add("data-visible-date-format", options.VisibleDateFormat); } - if(!options.InputDateFormat.IsNullOrEmpty()) + if (!options.InputDateFormat.IsNullOrEmpty()) { attrList.Add("data-input-date-format", options.InputDateFormat); } - if(options.Ranges != null && options.Ranges.Any()) + if (options.Ranges != null && options.Ranges.Any()) { var ranges = options.Ranges.ToDictionary(r => r.Label, r => r.Dates); attrList.Add("data-ranges", JsonSerializer.Serialize(ranges)); } - if(options.AlwaysShowCalendars != null) + if (options.AlwaysShowCalendars != null) { attrList.Add("data-always-show-calendars", options.AlwaysShowCalendars.ToString()!.ToLowerInvariant()); } - if(options.ShowCustomRangeLabel == false) + if (options.ShowCustomRangeLabel == false) { attrList.Add("data-show-custom-range-label", options.ShowCustomRangeLabel.ToString()!.ToLowerInvariant()); } - if(options.Options != null) + if (options.Options != null) { attrList.Add("data-options", JsonSerializer.Serialize(options.Options)); } @@ -443,7 +444,7 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp attrList.Add("id", options.PickerId); } - if(!options.SingleOpenAndClearButton) + if (!options.SingleOpenAndClearButton) { attrList.Add("data-single-open-and-clear-button", options.SingleOpenAndClearButton.ToString().ToLowerInvariant()); } @@ -614,7 +615,8 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp { return string.Empty; } - var labelTagHelper = new LabelTagHelper(Generator) { + var labelTagHelper = new LabelTagHelper(Generator) + { ViewContext = TagHelper.ViewContext, For = modelExpression }; @@ -764,7 +766,8 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp TagHelper.Size = attribute.Size; } - return TagHelper.Size switch { + return TagHelper.Size switch + { AbpFormControlSize.Small => "form-control-sm", AbpFormControlSize.Medium => "form-control-md", AbpFormControlSize.Large => "form-control-lg", @@ -785,14 +788,14 @@ public abstract class AbpDatePickerBaseTagHelperService : AbpTagHelp protected virtual async Task GetValidationAsHtmlByInputAsync(TagHelperContext context, TagHelperOutput output, - [NotNull]ModelExpression @for) + [NotNull] ModelExpression @for) { var validationMessageTagHelper = new ValidationMessageTagHelper(Generator) { For = @for, ViewContext = TagHelper.ViewContext }; var attributeList = new TagHelperAttributeList { { "class", "text-danger" } }; - if(!output.Attributes.TryGetAttribute("name", out var nameAttribute) || nameAttribute == null || nameAttribute.Value == null) + if (!output.Attributes.TryGetAttribute("name", out var nameAttribute) || nameAttribute == null || nameAttribute.Value == null) { if (nameAttribute != null) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs index 62b86bc1c5..ae5d30a2ac 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; @@ -8,7 +9,7 @@ namespace Volo.Abp.ObjectExtending; public static class MvcUiObjectExtensionPropertyInfoExtensions { - private static readonly HashSet NumberTypes = new HashSet { + private static readonly FrozenSet NumberTypes = new HashSet { typeof(int), typeof(long), typeof(byte), @@ -33,7 +34,7 @@ public static class MvcUiObjectExtensionPropertyInfoExtensions typeof(float?), typeof(double?), typeof(decimal?) - }; + }.ToFrozenSet(); public static string? GetInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) { diff --git a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQOptions.cs b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQOptions.cs index 6d48a21262..438686d29f 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQOptions.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers.TickerQ/Volo/Abp/BackgroundWorkers/TickerQ/AbpBackgroundWorkersTickerQOptions.cs @@ -5,11 +5,11 @@ namespace Volo.Abp.BackgroundWorkers.TickerQ; public class AbpBackgroundWorkersTickerQOptions { - private readonly Dictionary _onfigurations; + private readonly Dictionary _configurations; public AbpBackgroundWorkersTickerQOptions() { - _onfigurations = new Dictionary(); + _configurations = new Dictionary(); } public void AddConfiguration(AbpBackgroundWorkersCronTickerConfiguration configuration) @@ -19,7 +19,7 @@ public class AbpBackgroundWorkersTickerQOptions public void AddConfiguration(Type workerType, AbpBackgroundWorkersCronTickerConfiguration configuration) { - _onfigurations[workerType] = configuration; + _configurations[workerType] = configuration; } public AbpBackgroundWorkersCronTickerConfiguration? GetConfigurationOrNull() @@ -29,6 +29,6 @@ public class AbpBackgroundWorkersTickerQOptions public AbpBackgroundWorkersCronTickerConfiguration? GetConfigurationOrNull(Type workerType) { - return _onfigurations.GetValueOrDefault(workerType); + return _configurations.GetValueOrDefault(workerType); } } diff --git a/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs index 3026682f62..d535c87c00 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs @@ -1,5 +1,6 @@ using Blazorise; using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -11,7 +12,7 @@ namespace Volo.Abp.BlazoriseUI; public static class BlazoriseUiObjectExtensionPropertyInfoExtensions { - private static readonly HashSet NumberTypes = new HashSet { + private static readonly FrozenSet NumberTypes = new HashSet { typeof(int), typeof(long), typeof(byte), @@ -36,13 +37,13 @@ public static class BlazoriseUiObjectExtensionPropertyInfoExtensions typeof(float?), typeof(double?), typeof(decimal?) - }; + }.ToFrozenSet(); - private static readonly HashSet TextEditSupportedAttributeTypes = new HashSet { + private static readonly FrozenSet TextEditSupportedAttributeTypes = new HashSet { typeof(EmailAddressAttribute), typeof(UrlAttribute), typeof(PhoneAttribute) - }; + }.ToFrozenSet(); public static string? GetDateEditInputFormatOrNull(this IBasicObjectExtensionPropertyInfo property) { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs index b8ba1ea113..9f2cb4d221 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs @@ -3,14 +3,12 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NuGet.Versioning; -using Volo.Abp.Cli.Http; using Volo.Abp.Cli.LIbs; using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; @@ -28,14 +26,12 @@ public class NpmPackagesUpdater : ITransientDependency private readonly PackageJsonFileFinder _packageJsonFileFinder; private readonly NpmGlobalPackagesChecker _npmGlobalPackagesChecker; - private readonly Dictionary _fileVersionStorage = new Dictionary(); - private readonly CliHttpClientFactory _cliHttpClientFactory; + private readonly Dictionary _fileVersionStorage = []; public NpmPackagesUpdater( PackageJsonFileFinder packageJsonFileFinder, NpmGlobalPackagesChecker npmGlobalPackagesChecker, ICancellationTokenProvider cancellationTokenProvider, - CliHttpClientFactory cliHttpClientFactory, IInstallLibsService installLibsService, ICmdHelper cmdHelper) { @@ -44,7 +40,6 @@ public class NpmPackagesUpdater : ITransientDependency CancellationTokenProvider = cancellationTokenProvider; InstallLibsService = installLibsService; CmdHelper = cmdHelper; - _cliHttpClientFactory = cliHttpClientFactory; Logger = NullLogger.Instance; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs index 1fa5d45090..638888493d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Logging/DefaultInitLoggerFactory.cs @@ -5,10 +5,10 @@ namespace Volo.Abp.Logging; public class DefaultInitLoggerFactory : IInitLoggerFactory { - private readonly Dictionary _cache = new Dictionary(); + private readonly Dictionary _cache = []; public virtual IInitLogger Create() { - return (IInitLogger)_cache.GetOrAdd(typeof(T), () => new DefaultInitLogger()); ; + return (IInitLogger)_cache.GetOrAdd(typeof(T), () => new DefaultInitLogger()); } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 0118fd9874..87c9419cfa 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; @@ -13,14 +14,14 @@ namespace Volo.Abp.Reflection; public static class TypeHelper { - private static readonly HashSet FloatingTypes = new HashSet + private static readonly FrozenSet FloatingTypes = new HashSet { typeof(float), typeof(double), typeof(decimal) - }; + }.ToFrozenSet(); - private static readonly HashSet NonNullablePrimitiveTypes = new HashSet + private static readonly FrozenSet NonNullablePrimitiveTypes = new HashSet { typeof(byte), typeof(short), @@ -37,7 +38,7 @@ public static class TypeHelper typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid) - }; + }.ToFrozenSet(); public static bool IsNonNullablePrimitiveType(Type type) { diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs index f4e69c46c6..2c400e01da 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs @@ -1,3 +1,4 @@ +using System.Collections.Frozen; using System.Collections.Generic; using Microsoft.Extensions.Options; @@ -5,8 +6,8 @@ namespace Volo.Abp.EventBus.RabbitMq; public class PostConfigureAbpRabbitMqEventBusOptions : IPostConfigureOptions { - private readonly HashSet _uint64QueueArguments = - [ + private readonly FrozenSet _uint64QueueArguments = new HashSet() + { "x-delivery-limit", "x-expires", "x-message-ttl", @@ -16,12 +17,12 @@ public class PostConfigureAbpRabbitMqEventBusOptions : IPostConfigureOptions _boolQueueArguments = - [ + private readonly FrozenSet _boolQueueArguments = new HashSet() + { "x-single-active-consumer" - ]; + }.ToFrozenSet(); public virtual void PostConfigure(string? name, AbpRabbitMqEventBusOptions options) { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs index 33c5b0d05f..21547bd05d 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; using System.Text; @@ -10,7 +11,7 @@ internal static class ProxyScriptingJsFuncHelper { private const string ValidJsVariableNameChars = "abcdefghijklmnopqrstuxwvyzABCDEFGHIJKLMNOPQRSTUXWVYZ0123456789_"; - private static readonly HashSet ReservedWords = new HashSet { + private static readonly FrozenSet ReservedWords = new HashSet { "abstract", "else", "instanceof", @@ -71,7 +72,7 @@ internal static class ProxyScriptingJsFuncHelper "in", "static", "with" - }; + }.ToFrozenSet(); public static string NormalizeJsVariableName(string name, string additionalChars = "") { diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs index f6487a85b7..0b8d3d7f63 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceDictionary.cs @@ -6,7 +6,7 @@ namespace Volo.Abp.Localization; public class LocalizationResourceDictionary : Dictionary { - private readonly Dictionary _resourcesByTypes = new(); + private readonly Dictionary _resourcesByTypes = []; public LocalizationResource Add(string? defaultCultureName = null) { diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDatabaseCollection.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDatabaseCollection.cs index 3e08c63417..7f4f9dbcdf 100644 --- a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDatabaseCollection.cs +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDatabaseCollection.cs @@ -8,7 +8,7 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb; public class MemoryDatabaseCollection : IMemoryDatabaseCollection where TEntity : class, IEntity { - private readonly Dictionary _dictionary = new Dictionary(); + private readonly Dictionary _dictionary = []; private readonly IMemoryDbSerializer _memoryDbSerializer; diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoModelBuilder.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoModelBuilder.cs index d7ad79f7d0..a68f1584f8 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoModelBuilder.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoModelBuilder.cs @@ -17,7 +17,7 @@ public class MongoModelBuilder : IMongoModelBuilder { private readonly Dictionary _entityModelBuilders; - private static readonly object SyncObj = new object(); + private static readonly object SyncObj = new(); public MongoModelBuilder() { diff --git a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs index d3553369b8..be0d8ab3e9 100644 --- a/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs +++ b/framework/src/Volo.Abp.Specifications/Volo/Abp/Specifications/ParameterRebinder.cs @@ -15,7 +15,7 @@ internal class ParameterRebinder : ExpressionVisitor internal ParameterRebinder(Dictionary map) { - _map = map ?? new Dictionary(); + _map = map ?? []; } internal static Expression ReplaceParameters(Dictionary map, diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs index e442d80953..6b18822df3 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -18,12 +19,12 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest _books; private readonly IMapperAccessor _mapperAccessor; - private readonly Dictionary _testTranslations = new() + private readonly FrozenDictionary _testTranslations = new Dictionary() { ["ar"] = "C# التعمق في", ["zh-Hans"] = "深入理解C#", ["en"] = "C# in Depth" - }; + }.ToFrozenDictionary(); public MultiLingualObjectManager_Tests() { diff --git a/modules/docs/app/VoloDocs.Web/Pages/Error.cshtml.cs b/modules/docs/app/VoloDocs.Web/Pages/Error.cshtml.cs index 5e5c4f639b..88ed671381 100644 --- a/modules/docs/app/VoloDocs.Web/Pages/Error.cshtml.cs +++ b/modules/docs/app/VoloDocs.Web/Pages/Error.cshtml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Net; using Microsoft.AspNetCore.Diagnostics; @@ -48,7 +49,7 @@ namespace VoloDocs.Web.Pages #region Error Messages /*For more ASCII arts http://patorjk.com/software/taag/#p=display&h=0&f=Big&t=400*/ - private readonly Dictionary _errorMessages = new Dictionary + private readonly FrozenDictionary _errorMessages = new Dictionary { { 400, @" @@ -131,7 +132,7 @@ Ooops! Our server is experiencing a mild case of the hiccups." Looks like we're having some server issues." } - }; + }.ToFrozenDictionary(); #endregion } } \ No newline at end of file From 9db10bbd578390b459a3c6557bdc4b47e1d0db80 Mon Sep 17 00:00:00 2001 From: Mark Cilia Vincenti Date: Tue, 9 Dec 2025 08:21:21 +0100 Subject: [PATCH 02/35] Cleaning --- ...UiObjectExtensionPropertyInfoExtensions.cs | 3 +- ...UiObjectExtensionPropertyInfoExtensions.cs | 6 +- ...PostConfigureAbpRabbitMqEventBusOptions.cs | 32 ++-- .../Generators/ProxyScriptingJsFuncHelper.cs | 3 +- .../MultiLingualObjectManager_Tests.cs | 166 +++++++++--------- 5 files changed, 107 insertions(+), 103 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs index ae5d30a2ac..49ff15f9f5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI/Volo/Abp/ObjectExtending/MvcUiObjectExtensionPropertyInfoExtensions.cs @@ -9,7 +9,8 @@ namespace Volo.Abp.ObjectExtending; public static class MvcUiObjectExtensionPropertyInfoExtensions { - private static readonly FrozenSet NumberTypes = new HashSet { + private static readonly FrozenSet NumberTypes = new HashSet + { typeof(int), typeof(long), typeof(byte), diff --git a/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs b/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs index d535c87c00..5794021488 100644 --- a/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs +++ b/framework/src/Volo.Abp.BlazoriseUI/BlazoriseUiObjectExtensionPropertyInfoExtensions.cs @@ -12,7 +12,8 @@ namespace Volo.Abp.BlazoriseUI; public static class BlazoriseUiObjectExtensionPropertyInfoExtensions { - private static readonly FrozenSet NumberTypes = new HashSet { + private static readonly FrozenSet NumberTypes = new HashSet + { typeof(int), typeof(long), typeof(byte), @@ -39,7 +40,8 @@ public static class BlazoriseUiObjectExtensionPropertyInfoExtensions typeof(decimal?) }.ToFrozenSet(); - private static readonly FrozenSet TextEditSupportedAttributeTypes = new HashSet { + private static readonly FrozenSet TextEditSupportedAttributeTypes = new HashSet + { typeof(EmailAddressAttribute), typeof(UrlAttribute), typeof(PhoneAttribute) diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs index 2c400e01da..76a74e2b3e 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/PostConfigureAbpRabbitMqEventBusOptions.cs @@ -6,23 +6,23 @@ namespace Volo.Abp.EventBus.RabbitMq; public class PostConfigureAbpRabbitMqEventBusOptions : IPostConfigureOptions { - private readonly FrozenSet _uint64QueueArguments = new HashSet() - { - "x-delivery-limit", - "x-expires", - "x-message-ttl", - "x-max-length", - "x-max-length-bytes", - "x-quorum-initial-group-size", - "x-quorum-target-group-size", - "x-stream-filter-size-bytes", - "x-stream-max-segment-size-bytes", - }.ToFrozenSet(); + private readonly FrozenSet _uint64QueueArguments = new HashSet + { + "x-delivery-limit", + "x-expires", + "x-message-ttl", + "x-max-length", + "x-max-length-bytes", + "x-quorum-initial-group-size", + "x-quorum-target-group-size", + "x-stream-filter-size-bytes", + "x-stream-max-segment-size-bytes", + }.ToFrozenSet(); - private readonly FrozenSet _boolQueueArguments = new HashSet() - { - "x-single-active-consumer" - }.ToFrozenSet(); + private readonly FrozenSet _boolQueueArguments = new HashSet + { + "x-single-active-consumer" + }.ToFrozenSet(); public virtual void PostConfigure(string? name, AbpRabbitMqEventBusOptions options) { diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs index 21547bd05d..ebc296e697 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ProxyScriptingJsFuncHelper.cs @@ -11,7 +11,8 @@ internal static class ProxyScriptingJsFuncHelper { private const string ValidJsVariableNameChars = "abcdefghijklmnopqrstuxwvyzABCDEFGHIJKLMNOPQRSTUXWVYZ0123456789_"; - private static readonly FrozenSet ReservedWords = new HashSet { + private static readonly FrozenSet ReservedWords = new HashSet + { "abstract", "else", "instanceof", diff --git a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs index 6b18822df3..bb527d2fcd 100644 --- a/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs +++ b/framework/test/Volo.Abp.MultiLingualObjects.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs @@ -1,38 +1,38 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; +using System; +using System.Collections.Frozen; +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Shouldly; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; using Volo.Abp.AutoMapper; -using Volo.Abp.Localization; -using Volo.Abp.MultiLingualObjects.TestObjects; -using Volo.Abp.Testing; -using Xunit; - -namespace Volo.Abp.MultiLingualObjects; - -public class MultiLingualObjectManager_Tests : AbpIntegratedTest -{ - private readonly IMultiLingualObjectManager _multiLingualObjectManager; - private readonly MultiLingualBook _book; - private readonly List _books; - private readonly IMapperAccessor _mapperAccessor; - private readonly FrozenDictionary _testTranslations = new Dictionary() - { - ["ar"] = "C# التعمق في", - ["zh-Hans"] = "深入理解C#", - ["en"] = "C# in Depth" - }.ToFrozenDictionary(); +using Volo.Abp.Localization; +using Volo.Abp.MultiLingualObjects.TestObjects; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.MultiLingualObjects; + +public class MultiLingualObjectManager_Tests : AbpIntegratedTest +{ + private readonly IMultiLingualObjectManager _multiLingualObjectManager; + private readonly MultiLingualBook _book; + private readonly List _books; + private readonly IMapperAccessor _mapperAccessor; + private readonly FrozenDictionary _testTranslations = new Dictionary + { + ["ar"] = "C# التعمق في", + ["zh-Hans"] = "深入理解C#", + ["en"] = "C# in Depth" + }.ToFrozenDictionary(); - public MultiLingualObjectManager_Tests() - { + public MultiLingualObjectManager_Tests() + { _multiLingualObjectManager = ServiceProvider.GetRequiredService(); //Single Lookup - _book = GetTestBook("en", "zh-Hans"); - //Bulk lookup + _book = GetTestBook("en", "zh-Hans"); + //Bulk lookup _books = new List { //has no translations @@ -46,14 +46,14 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest(); + _mapperAccessor = ServiceProvider.GetRequiredService(); } MultiLingualBook GetTestBook(params string[] included) { - var id = Guid.NewGuid(); - //Single book - var res = new MultiLingualBook(id, 100); - + var id = Guid.NewGuid(); + //Single book + var res = new MultiLingualBook(id, 100); + foreach (var language in included) { res.Translations.Add(new MultiLingualBookTranslation @@ -66,45 +66,45 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest(_book); - translation.ShouldNotBeNull(); - translation.Name.ShouldBe(_testTranslations["en"]); - } - } - - [Fact] - public async Task GetTranslationFromListAsync() - { - using (CultureHelper.Use("en-us")) - { - var translation = await _multiLingualObjectManager.GetTranslationAsync(_book.Translations); - translation.ShouldNotBeNull(); - translation.Name.ShouldBe(_testTranslations["en"]); - } - } - - [Fact] - public async Task Should_Get_Specified_Language() - { - using (CultureHelper.Use("zh-Hans")) - { - var translation = await _multiLingualObjectManager.GetTranslationAsync(_book, culture: "en"); - translation.ShouldNotBeNull(); - translation.Name.ShouldBe(_testTranslations["en"]); - } + [Fact] + public async Task GetTranslationAsync() + { + using (CultureHelper.Use("en-us")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task GetTranslationFromListAsync() + { + using (CultureHelper.Use("en-us")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book.Translations); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task Should_Get_Specified_Language() + { + using (CultureHelper.Use("zh-Hans")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book, culture: "en"); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } } - [Fact] - public async Task GetBulkTranslationsAsync() - { - using (CultureHelper.Use("en-us")) - { + [Fact] + public async Task GetBulkTranslationsAsync() + { + using (CultureHelper.Use("en-us")) + { var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); foreach (var (entity, translation) in translations) { @@ -118,26 +118,26 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest x.Translations)); foreach (var translation in translations) { translation?.Name.ShouldBe(_testTranslations["en"]); } - } - } - - [Fact] + } + } + + [Fact] public async Task TestBulkMapping() { - using (CultureHelper.Use("en-us")) + using (CultureHelper.Use("en-us")) { var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); var translationsDict = translations.ToDictionary(x => x.entity.Id, x => x.translation); @@ -153,5 +153,5 @@ public class MultiLingualObjectManager_Tests : AbpIntegratedTest x.Language == "en")?.Name, m.Name); } } - } -} + } +} From 0294c80bccc542339a2d4e029c11ceb95374a626 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 15 Dec 2025 16:50:45 +0800 Subject: [PATCH 03/35] Remove AsyncKeyedLock dependency and refactor locking --- Directory.Packages.props | 1 - ...Abp.DistributedLocking.Abstractions.csproj | 1 - .../LocalAbpDistributedLock.cs | 19 +++++-------------- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 9a1fcbb674..ef813ad076 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ - 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..d8d8eb9d09 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,14 @@ using System; -using System.Runtime.CompilerServices; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -using AsyncKeyedLock; using Volo.Abp.DependencyInjection; namespace Volo.Abp.DistributedLocking; public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency { - private readonly AsyncKeyedLocker _localSyncObjects = new(o => - { - o.PoolSize = 20; - o.PoolInitialFill = 1; - }); + private readonly ConcurrentDictionary _localSyncObjects = new(); protected IDistributedLockKeyNormalizer DistributedLockKeyNormalizer { get; } public LocalAbpDistributedLock(IDistributedLockKeyNormalizer distributedLockKeyNormalizer) @@ -21,7 +16,6 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency DistributedLockKeyNormalizer = distributedLockKeyNormalizer; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public async Task TryAcquireAsync( string name, TimeSpan timeout = default, @@ -30,11 +24,8 @@ 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) - { - return new LocalAbpDistributedLockHandle(timeoutReleaser); - } - return null; + var semaphore = _localSyncObjects.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + var acquired = await semaphore.WaitAsync(timeout, cancellationToken); + return acquired ? new LocalAbpDistributedLockHandle(semaphore) : null; } } From 917069fdf59909457cd552b92b40f0d0eca8de39 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 15 Dec 2025 18:05:17 +0800 Subject: [PATCH 04/35] Refactor LocalAbpDistributedLockHandle to use SemaphoreSlim --- .../LocalAbpDistributedLockHandle.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs index d08451657e..5ffe95af5e 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs @@ -1,20 +1,21 @@ -using System; +using System.Threading; using System.Threading.Tasks; -namespace Volo.Abp.DistributedLocking; - -public class LocalAbpDistributedLockHandle : IAbpDistributedLockHandle +namespace Volo.Abp.DistributedLocking { - private readonly IDisposable _disposable; - - public LocalAbpDistributedLockHandle(IDisposable disposable) + public class LocalAbpDistributedLockHandle : IAbpDistributedLockHandle { - _disposable = disposable; - } + private readonly SemaphoreSlim _semaphore; - public ValueTask DisposeAsync() - { - _disposable.Dispose(); - return default; + public LocalAbpDistributedLockHandle(SemaphoreSlim semaphore) + { + _semaphore = semaphore; + } + + public ValueTask DisposeAsync() + { + _semaphore.Release(); + return default; + } } } From a565abc59a5edfa73c13bae2c2ecf4baf6e712bc Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 26 Dec 2025 16:54:28 +0800 Subject: [PATCH 05/35] Provide a way to clear the `application configuration` cache for all users. https://abp.io/support/questions/10259 --- ...plicationConfigurationCacheResetService.cs | 3 ++- ...plicationConfigurationCacheResetService.cs | 9 +++---- ...plicationConfigurationCacheResetService.cs | 5 ++-- ...plicationConfigurationCacheResetService.cs | 3 ++- ...plicationConfigurationCacheResetService.cs | 3 ++- ...hedApplicationConfigurationClientHelper.cs | 25 ++++++++++++----- .../MvcCachedApplicationVersionCacheItem.cs | 13 +++++++++ ...eDynamicClaimsPrincipalContributorCache.cs | 7 +++-- ...MvcCachedApplicationConfigurationClient.cs | 14 ++++++---- ...tionConfigurationCacheResetEventHandler.cs | 27 +++++++++++-------- ...icationConfigurationCacheResetEventData.cs | 14 +++++++++- .../PermissionManagementModal.razor.cs | 9 ++++++- .../PermissionManagementModal.cshtml.cs | 14 +++++++--- 13 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc.Client.Common/Volo/Abp/AspNetCore/Mvc/Client/MvcCachedApplicationVersionCacheItem.cs 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..1034c92c14 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..ca1566d4a6 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()))) ?? + new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString()); + var userKey = userId?.ToString("N") ?? "Anonymous"; + return $"ApplicationConfiguration_{appVersion}_{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..9f9205f92d 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; @@ -17,10 +18,12 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu 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,12 +36,13 @@ 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(); + var cacheKey = await CreateCacheKeyAsync(); var httpContext = HttpContextAccessor?.HttpContext; if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration) @@ -86,7 +90,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu public ApplicationConfigurationDto Get() { - var cacheKey = CreateCacheKey(); + var cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync); var httpContext = HttpContextAccessor?.HttpContext; if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration) @@ -97,8 +101,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/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.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml.cs index 2c9d2dcc1c..10e002d95f 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(ProviderName, 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); } } From e43308e9dc1ee6aabd2d4ad557744f7dc8be3bac Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 26 Dec 2025 17:02:20 +0800 Subject: [PATCH 06/35] Fix cache key to use app version string --- .../Mvc/Client/MvcCachedApplicationConfigurationClientHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ca1566d4a6..499fc8f646 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 @@ -21,6 +21,6 @@ public class MvcCachedApplicationConfigurationClientHelper : ITransientDependenc () => Task.FromResult(new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString()))) ?? new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString()); var userKey = userId?.ToString("N") ?? "Anonymous"; - return $"ApplicationConfiguration_{appVersion}_{userKey}_{CultureInfo.CurrentUICulture.Name}"; + return $"ApplicationConfiguration_{appVersion.Version}_{userKey}_{CultureInfo.CurrentUICulture.Name}"; } } From f8c240b42e81a2ee5f53cf34b7bdf6cca9457200 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 26 Dec 2025 17:23:21 +0800 Subject: [PATCH 07/35] Fix cache key and user ID parsing logic --- .../Client/MvcCachedApplicationConfigurationClientHelper.cs | 4 ++-- .../PermissionManagementModal.cshtml.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 499fc8f646..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 @@ -18,8 +18,8 @@ public class MvcCachedApplicationConfigurationClientHelper : ITransientDependenc public virtual async Task CreateCacheKeyAsync(Guid? userId) { var appVersion = await ApplicationVersionCache.GetOrAddAsync(MvcCachedApplicationVersionCacheItem.CacheKey, - () => Task.FromResult(new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString()))) ?? - new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString()); + () => 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/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 10e002d95f..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 @@ -108,7 +108,7 @@ public class PermissionManagementModal : AbpPageModel ); Guid? userId = null; - if (ProviderName == UserPermissionValueProvider.ProviderName && Guid.TryParse(ProviderName, out var parsedUserId)) + if (ProviderName == UserPermissionValueProvider.ProviderName && Guid.TryParse(ProviderKey, out var parsedUserId)) { userId = parsedUserId; } From 21b4ddd58c318c4fdfff827f3ad434ce6e4d33cc Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Fri, 26 Dec 2025 17:27:05 +0800 Subject: [PATCH 08/35] Format ResetAsync method declaration --- .../NullCurrentApplicationConfigurationCacheResetService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1034c92c14..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 @@ -6,7 +6,7 @@ namespace Volo.Abp.AspNetCore.Components.Web.Configuration; public class NullCurrentApplicationConfigurationCacheResetService : ICurrentApplicationConfigurationCacheResetService, ISingletonDependency { - public Task ResetAsync(Guid? userId = null) + public Task ResetAsync(Guid? userId = null) { return Task.CompletedTask; } From b9acc164d8bbd8e723afe42f91b5fbddeab85565 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 29 Dec 2025 19:21:28 +0800 Subject: [PATCH 09/35] Optimize cache key retrieval in configuration client --- ...MvcCachedApplicationConfigurationClient.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) 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 9f9205f92d..b4e81c986a 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 @@ -42,8 +42,21 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu public virtual async Task GetAsync() { - var cacheKey = await CreateCacheKeyAsync(); + string? cacheKey = null; var httpContext = HttpContextAccessor?.HttpContext; + if (httpContext != null && httpContext.Items["ApplicationConfigurationDto_CacheKey"] is string key) + { + cacheKey = key; + } + + if (cacheKey.IsNullOrWhiteSpace()) + { + cacheKey = await CreateCacheKeyAsync(); + if (httpContext != null) + { + httpContext.Items["ApplicationConfigurationDto_CacheKey"] = cacheKey; + } + } if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration) { @@ -90,8 +103,21 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu public ApplicationConfigurationDto Get() { - var cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync); + string? cacheKey = null; var httpContext = HttpContextAccessor?.HttpContext; + if (httpContext != null && httpContext.Items["ApplicationConfigurationDto_CacheKey"] is string key) + { + cacheKey = key; + } + + if (cacheKey.IsNullOrWhiteSpace()) + { + cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync); + if (httpContext != null) + { + httpContext.Items["ApplicationConfigurationDto_CacheKey"] = cacheKey; + } + } if (httpContext != null && httpContext.Items[cacheKey] is ApplicationConfigurationDto configuration) { From 3de1c83233f95cc5e661861731764d54c1ffa79c Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 29 Dec 2025 21:35:08 +0800 Subject: [PATCH 10/35] Add handler for static template definition changes --- ...icTemplateDefinitionChangedEventHandler.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 text-template-management/src/Volo.Abp.TextTemplateManagement.Domain/Volo/Abp/TextTemplateManagement/StaticTemplateDefinitionChangedEventHandler.cs 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); + } +} From 7002b5a510ed970d7afe0ca27617555cba2194c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Mon, 29 Dec 2025 16:52:52 +0300 Subject: [PATCH 11/35] Explain how to install an older version of ABP Studio --- docs/en/studio/installation.md | 8 +++++ .../switch-abp-studio-version.ps1 | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 diff --git a/docs/en/studio/installation.md b/docs/en/studio/installation.md index 6f12953f99..6b051b4b5c 100644 --- a/docs/en/studio/installation.md +++ b/docs/en/studio/installation.md @@ -65,3 +65,11 @@ When you see the "New Version Available" window, follow these steps to upgrade A 2. A progress indicator will display the download status. 3. Once the download is complete, a new modal will appear with the "Install and Relaunch" buttons. 4. Click on the "Install and Relaunch" button to complete the installation process. + +## Installing a Specific Version + +If you want to install an older version of ABP Studio, you can download [Abp Studio Version Switcher](https://github.com/abpframework/abp/tree/dev/tools/abp-studio-version-switcher/abp-studio-version-switcher.ps1) script and run it as shown below; + +```bash +./switch-abp-studio-version.ps1 2.0.0 +``` \ No newline at end of file diff --git a/tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 b/tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 new file mode 100644 index 0000000000..a5e1308245 --- /dev/null +++ b/tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 @@ -0,0 +1,33 @@ +param ( + [string]$version = "0.7.6", + [string]$channel = "beta" +) +$installdir = "$env:LOCALAPPDATA\abp-studio\" + +Write-Host "----------------------------------------" +Write-Host "Switching to ABP Studio version $version on channel $channel..." +Write-Host "----------------------------------------" +Write-Host "Installing to $installdir" + + +$url = "https://abp.io/api/abp-studio/download/r/windows/abp-studio-$version-$channel-full.nupkg" +$output = "abp-studio-$version-$channel-full.nupkg" + +$outputPath = "$installdir\packages\$output" + +if (Test-Path $outputPath) { + Write-Host "File $output already exists. Skipping download." +} else { + Write-Host "Downloading $url to $outputPath" + Invoke-WebRequest -Uri $url -OutFile $outputPath +} +Write-Host "----------------------------------------" + +$installdirUpdate = "$installdir\Update.exe" + +Write-Host "Running $installdirUpdate apply --package $outputPath" +Invoke-Expression "$installdirUpdate apply --package $outputPath" + +Write-Host "----------------------------------------" +Write-Host "ABP Studio version $version on channel $channel installed successfully." +Write-Host "----------------------------------------" \ No newline at end of file From c1d5856572cccf52c554c8495c66be2d11d8ab4f Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 29 Dec 2025 21:55:11 +0800 Subject: [PATCH 12/35] Refactor cache key usage in MvcCachedApplicationConfigurationClient --- .../Client/MvcCachedApplicationConfigurationClient.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 b4e81c986a..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 @@ -14,6 +14,8 @@ 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; } @@ -44,7 +46,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu { string? cacheKey = null; var httpContext = HttpContextAccessor?.HttpContext; - if (httpContext != null && httpContext.Items["ApplicationConfigurationDto_CacheKey"] is string key) + if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key) { cacheKey = key; } @@ -54,7 +56,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu cacheKey = await CreateCacheKeyAsync(); if (httpContext != null) { - httpContext.Items["ApplicationConfigurationDto_CacheKey"] = cacheKey; + httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey; } } @@ -105,7 +107,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu { string? cacheKey = null; var httpContext = HttpContextAccessor?.HttpContext; - if (httpContext != null && httpContext.Items["ApplicationConfigurationDto_CacheKey"] is string key) + if (httpContext != null && httpContext.Items[ApplicationConfigurationDtoCacheKey] is string key) { cacheKey = key; } @@ -115,7 +117,7 @@ public class MvcCachedApplicationConfigurationClient : ICachedApplicationConfigu cacheKey = AsyncHelper.RunSync(CreateCacheKeyAsync); if (httpContext != null) { - httpContext.Items["ApplicationConfigurationDto_CacheKey"] = cacheKey; + httpContext.Items[ApplicationConfigurationDtoCacheKey] = cacheKey; } } From d8441d7fc400f98f8648bde0942bfb888a7bab1a Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 10:10:56 +0800 Subject: [PATCH 13/35] Add KeyedLock for per-key async locking and update local distributed lock --- .../Volo/Abp/Threading/KeyedLock.cs | 152 +++++++++++++++ .../LocalAbpDistributedLock.cs | 13 +- .../LocalAbpDistributedLockHandle.cs | 26 ++- .../Volo/Abp/Threading/KeyedLock_Tests.cs | 179 ++++++++++++++++++ 4 files changed, 350 insertions(+), 20 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs create mode 100644 framework/test/Volo.Abp.Core.Tests/Volo/Abp/Threading/KeyedLock_Tests.cs 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..80e750ac33 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Threading; + +/// +/// Per-key asynchronous lock. +/// https://stackoverflow.com/a/31194647 +/// +public static class KeyedLock +{ + private static readonly Dictionary> SemaphoreSlims = new(); + + public static async Task LockAsync(object key) + { + return await LockAsync(key, CancellationToken.None).ConfigureAwait(false); + } + + public static async Task LockAsync(object key, CancellationToken cancellationToken) + { + var semaphore = GetOrCreate(key); + try + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + var toDispose = DecrementRefAndMaybeRemove(key); + toDispose?.Dispose(); + throw; + } + return new Releaser(key); + } + + public static async Task TryLockAsync(object key) + { + return await TryLockAsync(key, default, CancellationToken.None).ConfigureAwait(false); + } + + public static async Task TryLockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken = default) + { + var semaphore = GetOrCreate(key); + bool acquired; + try + { + if (timeout == default) + { + acquired = await semaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false); + } + else + { + acquired = await semaphore.WaitAsync(timeout, cancellationToken).ConfigureAwait(false); + } + } + 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 + { + public void Dispose() + { + RefCounted item; + lock (SemaphoreSlims) + { + if (!SemaphoreSlims.TryGetValue(key, out item!)) + { + return; + } + --item.RefCount; + } + item.Value.Release(); + + bool shouldDispose = false; + lock (SemaphoreSlims) + { + if (SemaphoreSlims.TryGetValue(key, out var current) && ReferenceEquals(current, item)) + { + if (item.RefCount == 0) + { + SemaphoreSlims.Remove(key); + shouldDispose = true; + } + } + } + + if (shouldDispose) + { + item.Value.Dispose(); + } + } + } + + 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/LocalAbpDistributedLock.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLock.cs index d8d8eb9d09..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,14 +1,13 @@ using System; -using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; namespace Volo.Abp.DistributedLocking; public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency { - private readonly ConcurrentDictionary _localSyncObjects = new(); protected IDistributedLockKeyNormalizer DistributedLockKeyNormalizer { get; } public LocalAbpDistributedLock(IDistributedLockKeyNormalizer distributedLockKeyNormalizer) @@ -23,9 +22,11 @@ public class LocalAbpDistributedLock : IAbpDistributedLock, ISingletonDependency { Check.NotNullOrWhiteSpace(name, nameof(name)); var key = DistributedLockKeyNormalizer.NormalizeKey(name); - - var semaphore = _localSyncObjects.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - var acquired = await semaphore.WaitAsync(timeout, cancellationToken); - return acquired ? new LocalAbpDistributedLockHandle(semaphore) : null; + var disposable = await KeyedLock.TryLockAsync(key, timeout, cancellationToken); + if (disposable == null) + { + return null; + } + return new LocalAbpDistributedLockHandle(disposable); } } diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs index 5ffe95af5e..70427db7b2 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs @@ -1,21 +1,19 @@ -using System.Threading; +using System; using System.Threading.Tasks; +using Volo.Abp.DistributedLocking; -namespace Volo.Abp.DistributedLocking +public class LocalAbpDistributedLockHandle : IAbpDistributedLockHandle { - public class LocalAbpDistributedLockHandle : IAbpDistributedLockHandle - { - private readonly SemaphoreSlim _semaphore; + private readonly IDisposable _disposable; - public LocalAbpDistributedLockHandle(SemaphoreSlim semaphore) - { - _semaphore = semaphore; - } + public LocalAbpDistributedLockHandle(IDisposable disposable) + { + _disposable = disposable; + } - public ValueTask DisposeAsync() - { - _semaphore.Release(); - return default; - } + public ValueTask DisposeAsync() + { + _disposable.Dispose(); + return default; } } 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..633ef3dd76 --- /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(150); + inner.IsCompleted.ShouldBeFalse(); + } + + // After releasing, inner should complete; elapsed >= hold time + await inner; + sw.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(150); + } + + [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(); + } +} From b66d595e5669e45095b6d4848e46552ef1356492 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 11:05:15 +0800 Subject: [PATCH 14/35] Refactor dynamic initializer task handling in management modules --- .../AbpFeatureManagementDomainModule.cs | 7 ------- .../FeatureDynamicInitializer.cs | 12 ++---------- ...tureManagementEntityFrameworkCoreTestModule.cs | 14 ++++---------- .../AbpPermissionManagementDomainModule.cs | 7 ------- .../PermissionDynamicInitializer.cs | 12 ++---------- ...sionManagementEntityFrameworkCoreTestModule.cs | 15 ++++----------- .../AbpSettingManagementDomainModule.cs | 7 ------- .../SettingDynamicInitializer.cs | 12 ++---------- 8 files changed, 14 insertions(+), 72 deletions(-) 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 001bbfe0d8..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 @@ -25,7 +25,6 @@ namespace Volo.Abp.FeatureManagement; public class AbpFeatureManagementDomainModule : AbpModule { private readonly CancellationTokenSource _cancellationTokenSource = new(); - private Task _initializeDynamicFeaturesTask; public override void ConfigureServices(ServiceConfigurationContext context) { @@ -64,7 +63,6 @@ public class AbpFeatureManagementDomainModule : AbpModule var rootServiceProvider = context.ServiceProvider.GetRequiredService(); var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(true, _cancellationTokenSource.Token); - _initializeDynamicFeaturesTask = initializer.GetInitializationTask(); } public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) @@ -72,9 +70,4 @@ public class AbpFeatureManagementDomainModule : AbpModule _cancellationTokenSource.Cancel(); return Task.CompletedTask; } - - public Task GetInitializeDynamicFeaturesTask() - { - return _initializeDynamicFeaturesTask ?? Task.CompletedTask; - } } 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 index 9f6820274c..32ed4cb646 100644 --- 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 @@ -16,8 +16,6 @@ namespace Volo.Abp.FeatureManagement; public class FeatureDynamicInitializer : ITransientDependency { - private Task _initializeDynamicFeaturesTask; - public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } @@ -56,7 +54,7 @@ public class FeatureDynamicInitializer : ITransientDependency if (runInBackground) { - _initializeDynamicFeaturesTask = Task.Run(async () => + Task.Run(async () => { if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) { @@ -67,13 +65,7 @@ public class FeatureDynamicInitializer : ITransientDependency return Task.CompletedTask; } - _initializeDynamicFeaturesTask = ExecuteInitializationAsync(options, cancellationToken); - return _initializeDynamicFeaturesTask; - } - - public virtual Task GetInitializationTask() - { - return _initializeDynamicFeaturesTask ?? Task.CompletedTask; + return ExecuteInitializationAsync(options, cancellationToken); } protected virtual async Task ExecuteInitializationAsync(FeatureManagementOptions options, CancellationToken cancellationToken) 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.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDomainModule.cs index e3825ff397..9014b04ac2 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 @@ -26,7 +26,6 @@ namespace Volo.Abp.PermissionManagement; public class AbpPermissionManagementDomainModule : AbpModule { private readonly CancellationTokenSource _cancellationTokenSource = new(); - private Task _initializeDynamicPermissionsTask; public override void ConfigureServices(ServiceConfigurationContext context) { @@ -50,7 +49,6 @@ public class AbpPermissionManagementDomainModule : AbpModule var rootServiceProvider = context.ServiceProvider.GetRequiredService(); var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(true, _cancellationTokenSource.Token); - _initializeDynamicPermissionsTask = initializer.GetInitializationTask(); } public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) @@ -58,9 +56,4 @@ public class AbpPermissionManagementDomainModule : AbpModule _cancellationTokenSource.Cancel(); return Task.CompletedTask; } - - public Task GetInitializeDynamicPermissionsTask() - { - return _initializeDynamicPermissionsTask ?? Task.CompletedTask; - } } 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 index e78885c07f..74bdf1821b 100644 --- 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 @@ -16,8 +16,6 @@ namespace Volo.Abp.PermissionManagement; public class PermissionDynamicInitializer : ITransientDependency { - private Task _initializeDynamicPermissionsTask; - public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } @@ -56,7 +54,7 @@ public class PermissionDynamicInitializer : ITransientDependency if (runInBackground) { - _initializeDynamicPermissionsTask = Task.Run(async () => + Task.Run(async () => { if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) { @@ -67,13 +65,7 @@ public class PermissionDynamicInitializer : ITransientDependency return Task.CompletedTask; } - _initializeDynamicPermissionsTask = ExecuteInitializationAsync(options, cancellationToken); - return _initializeDynamicPermissionsTask; - } - - public virtual Task GetInitializationTask() - { - return _initializeDynamicPermissionsTask ?? Task.CompletedTask; + return ExecuteInitializationAsync(options, cancellationToken); } protected virtual async Task ExecuteInitializationAsync(PermissionManagementOptions options, CancellationToken cancellationToken) 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 c3cd35daef..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) { @@ -58,7 +57,6 @@ public class AbpSettingManagementDomainModule : AbpModule var rootServiceProvider = context.ServiceProvider.GetRequiredService(); var initializer = rootServiceProvider.GetRequiredService(); await initializer.InitializeAsync(true, _cancellationTokenSource.Token); - _initializeDynamicSettingsTask = initializer.GetInitializationTask(); } public override Task OnApplicationShutdownAsync(ApplicationShutdownContext context) @@ -66,9 +64,4 @@ public class AbpSettingManagementDomainModule : AbpModule _cancellationTokenSource.Cancel(); return Task.CompletedTask; } - - public Task GetInitializeDynamicSettingsTask() - { - return _initializeDynamicSettingsTask ?? Task.CompletedTask; - } } 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 index 6f388c8de8..41db669d9b 100644 --- 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 @@ -16,8 +16,6 @@ namespace Volo.Abp.SettingManagement; public class SettingDynamicInitializer : ITransientDependency { - private Task _initializeDynamicSettingsTask; - public ILogger Logger { get; set; } protected IServiceProvider ServiceProvider { get; } @@ -56,7 +54,7 @@ public class SettingDynamicInitializer : ITransientDependency if (runInBackground) { - _initializeDynamicSettingsTask = Task.Run(async () => + Task.Run(async () => { if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) { @@ -68,13 +66,7 @@ public class SettingDynamicInitializer : ITransientDependency return Task.CompletedTask; } - _initializeDynamicSettingsTask = ExecuteInitializationAsync(options, cancellationToken); - return _initializeDynamicSettingsTask; - } - - public virtual Task GetInitializationTask() - { - return _initializeDynamicSettingsTask ?? Task.CompletedTask; + return ExecuteInitializationAsync(options, cancellationToken); } protected virtual async Task ExecuteInitializationAsync(SettingManagementOptions options, CancellationToken cancellationToken) From b06e8a938143fe9ddb641b6b1939dc19fb493b27 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 12:50:35 +0800 Subject: [PATCH 15/35] Refactor KeyedLock disposal logic and update tests --- .../Volo/Abp/Threading/KeyedLock.cs | 37 ++++++++++--------- .../Volo/Abp/Threading/KeyedLock_Tests.cs | 4 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs index 80e750ac33..b7871cb0e7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs @@ -15,7 +15,7 @@ public static class KeyedLock public static async Task LockAsync(object key) { - return await LockAsync(key, CancellationToken.None).ConfigureAwait(false); + return await LockAsync(key, CancellationToken.None); } public static async Task LockAsync(object key, CancellationToken cancellationToken) @@ -23,7 +23,7 @@ public static class KeyedLock var semaphore = GetOrCreate(key); try { - await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await semaphore.WaitAsync(cancellationToken); } catch (OperationCanceledException) { @@ -36,7 +36,7 @@ public static class KeyedLock public static async Task TryLockAsync(object key) { - return await TryLockAsync(key, default, CancellationToken.None).ConfigureAwait(false); + return await TryLockAsync(key, default, CancellationToken.None); } public static async Task TryLockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken = default) @@ -47,11 +47,11 @@ public static class KeyedLock { if (timeout == default) { - acquired = await semaphore.WaitAsync(0, cancellationToken).ConfigureAwait(false); + acquired = await semaphore.WaitAsync(0, cancellationToken); } else { - acquired = await semaphore.WaitAsync(timeout, cancellationToken).ConfigureAwait(false); + acquired = await semaphore.WaitAsync(timeout, cancellationToken); } } catch (OperationCanceledException) @@ -99,9 +99,17 @@ public static class KeyedLock 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!)) @@ -109,19 +117,10 @@ public static class KeyedLock return; } --item.RefCount; - } - item.Value.Release(); - - bool shouldDispose = false; - lock (SemaphoreSlims) - { - if (SemaphoreSlims.TryGetValue(key, out var current) && ReferenceEquals(current, item)) + if (item.RefCount == 0) { - if (item.RefCount == 0) - { - SemaphoreSlims.Remove(key); - shouldDispose = true; - } + SemaphoreSlims.Remove(key); + shouldDispose = true; } } @@ -129,6 +128,10 @@ public static class KeyedLock { item.Value.Dispose(); } + else + { + item.Value.Release(); + } } } 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 index 633ef3dd76..1477bd7dd5 100644 --- 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 @@ -56,13 +56,13 @@ public class KeyedLock_Tests }); // While holding the outer lock, inner waiter should not complete - await Task.Delay(150); + await Task.Delay(200); inner.IsCompleted.ShouldBeFalse(); } // After releasing, inner should complete; elapsed >= hold time await inner; - sw.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(150); + sw.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(180); } [Fact] From 068a568bcc368b4f19fc62a9d5e5b19b418e7ca1 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 13:07:30 +0800 Subject: [PATCH 16/35] Enhance KeyedLock with additional locking methods and improve documentation --- .../Volo/Abp/Threading/KeyedLock.cs | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs index b7871cb0e7..7cab60c8d7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Threading/KeyedLock.cs @@ -6,20 +6,58 @@ using System.Threading.Tasks; namespace Volo.Abp.Threading; /// -/// Per-key asynchronous lock. -/// https://stackoverflow.com/a/31194647 +/// 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 { @@ -34,13 +72,62 @@ public static class KeyedLock 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 From e1e1d146c867bc73727686a453f77e0c8fa1b5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Tue, 30 Dec 2025 08:10:30 +0300 Subject: [PATCH 17/35] Suggest AbpDevTools to switch ABP Studio version --- docs/en/studio/installation.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/en/studio/installation.md b/docs/en/studio/installation.md index 6b051b4b5c..2e1d061920 100644 --- a/docs/en/studio/installation.md +++ b/docs/en/studio/installation.md @@ -68,8 +68,4 @@ When you see the "New Version Available" window, follow these steps to upgrade A ## Installing a Specific Version -If you want to install an older version of ABP Studio, you can download [Abp Studio Version Switcher](https://github.com/abpframework/abp/tree/dev/tools/abp-studio-version-switcher/abp-studio-version-switcher.ps1) script and run it as shown below; - -```bash -./switch-abp-studio-version.ps1 2.0.0 -``` \ No newline at end of file +There is no official support for installing an older version of ABP Studio yet. But, if you want to install an older version of ABP Studio, you can use approach explanined here [https://github.com/enisn/AbpDevTools?tab=readme-ov-file#switch-abp-studio-version](https://github.com/enisn/AbpDevTools?tab=readme-ov-file#switch-abp-studio-version) \ No newline at end of file From 7733429c4e617e749db1f5310fab2d9087dde5b9 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 13:25:17 +0800 Subject: [PATCH 18/35] Fix namespace declaration in LocalAbpDistributedLockHandle --- .../Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs index 70427db7b2..d08451657e 100644 --- a/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs +++ b/framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/LocalAbpDistributedLockHandle.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; -using Volo.Abp.DistributedLocking; + +namespace Volo.Abp.DistributedLocking; public class LocalAbpDistributedLockHandle : IAbpDistributedLockHandle { From c7b08487abae39eb9f6731fececb3de19bdcc90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0smail=20=C3=87A=C4=9EDA=C5=9E?= Date: Tue, 30 Dec 2025 08:45:00 +0300 Subject: [PATCH 19/35] Delete not used script --- .../switch-abp-studio-version.ps1 | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 diff --git a/tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 b/tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 deleted file mode 100644 index a5e1308245..0000000000 --- a/tools/abp-studio-version-switcher/switch-abp-studio-version.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -param ( - [string]$version = "0.7.6", - [string]$channel = "beta" -) -$installdir = "$env:LOCALAPPDATA\abp-studio\" - -Write-Host "----------------------------------------" -Write-Host "Switching to ABP Studio version $version on channel $channel..." -Write-Host "----------------------------------------" -Write-Host "Installing to $installdir" - - -$url = "https://abp.io/api/abp-studio/download/r/windows/abp-studio-$version-$channel-full.nupkg" -$output = "abp-studio-$version-$channel-full.nupkg" - -$outputPath = "$installdir\packages\$output" - -if (Test-Path $outputPath) { - Write-Host "File $output already exists. Skipping download." -} else { - Write-Host "Downloading $url to $outputPath" - Invoke-WebRequest -Uri $url -OutFile $outputPath -} -Write-Host "----------------------------------------" - -$installdirUpdate = "$installdir\Update.exe" - -Write-Host "Running $installdirUpdate apply --package $outputPath" -Invoke-Expression "$installdirUpdate apply --package $outputPath" - -Write-Host "----------------------------------------" -Write-Host "ABP Studio version $version on channel $channel installed successfully." -Write-Host "----------------------------------------" \ No newline at end of file From 67310e726012c7559c501f7362d3a027bb87a664 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 14:34:12 +0800 Subject: [PATCH 20/35] Add NullAbpDistributedLock and register as default lock for unit test. --- .../NullAbpDistributedLock.cs | 17 +++++++++++++++++ .../AbpPermissionManagementDomainModule.cs | 4 ++++ 2 files changed, 21 insertions(+) create mode 100644 framework/src/Volo.Abp.DistributedLocking.Abstractions/Volo/Abp/DistributedLocking/NullAbpDistributedLock.cs 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/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 9014b04ac2..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; @@ -29,6 +31,8 @@ public class AbpPermissionManagementDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.Replace(ServiceDescriptor.Singleton()); + if (context.Services.IsDataMigrationEnvironment()) { Configure(options => From 45d300a01a32ca59a16db2948311b5eca9ce46b9 Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Tue, 30 Dec 2025 15:05:59 +0800 Subject: [PATCH 21/35] Update MySql.EntityFrameworkCore version to 10.0.0-rc https://www.nuget.org/packages/MySql.EntityFrameworkCore/10.0.0-rc --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8ee0831103..453ddc618f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -140,7 +140,7 @@ - + From 0c006810bd70d44d00345a52e2c77f43fbf2b5bc Mon Sep 17 00:00:00 2001 From: selman koc <64414348+skoc10@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:51:26 +0300 Subject: [PATCH 22/35] Update workflow to merge rel-10.1 with rel-10.0 --- .github/workflows/auto-pr.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 }} From 7be2bfa46ecf2208ee829cdf3841e918e5d58155 Mon Sep 17 00:00:00 2001 From: selman koc <64414348+skoc10@users.noreply.github.com> Date: Tue, 30 Dec 2025 10:55:56 +0300 Subject: [PATCH 23/35] Update GitHub Actions workflow for branch merging --- .github/workflows/auto-pr.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml index 80d0acd30f..4cb89fd21d 100644 --- a/.github/workflows/auto-pr.yml +++ b/.github/workflows/auto-pr.yml @@ -1,13 +1,13 @@ -name: Merge branch rel-10.1 with rel-10.0 +name: Merge branch dev with rel-10.1 on: push: branches: - - rel-10.0 + - rel-10.1 permissions: contents: read jobs: - merge-rel-10-1-with-rel-10-0: + merge-dev-with-rel-10-1: 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,17 +15,17 @@ jobs: steps: - uses: actions/checkout@v2 with: - ref: rel-10.1 + ref: dev - name: Reset promotion branch run: | - git fetch origin rel-10.0:rel-10.0 - git reset --hard rel-10.0 + git fetch origin rel-10.1:rel-10.1 + git reset --hard rel-10.1 - name: Create Pull Request uses: peter-evans/create-pull-request@v3 with: - branch: auto-merge/rel-10-0/${{github.run_number}} - 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. + branch: auto-merge/rel-10-1/${{github.run_number}} + title: Merge branch dev with rel-10.1 + body: This PR generated automatically to merge dev with rel-10.1. Please review the changed files before merging to prevent any errors that may occur. reviewers: maliming draft: true token: ${{ github.token }} @@ -34,5 +34,5 @@ jobs: GH_TOKEN: ${{ secrets.BOT_SECRET }} run: | gh pr ready - gh pr review auto-merge/rel-10-0/${{github.run_number}} --approve - gh pr merge auto-merge/rel-10-0/${{github.run_number}} --merge --auto --delete-branch + gh pr review auto-merge/rel-10-1/${{github.run_number}} --approve + gh pr merge auto-merge/rel-10-1/${{github.run_number}} --merge --auto --delete-branch From 12f4797179a927e906f9160d57e39e4d8d2b47a7 Mon Sep 17 00:00:00 2001 From: selman koc <64414348+skoc10@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:12:31 +0300 Subject: [PATCH 24/35] Update version for nightly build 10.2.0preview --- common.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common.props b/common.props index 42e230791c..2b274e5d75 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 10.1.0-preview - 5.1.0-preview + 10.2.0-preview + 5.2.0-preview $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ From 3de33e51eea72266c86c7297c83e1022d9d94491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Tue, 30 Dec 2025 12:48:49 +0300 Subject: [PATCH 25/35] Use GetOrNullAsync for feature definition lookup Replaces GetAsync with GetOrNullAsync when retrieving feature definitions in FeatureChecker. This prevents exceptions when a feature is not found and returns null instead, improving error handling. --- .../Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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(); From 5bf06b50f55e432b2372b72ba5f83f25c0aa9f16 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 17:58:40 +0800 Subject: [PATCH 26/35] Use GetOrNullAsync for setting definition lookup --- .../Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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); From f5a3526d7b0f746525e457878aa242021084ea05 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 18:44:30 +0800 Subject: [PATCH 27/35] Update test for undefined setting to expect null --- .../SettingManagement/SettingManager_Basic_Tests.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 +} From f253f9945e4be1b1c076aaefd5a5cbfb9fa2ab06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 15:19:43 +0300 Subject: [PATCH 28/35] Improve entity history handling for shared types and JSON Enhanced EntityHistoryHelper to correctly handle entities with shared CLR types and properties mapped to JSON. Added logic to determine property type from entry values when the type is object, and improved property change tracking for JSON-mapped navigation properties. --- .../EntityHistory/EntityHistoryHelper.cs | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index f87a05e4a9..c23eee6450 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -108,12 +109,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } var entityType = entity.GetType(); + var entityFullName = entityType.FullName!; + if (entityEntry.Metadata.HasSharedClrType) + { + entityFullName = entityEntry.Metadata.Name; + } var entityChange = new EntityChangeInfo { ChangeType = changeType, EntityEntry = entityEntry, EntityId = entityId, - EntityTypeFullName = entityType.FullName, + EntityTypeFullName = entityFullName, PropertyChanges = GetPropertyChanges(entityEntry), EntityTenantId = GetTenantId(entity) }; @@ -184,12 +190,14 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency var propertyEntry = entityEntry.Property(property.Name); if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry)) { + var propertyType = DeterminePropertyTypeFromEntry(property, propertyEntry); + propertyChanges.Add(new EntityPropertyChangeInfo { NewValue = isDeleted ? null : JsonSerializer.Serialize(propertyEntry.CurrentValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), OriginalValue = isCreated ? null : JsonSerializer.Serialize(propertyEntry.OriginalValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), PropertyName = property.Name, - PropertyTypeFullName = property.ClrType.GetFirstGenericArgumentIfNullable().FullName! + PropertyTypeFullName = propertyType.FullName! }); } } @@ -208,14 +216,22 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index); - var isCollection = navigationEntry.Metadata.IsCollection; - propertyChanges.Add(new EntityPropertyChangeInfo + if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) + { + var jsonPropertyChanges = GetPropertyChanges(referenceEntry.TargetEntry); + propertyChanges.AddRange(jsonPropertyChanges); + } + else { - PropertyName = navigationEntry.Metadata.Name, - PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, - OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), - NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) - }); + var isCollection = navigationEntry.Metadata.IsCollection; + propertyChanges.Add(new EntityPropertyChangeInfo + { + PropertyName = navigationEntry.Metadata.Name, + PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, + OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), + NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) + }); + } } } } @@ -223,6 +239,27 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) + { + var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); + + if (propertyType != typeof(object)) + { + return propertyType; + } + + if (propertyEntry.CurrentValue != null) + { + propertyType = propertyEntry.CurrentValue.GetType().GetFirstGenericArgumentIfNullable(); + } + else if (propertyEntry.OriginalValue != null) + { + propertyType = propertyEntry.OriginalValue.GetType().GetFirstGenericArgumentIfNullable(); + } + + return propertyType; + } + protected virtual string? GetNavigationPropertyValue(object? entity, bool isCollection) { switch (entity) From bdd0901dbad4618476f13642117a277c31317788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 15:41:38 +0300 Subject: [PATCH 29/35] Update framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../EntityHistory/EntityHistoryHelper.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index c23eee6450..7554c1d281 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -239,6 +239,18 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + /// + /// Determines the CLR type of a property based on its EF Core metadata and the values in the given . + /// + /// The EF Core property metadata that provides the declared CLR type. + /// The property entry that contains the current and original values for the property. + /// + /// The most specific CLR type inferred for the property. This is normally the property's declared CLR type (with + /// nullable wrappers removed). If the declared type is , the type is inferred from the + /// runtime type of or, if that is null, from + /// . If both values are null, the declared CLR type + /// (which may remain ) is returned. + /// protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) { var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); From d041aeb1fa2fc1b5dd231d32ab7d5ecd0c0e77d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 15:58:32 +0300 Subject: [PATCH 30/35] Improve JSON navigation property change tracking Refactored EntityHistoryHelper to better handle property changes for navigation properties mapped to JSON. Now, nested property changes are prefixed with the navigation property name, and duplicate property names are handled to avoid conflicts. --- .../EntityHistory/EntityHistoryHelper.cs | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index 7554c1d281..66e632dc8c 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -202,37 +202,47 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } - if (AbpEfCoreNavigationHelper != null) + if (AbpEfCoreNavigationHelper == null) { - foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) - { - var propertyInfo = navigationEntry.Metadata.PropertyInfo; - if (propertyInfo != null && - propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) - { - continue; - } + return propertyChanges; + } - if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) + foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) + { + var propertyInfo = navigationEntry.Metadata.PropertyInfo; + if (propertyInfo != null && + propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + continue; + } + + if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) + { + foreach (var propertyChange in GetPropertyChanges(referenceEntry.TargetEntry)) { - var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index); - if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) - { - var jsonPropertyChanges = GetPropertyChanges(referenceEntry.TargetEntry); - propertyChanges.AddRange(jsonPropertyChanges); - } - else + if (propertyChanges.Any(pc => pc.PropertyName == propertyChange.PropertyName)) { - var isCollection = navigationEntry.Metadata.IsCollection; - propertyChanges.Add(new EntityPropertyChangeInfo - { - PropertyName = navigationEntry.Metadata.Name, - PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, - OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), - NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) - }); + propertyChange.PropertyName = $"{referenceEntry.Metadata.Name}.{propertyChange.PropertyName}"; } + + propertyChanges.Add(propertyChange); } + + continue; + } + + if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) + { + var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index); + + var isCollection = navigationEntry.Metadata.IsCollection; + propertyChanges.Add(new EntityPropertyChangeInfo + { + PropertyName = navigationEntry.Metadata.Name, + PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, + OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), + NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) + }); } } From abe5a5c18b1f0426f123216fa21d508cc72571d6 Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Tue, 30 Dec 2025 21:02:38 +0800 Subject: [PATCH 31/35] Fix resource display name in modal header --- .../ResourcePermissionManagementModal.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml index 5717448c70..15a5c0bff4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/ResourcePermissionManagementModal.cshtml @@ -34,7 +34,7 @@ else { - +