Compose reusable EF Core queries, runtime predicates and paging through one repository for .NET developers.
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.12
using 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.