From d8441d7fc400f98f8648bde0942bfb888a7bab1a Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 10:10:56 +0800 Subject: [PATCH 1/5] 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 b06e8a938143fe9ddb641b6b1939dc19fb493b27 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 12:50:35 +0800 Subject: [PATCH 2/5] 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 3/5] 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 7733429c4e617e749db1f5310fab2d9087dde5b9 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 13:25:17 +0800 Subject: [PATCH 4/5] 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 67310e726012c7559c501f7362d3a027bb87a664 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 30 Dec 2025 14:34:12 +0800 Subject: [PATCH 5/5] 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 =>