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