eQuantic.Core.Data.MongoDb
5.7.0
See the version list below for details.
dotnet add package eQuantic.Core.Data.MongoDb --version 5.7.0
NuGet\Install-Package eQuantic.Core.Data.MongoDb -Version 5.7.0
<PackageReference Include="eQuantic.Core.Data.MongoDb" Version="5.7.0" />
<PackageVersion Include="eQuantic.Core.Data.MongoDb" Version="5.7.0" />
<PackageReference Include="eQuantic.Core.Data.MongoDb" />
paket add eQuantic.Core.Data.MongoDb --version 5.7.0
#r "nuget: eQuantic.Core.Data.MongoDb, 5.7.0"
#:package eQuantic.Core.Data.MongoDb@5.7.0
#addin nuget:?package=eQuantic.Core.Data.MongoDb&version=5.7.0
#tool nuget:?package=eQuantic.Core.Data.MongoDb&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.Linq.Expressions (>= 3.7.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- MongoDB.Driver (>= 3.10.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.0 | 0 | 7/22/2026 |
| 5.8.0 | 0 | 7/22/2026 |
| 5.7.0 | 0 | 7/22/2026 |
| 5.6.1 | 0 | 7/21/2026 |
| 5.6.0 | 0 | 7/21/2026 |
| 5.5.0 | 0 | 7/21/2026 |
| 5.4.0 | 0 | 7/21/2026 |
| 5.3.0 | 35 | 7/20/2026 |
| 5.2.0 | 38 | 7/20/2026 |
| 1.1.1 | 427 | 4/17/2024 |
| 1.1.0 | 474 | 2/12/2024 |
| 1.1.0-beta7 | 204 | 2/12/2024 |
| 1.1.0-beta6 | 239 | 1/19/2024 |
| 1.1.0-beta5 | 208 | 1/19/2024 |
| 1.1.0-beta4 | 208 | 1/17/2024 |
| 1.1.0-beta3 | 301 | 12/21/2023 |
| 1.1.0-beta2 | 210 | 12/21/2023 |
| 1.1.0-beta1 | 219 | 12/19/2023 |
| 1.0.0.3-beta6 | 651 | 7/5/2020 |
| 1.0.0.2-beta6 | 602 | 6/7/2020 |
Native MongoDB Driver implementation of the eQuantic.Core.Data contracts