From 2013c58c5d053a18452a1cc259d932a42a55f64f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 12:25:29 +0300 Subject: [PATCH 1/3] docs(studio): release 3.0.2 - 3.0.2 (#25411) * docs(studio): update documentation for release 3.0.2 - Updated release notes for 3.0.2 - Updated version mapping with ABP 10.3.0 Release: 3.0.2 * Revise release notes for version 3.0.2 Updated release notes for version 3.0.2 to include major features and enhancements. * Update version-mapping.md --------- Co-authored-by: github-actions[bot] Co-authored-by: selman koc <64414348+skoc10@users.noreply.github.com> --- docs/en/studio/release-notes.md | 13 ++++++++++++- docs/en/studio/version-mapping.md | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index dcfa904fe0..430bd1e892 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -9,7 +9,18 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. -## 3.0.1 (2026-05-06) Latest +## 3.0.2 (2026-05-12) Latest + +* Modern template osx fix +* Update dark theme - BackgroundColorLighter +* Fix initial task warning handling +* Abp Studio ai agent cont +* Add Volo.Abp.Elsa to module list +* Register reCAPTCHA in HttpApi host templates for CmsKit contact endpoint +* Optimized system prompt in ABP Studio +* Register `MVC.RootUrl` in `HttpApi.Host` template + +## 3.0.1 (2026-05-06) * Enhanced Project Wizard: Fixed the optional module selection step for a smoother project setup experience * React Admin Console Improvements: Resolved identified issues in the React administration templates for better stability diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md index 209413d5dd..f16bb25f7a 100644 --- a/docs/en/studio/version-mapping.md +++ b/docs/en/studio/version-mapping.md @@ -11,7 +11,7 @@ This document provides a general overview of the relationship between various ve | **ABP Studio Version** | **ABP Version of Startup Template** | |------------------------|---------------------------| -| 2.2.7 - 3.0.1 | 10.3.0 | +| 2.2.7 - 3.0.2 | 10.3.0 | | 2.2.5 - 2.2.6 | 10.2.0 | | 2.2.2 - 2.2.4 | 10.1.1 | | 2.2.1 | 10.1.0 | From a2eaaae99407c77493508c04dc04f80b79313e04 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 13 May 2026 21:24:09 +0800 Subject: [PATCH 2/3] Serialize batch state checkers per permission in dynamic distribution - Add state-aware overload to ISimpleStateCheckerSerializer/Contributor - Features and Permissions contributors recognise their batch checker and emit a per-state record - PermissionDefinitionSerializer threads the owning permission through - Pin equality semantics to match the batch runtime (default comparer) --- ...SimpleStateCheckerSerializerContributor.cs | 6 +- ...SimpleStateCheckerSerializerContributor.cs | 24 ++++++- ...quirePermissionsSimpleBatchStateChecker.cs | 5 ++ .../ISimpleStateCheckerSerializer.cs | 4 ++ ...SimpleStateCheckerSerializerContributor.cs | 3 + .../SimpleStateCheckerSerializer.cs | 17 ++++- .../SimpleStateCheckerSerializerExtensions.cs | 25 +++++-- ...SimpleStateCheckerSerializerContributor.cs | 24 ++++++- .../RequireFeaturesSimpleBatchStateChecker.cs | 5 ++ ...SimpleStateCheckerSerializerContributor.cs | 4 ++ ...reFeaturesSimpleBatchStateChecker_Tests.cs | 35 ++++++++++ .../PermissionDefinitionSerializer.cs | 8 ++- .../PermissionDefinitionSerializer_Tests.cs | 68 +++++++++++++++++++ 13 files changed, 213 insertions(+), 15 deletions(-) diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs index d23273e7a2..13c6f92d94 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/AuthenticatedSimpleStateCheckerSerializerContributor.cs @@ -10,7 +10,7 @@ public class AuthenticatedSimpleStateCheckerSerializerContributor : { public const string CheckerShortName = "A"; - public string? SerializeToJson(ISimpleStateChecker checker) + public string? SerializeToJson(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers { if (checker is not RequireAuthenticatedSimpleStateChecker) @@ -25,6 +25,10 @@ public class AuthenticatedSimpleStateCheckerSerializerContributor : return jsonObject.ToJsonString(); } + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + => SerializeToJson(checker); + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers { diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs index 1a06b83ed7..61978be6c4 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionsSimpleStateCheckerSerializerContributor.cs @@ -20,13 +20,31 @@ public class PermissionsSimpleStateCheckerSerializerContributor : return null; } - var jsonObject = new JsonObject { + return BuildJson(permissionsSimpleStateChecker.RequiresAll, permissionsSimpleStateChecker.PermissionNames); + } + + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + { + if (checker is RequirePermissionsSimpleBatchStateChecker batch) + { + var model = batch.GetModelOrNull(state); + return model == null ? null : BuildJson(model.RequiresAll, model.Permissions); + } + + return SerializeToJson(checker); + } + + private static string BuildJson(bool requiresAll, string[] permissionNames) + { + var jsonObject = new JsonObject + { ["T"] = CheckerShortName, - ["A"] = permissionsSimpleStateChecker.RequiresAll + ["A"] = requiresAll }; var nameArray = new JsonArray(); - foreach (var permissionName in permissionsSimpleStateChecker.PermissionNames) + foreach (var permissionName in permissionNames) { nameArray.Add(permissionName); } diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs index 0957f51666..3fdf76a9d2 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs @@ -47,6 +47,11 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat return new DisposeAction(() => _current.Value = previousValue); } + public virtual RequirePermissionsSimpleBatchStateCheckerModel? GetModelOrNull(TState state) + { + return _models.FirstOrDefault(m => EqualityComparer.Default.Equals(m.State, state)); + } + public override async Task> IsEnabledAsync(SimpleBatchStateCheckerContext context) { var permissionChecker = context.ServiceProvider.GetRequiredService(); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs index 96c0785d6e..0be80ff319 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializer.cs @@ -7,6 +7,10 @@ public interface ISimpleStateCheckerSerializer public string? Serialize(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers; + + public string? Serialize(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers; + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers; } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs index 79510024b7..b8c3e4943f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/ISimpleStateCheckerSerializerContributor.cs @@ -7,6 +7,9 @@ public interface ISimpleStateCheckerSerializerContributor public string? SerializeToJson(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers; + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers; + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers; } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs index 8ae58d16be..bc4004ec89 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializer.cs @@ -15,7 +15,7 @@ public class SimpleStateCheckerSerializer : _contributors = contributors; } - public string? Serialize(ISimpleStateChecker checker) + public string? Serialize(ISimpleStateChecker checker) where TState : IHasSimpleStateCheckers { foreach (var contributor in _contributors) @@ -30,6 +30,21 @@ public class SimpleStateCheckerSerializer : return null; } + public string? Serialize(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + { + foreach (var contributor in _contributors) + { + var result = contributor.SerializeToJson(checker, state); + if (result != null) + { + return result; + } + } + + return null; + } + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers { diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs index 8d06c7cfc3..43e8ca0d6c 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/SimpleStateChecking/SimpleStateCheckerSerializerExtensions.cs @@ -8,25 +8,42 @@ namespace Volo.Abp.SimpleStateChecking; public static class SimpleStateCheckerSerializerExtensions { public static string? Serialize( - this ISimpleStateCheckerSerializer serializer, + this ISimpleStateCheckerSerializer serializer, IList> stateCheckers) where TState : IHasSimpleStateCheckers + { + return SerializeCore(stateCheckers, serializer.Serialize); + } + + public static string? Serialize( + this ISimpleStateCheckerSerializer serializer, + IList> stateCheckers, + TState state) + where TState : IHasSimpleStateCheckers + { + return SerializeCore(stateCheckers, c => serializer.Serialize(c, state)); + } + + private static string? SerializeCore( + IList> stateCheckers, + Func, string?> serializeChecker) + where TState : IHasSimpleStateCheckers { switch (stateCheckers.Count) { case 0: return null; case 1: - var serializedChecker = serializer.Serialize(stateCheckers.Single()); + var serializedChecker = serializeChecker(stateCheckers.Single()); return serializedChecker != null ? $"[{serializedChecker}]" : null; default: var serializedCheckers = new List(stateCheckers.Count); - + foreach (var stateChecker in stateCheckers) { - var serialized = serializer.Serialize(stateChecker); + var serialized = serializeChecker(stateChecker); if (serialized != null) { serializedCheckers.Add(serialized); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs index 5f652269b5..555a9d32a5 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeaturesSimpleStateCheckerSerializerContributor.cs @@ -19,13 +19,31 @@ public class FeaturesSimpleStateCheckerSerializerContributor : return null; } - var jsonObject = new JsonObject { + return BuildJson(featuresSimpleStateChecker.RequiresAll, featuresSimpleStateChecker.FeatureNames); + } + + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + { + if (checker is RequireFeaturesSimpleBatchStateChecker batch) + { + var model = batch.GetModelOrNull(state); + return model == null ? null : BuildJson(model.RequiresAll, model.FeatureNames); + } + + return SerializeToJson(checker); + } + + private static string BuildJson(bool requiresAll, string[] featureNames) + { + var jsonObject = new JsonObject + { ["T"] = CheckerShortName, - ["A"] = featuresSimpleStateChecker.RequiresAll + ["A"] = requiresAll }; var nameArray = new JsonArray(); - foreach (var featureName in featuresSimpleStateChecker.FeatureNames) + foreach (var featureName in featureNames) { nameArray.Add(featureName); } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs index 66d484f7f9..64ad3df37a 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs @@ -48,6 +48,11 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh return new DisposeAction(() => _current.Value = previousValue); } + public virtual RequireFeaturesSimpleBatchStateCheckerModel? GetModelOrNull(TState state) + { + return _models.FirstOrDefault(x => EqualityComparer.Default.Equals(x.State, state)); + } + public override async Task> IsEnabledAsync( SimpleBatchStateCheckerContext context) { diff --git a/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs b/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs index fe8530435b..4063197cf1 100644 --- a/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs +++ b/framework/src/Volo.Abp.GlobalFeatures/Volo/Abp/GlobalFeatures/GlobalFeaturesSimpleStateCheckerSerializerContributor.cs @@ -34,6 +34,10 @@ public class GlobalFeaturesSimpleStateCheckerSerializerContributor : return jsonObject.ToJsonString(); } + public string? SerializeToJson(ISimpleStateChecker checker, TState state) + where TState : IHasSimpleStateCheckers + => SerializeToJson(checker); + public ISimpleStateChecker? Deserialize(JsonObject jsonObject, TState state) where TState : IHasSimpleStateCheckers { diff --git a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs index b0775e70d2..3e26235672 100644 --- a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs +++ b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs @@ -85,6 +85,41 @@ public class RequireFeaturesSimpleBatchStateChecker_Tests : FeatureTestBase } } + [Fact] + public void GetModelOrNull_Uses_Same_Equality_As_Runtime() + { + // The runtime path (IsEnabledAsync) looks up models via HashSet(context.States), + // i.e. EqualityComparer.Default. GetModelOrNull must use the same semantics or + // a custom TState.Equals would make the runtime gate and the serializer disagree. + + var checker = new RequireFeaturesSimpleBatchStateChecker(); + var stateA1 = new NamedState("A"); + var stateA2 = new NamedState("A"); // distinct instance, equal by Name + var stateB = new NamedState("B"); + + checker.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(stateA1, new[] { "F1" }, true), + new RequireFeaturesSimpleBatchStateCheckerModel(stateB, new[] { "F2" }, true)); + + // Same equality semantics as the runtime: A2 hits A1's model. + checker.GetModelOrNull(stateA1).ShouldNotBeNull(); + checker.GetModelOrNull(stateA2).ShouldNotBeNull(); + checker.GetModelOrNull(stateA2)!.FeatureNames.ShouldBe(new[] { "F1" }); + checker.GetModelOrNull(new NamedState("missing")).ShouldBeNull(); + } + + private sealed class NamedState : IHasSimpleStateCheckers, IEquatable + { + public string Name { get; } + public List> StateCheckers { get; } = new(); + + public NamedState(string name) => Name = name; + + public bool Equals(NamedState? other) => other is not null && other.Name == Name; + public override bool Equals(object? obj) => obj is NamedState other && Equals(other); + public override int GetHashCode() => Name.GetHashCode(); + } + [Fact] public async Task Current_Should_Not_Be_Null_In_Fresh_ExecutionContext() { diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs index 707a3e89ea..22d0b0e179 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer.cs @@ -93,7 +93,7 @@ public class PermissionDefinitionSerializer : IPermissionDefinitionSerializer, I permission.IsEnabled, permission.MultiTenancySide, SerializeProviders(permission.Providers), - SerializeStateCheckers(permission.StateCheckers) + SerializeStateCheckers(permission, permission.StateCheckers) ); foreach (var property in permission.Properties) @@ -112,8 +112,10 @@ public class PermissionDefinitionSerializer : IPermissionDefinitionSerializer, I : null; } - protected virtual string SerializeStateCheckers(List> stateCheckers) + protected virtual string SerializeStateCheckers( + PermissionDefinition permission, + List> stateCheckers) { - return StateCheckerSerializer.Serialize(stateCheckers); + return StateCheckerSerializer.Serialize(stateCheckers, permission); } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs index a8bdc4b4ea..c395398bb7 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionDefinitionSerializer_Tests.cs @@ -79,6 +79,74 @@ public class PermissionDefinitionSerializer_Tests : PermissionTestBase } + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequireFeatures() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("AbpAuditLogging"); + var permission = group.AddPermission( + "AbpAuditLogging.Settings", + new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequireFeatures("AbpAuditLogging.SettingManagement"); + + var record = await _serializer.SerializeAsync(permission, group); + + record.StateCheckers.ShouldBe( + "[{\"T\":\"F\",\"A\":true,\"N\":[\"AbpAuditLogging.SettingManagement\"]}]"); + } + + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequireFeatures_Picks_Per_State_Model() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("Group1"); + + var permA = group.AddPermission("PermA", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequireFeatures("FeatureForA"); + var permB = group.AddPermission("PermB", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequireFeatures("FeatureForB1", "FeatureForB2"); + + var recordA = await _serializer.SerializeAsync(permA, group); + var recordB = await _serializer.SerializeAsync(permB, group); + + recordA.StateCheckers.ShouldBe("[{\"T\":\"F\",\"A\":true,\"N\":[\"FeatureForA\"]}]"); + recordB.StateCheckers.ShouldBe("[{\"T\":\"F\",\"A\":true,\"N\":[\"FeatureForB1\",\"FeatureForB2\"]}]"); + } + + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequirePermissions() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("Group1"); + var permission = group.AddPermission( + "Permission1", + new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequirePermissions("OtherPermission1", "OtherPermission2"); + + var record = await _serializer.SerializeAsync(permission, group); + + record.StateCheckers.ShouldBe( + "[{\"T\":\"P\",\"A\":true,\"N\":[\"OtherPermission1\",\"OtherPermission2\"]}]"); + } + + [Fact] + public async Task Serialize_Permission_With_Default_Batch_RequirePermissions_Picks_Per_State_Model() + { + var context = new PermissionDefinitionContext(null); + var group = context.AddGroup("Group1"); + + var permA = group.AddPermission("PermA", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequirePermissions("OtherForA"); + var permB = group.AddPermission("PermB", new LocalizableString(typeof(AbpPermissionManagementResource), "Permission1")) + .RequirePermissions(requiresAll: false, "OtherForB1", "OtherForB2"); + + var recordA = await _serializer.SerializeAsync(permA, group); + var recordB = await _serializer.SerializeAsync(permB, group); + + recordA.StateCheckers.ShouldBe("[{\"T\":\"P\",\"A\":true,\"N\":[\"OtherForA\"]}]"); + recordB.StateCheckers.ShouldBe("[{\"T\":\"P\",\"A\":false,\"N\":[\"OtherForB1\",\"OtherForB2\"]}]"); + } + [Fact] public async Task Serialize_Complex_Resource_Permission_Definition() { From 340bd51f490943eb19bc13d47c654eb6d1b6a083 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 14 May 2026 09:12:52 +0800 Subject: [PATCH 3/3] Index batch state checker models by state for O(1) lookup - Mirror _models with Dictionary populated in AddCheckModels - GetModelOrNull goes through the dict, matching IsEnabledAsync's first-wins - Pin first-wins via a regression test --- ...RequirePermissionsSimpleBatchStateChecker.cs | 12 +++++++++++- .../RequireFeaturesSimpleBatchStateChecker.cs | 12 +++++++++++- ...uireFeaturesSimpleBatchStateChecker_Tests.cs | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs index 3fdf76a9d2..d78e0ceb6f 100644 --- a/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs +++ b/framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/RequirePermissionsSimpleBatchStateChecker.cs @@ -27,9 +27,12 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat private readonly List> _models; + private readonly Dictionary> _modelsByState; + public RequirePermissionsSimpleBatchStateChecker() { _models = new List>(); + _modelsByState = new Dictionary>(); } public RequirePermissionsSimpleBatchStateChecker AddCheckModels(params RequirePermissionsSimpleBatchStateCheckerModel[] models) @@ -37,6 +40,13 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat Check.NotNullOrEmpty(models, nameof(models)); _models.AddRange(models); + foreach (var model in models) + { + if (!_modelsByState.ContainsKey(model.State)) + { + _modelsByState[model.State] = model; + } + } return this; } @@ -49,7 +59,7 @@ public class RequirePermissionsSimpleBatchStateChecker : SimpleBatchStat public virtual RequirePermissionsSimpleBatchStateCheckerModel? GetModelOrNull(TState state) { - return _models.FirstOrDefault(m => EqualityComparer.Default.Equals(m.State, state)); + return _modelsByState.TryGetValue(state, out var model) ? model : null; } public override async Task> IsEnabledAsync(SimpleBatchStateCheckerContext context) diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs index 64ad3df37a..5faf1b891a 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker.cs @@ -27,9 +27,12 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh private readonly List> _models; + private readonly Dictionary> _modelsByState; + public RequireFeaturesSimpleBatchStateChecker() { _models = new List>(); + _modelsByState = new Dictionary>(); } public RequireFeaturesSimpleBatchStateChecker AddCheckModels( @@ -38,6 +41,13 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh Check.NotNullOrEmpty(models, nameof(models)); _models.AddRange(models); + foreach (var model in models) + { + if (!_modelsByState.ContainsKey(model.State)) + { + _modelsByState[model.State] = model; + } + } return this; } @@ -50,7 +60,7 @@ public class RequireFeaturesSimpleBatchStateChecker : SimpleBatchStateCh public virtual RequireFeaturesSimpleBatchStateCheckerModel? GetModelOrNull(TState state) { - return _models.FirstOrDefault(x => EqualityComparer.Default.Equals(x.State, state)); + return _modelsByState.TryGetValue(state, out var model) ? model : null; } public override async Task> IsEnabledAsync( diff --git a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs index 3e26235672..acd961931e 100644 --- a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs +++ b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/RequireFeaturesSimpleBatchStateChecker_Tests.cs @@ -85,6 +85,23 @@ public class RequireFeaturesSimpleBatchStateChecker_Tests : FeatureTestBase } } + [Fact] + public void GetModelOrNull_Returns_First_Win_When_Same_State_Registered_Twice() + { + // Mirrors IsEnabledAsync's modelLookup behaviour: when the same state is registered + // multiple times, the first registration wins. Backed by the dictionary index. + + var checker = new RequireFeaturesSimpleBatchStateChecker(); + var state = new NamedState("A"); + + checker.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(state, new[] { "First" }, true)); + checker.AddCheckModels( + new RequireFeaturesSimpleBatchStateCheckerModel(state, new[] { "Second" }, true)); + + checker.GetModelOrNull(state)!.FeatureNames.ShouldBe(new[] { "First" }); + } + [Fact] public void GetModelOrNull_Uses_Same_Equality_As_Runtime() {