mirror of https://github.com/abpframework/abp.git
55 changed files with 1041 additions and 311 deletions
@ -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); |
|||
} |
|||
|
|||
@ -1,13 +1,26 @@ |
|||
using System.Globalization; |
|||
using Volo.Abp.Users; |
|||
using System; |
|||
using System.Globalization; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.Client; |
|||
|
|||
public static class MvcCachedApplicationConfigurationClientHelper |
|||
public class MvcCachedApplicationConfigurationClientHelper : ITransientDependency |
|||
{ |
|||
public static string CreateCacheKey(ICurrentUser currentUser) |
|||
protected IDistributedCache<MvcCachedApplicationVersionCacheItem> ApplicationVersionCache { get; } |
|||
|
|||
public MvcCachedApplicationConfigurationClientHelper(IDistributedCache<MvcCachedApplicationVersionCacheItem> applicationVersionCache) |
|||
{ |
|||
ApplicationVersionCache = applicationVersionCache; |
|||
} |
|||
|
|||
public virtual async Task<string> CreateCacheKeyAsync(Guid? userId) |
|||
{ |
|||
var userKey = currentUser.Id?.ToString("N") ?? "Anonymous"; |
|||
return $"ApplicationConfiguration_{userKey}_{CultureInfo.CurrentUICulture.Name}"; |
|||
var appVersion = await ApplicationVersionCache.GetOrAddAsync(MvcCachedApplicationVersionCacheItem.CacheKey, |
|||
() => Task.FromResult(new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N")))) ?? |
|||
new MvcCachedApplicationVersionCacheItem(Guid.NewGuid().ToString("N")); |
|||
var userKey = userId?.ToString("N") ?? "Anonymous"; |
|||
return $"ApplicationConfiguration_{appVersion.Version}_{userKey}_{CultureInfo.CurrentUICulture.Name}"; |
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -1,9 +1,21 @@ |
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; |
|||
using System; |
|||
|
|||
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; |
|||
|
|||
/// <summary>
|
|||
/// This event is used to invalidate current user's cached configuration.
|
|||
/// </summary>
|
|||
public class CurrentApplicationConfigurationCacheResetEventData |
|||
{ |
|||
public Guid? UserId { get; set; } |
|||
|
|||
public CurrentApplicationConfigurationCacheResetEventData() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public CurrentApplicationConfigurationCacheResetEventData(Guid? userId) |
|||
{ |
|||
UserId = userId; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,242 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.Threading; |
|||
|
|||
/// <summary>
|
|||
/// Per-key asynchronous lock for coordinating concurrent flows.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Based on the pattern described in https://stackoverflow.com/a/31194647.
|
|||
/// Use within a <c>using</c> scope to ensure the lock is released via <c>IDisposable.Dispose()</c>.
|
|||
/// </remarks>
|
|||
public static class KeyedLock |
|||
{ |
|||
private static readonly Dictionary<object, RefCounted<SemaphoreSlim>> SemaphoreSlims = new(); |
|||
|
|||
/// <summary>
|
|||
/// Acquires an exclusive asynchronous lock for the specified <paramref name="key"/>.
|
|||
/// This method waits until the lock becomes available.
|
|||
/// </summary>
|
|||
/// <param name="key">A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock.</param>
|
|||
/// <returns>An <see cref="IDisposable"/> handle that must be disposed to release the lock.</returns>
|
|||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
|
|||
/// <example>
|
|||
/// <code>
|
|||
/// var key = "my-critical-section";
|
|||
/// using (await KeyedLock.LockAsync(key))
|
|||
/// {
|
|||
/// // protected work
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
public static async Task<IDisposable> LockAsync(object key) |
|||
{ |
|||
Check.NotNull(key, nameof(key)); |
|||
return await LockAsync(key, CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Acquires an exclusive asynchronous lock for the specified <paramref name="key"/>, observing a <paramref name="cancellationToken"/>.
|
|||
/// </summary>
|
|||
/// <param name="key">A non-null object that identifies the lock. Objects considered equal by dictionary semantics will share the same lock.</param>
|
|||
/// <param name="cancellationToken">A token to cancel the wait for the lock.</param>
|
|||
/// <returns>An <see cref="IDisposable"/> handle that must be disposed to release the lock.</returns>
|
|||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
|
|||
/// <exception cref="OperationCanceledException">Thrown if the wait is canceled via <paramref name="cancellationToken"/>.</exception>
|
|||
/// <example>
|
|||
/// <code>
|
|||
/// var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|||
/// using (await KeyedLock.LockAsync("db-update", cts.Token))
|
|||
/// {
|
|||
/// // protected work
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
public static async Task<IDisposable> LockAsync(object key, CancellationToken cancellationToken) |
|||
{ |
|||
Check.NotNull(key, nameof(key)); |
|||
var semaphore = GetOrCreate(key); |
|||
try |
|||
{ |
|||
await semaphore.WaitAsync(cancellationToken); |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
var toDispose = DecrementRefAndMaybeRemove(key); |
|||
toDispose?.Dispose(); |
|||
throw; |
|||
} |
|||
return new Releaser(key); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to acquire an exclusive lock for the specified <paramref name="key"/> without waiting.
|
|||
/// </summary>
|
|||
/// <param name="key">A non-null object that identifies the lock.</param>
|
|||
/// <returns>
|
|||
/// An <see cref="IDisposable"/> handle if the lock was immediately acquired; otherwise <see langword="null"/>.
|
|||
/// </returns>
|
|||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
|
|||
/// <example>
|
|||
/// <code>
|
|||
/// var handle = await KeyedLock.TryLockAsync("cache-key");
|
|||
/// if (handle != null)
|
|||
/// {
|
|||
/// using (handle)
|
|||
/// {
|
|||
/// // protected work
|
|||
/// }
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
public static async Task<IDisposable?> TryLockAsync(object key) |
|||
{ |
|||
Check.NotNull(key, nameof(key)); |
|||
return await TryLockAsync(key, default, CancellationToken.None); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to acquire an exclusive lock for the specified <paramref name="key"/>, waiting up to <paramref name="timeout"/>.
|
|||
/// </summary>
|
|||
/// <param name="key">A non-null object that identifies the lock.</param>
|
|||
/// <param name="timeout">Maximum time to wait for the lock. If set to <see cref="TimeSpan.Zero"/>, the method performs an immediate, non-blocking attempt.</param>
|
|||
/// <param name="cancellationToken">A token to cancel the wait.</param>
|
|||
/// <returns>
|
|||
/// An <see cref="IDisposable"/> handle if the lock was acquired within the timeout; otherwise <see langword="null"/>.
|
|||
/// </returns>
|
|||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="key"/> is <see langword="null"/>.</exception>
|
|||
/// <exception cref="OperationCanceledException">Thrown if the wait is canceled via <paramref name="cancellationToken"/>.</exception>
|
|||
/// <example>
|
|||
/// <code>
|
|||
/// var handle = await KeyedLock.TryLockAsync("send-mail", TimeSpan.FromSeconds(1));
|
|||
/// if (handle != null)
|
|||
/// {
|
|||
/// using (handle)
|
|||
/// {
|
|||
/// // protected work
|
|||
/// }
|
|||
/// }
|
|||
/// else
|
|||
/// {
|
|||
/// // lock not acquired within timeout
|
|||
/// }
|
|||
/// </code>
|
|||
/// </example>
|
|||
public static async Task<IDisposable?> TryLockAsync(object key, TimeSpan timeout, CancellationToken cancellationToken = default) |
|||
{ |
|||
Check.NotNull(key, nameof(key)); |
|||
var semaphore = GetOrCreate(key); |
|||
bool acquired; |
|||
try |
|||
{ |
|||
if (timeout == default) |
|||
{ |
|||
acquired = await semaphore.WaitAsync(0, cancellationToken); |
|||
} |
|||
else |
|||
{ |
|||
acquired = await semaphore.WaitAsync(timeout, cancellationToken); |
|||
} |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
var toDispose = DecrementRefAndMaybeRemove(key); |
|||
toDispose?.Dispose(); |
|||
throw; |
|||
} |
|||
|
|||
if (acquired) |
|||
{ |
|||
return new Releaser(key); |
|||
} |
|||
|
|||
var toDisposeOnFail = DecrementRefAndMaybeRemove(key); |
|||
toDisposeOnFail?.Dispose(); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static SemaphoreSlim GetOrCreate(object key) |
|||
{ |
|||
RefCounted<SemaphoreSlim> item; |
|||
lock (SemaphoreSlims) |
|||
{ |
|||
if (SemaphoreSlims.TryGetValue(key, out item!)) |
|||
{ |
|||
++item.RefCount; |
|||
} |
|||
else |
|||
{ |
|||
item = new RefCounted<SemaphoreSlim>(new SemaphoreSlim(1, 1)); |
|||
SemaphoreSlims[key] = item; |
|||
} |
|||
} |
|||
return item.Value; |
|||
} |
|||
|
|||
private sealed class RefCounted<T>(T value) |
|||
{ |
|||
public int RefCount { get; set; } = 1; |
|||
|
|||
public T Value { get; } = value; |
|||
} |
|||
|
|||
private sealed class Releaser(object key) : IDisposable |
|||
{ |
|||
private int _disposed; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (Interlocked.Exchange(ref _disposed, 1) == 1) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
RefCounted<SemaphoreSlim> item; |
|||
var shouldDispose = false; |
|||
lock (SemaphoreSlims) |
|||
{ |
|||
if (!SemaphoreSlims.TryGetValue(key, out item!)) |
|||
{ |
|||
return; |
|||
} |
|||
--item.RefCount; |
|||
if (item.RefCount == 0) |
|||
{ |
|||
SemaphoreSlims.Remove(key); |
|||
shouldDispose = true; |
|||
} |
|||
} |
|||
|
|||
if (shouldDispose) |
|||
{ |
|||
item.Value.Dispose(); |
|||
} |
|||
else |
|||
{ |
|||
item.Value.Release(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static SemaphoreSlim? DecrementRefAndMaybeRemove(object key) |
|||
{ |
|||
RefCounted<SemaphoreSlim>? itemToDispose = null; |
|||
lock (SemaphoreSlims) |
|||
{ |
|||
if (SemaphoreSlims.TryGetValue(key, out var item)) |
|||
{ |
|||
--item.RefCount; |
|||
if (item.RefCount == 0) |
|||
{ |
|||
SemaphoreSlims.Remove(key); |
|||
itemToDispose = item; |
|||
} |
|||
} |
|||
} |
|||
return itemToDispose?.Value; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.DistributedLocking; |
|||
|
|||
/// <summary>
|
|||
/// This implementation of <see cref="IAbpDistributedLock"/> does not provide any distributed locking functionality.
|
|||
/// Useful in scenarios where distributed locking is not required or during testing.
|
|||
/// </summary>
|
|||
public class NullAbpDistributedLock : IAbpDistributedLock |
|||
{ |
|||
public Task<IAbpDistributedLockHandle?> TryAcquireAsync(string name, TimeSpan timeout = default, CancellationToken cancellationToken = default) |
|||
{ |
|||
return Task.FromResult<IAbpDistributedLockHandle?>(new LocalAbpDistributedLockHandle(NullDisposable.Instance)); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Volo.Abp.Domain.Entities.Auditing; |
|||
|
|||
namespace Volo.Abp.Auditing.App.Entities; |
|||
|
|||
public class AppEntityWithJsonProperty : FullAuditedAggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public JsonPropertyObject Data { get; set; } |
|||
|
|||
public int Count { get; set; } |
|||
|
|||
public AppEntityWithJsonProperty() |
|||
{ |
|||
} |
|||
|
|||
public AppEntityWithJsonProperty(Guid id, string name) : base(id) |
|||
{ |
|||
Name = name; |
|||
} |
|||
} |
|||
|
|||
public class JsonPropertyObject : Dictionary<string, object> |
|||
{ |
|||
} |
|||
@ -0,0 +1,179 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Threading; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Threading; |
|||
|
|||
public class KeyedLock_Tests |
|||
{ |
|||
[Fact] |
|||
public async Task TryLock_Should_Acquire_Immediately_When_Free() |
|||
{ |
|||
var key = "key-try-1"; |
|||
var handle = await KeyedLock.TryLockAsync(key); |
|||
handle.ShouldNotBeNull(); |
|||
handle!.Dispose(); |
|||
|
|||
var handle2 = await KeyedLock.TryLockAsync(key); |
|||
handle2.ShouldNotBeNull(); |
|||
handle2!.Dispose(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TryLock_Should_Return_Null_When_Already_Locked() |
|||
{ |
|||
var key = "key-try-2"; |
|||
using (await KeyedLock.LockAsync(key)) |
|||
{ |
|||
var handle2 = await KeyedLock.TryLockAsync(key); |
|||
handle2.ShouldBeNull(); |
|||
} |
|||
|
|||
var handle3 = await KeyedLock.TryLockAsync(key); |
|||
handle3.ShouldNotBeNull(); |
|||
handle3!.Dispose(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task LockAsync_Should_Block_Until_Released() |
|||
{ |
|||
var key = "key-block-1"; |
|||
var sw = Stopwatch.StartNew(); |
|||
|
|||
Task inner; |
|||
using (await KeyedLock.LockAsync(key)) |
|||
{ |
|||
inner = Task.Run(async () => |
|||
{ |
|||
using (await KeyedLock.LockAsync(key)) |
|||
{ |
|||
// Acquired only after outer lock is released
|
|||
} |
|||
}); |
|||
|
|||
// While holding the outer lock, inner waiter should not complete
|
|||
await Task.Delay(200); |
|||
inner.IsCompleted.ShouldBeFalse(); |
|||
} |
|||
|
|||
// After releasing, inner should complete; elapsed >= hold time
|
|||
await inner; |
|||
sw.ElapsedMilliseconds.ShouldBeGreaterThanOrEqualTo(180); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TryLock_With_Timeout_Should_Return_Null_When_Busy() |
|||
{ |
|||
var key = "key-timeout-1"; |
|||
using (await KeyedLock.LockAsync(key)) |
|||
{ |
|||
var handle = await KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(50)); |
|||
handle.ShouldBeNull(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TryLock_With_Timeout_Should_Succeed_If_Released_In_Time() |
|||
{ |
|||
var key = "key-timeout-2"; |
|||
// Hold the lock manually
|
|||
var outer = await KeyedLock.LockAsync(key); |
|||
var tryTask = KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(200)); |
|||
await Task.Delay(50); |
|||
// Release within the timeout window
|
|||
outer.Dispose(); |
|||
var handle2 = await tryTask; |
|||
handle2.ShouldNotBeNull(); |
|||
handle2!.Dispose(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task LockAsync_With_Cancellation_Should_Rollback_RefCount() |
|||
{ |
|||
var key = "key-cancel-1"; |
|||
var cts = new CancellationTokenSource(); |
|||
await cts.CancelAsync(); |
|||
await Should.ThrowAsync<OperationCanceledException>(async () => |
|||
{ |
|||
await KeyedLock.LockAsync(key, cts.Token); |
|||
}); |
|||
|
|||
// After cancellation, we should still be able to acquire the key
|
|||
var handle = await KeyedLock.TryLockAsync(key); |
|||
handle.ShouldNotBeNull(); |
|||
handle!.Dispose(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TryLock_With_Cancellation_Should_Rollback() |
|||
{ |
|||
var key = "key-cancel-2"; |
|||
// Ensure it's initially free
|
|||
var h0 = await KeyedLock.TryLockAsync(key); |
|||
h0?.Dispose(); |
|||
|
|||
var cts = new CancellationTokenSource(); |
|||
await cts.CancelAsync(); |
|||
await Should.ThrowAsync<OperationCanceledException>(async () => |
|||
{ |
|||
await KeyedLock.TryLockAsync(key, TimeSpan.FromMilliseconds(200), cts.Token); |
|||
}); |
|||
|
|||
// After cancellation, the key should be acquirable
|
|||
var handle = await KeyedLock.TryLockAsync(key); |
|||
handle.ShouldNotBeNull(); |
|||
handle!.Dispose(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Serializes_Access_For_Same_Key() |
|||
{ |
|||
var key = "key-serial-1"; |
|||
int counter = 0; |
|||
var tasks = Enumerable.Range(0, 10).Select(async _ => |
|||
{ |
|||
using (await KeyedLock.LockAsync(key)) |
|||
{ |
|||
var current = counter; |
|||
await Task.Delay(10); |
|||
counter = current + 1; |
|||
} |
|||
}); |
|||
|
|||
await Task.WhenAll(tasks); |
|||
counter.ShouldBe(10); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Multiple_Keys_Should_Not_Block_Each_Other() |
|||
{ |
|||
var key1 = "key-multi-1"; |
|||
var key2 = "key-multi-2"; |
|||
|
|||
using (await KeyedLock.LockAsync(key1)) |
|||
{ |
|||
var handle2 = await KeyedLock.TryLockAsync(key2); |
|||
handle2.ShouldNotBeNull(); |
|||
handle2!.Dispose(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task TryLock_Default_Overload_Delegates_To_Full_Overload() |
|||
{ |
|||
var key = "key-default-1"; |
|||
using (await KeyedLock.LockAsync(key)) |
|||
{ |
|||
var h1 = await KeyedLock.TryLockAsync(key); |
|||
h1.ShouldBeNull(); |
|||
} |
|||
|
|||
var h2 = await KeyedLock.TryLockAsync(key); |
|||
h2.ShouldNotBeNull(); |
|||
h2!.Dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.StaticDefinitions; |
|||
using Volo.Abp.TextTemplating; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.TextTemplateManagement; |
|||
|
|||
public class StaticTemplateDefinitionChangedEventHandler : ILocalEventHandler<StaticTemplateDefinitionChangedEvent>, ITransientDependency |
|||
{ |
|||
protected IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> DefinitionCache { get; } |
|||
protected TextTemplateDynamicInitializer TextTemplateDynamicInitializer { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
public StaticTemplateDefinitionChangedEventHandler( |
|||
IStaticDefinitionCache<TemplateDefinition, Dictionary<string, TemplateDefinition>> definitionCache, |
|||
TextTemplateDynamicInitializer textTemplateDynamicInitializer, |
|||
ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
DefinitionCache = definitionCache; |
|||
TextTemplateDynamicInitializer = textTemplateDynamicInitializer; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(StaticTemplateDefinitionChangedEvent eventData) |
|||
{ |
|||
await DefinitionCache.ClearAsync(); |
|||
await TextTemplateDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue