10 KiB
//[doc-seo]
{
"Description": "Learn how to efficiently implement entity caching in ABP, enhancing performance by reducing database calls and ensuring data consistency."
}
Entity Cache
ABP provides an entity caching system that works on top of the distributed caching system. It does the following operations on behalf of you:
- Gets the entity from the database (by using the repositories) in its first call and then gets it from the cache in subsequent calls.
- Automatically invalidates the cached entity if the entity is updated or deleted. Thus, it will be retrieved from the database in the next call and will be re-cached.
Caching Entity Objects
IEntityCache<TEntityCacheItem, TKey> is a simple service provided by the ABP for caching entities. Assume that you have a Product entity as shown below:
public class Product : AggregateRoot<Guid>
{
public Product(Guid id)
{
Id = id;
}
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int StockCount { get; set; }
}
If you want to cache this entity, you should first configure the dependency injection system to register the IEntityCache service in the ConfigureServices method of your module class:
context.Services.AddEntityCache<Product, Guid>();
Now you can inject the IEntityCache<Product, Guid> service wherever you need:
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IEntityCache<Product, Guid> _productCache;
public ProductAppService(IEntityCache<Product, Guid> productCache)
{
_productCache = productCache;
}
public async Task<ProductDto> GetAsync(Guid id)
{
var product = await _productCache.GetAsync(id);
return ObjectMapper.Map<Product, ProductDto>(product);
}
}
Note that we've used the
ObjectMapperservice to map fromProducttoProductDto. You should configure that object mapping to make that example service properly work.
That's all. The cache name (in the distributed cache server) will be the full name (with namespace) of the Product class. You can use the [CacheName] attribute to change it. Please refer to the caching document for details.
Using a Cache Item Class
In the previous section, we've directly cached the Product entity. In that case, the Product class must be serializable to JSON (and deserializable from JSON). Sometimes that might not be possible or you may want to use another class to store the cache data. For example, we may want to use the ProductDto class instead of the Product class for the cached object of the Product entity.
Assume that we've created a ProductDto class as shown below:
public class ProductDto : EntityDto<Guid>
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int StockCount { get; set; }
}
Now, we can register the entity cache services to dependency injection in the ConfigureServices method of your module class with three generic parameters, as shown below:
context.Services.AddEntityCache<Product, ProductDto, Guid>();
Since the entity cache system will perform the object mapping (from Product to ProductDto), we should configure the object map. Here, an example configuration with AutoMapper:
public class MyMapperProfile : Profile
{
public MyMapperProfile()
{
CreateMap<Product, ProductDto>();
}
}
If you are using Mapperly, you can create a new mapping class that implements the MapperBase<Product, ProductDto> class with the [Mapper] attribute as follows:
[Mapper]
public partial class ProductToProductDtoMapper : MapperBase<Product, ProductDto>
{
public override partial ProductDto Map(Product source);
public override partial void Map(Product source, ProductDto destination);
}
Now, you can inject the IEntityCache<ProductDto, Guid> service wherever you want:
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IEntityCache<ProductDto, Guid> _productCache;
public ProductAppService(IEntityCache<ProductDto, Guid> productCache)
{
_productCache = productCache;
}
public async Task<ProductDto> GetAsync(Guid id)
{
return await _productCache.GetAsync(id);
}
}
Notice that the _productCache.GetAsync method already returns a ProductDto object, so we could directly return it from our application service.
Configuration
All of the context.Services.AddEntityCache() methods get an optional DistributedCacheEntryOptions parameter where you can easily configure the caching options:
context.Services.AddEntityCache<Product, ProductDto, Guid>(
new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30)
}
);
The default cache duration is 2 minutes with the
AbsoluteExpirationRelativeToNowconfiguration.
Additional Notes
- Entity classes should be serializable/deserializable to/from JSON to be cached (because it's serialized to JSON when saving in the Distributed Cache). 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 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 provides batch retrieval methods for retrieving multiple entities at once.
List-Based Batch Retrieval
FindManyAsync and GetManyAsync return results as a list, preserving the order of the given IDs (including duplicates):
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IEntityCache<ProductDto, Guid> _productCache;
public ProductAppService(IEntityCache<ProductDto, Guid> productCache)
{
_productCache = productCache;
}
public async Task<List<ProductDto>> GetManyAsync(List<Guid> ids)
{
return await _productCache.GetManyAsync(ids);
}
public async Task<List<ProductDto?>> FindManyAsync(List<Guid> ids)
{
return await _productCache.FindManyAsync(ids);
}
}
GetManyAsyncthrowsEntityNotFoundExceptionif any entity is not found for the given IDs.FindManyAsyncreturns a list where each entry corresponds to the given ID in the same order; an entry will benullif the entity was not found.
Dictionary-Based Batch Retrieval
FindManyAsDictionaryAsync and GetManyAsDictionaryAsync return a Dictionary<TKey, TEntityCacheItem> keyed by entity ID, which is convenient when you need fast lookup by ID:
public async Task<Dictionary<Guid, ProductDto?>> FindManyAsDictionaryAsync(List<Guid> ids)
{
return await _productCache.FindManyAsDictionaryAsync(ids);
}
public async Task<Dictionary<Guid, ProductDto>> GetManyAsDictionaryAsync(List<Guid> ids)
{
return await _productCache.GetManyAsDictionaryAsync(ids);
}
GetManyAsDictionaryAsyncthrowsEntityNotFoundExceptionif any entity is not found for the given IDs.FindManyAsDictionaryAsyncreturns a dictionary where the value isnullif the entity was not found for the corresponding key.
All batch 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:
First, define the cache item class:
public class ProductCacheDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Then, derive from EntityCacheWithObjectMapper and override MapToValue:
public class ProductEntityCache :
EntityCacheWithObjectMapper<Product, ProductCacheDto, Guid>
{
public ProductEntityCache(
IReadOnlyRepository<Product, Guid> repository,
IDistributedCache<EntityCacheItemWrapper<ProductCacheDto>, 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:
context.Services.ReplaceEntityCache<ProductEntityCache, Product, ProductCacheDto, Guid>(
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
If no prior
AddEntityCacheregistration exists for the same cache item type,ReplaceEntityCachewill simply add the service instead of throwing an error.