Repository Lifecycle
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 (if you are not using the builder):
builder.Services.AddRepositoryLifecycleOrchestrator(options => {
options.FailFast = true;
});
The builder approach via
ConfigureLifecycle()is recommended for new code. The directAddRepositoryLifecycleOrchestrator()method is kept for backward compatibility.
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. |
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.
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).
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);
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 via one of the methods described in Seeding.
Handler vs Controllable
| Approach | When to Use |
|---|---|
IRepositoryLifecycleHandler<TEntity> | Custom lifecycle logic separate from the repository (e.g., creating a database schema, running migrations). 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
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);