Skip to main content

Caching Entities

Renamed: This project was renamed from Deveel.Repository to Kista on May 26, 2025. The name Kista is Old Norse for "chest" or "repository", better reflecting the project purpose as a data access framework.

The EntityManager<TEntity> supports an optional second-level cache via the IEntityCache<TEntity> service. When registered, the manager transparently caches entities on write and serves them from the cache on subsequent reads, reducing calls to the underlying repository.

Installation

Install the EasyCaching integration package:

dotnet add package Kista.Manager.EasyCaching

EasyCaching must be configured globally with a provider (e.g., in-memory, Redis, Memcached):

builder.Services.AddEasyCaching(options =>
options.UseInMemory("default"));

Registration

When using the per-repository WithManagement() callback, use WithEasyCaching() on the EntityManagerBuilder:

services.AddRepositoryContext()
.AddRepository<PersonRepository>(repo => repo
.WithManagement(mgmt => mgmt
.WithEasyCaching(opts =>
{
opts.DefaultExpiration = TimeSpan.FromMinutes(15);
})))
.UseInMemory();

Global registration

Enable caching for all tracked entity types via the RepositoryContextBuilder:

services.AddRepositoryContext()
.AddRepository<PersonRepository>(_ => { })
.AddRepository<OrderRepository>(_ => { })
.WithEasyCaching(opts =>
{
opts.DefaultExpiration = TimeSpan.FromMinutes(5);
});

Direct registration (legacy)

The following methods on IServiceCollection are still supported but deprecated in favor of the fluent builder:

builder.Services.AddEntityEasyCacheFor<MyEntity>();

builder.Services.AddManagerFor<MyEntity>();

You can configure cache options inline or from configuration:

// Inline
builder.Services.AddEntityEasyCacheFor<MyEntity>(options =>
{
options.Expiration = TimeSpan.FromMinutes(5);
});

// From appsettings.json
builder.Services.AddEntityEasyCacheFor<MyEntity>("Caching:MyEntity");

How the Cache Interacts with Manager Operations

The EntityManager<TEntity> intercepts the following operations to read from or write to the cache:

Manager MethodCache OperationDescription
FindAsync(key)GetOrSetAsyncReturns the cached entity if present; otherwise calls the repository and caches the result.
AddAsyncSetAsyncStores the newly added entity in the cache.
AddRangeAsyncSetAsync (batch)Stores all added entities in the cache.
UpdateAsyncSetAsyncUpdates the cached entry after a successful repository update.
RemoveAsyncRemoveAsyncEvicts the entity from the cache after removal.
RemoveRangeAsyncRemoveAsync (batch)Evicts all removed entities from the cache.

Cache invalidation

The manager automatically invalidates cached entries when entities are updated or removed. There is no time-based invalidation by default; expiration is controlled by the EasyCaching provider configuration (e.g., DefaultExpiration in WithEasyCaching()).

If an entity is modified outside the manager (directly through the repository), cached entries may become stale. In that case, call RemoveAsync through the manager or evict entries directly using the cache provider.

Cache on Find vs FindFirst

FindAsync always checks the cache first. FindFirstAsync does not cache results, since queries are dynamic and caching their output is not generally safe without additional semantics.

Cache Keys

By default, the entity's primary key (IHaveKey<TKey>.Key) is converted to a string and used as the cache key. The default key format is {EntityType.Name}:{key}.

To customize key generation, implement IEntityCacheKeyGenerator<TEntity> and register it via the EntityManagerBuilder:

public class PersonKeyGenerator : IEntityCacheKeyGenerator<Person>
{
public string GenerateKey(object key) => $"person:{key}";
public string[] GenerateAllKeys(Person entity) =>
[$"person:{entity.Id}", $"person:email:{entity.Email}"];
}

services.AddRepositoryContext()
.AddRepository<PersonRepository>(repo => repo
.WithManagement(mgmt => mgmt
.WithCacheKeyGenerator<PersonKeyGenerator>()));

The WithCacheKeyGenerator<TGenerator>() method scans the type for implemented IEntityCacheKeyGenerator<> interfaces and registers each one matching the builder's current entity type.

Multiple keys from GenerateAllKeys enable lookups by alternate identifiers (e.g., email, external ID) while keeping a single entry in the cache.

For legacy direct registration (deprecated):

builder.Services.AddEntityCacheKeyGenerator<MyEntityCacheKeyGenerator>();

Cache Serialization

Some cache providers (Redis, distributed caches) require entities to be serialized to a specific format. To handle this, implement IEntityEasyCacheConverter<TEntity, TCached> and register a typed cache:

public class MyEntityCacheConverter
: IEntityEasyCacheConverter<MyEntity, MyEntityCacheModel>
{
public MyEntityCacheModel ToCache(MyEntity entity) => new MyEntityCacheModel { /* ... */ };
public MyEntity FromCache(MyEntityCacheModel cached) => new MyEntity { /* ... */ };
}

builder.Services.AddEntityEasyCacheConverter<MyEntityCacheConverter>();

Then register a typed EntityEasyCache<MyEntity, MyEntityCacheModel>:

builder.Services.AddEntityEasyCache<EntityEasyCache<MyEntity, MyEntityCacheModel>>();

See Also