The Entity Framework Core implementation of eQuantic.Core.Data.
You code against the provider-agnostic IRepository<TEntity, TKey> / IUnitOfWork contracts; this package
supplies the EF Core engine for SQL Server, PostgreSQL, MySQL, MongoDB and Azure Cosmos DB.
// A repository over any IEntity<TKey>, obtained from your DbContext-backed unit of work:
var repo = unitOfWork.GetAsyncRepository<OrderData, Guid>();
// Query typed and fluent — one QueryOptions, no magic strings:
var page = await repo.GetPagedAsync(
PageRequest.Of(pageIndex: 1, pageSize: 20),
new QueryOptions<OrderData>()
.Where(o => o.Total, FilterOperator.GreaterThan, 100m)
.And(o => o.Customer.Name, FilterOperator.Contains, term)
.OrderByDescending(o => o.CreatedAt)
.Include(nameof(OrderData.Customer))
.NoTracking());
// page is a PagedResult<OrderData>: Items + TotalCount + PageIndex/PageSize/PageCount + Has*PageThe Repository pattern keeps your domain ignorant of the persistence engine — you code against
IRepository<TEntity, TKey> and can swap SQL Server for PostgreSQL, MongoDB or Cosmos DB without touching a
line of domain code. What usually rots is the query surface: a sprawl of GetPaged/GetFiltered
overloads and Action<Configuration> callbacks, with filters passed as magic strings.
On the eQuantic.Core.Data.Abstractions contracts, this provider collapses that into one QueryOptions<TEntity>
per read — authored typed and fluent, checked at compile time, and translated to EF Core server-side.
Install the provider for your database — pick the major that matches your runtime (see Versioning below):
dotnet add package eQuantic.Core.Data.EntityFramework.SqlServer --version 8.*Give your entities a key via IEntity<TKey>, keep your usual DbContext, and derive the provider's unit of
work:
using eQuantic.Core.Data.Repository;
using eQuantic.Core.Data.EntityFramework.SqlServer.Repository;
using Microsoft.EntityFrameworkCore;
public class OrderData : IEntity<Guid>
{
public Guid Id { get; set; }
public decimal Total { get; set; }
public Guid GetKey() => Id;
public void SetKey(Guid key) => Id = key;
}
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<OrderData> Orders => Set<OrderData>();
}
public interface IAppUnitOfWork : IQueryableUnitOfWork { }
public class AppUnitOfWork(IServiceProvider sp, AppDbContext ctx)
: UnitOfWork<AppDbContext>(sp, ctx), IAppUnitOfWork;Register the context and repositories — AddRelationalRepositories for the SQL providers,
AddQueryableRepositories for the document providers (MongoDB, Cosmos DB):
services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));
services.AddRelationalRepositories<IAppUnitOfWork, AppUnitOfWork>();Then inject IAppUnitOfWork, ask it for a repository, and query with a QueryOptions (the snippet above).
The full slice — specifications, custom repositories, a domain service — is in the
walkthrough.
Swap the suffix (
PostgreSql,MySql,MongoDb,CosmosDb) and theUseXxxcall to target another database.
eQuantic.Core.Data.Abstractions defines the contracts — IRepository, IUnitOfWork,
QueryOptions, PageRequest, PagedResult, specifications — with the persistence engine kept out of the
type signatures (IRepository<TEntity, TKey>, not IRepository<TUnitOfWork, TEntity, TKey>).
That is the only eQuantic package this one depends on. It carries no engine: installing an EF Core
provider from here brings the vocabulary and Entity Framework, and nothing of the native
eQuantic.Core.Data engine — no SQL renderer, no expression interpreters, no analyzers. If you want the
native engine as well, reference it yourself; the two implement the same contracts side by side.
This package is the Entity Framework Core implementation of those contracts. It translates a single
QueryOptions<TEntity> into an EF IQueryable — applying, in order, custom before hooks, the
specification and predicate filter, eager Includes, sortings (server-side / EF-translatable),
AsNoTracking, IgnoreQueryFilters, query tags and custom after hooks — and returns PagedResult<T>
from paged reads. The relational providers also carry a parameterized raw-SQL executor (functions and
stored procedures, always via DbParameter).
QueryOptions<TEntity> mirrors the eQuantic.Linq query builders, so filters read like code and fail at
compile time — not at runtime:
new QueryOptions<OrderData>()
.Where(o => o.Total, FilterOperator.GreaterThanOrEqual, 100m) // typed member selector
.And(o => o.Status, FilterOperator.Equal, OrderStatus.Paid) // clauses fold left to right:
.Or(o => o.Customer.IsVip, FilterOperator.Equal, true) // (total>=100 AND paid) OR vip
.OrderByDescending(o => o.CreatedAt)
.ThenBy("customer.name") // string path for dynamic columns
.Include(nameof(OrderData.Customer))
.NoTracking();You reach for whichever filter form fits — member selector, string path, ISpecification<T>,
Expression<Func<T,bool>>, a serialized ExpressionModel<T>, or an eQuantic.Linq.Web query string — all
end up as one predicate the provider translates. The full query-string grammar is documented in the
eQuantic.Linq reference.
| Package | Database |
|---|---|
eQuantic.Core.Data.EntityFramework.SqlServer |
SQL Server |
eQuantic.Core.Data.EntityFramework.PostgreSql |
PostgreSQL |
eQuantic.Core.Data.EntityFramework.MySql |
MySQL (Pomelo) |
eQuantic.Core.Data.EntityFramework.MongoDb |
MongoDB (EF Core provider) |
eQuantic.Core.Data.EntityFramework.CosmosDb |
Azure Cosmos DB (EF Core provider) |
The three relational providers share eQuantic.Core.Data.EntityFramework.Relational; the document providers
(MongoDb, CosmosDb) are non-relational and build directly on the base
eQuantic.Core.Data.EntityFramework. Register your DbContext-backed unit of work and the open-generic
repositories through AddRelationalRepositories<TUnitOfWorkInterface, TUnitOfWorkImpl>() (relational) or the
base AddQueryableRepositories<TUnitOfWork>() (document) — the full wiring is in the
walkthrough.
Azure Cosmos DB: scope a read to one logical partition with the Cosmos-specific WithPartitionKey
extension so it doesn't fan out into a cross-partition scan:
new QueryOptions<OrderData>()
.WithPartitionKey(tenantId)
.Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid);Cosmos has no server-side set-based delete/update (ExecuteDelete/ExecuteUpdate are relational-only), so
DeleteMany/UpdateMany load the matching documents and modify them through the context.
Pick the package major that matches your runtime — this library targets .NET 8 and .NET 10, and each runtime is published as its own package major so the EF Core lines never mix:
| Your app | Install | Targets |
|---|---|---|
| .NET 8 | 8.x (e.g. 8.2.0) |
net8.0, EF Core 8 |
| .NET 10 | 10.x (e.g. 10.1.0) |
net10.0, EF Core 10 |
The shared multi-framework assemblies (the referenceable base
eQuantic.Core.Data.EntityFrameworkandeQuantic.Core.Data.EntityFramework.Relational) stay in the 4.x line on purpose — a neutral lane that must not be read as a .NET version. You normally consume only the provider package for your runtime (8.x / 10.x), which pulls the right shared assemblies transitively.
Entity Framework records which migrations ran, and has-pending-model-changes compares the model to
the migrations. Neither looks at the schema — so neither can tell you that a column was altered by hand
on staging, that a migration stopped halfway, or that an environment was restored from a backup older
than the last release. Only looking answers those.
services.AddDbContext<ShopContext>(options => options.UseNpgsql(connectionString));
services.AddDriftCheck<ShopContext>();That registers IDatabaseSnapshotSource, which describes both sides of the comparison — what the model
says the tables should be, and what the database actually has — in the provider's own store-type
vocabulary, so a healthy schema compares silently:
var source = provider.GetRequiredService<IDatabaseSnapshotSource>();
var report = DriftComparer.Compare(source.Expect(), await source.ObserveAsync());
if (report.Breaks)
{
// something the application reads on every query is not there, or not as it expects
}report.Breaks separates the differences that stop the application from the ones that do not: a column
the model does not map belongs to somebody else — databases get shared — and tables you do not map are
not read at all. The command-line tool eqdata drift
runs the same check and exits non-zero on the first kind, which makes it a deployment gate.
Reading the catalogue is provider-specific: PostgreSQL, MySQL and SQL Server are covered. Any other provider says it cannot answer rather than answering wrongly.
Generating migrations is deliberately absent. Entity Framework already does that, from the same model, and better than a second generator could. What this adds is the part it leaves out.
- Repository Pattern walkthrough — data entities, unit of work, repository and specifications, end to end.
- eQuantic.Core.Data.Abstractions — the contracts and the
QueryOptions/PagedResult/PageRequestquery surface, backed by the eQuantic.Linq query engine. This is what this package depends on. - eQuantic.Core.Data — the native engine over the same contracts, for the six stores it drives without Entity Framework.
MIT © eQuantic Tech