eQuantic.Core.Data.PostgreSql
5.7.0
See the version list below for details.
dotnet add package eQuantic.Core.Data.PostgreSql --version 5.7.0
NuGet\Install-Package eQuantic.Core.Data.PostgreSql -Version 5.7.0
<PackageReference Include="eQuantic.Core.Data.PostgreSql" Version="5.7.0" />
<PackageVersion Include="eQuantic.Core.Data.PostgreSql" Version="5.7.0" />
<PackageReference Include="eQuantic.Core.Data.PostgreSql" />
paket add eQuantic.Core.Data.PostgreSql --version 5.7.0
#r "nuget: eQuantic.Core.Data.PostgreSql, 5.7.0"
#:package eQuantic.Core.Data.PostgreSql@5.7.0
#addin nuget:?package=eQuantic.Core.Data.PostgreSql&version=5.7.0
#tool nuget:?package=eQuantic.Core.Data.PostgreSql&version=5.7.0
eQuantic.Core.Data
A provider-agnostic Repository + Unit of Work for .NET, where filtering, sorting and paging are authored typed and fluent — not as stringly-typed configuration.
var repo = unitOfWork.GetAsyncRepository<OrderData, Guid>();
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*Page
Why
The Repository pattern keeps your domain ignorant of the persistence engine — you code against
IRepository<TEntity, TKey>, and the Entity Framework (or other) provider supplies the
implementation. What usually rots is the query surface: dozens of GetPaged/GetFiltered
overloads and Action<Configuration> callbacks, with filters and sorts passed as magic strings.
eQuantic.Core.Data v5 collapses that: one method per operation, each taking a single
QueryOptions<TEntity> that you compose fluently and typed, backed by the
eQuantic.Linq query engine.
Getting started
This package is the contracts — pair it with a provider that supplies the engine:
dotnet add package eQuantic.Core.Data
dotnet add package eQuantic.Core.Data.EntityFramework.SqlServer # or PostgreSql / MySql / MongoDb / CosmosDb
Give your entities a key via IEntity<TKey> (the key is exposed through GetKey/SetKey — no mandated
Id property):
using eQuantic.Core.Data.Repository;
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;
}
Register the provider's unit of work and repositories — the wiring is provider-specific (see the
Entity Framework Core provider) — then inject
IUnitOfWork, ask it for a repository, and query with a QueryOptions (the snippet at the top).
How you query
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
You reach for whichever filter form fits — all end up as one predicate the provider translates:
| Form | When |
|---|---|
Where(selector, op, value) / And / Or |
Primary — typed, fluent, compile-checked. |
Where(string path, op, value) |
Dynamic column names; operator and value stay typed. |
Where(ISpecification<T>) |
A reusable domain rule (specification pattern). |
Where(Expression<Func<T, bool>>) |
An arbitrary predicate you already hold. |
Where(ExpressionModel<T>) |
A serialized filter — built in code or received over the wire. |
Where("total:gt(100)") |
The boundary where a filter arrives as a query string (e.g. a filterBy parameter). Prefer the typed form in code. |
The query-string grammar behind the string forms (total:gt(100),status:eq(Paid)) is documented in
the eQuantic.Linq query-string reference.
Paging that tells you what you got
PagedResult<OrderData> page = await repo.GetPagedAsync(PageRequest.Of(2, 20), options);
// page.Items, page.TotalCount, page.PageIndex, page.PageSize, page.PageCount,
// page.HasPreviousPage, page.HasNextPage
PageRequest is one-based (Skip/Take derived); PagedResult<T> carries the items and the
totals — no second count call, no bare IEnumerable<T>.
Provider-agnostic by design
This package is the contracts (IRepository, IUnitOfWork, QueryOptions, PageRequest,
PagedResult, specifications). The persistence engine is kept out of the type signatures —
IRepository<TEntity, TKey>, not IRepository<TUnitOfWork, TEntity, TKey>. The Entity Framework
implementation lives in the provider packages (eQuantic.Core.Data.EntityFramework and friends).
Native providers — MongoDB, Azure Cosmos DB, Apache Cassandra, PostgreSQL, MySQL, SQL Server
The same contracts implemented directly on each official driver — no Entity Framework — with a
lean write model: Add/Modify/Remove buffer typed writes and one native batch runs on Commit
(no change tracking or snapshotting). Explicit transactions are opt-in: multi-document sessions on
MongoDB (reads inside the transaction see its writes), a single-partition TransactionalBatch on
Cosmos, an atomic LOGGED BATCH on Cassandra — and on PostgreSQL the commit itself is atomic
(one batched flush in a transaction, generated keys read back — RETURNING on PostgreSQL,
OUTPUT INSERTED on SQL Server — and explicit transactions spanning commits with
read-your-writes). The relational providers are thin dialects over the shared
eQuantic.Core.Data.Relational engine: PostgreSQL (Npgsql), MySQL (MySqlConnector) and
SQL Server (Microsoft.Data.SqlClient) differ only in their SqlDialect.
All providers ship the same fluent, typed migrations, authored with member selectors instead of
field strings:
services.AddMongoRepositories("mongodb://localhost:27017", "shop");
services.AddMongoMigrations(typeof(AddProductIndexes).Assembly);
[Migration("Product indexes", 2026, 7, 20, 14, 0, 0)]
public sealed class AddProductIndexes : Migration
{
public override void Up(IMigrationBuilder migration) =>
migration.For<Product>(product => product
.EnsureCollection()
.Index(x => x.Category)
.CompositeIndex(keys => keys.Descending(x => x.Price).Ascending(x => x.Name)));
}
// on startup: apply pending migrations, once each
await serviceProvider.GetRequiredService<IMigrationRunner>().RunAsync();
The engine — what the providers add on top of the drivers
- A complete SQL target (PostgreSQL) —
OR/NOT/!=/NULLpush down whole with C# semantics preserved (== null→IS NULL,!=matchesNULLrows,Containswith anullin the list matchesNULLcolumns), arrays are native (= ANY, atomic append/remove), sorting works on any column and paging is nativeLIMIT/OFFSETplus keyset continuation. - Pushdown + residual filtering (Cassandra) — every clause CQL can express runs on the cluster;
the ones it cannot (
ORacross columns,!=,NULL, arbitrary predicates) run client-side over the fetched rows behind the explicit.AllowClientEvaluation()opt-in, with.AllowFiltering()gating scans. AnORwhose branches each pin a partition needs no opt-in at all: it runs as parallel native queries, merged and de-duplicated by primary key. Explain()on every provider (IExplainableRepository<T>) — the native statement (CQL, Cosmos SQL, aggregation pipeline), what is pushed down versus evaluated client-side, partition scoping, and the opt-ins a query still needs. Builds the plan without executing anything.- Computed set-based updates —
UpdateMany(filter, x => new E { N = x.N + 1, Tags = x.Tags.Append("vip").ToList() })renders to the store's atomic form:$inc/$mul/$push/$addToSet/$pullAllon MongoDB (honouring[BsonElement]and custom serializers),PatchOperation.Increment/Addon Cosmos, collectioncol = col + ?and counter columns on Cassandra. Shapes a store cannot apply atomically are rejected with the reason — an update never silently degrades to read-modify-write. - Continuation paging and streaming (
IContinuationReadRepository<T>,IStreamingReadRepository<T>) — deep pages walk the native path (CassandraPagingState, Cosmos continuation tokens, MongoDB keyset by id), every page costing the same as the first, andIAsyncEnumerable<T>streams hold one page in memory at a time. - Typed
GroupBywithHAVING, and aggregates on the store (IGroupedReadRepository<T>,IAggregateReadRepository<T>) —GroupByAsync(x => x.Customer, g => new { g.Key, Orders = g.Count(), Revenue = g.Sum(x => x.Total) })renders to a nativeGROUP BYon the relational providers, a$grouppipeline on MongoDB, and a primary-key-restricted CQLGROUP BYon Cassandra. The filter pushes before grouping, the typedhavingpredicate —g => g.Sum(x => x.Total) > 100 && g.Count() >= 2— runs as SQLHAVING/ a Mongo$matchover the groups / against Cassandra's cluster-computed aggregate cells (CQL has noHAVING; no extra rows travel), and only the grouped rows come back.Min/Max/Averagepush down alongsideSum/Counton every provider. A filter the store cannot express degrades — behind.AllowClientEvaluation()— to grouping the fetched rows with the selectors themselves; a projection shape outside the portable contract is rejected with the supported shapes, never silently fetched. - Typed
UNION/UNION ALLacross entities (IUnionQueryRunner) — compose branches withUnion.Of<Order>().Where(x => x.Status == "overdue").Select(x => new { Name = x.Customer, Origin = "order" }), combine withUnionQuery.All(...)orUnionQuery.Distinct(...), order and page the combined result — one statement on the relational providers, one$unionWithaggregation on MongoDB. Branches project entity members or constants (tagging which branch a row came from), each branch's filter pushes inside its own branch with the entity's global filter applied per branch (IgnoringQueryFilters()opts a branch out), and a branch the store cannot express is rejected with guidance — a union never runs half a branch client-side. - Partition-key inference and ETag concurrency (Cosmos) — a filter that pins the partition key
scopes the query to a single partition automatically (the biggest RU saving there is), and
ConcurrencyToken(x => x.ETag)turnsModifyinto a conditionalIf-Matchreplace. - Prepared statements and LWT (Cassandra) — every repeated CQL text is prepared once per
session and bound thereafter;
AddIfNotExistsAsyncrunsINSERT … IF NOT EXISTSand reports whether it applied;WithConsistency(...)sets a per-query consistency level andCommitAsync(o => o.WithTtl(...))writes expiring rows. - Global query filters (multi-tenancy) — register
new QueryFilters().For<Order>(...)as a singleton and every read and set-based write is scoped by it on all three providers; a per-request factory receives the scope'sIServiceProvider(resolve the current tenant there), andIgnoringQueryFilters()opts a read out. A tenant filter on the Cosmos partition key auto-scopes the query to that partition. - OpenTelemetry built in — subscribe to the
eQuantic.Core.DataActivitySourceand every provider emits spans with the statement (placeholders, never values) plus the engine's own facts: client evaluation, split-query count, partition scoping, staged write counts.
The rule behind all of it: a query never lies about its cost. Anything beyond the store's
native access path is explicit and opt-in, and Explain() shows exactly what will run. Providers
target net10.0.
Install
dotnet add package eQuantic.Core.Data
Targets net8.0 and net10.0. Depends only on the framework-free
eQuantic.Linq.Web and
eQuantic.Linq.Specification.
Learn more
- Upgrading to v5.6 — the behaviour corrections in 5.6.0 and what they mean for code written against 5.4/5.5.
- Repository Pattern walkthrough — a full example: data entities, unit of work, repository, specifications and domain services.
- v5 contracts design — the rationale, the consolidated interface and the breaking-change/migration summary.
- Releasing — the automated release flow (maintainers).
Upgrading to v5
v5 is a deliberate breaking redesign: one QueryOptions argument per read (not
Action<Configuration> + overloads), PagedResult<T> paging, the unit-of-work type parameter
removed from IRepository/GetRepository, an IEntity<TKey> constraint (no new()), the
SQL/relational surface moved to the provider layer, and the monolithic eQuantic.Linq dependency
replaced by eQuantic.Linq.Web + eQuantic.Linq.Specification. The full before/after is in the
design doc.
MIT © eQuantic Tech
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- eQuantic.Core.Data (>= 5.7.0)
- eQuantic.Core.Data.Relational (>= 5.7.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Npgsql (>= 9.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Native PostgreSQL (Npgsql) implementation of the eQuantic.Core.Data contracts