Compose reusable EF Core queries, runtime predicates and paging through one repository for .NET developers.
- Name reusable filter, include and ordering logic in a specification.
- Use one repository per configured DbContext for reads and tracked writes.
- Build runtime filters with explicit validation and choose offset, streamed or cursor paging.
Use a .NET 10 console project. Install the packages, then replace Program.cs with the example.
dotnet new console -f net10.0 -n EfCoreDemo
cd EfCoreDemo
dotnet add package DKNet.EfCore.Specifications
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.12using DKNet.EfCore.Specifications;
using DKNet.EfCore.Specifications.Definitions;
using DKNet.EfCore.Specifications.Extensions;
using DKNet.EfCore.Specifications.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
connection.Open();
var services = new ServiceCollection();
services.AddDbContext<DemoDb>(o => o.UseSqlite(connection));
services.AddSpecRepo<DemoDb>();
using var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
await using var db = scope.ServiceProvider.GetRequiredService<DemoDb>();
await db.Database.EnsureCreatedAsync();
var repo = scope.ServiceProvider.GetRequiredService<IRepositorySpec>();
await repo.AddRangeAsync(new[]
{
new Product { Name = "Coffee", Price = 12 },
new Product { Name = "Tea", Price = 6 }
});
await repo.SaveChangesAsync();
var rows = await repo.ToListAsync(new MinimumPrice(10));
Console.WriteLine(rows.Single().Name); // Coffee
public sealed class DemoDb(DbContextOptions<DemoDb> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}
public sealed class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public int Price { get; set; }
}
public sealed class MinimumPrice : Specification<Product>
{
public MinimumPrice(int minimum)
{
WithFilter(p => p.Price >= minimum);
AddOrderBy(p => p.Id);
AsNoTracking();
}
}Run with dotnet run.
IRepositorySpec is scoped and uses the registered DbContext. IRepositorySpecFactory is singleton; each provider it creates owns a new scope and factory-created context. Query operations remain deferred until materialized.
RepositorySpec.Query starts with AsExpandable() and then applies the specification. Query composition does not execute SQL; a terminal operation such as ToListAsync does. Null specifications throw, and database translation/execution failures propagate.
- Compose reusable queries — Configure filters, includes, ordering and tracking.
- Query and save through a repository — Register repositories, persist changes and own background scopes.
- Build runtime predicates — Validate caller filters before composing LINQ expressions.
- Project queries into models — Use Mapster-backed non-tracking model queries.
- Page and stream results — Choose offset pages, buffered enumeration or keysets.
- DKNet.EfCore.Abstractions — use it to model entities; specifications also accept any class entity.
- DKNet.EfCore.Extensions — use it for global filters and concurrency-aware persistence helpers.
- DKNet.EfCore.DtoGenerator — use it to generate model types for projections.
- DKNet.EfCore.DataAuthorization — use it for non-ignorable row ownership filtering.
- Repository migration guide — use it to move from the removed Repos packages.