Skip to main content

In-Memory Repository

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.

FeatureStatusNotes
Base Repository
Filterable
QueryableNative .NET IQueryable
Pageable
Tracking
Multi-tenant
Thread-SafeReaderWriterLockSlim for concurrent access

The InMemoryRepository<TEntity> class stores entities in the memory of the running process. It is intended for testing, prototyping, and scenarios where data persistence is not required.

Thread Safety

InMemoryRepository<TEntity> is fully thread-safe. All read operations acquire a shared read lock, and all write operations acquire an exclusive write lock via ReaderWriterLockSlim. Multiple threads can safely read and write to the same repository instance concurrently.

Installation

dotnet add package Kista.InMemory

Registration

Use the fluent builder API to register the In-Memory driver:

// Program.cs
builder.Services.AddRepositoryContext()
.UseInMemory();

You can also configure field mappers and initial data:

builder.Services.AddRepositoryContext()
.UseInMemory(b => b
.WithFieldMapper<MyEntity, MyFieldMapper>()
.WithInitialData(new[] { new MyEntity { /* ... */ } }));

Bulk vs Per-Entity

.UseInMemory() registers the open generic InMemoryRepository<> so that IRepository<Person>, IRepository<Order>, and any other entity type resolve to InMemoryRepository automatically — no per-entity class is needed:

builder.Services.AddRepositoryContext()
.UseInMemory(); // IRepository<Person>, IRepository<Order>, etc. all work

For entities that need custom query methods or overrides, create a concrete repository class and register it with AddRepository<T>(). The concrete type overrides the open generic for that entity only:

builder.Services.AddRepositoryContext()
.UseInMemory()
.AddRepository<PersonRepository>(); // PersonRepository used for Person
// InMemoryRepository used for everything else

Pre-seeding Data

You can seed initial data using the WithInitialData method:

var seeded = new List<MyEntity> { /* ... */ };
builder.Services.AddRepositoryContext()
.UseInMemory(b => b.WithInitialData(seeded));

Or register the repository directly with pre-seeded data:

var seeded = new List<MyEntity> { /* ... */ };
builder.Services.AddSingleton(new InMemoryRepository<MyEntity>(seeded));

Lifecycle Support

The In-Memory driver provides InMemoryRepositoryLifecycleHandler<TEntity> for lifecycle orchestration (create, drop, seed). It is disabled by default and must be explicitly enabled.

Enabling Lifecycle

builder.Services.AddRepositoryContext()
.UseInMemory(b => b.WithLifecycle())
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
});

Handler Behavior

OperationBehavior
ExistsAsyncAlways returns false (in-memory storage is transient).
CreateAsyncNo-op (no physical resource to create).
DropAsyncNo-op (no physical resource to drop).
SeedAsyncResolves the IRepository<TEntity> from DI and inserts data via AddRangeAsync / AddAsync. Supports IEnumerable<TEntity>, IEnumerable<object>, and single entities.

Seeding Examples

Using a provider class:

public class ProductSeedProvider : IRepositorySeedDataProvider<Product> {
public IEnumerable<Product> GetSeedData() {
yield return new Product { Name = "Widget", Price = 9.99m };
yield return new Product { Name = "Gadget", Price = 24.99m };
}

IEnumerable<object> IRepositorySeedDataProvider.GetSeedData()
=> GetSeedData().Cast<object>();
}

// Program.cs
builder.Services.AddRepositoryContext()
.UseInMemory(b => b.WithLifecycle())
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
})
.WithSeedData<Product, ProductSeedProvider>();

Using inline data (no provider class needed):

builder.Services.AddRepositoryContext()
.UseInMemory(b => b.WithLifecycle())
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
})
.WithSeedData<Product>(new[] {
new Product { Name = "Widget", Price = 9.99m },
new Product { Name = "Gadget", Price = 24.99m }
});

The handler resolves the provider during seeding and inserts the entities through IRepository<Product>.AddRangeAsync.

Querying

InMemoryRepository<TEntity> implements both IQueryableRepository<TEntity> and IFilterableRepository<TEntity>.

With LINQ (IQueryableRepository):

var items = repository.AsQueryable()
.Where(x => x.IsActive)
.OrderBy(x => x.Name)
.ToList();

With filter types (IFilterableRepository):

The only supported filter type is ExpressionQueryFilter<TEntity> (backed by a lambda expression). You can pass it directly or use the convenience Query.Where<TEntity> factory:

// Using ExpressionQueryFilter
var query = new Query(new ExpressionQueryFilter<MyEntity>(x => x.Name == "John"));
var items = await repository.FindAllAsync(query);

// Using lambda shorthand (extension method)
var items = await repository.FindAllAsync(x => x.Name == "John");

Passing an unsupported filter type (e.g., a MongoDB-specific filter) will throw NotSupportedException.