@ -0,0 +1,119 @@ |
|||
# LeptonX CSS Variables Documentation |
|||
|
|||
LeptonX uses CSS custom properties (variables) prefixed with `--lpx-*` to provide a flexible theming system. These variables control colors, spacing, shadows, and component-specific styles throughout the application. |
|||
|
|||
## Brand & Semantic Colors |
|||
|
|||
| Variable | Description | |
|||
|----------|-------------| |
|||
| `--lpx-brand` | Brand-specific accent color | |
|||
| `--lpx-brand-text` | Text color used on brand-colored backgrounds | |
|||
|
|||
## Base Colors |
|||
|
|||
| Variable | Description | |
|||
|----------|-------------| |
|||
| `--lpx-light` | Light shade for subtle backgrounds or text | |
|||
| `--lpx-dark` | Dark shade for contrasting elements | |
|||
|
|||
## Layout & Surface Colors |
|||
|
|||
| Variable | Description | |
|||
|----------|-------------| |
|||
| `--lpx-content-bg` | Main content area background color | |
|||
| `--lpx-content-text` | Default text color for content areas | |
|||
| `--lpx-card-bg` | Card component background color | |
|||
| `--lpx-card-title-text-color` | Card title text color | |
|||
| `--lpx-border-color` | Default border color for dividers and outlines | |
|||
| `--lpx-shadow` | Box shadow definition for elevated elements | |
|||
|
|||
## Navigation |
|||
|
|||
| Variable | Description | |
|||
|----------|-------------| |
|||
| `--lpx-navbar-color` | Navbar background color | |
|||
| `--lpx-navbar-text-color` | Navbar default text/icon color | |
|||
| `--lpx-navbar-active-text-color` | Navbar active/hover text color | |
|||
| `--lpx-navbar-active-bg-color` | Navbar active item background color | |
|||
|
|||
## Utility |
|||
|
|||
| Variable | Description | |
|||
|----------|-------------| |
|||
| `--lpx-radius` | Global border-radius value for rounded corners | |
|||
|
|||
## Global Override |
|||
|
|||
Applies to all themes and pages: |
|||
|
|||
```css |
|||
:root { |
|||
/* Brand & Semantic */ |
|||
--lpx-brand: #f72585; |
|||
|
|||
/* Base Colors */ |
|||
--lpx-light: #f5f7fb; |
|||
--lpx-dark: #0b0f19; |
|||
|
|||
/* Layout & Surface */ |
|||
--lpx-content-bg: #101018; |
|||
--lpx-content-text: #cfd6e4; |
|||
--lpx-card-bg: #151a2b; |
|||
--lpx-card-title-text-color: #ffffff; |
|||
--lpx-border-color: #242836; |
|||
--lpx-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); |
|||
|
|||
/* Navigation */ |
|||
--lpx-navbar-color: #0d1020; |
|||
--lpx-navbar-text-color: #aab2c8; |
|||
--lpx-navbar-active-text-color: #ffffff; |
|||
--lpx-navbar-active-bg-color: rgba(247, 37, 133, 0.15); |
|||
|
|||
/* Utility */ |
|||
--lpx-radius: 10px; |
|||
} |
|||
|
|||
``` |
|||
|
|||
## Theme-Scoped Override |
|||
|
|||
Applies only when a specific theme class is active (e.g., `.lpx-theme-dark` on `<html>` or `<body>`): |
|||
|
|||
```css |
|||
:root .lpx-theme-dark { |
|||
/* Brand & Semantic */ |
|||
--lpx-brand: #4dd0e1; |
|||
|
|||
/* Base Colors */ |
|||
--lpx-light: #e0f7fa; |
|||
--lpx-dark: #020617; |
|||
|
|||
/* Layout & Surface */ |
|||
--lpx-content-bg: #0b1118; |
|||
--lpx-content-text: #c7d0e0; |
|||
--lpx-card-bg: #111a24; |
|||
--lpx-card-title-text-color: #e6f1ff; |
|||
--lpx-border-color: #1e2a3a; |
|||
--lpx-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); |
|||
|
|||
/* Navigation */ |
|||
--lpx-navbar-color: #0f1a22; |
|||
--lpx-navbar-text-color: #9fb3c8; |
|||
--lpx-navbar-active-text-color: #ffffff; |
|||
--lpx-navbar-active-bg-color: rgba(77, 208, 225, 0.18); |
|||
|
|||
/* Utility */ |
|||
--lpx-radius: 12px; |
|||
} |
|||
``` |
|||
|
|||
## Component/Page-Specific Override |
|||
|
|||
For targeted customizations that should only affect a specific section: |
|||
|
|||
```css |
|||
.my-custom-page { |
|||
--lpx-brand: #e91e63; |
|||
--lpx-card-bg: #1a1a2e; |
|||
} |
|||
``` |
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 29 KiB |
@ -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,9 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Authorization.Permissions; |
|||
|
|||
[Serializable] |
|||
public class StaticPermissionDefinitionChangedEvent |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.StaticDefinitions; |
|||
|
|||
public interface IStaticDefinitionCache<TKey, TValue> |
|||
{ |
|||
Task<TValue> GetOrCreateAsync(Func<Task<TValue>> factory); |
|||
|
|||
Task ClearAsync(); |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Volo.Abp.StaticDefinitions; |
|||
|
|||
public class StaticDefinitionCache<TKey, TValue> : IStaticDefinitionCache<TKey, TValue> |
|||
{ |
|||
private Lazy<Task<TValue>>? _lazy; |
|||
|
|||
public virtual async Task<TValue> GetOrCreateAsync(Func<Task<TValue>> factory) |
|||
{ |
|||
var lazy = _lazy; |
|||
if (lazy != null) |
|||
{ |
|||
return await lazy.Value; |
|||
} |
|||
|
|||
var newLazy = new Lazy<Task<TValue>>(factory, LazyThreadSafetyMode.ExecutionAndPublication); |
|||
lazy = Interlocked.CompareExchange(ref _lazy, newLazy, null) ?? newLazy; |
|||
|
|||
return await lazy.Value; |
|||
} |
|||
|
|||
public virtual Task ClearAsync() |
|||
{ |
|||
Interlocked.Exchange(ref _lazy, null); |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
@ -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,9 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Features; |
|||
|
|||
[Serializable] |
|||
public class StaticFeatureDefinitionChangedEvent |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Settings; |
|||
|
|||
[Serializable] |
|||
public class StaticSettingDefinitionChangedEvent |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.TextTemplating; |
|||
|
|||
[Serializable] |
|||
public class StaticTemplateDefinitionChangedEvent |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Testing; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.StaticDefinitions; |
|||
|
|||
public class StaticDefinitionCache_Tests : AbpIntegratedTest<AbpTestModule> |
|||
{ |
|||
protected readonly IStaticDefinitionCache<StaticDefinition1, List<StaticDefinition1>> _staticDefinitionCache1; |
|||
protected readonly IStaticDefinitionCache<StaticDefinition2, List<StaticDefinition2>> _staticDefinitionCache2; |
|||
|
|||
public StaticDefinitionCache_Tests() |
|||
{ |
|||
_staticDefinitionCache1 = GetRequiredService<IStaticDefinitionCache<StaticDefinition1, List<StaticDefinition1>>>(); |
|||
_staticDefinitionCache2 = GetRequiredService<IStaticDefinitionCache<StaticDefinition2, List<StaticDefinition2>>>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task GetOrCreate_Test() |
|||
{ |
|||
var definition1 = new StaticDefinition1 { Name = "Definition1", Value = 1 }; |
|||
var definition2 = new StaticDefinition1 { Name = "Definition2", Value = 2 }; |
|||
|
|||
var definitionsFirstRetrieval = await _staticDefinitionCache1.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition1> { definition1, definition2 }); |
|||
}); |
|||
|
|||
var definitionsSecondRetrieval = await _staticDefinitionCache1.GetOrCreateAsync(() => |
|||
{ |
|||
throw new AbpException("Factory should not be called on second retrieval"); |
|||
}); |
|||
|
|||
definitionsFirstRetrieval.ShouldBe(definitionsSecondRetrieval); |
|||
|
|||
definitionsSecondRetrieval.Count.ShouldBe(2); |
|||
|
|||
definitionsSecondRetrieval[0].Name.ShouldBe("Definition1"); |
|||
definitionsSecondRetrieval[0].Value.ShouldBe(1); |
|||
|
|||
definitionsSecondRetrieval[1].Name.ShouldBe("Definition2"); |
|||
definitionsSecondRetrieval[1].Value.ShouldBe(2); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Separate_Caches_For_Different_Types_Test() |
|||
{ |
|||
var definitions1 = await _staticDefinitionCache1.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition1> { new StaticDefinition1 {Name = "Definition1", Value = 1} }); |
|||
}); |
|||
var definitions2 = await _staticDefinitionCache2.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition2> { new StaticDefinition2 {Name = "DefinitionA", Value = 100} }); |
|||
}); |
|||
|
|||
definitions1.Count.ShouldBe(1); |
|||
definitions1[0].Name.ShouldBe("Definition1"); |
|||
definitions1[0].Value.ShouldBe(1); |
|||
|
|||
definitions2.Count.ShouldBe(1); |
|||
definitions2[0].Name.ShouldBe("DefinitionA"); |
|||
definitions2[0].Value.ShouldBe(100); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Clear_Test() |
|||
{ |
|||
var definitions1 = await _staticDefinitionCache1.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition1> { new StaticDefinition1 {Name = "Definition1", Value = 1} }); |
|||
}); |
|||
var definitions2 = await _staticDefinitionCache2.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition2> { new StaticDefinition2 {Name = "DefinitionA", Value = 100} }); |
|||
}); |
|||
|
|||
definitions1.Count.ShouldBe(1); |
|||
definitions1[0].Name.ShouldBe("Definition1"); |
|||
definitions1[0].Value.ShouldBe(1); |
|||
|
|||
definitions2.Count.ShouldBe(1); |
|||
definitions2[0].Name.ShouldBe("DefinitionA"); |
|||
definitions2[0].Value.ShouldBe(100); |
|||
|
|||
await _staticDefinitionCache1.ClearAsync(); |
|||
await _staticDefinitionCache2.ClearAsync(); |
|||
|
|||
var definitions1AfterClear = await _staticDefinitionCache1.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition1> { new StaticDefinition1 {Name = "DefinitionNew", Value = 10} }); |
|||
}); |
|||
var definitions2AfterClear = await _staticDefinitionCache2.GetOrCreateAsync(() => |
|||
{ |
|||
return Task.FromResult(new List<StaticDefinition2> {new StaticDefinition2 {Name = "DefinitionNewA", Value = 200}}); |
|||
}); |
|||
|
|||
definitions1AfterClear.Count.ShouldBe(1); |
|||
definitions1AfterClear[0].Name.ShouldBe("DefinitionNew"); |
|||
definitions1AfterClear[0].Value.ShouldBe(10); |
|||
|
|||
definitions2AfterClear.Count.ShouldBe(1); |
|||
definitions2AfterClear[0].Name.ShouldBe("DefinitionNewA"); |
|||
definitions2AfterClear[0].Value.ShouldBe(200); |
|||
} |
|||
|
|||
public class StaticDefinition1 |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public int Value { get; set; } |
|||
} |
|||
|
|||
public class StaticDefinition2 |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public int Value { get; set; } |
|||
} |
|||
} |
|||
@ -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,149 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using Polly; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.FeatureManagement; |
|||
|
|||
public class FeatureDynamicInitializer : ITransientDependency |
|||
{ |
|||
public ILogger<FeatureDynamicInitializer> Logger { get; set; } |
|||
|
|||
protected IServiceProvider ServiceProvider { get; } |
|||
protected IOptions<FeatureManagementOptions> Options { get; } |
|||
[CanBeNull] |
|||
protected IHostApplicationLifetime ApplicationLifetime { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
protected IDynamicFeatureDefinitionStore DynamicFeatureDefinitionStore { get; } |
|||
protected IStaticFeatureSaver StaticFeatureSaver { get; } |
|||
|
|||
public FeatureDynamicInitializer( |
|||
IServiceProvider serviceProvider, |
|||
IOptions<FeatureManagementOptions> options, |
|||
ICancellationTokenProvider cancellationTokenProvider, |
|||
IDynamicFeatureDefinitionStore dynamicFeatureDefinitionStore, |
|||
IStaticFeatureSaver staticFeatureSaver) |
|||
{ |
|||
Logger = NullLogger<FeatureDynamicInitializer>.Instance; |
|||
|
|||
ServiceProvider = serviceProvider; |
|||
Options = options; |
|||
ApplicationLifetime = ServiceProvider.GetService<IHostApplicationLifetime>(); |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
DynamicFeatureDefinitionStore = dynamicFeatureDefinitionStore; |
|||
StaticFeatureSaver = staticFeatureSaver; |
|||
} |
|||
|
|||
public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) |
|||
{ |
|||
var options = Options.Value; |
|||
|
|||
if (!options.SaveStaticFeaturesToDatabase && !options.IsDynamicFeatureStoreEnabled) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
if (runInBackground) |
|||
{ |
|||
Task.Run(async () => |
|||
{ |
|||
if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) |
|||
{ |
|||
cancellationToken = ApplicationLifetime.ApplicationStopping; |
|||
} |
|||
await ExecuteInitializationAsync(options, cancellationToken); |
|||
}, cancellationToken); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
return ExecuteInitializationAsync(options, cancellationToken); |
|||
} |
|||
|
|||
protected virtual async Task ExecuteInitializationAsync(FeatureManagementOptions options, CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
using (CancellationTokenProvider.Use(cancellationToken)) |
|||
{ |
|||
if (CancellationTokenProvider.Token.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await SaveStaticFeaturesToDatabaseAsync(options, cancellationToken); |
|||
|
|||
if (CancellationTokenProvider.Token.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await PreCacheDynamicFeaturesAsync(options); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
// No need to log here since inner calls log
|
|||
} |
|||
} |
|||
|
|||
protected virtual async Task SaveStaticFeaturesToDatabaseAsync( |
|||
FeatureManagementOptions options, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
if (!options.SaveStaticFeaturesToDatabase) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await Policy |
|||
.Handle<Exception>() |
|||
.WaitAndRetryAsync( |
|||
8, |
|||
retryAttempt => TimeSpan.FromSeconds( |
|||
Volo.Abp.RandomHelper.GetRandom( |
|||
(int)Math.Pow(2, retryAttempt) * 8, |
|||
(int)Math.Pow(2, retryAttempt) * 12) |
|||
) |
|||
) |
|||
.ExecuteAsync(async _ => |
|||
{ |
|||
try |
|||
{ |
|||
await StaticFeatureSaver.SaveAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogException(ex); |
|||
throw; // Polly will catch it
|
|||
} |
|||
}, cancellationToken); |
|||
} |
|||
|
|||
protected virtual async Task PreCacheDynamicFeaturesAsync(FeatureManagementOptions options) |
|||
{ |
|||
if (!options.IsDynamicFeatureStoreEnabled) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
// Pre-cache features, so first request doesn't wait
|
|||
await DynamicFeatureDefinitionStore.GetGroupsAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogException(ex); |
|||
throw; // It will be cached in Initialize()
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.Features; |
|||
using Volo.Abp.StaticDefinitions; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.FeatureManagement; |
|||
|
|||
public class StaticFeatureDefinitionChangedEventHandler : ILocalEventHandler<StaticFeatureDefinitionChangedEvent>, ITransientDependency |
|||
{ |
|||
protected IStaticDefinitionCache<FeatureGroupDefinition, Dictionary<string, FeatureGroupDefinition>> GroupCache { get; } |
|||
protected IStaticDefinitionCache<FeatureDefinition, Dictionary<string, FeatureDefinition>> DefinitionCache { get; } |
|||
protected FeatureDynamicInitializer FeatureDynamicInitializer { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
public StaticFeatureDefinitionChangedEventHandler( |
|||
IStaticDefinitionCache<FeatureGroupDefinition, Dictionary<string, FeatureGroupDefinition>> groupCache, |
|||
IStaticDefinitionCache<FeatureDefinition, Dictionary<string, FeatureDefinition>> definitionCache, |
|||
FeatureDynamicInitializer featureDynamicInitializer, |
|||
ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
GroupCache = groupCache; |
|||
DefinitionCache = definitionCache; |
|||
FeatureDynamicInitializer = featureDynamicInitializer; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(StaticFeatureDefinitionChangedEvent eventData) |
|||
{ |
|||
await GroupCache.ClearAsync(); |
|||
await DefinitionCache.ClearAsync(); |
|||
await FeatureDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token); |
|||
} |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using Polly; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.PermissionManagement; |
|||
|
|||
public class PermissionDynamicInitializer : ITransientDependency |
|||
{ |
|||
public ILogger<PermissionDynamicInitializer> Logger { get; set; } |
|||
|
|||
protected IServiceProvider ServiceProvider { get; } |
|||
protected IOptions<PermissionManagementOptions> Options { get; } |
|||
[CanBeNull] |
|||
protected IHostApplicationLifetime ApplicationLifetime { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
protected IDynamicPermissionDefinitionStore DynamicPermissionDefinitionStore { get; } |
|||
protected IStaticPermissionSaver StaticPermissionSaver { get; } |
|||
|
|||
public PermissionDynamicInitializer( |
|||
IServiceProvider serviceProvider, |
|||
IOptions<PermissionManagementOptions> options, |
|||
ICancellationTokenProvider cancellationTokenProvider, |
|||
IDynamicPermissionDefinitionStore dynamicPermissionDefinitionStore, |
|||
IStaticPermissionSaver staticPermissionSaver) |
|||
{ |
|||
Logger = NullLogger<PermissionDynamicInitializer>.Instance; |
|||
|
|||
ServiceProvider = serviceProvider; |
|||
Options = options; |
|||
ApplicationLifetime = ServiceProvider.GetService<IHostApplicationLifetime>(); |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
DynamicPermissionDefinitionStore = dynamicPermissionDefinitionStore; |
|||
StaticPermissionSaver = staticPermissionSaver; |
|||
} |
|||
|
|||
public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) |
|||
{ |
|||
var options = Options.Value; |
|||
|
|||
if (!options.SaveStaticPermissionsToDatabase && !options.IsDynamicPermissionStoreEnabled) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
if (runInBackground) |
|||
{ |
|||
Task.Run(async () => |
|||
{ |
|||
if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) |
|||
{ |
|||
cancellationToken = ApplicationLifetime.ApplicationStopping; |
|||
} |
|||
await ExecuteInitializationAsync(options, cancellationToken); |
|||
}, cancellationToken); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
return ExecuteInitializationAsync(options, cancellationToken); |
|||
} |
|||
|
|||
protected virtual async Task ExecuteInitializationAsync(PermissionManagementOptions options, CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
using (CancellationTokenProvider.Use(cancellationToken)) |
|||
{ |
|||
if (CancellationTokenProvider.Token.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await SaveStaticPermissionsToDatabaseAsync(options, cancellationToken); |
|||
|
|||
if (CancellationTokenProvider.Token.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await PreCacheDynamicPermissionsAsync(options); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
// No need to log here since inner calls log
|
|||
} |
|||
} |
|||
|
|||
protected virtual async Task SaveStaticPermissionsToDatabaseAsync( |
|||
PermissionManagementOptions options, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
if (!options.SaveStaticPermissionsToDatabase) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await Policy |
|||
.Handle<Exception>() |
|||
.WaitAndRetryAsync( |
|||
8, |
|||
retryAttempt => TimeSpan.FromSeconds( |
|||
Volo.Abp.RandomHelper.GetRandom( |
|||
(int)Math.Pow(2, retryAttempt) * 8, |
|||
(int)Math.Pow(2, retryAttempt) * 12) |
|||
) |
|||
) |
|||
.ExecuteAsync(async _ => |
|||
{ |
|||
try |
|||
{ |
|||
await StaticPermissionSaver.SaveAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogException(ex); |
|||
|
|||
throw; // Polly will catch it
|
|||
} |
|||
}, cancellationToken); |
|||
} |
|||
|
|||
protected virtual async Task PreCacheDynamicPermissionsAsync(PermissionManagementOptions options) |
|||
{ |
|||
if (!options.IsDynamicPermissionStoreEnabled) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
// Pre-cache permissions, so first request doesn't wait
|
|||
await DynamicPermissionDefinitionStore.GetGroupsAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogException(ex); |
|||
|
|||
throw; // It will be cached in Initialize()
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.Authorization.Permissions; |
|||
using Volo.Abp.StaticDefinitions; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.PermissionManagement; |
|||
|
|||
public class StaticPermissionDefinitionChangedEventHandler : ILocalEventHandler<StaticPermissionDefinitionChangedEvent>, ITransientDependency |
|||
{ |
|||
protected IStaticDefinitionCache<PermissionGroupDefinition, Dictionary<string, PermissionGroupDefinition>> GroupCache { get; } |
|||
protected IStaticDefinitionCache<PermissionDefinition, Dictionary<string, PermissionDefinition>> DefinitionCache { get; } |
|||
protected PermissionDynamicInitializer PermissionDynamicInitializer { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
public StaticPermissionDefinitionChangedEventHandler( |
|||
IStaticDefinitionCache<PermissionGroupDefinition, Dictionary<string, PermissionGroupDefinition>> groupCache, |
|||
IStaticDefinitionCache<PermissionDefinition, Dictionary<string, PermissionDefinition>> definitionCache, |
|||
PermissionDynamicInitializer permissionDynamicInitializer, |
|||
ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
GroupCache = groupCache; |
|||
DefinitionCache = definitionCache; |
|||
PermissionDynamicInitializer = permissionDynamicInitializer; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(StaticPermissionDefinitionChangedEvent eventData) |
|||
{ |
|||
await GroupCache.ClearAsync(); |
|||
await DefinitionCache.ClearAsync(); |
|||
await PermissionDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token); |
|||
} |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
using System; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using Polly; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.SettingManagement; |
|||
|
|||
public class SettingDynamicInitializer : ITransientDependency |
|||
{ |
|||
public ILogger<SettingDynamicInitializer> Logger { get; set; } |
|||
|
|||
protected IServiceProvider ServiceProvider { get; } |
|||
protected IOptions<SettingManagementOptions> Options { get; } |
|||
[CanBeNull] |
|||
protected IHostApplicationLifetime ApplicationLifetime { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
protected IDynamicSettingDefinitionStore DynamicSettingDefinitionStore { get; } |
|||
protected IStaticSettingSaver StaticSettingSaver { get; } |
|||
|
|||
public SettingDynamicInitializer( |
|||
IServiceProvider serviceProvider, |
|||
IOptions<SettingManagementOptions> options, |
|||
ICancellationTokenProvider cancellationTokenProvider, |
|||
IDynamicSettingDefinitionStore dynamicSettingDefinitionStore, |
|||
IStaticSettingSaver staticSettingSaver) |
|||
{ |
|||
Logger = NullLogger<SettingDynamicInitializer>.Instance; |
|||
|
|||
ServiceProvider = serviceProvider; |
|||
Options = options; |
|||
ApplicationLifetime = ServiceProvider.GetService<IHostApplicationLifetime>(); |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
DynamicSettingDefinitionStore = dynamicSettingDefinitionStore; |
|||
StaticSettingSaver = staticSettingSaver; |
|||
} |
|||
|
|||
public virtual Task InitializeAsync(bool runInBackground, CancellationToken cancellationToken = default) |
|||
{ |
|||
var options = Options.Value; |
|||
|
|||
if (!options.SaveStaticSettingsToDatabase && !options.IsDynamicSettingStoreEnabled) |
|||
{ |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
if (runInBackground) |
|||
{ |
|||
Task.Run(async () => |
|||
{ |
|||
if (cancellationToken == default && ApplicationLifetime?.ApplicationStopping != null) |
|||
{ |
|||
cancellationToken = ApplicationLifetime.ApplicationStopping; |
|||
} |
|||
await ExecuteInitializationAsync(options, cancellationToken); |
|||
}, cancellationToken); |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
return ExecuteInitializationAsync(options, cancellationToken); |
|||
} |
|||
|
|||
protected virtual async Task ExecuteInitializationAsync(SettingManagementOptions options, CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
using (CancellationTokenProvider.Use(cancellationToken)) |
|||
{ |
|||
if (CancellationTokenProvider.Token.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await SaveStaticSettingsToDatabaseAsync(options, cancellationToken); |
|||
|
|||
if (CancellationTokenProvider.Token.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await PreCacheDynamicSettingsAsync(options); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
// No need to log here since inner calls log
|
|||
} |
|||
} |
|||
|
|||
protected virtual async Task SaveStaticSettingsToDatabaseAsync( |
|||
SettingManagementOptions options, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
if (!options.SaveStaticSettingsToDatabase) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
await Policy |
|||
.Handle<Exception>() |
|||
.WaitAndRetryAsync( |
|||
8, |
|||
retryAttempt => TimeSpan.FromSeconds( |
|||
Volo.Abp.RandomHelper.GetRandom( |
|||
(int)Math.Pow(2, retryAttempt) * 8, |
|||
(int)Math.Pow(2, retryAttempt) * 12) |
|||
) |
|||
) |
|||
.ExecuteAsync(async _ => |
|||
{ |
|||
try |
|||
{ |
|||
await StaticSettingSaver.SaveAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogException(ex); |
|||
throw; // Polly will catch it
|
|||
} |
|||
}, cancellationToken); |
|||
} |
|||
|
|||
protected virtual async Task PreCacheDynamicSettingsAsync(SettingManagementOptions options) |
|||
{ |
|||
if (!options.IsDynamicSettingStoreEnabled) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
// Pre-cache settings, so first request doesn't wait
|
|||
await DynamicSettingDefinitionStore.GetAllAsync(); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Logger.LogException(ex); |
|||
throw; // It will be cached in Initialize()
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.Settings; |
|||
using Volo.Abp.StaticDefinitions; |
|||
using Volo.Abp.Threading; |
|||
|
|||
namespace Volo.Abp.SettingManagement; |
|||
|
|||
public class StaticSettingDefinitionChangedEventHandler : ILocalEventHandler<StaticSettingDefinitionChangedEvent>, ITransientDependency |
|||
{ |
|||
protected IStaticDefinitionCache<SettingDefinition, Dictionary<string, SettingDefinition>> DefinitionCache { get; } |
|||
protected SettingDynamicInitializer SettingDynamicInitializer { get; } |
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
public StaticSettingDefinitionChangedEventHandler( |
|||
IStaticDefinitionCache<SettingDefinition, Dictionary<string, SettingDefinition>> definitionCache, |
|||
SettingDynamicInitializer settingDynamicInitializer, |
|||
ICancellationTokenProvider cancellationTokenProvider) |
|||
{ |
|||
DefinitionCache = definitionCache; |
|||
SettingDynamicInitializer = settingDynamicInitializer; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
} |
|||
|
|||
public virtual async Task HandleEventAsync(StaticSettingDefinitionChangedEvent eventData) |
|||
{ |
|||
await DefinitionCache.ClearAsync(); |
|||
await SettingDynamicInitializer.InitializeAsync(false, CancellationTokenProvider.Token); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||