Skip to main content
Version: 1.6.8

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:

MethodDescription
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 direct AddRepositoryLifecycleOrchestrator() 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 a DefaultRepositoryLifecycleProfile if no profile is already registered.

Configuration

RepositoryLifecycleOptions controls every aspect of lifecycle behavior:

PropertyDefaultDescription
DeleteIfExiststrueDrop and re-create if the repository already exists.
DontCreateExistingtrueSkip creation if the repository already exists.
FailFastfalseThrow if no lifecycle handler is found.
SeedStrategyNeverDetermines when seeding occurs.
EnvironmentNamenullOverrides the hosting environment name.
SeedActionnullCustom 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:

  1. Checks the service container for an IRepositoryLifecycleHandler<TEntity>.
  2. If not found, resolves IRepository<TEntity> and tests it for IControllableRepository.
  3. If the repository implements IControllableRepository, it wraps it in an internal ControllableRepositoryHandler<TEntity> that delegates ExistsAsync, CreateAsync, and DropAsync directly to the repository.
  4. If neither exists and FailFast is true, throws RepositoryException. Otherwise returns null (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

ApproachWhen 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 classThe repository is the storage and can create/drop itself. Common when the repository directly wraps a database collection or table.

Driver Support

DriverImplements IControllableRepository?Default
MongoDB✅ — MongoRepository<TEntity> implements the interface nativelyEnabled (use WithoutLifecycle() to disable)
In-Memory❌ — uses InMemoryRepositoryLifecycleHandler<TEntity> insteadDisabled (use WithLifecycle() to enable)
EF Core❌ — uses a dedicated lifecycle handlerEnabled (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

  • NotSupportedException is re-thrown as-is.
  • RepositoryException is 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: Contact entity with GuidId
  • Custom Repository: ContactRepository extending IRepository<Contact, Guid>
  • Lifecycle Handler: ContactLifecycleHandler for create/drop/seed operations
  • Lifecycle Profile: SampleLifecycleProfile for environment-aware seeding
  • Seed Data: DefaultContactSeedData implementing IRepositorySeedDataProvider<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);