From 9ba5f43312077936857cbbbf2ce93c559ccbd043 Mon Sep 17 00:00:00 2001 From: Koen Date: Tue, 7 Apr 2026 16:56:14 +0000 Subject: [PATCH 1/4] Mongo integration --- .devcontainer/devcontainer.json | 5 +- .github/workflows/ci.yml | 64 +++ Directory.Packages.props | 2 + ExpressiveSharp.slnx | 2 + .../ExpressiveMongoCollection.cs | 56 +++ .../ExpressiveSharp.MongoDB.csproj | 19 + .../ExpressiveQueryableMongoExtensions.cs | 438 ++++++++++++++++++ .../Extensions/MongoExpressiveExtensions.cs | 61 +++ .../IExpressiveMongoQueryable.cs | 17 + .../ExpressiveMongoQueryProvider.cs | 59 +++ .../ExpressiveMongoQueryable.cs | 32 ++ .../MongoExpressiveOptions.cs | 28 ++ ...ssiveSharp.MongoDB.IntegrationTests.csproj | 24 + .../Infrastructure/MongoContainerFixture.cs | 49 ++ .../Infrastructure/MongoTestBase.cs | 108 +++++ .../Tests/AsyncMethodTests.cs | 105 +++++ .../Tests/EmbeddedDocumentTests.cs | 78 ++++ .../Tests/ExpressiveMongoCollectionTests.cs | 61 +++ .../ExpressiveMongoQueryProviderTests.cs | 117 +++++ .../Tests/ExpressiveMongoQueryableTests.cs | 73 +++ .../Tests/PolyfillInterceptorTests.cs | 76 +++ .../Tests/TransformerPipelineTests.cs | 90 ++++ 22 files changed, 1563 insertions(+), 1 deletion(-) create mode 100644 src/ExpressiveSharp.MongoDB/ExpressiveMongoCollection.cs create mode 100644 src/ExpressiveSharp.MongoDB/ExpressiveSharp.MongoDB.csproj create mode 100644 src/ExpressiveSharp.MongoDB/Extensions/ExpressiveQueryableMongoExtensions.cs create mode 100644 src/ExpressiveSharp.MongoDB/Extensions/MongoExpressiveExtensions.cs create mode 100644 src/ExpressiveSharp.MongoDB/IExpressiveMongoQueryable.cs create mode 100644 src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryProvider.cs create mode 100644 src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs create mode 100644 src/ExpressiveSharp.MongoDB/MongoExpressiveOptions.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/ExpressiveSharp.MongoDB.IntegrationTests.csproj create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoTestBase.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/AsyncMethodTests.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/EmbeddedDocumentTests.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoCollectionTests.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryableTests.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs create mode 100644 tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/TransformerPipelineTests.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b17e7ce..a3e7d36 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,10 @@ "image": "mcr.microsoft.com/devcontainers/dotnet:2-10.0-noble", "features": { "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {} + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "dockerDashComposeVersion": "none", + "installDockerBuildx": false + } }, "postCreateCommand": "dotnet restore" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae75840..7febb0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,3 +169,67 @@ jobs: name: Container Test Results (${{ matrix.database }}) path: ./test-results/**/*.trx reporter: dotnet-trx + + mongodb-tests: + name: Container Tests (MongoDB) + runs-on: ubuntu-latest + needs: build-and-test + timeout-minutes: 10 + + env: + CI: true + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-MongoDB-${{ hashFiles('**/*.csproj', 'Directory.Packages.props') }} + restore-keys: | + nuget-${{ runner.os }}-MongoDB- + nuget-${{ runner.os }}- + + - name: Restore + run: >- + dotnet restore + tests/ExpressiveSharp.MongoDB.IntegrationTests/ExpressiveSharp.MongoDB.IntegrationTests.csproj + + - name: Build + run: >- + dotnet build --no-restore -c Release + tests/ExpressiveSharp.MongoDB.IntegrationTests/ExpressiveSharp.MongoDB.IntegrationTests.csproj + + - name: Test + run: >- + dotnet test --no-build -c Release + --project tests/ExpressiveSharp.MongoDB.IntegrationTests + -- + --report-trx --report-trx-filename results.trx + --results-directory ./test-results + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: container-test-results-MongoDB + path: ./test-results/**/*.trx + retention-days: 14 + + - name: Test report + if: always() + uses: dorny/test-reporter@v1 + with: + name: Container Test Results (MongoDB) + path: ./test-results/**/*.trx + reporter: dotnet-trx diff --git a/Directory.Packages.props b/Directory.Packages.props index dc0adfb..29af0cf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,5 +24,7 @@ + + diff --git a/ExpressiveSharp.slnx b/ExpressiveSharp.slnx index 2cc5546..ce84c9b 100644 --- a/ExpressiveSharp.slnx +++ b/ExpressiveSharp.slnx @@ -15,11 +15,13 @@ + + diff --git a/src/ExpressiveSharp.MongoDB/ExpressiveMongoCollection.cs b/src/ExpressiveSharp.MongoDB/ExpressiveMongoCollection.cs new file mode 100644 index 0000000..0a7b8c0 --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/ExpressiveMongoCollection.cs @@ -0,0 +1,56 @@ +using ExpressiveSharp.MongoDB.Extensions; +using ExpressiveSharp.Services; +using MongoDB.Driver; + +namespace ExpressiveSharp.MongoDB; + +/// +/// A wrapper around that provides an +/// for delegate-based LINQ queries +/// with automatic [Expressive] member expansion. +/// +/// +/// Analogous to ExpressiveDbSet<TEntity> in the EF Core integration. +/// CRUD operations delegate directly to the inner collection. +/// +/// +/// +/// var orders = new ExpressiveMongoCollection<Order>(collection); +/// var results = await orders.AsQueryable() +/// .Where(o => o.Customer?.Name == "Alice") +/// .ToListAsync(); +/// +/// +public class ExpressiveMongoCollection +{ + private readonly IMongoCollection _inner; + private readonly ExpressiveOptions _options; + + /// + /// Creates a new wrapping the specified collection. + /// + /// The underlying MongoDB collection. + /// + /// Optional controlling the transformer pipeline. + /// When null, is used. + /// + public ExpressiveMongoCollection(IMongoCollection inner, ExpressiveOptions? options = null) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _options = options ?? MongoExpressiveOptions.CreateDefault(); + } + + /// + /// Gets the underlying for direct access + /// to non-LINQ operations (inserts, updates, deletes, aggregation pipeline, etc.). + /// + public IMongoCollection Inner => _inner; + + /// + /// Returns an that supports delegate-based + /// LINQ with modern C# syntax and automatic [Expressive] expansion. + /// + /// Optional MongoDB aggregation options. + public IExpressiveMongoQueryable AsQueryable(AggregateOptions? aggregateOptions = null) + => _inner.AsExpressive(_options, aggregateOptions); +} diff --git a/src/ExpressiveSharp.MongoDB/ExpressiveSharp.MongoDB.csproj b/src/ExpressiveSharp.MongoDB/ExpressiveSharp.MongoDB.csproj new file mode 100644 index 0000000..51d5e70 --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/ExpressiveSharp.MongoDB.csproj @@ -0,0 +1,19 @@ + + + + MongoDB Driver integration for ExpressiveSharp — automatically expands [Expressive] members in MongoDB LINQ queries + + + + + + + + + + + + + + + diff --git a/src/ExpressiveSharp.MongoDB/Extensions/ExpressiveQueryableMongoExtensions.cs b/src/ExpressiveSharp.MongoDB/Extensions/ExpressiveQueryableMongoExtensions.cs new file mode 100644 index 0000000..84a8b7e --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/Extensions/ExpressiveQueryableMongoExtensions.cs @@ -0,0 +1,438 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Linq.Expressions; +using ExpressiveSharp; +using MongoDB.Driver.Linq; + +// ReSharper disable once CheckNamespace — intentionally in MongoDB.Driver namespace for discoverability +namespace MongoDB.Driver; + +/// +/// Delegate-based async method stubs on for MongoDB +/// async operations. These stubs are intercepted at compile time by the ExpressiveSharp +/// source generator via and forwarded to +/// extension methods with expression tree arguments. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ExpressiveQueryableMongoExtensions +{ + private const string InterceptedMessage = + "This method must be intercepted by the ExpressiveSharp source generator. " + + "Ensure the generator package is installed and the InterceptorsNamespaces MSBuild property is configured."; + + // ── AnyAsync ──────────────────────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AnyAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── CountAsync ────────────────────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task CountAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── LongCountAsync ───────────────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task LongCountAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── FirstAsync / FirstOrDefaultAsync ──────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task FirstAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task FirstOrDefaultAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── SingleAsync / SingleOrDefaultAsync ────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SingleAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SingleOrDefaultAsync( + this IRewritableQueryable source, + Func predicate, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── MinAsync / MaxAsync ───────────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task MinAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task MaxAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── SumAsync (int) ────────────────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── SumAsync (nullable int) ───────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task SumAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── AverageAsync (double) ─────────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── AverageAsync (nullable) ───────────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task AverageAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── StandardDeviationPopulationAsync ───────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationPopulationAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + // ── StandardDeviationSampleAsync ───────────────────────────────────── + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); + + [EditorBrowsable(EditorBrowsableState.Never)] + [PolyfillTarget(typeof(MongoQueryable))] + public static Task StandardDeviationSampleAsync( + this IRewritableQueryable source, + Func selector, + CancellationToken cancellationToken = default) + => throw new UnreachableException(InterceptedMessage); +} diff --git a/src/ExpressiveSharp.MongoDB/Extensions/MongoExpressiveExtensions.cs b/src/ExpressiveSharp.MongoDB/Extensions/MongoExpressiveExtensions.cs new file mode 100644 index 0000000..72155fd --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/Extensions/MongoExpressiveExtensions.cs @@ -0,0 +1,61 @@ +using ExpressiveSharp.MongoDB.Infrastructure; +using ExpressiveSharp.Services; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.Extensions; + +/// +/// Entry-point extension methods for wrapping MongoDB queryables and collections +/// with ExpressiveSharp expression expansion. +/// +public static class MongoExpressiveExtensions +{ + /// + /// Wraps an backed by MongoDB's LINQ provider in an + /// that automatically expands + /// [Expressive] members before query execution. + /// + /// The MongoDB queryable source (typically from collection.AsQueryable()). + /// + /// Optional controlling the transformer pipeline. + /// When null, is used. + /// + public static IExpressiveMongoQueryable AsExpressive( + this IQueryable source, + ExpressiveOptions? options = null) + { + var mongoProvider = source.Provider as IMongoQueryProvider + ?? throw new ArgumentException( + "The source queryable's Provider must implement IMongoQueryProvider. " + + "Use collection.AsQueryable() to obtain a MongoDB-backed queryable.", + nameof(source)); + + var effectiveOptions = options ?? MongoExpressiveOptions.CreateDefault(); + var provider = new ExpressiveMongoQueryProvider(mongoProvider, effectiveOptions); + return new ExpressiveMongoQueryable(source, provider); + } + + /// + /// Creates an directly from an + /// , combining AsQueryable() + /// with ExpressiveSharp expression expansion. + /// + /// The MongoDB collection. + /// + /// Optional controlling the transformer pipeline. + /// When null, is used. + /// + /// Optional MongoDB aggregation options. + public static IExpressiveMongoQueryable AsExpressive( + this IMongoCollection collection, + ExpressiveOptions? options = null, + AggregateOptions? aggregateOptions = null) + { + var queryable = aggregateOptions is not null + ? collection.AsQueryable(aggregateOptions) + : collection.AsQueryable(); + + return queryable.AsExpressive(options); + } +} diff --git a/src/ExpressiveSharp.MongoDB/IExpressiveMongoQueryable.cs b/src/ExpressiveSharp.MongoDB/IExpressiveMongoQueryable.cs new file mode 100644 index 0000000..1b45172 --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/IExpressiveMongoQueryable.cs @@ -0,0 +1,17 @@ +namespace ExpressiveSharp.MongoDB; + +/// +/// Represents a MongoDB queryable with expression-rewrite support. Extends +/// to enable delegate-based LINQ methods +/// with modern C# syntax (e.g., ?., switch expressions, pattern matching) +/// when querying MongoDB collections. +/// +/// +/// In MongoDB.Driver v3, the driver's LINQ provider works through standard +/// with an . +/// This interface marks a queryable whose provider is an +/// that automatically expands [Expressive] members before MongoDB translates the query. +/// +public interface IExpressiveMongoQueryable : IRewritableQueryable +{ +} diff --git a/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryProvider.cs b/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryProvider.cs new file mode 100644 index 0000000..5f031c7 --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryProvider.cs @@ -0,0 +1,59 @@ +using System.Linq.Expressions; +using ExpressiveSharp.Extensions; +using ExpressiveSharp.Services; +using MongoDB.Bson; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.Infrastructure; + +/// +/// Decorates MongoDB's to automatically expand +/// member references before query execution. +/// +/// +/// returns an +/// wrapper so that chained operations continue to use this provider. +/// and call +/// +/// on the expression before delegating to the inner provider. +/// +internal sealed class ExpressiveMongoQueryProvider : IMongoQueryProvider +{ + private readonly IMongoQueryProvider _inner; + private readonly ExpressiveOptions _options; + + public ExpressiveMongoQueryProvider(IMongoQueryProvider inner, ExpressiveOptions options) + { + _inner = inner; + _options = options; + } + + public BsonDocument[] LoggedStages => _inner.LoggedStages; + + public IQueryable CreateQuery(Expression expression) + { + var inner = _inner.CreateQuery(expression); + // Wrap in our queryable to maintain provider chain + var elementType = inner.ElementType; + var wrapperType = typeof(ExpressiveMongoQueryable<>).MakeGenericType(elementType); + return (IQueryable)Activator.CreateInstance(wrapperType, inner, this)!; + } + + public IQueryable CreateQuery(Expression expression) + { + var inner = _inner.CreateQuery(expression); + return new ExpressiveMongoQueryable(inner, this); + } + + public object? Execute(Expression expression) + => _inner.Execute(Expand(expression)); + + public TResult Execute(Expression expression) + => _inner.Execute(Expand(expression)); + + public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) + => _inner.ExecuteAsync(Expand(expression), cancellationToken); + + private Expression Expand(Expression expression) + => expression.ExpandExpressives(_options); +} diff --git a/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs b/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs new file mode 100644 index 0000000..27fa8b6 --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs @@ -0,0 +1,32 @@ +using System.Collections; +using System.Linq.Expressions; + +namespace ExpressiveSharp.MongoDB.Infrastructure; + +/// +/// Internal wrapper that adapts an backed by MongoDB's LINQ provider +/// to , enabling delegate-based LINQ overloads +/// with modern C# syntax via source generator interception. +/// +/// +/// Also implements so that ThenBy/ThenByDescending +/// interceptors can cast the wrapper without a runtime exception. +/// +internal sealed class ExpressiveMongoQueryable : IExpressiveMongoQueryable, IOrderedQueryable +{ + private readonly IQueryable _source; + private readonly ExpressiveMongoQueryProvider _provider; + + public ExpressiveMongoQueryable(IQueryable source, ExpressiveMongoQueryProvider provider) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + public Type ElementType => _source.ElementType; + public Expression Expression => _source.Expression; + public IQueryProvider Provider => _provider; + + public IEnumerator GetEnumerator() => _source.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_source).GetEnumerator(); +} diff --git a/src/ExpressiveSharp.MongoDB/MongoExpressiveOptions.cs b/src/ExpressiveSharp.MongoDB/MongoExpressiveOptions.cs new file mode 100644 index 0000000..825d677 --- /dev/null +++ b/src/ExpressiveSharp.MongoDB/MongoExpressiveOptions.cs @@ -0,0 +1,28 @@ +using ExpressiveSharp.Services; +using ExpressiveSharp.Transformers; + +namespace ExpressiveSharp.MongoDB; + +/// +/// Factory for creating an pre-configured with +/// transformers suitable for MongoDB's LINQ provider. +/// +public static class MongoExpressiveOptions +{ + /// + /// Creates an with the default transformer pipeline + /// for MongoDB queries. Matches the transformers used by the EF Core integration. + /// + public static ExpressiveOptions CreateDefault() + { + var options = new ExpressiveOptions(); + options.AddTransformers( + new ReplaceThrowWithDefault(), + new ConvertLoopsToLinq(), + new RemoveNullConditionalPatterns(), + new FlattenTupleComparisons(), + new FlattenConcatArrayCalls(), + new FlattenBlockExpressions()); + return options; + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/ExpressiveSharp.MongoDB.IntegrationTests.csproj b/tests/ExpressiveSharp.MongoDB.IntegrationTests/ExpressiveSharp.MongoDB.IntegrationTests.csproj new file mode 100644 index 0000000..b8266d9 --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/ExpressiveSharp.MongoDB.IntegrationTests.csproj @@ -0,0 +1,24 @@ + + + + false + true + + + $(NoWarn);NU1903;NU1902;NU1901;NU1904 + + + + + + + + + + + + + + diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs new file mode 100644 index 0000000..3cd0469 --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs @@ -0,0 +1,49 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Testcontainers.MongoDb; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; + +[TestClass] +public static class MongoContainerFixture +{ + public static bool IsDockerAvailable { get; private set; } + public static string? ConnectionString { get; private set; } + + private static MongoDbContainer? _container; + + [AssemblyInitialize] + public static async Task InitializeAsync(TestContext _) + { + if (!DetectDocker()) + { + IsDockerAvailable = false; + return; + } + + IsDockerAvailable = true; + _container = new MongoDbBuilder().Build(); + await _container.StartAsync(); + ConnectionString = _container.GetConnectionString(); + } + + [AssemblyCleanup] + public static async Task CleanupAsync() + { + if (_container is not null) + await _container.DisposeAsync(); + } + + private static bool DetectDocker() + { + try + { + using var client = new Docker.DotNet.DockerClientConfiguration().CreateClient(); + client.System.PingAsync().GetAwaiter().GetResult(); + return true; + } + catch + { + return false; + } + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoTestBase.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoTestBase.cs new file mode 100644 index 0000000..fcaeccc --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoTestBase.cs @@ -0,0 +1,108 @@ +using ExpressiveSharp.IntegrationTests.Scenarios.Store; +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.Extensions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; + +/// +/// Base class for all MongoDB integration tests. Creates a unique database per test, +/// seeds embedded Order documents, and drops the database on cleanup. +/// +public abstract class MongoTestBase +{ + protected IMongoDatabase Database { get; private set; } = null!; + protected IMongoCollection Orders { get; private set; } = null!; + protected IExpressiveMongoQueryable Query { get; private set; } = null!; + + private MongoClient? _client; + private string? _dbName; + + [TestInitialize] + public async Task InitMongo() + { + if (!MongoContainerFixture.IsDockerAvailable) + Assert.Inconclusive("Docker not available"); + + _client = new MongoClient(MongoContainerFixture.ConnectionString); + _dbName = $"test_{Guid.NewGuid():N}"; + Database = _client.GetDatabase(_dbName); + Orders = Database.GetCollection("orders"); + Query = Orders.AsExpressive(); + + await SeedDataAsync(); + } + + [TestCleanup] + public async Task CleanupMongo() + { + if (_client is not null && _dbName is not null) + await _client.DropDatabaseAsync(_dbName); + } + + /// + /// Seeds the database with embedded Order documents. + /// Customer and Address are embedded within each Order (document model). + /// + protected virtual async Task SeedDataAsync() + { + var addressLookup = SeedData.Addresses.ToDictionary(a => a.Id); + var customerLookup = SeedData.Customers.ToDictionary(c => c.Id); + var lineItemsByOrder = SeedData.LineItems + .GroupBy(li => li.OrderId) + .ToDictionary(g => g.Key, g => g.ToList()); + + var orders = new List(); + + foreach (var order in SeedData.Orders) + { + var mongoOrder = new Order + { + Id = order.Id, + Tag = order.Tag, + Price = order.Price, + Quantity = order.Quantity, + Status = order.Status, + CustomerId = order.CustomerId, + }; + + if (order.CustomerId is { } customerId && customerLookup.TryGetValue(customerId, out var customer)) + { + mongoOrder.Customer = new Customer + { + Id = customer.Id, + Name = customer.Name, + Email = customer.Email, + AddressId = customer.AddressId, + }; + + if (customer.AddressId is { } addressId && addressLookup.TryGetValue(addressId, out var address)) + { + mongoOrder.Customer.Address = new Address + { + Id = address.Id, + City = address.City, + Country = address.Country, + }; + } + } + + if (lineItemsByOrder.TryGetValue(order.Id, out var items)) + { + mongoOrder.Items = items.Select(li => new LineItem + { + Id = li.Id, + OrderId = li.OrderId, + ProductName = li.ProductName, + UnitPrice = li.UnitPrice, + Quantity = li.Quantity, + }).ToList(); + } + + orders.Add(mongoOrder); + } + + await Orders.InsertManyAsync(orders); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/AsyncMethodTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/AsyncMethodTests.cs new file mode 100644 index 0000000..e46cdfe --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/AsyncMethodTests.cs @@ -0,0 +1,105 @@ +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies MongoDB-specific async methods work through delegate-based stubs +/// with [PolyfillTarget(typeof(MongoQueryable))]. +/// +[TestClass] +public class AsyncMethodTests : MongoTestBase +{ + [TestMethod] + public async Task AnyAsync_WithDelegatePredicate_Executes() + { + var result = await MongoQueryable.AnyAsync( + Query.Where(o => o.Price > 100)); + Assert.IsTrue(result); + } + + [TestMethod] + public async Task CountAsync_WithDelegatePredicate_Executes() + { + var result = await MongoQueryable.CountAsync( + Query.Where(o => o.Status == OrderStatus.Pending)); + Assert.AreEqual(2, result); + } + + [TestMethod] + public async Task FirstAsync_WithDelegatePredicate_Executes() + { + var result = await MongoQueryable.FirstAsync( + Query.Where(o => o.Id == 1)); + Assert.AreEqual(1, result.Id); + Assert.AreEqual(120.0, result.Price); + } + + [TestMethod] + public async Task FirstOrDefaultAsync_WithDelegatePredicate_ReturnsNull() + { + var result = await MongoQueryable.FirstOrDefaultAsync( + Query.Where(o => o.Id == 999)); + Assert.IsNull(result); + } + + [TestMethod] + public async Task SingleAsync_WithDelegatePredicate_Executes() + { + var result = await MongoQueryable.SingleAsync( + Query.Where(o => o.Id == 2)); + Assert.AreEqual(2, result.Id); + } + + [TestMethod] + public async Task SingleOrDefaultAsync_WithDelegatePredicate_ReturnsNull() + { + var result = await MongoQueryable.SingleOrDefaultAsync( + Query.Where(o => o.Id == 999)); + Assert.IsNull(result); + } + + [TestMethod] + public async Task SumAsync_WithSelector_Executes() + { + var result = await MongoQueryable.SumAsync( + Query.Select(o => (int)o.Price)); + // 120 + 75 + 10 + 50 = 255 + Assert.AreEqual(255, result); + } + + [TestMethod] + public async Task MinAsync_WithSelector_Executes() + { + var result = await MongoQueryable.MinAsync( + Query.Select(o => o.Price)); + Assert.AreEqual(10.0, result); + } + + [TestMethod] + public async Task MaxAsync_WithSelector_Executes() + { + var result = await MongoQueryable.MaxAsync( + Query.Select(o => o.Price)); + Assert.AreEqual(120.0, result); + } + + [TestMethod] + public async Task AverageAsync_WithSelector_Executes() + { + var result = await MongoQueryable.AverageAsync( + Query.Select(o => o.Price)); + // (120 + 75 + 10 + 50) / 4 = 63.75 + Assert.AreEqual(63.75, result); + } + + [TestMethod] + public async Task ToListAsync_Works() + { + var results = await MongoQueryable.ToListAsync(Query); + Assert.AreEqual(4, results.Count); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/EmbeddedDocumentTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/EmbeddedDocumentTests.cs new file mode 100644 index 0000000..102464e --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/EmbeddedDocumentTests.cs @@ -0,0 +1,78 @@ +using System.Linq.Expressions; +using ExpressiveSharp.Extensions; +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies expressive features work with MongoDB's embedded document model. +/// +[TestClass] +public class EmbeddedDocumentTests : MongoTestBase +{ + [TestMethod] + public async Task ExpressiveMember_OnEmbeddedDocument_Expands() + { + // CustomerName is [Expressive] => Customer?.Name + // Customer is an embedded document in MongoDB + Expression> expr = o => o.CustomerName; + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Query + .Where(o => o.Customer != null) + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + Assert.AreEqual(3, results.Count); + Assert.AreEqual("Alice", results[0]); + Assert.AreEqual("Bob", results[1]); + Assert.IsNull(results[2]); // Customer exists but Name is null + } + + [TestMethod] + public async Task NullConditional_ThroughEmbeddedDocument_Works() + { + // CustomerCountry is [Expressive] => Customer?.Address?.Country + // Two levels of embedded document navigation + Expression> expr = o => o.CustomerCountry; + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Query + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + Assert.AreEqual("US", results[0]); // Order 1: Alice → New York, US + Assert.AreEqual("UK", results[1]); // Order 2: Bob → London, UK + Assert.IsNull(results[2]); // Order 3: no customer + Assert.IsNull(results[3]); // Order 4: customer has no address + } + + [TestMethod] + public async Task Query_EmbeddedCustomer_ByName() + { + var results = await Query + .Where(o => o.Customer != null && o.Customer.Name == "Alice") + .Select(o => o.Id) + .ToListAsync(); + + Assert.AreEqual(1, results.Count); + Assert.AreEqual(1, results[0]); + } + + [TestMethod] + public async Task Query_EmbeddedAddress_ByCity() + { + var results = await Query + .Where(o => o.Customer != null && o.Customer.Address != null && o.Customer.Address.City == "London") + .Select(o => o.Id) + .ToListAsync(); + + Assert.AreEqual(1, results.Count); + Assert.AreEqual(2, results[0]); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoCollectionTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoCollectionTests.cs new file mode 100644 index 0000000..06fe10a --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoCollectionTests.cs @@ -0,0 +1,61 @@ +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies the high-level wrapper +/// correctly provides queryable access and delegates CRUD operations. +/// +[TestClass] +public class ExpressiveMongoCollectionTests : MongoTestBase +{ + [TestMethod] + public void AsQueryable_ReturnsExpressiveMongoQueryable() + { + var wrapper = new ExpressiveMongoCollection(Orders); + var queryable = wrapper.AsQueryable(); + Assert.IsInstanceOfType>(queryable); + } + + [TestMethod] + public async Task CrudOperations_DelegateToInner() + { + var wrapper = new ExpressiveMongoCollection(Orders); + + // Insert via inner + var newOrder = new Order { Id = 100, Tag = "TEST", Price = 99.0, Quantity = 1, Status = OrderStatus.Approved }; + await wrapper.Inner.InsertOneAsync(newOrder); + + // Query via wrapper + var results = await MongoQueryable.ToListAsync( + wrapper.AsQueryable().Where(o => o.Id == 100)); + Assert.AreEqual(1, results.Count); + Assert.AreEqual("TEST", results[0].Tag); + + // Delete via inner + await wrapper.Inner.DeleteOneAsync(Builders.Filter.Eq(o => o.Id, 100)); + var count = await MongoQueryable.CountAsync( + wrapper.AsQueryable().Where(o => o.Id == 100)); + Assert.AreEqual(0, count); + } + + [TestMethod] + public async Task AsQueryable_WithExpressiveExpansion_Works() + { + var wrapper = new ExpressiveMongoCollection(Orders); + + // End-to-end: collection → queryable → Where(computed property) → ToListAsync + var results = await MongoQueryable.ToListAsync( + wrapper.AsQueryable() + .Where(o => o.Price > 50) + .OrderBy(o => o.Id)); + + Assert.AreEqual(2, results.Count); + Assert.AreEqual(1, results[0].Id); + Assert.AreEqual(2, results[1].Id); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs new file mode 100644 index 0000000..3dd18f0 --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs @@ -0,0 +1,117 @@ +using System.Linq.Expressions; +using ExpressiveSharp.Extensions; +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies that correctly +/// expands [Expressive] members before MongoDB's LINQ provider processes the query. +/// +[TestClass] +public class ExpressiveMongoQueryProviderTests : MongoTestBase +{ + [TestMethod] + public async Task Select_ExpressiveMember_ExpandsBeforeMongoTranslation() + { + // Total is [Expressive] => Price * Quantity + // Provider should expand before MongoDB sees it + Expression> totalExpr = o => o.Total; + var expanded = (Expression>)totalExpr.ExpandExpressives(); + + var results = await Orders.AsQueryable() + .AsExpressive() + .Select(o => o.Total) + .ToListAsync(); + + // Order 1: 120*2=240, Order 2: 75*20=1500, Order 3: 10*3=30, Order 4: 50*5=250 + CollectionAssert.AreEquivalent(new[] { 240.0, 1500.0, 30.0, 250.0 }, results); + } + + [TestMethod] + public async Task Where_ExpressiveMember_ExpandsInPredicate() + { + Expression> predicate = o => o.Total > 200; + var expanded = (Expression>)predicate.ExpandExpressives(); + + var results = await Orders.AsQueryable() + .AsExpressive() + .Where(expanded) + .OrderBy(o => o.Id) + .Select(o => o.Id) + .ToListAsync(); + + // Totals: 240, 1500, 30, 250 → >200 keeps orders 1, 2, 4 + CollectionAssert.AreEqual(new[] { 1, 2, 4 }, results); + } + + [TestMethod] + public async Task OrderBy_ExpressiveMember_ExpandsInSort() + { + Expression> totalExpr = o => o.Total; + var expanded = (Expression>)totalExpr.ExpandExpressives(); + + var results = await Orders.AsQueryable() + .AsExpressive() + .OrderBy(expanded) + .Select(o => o.Id) + .ToListAsync(); + + // Totals: 30(3), 240(1), 250(4), 1500(2) + CollectionAssert.AreEqual(new[] { 3, 1, 4, 2 }, results); + } + + [TestMethod] + public async Task ExpressiveFor_StaticMethod_ExpandsCorrectly() + { + Expression> expr = o => PricingUtils.Clamp(o.Price, 20.0, 100.0); + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Orders.AsQueryable() + .AsExpressive() + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + // Prices: 120→100, 75→75, 10→20, 50→50 + CollectionAssert.AreEqual(new[] { 100.0, 75.0, 20.0, 50.0 }, results); + } + + [TestMethod] + public async Task NestedExpressive_ExpandsRecursively() + { + // PricingUtils.Clamp is [ExpressiveFor], Total is [Expressive] + Expression> expr = o => PricingUtils.Clamp(o.Total, 0.0, 200.0); + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Orders.AsQueryable() + .AsExpressive() + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + // Totals: 240→200, 1500→200, 30→30, 250→200 + CollectionAssert.AreEqual(new[] { 200.0, 200.0, 30.0, 200.0 }, results); + } + + [TestMethod] + public async Task CapturedVariable_WithExpressive_BothResolve() + { + var minTotal = 200.0; + Expression> expr = o => o.Total > minTotal; + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Orders.AsQueryable() + .AsExpressive() + .Where(expanded) + .OrderBy(o => o.Id) + .Select(o => o.Id) + .ToListAsync(); + + CollectionAssert.AreEqual(new[] { 1, 2, 4 }, results); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryableTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryableTests.cs new file mode 100644 index 0000000..486b1a5 --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryableTests.cs @@ -0,0 +1,73 @@ +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.Extensions; +using ExpressiveSharp.MongoDB.Infrastructure; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies that the wrapper correctly +/// implements the expected interfaces and preserves MongoDB capabilities. +/// +[TestClass] +public class ExpressiveMongoQueryableTests : MongoTestBase +{ + [TestMethod] + public void AsExpressive_ReturnsIExpressiveMongoQueryable() + { + var result = Orders.AsExpressive(); + Assert.IsInstanceOfType>(result); + } + + [TestMethod] + public void AsExpressive_FromCollection_CreatesQueryable() + { + var result = Orders.AsExpressive(); + Assert.IsNotNull(result); + Assert.IsInstanceOfType>(result); + } + + [TestMethod] + public void Provider_IsExpressiveMongoQueryProvider() + { + var result = Orders.AsExpressive(); + Assert.IsInstanceOfType(result.Provider); + } + + [TestMethod] + public void Expression_MatchesUnderlyingQueryable() + { + var baseline = Orders.AsQueryable(); + var expressive = Orders.AsExpressive(); + + // Both should have the same root expression (ConstantExpression pointing to the collection) + Assert.AreEqual(baseline.Expression.NodeType, expressive.Expression.NodeType); + } + + [TestMethod] + public async Task ChainedOperations_MaintainWrapperType() + { + // After Where/Select, the result should still go through our provider + var filtered = Query.Where(o => o.Price > 50); + Assert.IsInstanceOfType(filtered.Provider); + + var results = await MongoQueryable.ToListAsync(filtered); + Assert.IsTrue(results.Count > 0); + } + + [TestMethod] + public async Task ToCursorAsync_WorksThroughWrapper() + { + // MongoDB-specific: ToCursorAsync should work through the wrapper + using var cursor = await MongoQueryable.ToCursorAsync(Query); + var results = new List(); + while (await cursor.MoveNextAsync()) + { + results.AddRange(cursor.Current); + } + Assert.AreEqual(4, results.Count); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs new file mode 100644 index 0000000..f523e26 --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs @@ -0,0 +1,76 @@ +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies that delegate-based lambdas (modern C# syntax) are rewritten to +/// expression trees by the source generator and correctly forwarded through +/// the MongoDB provider. +/// +[TestClass] +public class PolyfillInterceptorTests : MongoTestBase +{ + [TestMethod] + public async Task Where_DelegateLambda_InterceptedAndTranslated() + { + // Delegate lambda with null-conditional — should be intercepted by source generator + var results = await Query + .Where(o => o.Price > 50) + .Select(o => o.Id) + .ToListAsync(); + + // Orders with Price > 50: 1 (120), 2 (75), 4 (50 is not > 50) + CollectionAssert.AreEquivalent(new[] { 1, 2 }, results); + } + + [TestMethod] + public async Task Select_DelegateLambda_InterceptedAndTranslated() + { + var results = await Query + .Select(o => o.Price * 2) + .ToListAsync(); + + CollectionAssert.AreEquivalent(new[] { 240.0, 150.0, 20.0, 100.0 }, results); + } + + [TestMethod] + public async Task OrderBy_DelegateLambda_InterceptedAndTranslated() + { + var results = await Query + .OrderBy(o => o.Price) + .Select(o => o.Id) + .ToListAsync(); + + // Prices: 10(3), 50(4), 75(2), 120(1) + CollectionAssert.AreEqual(new[] { 3, 4, 2, 1 }, results); + } + + [TestMethod] + public async Task NullConditional_InDelegateLambda_TranslatesToMongo() + { + // This uses ?. which requires source generator interception + var results = await Query + .Where(o => o.Tag != null) + .Select(o => o.Tag) + .ToListAsync(); + + Assert.AreEqual(3, results.Count); + CollectionAssert.AreEquivalent(new[] { "RUSH", "STD", "SPECIAL" }, results); + } + + [TestMethod] + public async Task Compound_Where_Select_OrderBy_Works() + { + var results = await Query + .Where(o => o.Status == OrderStatus.Pending) + .OrderBy(o => o.Price) + .Select(o => o.Id) + .ToListAsync(); + + // Pending orders: 2 (75), 4 (50) → sorted by price: 4, 2 + CollectionAssert.AreEqual(new[] { 4, 2 }, results); + } +} diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/TransformerPipelineTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/TransformerPipelineTests.cs new file mode 100644 index 0000000..4f6ca35 --- /dev/null +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/TransformerPipelineTests.cs @@ -0,0 +1,90 @@ +using System.Linq.Expressions; +using ExpressiveSharp.Extensions; +using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; +using ExpressiveSharp.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Driver.Linq; + +namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; + +/// +/// Verifies the default transformer pipeline produces expressions that +/// MongoDB's LINQ provider can translate. +/// +[TestClass] +public class TransformerPipelineTests : MongoTestBase +{ + [TestMethod] + public async Task NullConditionalPatterns_FlattenedForMongo() + { + // CustomerName is [Expressive] => Customer?.Name + // RemoveNullConditionalPatterns should flatten the null-conditional pattern + Expression> expr = o => o.CustomerName; + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Query + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + Assert.AreEqual("Alice", results[0]); + Assert.AreEqual("Bob", results[1]); + Assert.IsNull(results[2]); // Order 3: no customer + Assert.IsNull(results[3]); // Order 4: customer with null name + } + + [TestMethod] + public async Task BlockExpressions_Flattened() + { + // GetCategory is [Expressive(AllowBlockBody = true)] with if/else + Expression> expr = o => o.GetCategory(); + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Query + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + // Order 1: Qty*10=20 → Regular, Order 2: Qty*10=200 → Bulk + // Order 3: Qty*10=30 → Regular, Order 4: Qty*10=50 → Regular + Assert.AreEqual("Regular", results[0]); + Assert.AreEqual("Bulk", results[1]); + Assert.AreEqual("Regular", results[2]); + Assert.AreEqual("Regular", results[3]); + } + + [TestMethod] + public async Task ThrowExpressions_ReplacedWithDefault() + { + // SafeTag => Tag ?? throw ... — ReplaceThrowWithDefault should replace throw with default + Expression> expr = o => o.SafeTag; + var expanded = (Expression>)expr.ExpandExpressives(); + + var results = await Query + .OrderBy(o => o.Id) + .Select(expanded) + .ToListAsync(); + + Assert.AreEqual("RUSH", results[0]); + Assert.AreEqual("STD", results[1]); + Assert.IsNull(results[2]); // Tag is null → throw replaced with default(string) = null + Assert.AreEqual("SPECIAL", results[3]); + } + + [TestMethod] + public async Task CustomOptions_OverrideDefaults() + { + // Use custom options with no transformers — raw expansion only + var customOptions = new ExpressiveOptions(); + var customQuery = MongoDB.Extensions.MongoExpressiveExtensions.AsExpressive( + Orders, customOptions); + + // Simple query should still work (no transformers needed for basic arithmetic) + var results = await customQuery + .Select(o => o.Price * 2) + .ToListAsync(); + + CollectionAssert.AreEquivalent(new[] { 240.0, 150.0, 20.0, 100.0 }, results); + } +} From c613f6c6c3e609083285532261e14d54495a877a Mon Sep 17 00:00:00 2001 From: Koen Date: Tue, 7 Apr 2026 18:14:06 +0100 Subject: [PATCH 2/4] Update tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Tests/PolyfillInterceptorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs index f523e26..4936953 100644 --- a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs @@ -53,7 +53,7 @@ public async Task NullConditional_InDelegateLambda_TranslatesToMongo() { // This uses ?. which requires source generator interception var results = await Query - .Where(o => o.Tag != null) + .Where(o => o.Tag?.Length > 0) .Select(o => o.Tag) .ToListAsync(); From 5a0427f84e6e2abecc241518f420fc46197b09cb Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 8 Apr 2026 00:20:08 +0000 Subject: [PATCH 3/4] updated tests --- .../Tests/PolyfillInterceptorTests.cs | 173 +++++++++++++++--- 1 file changed, 151 insertions(+), 22 deletions(-) diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs index 4936953..d9653fa 100644 --- a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/PolyfillInterceptorTests.cs @@ -1,3 +1,4 @@ +using ExpressiveSharp.Extensions; using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -6,66 +7,194 @@ namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; /// -/// Verifies that delegate-based lambdas (modern C# syntax) are rewritten to +/// Verifies that delegate-based lambdas with modern C# syntax are rewritten to /// expression trees by the source generator and correctly forwarded through -/// the MongoDB provider. +/// the MongoDB provider. These features would normally fail in raw expression trees. /// [TestClass] public class PolyfillInterceptorTests : MongoTestBase { + // ── Null-conditional (?.) ─────────────────────────────────────────── + [TestMethod] - public async Task Where_DelegateLambda_InterceptedAndTranslated() + public async Task NullConditional_PropertyAccess() { - // Delegate lambda with null-conditional — should be intercepted by source generator var results = await Query - .Where(o => o.Price > 50) + .OrderBy(o => o.Id) + .Select(o => o.Customer?.Name) + .ToListAsync(); + + Assert.AreEqual("Alice", results[0]); + Assert.AreEqual("Bob", results[1]); + Assert.IsNull(results[2]); // Order 3: no customer + Assert.IsNull(results[3]); // Order 4: customer with null name + } + + [TestMethod] + public async Task NullConditional_ChainedAccess() + { + var results = await Query + .OrderBy(o => o.Id) + .Select(o => o.Customer?.Address?.Country) + .ToListAsync(); + + Assert.AreEqual("US", results[0]); + Assert.AreEqual("UK", results[1]); + Assert.IsNull(results[2]); // No customer + Assert.IsNull(results[3]); // Customer has no address + } + + [TestMethod] + public async Task NullConditional_MethodCall() + { + var results = await Query + .OrderBy(o => o.Id) + .Select(o => o.Tag?.ToUpper()) + .ToListAsync(); + + Assert.AreEqual("RUSH", results[0]); + Assert.AreEqual("STD", results[1]); + Assert.IsNull(results[2]); + Assert.AreEqual("SPECIAL", results[3]); + } + + [TestMethod] + public async Task NullConditional_InWherePredicate() + { + var results = await Query + .Where(o => o.Customer?.Address?.Country == "US") .Select(o => o.Id) .ToListAsync(); - // Orders with Price > 50: 1 (120), 2 (75), 4 (50 is not > 50) - CollectionAssert.AreEquivalent(new[] { 1, 2 }, results); + CollectionAssert.AreEquivalent(new[] { 1 }, results); } + // ── Null-coalescing (??) ──────────────────────────────────────────── + [TestMethod] - public async Task Select_DelegateLambda_InterceptedAndTranslated() + public async Task NullCoalescing_WithFallback() { var results = await Query - .Select(o => o.Price * 2) + .OrderBy(o => o.Id) + .Select(o => o.Tag ?? "NONE") .ToListAsync(); - CollectionAssert.AreEquivalent(new[] { 240.0, 150.0, 20.0, 100.0 }, results); + CollectionAssert.AreEqual(new[] { "RUSH", "STD", "NONE", "SPECIAL" }, results); } [TestMethod] - public async Task OrderBy_DelegateLambda_InterceptedAndTranslated() + public async Task NullCoalescing_ChainedWithNullConditional() { var results = await Query - .OrderBy(o => o.Price) + .OrderBy(o => o.Id) + .Select(o => o.Customer?.Name ?? "Unknown") + .ToListAsync(); + + CollectionAssert.AreEqual(new[] { "Alice", "Bob", "Unknown", "Unknown" }, results); + } + + // ── Switch expressions ────────────────────────────────────────────── + + [TestMethod] + public async Task SwitchExpression_OnEnum() + { + var results = await Query + .OrderBy(o => o.Id) + .Select(o => o.Status switch + { + OrderStatus.Approved => "OK", + OrderStatus.Pending => "WAIT", + _ => "NO", + }) + .ToListAsync(); + + // Order 1: Approved, Order 2: Pending, Order 3: Rejected, Order 4: Pending + CollectionAssert.AreEqual(new[] { "OK", "WAIT", "NO", "WAIT" }, results); + } + + [TestMethod] + public async Task SwitchExpression_OnNumericRange() + { + var results = await Query + .OrderBy(o => o.Id) + .Select(o => o.Price switch + { + >= 100 => "Premium", + >= 50 => "Standard", + _ => "Budget", + }) + .ToListAsync(); + + // Prices: 120, 75, 10, 50 + CollectionAssert.AreEqual(new[] { "Premium", "Standard", "Budget", "Standard" }, results); + } + + // ── Pattern matching ──────────────────────────────────────────────── + + [TestMethod] + public async Task PatternMatching_IsNotNull() + { + var results = await Query + .Where(o => o.Customer is not null) + .OrderBy(o => o.Id) + .Select(o => o.Id) + .ToListAsync(); + + CollectionAssert.AreEqual(new[] { 1, 2, 4 }, results); + } + + [TestMethod] + public async Task PatternMatching_IsNull() + { + var results = await Query + .Where(o => o.Customer is null) .Select(o => o.Id) .ToListAsync(); - // Prices: 10(3), 50(4), 75(2), 120(1) - CollectionAssert.AreEqual(new[] { 3, 4, 2, 1 }, results); + CollectionAssert.AreEquivalent(new[] { 3 }, results); + } + + // ── Conditional (ternary) ─────────────────────────────────────────── + + [TestMethod] + public async Task Ternary_InSelect() + { + var results = await Query + .OrderBy(o => o.Id) + .Select(o => o.Price > 50 ? "Expensive" : "Affordable") + .ToListAsync(); + + // Prices: 120(>50), 75(>50), 10(≤50), 50(≤50) + CollectionAssert.AreEqual( + new[] { "Expensive", "Expensive", "Affordable", "Affordable" }, results); } + // ── Compound queries ──────────────────────────────────────────────── + [TestMethod] - public async Task NullConditional_InDelegateLambda_TranslatesToMongo() + public async Task Compound_NullConditional_WithCoalescing_InWhere() { - // This uses ?. which requires source generator interception + // Filter orders where the customer's country is unknown (null) then default var results = await Query - .Where(o => o.Tag?.Length > 0) - .Select(o => o.Tag) + .Where(o => (o.Customer?.Address?.Country ?? "Unknown") == "Unknown") + .OrderBy(o => o.Id) + .Select(o => o.Id) .ToListAsync(); - Assert.AreEqual(3, results.Count); - CollectionAssert.AreEquivalent(new[] { "RUSH", "STD", "SPECIAL" }, results); + // Order 3: no customer, Order 4: customer but no address + CollectionAssert.AreEqual(new[] { 3, 4 }, results); } [TestMethod] - public async Task Compound_Where_Select_OrderBy_Works() + public async Task Compound_SwitchExpression_InWhere() { var results = await Query - .Where(o => o.Status == OrderStatus.Pending) + .Where(o => (o.Status switch + { + OrderStatus.Approved => 1, + OrderStatus.Pending => 2, + _ => 0, + }) == 2) .OrderBy(o => o.Price) .Select(o => o.Id) .ToListAsync(); From a199c2763554703c5d5559d4e63c4eb6372cbcca Mon Sep 17 00:00:00 2001 From: Koen Date: Wed, 8 Apr 2026 00:20:46 +0000 Subject: [PATCH 4/4] Refactor ExpressiveMongoQueryable and MongoContainerFixture for improved readability and functionality --- .../Infrastructure/ExpressiveMongoQueryable.cs | 6 ++++-- .../Infrastructure/MongoContainerFixture.cs | 3 ++- .../Tests/ExpressiveMongoQueryProviderTests.cs | 8 +++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs b/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs index 27fa8b6..e1ec2b5 100644 --- a/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs +++ b/src/ExpressiveSharp.MongoDB/Infrastructure/ExpressiveMongoQueryable.cs @@ -27,6 +27,8 @@ public ExpressiveMongoQueryable(IQueryable source, ExpressiveMongoQueryProvid public Expression Expression => _source.Expression; public IQueryProvider Provider => _provider; - public IEnumerator GetEnumerator() => _source.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_source).GetEnumerator(); + public IEnumerator GetEnumerator() + => _provider.Execute>(Expression).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs index 3cd0469..41fe8ea 100644 --- a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Infrastructure/MongoContainerFixture.cs @@ -37,7 +37,8 @@ private static bool DetectDocker() { try { - using var client = new Docker.DotNet.DockerClientConfiguration().CreateClient(); + using var config = new Docker.DotNet.DockerClientConfiguration(); + using var client = config.CreateClient(); client.System.PingAsync().GetAwaiter().GetResult(); return true; } diff --git a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs index 3dd18f0..fed137d 100644 --- a/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs +++ b/tests/ExpressiveSharp.MongoDB.IntegrationTests/Tests/ExpressiveMongoQueryProviderTests.cs @@ -1,15 +1,16 @@ using System.Linq.Expressions; -using ExpressiveSharp.Extensions; using ExpressiveSharp.IntegrationTests.Scenarios.Store.Models; +using ExpressiveSharp.MongoDB.Extensions; using ExpressiveSharp.MongoDB.IntegrationTests.Infrastructure; using Microsoft.VisualStudio.TestTools.UnitTesting; using MongoDB.Driver; using MongoDB.Driver.Linq; +using static ExpressiveSharp.Extensions.ExpressionExtensions; namespace ExpressiveSharp.MongoDB.IntegrationTests.Tests; /// -/// Verifies that correctly +/// Verifies that correctly /// expands [Expressive] members before MongoDB's LINQ provider processes the query. /// [TestClass] @@ -20,9 +21,6 @@ public async Task Select_ExpressiveMember_ExpandsBeforeMongoTranslation() { // Total is [Expressive] => Price * Quantity // Provider should expand before MongoDB sees it - Expression> totalExpr = o => o.Total; - var expanded = (Expression>)totalExpr.ExpandExpressives(); - var results = await Orders.AsQueryable() .AsExpressive() .Select(o => o.Total)