Migrating from 1.7.2 to 1.7.3
The 1.7.3 — Extensible Operation Pipeline & Obsoletes Cleanup release ships two coordinated changes:
- New feature — an extensible operation pipeline on
EntityManagerfor cross-cutting concerns (audit, events, tracing, validation short-circuit). See Operation Pipeline. - Breaking removal — every
[Obsolete]member in the framework is gone. Code that still referenced the legacy registration extensions, the repository controller, orEntityManager.GetPageAsync(PageQuery<T>)will no longer compile.
This page covers the breaking half. For the prior 1.7.0 → 1.7.1 migration (IQueryableRepository / IPageableRepository / IFilterableRepository removal), see Migrating from 1.7.
Why the obsolete APIs were removed
The framework has been nudging consumers toward the fluent AddRepositoryContext() builder since v1.6.0. The legacy IServiceCollection extensions and IRepositoryController family were marked [Obsolete] through 1.7.x to give migration time; 1.7.3 closes that window. Keeping them any longer would mean maintaining two parallel registration surfaces indefinitely — and the new operation pipeline is wired through the builder, not the legacy extensions, so the legacy surface could not have gained the feature anyway.
Migration table
| Before (1.7.2) | After (1.7.3) |
|---|---|
services.AddRepository<TRepo>() | services.AddRepositoryContext().AddRepository<TRepo>(_ => { }) then .UseInMemory() / .UseEntityFramework<TContext>() / .UseMongoDB<TContext>() |
services.AddRepository<TImpl, TService>() | Register the concrete via .AddRepository<TImpl>() and resolve the service interface from DI |
services.AddRepositoryController<T>() / AddRepositoryController() | services.AddRepositoryContext().UseInMemory(b => b.WithLifecycle()) and resolve IRepositoryLifecycleService — see Repository Lifecycle |
IRepositoryController / DefaultRepositoryController / RepositoryControllerAdapter / RepositoryControllerOptions | IRepositoryLifecycleService and driver-specific IRepositoryLifecycleHandler<T> implementations |
services.AddEntityManager<TManager>() | services.AddRepositoryContext().AddRepository<TEntityRepository>(repo => repo.WithManagement(mgmt => mgmt.UsingManager<TManager>())) |
services.AddManagerFor<TEntity>() / AddManagerFor<TEntity, TKey>() | .WithManagement() on the RepositoryBuilder (registers EntityManager<TEntity> / EntityManager<TEntity, TKey> automatically) |
services.AddEntityCacheOptions<T>() | .WithManagement(mgmt => mgmt.WithEasyCaching(opts => { ... })) |
services.AddEntityCacheKeyGenerator<T>() | .WithManagement(mgmt => mgmt.WithCacheKeyGenerator<T>()) |
services.AddEntityValidator<T>() | .WithManagement(mgmt => mgmt.WithValidator<T>()) |
services.AddEntityRepository<T>() | .AddRepository<T>() on the RepositoryContextBuilder |
services.AddMongoDbContext<TContext>() | services.AddRepositoryContext().UseMongoDB<TContext>() — tenant context types (IMongoDbTenantContext, MongoDbContext, MongoDbTenantContext) are registered via MongoRepositoryBuilder.RegisterAdditionalContextTypes() |
services.AddEntityEasyCache<T>() / AddEntityEasyCacheConverter<T>() | .WithEasyCaching() on the RepositoryBuilder / RepositoryContextBuilder |
manager.GetPageAsync(PageQuery<T>) | manager.GetPageAsync(new PageRequest(page, size)) for unsorted pages; for filtered/sorted pages expose a domain-specific paged method on a custom repository via the protected QueryPageAsync(PageQuery<T>) |
Replace AddRepository<T>()
Before (1.7.2):
services.AddRepository<InMemoryRepository<Person>>();
services.AddRepository<PersonRepository, IPersonRepository>();
After (1.7.3):
services.AddRepositoryContext()
.AddRepository<PersonRepository>(_ => { }) // registers PersonRepository, IPersonRepository, IRepository<Person>
.UseInMemory(); // also registers open-generic InMemoryRepository<Person>
Closed repositories registered via
AddRepository<T>()take precedence over the open generic for that specific entity. The two registration styles coexist — see Registration.
Replace AddEntityManager<TManager>()
The replacement is UsingManager<TManager>() on EntityManagerBuilder. It inspects the manager type via reflection to infer the entity (and key) types, mirroring what AddEntityManager<TManager>() did internally.
Before (1.7.2):
services.AddRepository<OrderRepository>();
services.AddEntityManager<OrderManager>();
After (1.7.3):
services.AddRepositoryContext()
.AddRepository<OrderRepository>(repo => repo
.WithManagement(mgmt => mgmt
.WithValidator<OrderValidator>()
.UsingManager<OrderManager>()))
.UseInMemory();
The base EntityManager<Order> is still registered automatically by WithManagement(), so DI continues to resolve both the base type and your custom subclass.
Replace AddMongoDbContext<TContext>()
The obsolete extension handled three concerns at once: registering the Mongo context, registering repositories, and registering tenant context types. The builder splits them cleanly.
Before (1.7.2):
services.AddMongoDbContext<TenantMongoContext>();
After (1.7.3):
services.AddRepositoryContext()
.UseMongoDB<TenantMongoContext>(b => b
.WithConnectionString("mongodb://..."));
For multi-tenant contexts that previously surfaced IMongoDbTenantContext / MongoDbTenantContext as DI services, MongoRepositoryBuilder.RegisterAdditionalContextTypes() is now called internally by UseMongoDB<TContext>() when the context implements the tenant interfaces — no manual call is required in consumer code. Custom driver authors who build a MongoRepositoryBuilder directly should call RegisterAdditionalContextTypes() once after construction.
Replace IRepositoryController and DefaultRepositoryController
IRepositoryController was a generic create/drop/seed surface that did not map cleanly to driver-specific behavior. It is replaced by IRepositoryLifecycleService and driver-specific IRepositoryLifecycleHandler<T> implementations — see Repository Lifecycle.
Before (1.7.2):
services.AddRepositoryController<ContactRepository>();
// ...
var controller = provider.GetRequiredService<IRepositoryController>();
await controller.CreateRepositoryAsync<Contact, Guid>(ct);
await controller.SeedRepositoryAsync<Contact, Guid>(seedData, ct);
After (1.7.3):
services.AddRepositoryContext()
.UseInMemory(b => b.WithLifecycle())
.ConfigureLifecycle(options => {
options.SeedStrategy = SeedStrategy.ByEnvironment;
})
.WithLifecycleHandler<Contact, ContactLifecycleHandler>();
// ...
var lifecycle = provider.GetRequiredService<IRepositoryLifecycleService>();
await lifecycle.CreateRepositoryAsync<Contact, Guid>(ct);
await lifecycle.SeedRepositoryAsync<Contact, Guid>(null, ct);
Replace AddEntityEasyCache<T>() and cache-key generators
Before (1.7.2):
services.AddRepository<PersonRepository>();
services.AddEntityEasyCache<Person>();
services.AddEntityCacheKeyGenerator<PersonCacheKeyGenerator>();
After (1.7.3):
services.AddRepositoryContext()
.AddRepository<PersonRepository>(repo => repo
.WithManagement(mgmt => mgmt
.WithEasyCaching(opts => opts.DefaultExpiration = TimeSpan.FromMinutes(15))
.WithCacheKeyGenerator<PersonCacheKeyGenerator>()))
.UseInMemory();
See Caching Entities for the full caching pipeline.
Replace EntityManager.GetPageAsync(PageQuery<T>)
The PageQuery<T> overload on EntityManager is gone. Use PageRequest for unsorted pages, or expose a domain-specific paged method on a custom repository via the protected QueryPageAsync(PageQuery<T>).
Before (1.7.2):
var query = new PageQuery<Person>(1, 20)
.Where(p => p.IsActive)
.OrderBy(p => p.LastName);
var page = await manager.GetPageAsync(query, ct);
After (1.7.3) — unsorted page:
var page = await manager.GetPageAsync(new PageRequest(1, 20), ct);
After (1.7.3) — filtered / sorted page:
public interface IPersonRepository : IRepository<Person, Guid> {
Task<PageQueryResult<Person>> FindActivePagedAsync(PageRequest request, CancellationToken ct = default);
}
public class PersonRepository : EntityRepository<Person, Guid>, IPersonRepository {
public async Task<PageQueryResult<Person>> FindActivePagedAsync(
PageRequest request, CancellationToken ct = default)
{
var query = new PageQuery<Person>(request.Page, request.Size)
.Where(p => p.IsActive)
.OrderBy(p => p.LastName);
return await QueryPageAsync(query, ct); // protected member
}
}
// caller
var page = await personRepo.FindActivePagedAsync(new PageRequest(1, 20), ct);
Operation pipeline: the new extension point
If you previously subclassed EntityManager to add cross-cutting concerns (audit, event emission, OpenTelemetry), or built bespoke decorators, migrate to the pipeline. The existing protected virtual On*Async hooks are preserved and run as a builtin interceptor appended last in the chain, so subclass overrides keep working with no code change — but new concerns should be added as IEntityManagerInterceptor<,> implementations registered via WithInterceptor<T>().
See Operation Pipeline for the full contract, registration, and short-circuit examples.
Cache alignment to the operation pipeline
The entity cache is now aligned to the operation pipeline through a builtin CacheInterceptor (see Builtin CacheInterceptor). This is a non-breaking change for the vast majority of consumers: the cache backend packages (Kista.Manager.EasyCaching, MemoryCache, DistributedCache, FusionCache) register IEntityCache<TEntity> unchanged, and the interceptor consumes it — only the call site moves from inline SetToCacheAsync / EvictAsync helpers (now removed from EntityManager) to PostWriteAsync.
Behavior change: RemoveRangeAsync and soft-deletable entities
Before this release, RemoveRangeAsync always evicted every entity in the batch from the cache, even ISoftDeletable ones — inconsistent with RemoveAsync, which re-caches soft-deletable entities (the soft-deleted row still exists in the repository). The builtin CacheInterceptor now handles each entity in the batch per the same ISoftDeletable rule as the single RemoveAsync:
- Soft-deletable entities in a range Remove are now re-cached (the cached entry is refreshed with the soft-delete stamp applied), matching
RemoveAsync. - Non-soft-deletable entities in a range Remove continue to be evicted, matching
RemoveAsyncandHardDeleteAsync.
If you relied on the old per-entity eviction of soft-deletable entities in RemoveRangeAsync, register a custom interceptor that calls IEntityCache<TEntity>.RemoveAsync in PostWriteAsync for EntityOperationKind.Remove on ISoftDeletable entities, or do not register a cache.
Reference
- Operation Pipeline — the new interceptor chain on
EntityManager(including the builtinCacheInterceptor) - The Entity Manager — registration, validation, caching, HTTP request cancellation
- Migrating from 1.7 — the prior 1.7.0 → 1.7.1 migration (
IQueryableRepository/IPageableRepository/IFilterableRepositoryremoval) - Repository Lifecycle — the
IRepositoryLifecycleServicereplacement forIRepositoryController - Customize the Repository — the fluent
AddRepositoryContext()builder API