Skip to main content
Version: v1.6.8

In-Memory Repository

FeatureStatusNotes
Base Repository✅
Filterable✅
Queryable✅Native .NET IQueryable
Pageable✅
Tracking❌
Multi-tenant❌
Thread-Safe✅ReaderWriterLockSlim 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.