mirror of https://github.com/abpframework/abp.git
76 changed files with 6746 additions and 1008 deletions
@ -0,0 +1,99 @@ |
|||
### ASP.NET Core SignalR New Features — Summary |
|||
|
|||
In this article, I’ll highlight the latest .**NET 9 SignalR updates** for ASP.NET Core 9.0. |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
### SignalR Hub Accepts Base Classes |
|||
|
|||
SignalR `Hub` class can now get a base class of a polymorphic class. As you see in the example below, I can send `Animal` to `Process` method. Before .NET 9, we could only pass the derived classes: `Cat` and `Dog`. |
|||
|
|||
```csharp |
|||
/*** My Base Class is Animal ***/ |
|||
[JsonPolymorphic] |
|||
[JsonDerivedType(typeof(Cat), nameof(Cat))] |
|||
[JsonDerivedType(typeof(Dog), nameof(Dog))] |
|||
private class Animal |
|||
{ |
|||
public string Name { get; set; } |
|||
} |
|||
|
|||
/*** CAT derived from Animal ***/ |
|||
private class Cat : Animal |
|||
{ |
|||
public CatTypes CatType { get; set; } |
|||
} |
|||
|
|||
/*** DOG derived from Animal ***/ |
|||
private class Dog : Animal |
|||
{ |
|||
public DogTypes DogType { get; set; } |
|||
} |
|||
|
|||
|
|||
public class MyHub : Hub |
|||
{ |
|||
/*** We can use the base type Animal here ***/ |
|||
public void Process(Animal animal) |
|||
{ |
|||
if (animal is Cat) { ... } |
|||
else if (animal is Dog) { ... } |
|||
} |
|||
} |
|||
|
|||
``` |
|||
|
|||
|
|||
|
|||
### Better Diagnostics and Telemetry |
|||
|
|||
Microsoft focuses mainly on .NET Aspire nowadays. That’s why SignalR now integrates more deeply with the .NET Activity API, which is commonly used for distributed tracing. The enhancement is implemented for better monitoring in [.NET Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash#using-the-dashboard-with-net-aspire-projects). To support this feature: |
|||
|
|||
1- Add these packages to your`csproj`: |
|||
|
|||
```xml |
|||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" /> |
|||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" /> |
|||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" /> |
|||
``` |
|||
|
|||
2- Add the following startup code to your host project: |
|||
|
|||
```csharp |
|||
builder.Services.AddSignalR(); |
|||
/* After AddSignalR use AddOpenTelemetry() */ |
|||
builder |
|||
.Services |
|||
.AddOpenTelemetry() |
|||
.WithTracing(tracing => |
|||
{ |
|||
if (builder.Environment.IsDevelopment()) |
|||
{ |
|||
tracing.SetSampler(new AlwaysOnSampler()); //for dev env monitor all traces |
|||
} |
|||
|
|||
tracing.AddAspNetCoreInstrumentation(); |
|||
tracing.AddSource("Microsoft.AspNetCore.SignalR.Server"); |
|||
}); |
|||
|
|||
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); |
|||
``` |
|||
|
|||
Finally, you’ll see the **SignalR Hub** events on the [Aspire Dashboard](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dashboard/overview): |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
### Trimming and Native AOT Support |
|||
|
|||
With .NET 9, **trimming** and **native** **Ahead Of Time** compilation are **supported**. This will improve our application performance. To support AOT, your SignalR object serialization needs to be JSON, and you must use the `System.Text.Json` s[ource generator](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation). Also on the server side, [you shouldn't use](https://github.com/dotnet/aspnetcore/issues/56179) `IAsyncEnumerable<T>` and `ChannelReader<T>` where `T` is a ValueType (`struct`) for Hub method arguments. One more limitation; [Strongly typed hubs](https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-8.0#strongly-typed-hubs) aren't supported with Native AOT (`PublishAot`). And you should use only `Task`, `Task<T>`, `ValueTask`, `ValueTask<T>` for `async` return types. |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
That's all the new features coming to SignalR in .NET 9! |
|||
Happy coding 🧑🏽💻 |
|||
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 66 KiB |
@ -0,0 +1,459 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.Caching.Hybrid; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Logging.Abstractions; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.ExceptionHandling; |
|||
using Volo.Abp.MultiTenancy; |
|||
using Volo.Abp.Threading; |
|||
using Volo.Abp.Uow; |
|||
|
|||
namespace Volo.Abp.Caching.Hybrid; |
|||
|
|||
/// <summary>
|
|||
/// Represents a hybrid cache of <typeparamref name="TCacheItem"/> items.
|
|||
/// </summary>
|
|||
/// <typeparam name="TCacheItem">The type of the cache item being cached.</typeparam>
|
|||
public class AbpHybridCache<TCacheItem> : IHybridCache<TCacheItem> |
|||
where TCacheItem : class |
|||
{ |
|||
public IHybridCache<TCacheItem, string> InternalCache { get; } |
|||
|
|||
public AbpHybridCache(IHybridCache<TCacheItem, string> internalCache) |
|||
{ |
|||
InternalCache = internalCache; |
|||
} |
|||
|
|||
public virtual async Task<TCacheItem?> GetOrCreateAsync(string key, Func<Task<TCacheItem>> factory, Func<HybridCacheEntryOptions>? optionsFactory = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) |
|||
{ |
|||
return await InternalCache.GetOrCreateAsync(key, factory, optionsFactory, hideErrors, considerUow, token); |
|||
} |
|||
|
|||
public virtual async Task SetAsync(string key, TCacheItem value, HybridCacheEntryOptions? options = null, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) |
|||
{ |
|||
await InternalCache.SetAsync(key, value, options, hideErrors, considerUow, token); |
|||
} |
|||
|
|||
public virtual async Task RemoveAsync(string key, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) |
|||
{ |
|||
await InternalCache.RemoveAsync(key, hideErrors, considerUow, token); |
|||
} |
|||
|
|||
public virtual async Task RemoveManyAsync(IEnumerable<string> keys, bool? hideErrors = null, bool considerUow = false, CancellationToken token = default) |
|||
{ |
|||
await InternalCache.RemoveManyAsync(keys, hideErrors, considerUow, token); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a hybrid cache of <typeparamref name="TCacheItem"/> items.
|
|||
/// Uses <typeparamref name="TCacheKey"/> as the key type.
|
|||
/// </summary>
|
|||
/// <typeparam name="TCacheItem">The type of cache item being cached.</typeparam>
|
|||
/// <typeparam name="TCacheKey">The type of cache key being used.</typeparam>
|
|||
public class AbpHybridCache<TCacheItem, TCacheKey> : IHybridCache<TCacheItem, TCacheKey> |
|||
where TCacheItem : class |
|||
where TCacheKey : notnull |
|||
{ |
|||
public const string UowCacheName = "AbpHybridCache"; |
|||
|
|||
public ILogger<AbpHybridCache<TCacheItem, TCacheKey>> Logger { get; set; } |
|||
|
|||
protected string CacheName { get; set; } = default!; |
|||
|
|||
protected bool IgnoreMultiTenancy { get; set; } |
|||
|
|||
protected IServiceProvider ServiceProvider { get; } |
|||
|
|||
protected HybridCache HybridCache { get; } |
|||
|
|||
protected IDistributedCache DistributedCacheCache { get; } |
|||
|
|||
protected ICancellationTokenProvider CancellationTokenProvider { get; } |
|||
|
|||
protected IDistributedCacheKeyNormalizer KeyNormalizer { get; } |
|||
|
|||
protected IServiceScopeFactory ServiceScopeFactory { get; } |
|||
|
|||
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
|||
|
|||
protected SemaphoreSlim SyncSemaphore { get; } |
|||
|
|||
protected HybridCacheEntryOptions DefaultCacheOptions = default!; |
|||
|
|||
protected AbpHybridCacheOptions DistributedCacheOption { get; } |
|||
|
|||
public AbpHybridCache( |
|||
IServiceProvider serviceProvider, |
|||
IOptions<AbpHybridCacheOptions> distributedCacheOption, |
|||
HybridCache hybridCache, |
|||
IDistributedCache distributedCache, |
|||
ICancellationTokenProvider cancellationTokenProvider, |
|||
IDistributedCacheSerializer serializer, |
|||
IDistributedCacheKeyNormalizer keyNormalizer, |
|||
IServiceScopeFactory serviceScopeFactory, |
|||
IUnitOfWorkManager unitOfWorkManager) |
|||
{ |
|||
ServiceProvider = serviceProvider; |
|||
DistributedCacheOption = distributedCacheOption.Value; |
|||
HybridCache = hybridCache; |
|||
DistributedCacheCache = distributedCache; |
|||
CancellationTokenProvider = cancellationTokenProvider; |
|||
Logger = NullLogger<AbpHybridCache<TCacheItem, TCacheKey>>.Instance; |
|||
KeyNormalizer = keyNormalizer; |
|||
ServiceScopeFactory = serviceScopeFactory; |
|||
UnitOfWorkManager = unitOfWorkManager; |
|||
|
|||
SyncSemaphore = new SemaphoreSlim(1, 1); |
|||
|
|||
SetDefaultOptions(); |
|||
} |
|||
|
|||
protected virtual string NormalizeKey(TCacheKey key) |
|||
{ |
|||
return KeyNormalizer.NormalizeKey( |
|||
new DistributedCacheKeyNormalizeArgs( |
|||
key.ToString()!, |
|||
CacheName, |
|||
IgnoreMultiTenancy |
|||
) |
|||
); |
|||
} |
|||
|
|||
protected virtual HybridCacheEntryOptions GetDefaultCacheEntryOptions() |
|||
{ |
|||
foreach (var configure in DistributedCacheOption.CacheConfigurators) |
|||
{ |
|||
var options = configure.Invoke(CacheName); |
|||
if (options != null) |
|||
{ |
|||
return options; |
|||
} |
|||
} |
|||
|
|||
return DistributedCacheOption.GlobalHybridCacheEntryOptions; |
|||
} |
|||
|
|||
protected virtual void SetDefaultOptions() |
|||
{ |
|||
CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem)); |
|||
|
|||
//IgnoreMultiTenancy
|
|||
IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true); |
|||
|
|||
//Configure default cache entry options
|
|||
DefaultCacheOptions = GetDefaultCacheEntryOptions(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or Creates a cache item with the given key. If no cache item is found for the given key then adds a cache item
|
|||
/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
|
|||
/// </summary>
|
|||
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
|
|||
/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
|
|||
/// <param name="optionsFactory">The cache options for the factory delegate.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The cache item.</returns>
|
|||
public virtual async Task<TCacheItem?> GetOrCreateAsync( |
|||
TCacheKey key, |
|||
Func<Task<TCacheItem>> factory, |
|||
Func<HybridCacheEntryOptions>? optionsFactory = null, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default) |
|||
{ |
|||
token = CancellationTokenProvider.FallbackToProvider(token); |
|||
hideErrors ??= DistributedCacheOption.HideErrors; |
|||
|
|||
TCacheItem? value = null; |
|||
|
|||
if (!considerUow) |
|||
{ |
|||
try |
|||
{ |
|||
value = await HybridCache.GetOrCreateAsync( |
|||
key: NormalizeKey(key), |
|||
factory: async cancel => await factory(), |
|||
options: optionsFactory?.Invoke(), |
|||
tags: null, |
|||
cancellationToken: token); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (hideErrors == true) |
|||
{ |
|||
await HandleExceptionAsync(ex); |
|||
return null; |
|||
} |
|||
|
|||
throw; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
using (await SyncSemaphore.LockAsync(token)) |
|||
{ |
|||
if (ShouldConsiderUow(considerUow)) |
|||
{ |
|||
value = GetUnitOfWorkCache().GetOrDefault(key)?.GetUnRemovedValueOrNull(); |
|||
if (value != null) |
|||
{ |
|||
return value; |
|||
} |
|||
} |
|||
|
|||
var bytes = await DistributedCacheCache.GetAsync(NormalizeKey(key), token); |
|||
if (bytes != null) |
|||
{ |
|||
return ResolveSerializer().Deserialize(new ReadOnlySequence<byte>(bytes, 0, bytes.Length));; |
|||
} |
|||
|
|||
value = await factory(); |
|||
|
|||
if (ShouldConsiderUow(considerUow)) |
|||
{ |
|||
var uowCache = GetUnitOfWorkCache(); |
|||
if (uowCache.TryGetValue(key, out var item)) |
|||
{ |
|||
item.SetValue(value); |
|||
} |
|||
else |
|||
{ |
|||
uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value)); |
|||
} |
|||
} |
|||
|
|||
await SetAsync(key, value, optionsFactory?.Invoke(), hideErrors, considerUow, token); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (hideErrors == true) |
|||
{ |
|||
await HandleExceptionAsync(ex); |
|||
return null; |
|||
} |
|||
|
|||
throw; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the cache item value for the provided key.
|
|||
/// </summary>
|
|||
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
|
|||
/// <param name="value">The cache item value to set in the cache.</param>
|
|||
/// <param name="options">The cache options for the value.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
|
|||
public virtual async Task SetAsync( |
|||
TCacheKey key, |
|||
TCacheItem value, |
|||
HybridCacheEntryOptions? options = null, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default) |
|||
{ |
|||
async Task SetRealCache() |
|||
{ |
|||
token = CancellationTokenProvider.FallbackToProvider(token); |
|||
hideErrors ??= DistributedCacheOption.HideErrors; |
|||
|
|||
try |
|||
{ |
|||
await HybridCache.SetAsync( |
|||
key: NormalizeKey(key), |
|||
value: value, |
|||
options: options ?? DefaultCacheOptions, |
|||
tags: null, |
|||
cancellationToken: token |
|||
); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (hideErrors == true) |
|||
{ |
|||
await HandleExceptionAsync(ex); |
|||
return; |
|||
} |
|||
|
|||
throw; |
|||
} |
|||
} |
|||
|
|||
if (ShouldConsiderUow(considerUow)) |
|||
{ |
|||
var uowCache = GetUnitOfWorkCache(); |
|||
if (uowCache.TryGetValue(key, out _)) |
|||
{ |
|||
uowCache[key].SetValue(value); |
|||
} |
|||
else |
|||
{ |
|||
uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value)); |
|||
} |
|||
|
|||
UnitOfWorkManager.Current?.OnCompleted(SetRealCache); |
|||
} |
|||
else |
|||
{ |
|||
await SetRealCache(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the cache item for given key from cache.
|
|||
/// </summary>
|
|||
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
|
|||
public virtual async Task RemoveAsync( |
|||
TCacheKey key, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default) |
|||
{ |
|||
await RemoveManyAsync(new[] { key }, hideErrors, considerUow, token); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes the cache items for given keys from cache.
|
|||
/// </summary>
|
|||
/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
|
|||
public async Task RemoveManyAsync( |
|||
IEnumerable<TCacheKey> keys, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default) |
|||
{ |
|||
var keyArray = keys.ToArray(); |
|||
|
|||
async Task RemoveRealCache() |
|||
{ |
|||
hideErrors ??= DistributedCacheOption.HideErrors; |
|||
|
|||
try |
|||
{ |
|||
await HybridCache.RemoveAsync( |
|||
keyArray.Select(NormalizeKey), token); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (hideErrors == true) |
|||
{ |
|||
await HandleExceptionAsync(ex); |
|||
return; |
|||
} |
|||
|
|||
throw; |
|||
} |
|||
} |
|||
|
|||
if (ShouldConsiderUow(considerUow)) |
|||
{ |
|||
var uowCache = GetUnitOfWorkCache(); |
|||
|
|||
foreach (var key in keyArray) |
|||
{ |
|||
if (uowCache.TryGetValue(key, out _)) |
|||
{ |
|||
uowCache[key].RemoveValue(); |
|||
} |
|||
} |
|||
|
|||
UnitOfWorkManager.Current?.OnCompleted(RemoveRealCache); |
|||
} |
|||
else |
|||
{ |
|||
await RemoveRealCache(); |
|||
} |
|||
} |
|||
|
|||
protected virtual async Task HandleExceptionAsync(Exception ex) |
|||
{ |
|||
Logger.LogException(ex, LogLevel.Warning); |
|||
|
|||
using (var scope = ServiceScopeFactory.CreateScope()) |
|||
{ |
|||
await scope.ServiceProvider |
|||
.GetRequiredService<IExceptionNotifier>() |
|||
.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning)); |
|||
} |
|||
} |
|||
|
|||
protected virtual bool ShouldConsiderUow(bool considerUow) |
|||
{ |
|||
return considerUow && UnitOfWorkManager.Current != null; |
|||
} |
|||
|
|||
protected virtual string GetUnitOfWorkCacheKey() |
|||
{ |
|||
return UowCacheName + CacheName; |
|||
} |
|||
|
|||
protected virtual Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>> GetUnitOfWorkCache() |
|||
{ |
|||
if (UnitOfWorkManager.Current == null) |
|||
{ |
|||
throw new AbpException($"There is no active UOW."); |
|||
} |
|||
|
|||
return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(), |
|||
key => new Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>>()); |
|||
} |
|||
|
|||
private readonly ConcurrentDictionary<Type, object> _serializersCache = new(); |
|||
|
|||
protected virtual IHybridCacheSerializer<TCacheItem> ResolveSerializer() |
|||
{ |
|||
if (_serializersCache.TryGetValue(typeof(TCacheItem), out var serializer)) |
|||
{ |
|||
return serializer.As<IHybridCacheSerializer<TCacheItem>>(); |
|||
} |
|||
|
|||
serializer = ServiceProvider.GetService<IHybridCacheSerializer<TCacheItem>>(); |
|||
if (serializer is null) |
|||
{ |
|||
var factories = ServiceProvider.GetServices<IHybridCacheSerializerFactory>().ToArray(); |
|||
Array.Reverse(factories); |
|||
foreach (var factory in factories) |
|||
{ |
|||
if (factory.TryCreateSerializer<TCacheItem>(out var current)) |
|||
{ |
|||
serializer = current; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (serializer is null) |
|||
{ |
|||
throw new InvalidOperationException($"No {nameof(IHybridCacheSerializer<TCacheItem>)} configured for type '{typeof(TCacheItem).Name}'"); |
|||
} |
|||
|
|||
return serializer.As<IHybridCacheSerializer<TCacheItem>>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System.Buffers; |
|||
using System.Text.Json; |
|||
using Microsoft.Extensions.Caching.Hybrid; |
|||
|
|||
namespace Volo.Abp.Caching.Hybrid; |
|||
|
|||
public class AbpHybridCacheJsonSerializer<T> : IHybridCacheSerializer<T> |
|||
{ |
|||
protected JsonSerializerOptions JsonSerializerOptions { get; } |
|||
|
|||
public AbpHybridCacheJsonSerializer(JsonSerializerOptions jsonSerializerOptions) |
|||
{ |
|||
JsonSerializerOptions = jsonSerializerOptions; |
|||
} |
|||
|
|||
public virtual T Deserialize(ReadOnlySequence<byte> source) |
|||
{ |
|||
var reader = new Utf8JsonReader(source); |
|||
return JsonSerializer.Deserialize<T>(ref reader, JsonSerializerOptions)!; |
|||
} |
|||
|
|||
public virtual void Serialize(T value, IBufferWriter<byte> target) |
|||
{ |
|||
using var writer = new Utf8JsonWriter(target); |
|||
JsonSerializer.Serialize<T>(writer, value, JsonSerializerOptions); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using Microsoft.Extensions.Caching.Hybrid; |
|||
using Microsoft.Extensions.Options; |
|||
using Volo.Abp.Json.SystemTextJson; |
|||
|
|||
namespace Volo.Abp.Caching.Hybrid; |
|||
|
|||
public class AbpHybridCacheJsonSerializerFactory : IHybridCacheSerializerFactory |
|||
{ |
|||
protected IOptions<AbpSystemTextJsonSerializerOptions> Options { get; } |
|||
|
|||
public AbpHybridCacheJsonSerializerFactory(IOptions<AbpSystemTextJsonSerializerOptions> options) |
|||
{ |
|||
Options = options; |
|||
} |
|||
|
|||
public bool TryCreateSerializer<T>(out IHybridCacheSerializer<T>? serializer) |
|||
{ |
|||
if (typeof(T) == typeof(string) || typeof(T) == typeof(byte[])) |
|||
{ |
|||
serializer = null; |
|||
return false; |
|||
} |
|||
|
|||
serializer = new AbpHybridCacheJsonSerializer<T>(Options.Value.JsonSerializerOptions); |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Microsoft.Extensions.Caching.Hybrid; |
|||
|
|||
namespace Volo.Abp.Caching.Hybrid; |
|||
|
|||
public class AbpHybridCacheOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Throw or hide exceptions for the distributed cache.
|
|||
/// </summary>
|
|||
public bool HideErrors { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// Cache key prefix.
|
|||
/// </summary>
|
|||
public string KeyPrefix { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Global Cache entry options.
|
|||
/// </summary>
|
|||
public HybridCacheEntryOptions GlobalHybridCacheEntryOptions { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// List of all cache configurators.
|
|||
/// (func argument:Name of cache)
|
|||
/// </summary>
|
|||
public List<Func<string, HybridCacheEntryOptions?>> CacheConfigurators { get; set; } //TODO: use a configurator interface instead?
|
|||
|
|||
public AbpHybridCacheOptions() |
|||
{ |
|||
CacheConfigurators = new List<Func<string, HybridCacheEntryOptions?>>(); |
|||
GlobalHybridCacheEntryOptions = new HybridCacheEntryOptions(); |
|||
KeyPrefix = ""; |
|||
} |
|||
|
|||
public void ConfigureCache<TCacheItem>(HybridCacheEntryOptions? options) |
|||
{ |
|||
ConfigureCache(typeof(TCacheItem), options); |
|||
} |
|||
|
|||
public void ConfigureCache(Type cacheItemType, HybridCacheEntryOptions? options) |
|||
{ |
|||
ConfigureCache(CacheNameAttribute.GetCacheName(cacheItemType), options); |
|||
} |
|||
|
|||
public void ConfigureCache(string cacheName, HybridCacheEntryOptions? options) |
|||
{ |
|||
CacheConfigurators.Add(name => cacheName != name ? null : options); |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.Extensions.Caching.Hybrid; |
|||
|
|||
namespace Volo.Abp.Caching.Hybrid; |
|||
|
|||
/// <summary>
|
|||
/// Represents a hybrid cache of <typeparamref name="TCacheItem"/> items.
|
|||
/// </summary>
|
|||
/// <typeparam name="TCacheItem">The type of the cache item being cached.</typeparam>
|
|||
public interface IHybridCache<TCacheItem> : IHybridCache<TCacheItem, string> |
|||
where TCacheItem : class |
|||
{ |
|||
IHybridCache<TCacheItem, string> InternalCache { get; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a hybrid cache of <typeparamref name="TCacheItem"/> items.
|
|||
/// Uses <typeparamref name="TCacheKey"/> as the key type.
|
|||
/// </summary>
|
|||
/// <typeparam name="TCacheItem">The type of cache item being cached.</typeparam>
|
|||
/// <typeparam name="TCacheKey">The type of cache key being used.</typeparam>
|
|||
public interface IHybridCache<TCacheItem, TCacheKey> |
|||
where TCacheItem : class |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or Creates a cache item with the given key. If no cache item is found for the given key then adds a cache item
|
|||
/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
|
|||
/// </summary>
|
|||
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
|
|||
/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
|
|||
/// <param name="optionsFactory">The cache options for the factory delegate.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The cache item.</returns>
|
|||
Task<TCacheItem?> GetOrCreateAsync( |
|||
[NotNull]TCacheKey key, |
|||
Func<Task<TCacheItem>> factory, |
|||
Func<HybridCacheEntryOptions>? optionsFactory = null, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default); |
|||
|
|||
/// <summary>
|
|||
/// Sets the cache item value for the provided key.
|
|||
/// </summary>
|
|||
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
|
|||
/// <param name="value">The cache item value to set in the cache.</param>
|
|||
/// <param name="options">The cache options for the value.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
|
|||
Task SetAsync( |
|||
[NotNull]TCacheKey key, |
|||
TCacheItem value, |
|||
HybridCacheEntryOptions? options = null, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default); |
|||
|
|||
/// <summary>
|
|||
/// Removes the cache item for given key from cache.
|
|||
/// </summary>
|
|||
/// <param name="key">The key of cached item to be retrieved from the cache.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
|
|||
Task RemoveAsync( |
|||
[NotNull]TCacheKey key, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default); |
|||
|
|||
/// <summary>
|
|||
/// Removes the cache items for given keys from cache.
|
|||
/// </summary>
|
|||
/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
|
|||
/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
|
|||
/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
|
|||
/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
|
|||
/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
|
|||
Task RemoveManyAsync( |
|||
IEnumerable<TCacheKey> keys, |
|||
bool? hideErrors = null, |
|||
bool considerUow = false, |
|||
CancellationToken token = default); |
|||
} |
|||
@ -0,0 +1,519 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Shouldly; |
|||
using Volo.Abp.Caching.Hybrid; |
|||
using Volo.Abp.Testing; |
|||
using Volo.Abp.Uow; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Caching; |
|||
|
|||
public class HybridCache_Tests : AbpIntegratedTest<AbpCachingTestModule> |
|||
{ |
|||
[Fact] |
|||
public async Task Should_GetOrCreate_Set_And_Remove_Cache_Items() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheKey = Guid.NewGuid().ToString(); |
|||
|
|||
//GetOrCreateAsync
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("john nash"))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("john nash"); |
|||
|
|||
//SetAsync
|
|||
await personCache.SetAsync(cacheKey, new PersonCacheItem("baris")); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("john nash"))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("baris"); |
|||
|
|||
//RemoveAsync
|
|||
await personCache.RemoveAsync(cacheKey); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("lucas"))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("lucas"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task GetOrCreateAsync() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheKey = Guid.NewGuid().ToString(); |
|||
const string personName = "john nash"; |
|||
|
|||
//Will execute the factory method to create the cache item
|
|||
|
|||
bool factoryExecuted = false; |
|||
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, |
|||
() => |
|||
{ |
|||
factoryExecuted = true; |
|||
return Task.FromResult(new PersonCacheItem(personName)); |
|||
}); |
|||
|
|||
factoryExecuted.ShouldBeTrue(); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
|
|||
//This time, it will not execute the factory
|
|||
|
|||
factoryExecuted = false; |
|||
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, |
|||
() => |
|||
{ |
|||
factoryExecuted = true; |
|||
return Task.FromResult(new PersonCacheItem(personName)); |
|||
}); |
|||
|
|||
factoryExecuted.ShouldBeFalse(); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SameClassName_But_DiffNamespace_Should_Not_Use_Same_Cache() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
var otherPersonCache = GetRequiredService<IHybridCache<Sail.Testing.Caching.PersonCacheItem>>(); |
|||
|
|||
var cacheKey = Guid.NewGuid().ToString(); |
|||
const string personName = "john nash"; |
|||
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
var cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName))); |
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe(personName); |
|||
|
|||
await personCache.RemoveAsync(cacheKey); |
|||
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName + "1"))); |
|||
cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName + "1"))); |
|||
|
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName + "1"); |
|||
|
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe(personName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Set_Get_And_Remove_Cache_Items_With_Integer_Type_CacheKey() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem, int>>(); |
|||
|
|||
var cacheKey = 42; |
|||
const string personName = "john nash"; |
|||
|
|||
//GetOrCreateAsync
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
|
|||
//SetAsync
|
|||
await personCache.SetAsync(cacheKey, new PersonCacheItem("baris")); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("baris"); |
|||
|
|||
//RemoveAsync
|
|||
await personCache.RemoveAsync(cacheKey); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("lucas"))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("lucas"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task GetOrAddAsync_With_Integer_Type_CacheKey() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem, int>>(); |
|||
|
|||
var cacheKey = 42; |
|||
const string personName = "john nash"; |
|||
|
|||
//Will execute the factory method to create the cache item
|
|||
|
|||
bool factoryExecuted = false; |
|||
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, |
|||
() => |
|||
{ |
|||
factoryExecuted = true; |
|||
return Task.FromResult(new PersonCacheItem(personName)); |
|||
}); |
|||
|
|||
factoryExecuted.ShouldBeTrue(); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
|
|||
//This time, it will not execute the factory
|
|||
|
|||
factoryExecuted = false; |
|||
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, |
|||
() => |
|||
{ |
|||
factoryExecuted = true; |
|||
return Task.FromResult(new PersonCacheItem(personName)); |
|||
}); |
|||
|
|||
factoryExecuted.ShouldBeFalse(); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task SameClassName_But_DiffNamespace_Should_Not_Use_Same_Cache_With_Integer_CacheKey() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem, int>>(); |
|||
var otherPersonCache = GetRequiredService<IHybridCache<Sail.Testing.Caching.PersonCacheItem, int>>(); |
|||
|
|||
var cacheKey = 42; |
|||
const string personName = "john nash"; |
|||
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
var cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName))); |
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe(personName); |
|||
|
|||
await personCache.RemoveAsync(cacheKey); |
|||
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName + "1"))); |
|||
cacheItem1 = await otherPersonCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new Sail.Testing.Caching.PersonCacheItem(personName + "1"))); |
|||
|
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName + "1"); |
|||
|
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe(personName); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Set_Get_And_Remove_Cache_Items_With_Object_Type_CacheKey() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem, ComplexObjectAsCacheKey>>(); |
|||
|
|||
var cacheKey = new ComplexObjectAsCacheKey { Name = "DummyData", Age = 42 }; |
|||
const string personName = "john nash"; |
|||
|
|||
//GetOrCreateAsync
|
|||
var cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe(personName); |
|||
|
|||
//SetAsync
|
|||
await personCache.SetAsync(cacheKey, new PersonCacheItem("baris")); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("baris"); |
|||
|
|||
//RemoveAsync
|
|||
await personCache.RemoveAsync(cacheKey); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem = await personCache.GetOrCreateAsync(cacheKey, () => Task.FromResult(new PersonCacheItem("lucas"))); |
|||
cacheItem.ShouldNotBeNull(); |
|||
cacheItem.Name.ShouldBe("lucas"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Set_Get_And_Remove_Cache_Items_For_Same_Object_Type_With_Different_CacheKeys() |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem, ComplexObjectAsCacheKey>>(); |
|||
|
|||
var cache1Key = new ComplexObjectAsCacheKey { Name = "John", Age = 42 }; |
|||
var cache2Key = new ComplexObjectAsCacheKey { Name = "Jenny", Age = 24 }; |
|||
const string personName = "john nash"; |
|||
|
|||
//GetOrCreateAsync
|
|||
var cacheItem1 = await personCache.GetOrCreateAsync(cache1Key, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
var cacheItem2 = await personCache.GetOrCreateAsync(cache2Key, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe(personName); |
|||
cacheItem2.ShouldNotBeNull(); |
|||
cacheItem2.Name.ShouldBe(personName); |
|||
|
|||
//SetAsync
|
|||
cacheItem1 = new PersonCacheItem("baris"); |
|||
cacheItem2 = new PersonCacheItem("jack"); |
|||
await personCache.SetAsync(cache1Key, cacheItem1); |
|||
await personCache.SetAsync(cache2Key, cacheItem2); |
|||
|
|||
//GetOrCreateAsync
|
|||
cacheItem1 = await personCache.GetOrCreateAsync(cache1Key, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem2 = await personCache.GetOrCreateAsync(cache2Key, () => Task.FromResult(new PersonCacheItem(personName))); |
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe("baris"); |
|||
cacheItem2.ShouldNotBeNull(); |
|||
cacheItem2.Name.ShouldBe("jack"); |
|||
|
|||
//Remove
|
|||
await personCache.RemoveAsync(cache1Key); |
|||
await personCache.RemoveAsync(cache2Key); |
|||
|
|||
//Get (not exists since removed)
|
|||
cacheItem1 = await personCache.GetOrCreateAsync(cache1Key, () => Task.FromResult(new PersonCacheItem("lucas"))); |
|||
cacheItem2 = await personCache.GetOrCreateAsync(cache2Key, () => Task.FromResult(new PersonCacheItem("peter"))); |
|||
cacheItem1.ShouldNotBeNull(); |
|||
cacheItem1.Name.ShouldBe("lucas"); |
|||
cacheItem2.ShouldNotBeNull(); |
|||
cacheItem2.Name.ShouldBe("peter"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Cache_Should_Only_Available_In_Uow_For_GetOrCreateAsync() |
|||
{ |
|||
const string key = "testkey"; |
|||
|
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
await personCache.SetAsync(key, new PersonCacheItem("lucas"), considerUow: true); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("lucas"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john3"); |
|||
|
|||
uow.OnCompleted(async () => |
|||
{ |
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("lucas"); |
|||
}); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Cache_Should_Rollback_With_Uow_For_GetOrCreateAsync() |
|||
{ |
|||
const string key = "testkey"; |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
await personCache.SetAsync(key, new PersonCacheItem("john3"), considerUow: true); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john3"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john5")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
} |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john6")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Cache_Should_Only_Available_In_Uow_For_SetAsync() |
|||
{ |
|||
const string key = "testkey"; |
|||
|
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john3"); |
|||
|
|||
uow.OnCompleted(async () => |
|||
{ |
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
}); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Cache_Should_Rollback_With_Uow_For_SetAsync() |
|||
{ |
|||
const string key = "testkey"; |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
await personCache.SetAsync(key, new PersonCacheItem("john2"), considerUow: true); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john2"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
} |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john5")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Cache_Should_Only_Available_In_Uow_For_RemoveAsync() |
|||
{ |
|||
const string key = "testkey"; |
|||
|
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
await personCache.SetAsync(key, new PersonCacheItem("john"), considerUow: true); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john2")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
await personCache.RemoveAsync(key, considerUow: true); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john3"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john4")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john4"); |
|||
|
|||
uow.OnCompleted(async () => |
|||
{ |
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john5")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john3"); |
|||
}); |
|||
|
|||
await uow.CompleteAsync(); |
|||
} |
|||
} |
|||
|
|||
public async Task Cache_Should_Rollback_With_Uow_For_RemoveAsync() |
|||
{ |
|||
const string key = "testkey"; |
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin()) |
|||
{ |
|||
await personCache.SetAsync(key, new PersonCacheItem("john2"), considerUow: true); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john2"); |
|||
|
|||
await personCache.RemoveAsync(key, considerUow: true); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: true); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john3")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
} |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(key, () => Task.FromResult(new PersonCacheItem("john")), considerUow: false); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Remove_Multiple_Items_Async() |
|||
{ |
|||
var testkey = "testkey"; |
|||
var testkey2 = "testkey2"; |
|||
var testkey3 = new[] { testkey, testkey2 }; |
|||
|
|||
var personCache = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
|
|||
var cacheValue = await personCache.GetOrCreateAsync(testkey, () => Task.FromResult(new PersonCacheItem("john"))); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(testkey2, () => Task.FromResult(new PersonCacheItem("jack"))); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("jack"); |
|||
|
|||
await personCache.RemoveManyAsync(testkey3); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(testkey, () => Task.FromResult(new PersonCacheItem("john2"))); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("john2"); |
|||
|
|||
cacheValue = await personCache.GetOrCreateAsync(testkey2, () => Task.FromResult(new PersonCacheItem("jack2"))); |
|||
cacheValue.ShouldNotBeNull(); |
|||
cacheValue.Name.ShouldBe("jack2"); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_Get_Same_Cache_Set_When_Resolve_With_Or_Without_Key() |
|||
{ |
|||
var cache1 = GetRequiredService<IHybridCache<PersonCacheItem>>(); |
|||
var cache2 = GetRequiredService<IHybridCache<PersonCacheItem, string>>(); |
|||
|
|||
cache1.InternalCache.ShouldBe(cache2); |
|||
|
|||
await cache1.SetAsync("john", new PersonCacheItem("john")); |
|||
|
|||
var item1 = await cache1.GetOrCreateAsync("john", () => Task.FromResult(new PersonCacheItem("john2"))); |
|||
item1.ShouldNotBeNull(); |
|||
item1.Name.ShouldBe("john"); |
|||
|
|||
var item2 = await cache1.GetOrCreateAsync("john", () => Task.FromResult(new PersonCacheItem("john3"))); |
|||
item2.ShouldNotBeNull(); |
|||
item2.Name.ShouldBe("john"); |
|||
} |
|||
} |
|||
@ -1,22 +0,0 @@ |
|||
using System; |
|||
using Microsoft.AspNetCore.Builder; |
|||
using Microsoft.AspNetCore.Hosting; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Volo.Abp; |
|||
|
|||
namespace VoloDocs.Web |
|||
{ |
|||
public class Startup |
|||
{ |
|||
public void ConfigureServices(IServiceCollection services) |
|||
{ |
|||
services.AddApplication<VoloDocsWebModule>(); |
|||
} |
|||
|
|||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) |
|||
{ |
|||
app.InitializeApplication(); |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue