Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/agents/spec-conformance-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Read `tests/MongoDB.EntityFrameworkCore.SpecificationTests/AGENTS.md` first; the

## Review focus

- **Test parallelization is disabled by design.** `tests/.../FunctionalTests/Usings.cs` declares `[assembly: CollectionBehavior(DisableTestParallelization = true)]`. Shared MongoDB state and `[CallerMemberName]`-derived collection names both depend on serial execution. Don't enable parallel.
- **Per-test database isolation.** Tests get a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`. Tests using fixed names cross-pollute and produce intermittent failures.
- **Test concurrency is controlled per project and in CI.** `tests/.../SpecificationTests/Usings.cs` declares `[assembly: CollectionBehavior(DisableTestParallelization = true)]`; `FunctionalTests` (xUnit v3) does not and relies on unique per-fixture databases. Shared MongoDB state, `[CallerMemberName]`-derived collection names and MongoDB 5.0/6.0's snapshot errors (code 246) mean `evergreen/run-tests.sh` runs each test project in turn, with sequential collections, on those servers. Don't make CI run the projects concurrently there.
- **Per-test database isolation.** Tests get a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`; each test assembly has its own `TestDatabaseNamer` with a distinct prefix including the EF version (`EF<n>-FunctionalTest-`, `EF<n>-SpecTest-`) so concurrent processes never collide or clean up each other's databases. Tests using fixed names cross-pollute and produce intermittent failures.
- **`[CallerMemberName]` collection-name generation.** `TemporaryDatabaseFixtureBase.CreateCollectionName(...)` derives collection names from caller method names. Hard-coded names defeat isolation.
- **Specification-tests inheritance pattern.** Fixtures inherit `*FixtureBase<TModelCustomizer>` from `Microsoft.EntityFrameworkCore.Specification.Tests`; tests inherit `*TestBase<TFixture>`. Naming is `*MongoFixture<TCustomizer>` / `*MongoTest`.
- **EF-version `#if` discipline.** Common patterns: `#if EF8` (legacy customizer interface, `Seed()` method), `#if EF8 || EF9` (pre-EF10 shapes), `#if !EF8 && !EF9` (EF10+ features). New tests must compile under all three define-constant configurations.
Expand All @@ -32,6 +32,6 @@ See `.claude/agents/CONVENTIONS.md` for the report shape, tags, finding cap, and
- Enabling test parallelization at the assembly level.
- Skipping a Specification test without a clear "permanently unsupported" rationale.
- Changes to `TestServer.GetOrInitializeTestServerAsync()` connection-priority logic.
- Changes to unique-name generation in `TestDatabaseNamer` (the `Interlocked.Increment`-backed `GetUniqueDatabaseName`) or the manual `DatabaseCleaner.CleanDatabase` skip-fact.
- Changes to unique-name generation or the per-assembly prefixes in either `TestDatabaseNamer` (the `Interlocked.Increment`-backed `GetUniqueDatabaseName`) or the manual `DatabaseCleaner.CleanDatabase` skip-fact.
- New `[XUnitCollection]` that overlaps existing collection memberships in non-obvious ways.
- Bumping the upstream EF version in `Versions.props` (touches every Specification-tests inheritance chain).
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The MongoDB database provider for [Entity Framework Core](https://github.com/dot
- Multi-EF-version targeting via build *configurations* (not target frameworks): `Debug|Release EF8`, `Debug|Release EF9`, `Debug|Release EF10`. EF8/EF9 build for `net8.0`; EF10 builds for `net10.0`. The active EF version is selected by the `EF8` / `EF9` / `EF10` define constant — see the version-conditional `<PropertyGroup>`s in `src/MongoDB.EntityFrameworkCore/MongoDB.EntityFrameworkCore.csproj`.
- EF and driver versions are pinned in `Versions.props`. The C# driver version can be overridden by setting the `DRIVER_VERSION` env var (CI uses this to test forward-compat).
- `<Nullable>enable</Nullable>` on `src/`. `<NoWarn>EF1001</NoWarn>` — the provider intentionally consumes EF Core's internal APIs.
- xUnit + FluentAssertions for tests. Tests run **serially** — see `tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs` (`[assembly: CollectionBehavior(DisableTestParallelization = true)]`).
- xUnit + FluentAssertions for tests. `UnitTests` and `FunctionalTests` use xUnit v3 (`xunit.v3`); `SpecificationTests` stays on xUnit v2. `SpecificationTests` disables test parallelization at the assembly level (`Usings.cs`); `FunctionalTests` does not and relies on a unique database per fixture. CI runs the projects, and on MongoDB 5.0/6.0 the test collections, one at a time — see Testing.

## Project Structure
- `src/MongoDB.EntityFrameworkCore/` — the provider.
Expand Down Expand Up @@ -58,8 +58,9 @@ The provider follows EF Core's pattern, not the C# driver's. There is **no enfor
## Testing
- **Recommended: run with both `MONGODB_URI` and `ATLAS_URI` unset.** `TestServer` then has TestContainers boot a `mongodb/mongodb-atlas-local` container, which (a) runs the **Atlas-gated tests (e.g. vector search) for real** against Atlas Search, and (b) gives each `dotnet test` process its **own isolated container** (random host port) plus uniquely-named databases — so multiple test runs, **even parallel agents, don't collide**. Trade-off: Docker is required and each run pays a one-time container boot (image ≈2 GB, pulled once).
- Connection resolution (`tests/.../FunctionalTests/Utilities/TestServer.cs`): the *default* server comes from `MONGODB_URI` (else an atlas-local container); the *Atlas* (`IsAtlas`) server comes from `ATLAS_URI` (else an atlas-local container — so Atlas tests run whenever `ATLAS_URI` is not `"Disabled"`, regardless of `MONGODB_URI`). Set `ATLAS_URI="Disabled"` to skip Atlas tests; point either var at an external server to use it instead of a container (note: a plain `mongod`/replica set can't run Atlas Search, so Atlas tests against it won't be meaningful).
- Tests run **serially within a single test process** (parallelization disabled at the assembly level) — fixtures share global MongoDB state and each test relies on a uniquely-named database/collection that would race under intra-process parallelism. Across *separate* processes the per-process containers (above) keep runs isolated.
- Each test gets a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`.
- **Concurrency.** Each test relies on a uniquely-named database/collection and fixtures share global MongoDB state. `SpecificationTests` runs serially within its process (parallelization disabled at the assembly level); `FunctionalTests` may run test collections in parallel. Across *separate* processes the per-process containers (above) keep runs isolated.
- **MongoDB 5.0/6.0 snapshot errors.** On those servers overlapping writes intermittently fail with `Unable to read from a snapshot due to pending collection catalog changes` (server code 246): a transaction's snapshot is older than a collection created moments before. 7.0 and later were not affected. So `evergreen/run-tests.sh` runs each test project in turn with sequential test collections (`-- xUnit.ParallelizeTestCollections=false`) when `MONGODB_VERSION` is `5.0` or `6.0`, and runs the solution as usual otherwise. Don't run the test projects concurrently against a 5.0/6.0 server.
- Each test gets a unique database name via `TestDatabaseNamer.GetUniqueDatabaseName()`. Each test assembly has its own copy with a distinct prefix that includes the EF version (`EF<n>-FunctionalTest-`, `EF<n>-SpecTest-`), so concurrent processes never collide and the startup cleanup (which drops databases by prefix) never drops another assembly's live databases.
- `[ModuleInitializer]` in `tests/.../FunctionalTests/ModuleInitialization.cs` registers BSON serializers at load time.

| Feature area | Required environment variables |
Expand Down
8 changes: 4 additions & 4 deletions evergreen/evergreen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ functions:
OS=${OS} \
MONGODB_URI="${MONGODB_URI}" \
ATLAS_URI="${ATLAS_URI}" \
MONGODB_VERSION="${version}" \
MONGODB_VERSION="${VERSION}" \
CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \
BUILD_CONFIGURATION="Debug EF8" \
./evergreen/run-tests.sh
Expand All @@ -187,7 +187,7 @@ functions:
OS=${OS} \
MONGODB_URI="${MONGODB_URI}" \
ATLAS_URI="${ATLAS_URI}" \
MONGODB_VERSION="${version}" \
MONGODB_VERSION="${VERSION}" \
CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \
BUILD_CONFIGURATION="Debug EF9" \
./evergreen/run-tests.sh
Expand All @@ -201,7 +201,7 @@ functions:
OS=${OS} \
MONGODB_URI="${MONGODB_URI}" \
ATLAS_URI="${ATLAS_URI}" \
MONGODB_VERSION="${version}" \
MONGODB_VERSION="${VERSION}" \
CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \
BUILD_CONFIGURATION="Debug EF10" \
./evergreen/run-tests.sh
Expand All @@ -217,7 +217,7 @@ functions:
OS=${OS} \
MONGODB_URI="${MONGODB_URI}" \
ATLAS_URI="${ATLAS_URI}" \
MONGODB_VERSION="${version}" \
MONGODB_VERSION="${VERSION}" \
CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \
BUILD_CONFIGURATION="Debug EF10" \
./evergreen/run-tests.sh
Expand Down
18 changes: 17 additions & 1 deletion evergreen/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,20 @@ fi

echo "Running EF Core tests for '${BUILD_CONFIGURATION}' configuration"
dotnet clean "./MongoDB.EFCoreProvider.sln"
dotnet test "./MongoDB.EFCoreProvider.sln" -e MONGODB_URI="${MONGODB_URI}" -c "${BUILD_CONFIGURATION}" --results-directory "./artifacts/test-results/${BUILD_CONFIGURATION// /}" --logger "junit;LogFileName=TEST_{assembly}.xml;FailureBodyFormat=Verbose" --logger "console;verbosity=detailed"
test_args=(-e MONGODB_URI="${MONGODB_URI}" -c "${BUILD_CONFIGURATION}" --results-directory "./artifacts/test-results/${BUILD_CONFIGURATION// /}" --logger "junit;LogFileName=TEST_{assembly}.xml;FailureBodyFormat=Verbose" --logger "console;verbosity=detailed")

case "${MONGODB_VERSION}" in
5.0|6.0)
# MongoDB 5.0 and 6.0 intermittently fail with "Unable to read from a snapshot due to pending collection
# catalog changes" when writes overlap, so run each test project in turn (dotnet test on the solution runs
# them concurrently) with the test collections of each project running one at a time.
echo "Running test projects and test collections sequentially for MongoDB ${MONGODB_VERSION}"
exit_code=0
for project in tests/*/*.csproj; do
dotnet test "./${project}" "${test_args[@]}" -- xUnit.ParallelizeTestCollections=false || exit_code=$?
done
exit $exit_code
;;
esac

dotnet test "./MongoDB.EFCoreProvider.sln" "${test_args[@]}"
1 change: 0 additions & 1 deletion tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<ItemGroup>
<PackageReference Include="JunitXml.TestLogger" Version="2.1.81" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"/>
<PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* limitations under the License.
*/

using System.Runtime.CompilerServices;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Encryption;
Expand Down Expand Up @@ -203,10 +204,16 @@ private BsonDocument CreatePatientEncryptionSchema()

protected class EncryptionTheory : TheoryAttribute
{
public override string? Skip
=> TestServer.SupportsEncryption
? null
: TestServer.EncryptionSkippedMessage;
public EncryptionTheory(
[CallerFilePath] string? sourceFilePath = null,
[CallerLineNumber] int sourceLineNumber = -1)
: base(sourceFilePath, sourceLineNumber)
{
if (!TestServer.SupportsEncryption)
{
Skip = TestServer.EncryptionSkippedMessage;
}
}
}

public enum EncryptionMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* limitations under the License.
*/

using System.Runtime.CompilerServices;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
Expand Down Expand Up @@ -48,12 +49,20 @@ protected MongoClient CreateEncryptedClient(

protected class QueryableEncryptionTheory : TheoryAttribute
{
public override string? Skip
=> !TestServer.SupportsEncryption
? TestServer.EncryptionSkippedMessage
: ShouldRunQueryableEncryptionTests
? null
: "These Queryable Encryption tests require MongoDB 8.0 or later as declared by the VERSION environment variable.";
public QueryableEncryptionTheory(
[CallerFilePath] string? sourceFilePath = null,
[CallerLineNumber] int sourceLineNumber = -1)
: base(sourceFilePath, sourceLineNumber)
{
if (!TestServer.SupportsEncryption)
{
Skip = TestServer.EncryptionSkippedMessage;
}
else if (!ShouldRunQueryableEncryptionTests)
{
Skip = "These Queryable Encryption tests require MongoDB 8.0 or later as declared by the VERSION environment variable.";
}
}
}

protected static bool ShouldRunQueryableEncryptionTests =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
using MongoDB.Driver;
using MongoDB.EntityFrameworkCore.Diagnostics;
using MongoDB.EntityFrameworkCore.FunctionalTests.Entities.Guides;
using Xunit.Abstractions;

namespace MongoDB.EntityFrameworkCore.FunctionalTests;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using MongoDB.EntityFrameworkCore.FunctionalTests.Utilities;
using MongoDB.EntityFrameworkCore.Metadata.Conventions;

namespace MongoDB.EntityFrameworkCore.FunctionalTests.Mapping;
Expand Down Expand Up @@ -321,12 +323,12 @@ public void Shadow_property_can_be_used_for_rowversion()
{
var staleAuthor = new Author {Name = "Damien"};

var staleDb = new BloggingContext(For(database.MongoDatabase).Options, ModelConfiguration);
var staleDb = new BloggingContext(RowVersionOptions(), ModelConfiguration);
staleDb.Authors.Add(staleAuthor);
staleDb.SaveChanges();

{
var freshDb = new BloggingContext(For(database.MongoDatabase).Options, ModelConfiguration);
var freshDb = new BloggingContext(RowVersionOptions(), ModelConfiguration);
var freshAuthor = freshDb.Authors.First(a => a.Id == staleAuthor.Id);
freshAuthor.Name = "Damien Modified Fresh";
freshDb.SaveChanges();
Expand All @@ -335,6 +337,13 @@ public void Shadow_property_can_be_used_for_rowversion()
staleAuthor.Name = "Damien Modified Stale";
Assert.Throws<DbUpdateConcurrencyException>(() => staleDb.SaveChanges());

// The model differs from other tests' BloggingContext model, so it must not share a cache entry.
DbContextOptions RowVersionOptions()
=> For(database.MongoDatabase)
.ReplaceService<IModelCacheKeyFactory, IgnoreCacheKeyFactory>()
.ConfigureWarnings(x => x.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning))
.Options;

void ModelConfiguration(ModelBuilder mb) => mb.Entity<Author>().Property<long>("RowVersion").IsRowVersion();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
<Configurations>Debug EF8;Release EF8;Debug EF9;Release EF9;Debug EF10;Release EF10</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);xUnit1051</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug EF8' ">
<DefineConstants>TRACE;DEBUG;EF8</DefineConstants>
</PropertyGroup>
Expand All @@ -24,6 +27,10 @@

<Import Project="..\..\Versions.props" />

<ItemGroup>
<PackageReference Include="xunit.v3" Version="3.2.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\MongoDB.EntityFrameworkCore\MongoDB.EntityFrameworkCore.csproj" />
<PackageReference Include="MongoDB.Driver.Encryption" Version="$(CSharpDriverVersion)" />
Expand All @@ -47,11 +54,4 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="$(EF10Version)" />
<Compile Include="Design\Generated\EF10\**" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="Xunit.TestFramework">
<_Parameter1>XUnit.Startup.Dispose.Hooks.TestFramework</_Parameter1>
<_Parameter2>XUnit.Startup.Dispose.Hooks</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,15 @@ class StringArrayEntity : BaseIdEntity
public string[] aStringArray { get; set; }
}

public static readonly TheoryData<string[][]> ArrayOfArraysData =
[
[],
[
public static readonly TheoryData<string[][]> ArrayOfArraysData = new()
{
Array.Empty<string[]>(),
new[]
{
new[] {"a", "b", "c"},
new[] {"d"}
]
];
}
};

[Theory]
[MemberData(nameof(ArrayOfArraysData))]
Expand Down
2 changes: 0 additions & 2 deletions tests/MongoDB.EntityFrameworkCore.FunctionalTests/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,3 @@
global using Xunit;
global using XUnitCollection = Xunit.CollectionAttribute;
global using MongoDB.EntityFrameworkCore.FunctionalTests.Utilities;

[assembly:CollectionBehavior(DisableTestParallelization = true)]
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@
* limitations under the License.
*/

using System.Runtime.CompilerServices;
namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities;

public class AtlasFact : FactAttribute
{
public override string? Skip
=> TestServer.SupportsAtlas
? null
: "Requires Atlas search capabilities.";
public AtlasFact(
[CallerFilePath] string? sourceFilePath = null,
[CallerLineNumber] int sourceLineNumber = -1)
: base(sourceFilePath, sourceLineNumber)
{
if (!TestServer.SupportsAtlas)
{
Skip = "Requires Atlas search capabilities.";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static async Task<AtlasTemporaryDatabaseFixture> CreateInitializedAsync()
public override TestServer TestServer
=> _server!;

public override async Task InitializeAsync()
public override async ValueTask InitializeAsync()
{
_server = await TestServer.GetOrInitializeTestServerAsync(MongoCondition.IsAtlas);
await base.InitializeAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@
* limitations under the License.
*/

using System.Runtime.CompilerServices;
namespace MongoDB.EntityFrameworkCore.FunctionalTests.Utilities;

public class AtlasTheory : TheoryAttribute
{
public override string? Skip
=> TestServer.SupportsAtlas
? null
: "Requires Atlas search capabilities.";
public AtlasTheory(
[CallerFilePath] string? sourceFilePath = null,
[CallerLineNumber] int sourceLineNumber = -1)
: base(sourceFilePath, sourceLineNumber)
{
if (!TestServer.SupportsAtlas)
{
Skip = "Requires Atlas search capabilities.";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ public class ReadOnlySampleGuidesFixture : IAsyncLifetime
public IMongoDatabase MongoDatabase { get; private set; }
public IMongoClient Client { get; private set; }

public async Task InitializeAsync()
public async ValueTask InitializeAsync()
{
var server = await TestServer.GetOrInitializeTestServerAsync(MongoCondition.None);
Client = server.Client;
MongoDatabase = Client.GetDatabase($"{TestDatabaseNamer.TestDatabasePrefix}SampleGuides");
SampleGuides.Populate(MongoDatabase);
}

public Task DisposeAsync()
=> Task.CompletedTask;
public ValueTask DisposeAsync()
=> ValueTask.CompletedTask;
}
Loading