Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchmarks/repobench/repobench.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Kista.Benchmarks</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* [Overview](filtering/index.md)
* [Filter Cache](filtering/filter-cache.md)
* [The Entity Manager](entity-manager/README.md)
* [Entity Validation](entity-manager/entity-validation.md)
* [HTTP Request Cancellation](entity-manager/http-request-cancellation.md)
* [Caching Entities](entity-manager/caching-entities.md)
* [Sample Application](sample-app.md)
Expand Down
8 changes: 7 additions & 1 deletion docs/custom-repository/registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ This page covers how to register custom repositories with the DI container using

## Basic Registration

Use `AddRepository<T>()` on the repository context builder:
Use `AddRepository<T>()` on the repository context builder. The driver's open-generic registrations (e.g. `InMemoryRepository<>` from `.UseInMemory()`) serve as defaults for all entities; `AddRepository<T>()` registers a concrete type that overrides the open generic for that specific entity:

```csharp
builder.Services.AddRepositoryContext()
.UseInMemory() // default: InMemoryRepository for all entities
.AddRepository<SpecialProductRepo>(); // override: SpecialProductRepo for Product only
```

```csharp
builder.Services.AddRepositoryContext()
Expand Down
138 changes: 104 additions & 34 deletions docs/entity-manager/README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,91 @@
# The Entity Manager

> **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>` class is an optional application-layer service that wraps an `IRepository<TEntity>` and enriches every operation with cross-cutting concerns:
The `EntityManager<TEntity>` (and `EntityManager<TEntity, TKey>`) is an optional application-layer service that wraps an `IRepository<TEntity>` and enriches every operation with cross-cutting concerns:

- **Validation** — entities are validated (via `IEntityValidator<TEntity>`) before being added or updated.
- **Caching** — frequently accessed entities can be served from a second-level cache (via `IEntityCache<TEntity>`).
- **Validation** — entities are validated before being added or updated.
- **Caching** — frequently accessed entities can be served from a second-level cache.
- **Timestamping** — entities implementing `IHaveTimeStamp` are automatically stamped on create/update.
- **Structured error handling** — operations return structured error results rather than throwing raw exceptions.
- **Logging** — all operations are logged through the standard `ILogger` infrastructure.

`EntityManager<TEntity>` is designed to be used at the **application service layer**, sitting between your controllers / use-case handlers and the underlying repository.
`EntityManager<TEntity>` is designed to sit between your controllers / use-case handlers and the underlying repository, at the **application service layer**.

## Constructor
## Prerequisites

```csharp
public EntityManager<TEntity>(
IRepository<TEntity> repository,
IEntityValidator<TEntity>? validator = null,
IEntityCache<TEntity>? cache = null,
ISystemTime? systemTime = null,
IOperationErrorFactory<TEntity>? errorFactory = null,
IServiceProvider? services = null,
ILoggerFactory? loggerFactory = null)
```
The entity manager is packaged separately from the core repository:

| Package | Description |
|---------|-------------|
| `Kista.Manager` | Base manager, validation abstractions, error factories |
| `Kista.Manager.EasyCaching` | Second-level caching via EasyCaching |
| `Kista.Manager.AspNetCore` | Automatic HTTP request cancellation for ASP.NET Core |
| `Kista.Manager.DynamicLinq` | Dynamic LINQ query extensions for the manager |

All parameters beyond `repository` are optional and resolved from the DI container at registration time (see below).
```bash
dotnet add package Kista.Manager
```

## Registration

Use `AddEntityManager<TManager>` to register a concrete manager type (or a custom sub-class):
There are two ways to register an entity manager: **per-repository** (explicit, recommended) and **global** (convenience for bulk registration).

### Per-repository (recommended)

Use `WithManagement()` on the `RepositoryBuilder` returned by `AddRepository<T>()`. This binds the manager to a specific repository and entity type:

```csharp
// Program.cs
services.AddRepositoryContext()
.AddRepository<PersonRepository>(repo => repo
.WithManagement(mgmt => mgmt
.WithValidator<PersonValidator>()
.WithCacheKeyGenerator<PersonKeyGenerator>()
.WithOperationErrorFactory<PersonErrorFactory>()
.WithEasyCaching(opts =>
{
opts.DefaultExpiration = TimeSpan.FromMinutes(15);
})))
.UseInMemory();
```

// Register the default EntityManager for MyEntity
builder.Services.AddManagerFor<MyEntity>();
To register a manager with no additional services (the EntityManager itself is still registered):

// Or register a custom sub-class that derives from EntityManager<MyEntity>
builder.Services.AddEntityManager<MyEntityManager>();
```csharp
services.AddRepositoryContext()
.AddRepository<PersonRepository>(repo => repo
.WithManagement())
.UseInMemory();
```

After registration, the DI container provides both the concrete type and all `EntityManager<TEntity>` base-type projections.
### Global (convenience)

Use `WithManagement()` on the `RepositoryContextBuilder` to register managers for **all tracked entity types** in one call:

```csharp
services.AddRepositoryContext()
.AddRepository<PersonRepository>(_ => { })
.AddRepository<OrderRepository>(_ => { })
.WithManagement() // registers EntityManager<Person> and EntityManager<Order>
.UseInMemory();
```

To register a custom validator:
You can pass a `ManagementOptions` delegate to control auto-registration:

```csharp
builder.Services.AddEntityValidator<MyEntityValidator>();
services.AddRepositoryContext()
.AddRepository<PersonRepository>(_ => { })
.WithManagement(opts =>
{
opts.AutoRegisterManagers = true; // default: true
});
```

> Both approaches coexist. Use per-repository when you need entity-specific configuration; use global when you want quick setup with no extras.

## Custom Managers

Derive from `EntityManager<TEntity>` to add domain-specific business operations:
Derive from `EntityManager<TEntity>` (or `EntityManager<TEntity, TKey>`) to add domain-specific business operations:

```csharp
public class OrderManager : EntityManager<Order>
Expand All @@ -66,30 +101,65 @@ public class OrderManager : EntityManager<Order>
{
var order = await FindAsync(orderId, ct);
if (order == null)
return NotFound("ORDER_NOT_FOUND");
return OperationResult.Fail(...);

order.Ship();
return await UpdateAsync(order, ct);
}
}
```

Register a custom manager by registering it directly in the DI container:

```csharp
services.AddScoped<OrderManager>();
```

The base `EntityManager<Order>` is registered automatically when using `WithManagement()`, so DI can resolve both the base type and your custom type.

## Operation Results

EntityManager methods return `OperationResult` (for void-like operations) or `OperationResult<T>` (for operations that return a value), rather than throwing exceptions for expected failures:

| Method | Returns |
|--------|---------|
| `AddAsync(entity)` | `OperationResult` |
| `UpdateAsync(entity)` | `OperationResult` |
| `RemoveAsync(entity)` | `OperationResult` |
| `FindAsync(key)` | `OperationResult<TEntity>` |
| `FindFirstAsync(query)` | `OperationResult<TEntity>` |

```csharp
var result = await manager.AddAsync(person);
if (result.IsSuccess)
{
// entity added
}
else
{
var error = result.Error;
// error.ErrorCode, error.Message
}
```

This allows the caller to handle validation failures, not-found conditions, and infrastructure errors uniformly without try/catch blocks.

## Operation Cancellation

Every async method accepts an optional `CancellationToken`. When no token is supplied (or `null` is passed), the manager checks for an `IOperationCancellationSource` registered in the DI container and uses its token automatically.
Every async method accepts an optional `CancellationToken`. When no token is supplied, the manager checks for an `IOperationCancellationSource` registered in the DI container and uses its token automatically.

This is useful for tying operation lifetime to an HTTP request:
For ASP.NET Core, install `Kista.Manager.AspNetCore` and call `AddHttpRequestTokenSource()`:

```csharp
// Register the ASP.NET Core HTTP cancellation source
// (provided by Kista.Manager.AspNetCore)
builder.Services.AddHttpRequestTokenSource();
```

With this in place, when the HTTP request is aborted, all in-flight repository operations are cancelled without any additional code in your manager.
When the HTTP client disconnects, all in-flight repository operations are cancelled automatically.

See [HTTP Request Cancellation](http-request-cancellation.md) for full details.

## Caching
## See Also

See [Caching Entities](caching-entities.md) for details on integrating second-level caching via [EasyCaching](https://easycaching.readthedocs.io/en/latest/).
- [Entity Validation](entity-validation.md) — validators, error factories, and the validation flow
- [Caching Entities](caching-entities.md) — cache registration, key generators, serialization
- [HTTP Request Cancellation](http-request-cancellation.md) — automatic cancellation via ASP.NET Core
95 changes: 77 additions & 18 deletions docs/entity-manager/caching-entities.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# 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 the number of calls to the underlying repository.
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

Expand All @@ -11,28 +12,58 @@ 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):

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

## Registration

Register EasyCaching and the entity cache in the DI container:
### Via EntityManagerBuilder (recommended)

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

```csharp
// Program.cs
builder.Services.AddEasyCaching(options =>
{
options.UseInMemory("default");
});
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`:

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

### Direct registration (legacy)

// Register the default EntityEasyCache<MyEntity>
The following methods on `IServiceCollection` are still supported but deprecated in favor of the fluent builder:

```csharp
builder.Services.AddEntityEasyCacheFor<MyEntity>();

// Then register the manager (which picks up the cache automatically)
builder.Services.AddManagerFor<MyEntity>();
```

You can configure cache options inline or from configuration:

```csharp
// Inline configuration
// Inline
builder.Services.AddEntityEasyCacheFor<MyEntity>(options =>
{
options.Expiration = TimeSpan.FromMinutes(5);
Expand All @@ -55,26 +86,49 @@ The `EntityManager<TEntity>` intercepts the following operations to read from or
| `RemoveAsync` | `RemoveAsync` | Evicts the entity from the cache after removal. |
| `RemoveRangeAsync` | `RemoveAsync` (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 primary key of the entity is used to derive the cache key. To customize key generation, implement `IEntityCacheKeyGenerator<TEntity>` and register it:
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`:

```csharp
public class MyEntityCacheKeyGenerator : IEntityCacheKeyGenerator<MyEntity>
public class PersonKeyGenerator : IEntityCacheKeyGenerator<Person>
{
public IEnumerable<string> GetKeys(MyEntity entity)
{
yield return $"myentity:{entity.Id}";
yield return $"myentity:by-name:{entity.Name}";
}
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):

```csharp
builder.Services.AddEntityCacheKeyGenerator<MyEntityCacheKeyGenerator>();
```

## Cache Serialization

In some scenarios, entities must be converted to a serializable form before being stored in the cache (e.g., when the cache provider serializes objects to JSON or binary). To handle this, derive from `EntityEasyCache<TEntity, TCached>` and implement `IEntityEasyCacheConverter<TEntity, TCached>`:
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:

```csharp
public class MyEntityCacheConverter
Expand All @@ -92,3 +146,8 @@ Then register a typed `EntityEasyCache<MyEntity, MyEntityCacheModel>`:
```csharp
builder.Services.AddEntityEasyCache<EntityEasyCache<MyEntity, MyEntityCacheModel>>();
```

## See Also

- [Entity Validation](entity-validation.md) — configure validators and error factories
- [HTTP Request Cancellation](http-request-cancellation.md) — automatic cancellation via ASP.NET Core
Loading
Loading