Repository Lifecycle
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 repository lifecycle feature provides a formalized mechanism to create, drop, and seed repositories during application startup. It replaces the obsolete IRepositoryController / RepositoryControllerAdapter model with a cleaner handler-based abstraction.
Overview
The lifecycle is orchestrated by IRepositoryLifecycleService (default implementation: RepositoryLifecycleService). On startup you call one or more of its methods:
| Method | Description |
|---|---|
CreateRepositoryAsync<TEntity> / <TEntity, TKey> | Creates the repository (e.g. a database or collection) |
DropRepositoryAsync<TEntity> / <TEntity, TKey> | Drops the repository |
SeedRepositoryAsync<TEntity> / <TEntity, TKey> | Seeds initial data |
Each operation is delegated to a registered IRepositoryLifecycleHandler<TEntity>. If no handler is found, the service falls back to an IControllableRepository (a repository that can manage its own storage).
Registration
Use the AddRepositoryContext() fluent builder:
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.ConfigureLifecycle(options => {
options.DeleteIfExists = true;
options.SeedStrategy = SeedStrategy.Always;
});
Or register the lifecycle service directly:
builder.Services.AddRepositoryLifecycleOrchestrator(options => {
options.FailFast = true;
});
Registering Lifecycle Handlers
Use the WithLifecycleHandler() extensions on RepositoryContextBuilder:
// By type
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.WithLifecycleHandler<MyEntity, MyEntityLifecycleHandler>();
// By factory delegate
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.WithLifecycleHandler<MyEntity>(sp => new MyEntityLifecycleHandler(sp.GetRequiredService<ILogger>()));
// By instance
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.WithLifecycleHandler(new MyEntityLifecycleHandler());
Registering Lifecycle Profiles
Use the WithLifecycleProfile() extensions:
// By type
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.WithLifecycleProfile<StagingLifecycleProfile>();
// By instance
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.WithLifecycleProfile(new StagingLifecycleProfile());
ConfigureLifecycle()automatically registers aDefaultRepositoryLifecycleProfileif no profile is already registered.
Configuration
RepositoryLifecycleOptions controls every aspect of lifecycle behavior:
| Property | Default | Description |
|---|---|---|
DeleteIfExists | true | Drop and re-create if the repository already exists. |
DontCreateExisting | true | Skip creation if the repository already exists. |
FailFast | false | Throw if no lifecycle handler is found. |
SeedStrategy | Never | Determines when seeding occurs (see below). |
EnvironmentName | null | Overrides the hosting environment name. |
SeedAction | null | Custom action invoked instead of the handler's SeedAsync. |
Conflict Resolution
When both DeleteIfExists and DontCreateExisting are false and the repository exists, CreateRepositoryAsync throws RepositoryException.
Seed Strategies
The SeedStrategy enum controls when seed data is applied:
| Value | Behavior |
|---|---|
Never | No seeding is performed. |
Always | Seeding is always performed. |
IfMissing | Seeding is performed only when the repository does not exist. |
ByEnvironment | The strategy is delegated to an IRepositoryLifecycleProfile. |
Environment-Aware Seeding
When SeedStrategy is ByEnvironment, the service:
- Reads
EnvironmentNamefrom options (or resolves it fromIHostEnvironment). - Queries the registered
IRepositoryLifecycleProfilefor the effective strategy. - Falls back to
Alwaysif no profile is registered.
Seed Data Sources
Seed data can come from three sources (evaluated in order):
- Explicit data passed to
SeedRepositoryAsync(data). - An
IRepositorySeedDataProvider<TEntity>registered in DI. - A
SeedActioncallback onRepositoryLifecycleOptions.
If no source provides data, seeding is silently skipped.
Automatic Discovery
When you call ConfigureLifecycle(), the builder automatically scans the entry assembly for any class implementing IRepositorySeedDataProvider<TEntity> and registers it with TryAdd — so explicit registrations always take priority.
// This provider is auto-discovered — no explicit registration needed
public class ProductSeedProvider : IRepositorySeedDataProvider<Product> {
public IEnumerable<Product> GetSeedData() {
yield return new Product { Name = "Widget", Price = 9.99m };
}
}
// ConfigureLifecycle triggers the auto-scan
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
});
Explicit Registration
When you need to register providers from multiple assemblies or want to be explicit, use WithSeedDataFrom():
builder.Services.AddRepositoryContext()
.UseInMemory(b => b.WithLifecycle())
.ConfigureLifecycle(options => { ... })
.WithSeedDataFrom(typeof(ProductSeedProvider).Assembly,
typeof(OtherSeedProvider).Assembly);
Seeding Examples
Via WithSeedData (Recommended)
The RepositoryContextBuilder provides two overloads:
Using a provider class:
public class MyEntitySeedProvider : IRepositorySeedDataProvider<MyEntity> {
public IEnumerable<MyEntity> GetSeedData() {
yield return new MyEntity { Name = "Default Item", IsActive = true };
yield return new MyEntity { Name = "Another Item", IsActive = true };
}
IEnumerable<object> IRepositorySeedDataProvider.GetSeedData()
=> GetSeedData().Cast<object>();
}
// Program.cs
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
})
.WithSeedData<MyEntity, MyEntitySeedProvider>();
Using inline data (no provider class needed):
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
})
.WithSeedData<MyEntity>(new[] {
new MyEntity { Name = "Default Item", IsActive = true },
new MyEntity { Name = "Another Item", IsActive = true }
});
All overloads register an IRepositorySeedDataProvider<TEntity> in DI behind the scenes. The service resolves it during SeedRepositoryAsync<TEntity> and the driver's lifecycle handler inserts the data.
Explicit
WithSeedDatacalls useAdd(always registers). Auto-discovered providers useTryAdd— so explicit wins if both are present.
Via SeedAction Callback
Override the default seeding behavior with a custom action:
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.Always;
options.SeedAction = (sp, entityType, seedData) => {
if (entityType == typeof(MyEntity) && seedData is IEnumerable<MyEntity> entities) {
var context = sp.GetRequiredService<AppDbContext>();
context.Set<MyEntity>().AddRange(entities);
context.SaveChanges();
}
};
});
The SeedAction receives the service provider, the entity type being seeded, and the resolved seed data (or null).
Via Explicit Call
Trigger lifecycle operations explicitly from your application:
var lifecycleService = app.Services.GetRequiredService<IRepositoryLifecycleService>();
// Create the repository and seed in one flow
await lifecycleService.CreateRepositoryAsync<MyEntity>();
await lifecycleService.SeedRepositoryAsync<MyEntity>(new[] {
new MyEntity { Name = "Startup Item" }
});
// Or use the combined lifecycle flow (create + seed)
await lifecycleService.SeedRepositoryAsync<MyEntity>(new[] {
new MyEntity { Name = "Startup Item" }
});
When
SeedRepositoryAsyncis called with explicit data andSeedStrategyis notNever, the data is passed directly to the driver's lifecycle handler without consultingIRepositorySeedDataProviderorSeedAction.
Lifecycle Handlers
Implement IRepositoryLifecycleHandler<TEntity> to control how a specific entity type is created, dropped, and seeded:
public class MyEntityHandler : IRepositoryLifecycleHandler<MyEntity> {
public async ValueTask<bool> ExistsAsync(CancellationToken ct) { ... }
public async ValueTask CreateAsync(CancellationToken ct) { ... }
public async ValueTask DropAsync(CancellationToken ct) { ... }
public async ValueTask SeedAsync(object? seedData, CancellationToken ct) { ... }
}
Register the handler via WithLifecycleHandler() on the builder or directly in DI:
// Via builder (recommended)
builder.Services.AddRepositoryContext()
.UseEntityFramework<AppDbContext>(...)
.WithLifecycleHandler<MyEntity, MyEntityHandler>();
// Or directly
builder.Services.AddSingleton<IRepositoryLifecycleHandler<MyEntity>, MyEntityHandler>();
Controllable Repository Fallback
If no IRepositoryLifecycleHandler<TEntity> is registered for an entity type, the service falls back to checking whether the registered repository itself implements IControllableRepository — meaning the repository self-manages its own storage lifecycle.
public interface IControllableRepository {
ValueTask<bool> ExistsAsync(CancellationToken cancellationToken = default);
ValueTask CreateAsync(CancellationToken cancellationToken = default);
ValueTask DropAsync(CancellationToken cancellationToken = default);
}
How the Fallback Works
Inside ResolveHandler<TEntity>() (and its TEntity, TKey variant), the service:
- Checks the service container for an
IRepositoryLifecycleHandler<TEntity>. - If not found, resolves
IRepository<TEntity>and tests it forIControllableRepository. - If the repository implements
IControllableRepository, it wraps it in an internalControllableRepositoryHandler<TEntity>that delegatesExistsAsync,CreateAsync, andDropAsyncdirectly to the repository. - If neither exists and
FailFastistrue, throwsRepositoryException. Otherwise returnsnull(operation is silently skipped).
// Pseudocode of the fallback logic
var handler = serviceProvider.GetService<IRepositoryLifecycleHandler<TEntity>>();
if (handler != null) return handler;
var repository = serviceProvider.GetService<IRepository<TEntity>>();
if (repository is IControllableRepository controllable)
return new ControllableRepositoryHandler<TEntity>(controllable);
// No handler, no controllable repository — skip or fail
Seeding Note
ControllableRepositoryHandler<TEntity>.SeedAsync is a no-op — the controllable interface has no seed operation. When using the controllable-repository fallback, seeding must be done either:
- Via an
IRepositorySeedDataProvider<TEntity>(auto-discovered or explicit) - Via a
SeedActioncallback inRepositoryLifecycleOptions - By passing explicit data to
SeedRepositoryAsync(data)
The service's SeedRepository method itself handles data resolution (provider → service → explicit), so seed data flows normally even though the handler's SeedAsync is a no-op.
Handler vs Controllable
| Approach | When to Use |
|---|---|
IRepositoryLifecycleHandler<TEntity> | Custom lifecycle logic that is separate from the repository (e.g., creating a database schema, running migrations). Also required when seeding data. |
IControllableRepository on the repository class | The repository is the storage and can create/drop itself. Common when the repository directly wraps a database collection or table. |
Driver Support
| Driver | Implements IControllableRepository? | Default |
|---|---|---|
| MongoDB | ✅ — MongoRepository<TEntity> implements the interface natively | Enabled (use WithoutLifecycle() to disable) |
| In-Memory | ❌ — uses InMemoryRepositoryLifecycleHandler<TEntity> instead | Disabled (use WithLifecycle() to enable) |
| EF Core | ❌ — uses a dedicated lifecycle handler | Enabled (use WithoutLifecycle() to disable) |
When a driver does not implement IControllableRepository, the driver's .WithLifecycle() (or default-enabled lifecycle) registers a handler that fits the IRepositoryLifecycleHandler<TEntity> contract, and the fallback path is not needed.
Lifecycle Profiles
IRepositoryLifecycleProfile provides environment-specific seed strategies and data:
public class StagingProfile : IRepositoryLifecycleProfile {
public SeedStrategy GetSeedStrategy(string? environmentName)
=> environmentName == "Staging" ? SeedStrategy.Always : SeedStrategy.Never;
public object? GetSeedData<TEntity>() where TEntity : class => null;
public object? GetSeedData(Type entityType) => null;
}
Error Handling
NotSupportedExceptionis re-thrown as-is.RepositoryExceptionis re-thrown as-is.- Any other exception is wrapped in a
RepositoryException.
When FailFast is true and no handler or controllable repository is found, a RepositoryException is thrown immediately.
Sample Project
The Kista.SampleApp demonstrates a complete ASP.NET Core application using lifecycle management and CRUD endpoints. It includes:
- Model:
Contactentity withGuidId - Custom Repository:
ContactRepositoryextendingIRepository<Contact, Guid> - Lifecycle Handler:
ContactLifecycleHandlerfor create/drop/seed operations - Lifecycle Profile:
SampleLifecycleProfilefor environment-aware seeding - Seed Data:
DefaultContactSeedDataimplementingIRepositorySeedDataProvider<Contact> - Endpoints:
/api/lifecycle/create— Create the repository/api/lifecycle/drop— Drop the repository/api/lifecycle/seed— Seed the repository/api/lifecycle/initialize— Drop, create, and seed in one call/api/contacts— Full CRUD for contacts
Registration
// Extension method (see sample for full implementation)
builder.Services.AddContactRepository(builder.Configuration);
Lifecycle Endpoint Usage
// Create
await service.CreateRepositoryAsync<Contact, Guid>(ct);
// Drop
await service.DropRepositoryAsync<Contact, Guid>(ct);
// Seed (uses registered seed data provider)
await service.SeedRepositoryAsync<Contact, Guid>(null, ct);
// Full initialization
await service.DropRepositoryAsync<Contact, Guid>(ct);
await service.CreateRepositoryAsync<Contact, Guid>(ct);
await service.SeedRepositoryAsync<Contact, Guid>(null, ct);