From 8a0b5c2d4c0bebbf574adf34fccc08023ca63983 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:09:28 +0000 Subject: [PATCH 2/5] Add GetManyAsync/FindManyAsync to IEntityCache and extract MapToValue virtual method Co-authored-by: hikalkan <1210527+hikalkan@users.noreply.github.com> --- .../framework/infrastructure/entity-cache.md | 77 +++++++++++++++++++ .../Entities/Caching/EntityCacheBase.cs | 62 ++++++++++++++- .../Caching/EntityCacheWithObjectMapper.cs | 9 ++- .../Domain/Entities/Caching/IEntityCache.cs | 19 ++++- .../Abp/TestApp/Testing/EntityCache_Tests.cs | 71 +++++++++++++++++ 5 files changed, 233 insertions(+), 5 deletions(-) diff --git a/docs/en/framework/infrastructure/entity-cache.md b/docs/en/framework/infrastructure/entity-cache.md index 50f1bc1b3c..d43e3695e7 100644 --- a/docs/en/framework/infrastructure/entity-cache.md +++ b/docs/en/framework/infrastructure/entity-cache.md @@ -147,6 +147,83 @@ context.Services.AddEntityCache( * Entity classes should be serializable/deserializable to/from JSON to be cached (because it's serialized to JSON when saving in the [Distributed Cache](../fundamentals/caching.md)). If your entity class is not serializable, you can consider using a cache-item/DTO class instead, as explained before. * Entity Caching System is designed as **read-only**. You should use the standard [repository](../architecture/domain-driven-design/repositories.md) methods to manipulate the entity if you need to. If you need to manipulate (update) the entity, do not get it from the entity cache. Instead, read it from the repository, change it and update using the repository. +## Getting Multiple Entities + +In addition to the single-entity methods `FindAsync` and `GetAsync`, the `IEntityCache` service also provides `FindManyAsync` and `GetManyAsync` methods for retrieving multiple entities at once: + +```csharp +public class ProductAppService : ApplicationService, IProductAppService +{ + private readonly IEntityCache _productCache; + + public ProductAppService(IEntityCache productCache) + { + _productCache = productCache; + } + + public async Task> GetManyAsync(List ids) + { + return await _productCache.GetManyAsync(ids); + } + + public async Task> FindManyAsync(List ids) + { + return await _productCache.FindManyAsync(ids); + } +} +``` + +* `GetManyAsync` throws `EntityNotFoundException` if any entity is not found for the given IDs. +* `FindManyAsync` returns a list where each entry corresponds to the given ID in the same order; an entry will be `null` if the entity was not found. + +Both methods internally use `IDistributedCache.GetOrAddManyAsync` to batch-fetch only the cache-missed entities from the database, making them more efficient than calling `FindAsync` or `GetAsync` in a loop. + +## Custom Object Mapping + +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: + +```csharp +public class ProductEntityCache : + EntityCacheWithObjectMapper +{ + public ProductEntityCache( + IReadOnlyRepository repository, + IDistributedCache, Guid> cache, + IUnitOfWorkManager unitOfWorkManager, + IObjectMapper objectMapper) + : base(repository, cache, unitOfWorkManager, objectMapper) + { + } + + protected override ProductCacheDto MapToValue(Product entity) + { + // Custom mapping logic here + return new ProductCacheDto + { + Id = entity.Id, + Name = entity.Name.ToUpperInvariant(), + Price = entity.Price + }; + } +} +``` + +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) + }); +}); +``` + ## 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 7cea931c12..3fe5ca9456 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 @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Volo.Abp.Caching; using Volo.Abp.Data; using Volo.Abp.Domain.Entities.Events; @@ -44,6 +46,16 @@ public abstract class EntityCacheBase : }))?.Value; } + public virtual async Task> FindManyAsync(IEnumerable ids) + { + var idArray = ids.ToArray(); + var cacheItems = await GetOrAddManyCacheItemsAsync(idArray); + + return idArray + .Select(id => cacheItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Key, id)).Value?.Value) + .ToList(); + } + public virtual async Task GetAsync(TKey id) { return (await Cache.GetOrAddAsync( @@ -59,6 +71,54 @@ public abstract class EntityCacheBase : }))!.Value!; } + public virtual async Task> GetManyAsync(IEnumerable ids) + { + var idArray = ids.ToArray(); + var cacheItems = await GetOrAddManyCacheItemsAsync(idArray); + + return idArray + .Select(id => + { + var cacheItem = cacheItems.FirstOrDefault(x => EqualityComparer.Default.Equals(x.Key, id)).Value?.Value; + if (cacheItem == null) + { + throw new EntityNotFoundException(typeof(TEntity), id); + } + + return cacheItem; + }) + .ToList(); + } + + protected virtual async Task?>[]> GetOrAddManyCacheItemsAsync(TKey[] ids) + { + return await Cache.GetOrAddManyAsync( + ids, + async missingKeys => + { + if (HasObjectExtensionInfo()) + { + Repository.EnableTracking(); + } + + var missingKeyArray = missingKeys.ToArray(); + var entities = await Repository.GetListAsync( + x => missingKeyArray.Contains(x.Id) + ); + + return missingKeyArray + .Select(key => + { + var entity = entities.FirstOrDefault(e => EqualityComparer.Default.Equals(e.Id, key)); + return new KeyValuePair>( + key, + MapToCacheItem(entity)! + ); + }) + .ToList(); + }); + } + protected virtual bool HasObjectExtensionInfo() { return typeof(IHasExtraProperties).IsAssignableFrom(typeof(TEntity)) && diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs index 62b3162cdd..472443e307 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Caching/EntityCacheWithObjectMapper.cs @@ -30,11 +30,16 @@ public class EntityCacheWithObjectMapper : return null; } + return new EntityCacheItemWrapper(MapToValue(entity)); + } + + protected virtual TEntityCacheItem MapToValue(TEntity entity) + { if (typeof(TEntity) == typeof(TEntityCacheItem)) { - return new EntityCacheItemWrapper(entity.As()); + return entity.As(); } - return new EntityCacheItemWrapper(ObjectMapper.Map(entity)); + return ObjectMapper.Map(entity); } } 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 bd8ba80a63..8f1dd0931e 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 @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; using JetBrains.Annotations; namespace Volo.Abp.Domain.Entities.Caching; @@ -11,11 +12,25 @@ public interface IEntityCache /// or returns null if the entity was not found. /// Task FindAsync(TKey id); - + + /// + /// Gets multiple entities with the given . + /// Returns a list where each entry corresponds to the given id in the same order. + /// An entry will be null if the entity was not found for the corresponding id. + /// + Task> FindManyAsync(IEnumerable ids); + /// /// Gets the entity with given , /// or throws if the entity was not found. /// [ItemNotNull] Task GetAsync(TKey id); + + /// + /// Gets multiple entities with the given . + /// Returns a list where each entry corresponds to the given id in the same order. + /// 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/Testing/EntityCache_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs index 6ef8da9b7c..8f30c65f27 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 @@ -35,6 +35,19 @@ public abstract class EntityCache_Tests : TestAppTestBase : TestAppTestBase>(() => ProductCacheItem.GetAsync(notExistId)); } + [Fact] + public async Task GetMany_Should_Throw_EntityNotFoundException_For_Not_Existing_Entities() + { + var notExistId = Guid.NewGuid(); + await Assert.ThrowsAsync(() => ProductEntityCache.GetManyAsync(new[] { notExistId })); + await Assert.ThrowsAsync(() => ProductCacheItem.GetManyAsync(new[] { notExistId })); + } + [Fact] public async Task Should_Return_EntityCache() { @@ -63,6 +84,56 @@ public abstract class EntityCache_Tests : TestAppTestBase Date: Sat, 14 Mar 2026 10:26:31 +0800 Subject: [PATCH 3/5] Add example for custom entity cache implementation and replace functionality --- .../framework/infrastructure/entity-cache.md | 31 ++++--- .../Entities/Caching/EntityCacheBase.cs | 15 +++- .../EntityCacheServiceCollectionExtensions.cs | 18 ++++ .../Domain/Entities/Caching/IEntityCache.cs | 2 +- .../Volo/Abp/TestApp/TestAppModule.cs | 7 ++ .../Abp/TestApp/Testing/EntityCache_Tests.cs | 88 +++++++++++++++++++ 6 files changed, 146 insertions(+), 15 deletions(-) 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 + }; + } +} From 81d34da46cf4a847688cdd69c5a0e8c64a38e835 Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 14 Mar 2026 10:51:47 +0800 Subject: [PATCH 4/5] Refactor entity cache to use decimal for price and enhance FindManyAsync/GetManyAsync to handle duplicate IDs --- .../framework/infrastructure/entity-cache.md | 6 ++--- .../Entities/Caching/EntityCacheBase.cs | 9 ++++--- .../EntityCacheServiceCollectionExtensions.cs | 8 +++++++ .../Abp/TestApp/Testing/EntityCache_Tests.cs | 24 +++++++++++++++++++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/en/framework/infrastructure/entity-cache.md b/docs/en/framework/infrastructure/entity-cache.md index 8eace4c6d8..16adef5986 100644 --- a/docs/en/framework/infrastructure/entity-cache.md +++ b/docs/en/framework/infrastructure/entity-cache.md @@ -26,7 +26,7 @@ public class Product : AggregateRoot public string Name { get; set; } public string Description { get; set; } - public float Price { get; set; } + public decimal Price { get; set; } public int StockCount { get; set; } } ``` @@ -72,7 +72,7 @@ public class ProductDto : EntityDto { public string Name { get; set; } public string Description { get; set; } - public float Price { get; set; } + public decimal Price { get; set; } public int StockCount { get; set; } } ``` @@ -189,7 +189,7 @@ public class ProductCacheDto { public Guid Id { get; set; } public string Name { get; set; } - public float Price { get; set; } + public decimal Price { get; set; } } ``` 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 e04e96e798..f2525416d4 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 @@ -49,7 +49,8 @@ public abstract class EntityCacheBase : public virtual async Task> FindManyAsync(IEnumerable ids) { var idArray = ids.ToArray(); - var cacheItems = await GetOrAddManyCacheItemsAsync(idArray); + var distinctIds = idArray.Distinct().ToArray(); + var cacheItems = await GetOrAddManyCacheItemsAsync(distinctIds); #pragma warning disable CS8714 var cacheItemDict = cacheItems.ToDictionary(x => x.Key, x => x.Value); #pragma warning restore CS8714 @@ -77,7 +78,8 @@ public abstract class EntityCacheBase : public virtual async Task> GetManyAsync(IEnumerable ids) { var idArray = ids.ToArray(); - var cacheItems = await GetOrAddManyCacheItemsAsync(idArray); + var distinctIds = idArray.Distinct().ToArray(); + var cacheItems = await GetOrAddManyCacheItemsAsync(distinctIds); #pragma warning disable CS8714 var cacheItemDict = cacheItems.ToDictionary(x => x.Key, x => x.Value); #pragma warning restore CS8714 @@ -109,7 +111,8 @@ public abstract class EntityCacheBase : var missingKeyArray = missingKeys.ToArray(); var entities = await Repository.GetListAsync( - x => missingKeyArray.Contains(x.Id) + x => missingKeyArray.Contains(x.Id), + includeDetails: true ); #pragma warning disable CS8714 var entityDict = entities.ToDictionary(e => e.Id); 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 b2aa58cc4c..2919b6b0d5 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 @@ -80,6 +80,14 @@ public static class EntityCacheServiceCollectionExtensions options.ConfigureCache>(cacheOptions ?? GetDefaultCacheOptions()); }); + if (typeof(TEntity) == typeof(TEntityCacheItem)) + { + services.Configure(options => + { + options.Modifiers.Add(new AbpIncludeNonPublicPropertiesModifiers().CreateModifyAction(x => x.Id)); + }); + } + return services; } 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 ff0dc41f05..09913caaf2 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 @@ -196,6 +196,30 @@ public abstract class EntityCache_Tests : TestAppTestBase Date: Sat, 14 Mar 2026 11:16:32 +0800 Subject: [PATCH 5/5] Use generic EntityNotFoundException in GetManyAsync for consistency with GetAsync --- .../Volo/Abp/Domain/Entities/Caching/EntityCacheBase.cs | 2 +- .../Volo/Abp/TestApp/Testing/EntityCache_Tests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 f2525416d4..6fff3cf67c 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 @@ -90,7 +90,7 @@ public abstract class EntityCacheBase : var cacheItem = cacheItemDict.TryGetValue(id!, out var wrapper) ? wrapper?.Value : null; if (cacheItem == null) { - throw new EntityNotFoundException(typeof(TEntity), id); + throw new EntityNotFoundException(id); } return cacheItem; 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 09913caaf2..d4f278d468 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 @@ -66,8 +66,8 @@ public abstract class EntityCache_Tests : TestAppTestBase(() => ProductEntityCache.GetManyAsync(new[] { notExistId })); - await Assert.ThrowsAsync(() => ProductCacheItem.GetManyAsync(new[] { notExistId })); + await Assert.ThrowsAsync>(() => ProductEntityCache.GetManyAsync(new[] { notExistId })); + await Assert.ThrowsAsync>(() => ProductCacheItem.GetManyAsync(new[] { notExistId })); } [Fact]