mirror of https://github.com/abpframework/abp.git
committed by
GitHub
23 changed files with 566 additions and 14 deletions
@ -0,0 +1,70 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.Domain.Entities.Events; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.EventBus; |
||||
|
using Volo.Abp.Uow; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Caching; |
||||
|
|
||||
|
public abstract class EntityCacheBase<TEntity, TEntityCacheItem, TKey> : |
||||
|
IEntityCache<TEntityCacheItem, TKey>, |
||||
|
ILocalEventHandler<EntityChangedEventData<TEntity>> |
||||
|
where TEntity : Entity<TKey> |
||||
|
where TEntityCacheItem : class |
||||
|
{ |
||||
|
protected IReadOnlyRepository<TEntity, TKey> Repository { get; } |
||||
|
protected IDistributedCache<TEntityCacheItem, TKey> Cache { get; } |
||||
|
protected IUnitOfWorkManager UnitOfWorkManager { get; } |
||||
|
|
||||
|
protected EntityCacheBase( |
||||
|
IReadOnlyRepository<TEntity, TKey> repository, |
||||
|
IDistributedCache<TEntityCacheItem, TKey> cache, |
||||
|
IUnitOfWorkManager unitOfWorkManager) |
||||
|
{ |
||||
|
Repository = repository; |
||||
|
Cache = cache; |
||||
|
UnitOfWorkManager = unitOfWorkManager; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<TEntityCacheItem> FindAsync(TKey id) |
||||
|
{ |
||||
|
return await Cache.GetOrAddAsync( |
||||
|
id, |
||||
|
async () => MapToCacheItem(await Repository.FindAsync(id)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<TEntityCacheItem> GetAsync(TKey id) |
||||
|
{ |
||||
|
return await Cache.GetOrAddAsync( |
||||
|
id, |
||||
|
async () => MapToCacheItem(await Repository.GetAsync(id)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
protected abstract TEntityCacheItem MapToCacheItem(TEntity entity); |
||||
|
|
||||
|
public async Task HandleEventAsync(EntityChangedEventData<TEntity> eventData) |
||||
|
{ |
||||
|
if (eventData is EntityCreatedEventData<TEntity>) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
/* Why we are using double remove: |
||||
|
* First Cache.RemoveAsync drops the cache item in a unit of work. |
||||
|
* Some other application / thread may read the value from database and put it to the cache again |
||||
|
* before the UOW completes. |
||||
|
* The second Cache.RemoveAsync drops the cache item after the database transaction is complete. |
||||
|
* Only the second Cache.RemoveAsync may not be enough if the application crashes just after the UOW completes. |
||||
|
*/ |
||||
|
|
||||
|
await Cache.RemoveAsync(eventData.Entity.Id); |
||||
|
|
||||
|
if(UnitOfWorkManager.Current != null) |
||||
|
{ |
||||
|
await Cache.RemoveAsync(eventData.Entity.Id, considerUow: true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,82 @@ |
|||||
|
using System; |
||||
|
using JetBrains.Annotations; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
||||
|
using Volo.Abp.Caching; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Caching; |
||||
|
|
||||
|
public static class EntityCacheServiceCollectionExtensions |
||||
|
{ |
||||
|
public static IServiceCollection AddEntityCache<TEntity, TKey>( |
||||
|
this IServiceCollection services, |
||||
|
[CanBeNull] DistributedCacheEntryOptions cacheOptions = null) |
||||
|
where TEntity : Entity<TKey> |
||||
|
{ |
||||
|
services |
||||
|
.TryAddTransient< |
||||
|
IEntityCache<TEntity, TKey>, |
||||
|
EntityCacheWithoutCacheItem<TEntity, TKey> |
||||
|
>(); |
||||
|
services |
||||
|
.TryAddTransient<EntityCacheWithoutCacheItem<TEntity, TKey>>(); |
||||
|
|
||||
|
services.Configure<AbpDistributedCacheOptions>(options => |
||||
|
{ |
||||
|
options.ConfigureCache<TEntity>(cacheOptions ?? GetDefaultCacheOptions()); |
||||
|
}); |
||||
|
|
||||
|
return services; |
||||
|
} |
||||
|
|
||||
|
public static IServiceCollection AddEntityCache<TEntity, TEntityCacheItem, TKey>( |
||||
|
this IServiceCollection services, |
||||
|
[CanBeNull] DistributedCacheEntryOptions cacheOptions = null) |
||||
|
where TEntity : Entity<TKey> |
||||
|
where TEntityCacheItem : class |
||||
|
{ |
||||
|
services |
||||
|
.TryAddTransient< |
||||
|
IEntityCache<TEntityCacheItem, TKey>, |
||||
|
EntityCacheWithObjectMapper<TEntity, TEntityCacheItem, TKey> |
||||
|
>(); |
||||
|
services |
||||
|
.TryAddTransient<EntityCacheWithObjectMapper<TEntity, TEntityCacheItem, TKey>>(); |
||||
|
|
||||
|
services.Configure<AbpDistributedCacheOptions>(options => |
||||
|
{ |
||||
|
options.ConfigureCache<TEntityCacheItem>(cacheOptions ?? GetDefaultCacheOptions()); |
||||
|
}); |
||||
|
|
||||
|
return services; |
||||
|
} |
||||
|
|
||||
|
public static IServiceCollection AddEntityCache<TObjectMapperContext, TEntity, TEntityCacheItem, TKey>( |
||||
|
this IServiceCollection services, |
||||
|
[CanBeNull] DistributedCacheEntryOptions cacheOptions = null) |
||||
|
where TEntity : Entity<TKey> |
||||
|
where TEntityCacheItem : class |
||||
|
{ |
||||
|
services |
||||
|
.TryAddTransient< |
||||
|
IEntityCache<TEntityCacheItem, TKey>, |
||||
|
EntityCacheWithObjectMapperContext<TObjectMapperContext, TEntity, TEntityCacheItem, TKey> |
||||
|
>(); |
||||
|
services.TryAddTransient<EntityCacheWithObjectMapperContext<TObjectMapperContext, TEntity, TEntityCacheItem, TKey>>(); |
||||
|
|
||||
|
services.Configure<AbpDistributedCacheOptions>(options => |
||||
|
{ |
||||
|
options.ConfigureCache<TEntityCacheItem>(cacheOptions ?? GetDefaultCacheOptions()); |
||||
|
}); |
||||
|
|
||||
|
return services; |
||||
|
} |
||||
|
|
||||
|
private static DistributedCacheEntryOptions GetDefaultCacheOptions() |
||||
|
{ |
||||
|
return new DistributedCacheEntryOptions { |
||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2) |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.ObjectMapping; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Caching; |
||||
|
|
||||
|
public class EntityCacheWithObjectMapper<TEntity, TEntityCacheItem, TKey> : |
||||
|
EntityCacheBase<TEntity, TEntityCacheItem, TKey> |
||||
|
where TEntity : Entity<TKey> |
||||
|
where TEntityCacheItem : class |
||||
|
{ |
||||
|
protected IObjectMapper ObjectMapper { get; } |
||||
|
|
||||
|
public EntityCacheWithObjectMapper( |
||||
|
IReadOnlyRepository<TEntity, TKey> repository, |
||||
|
IDistributedCache<TEntityCacheItem, TKey> cache, |
||||
|
IObjectMapper objectMapper) |
||||
|
: base( |
||||
|
repository, |
||||
|
cache) |
||||
|
{ |
||||
|
ObjectMapper = objectMapper; |
||||
|
} |
||||
|
|
||||
|
protected override TEntityCacheItem MapToCacheItem(TEntity entity) |
||||
|
{ |
||||
|
if (entity == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (typeof(TEntity) == typeof(TEntityCacheItem)) |
||||
|
{ |
||||
|
return entity.As<TEntityCacheItem>(); |
||||
|
} |
||||
|
|
||||
|
return ObjectMapper.Map<TEntity, TEntityCacheItem>(entity); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.ObjectMapping; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Caching; |
||||
|
|
||||
|
public class EntityCacheWithObjectMapperContext<TObjectMapperContext, TEntity, TEntityCacheItem, TKey> : |
||||
|
EntityCacheWithObjectMapper<TEntity, TEntityCacheItem, TKey> |
||||
|
where TEntity : Entity<TKey> |
||||
|
where TEntityCacheItem : class |
||||
|
{ |
||||
|
public EntityCacheWithObjectMapperContext( |
||||
|
IReadOnlyRepository<TEntity, TKey> repository, |
||||
|
IDistributedCache<TEntityCacheItem, TKey> cache, |
||||
|
IObjectMapper<TObjectMapperContext> objectMapper) // Intentionally injected with TContext
|
||||
|
: base( |
||||
|
repository, |
||||
|
cache, |
||||
|
objectMapper) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Caching; |
||||
|
|
||||
|
public class EntityCacheWithoutCacheItem<TEntity, TKey> : |
||||
|
EntityCacheBase<TEntity, TEntity, TKey> |
||||
|
where TEntity : Entity<TKey> |
||||
|
{ |
||||
|
public EntityCacheWithoutCacheItem( |
||||
|
IReadOnlyRepository<TEntity, TKey> repository, |
||||
|
IDistributedCache<TEntity, TKey> cache) |
||||
|
: base( |
||||
|
repository, |
||||
|
cache) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
protected override TEntity MapToCacheItem(TEntity entity) |
||||
|
{ |
||||
|
return entity; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using JetBrains.Annotations; |
||||
|
|
||||
|
namespace Volo.Abp.Domain.Entities.Caching; |
||||
|
|
||||
|
public interface IEntityCache<TEntityCacheItem, in TKey> |
||||
|
where TEntityCacheItem : class |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the entity with given <paramref name="id"/>,
|
||||
|
/// or returns null if the entity was not found.
|
||||
|
/// </summary>
|
||||
|
[ItemCanBeNull] |
||||
|
Task<TEntityCacheItem> FindAsync(TKey id); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the entity with given <paramref name="id"/>,
|
||||
|
/// or throws <see cref="EntityNotFoundException"/> if the entity was not found.
|
||||
|
/// </summary>
|
||||
|
[ItemNotNull] |
||||
|
Task<TEntityCacheItem> GetAsync(TKey id); |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
using Volo.Abp.TestApp.Testing; |
||||
|
|
||||
|
namespace Volo.Abp.EntityFrameworkCore.Domain; |
||||
|
|
||||
|
public class EntityCache_Tests : EntityCache_Tests<AbpEntityFrameworkCoreTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
using Volo.Abp.TestApp.Testing; |
||||
|
|
||||
|
namespace Volo.Abp.MemoryDb.DomainEvents; |
||||
|
|
||||
|
public class EntityCache_Tests : EntityCache_Tests<AbpMemoryDbTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using Volo.Abp.TestApp.Testing; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.MongoDB.Domain; |
||||
|
|
||||
|
[Collection(MongoTestCollection.Name)] |
||||
|
public class EntityCache_Tests : EntityCache_Tests<AbpMongoDbTestModule> |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,93 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Distributed; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Caching; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.TestApp; |
||||
|
|
||||
|
[DisableConventionalRegistration] |
||||
|
public class TestMemoryDistributedCache : MemoryDistributedCache, ICacheSupportsMultipleItems |
||||
|
{ |
||||
|
public TestMemoryDistributedCache(IOptions<MemoryDistributedCacheOptions> optionsAccessor) |
||||
|
: base(optionsAccessor) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public TestMemoryDistributedCache(IOptions<MemoryDistributedCacheOptions> optionsAccessor, ILoggerFactory loggerFactory) |
||||
|
: base(optionsAccessor, loggerFactory) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public byte[][] GetMany(IEnumerable<string> keys) |
||||
|
{ |
||||
|
var values = new List<byte[]>(); |
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
values.Add(Get(key)); |
||||
|
} |
||||
|
return values.ToArray(); |
||||
|
} |
||||
|
|
||||
|
public async Task<byte[][]> GetManyAsync(IEnumerable<string> keys, CancellationToken token = default) |
||||
|
{ |
||||
|
var values = new List<byte[]>(); |
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
values.Add(await GetAsync(key, token)); |
||||
|
} |
||||
|
return values.ToArray(); |
||||
|
} |
||||
|
|
||||
|
public void SetMany(IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options) |
||||
|
{ |
||||
|
foreach (var item in items) |
||||
|
{ |
||||
|
Set(item.Key, item.Value, options); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task SetManyAsync(IEnumerable<KeyValuePair<string, byte[]>> items, DistributedCacheEntryOptions options, CancellationToken token = default) |
||||
|
{ |
||||
|
foreach (var item in items) |
||||
|
{ |
||||
|
await SetAsync(item.Key, item.Value, options, token); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void RefreshMany(IEnumerable<string> keys) |
||||
|
{ |
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
Refresh(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task RefreshManyAsync(IEnumerable<string> keys, CancellationToken token = default) |
||||
|
{ |
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
await RefreshAsync(key, token); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void RemoveMany(IEnumerable<string> keys) |
||||
|
{ |
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
Remove(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task RemoveManyAsync(IEnumerable<string> keys, CancellationToken token = default) |
||||
|
{ |
||||
|
foreach (var key in keys) |
||||
|
{ |
||||
|
await RemoveAsync(key, token); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
using System; |
||||
|
using System.Text.Json.Serialization; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Domain.Entities; |
||||
|
using Volo.Abp.Domain.Entities.Auditing; |
||||
|
using Volo.Abp.Domain.Entities.Caching; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.TestApp.Testing; |
||||
|
|
||||
|
public abstract class EntityCache_Tests<TStartupModule> : TestAppTestBase<TStartupModule> |
||||
|
where TStartupModule : IAbpModule |
||||
|
{ |
||||
|
protected readonly IRepository<Product, Guid> ProductRepository; |
||||
|
protected readonly IEntityCache<Product, Guid> ProductEntityCache; |
||||
|
protected readonly IEntityCache<ProductCacheItem, Guid> ProductCacheItem; |
||||
|
|
||||
|
protected EntityCache_Tests() |
||||
|
{ |
||||
|
ProductRepository = GetRequiredService<IRepository<Product, Guid>>(); |
||||
|
ProductEntityCache = GetRequiredService<IEntityCache<Product, Guid>>(); |
||||
|
ProductCacheItem = GetRequiredService<IEntityCache<ProductCacheItem, Guid>>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Return_Null_IF_Entity_Not_Exist() |
||||
|
{ |
||||
|
var notExistId = Guid.NewGuid(); |
||||
|
(await ProductEntityCache.FindAsync(notExistId)).ShouldBeNull(); |
||||
|
(await ProductCacheItem.FindAsync(notExistId)).ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Throw_EntityNotFoundException_IF_Entity_Not_Exist() |
||||
|
{ |
||||
|
var notExistId = Guid.NewGuid(); |
||||
|
await Assert.ThrowsAsync<EntityNotFoundException>(() => ProductEntityCache.GetAsync(notExistId)); |
||||
|
await Assert.ThrowsAsync<EntityNotFoundException>(() => ProductCacheItem.GetAsync(notExistId)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Return_EntityCache() |
||||
|
{ |
||||
|
var product = await ProductEntityCache.FindAsync(TestDataBuilder.ProductId); |
||||
|
product.ShouldNotBeNull(); |
||||
|
product.Id.ShouldBe(TestDataBuilder.ProductId); |
||||
|
product.Name.ShouldBe("Product1"); |
||||
|
product.Price.ShouldBe(decimal.One); |
||||
|
|
||||
|
var productCacheItem = await ProductCacheItem.FindAsync(product.Id); |
||||
|
productCacheItem.ShouldNotBeNull(); |
||||
|
productCacheItem.Id.ShouldBe(TestDataBuilder.ProductId); |
||||
|
productCacheItem.Name.ShouldBe("Product1"); |
||||
|
productCacheItem.Price.ShouldBe(decimal.One); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Return_Null_IF_Deleted() |
||||
|
{ |
||||
|
await ProductRepository.DeleteAsync(TestDataBuilder.ProductId); |
||||
|
|
||||
|
(await ProductEntityCache.FindAsync(TestDataBuilder.ProductId)).ShouldBeNull(); |
||||
|
(await ProductCacheItem.FindAsync(TestDataBuilder.ProductId)).ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Return_New_EntityCache_IF_Updated() |
||||
|
{ |
||||
|
(await ProductEntityCache.FindAsync(TestDataBuilder.ProductId)).ShouldNotBeNull(); |
||||
|
(await ProductCacheItem.FindAsync(TestDataBuilder.ProductId)).ShouldNotBeNull(); |
||||
|
|
||||
|
var product = await ProductRepository.FindAsync(TestDataBuilder.ProductId); |
||||
|
product.Name = "Product2"; |
||||
|
product.Price = decimal.Zero; |
||||
|
await ProductRepository.UpdateAsync(product); |
||||
|
|
||||
|
product = await ProductEntityCache.FindAsync(product.Id); |
||||
|
product.ShouldNotBeNull(); |
||||
|
product.Id.ShouldBe(TestDataBuilder.ProductId); |
||||
|
product.Name.ShouldBe("Product2"); |
||||
|
product.Price.ShouldBe(decimal.Zero); |
||||
|
|
||||
|
var productCacheItem = await ProductCacheItem.FindAsync(product.Id); |
||||
|
productCacheItem.ShouldNotBeNull(); |
||||
|
productCacheItem.Id.ShouldBe(TestDataBuilder.ProductId); |
||||
|
productCacheItem.Name.ShouldBe("Product2"); |
||||
|
productCacheItem.Price.ShouldBe(decimal.Zero); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Serializable] |
||||
|
public class Product : FullAuditedAggregateRoot<Guid> |
||||
|
{ |
||||
|
public Product(Guid id, string name, decimal price) |
||||
|
: base(id) |
||||
|
{ |
||||
|
Name = name; |
||||
|
Price = price; |
||||
|
} |
||||
|
|
||||
|
[JsonInclude] |
||||
|
public override Guid Id { get; protected set; } |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public decimal Price { get; set; } |
||||
|
} |
||||
|
|
||||
|
[Serializable] |
||||
|
public class ProductCacheItem |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public decimal Price { get; set; } |
||||
|
} |
||||
Loading…
Reference in new issue