diff --git a/docs/en/framework/infrastructure/entity-cache.md b/docs/en/framework/infrastructure/entity-cache.md index d43e3695e7..8eace4c6d8 100644 --- a/docs/en/framework/infrastructure/entity-cache.md +++ b/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 @@ -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, ProductEntityCache>(); -context.Services.TryAddTransient(); - -context.Services.Configure(options => -{ - options.ConfigureCache>( - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) - }); -}); +context.Services.ReplaceEntityCache( + 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) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs index 3fe5ca9456..e04e96e798 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs @@ -50,9 +50,12 @@ public abstract class EntityCacheBase : { 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.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 : { 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.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 : 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.Default.Equals(e.Id, key)); + entityDict.TryGetValue(key!, out var entity); return new KeyValuePair>( key, MapToCacheItem(entity)! diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs index c35406b45b..b2aa58cc4c 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheServiceCollectionExtensions.cs +++ b/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( + this IServiceCollection services, + DistributedCacheEntryOptions? cacheOptions = null) + where TEntityCache : EntityCacheBase + where TEntity : Entity + where TEntityCacheItem : class + { + services.Replace(ServiceDescriptor.Transient, TEntityCache>()); + services.TryAddTransient(); + + services.Configure(options => + { + options.ConfigureCache>(cacheOptions ?? GetDefaultCacheOptions()); + }); + + return services; + } + private static DistributedCacheEntryOptions GetDefaultCacheOptions() { return new DistributedCacheEntryOptions { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs index 8f1dd0931e..3441d7bdcc 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/IEntityCache.cs @@ -33,4 +33,4 @@ public interface IEntityCache /// Throws if any entity was not found. /// Task> GetManyAsync(IEnumerable ids); -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs index 438bf4b3b9..466d3c9dd7 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestAppModule.cs +++ b/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(); + + // Test ReplaceEntityCache: first add default, then replace with custom implementation + context.Services.AddEntityCache(); + context.Services.ReplaceEntityCache(); + + // Test ReplaceEntityCache without prior registration + context.Services.ReplaceEntityCache(); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs index 8f30c65f27..ff0dc41f05 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs +++ b/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 : TestAppTestBase ProductRepository; protected readonly IEntityCache ProductEntityCache; protected readonly IEntityCache ProductCacheItem; + protected readonly IEntityCache CustomProductCacheItem; + protected readonly IEntityCache CustomProductCacheItemWithoutPriorRegistration; protected EntityCache_Tests() { ProductRepository = GetRequiredService>(); ProductEntityCache = GetRequiredService>(); ProductCacheItem = GetRequiredService>(); + CustomProductCacheItem = GetRequiredService>(); + CustomProductCacheItemWithoutPriorRegistration = GetRequiredService>(); } [Fact] @@ -190,6 +196,22 @@ public abstract class EntityCache_Tests : TestAppTestBase +{ + public CustomProductEntityCache( + IReadOnlyRepository repository, + IDistributedCache, 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 +{ + public CustomProductEntityCacheWithoutPriorRegistration( + IReadOnlyRepository repository, + IDistributedCache, 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 + }; + } +}