Table of Contents

In-memory provider

InMemoryDataContext runs 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:

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 NotSupportedException with 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."
    
  • Distinct over a reference type without value equality throws NotSupportedException mentioning DISTINCT: SQL compares by value, which the CLR type cannot reproduce without an Equals/GetHashCode override. 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 GroupBy over an IAsyncEnumerable source throw NotSupportedException; both are computed for buffered IEnumerable sources.

  • Filtered aggregates (SqlFunctions.Sql.count(e => filter) and the (value, filter) overloads) and grouped ordering by expression throw NotSupportedException; order groups by column index instead.

  • Set operations over an IAsyncEnumerable source throw NotSupportedException; operands are buffered.

  • Last/LastOrDefault without ORDER BY throw InvalidOperationException: 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, EXISTS and IN work in SELECT, WHERE and ORDER BY (see Subqueries). Correlation depth greater than one, an outer reference inside the inner projection or ORDER BY, a correlated GROUP BY/HAVING and an async inner source throw NotSupportedException.

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


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.