Skip to main content
Version: v1.7.2

Migrating from 1.7 to 1.7.1

The 1.7.1 — Obsoletes Cleanup release removes the long-deprecated IQueryableRepository, IPageableRepository, and IFilterableRepository interfaces (in both <TEntity> and <TEntity, TKey> arities), together with their associated extension classes:

  • Kista.PageableRepositoryExtensions
  • Kista.QueryableRepositoryExtensions
  • The AsFilterable() and AsQueryable() helper extensions on RepositoryExtensions

All functionality they provided is available through the Repository<TEntity, TKey> base class (via protected members) and through extension methods on IRepository<TEntity>. This is a breaking change for code that explicitly implemented, injected, or called the removed types — such code will no longer compile.

Why the interfaces were removed

The three legacy interfaces exposed query internals (IQueryable<T>, PageQuery<T>, IQueryFilter) as a public contract, leaking the data layer into consumer code. The same capabilities are already provided by the Repository<TEntity, TKey> abstract class through protected members, which keeps query composition inside the data layer and avoids the IQueryable<T> leak that produced NotSupportedExceptions far from the repository.

See the Repository Pattern page for the design rationale, and Customize the Repository for the recommended patterns going forward.

Migration table

Before (1.7.0)After (1.7.1)
Inject IQueryableRepository<T> / IQueryableRepository<T, TKey>Inject IRepository<T> and call a domain-specific method on a custom repository interface
Inject IPageableRepository<T> / IPageableRepository<T, TKey>Inject IRepository<T> and call GetPageAsync(PageRequest) for unsorted pagination, or a domain-specific paged method for filtered/sorted pages
Inject IFilterableRepository<T> / IFilterableRepository<T, TKey>Inject IRepository<T> and call extension methods (FindAllAsync, CountAsync, ExistsAsync, FindFirstAsync) that accept IQueryFilter / lambda expressions
repository.AsQueryable()Define a domain-specific method on a custom repository (e.g. IProductRepository.FindDiscontinuedAsync) implemented via the protected Queryable() hatch
repository.AsFilterable()Call FindAllAsync, CountAsync, ExistsAsync, FindFirstAsync directly on IRepository<T> (extension methods) or on the Repository<T,TKey> base class from a subclass
repository.GetPageAsync(PageQuery<T>)repository.GetPageAsync(PageRequest) for unsorted pages; for filtered/sorted pages expose a domain-specific paged method implemented via the protected QueryPageAsync(PageQuery<T>)
Class : Repository<T, TKey>, IQueryableRepository<T, TKey>Class : Repository<T, TKey> only — the Repository base class already provides the query capabilities through protected members

Replace AsQueryable() consumer code

The AsQueryable() extension leaked the underlying IQueryable<T> to consumers. Replace it with a domain-specific method on a custom repository, implemented through the protected Queryable() hatch.

Before (1.7.0):

// Consumer code — leaks IQueryable
public class ProductSearch {
private readonly IQueryableRepository<Product> _products;

public ProductSearch(IQueryableRepository<Product> products) {
_products = products;
}

public IReadOnlyList<Product> ActiveSorted() {
return _products.AsQueryable()
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToList();
}
}

After (1.7.1):

// Domain interface — query is encapsulated
public interface IProductRepository : IRepository<Product, Guid> {
Task<IReadOnlyList<Product>> FindActiveSortedAsync(CancellationToken ct = default);
}

// Implementation — uses the protected Queryable() hatch
public class ProductRepository : InMemoryRepository<Product, Guid>, IProductRepository {
public async Task<IReadOnlyList<Product>> FindActiveSortedAsync(CancellationToken ct = default) {
return await Queryable()
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToListAsync(ct);
}
}

// Consumer code — depends on the domain contract, not on IQueryable
public class ProductSearch {
private readonly IProductRepository _products;

public ProductSearch(IProductRepository products) {
_products = products;
}

public Task<IReadOnlyList<Product>> ActiveSorted(CancellationToken ct = default)
=> _products.FindActiveSortedAsync(ct);
}

Replace AsFilterable() and filter-based queries

Filter-based queries no longer require resolving IFilterableRepository<T>. Call the extension methods on IRepository<T> directly, or use the protected members on a Repository<T, TKey> subclass.

Before (1.7.0):

var filterable = repository.AsFilterable();
var items = await filterable.FindAllAsync(filter);
var count = await filterable.CountAsync(filter);
var exists = await filterable.ExistsAsync(filter);

After (1.7.1):

// Extension methods on IRepository<T> accept IQueryFilter or lambdas
var items = await repository.FindAllAsync(filter);
var count = await repository.CountAsync(filter);
var exists = await repository.ExistsAsync(filter);

// Lambda shorthand also works
var active = await repository.FindAllAsync(p => p.IsActive);

From inside a Repository<T, TKey> subclass, call the protected members directly:

public async Task<bool> CodeExistsAsync(string code, CancellationToken ct = default) {
var filter = new ExpressionQueryFilter<Product>(p => p.Code == code);
return await ExistsAsync(filter, ct); // protected member
}

Replace GetPageAsync(PageQuery<T>)

The public IPageableRepository<T>.GetPageAsync(PageQuery<T>) overload is gone. For unsorted pagination, use IRepository<T>.GetPageAsync(PageRequest):

var page = await repository.GetPageAsync(new PageRequest(page: 1, size: 20));

For filtered/sorted pagination, expose a domain-specific paged method on your custom repository, implemented through the protected QueryPageAsync(PageQuery<T>):

public interface IProductRepository : IRepository<Product, Guid> {
Task<PageQueryResult<Product>> FindByCategoryPagedAsync(
string category, PageRequest request, CancellationToken ct = default);
}

public class ProductRepository : InMemoryRepository<Product, Guid>, IProductRepository {
public async Task<PageQueryResult<Product>> FindByCategoryPagedAsync(
string category, PageRequest request, CancellationToken ct = default) {
var query = new PageQuery<Product>(request.Page, request.Size)
.Where(p => p.Category == category)
.OrderBy(p => p.Name);
return await QueryPageAsync(query, ct); // protected member
}
}

Remove the legacy interfaces from repository declarations

Concrete repositories that explicitly implemented the removed interfaces must drop those declarations. The Repository<TEntity, TKey> base class already provides the equivalent protected members.

Before (1.7.0):

public class LegacyRepository : Repository<Product, Guid>,
IQueryableRepository<Product, Guid>,
IPageableRepository<Product, Guid>,
IFilterableRepository<Product, Guid> {
// explicit interface method implementations...
}

After (1.7.1):

public class LegacyRepository : Repository<Product, Guid>, IProductRepository {
// domain-specific methods using protected members (Queryable(), ExistsAsync, etc.)
}

Drop any explicit interface method implementations (IFilterableRepository<...>.ExistsAsync, IPageableRepository<...>.GetPageAsync, etc.) — the base class now handles these through its protected members, and the public extension methods on IRepository<T> cover the consumer-side surface.

DI registration

AddRepository<T>() and ScanRepositories() no longer register IQueryableRepository<T>, IPageableRepository<T>, or IFilterableRepository<T> as services. This is expected — those interfaces no longer exist. Resolve IRepository<T> or your custom interface instead. See Registration for the current behaviour.

Queryable() is now protected

In a follow-on tightening to the 1.7.1 cleanup, the Repository<TEntity, TKey>.Queryable() method changed from public abstract to protected abstract. The in-memory RepositoryWrapper.Queryable() override was removed entirely. This closes the last public IQueryable<T> escape hatch on the base class — the queryable is now reachable only from inside a subclass and from the base-class query pipeline.

Who is affected

  • Consumer code that called repo.Queryable() directly from outside a subclass (or from another assembly) — this was already undocumented and discouraged, but it compiled in 1.7.1. It no longer compiles.
  • Companion-assembly authors who reached into Queryable() from another assembly to apply filters, counts, or existence checks. The Kista-owned companions (UserScopedRepositoryDecorator, SpecificationRepositoryExtensions) were migrated to the new dispatch path (see below); third-party companions doing the same must follow the same migration.
  • Subclasses that override Queryable() must change the override from public override to protected override to match the new base-class visibility.

Migration

From outside the subclass / another assembly — define a domain-specific method on a custom repository interface, implemented through the protected Queryable() hatch inside the subclass. This is the same pattern already documented above in Replace AsQueryable() consumer code: the consumer depends on a domain contract, not on IQueryable<T>.

From a Kista-owned companion assembly — use the new internal dispatch entry points on Repository<TEntity, TKey> instead of calling Queryable() directly:

Old (1.7.1)New (1.7.x)
query.Apply(repo.Queryable()).FirstOrDefault()repo.FindFirstAsyncInternal(query, ct)
query.Apply(repo.Queryable()).ToList()repo.FindAllAsyncInternal(query, ct)
filter.Apply(repo.Queryable()).LongCount()repo.CountAsyncInternal(filter, ct)
filter.Apply(repo.Queryable()).Any()repo.ExistsAsyncInternal(filter, ct)

These internal methods forward to the existing protected virtual filterable methods (FindFirstAsync, FindAllAsync, CountAsync, ExistsAsync) and are accessible to Kista-owned assemblies through InternalsVisibleTo. They are not part of the public API surface — consumer code must not call them.

In subclasses — change the override visibility:

// Before (1.7.1)
public override IQueryable<Product> Queryable() => _context.Set<Product>().AsQueryable();

// After (1.7.x)
protected override IQueryable<Product> Queryable() => _context.Set<Product>().AsQueryable();

The protected member is still overridable in subclasses; the change only closes the cross-assembly public escape. Subclass code that called Queryable() internally (to implement domain-specific query methods) is unaffected.

The ParameterReplacer expression visitor in Kista core was removed in the same change — it was only consumed by the removed queryable-composition paths. Consumer code never referenced it directly.

Reference