Browse Source

Add example for custom entity cache implementation and replace functionality

pull/25088/head
maliming 2 weeks ago
parent
commit
c762a652be
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 31
      docs/en/framework/infrastructure/entity-cache.md
  2. 15
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs
  3. 18
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs
  4. 2
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs
  5. 7
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs
  6. 88
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs

31
docs/en/framework/infrastructure/entity-cache.md

@ -182,6 +182,19 @@ Both methods internally use `IDistributedCache.GetOrAddManyAsync` to batch-fetch
When you need full control over how an entity is mapped to a cache item, you can derive from `EntityCacheWithObjectMapper` and override the `MapToValue` method:
First, define the cache item class:
```csharp
public class ProductCacheDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
}
```
Then, derive from `EntityCacheWithObjectMapper` and override `MapToValue`:
```csharp
public class ProductEntityCache :
EntityCacheWithObjectMapper<Product, ProductCacheDto, Guid>
@ -211,19 +224,15 @@ public class ProductEntityCache :
Register your custom cache class in the `ConfigureServices` method of your [module class](../architecture/modularity/basics.md):
```csharp
context.Services.TryAddTransient<IEntityCache<ProductCacheDto, Guid>, ProductEntityCache>();
context.Services.TryAddTransient<ProductEntityCache>();
context.Services.Configure<AbpDistributedCacheOptions>(options =>
{
options.ConfigureCache<EntityCacheItemWrapper<ProductCacheDto>>(
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
});
context.Services.ReplaceEntityCache<ProductEntityCache, Product, ProductCacheDto, Guid>(
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
```
> If no prior `AddEntityCache` registration exists for the same cache item type, `ReplaceEntityCache` will simply add the service instead of throwing an error.
## See Also
* [Distributed caching](../fundamentals/caching.md)

15
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs

@ -50,9 +50,12 @@ public abstract class EntityCacheBase<TEntity, TEntityCacheItem, TKey> :
{
var idArray = ids.ToArray();
var cacheItems = await GetOrAddManyCacheItemsAsync(idArray);
#pragma warning disable CS8714
var cacheItemDict = cacheItems.ToDictionary(x => x.Key, x => x.Value);
#pragma warning restore CS8714
return idArray
.Select(id => cacheItems.FirstOrDefault(x => EqualityComparer<TKey>.Default.Equals(x.Key, id)).Value?.Value)
.Select(id => cacheItemDict.TryGetValue(id!, out var wrapper) ? wrapper?.Value : null)
.ToList();
}
@ -75,11 +78,14 @@ public abstract class EntityCacheBase<TEntity, TEntityCacheItem, TKey> :
{
var idArray = ids.ToArray();
var cacheItems = await GetOrAddManyCacheItemsAsync(idArray);
#pragma warning disable CS8714
var cacheItemDict = cacheItems.ToDictionary(x => x.Key, x => x.Value);
#pragma warning restore CS8714
return idArray
.Select(id =>
{
var cacheItem = cacheItems.FirstOrDefault(x => EqualityComparer<TKey>.Default.Equals(x.Key, id)).Value?.Value;
var cacheItem = cacheItemDict.TryGetValue(id!, out var wrapper) ? wrapper?.Value : null;
if (cacheItem == null)
{
throw new EntityNotFoundException(typeof(TEntity), id);
@ -105,11 +111,14 @@ public abstract class EntityCacheBase<TEntity, TEntityCacheItem, TKey> :
var entities = await Repository.GetListAsync(
x => missingKeyArray.Contains(x.Id)
);
#pragma warning disable CS8714
var entityDict = entities.ToDictionary(e => e.Id);
#pragma warning restore CS8714
return missingKeyArray
.Select(key =>
{
var entity = entities.FirstOrDefault(e => EqualityComparer<TKey>.Default.Equals(e.Id, key));
entityDict.TryGetValue(key!, out var entity);
return new KeyValuePair<TKey, EntityCacheItemWrapper<TEntityCacheItem>>(
key,
MapToCacheItem(entity)!

18
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs

@ -65,6 +65,24 @@ public static class EntityCacheServiceCollectionExtensions
return services;
}
public static IServiceCollection ReplaceEntityCache<TEntityCache, TEntity, TEntityCacheItem, TKey>(
this IServiceCollection services,
DistributedCacheEntryOptions? cacheOptions = null)
where TEntityCache : EntityCacheBase<TEntity, TEntityCacheItem, TKey>
where TEntity : Entity<TKey>
where TEntityCacheItem : class
{
services.Replace(ServiceDescriptor.Transient<IEntityCache<TEntityCacheItem, TKey>, TEntityCache>());
services.TryAddTransient<TEntityCache>();
services.Configure<AbpDistributedCacheOptions>(options =>
{
options.ConfigureCache<EntityCacheItemWrapper<TEntityCacheItem>>(cacheOptions ?? GetDefaultCacheOptions());
});
return services;
}
private static DistributedCacheEntryOptions GetDefaultCacheOptions()
{
return new DistributedCacheEntryOptions {

2
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs

@ -33,4 +33,4 @@ public interface IEntityCache<TEntityCacheItem, in TKey>
/// Throws <see cref="EntityNotFoundException"/> if any entity was not found.
/// </summary>
Task<List<TEntityCacheItem>> GetManyAsync(IEnumerable<TKey> ids);
}
}

7
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs

@ -39,6 +39,13 @@ public class TestAppModule : AbpModule
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(9)
});
context.Services.AddEntityCache<Product, ProductCacheItem2, Guid>();
// Test ReplaceEntityCache: first add default, then replace with custom implementation
context.Services.AddEntityCache<Product, CustomProductCacheItem, Guid>();
context.Services.ReplaceEntityCache<CustomProductEntityCache, Product, CustomProductCacheItem, Guid>();
// Test ReplaceEntityCache without prior registration
context.Services.ReplaceEntityCache<CustomProductEntityCacheWithoutPriorRegistration, Product, CustomProductCacheItemWithoutPriorRegistration, Guid>();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)

88
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs

@ -9,6 +9,8 @@ using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.Domain.Entities.Caching;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectMapping;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.TestApp.Testing;
@ -19,12 +21,16 @@ public abstract class EntityCache_Tests<TStartupModule> : TestAppTestBase<TStart
protected readonly IRepository<Product, Guid> ProductRepository;
protected readonly IEntityCache<Product, Guid> ProductEntityCache;
protected readonly IEntityCache<ProductCacheItem, Guid> ProductCacheItem;
protected readonly IEntityCache<CustomProductCacheItem, Guid> CustomProductCacheItem;
protected readonly IEntityCache<CustomProductCacheItemWithoutPriorRegistration, Guid> CustomProductCacheItemWithoutPriorRegistration;
protected EntityCache_Tests()
{
ProductRepository = GetRequiredService<IRepository<Product, Guid>>();
ProductEntityCache = GetRequiredService<IEntityCache<Product, Guid>>();
ProductCacheItem = GetRequiredService<IEntityCache<ProductCacheItem, Guid>>();
CustomProductCacheItem = GetRequiredService<IEntityCache<CustomProductCacheItem, Guid>>();
CustomProductCacheItemWithoutPriorRegistration = GetRequiredService<IEntityCache<CustomProductCacheItemWithoutPriorRegistration, Guid>>();
}
[Fact]
@ -190,6 +196,22 @@ public abstract class EntityCache_Tests<TStartupModule> : TestAppTestBase<TStart
productCacheItem.Price.ShouldBe(decimal.Zero);
}
[Fact]
public async Task ReplaceEntityCache_Should_Use_Custom_Mapping()
{
var product = await CustomProductCacheItem.FindAsync(TestDataBuilder.ProductId);
product.ShouldNotBeNull();
product.Name.ShouldBe("PRODUCT1");
}
[Fact]
public async Task ReplaceEntityCache_Without_Prior_Registration_Should_Work()
{
var product = await CustomProductCacheItemWithoutPriorRegistration.FindAsync(TestDataBuilder.ProductId);
product.ShouldNotBeNull();
product.Name.ShouldBe("PRODUCT1");
}
[Fact]
public void EntityCache_Default_Options_Should_Be_2_Minutes()
{
@ -275,3 +297,69 @@ public class ProductCacheItem2
public decimal Price { get; set; }
}
[Serializable]
[CacheName("CustomProductCacheItem")]
public class CustomProductCacheItem
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class CustomProductEntityCache : EntityCacheWithObjectMapper<Product, CustomProductCacheItem, Guid>
{
public CustomProductEntityCache(
IReadOnlyRepository<Product, Guid> repository,
IDistributedCache<EntityCacheItemWrapper<CustomProductCacheItem>, Guid> cache,
IUnitOfWorkManager unitOfWorkManager,
IObjectMapper objectMapper)
: base(repository, cache, unitOfWorkManager, objectMapper)
{
}
protected override CustomProductCacheItem MapToValue(Product entity)
{
return new CustomProductCacheItem
{
Id = entity.Id,
Name = entity.Name.ToUpperInvariant(),
Price = entity.Price
};
}
}
[Serializable]
[CacheName("CustomProductCacheItemWithoutPriorRegistration")]
public class CustomProductCacheItemWithoutPriorRegistration
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class CustomProductEntityCacheWithoutPriorRegistration : EntityCacheWithObjectMapper<Product, CustomProductCacheItemWithoutPriorRegistration, Guid>
{
public CustomProductEntityCacheWithoutPriorRegistration(
IReadOnlyRepository<Product, Guid> repository,
IDistributedCache<EntityCacheItemWrapper<CustomProductCacheItemWithoutPriorRegistration>, Guid> cache,
IUnitOfWorkManager unitOfWorkManager,
IObjectMapper objectMapper)
: base(repository, cache, unitOfWorkManager, objectMapper)
{
}
protected override CustomProductCacheItemWithoutPriorRegistration MapToValue(Product entity)
{
return new CustomProductCacheItemWithoutPriorRegistration
{
Id = entity.Id,
Name = entity.Name.ToUpperInvariant(),
Price = entity.Price
};
}
}

Loading…
Cancel
Save