In-memory provider
InMemoryDataContextruns queries against in-process CLR collections; use it for unit tests and query-shape/plan-cache tests where starting a database is unnecessary.
Prerequisites: Provider overview ยท Dependency injection
Overview
The in-memory provider is built into nextorm (no extra package) and lives in
src/nextorm.core/DataContext/InMemory/InMemoryDataContext.cs. The public type is InMemoryDataContext, which
implements IDataContext directly โ it has no dialect, no connection and no SQL, and evaluates the
query expression tree over the data you attach to it.
Unlike the SQL providers, InMemoryDataContext requires no connection string. Query metadata and select
lists are shared process-wide with the SQL providers through DataContextCache, while compiled
delegates are per-instance (they capture the context they were compiled for).
Use it when you want:
- unit tests for application code that depends on
IDataContext; - fast tests of query shape, projection and join behaviour without a container or file;
- tests of the implicit plan cache (
QueryPlanEqualityComparer,GetCacheVersion).
It is not a SQL engine: it does not validate that a query would run on a real provider, and several features are deliberately unsupported (see below).
Registration and usage
With dependency injection:
using NextORM.Core;
services.AddNextOrmContext<InMemoryDataContext>();
using NextORM.Core;
using var ctx = new InMemoryDataContext();
ctx.From<SimpleEntity>().WithData(new[]
{
new SimpleEntity { Id = 1 },
new SimpleEntity { Id = 2 },
});
var ids = ctx.From<SimpleEntity>()
.Where(it => it.Id == 1)
.Select(it => new { it.Id })
.SingleOrDefault();
// ids.Id == 1
WithData and WithAsyncData (src/nextorm.core/Builders/InMemoryCommandBuilder.cs) attach a
collection to the context's Data dictionary keyed by entity type; WithData takes an IEnumerable<T>
and WithAsyncData an IAsyncEnumerable<T>. Both are no-ops for other providers.
ctx.From<SimpleEntity>().WithAsyncData(GetRows());
static async IAsyncEnumerable<SimpleEntity> GetRows()
{
yield return new SimpleEntity { Id = 1 };
await Task.Delay(0);
yield return new SimpleEntity { Id = 2 };
}
Supported feature subset
Covered by InMemoryTests and InMemoryJoinTests:
| Feature | Evidence |
|---|---|
Projection: anonymous, primitive/scalar, Tuple |
InMemoryTests.SelectPrimitive_ShouldReturnData, TestTuples |
Where, including == on nullable and captured values |
InMemoryTests.TestWhere, Contains_ShouldFilterData |
Subquery used as a FROM source (ctx.From(subQuery)) |
InMemoryTests.TestWhere_Subquery |
Buffered and async sources (WithData / WithAsyncData) |
InMemoryTests.TestAsync |
Streaming with Pipeline, observing cancellation |
InMemoryTests.TestFetch, TestFetch_PipelineStopsOnCancellation |
Limit / Offset / First / Single and their OrDefault forms |
InMemoryTests.Top_ShouldLimitData, First_ShouldReturnFirst, Single_ShouldReturnSingle |
OrderBy / OrderByDescending, including async sources |
InMemoryTests.OrderBy_ShouldSortData, OrderByOverAsyncSource_ShouldSortData |
Materializers ToArray / ToHashSet / ToDictionary (and async) |
InMemoryTests.ToArray_ShouldReturnData, ToHashSet_ShouldReturnData, ToDictionary_ShouldReturnData |
Any and projected exists |
InMemoryTests.SelectAny_ShouldReturnData, Any |
Distinct over value-equality projections |
InMemoryTests.TestDistinct |
| Joins: inner, left, right, full, cross, chained to 8 tables | InMemoryJoinTests.TestJoin, TestLeftJoin, TestRightJoinChained, TestFullJoinChained, TestCrossJoin8Tables_ShouldCloneAndMaterializeAtEveryArity |
Aggregates Count/Sum/Min/Max/Avg/Stdev/Var (buffered sources) |
InMemoryTests.Count_ShouldReturnRowCount, Sum_ShouldReturnSum, MinMax_ShouldReturnBounds, Avg_ShouldReturnAverage |
GroupBy / Having with per-group aggregates (buffered sources) |
InMemoryTests.GroupBy_ShouldAggregatePerGroup, GroupBy_Having_ShouldFilterGroups, GroupBy_Avg_ShouldAggregatePerGroup, GroupBy_OrderByColumn_ShouldSortGroups |
Set operations UNION / UNION ALL / INTERSECT / INTERSECT ALL / EXCEPT / EXCEPT ALL |
InMemoryTests.Union_ShouldRemoveDuplicates, UnionAll_ShouldKeepDuplicates, Intersect_ShouldReturnOnlyCommonRows, Except_ShouldReturnOnlyLeftRows, SetOperations_WhenChained_ShouldApplyLeftToRight |
Last / LastOrDefault over an ordered query (reversed ORDER BY) |
InMemoryTests.Last_ShouldReturnLastOrderedRow, LastOrDefault_ShouldReturnLastOrderedRow |
Buffered subquery source (ctx.From(cmd)) with sync materialization and aggregates |
InMemoryTests.SubquerySource_SyncToList_ShouldReturnRows, SubquerySource_Count_ShouldReturnRowCount, SetOperation_AsSubquery_Count_ShouldReturnRowCount |
SelectMany (flatten, correlated) and GroupJoin (grouped inner rows) |
InMemorySelectManyTests.SelectMany_Correlated_ShouldFlattenPerRow, GroupJoin_ShouldGroupInnerRows |
The in-memory provider shares the same EntityBuilder<T> fluent API as the SQL providers, so the same query
object works against both โ except SelectMany/GroupJoin, which are in-memory only: on a SQL provider
they throw NotSupportedException immediately (see Limitations).
Aggregate null semantics
For the four variability families the in-memory provider matches the SQL providers: Stdev and
Var return null for zero or one contributing non-null value, Stdevp and Varp return null
for zero and 0.0 for one, and two or more values keep the established formula. An undefined result
is null, never a substituted zero, so the same query returns the same shape on both backends.
LOB streaming
The scalar LOB terminals work on the in-memory provider. ToStream/ToStreamAsync return a MemoryStream over the single projected byte[], and ToTextReader/ToTextReaderAsync a StringReader over the single projected string. The projection must be exactly one byte[]/string column. Only the first row is read; an empty result and a first value that is NULL both return Stream.Null/TextReader.Null (the in-memory scalar API does not distinguish them). The returned object is an ordinary BCL MemoryStream/StringReader owned by the caller โ nothing on the context needs disposing. Because the value is already materialised, there is no O(buffer) benefit in memory. The multi-column ToDataReader remains unsupported (NotSupportedException: no DbDataReader). See Streaming large objects.
Unsupported
The provider fails loudly instead of returning wrong results:
Table-valued functions throw
NotSupportedExceptionwith a message mentioning the in-memory provider. A database TVF cannot be evaluated in process.var act = () => ctx .From(() => Tvf.SimpleTvf()) .Select(it => new { it.Id }) .ToList(); // NotSupportedException: "... not supported by the in-memory provider."Distinctover a reference type without value equality throwsNotSupportedExceptionmentioningDISTINCT: SQL compares by value, which the CLR type cannot reproduce without anEquals/GetHashCodeoverride. Project an anonymous type or a value type, or override equality.ctx.From<SimpleEntity>().Distinct().ToList(); // NotSupportedException: "DISTINCT is not supported by the in-memory provider for projection type ..."Raw SQL (
PrepareFromSql) is not supported (NotSupportedException).Aggregates or
GroupByover anIAsyncEnumerablesource throwNotSupportedException; both are computed for bufferedIEnumerablesources.Filtered aggregates (
SqlFunctions.Sql.count(e => filter)and the(value, filter)overloads) and grouped ordering by expression throwNotSupportedException; order groups by column index instead.Set operations over an
IAsyncEnumerablesource throwNotSupportedException; operands are buffered.Last/LastOrDefaultwithoutORDER BYthrowInvalidOperationException: a query without ordering has no defined last row.Aggregates over an async subquery source throw
NotSupportedException; buffered subquery sources are folded.Correlated subqueries (a subquery referencing the outer row, for example
SqlFunctions.Sql.exists(inner.Where(i => i.Id == outer.Id))) are evaluated once per outer row for depth-one correlation: scalar subqueries, aggregate terminals,EXISTSandINwork inSELECT,WHEREandORDER BY(see Subqueries). Correlation depth greater than one, an outer reference inside the inner projection orORDER BY, a correlatedGROUP BY/HAVINGand an async inner source throwNotSupportedException.
Provider differences
| Aspect | In-memory |
|---|---|
| Package | core nextorm |
| Connection | none |
| SQL text | none (expression tree is evaluated in process) |
| Parameter placeholders | not applicable |
| TVF sources | NotSupportedException |
| Raw SQL | NotSupportedException |
LOB streaming (ToStream/ToTextReader) |
supported (MemoryStream/StringReader over the single materialized value; ToDataReader remains NotSupportedException) |
Distinct on non-value-equality types |
NotSupportedException |
| Correlated subqueries | NotSupportedException |
SelectMany / GroupJoin |
supported (these operators are in-memory only) |
See also
- Provider overview
- SQLite
- SQL Server
- PostgreSQL
- Query reuse: cache vs Prepare
- Limitations and out-of-scope features
Source: tests/nextorm.core.tests/InMemory/Queries/InMemoryTests.cs:28,46,55,212,226,244,349,374,
tests/nextorm.core.tests/InMemory/Queries/InMemoryJoinTests.cs:14,47,64,107,145,162,
tests/nextorm.core.tests/InMemory/Queries/InMemorySelectManyTests.cs,
src/nextorm.core/DataContext/InMemory/InMemoryDataContext.cs,
src/nextorm.core/Builders/InMemoryCommandBuilder.cs.