From 5c5cf18abc4472df2eb3ee2e48bb9f53e02c7991 Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 11:54:09 -0500 Subject: [PATCH 1/8] remove older versions --- .github/workflows/build.yml | 4 +--- dotnet-tools.json | 13 +++++++++++++ .../MongoFramework.Profiling.MiniProfiler.csproj | 2 +- src/MongoFramework/IsExternalInit.cs | 13 ------------- src/MongoFramework/MongoFramework.csproj | 4 +--- .../MongoFramework.Tests.csproj | 4 +--- 6 files changed, 17 insertions(+), 23 deletions(-) create mode 100644 dotnet-tools.json delete mode 100644 src/MongoFramework/IsExternalInit.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00986fa2..4d50468b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,9 +72,7 @@ jobs: - name: Setup dotnet SDK uses: actions/setup-dotnet@v3 with: - dotnet-version: | - 6.0.x - 7.0.x + dotnet-version: 6.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 00000000..cdb3c8d1 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.5", + "commands": [ + "csharpier" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj b/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj index 549237af..69587a06 100644 --- a/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj +++ b/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0 + net6.0 MongoFramework.Profiling.MiniProfiler MiniProfiler for MongoFramework MongoFramework integration for MiniProfiler diff --git a/src/MongoFramework/IsExternalInit.cs b/src/MongoFramework/IsExternalInit.cs deleted file mode 100644 index 17a51c23..00000000 --- a/src/MongoFramework/IsExternalInit.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel; - -namespace System.Runtime.CompilerServices -{ - /// - /// Reserved to be used by the compiler for tracking metadata. - /// This class should not be used by developers in source code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - internal static class IsExternalInit - { - } -} \ No newline at end of file diff --git a/src/MongoFramework/MongoFramework.csproj b/src/MongoFramework/MongoFramework.csproj index 2e886d0a..77a0ddc6 100644 --- a/src/MongoFramework/MongoFramework.csproj +++ b/src/MongoFramework/MongoFramework.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netstandard2.1;net6.0 + net6.0 MongoFramework MongoFramework An "Entity Framework"-like interface for the MongoDB C# Driver @@ -11,9 +11,7 @@ - - diff --git a/tests/MongoFramework.Tests/MongoFramework.Tests.csproj b/tests/MongoFramework.Tests/MongoFramework.Tests.csproj index 553527c8..c32fe654 100644 --- a/tests/MongoFramework.Tests/MongoFramework.Tests.csproj +++ b/tests/MongoFramework.Tests/MongoFramework.Tests.csproj @@ -1,9 +1,8 @@  - MongoFramework.Tests MongoFramework.Tests - net461;net48;net6.0;net7.0 + net6.0 false @@ -16,7 +15,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - From 2851a2fa9d34e341a0796c9ce53d70a7ab8c72f3 Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 12:02:11 -0500 Subject: [PATCH 2/8] package updates --- .github/workflows/build.yml | 2 +- global.json | 6 ++ src/Directory.Build.props | 2 +- ...ngoFramework.Profiling.MiniProfiler.csproj | 4 +- src/MongoFramework/MongoFramework.csproj | 5 +- .../MongoFramework.Benchmarks.csproj | 4 +- .../ExpectedExceptionPatternAttribute.cs | 37 ------------ .../Commands/AddEntityCommandTests.cs | 11 ++-- .../Commands/UpdateEntityCommandTests.cs | 11 ++-- .../Indexing/EntityIndexWriterTests.cs | 9 ++- .../Linq/LinqExtensionsTests.cs | 3 +- .../MongoDbBucketSetTests.cs | 57 ++++++++++--------- .../MongoDbConnectionTests.cs | 4 +- .../MongoFramework.Tests.csproj | 12 ++-- 14 files changed, 74 insertions(+), 93 deletions(-) create mode 100644 global.json delete mode 100644 tests/MongoFramework.Tests/ExpectedExceptionPatternAttribute.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d50468b..4c1190e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: - name: Setup dotnet SDK uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/global.json b/global.json new file mode 100644 index 00000000..512142d2 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index db0700a8..eafe6553 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,7 @@ Latest - + diff --git a/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj b/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj index 69587a06..9b92ca60 100644 --- a/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj +++ b/src/MongoFramework.Profiling.MiniProfiler/MongoFramework.Profiling.MiniProfiler.csproj @@ -1,7 +1,7 @@  - net6.0 + net10.0 MongoFramework.Profiling.MiniProfiler MiniProfiler for MongoFramework MongoFramework integration for MiniProfiler @@ -10,7 +10,7 @@ - + diff --git a/src/MongoFramework/MongoFramework.csproj b/src/MongoFramework/MongoFramework.csproj index 77a0ddc6..91f56ad9 100644 --- a/src/MongoFramework/MongoFramework.csproj +++ b/src/MongoFramework/MongoFramework.csproj @@ -1,7 +1,7 @@  - net6.0 + net10.0 MongoFramework MongoFramework An "Entity Framework"-like interface for the MongoDB C# Driver @@ -11,8 +11,7 @@ - - + diff --git a/tests/MongoFramework.Benchmarks/MongoFramework.Benchmarks.csproj b/tests/MongoFramework.Benchmarks/MongoFramework.Benchmarks.csproj index d60c6bdd..6de186d6 100644 --- a/tests/MongoFramework.Benchmarks/MongoFramework.Benchmarks.csproj +++ b/tests/MongoFramework.Benchmarks/MongoFramework.Benchmarks.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net10.0 False - + diff --git a/tests/MongoFramework.Tests/ExpectedExceptionPatternAttribute.cs b/tests/MongoFramework.Tests/ExpectedExceptionPatternAttribute.cs deleted file mode 100644 index 82db18fa..00000000 --- a/tests/MongoFramework.Tests/ExpectedExceptionPatternAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MongoFramework.Tests -{ - public class ExpectedExceptionPatternAttribute : ExpectedExceptionBaseAttribute - { - private Type ExpectedExceptionType { get; } - private Regex MessagePattern { get; } - private string RawPattern { get; } - - public ExpectedExceptionPatternAttribute(Type expectedExceptionType, string exceptionMessagePattern) - { - ExpectedExceptionType = expectedExceptionType; - MessagePattern = new Regex(exceptionMessagePattern); - RawPattern = exceptionMessagePattern; - } - - protected override void Verify(Exception exception) - { - Assert.IsNotNull(exception, $"\"{nameof(exception)}\" is null"); - - var thrownExceptionType = exception.GetType(); - - if (ExpectedExceptionType != thrownExceptionType) - { - throw new Exception($"Test method threw exception {thrownExceptionType.FullName}, but exception {ExpectedExceptionType.FullName} was expected. Exception message: {exception.Message}"); - } - - if (!MessagePattern.IsMatch(exception.Message)) - { - throw new Exception($"Thrown exception message \"{exception.Message}\" does not match pattern \"{RawPattern}\"."); - } - } - } -} diff --git a/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs b/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs index 89258c8d..105c6d21 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs @@ -41,12 +41,15 @@ public void AddEntity() Assert.IsNotNull(entity.Id); } - [TestMethod, ExpectedException(typeof(ValidationException))] + [TestMethod] public void ValidationExceptionOnInvalidModel() { - var entity = new TestValidationModel { }; - var command = new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added)); - command.GetModel(WriteModelOptions.Default).FirstOrDefault(); + Assert.ThrowsException(() => + { + var entity = new TestValidationModel { }; + var command = new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added)); + command.GetModel(WriteModelOptions.Default).FirstOrDefault(); + }); } } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs b/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs index 56f7468e..49d0a681 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs @@ -51,12 +51,15 @@ public void UpdateEntity() Assert.AreEqual("UpdateEntityCommandTests.UpdateEntity-Updated", dbEntity.Title); } - [TestMethod, ExpectedException(typeof(ValidationException))] + [TestMethod] public void ValidationExceptionOnInvalidModel() { - var entity = new TestValidationModel { }; - var command = new UpdateEntityCommand(new EntityEntry(entity, EntityEntryState.Updated)); - command.GetModel(WriteModelOptions.Default).FirstOrDefault(); + Assert.ThrowsException(() => + { + var entity = new TestValidationModel { }; + var command = new UpdateEntityCommand(new EntityEntry(entity, EntityEntryState.Updated)); + command.GetModel(WriteModelOptions.Default).FirstOrDefault(); + }); } } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs b/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs index f0641e71..9b7c2275 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs @@ -77,11 +77,14 @@ public async Task NoIndexAsync() } - [TestMethod, ExpectedException(typeof(Exception), AllowDerivedTypes = true)] + [TestMethod] public void FailureFromMultipleTextIndexes() { - var connection = TestConfiguration.GetConnection(); - EntityIndexWriter.ApplyIndexing(connection); + Assert.ThrowsException(() => + { + var connection = TestConfiguration.GetConnection(); + EntityIndexWriter.ApplyIndexing(connection); + }); } } } \ No newline at end of file diff --git a/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs b/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs index b3d716b0..21609720 100644 --- a/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs +++ b/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs @@ -72,10 +72,9 @@ public void ValidToQuery() } [TestMethod] - [ExpectedException(typeof(ArgumentException), "ArgumentException")] public void InvalidToQuery() { - LinqExtensions.ToQuery(null); + Assert.ThrowsException(() => LinqExtensions.ToQuery(null)); } [TestMethod] diff --git a/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs b/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs index 6d9a403b..2d7e6aaa 100644 --- a/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs +++ b/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs @@ -31,20 +31,22 @@ public void InitialiseDbSet() })); } - [TestMethod, ExpectedException(typeof(ArgumentNullException))] + [TestMethod] public void MustBeSuppliedContext() { - new MongoDbBucketSet(null, new BucketSetOptions - { - BucketSize = 100, - EntityTimeProperty = "Date" - }); + Assert.ThrowsException(() => + new MongoDbBucketSet(null, new BucketSetOptions + { + BucketSize = 100, + EntityTimeProperty = "Date" + })); } - [TestMethod, ExpectedException(typeof(ArgumentException))] + [TestMethod] public void MustBeSuppliedOptions() { - new MongoDbBucketSet(new Mock().Object, null); + Assert.ThrowsException(() => + new MongoDbBucketSet(new Mock().Object, null)); } [TestMethod] @@ -97,34 +99,37 @@ public async Task SuccessfullyInsertAndQueryBackEntityBucketsAsync() Assert.IsTrue(dbSet.Any(b => b.Group.Name == "Group1" && b.Items.Any(i => i.Label == "Entry1" && i.Date == new DateTime(2020, 1, 1)))); } - [TestMethod, ExpectedException(typeof(ArgumentException))] + [TestMethod] public void InvalidBucketSize() { - new MongoDbBucketSet(new Mock().Object, new BucketSetOptions - { - BucketSize = 0, - EntityTimeProperty = "Date" - }); + Assert.ThrowsException(() => + new MongoDbBucketSet(new Mock().Object, new BucketSetOptions + { + BucketSize = 0, + EntityTimeProperty = "Date" + })); } - [TestMethod, ExpectedException(typeof(ArgumentException))] + [TestMethod] public void InvalidSubEntityTimeProperty_Missing() { - new MongoDbBucketSet(new Mock().Object, new BucketSetOptions - { - BucketSize = 100, - EntityTimeProperty = "MissingField" - }); + Assert.ThrowsException(() => + new MongoDbBucketSet(new Mock().Object, new BucketSetOptions + { + BucketSize = 100, + EntityTimeProperty = "MissingField" + })); } - [TestMethod, ExpectedException(typeof(ArgumentException))] + [TestMethod] public void InvalidSubEntityTimeProperty_WrongType() { - new MongoDbBucketSet(new Mock().Object, new BucketSetOptions - { - BucketSize = 100, - EntityTimeProperty = "Label" - }); + Assert.ThrowsException(() => + new MongoDbBucketSet(new Mock().Object, new BucketSetOptions + { + BucketSize = 100, + EntityTimeProperty = "Label" + })); } [TestMethod] diff --git a/tests/MongoFramework.Tests/MongoDbConnectionTests.cs b/tests/MongoFramework.Tests/MongoDbConnectionTests.cs index efc56855..9dd1b964 100644 --- a/tests/MongoFramework.Tests/MongoDbConnectionTests.cs +++ b/tests/MongoFramework.Tests/MongoDbConnectionTests.cs @@ -13,10 +13,10 @@ public void ConnectionFromConnectionString() Assert.IsNotNull(connection); } - [TestMethod, ExpectedException(typeof(ArgumentNullException))] + [TestMethod] public void NullUrlThrowsException() { - MongoDbConnection.FromUrl(null); + Assert.ThrowsException(() => MongoDbConnection.FromUrl(null)); } } } diff --git a/tests/MongoFramework.Tests/MongoFramework.Tests.csproj b/tests/MongoFramework.Tests/MongoFramework.Tests.csproj index c32fe654..3b726c15 100644 --- a/tests/MongoFramework.Tests/MongoFramework.Tests.csproj +++ b/tests/MongoFramework.Tests/MongoFramework.Tests.csproj @@ -2,16 +2,16 @@ MongoFramework.Tests MongoFramework.Tests - net6.0 + net10.0 false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 94af2f0e5d7c9deae861c2b328ffe9f744f89836 Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 12:13:04 -0500 Subject: [PATCH 3/8] Update to latest MSTest --- .../MongoFramework.Tests/AssertExtensions.cs | 12 --------- .../Bson/GetDifferencesTests.cs | 6 ++--- .../Commands/AddEntityCommandTests.cs | 2 +- .../Commands/UpdateEntityCommandTests.cs | 2 +- .../EntityEntryContainerTests.cs | 6 ++--- .../Indexing/EntityIndexWriterTests.cs | 6 ++--- .../Mapping/EntityDefinitionExtensionTests.cs | 6 ++--- .../Processors/ExtraElementsProcessorTests.cs | 2 +- .../MappingAdapterProcessorTests.cs | 4 +-- .../Processors/SkipMappingProcessorTests.cs | 4 +-- .../PropertyTraversalExtensionTests.cs | 2 +- .../TypeDiscoverySerializationTests.cs | 4 +-- .../Linq/LinqExtensionsTests.cs | 2 +- .../Linq/LinqExtensions_SearchGeoTests.cs | 12 ++++----- ...ueryableAsyncExtensionsMultiTenantTests.cs | 16 ++++++------ .../Linq/QueryableAsyncExtensionsTests.cs | 16 ++++++------ .../MappingBuilderExtensionTests.cs | 6 ++--- .../MappingBuilderTests.cs | 10 +++---- .../MongoDbBucketSetTests.cs | 22 ++++++++-------- .../MongoDbConnectionTests.cs | 2 +- .../MongoDbContextTenantTests.cs | 4 +-- tests/MongoFramework.Tests/MongoDbSetTests.cs | 4 +-- .../MongoDbTenantSetTests.cs | 26 +++++++++---------- .../MongoFramework.Tests.csproj | 9 +++++-- .../MiniProfilerDiagnosticListenerTests.cs | 22 ++++++++-------- .../Utilities/CheckTests.cs | 16 ++++++------ 26 files changed, 108 insertions(+), 115 deletions(-) diff --git a/tests/MongoFramework.Tests/AssertExtensions.cs b/tests/MongoFramework.Tests/AssertExtensions.cs index 3c0f495c..7e316047 100644 --- a/tests/MongoFramework.Tests/AssertExtensions.cs +++ b/tests/MongoFramework.Tests/AssertExtensions.cs @@ -23,12 +23,6 @@ public static void DoesNotThrow(Action expressionUnderTest, string exceptionM { Assert.Fail(exceptionMessage); } - catch (Exception) - { - Assert.IsTrue(true); - } - - Assert.IsTrue(true); } /// @@ -48,12 +42,6 @@ public static async Task DoesNotThrowAsync(Func expressionUnderTest, st { Assert.Fail(exceptionMessage); } - catch (Exception) - { - Assert.IsTrue(true); - } - - Assert.IsTrue(true); } } } \ No newline at end of file diff --git a/tests/MongoFramework.Tests/Bson/GetDifferencesTests.cs b/tests/MongoFramework.Tests/Bson/GetDifferencesTests.cs index 71f8c779..a4d488a6 100644 --- a/tests/MongoFramework.Tests/Bson/GetDifferencesTests.cs +++ b/tests/MongoFramework.Tests/Bson/GetDifferencesTests.cs @@ -131,8 +131,8 @@ public void GetBsonArrayDiffWithNoDifferences() var arrayA = new BsonArray(Enumerable.Range(1, 5)); var arrayB = new BsonArray(Enumerable.Range(1, 5)); - Assert.AreEqual(null, BsonDiff.GetDifferences(arrayA, arrayB).Difference); - Assert.AreEqual(null, BsonDiff.GetDifferences(arrayB, arrayA).Difference); + Assert.IsNull(BsonDiff.GetDifferences(arrayA, arrayB).Difference); + Assert.IsNull(BsonDiff.GetDifferences(arrayB, arrayA).Difference); } [TestMethod] @@ -158,7 +158,7 @@ public void GetBsonArrayDiffWithNullArrays() Assert.AreEqual(resultWithClearedValues, BsonDiff.GetDifferences(arrayA, null).Difference); Assert.AreEqual(resultWithSameValues, BsonDiff.GetDifferences(null, arrayA).Difference); - Assert.AreEqual(null, BsonDiff.GetDifferences((BsonArray)null, null).Difference); + Assert.IsNull(BsonDiff.GetDifferences((BsonArray)null, null).Difference); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs b/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs index 105c6d21..230291ea 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Commands/AddEntityCommandTests.cs @@ -44,7 +44,7 @@ public void AddEntity() [TestMethod] public void ValidationExceptionOnInvalidModel() { - Assert.ThrowsException(() => + Assert.Throws(() => { var entity = new TestValidationModel { }; var command = new AddEntityCommand(new EntityEntry(entity, EntityEntryState.Added)); diff --git a/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs b/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs index 49d0a681..83b9b34f 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Commands/UpdateEntityCommandTests.cs @@ -54,7 +54,7 @@ public void UpdateEntity() [TestMethod] public void ValidationExceptionOnInvalidModel() { - Assert.ThrowsException(() => + Assert.Throws(() => { var entity = new TestValidationModel { }; var command = new UpdateEntityCommand(new EntityEntry(entity, EntityEntryState.Updated)); diff --git a/tests/MongoFramework.Tests/Infrastructure/EntityEntryContainerTests.cs b/tests/MongoFramework.Tests/Infrastructure/EntityEntryContainerTests.cs index 02e53f68..4924cfc1 100644 --- a/tests/MongoFramework.Tests/Infrastructure/EntityEntryContainerTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/EntityEntryContainerTests.cs @@ -210,8 +210,8 @@ public void EnforceMultiTenantRequiresTenantId() }; entryContainer.SetEntityState(entity, EntityEntryState.Added); - Assert.ThrowsException(() => entryContainer.EnforceMultiTenant(null)); - Assert.ThrowsException(() => entryContainer.EnforceMultiTenant(" ")); + Assert.Throws(() => entryContainer.EnforceMultiTenant(null)); + Assert.Throws(() => entryContainer.EnforceMultiTenant(" ")); } [TestMethod] @@ -234,7 +234,7 @@ public void EnforceMultiTenantsSkipsNonTenantModels() entryContainer.EnforceMultiTenant(TestConfiguration.GetTenantId()); - Assert.AreEqual(entryContainer.Entries().Count(), 2); + Assert.AreEqual(2, entryContainer.Entries().Count()); } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs b/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs index 9b7c2275..60b57964 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Indexing/EntityIndexWriterTests.cs @@ -46,7 +46,7 @@ public void WriteIndexSync() var collection = connection.GetDatabase().GetCollection("IndexModel"); var dbIndexes = collection.Indexes.List().ToList(); - Assert.AreEqual(5, dbIndexes.Count); + Assert.HasCount(5, dbIndexes); } [TestMethod] @@ -59,7 +59,7 @@ public async Task WriteIndexAsync() var collection = connection.GetDatabase().GetCollection("IndexModel"); var dbIndexes = await collection.Indexes.List().ToListAsync().ConfigureAwait(false); - Assert.AreEqual(5, dbIndexes.Count); + Assert.HasCount(5, dbIndexes); } [TestMethod] @@ -80,7 +80,7 @@ public async Task NoIndexAsync() [TestMethod] public void FailureFromMultipleTextIndexes() { - Assert.ThrowsException(() => + Assert.Throws(() => { var connection = TestConfiguration.GetConnection(); EntityIndexWriter.ApplyIndexing(connection); diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs index 721e5bbf..fadba8cf 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityDefinitionExtensionTests.cs @@ -56,7 +56,7 @@ public void GetInheritedPropertiesTakesBaseProperties() { var definition = EntityMapping.RegisterType(typeof(OverridePropertyGrandChildModel)); var inheritedProperties = definition.GetInheritedProperties().ToArray(); - Assert.AreEqual(1, inheritedProperties.Length); + Assert.HasCount(1, inheritedProperties); Assert.AreEqual(typeof(OverridePropertyBaseModel), inheritedProperties[0].PropertyInfo.DeclaringType); } [TestMethod] @@ -64,7 +64,7 @@ public void GetAllPropertiesTakesBaseProperties() { var definition = EntityMapping.RegisterType(typeof(OverridePropertyChildModel)); var allProperties = definition.GetAllProperties().ToArray(); - Assert.AreEqual(1, allProperties.Length); + Assert.HasCount(1, allProperties); Assert.AreEqual(typeof(OverridePropertyBaseModel), allProperties[0].PropertyInfo.DeclaringType); } @@ -72,7 +72,7 @@ public void GetAllPropertiesTakesBaseProperties() public void GetTenantModelIdRequiresTenant() { var definition = EntityMapping.RegisterType(typeof(TenantModel)); - Assert.ThrowsException(() => definition.CreateIdFilter("id")); + Assert.Throws(() => definition.CreateIdFilter("id")); } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/ExtraElementsProcessorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/ExtraElementsProcessorTests.cs index 15857388..0b3a4623 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/ExtraElementsProcessorTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/ExtraElementsProcessorTests.cs @@ -66,7 +66,7 @@ public void ObeysExtraElementsAttribute() EntityMapping.RegisterType(typeof(IgnoreExtraElementsModel)); classMap = BsonClassMap.GetRegisteredClassMaps() .Where(cm => cm.ClassType == typeof(IgnoreExtraElementsModel)).FirstOrDefault(); - Assert.AreEqual(null, classMap.ExtraElementsMemberMap); + Assert.IsNull(classMap.ExtraElementsMemberMap); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs index e1aa25c9..1e51589b 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/MappingAdapterProcessorTests.cs @@ -68,7 +68,7 @@ public void AdapterRequiresIMappingProcessor() EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); EntityMapping.AddMappingProcessor(new EntityIdProcessor()); EntityMapping.AddMappingProcessor(new MappingAdapterProcessor()); - Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(AdapterTestModelNoInterface))); + Assert.Throws(() => EntityMapping.RegisterType(typeof(AdapterTestModelNoInterface))); } [TestMethod] @@ -78,7 +78,7 @@ public void AdapterRequiresParameterlessConstructor() EntityMapping.AddMappingProcessor(new PropertyMappingProcessor()); EntityMapping.AddMappingProcessor(new EntityIdProcessor()); EntityMapping.AddMappingProcessor(new MappingAdapterProcessor()); - Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(AdapterTestModelConstructor))); + Assert.Throws(() => EntityMapping.RegisterType(typeof(AdapterTestModelConstructor))); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs index 04945e20..ad33295f 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/Processors/SkipMappingProcessorTests.cs @@ -23,8 +23,8 @@ public void ModelSkippedWithAttribute() { EntityMapping.AddMappingProcessor(new SkipMappingProcessor()); - var exception = Assert.ThrowsException(() => EntityMapping.RegisterType(typeof(SkippedMappingModel))); - Assert.IsTrue(exception.Message.Contains("was skipped")); + var exception = Assert.Throws(() => EntityMapping.RegisterType(typeof(SkippedMappingModel))); + StringAssert.Contains(exception.Message, "was skipped"); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/PropertyTraversalExtensionTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/PropertyTraversalExtensionTests.cs index 6c601e1d..d8e9d520 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/PropertyTraversalExtensionTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/PropertyTraversalExtensionTests.cs @@ -38,7 +38,7 @@ public void TraverseProperties() var definition = EntityMapping.RegisterType(typeof(TraverseMappingModel)); var result = definition.TraverseProperties().ToArray(); - Assert.AreEqual(32, result.Length); + Assert.HasCount(32, result); Assert.IsTrue(result.Any(m => m.GetPath() == "RecursionType" && m.Depth == 0)); diff --git a/tests/MongoFramework.Tests/Infrastructure/Serialization/TypeDiscoverySerializationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Serialization/TypeDiscoverySerializationTests.cs index c368ccfc..42f5bb4a 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Serialization/TypeDiscoverySerializationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Serialization/TypeDiscoverySerializationTests.cs @@ -182,7 +182,7 @@ public void ReserializationWithoutDataLoss() var deserializedResult = BsonSerializer.Deserialize(document); - Assert.AreEqual(3, deserializedResult.KnownList.Count); + Assert.HasCount(3, deserializedResult.KnownList); Assert.IsInstanceOfType(deserializedResult.KnownList[0], typeof(KnownBaseModel)); Assert.IsInstanceOfType(deserializedResult.KnownList[1], typeof(UnknownChildModel)); Assert.IsInstanceOfType(deserializedResult.KnownList[2], typeof(UnknownGrandChildModel)); @@ -312,7 +312,7 @@ public void DeserializeUnknownTypesInDictionary() Assert.AreEqual("ObjectValueAsString", result.Dictionary["String"]); Assert.AreEqual(1, result.Dictionary["Number"]); Assert.AreEqual(new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc), result.Dictionary["Date"]); - Assert.AreEqual(true, result.Dictionary["Boolean"]); + Assert.IsTrue((bool)result.Dictionary["Boolean"]); Assert.AreEqual(20, ((object[])result.Dictionary["Array"])[1]); Assert.AreEqual(ObjectId.Parse("507f1f77bcf86cd799439011"), result.Dictionary["ObjectId"]); } diff --git a/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs b/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs index 21609720..9120be88 100644 --- a/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs +++ b/tests/MongoFramework.Tests/Linq/LinqExtensionsTests.cs @@ -74,7 +74,7 @@ public void ValidToQuery() [TestMethod] public void InvalidToQuery() { - Assert.ThrowsException(() => LinqExtensions.ToQuery(null)); + Assert.Throws(() => LinqExtensions.ToQuery(null)); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs b/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs index d2e5fa38..fed4104d 100644 --- a/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs +++ b/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs @@ -84,7 +84,7 @@ public void SearchGeoNearWithCustomDistanceField() Assert.AreNotEqual(0, results[0].CustomDistanceField); Assert.AreNotEqual(0, results[1].CustomDistanceField); - Assert.IsTrue(results[0].CustomDistanceField < results[1].CustomDistanceField); + Assert.IsLessThan(results[0].CustomDistanceField, results[1].CustomDistanceField); Assert.IsNull(results[0].ExtraElements); } @@ -122,23 +122,23 @@ SearchGeoModel[] GetResults(double? maxDistance = null, double? minDistance = nu var results = GetResults(maxDistance: 3000000); Assert.AreEqual(3, results.Count()); - Assert.IsTrue(results.Max(e => e.CustomDistanceField) < 3000000); + Assert.IsLessThan(results.Max(e => e.CustomDistanceField), 3000000); results = GetResults(maxDistance: 600000); Assert.AreEqual(1, results.Count()); - Assert.IsTrue(results.Max(e => e.CustomDistanceField) < 600000); + Assert.IsLessThan(results.Max(e => e.CustomDistanceField), 600000); results = GetResults(maxDistance: 17000000); Assert.AreEqual(4, results.Count()); results = GetResults(minDistance: 600000); Assert.AreEqual(3, results.Count()); - Assert.IsTrue(results.Min(e => e.CustomDistanceField) > 600000); + Assert.IsGreaterThan(results.Min(e => e.CustomDistanceField), 600000); results = GetResults(maxDistance: 3000000, minDistance: 600000); Assert.AreEqual(2, results.Count()); - Assert.IsTrue(results.Max(e => e.CustomDistanceField) < 3000000); - Assert.IsTrue(results.Min(e => e.CustomDistanceField) > 600000); + Assert.IsLessThan(results.Max(e => e.CustomDistanceField), 3000000); + Assert.IsGreaterThan(results.Min(e => e.CustomDistanceField), 600000); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsMultiTenantTests.cs b/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsMultiTenantTests.cs index 39ffa251..4e7d4abb 100644 --- a/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsMultiTenantTests.cs +++ b/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsMultiTenantTests.cs @@ -68,7 +68,7 @@ public async Task ToArrayAsync() var result = await dbSet.ToArrayAsync(); Assert.AreEqual(tenantId, result[0].TenantId); Assert.AreEqual(tenantId, result[3].TenantId); - Assert.AreEqual(4, result.Length); + Assert.HasCount(4, result); Assert.AreEqual("ModelTitle.1", result[0].Title); Assert.AreEqual("ModelTitle.4", result[3].Title); } @@ -82,7 +82,7 @@ public async Task ToListAsync() var result = await dbSet.ToListAsync(); Assert.AreEqual(tenantId, result[0].TenantId); Assert.AreEqual(tenantId, result[3].TenantId); - Assert.AreEqual(4, result.Count); + Assert.HasCount(4, result); Assert.AreEqual("ModelTitle.1", result[0].Title); Assert.AreEqual("ModelTitle.4", result[3].Title); } @@ -101,7 +101,7 @@ public async Task FirstAsync_NoValue() var context2 = new MongoDbTenantContext(connection, tenantId + "-2"); var dbSet2 = new MongoDbTenantSet(context2); - await Assert.ThrowsExceptionAsync(async () => await dbSet2.FirstAsync()); + await Assert.ThrowsAsync(async () => await dbSet2.FirstAsync()); } [TestMethod] public async Task FirstAsync_HasValue() @@ -175,7 +175,7 @@ public async Task SingleAsync_NoValue() var context2 = new MongoDbTenantContext(connection, tenantId + "-2"); var dbSet2 = new MongoDbTenantSet(context2); - await Assert.ThrowsExceptionAsync(async () => await dbSet2.SingleAsync()); + await Assert.ThrowsAsync(async () => await dbSet2.SingleAsync()); } [TestMethod] public async Task SingleAsync_HasValue() @@ -203,7 +203,7 @@ public async Task SingleAsync_HasMoreThanOneValue() var tenantId = TestConfiguration.GetTenantId(); var dbSet = SetupTwoTenantsData(tenantId); - await Assert.ThrowsExceptionAsync(async () => await dbSet.SingleOrDefaultAsync()); + await Assert.ThrowsAsync(async () => await dbSet.SingleOrDefaultAsync()); } [TestMethod] public async Task SingleAsync_WithPredicate() @@ -257,7 +257,7 @@ public async Task SingleOrDefaultAsync_HasMoreThanOneValue() var tenantId = TestConfiguration.GetTenantId(); var dbSet = SetupTwoTenantsData(tenantId); - await Assert.ThrowsExceptionAsync(async () => await dbSet.SingleOrDefaultAsync()); + await Assert.ThrowsAsync(async () => await dbSet.SingleOrDefaultAsync()); } [TestMethod] public async Task SingleOrDefaultAsync_WithPredicate() @@ -319,7 +319,7 @@ public async Task MaxAsync_NoValues() var context2 = new MongoDbTenantContext(connection, tenantId + "-2"); var dbSet2 = new MongoDbTenantSet(context2); - await Assert.ThrowsExceptionAsync(async () => await dbSet2.Select(e => e.IntNumber).MaxAsync()); + await Assert.ThrowsAsync(async () => await dbSet2.Select(e => e.IntNumber).MaxAsync()); } [TestMethod] public async Task MaxAsync_HasValues_Number() @@ -363,7 +363,7 @@ public async Task MinAsync_NoValues() var context2 = new MongoDbTenantContext(connection, tenantId + "-2"); var dbSet2 = new MongoDbTenantSet(context2); - await Assert.ThrowsExceptionAsync(async () => await dbSet2.Select(e => e.IntNumber).MinAsync()); + await Assert.ThrowsAsync(async () => await dbSet2.Select(e => e.IntNumber).MinAsync()); } [TestMethod] public async Task MinAsync_HasValues_Number() diff --git a/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsTests.cs b/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsTests.cs index dc30b86f..c0a61139 100644 --- a/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsTests.cs +++ b/tests/MongoFramework.Tests/Linq/QueryableAsyncExtensionsTests.cs @@ -53,7 +53,7 @@ public async Task ToArrayAsync() context.SaveChanges(); var result = await queryable.ToArrayAsync(); - Assert.AreEqual(1, result.Length); + Assert.HasCount(1, result); Assert.AreEqual("ToArrayAsync", result[0].Title); } @@ -71,7 +71,7 @@ public async Task ToListAsync() context.SaveChanges(); var result = await queryable.ToListAsync(); - Assert.AreEqual(1, result.Count); + Assert.HasCount(1, result); Assert.AreEqual("ToListAsync", result[0].Title); } @@ -84,7 +84,7 @@ public async Task FirstAsync_NoValue() var provider = new MongoFrameworkQueryProvider(connection); var queryable = new MongoFrameworkQueryable(provider); - await Assert.ThrowsExceptionAsync(async () => await queryable.FirstAsync()); + await Assert.ThrowsAsync(async () => await queryable.FirstAsync()); } [TestMethod] public async Task FirstAsync_HasValue() @@ -176,7 +176,7 @@ public async Task SingleAsync_NoValue() var provider = new MongoFrameworkQueryProvider(connection); var queryable = new MongoFrameworkQueryable(provider); - await Assert.ThrowsExceptionAsync(async () => await queryable.SingleAsync()); + await Assert.ThrowsAsync(async () => await queryable.SingleAsync()); } [TestMethod] public async Task SingleAsync_HasValue() @@ -208,7 +208,7 @@ public async Task SingleAsync_HasMoreThanOneValue() context.ChangeTracker.SetEntityState(new QueryableAsyncModel { Title = "SingleAsync_HasMoreThanOneValue.2" }, EntityEntryState.Added); context.SaveChanges(); - await Assert.ThrowsExceptionAsync(async () => await queryable.SingleOrDefaultAsync()); + await Assert.ThrowsAsync(async () => await queryable.SingleOrDefaultAsync()); } [TestMethod] public async Task SingleAsync_WithPredicate() @@ -269,7 +269,7 @@ public async Task SingleOrDefaultAsync_HasMoreThanOneValue() context.ChangeTracker.SetEntityState(new QueryableAsyncModel { Title = "SingleOrDefaultAsync_HasMoreThanOneValue.2" }, EntityEntryState.Added); context.SaveChanges(); - await Assert.ThrowsExceptionAsync(async () => await queryable.SingleOrDefaultAsync()); + await Assert.ThrowsAsync(async () => await queryable.SingleOrDefaultAsync()); } [TestMethod] public async Task SingleOrDefaultAsync_WithPredicate() @@ -351,7 +351,7 @@ public async Task MaxAsync_NoValues() context.SaveChanges(); - await Assert.ThrowsExceptionAsync(async () => await queryable.Select(e => e.IntNumber).MaxAsync()); + await Assert.ThrowsAsync(async () => await queryable.Select(e => e.IntNumber).MaxAsync()); } [TestMethod] public async Task MaxAsync_HasValues_Number() @@ -417,7 +417,7 @@ public async Task MinAsync_NoValues() context.SaveChanges(); - await Assert.ThrowsExceptionAsync(async () => await queryable.Select(e => e.IntNumber).MinAsync()); + await Assert.ThrowsAsync(async () => await queryable.Select(e => e.IntNumber).MinAsync()); } [TestMethod] public async Task MinAsync_HasValues_Number() diff --git a/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs b/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs index 21eca904..2148fccb 100644 --- a/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs +++ b/tests/MongoFramework.Tests/MappingBuilderExtensionTests.cs @@ -230,13 +230,13 @@ public void Ignore_RemovesAllPropertyReferences_Success() }); var testModelDefinition = GetDefinition(); - Assert.AreEqual(1, testModelDefinition.Properties.Count); + Assert.HasCount(1, testModelDefinition.Properties); Assert.AreEqual(typeof(TestModel).GetProperty("OneOfThem"), testModelDefinition.Properties[0].PropertyInfo); Assert.IsTrue(testModelDefinition.ExtraElements.IgnoreExtraElements); - Assert.AreEqual(0, testModelDefinition.Indexes.Count); + Assert.IsEmpty(testModelDefinition.Indexes); var testModelBaseDefinition = GetDefinition(); Assert.IsNull(testModelBaseDefinition.Key); - Assert.AreEqual(0, testModelBaseDefinition.Properties.Count); + Assert.IsEmpty(testModelBaseDefinition.Properties); } } diff --git a/tests/MongoFramework.Tests/MappingBuilderTests.cs b/tests/MongoFramework.Tests/MappingBuilderTests.cs index 0001ea8e..de6cce66 100644 --- a/tests/MongoFramework.Tests/MappingBuilderTests.cs +++ b/tests/MongoFramework.Tests/MappingBuilderTests.cs @@ -93,7 +93,7 @@ public void HasKey_WithProperty_ElementName() [TestMethod] public void HasKey_KeyMustBeDefinedOnDeclaredType() { - Assert.ThrowsException(() => + Assert.Throws(() => SetupMapping(mappingBuilder => { mappingBuilder.Entity() @@ -135,7 +135,7 @@ public void HasProperty_HasElementName_Success() [TestMethod] public void HasProperty_PropertyMustBeDefinedOnDeclaredType() { - Assert.ThrowsException(() => + Assert.Throws(() => SetupMapping(mappingBuilder => { mappingBuilder.Entity() @@ -367,13 +367,13 @@ public void Ignore_RemovesAllPropertyReferences_Success() }); var testModelDefinition = GetDefinition(); - Assert.AreEqual(1, testModelDefinition.Properties.Count); + Assert.HasCount(1, testModelDefinition.Properties); Assert.AreEqual(typeof(TestModel).GetProperty("OneOfThem"), testModelDefinition.Properties[0].PropertyInfo); Assert.IsTrue(testModelDefinition.ExtraElements.IgnoreExtraElements); - Assert.AreEqual(0, testModelDefinition.Indexes.Count); + Assert.IsEmpty(testModelDefinition.Indexes); var testModelBaseDefinition = GetDefinition(); Assert.IsNull(testModelBaseDefinition.Key); - Assert.AreEqual(0, testModelBaseDefinition.Properties.Count); + Assert.IsEmpty(testModelBaseDefinition.Properties); } } diff --git a/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs b/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs index 2d7e6aaa..24187095 100644 --- a/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs +++ b/tests/MongoFramework.Tests/MongoDbBucketSetTests.cs @@ -34,7 +34,7 @@ public void InitialiseDbSet() [TestMethod] public void MustBeSuppliedContext() { - Assert.ThrowsException(() => + Assert.Throws(() => new MongoDbBucketSet(null, new BucketSetOptions { BucketSize = 100, @@ -45,7 +45,7 @@ public void MustBeSuppliedContext() [TestMethod] public void MustBeSuppliedOptions() { - Assert.ThrowsException(() => + Assert.Throws(() => new MongoDbBucketSet(new Mock().Object, null)); } @@ -102,7 +102,7 @@ public async Task SuccessfullyInsertAndQueryBackEntityBucketsAsync() [TestMethod] public void InvalidBucketSize() { - Assert.ThrowsException(() => + Assert.Throws(() => new MongoDbBucketSet(new Mock().Object, new BucketSetOptions { BucketSize = 0, @@ -113,7 +113,7 @@ public void InvalidBucketSize() [TestMethod] public void InvalidSubEntityTimeProperty_Missing() { - Assert.ThrowsException(() => + Assert.Throws(() => new MongoDbBucketSet(new Mock().Object, new BucketSetOptions { BucketSize = 100, @@ -124,7 +124,7 @@ public void InvalidSubEntityTimeProperty_Missing() [TestMethod] public void InvalidSubEntityTimeProperty_WrongType() { - Assert.ThrowsException(() => + Assert.Throws(() => new MongoDbBucketSet(new Mock().Object, new BucketSetOptions { BucketSize = 100, @@ -325,7 +325,7 @@ public void ContinuousSubEntityAccessAcrossBuckets() Name = "Group1" }).ToArray(); - Assert.AreEqual(5, results.Length); + Assert.HasCount(5, results); Assert.AreEqual("Entry1", results[0].Label); Assert.AreEqual("Entry2", results[1].Label); Assert.AreEqual("Entry3", results[2].Label); @@ -391,7 +391,7 @@ public void DistinctGroups() var results = dbSet.Groups().OrderBy(g => g.Name).ToArray(); - Assert.AreEqual(2, results.Length); + Assert.HasCount(2, results); Assert.AreEqual("Group1", results[0].Name); Assert.AreEqual("Group2", results[1].Name); } @@ -444,9 +444,9 @@ public void InvalidAddArguments() EntityTimeProperty = "Date" }); - Assert.ThrowsException(() => dbSet.Add(null, new SubEntityClass())); - Assert.ThrowsException(() => dbSet.AddRange(null, null)); - Assert.ThrowsException(() => dbSet.AddRange(new EntityGroup(), null)); + Assert.Throws(() => dbSet.Add(null, new SubEntityClass())); + Assert.Throws(() => dbSet.AddRange(null, null)); + Assert.Throws(() => dbSet.AddRange(new EntityGroup(), null)); } [TestMethod] @@ -458,7 +458,7 @@ public void InvalidRemoveArguments() EntityTimeProperty = "Date" }); - Assert.ThrowsException(() => dbSet.Remove(null)); + Assert.Throws(() => dbSet.Remove(null)); } [TestMethod] diff --git a/tests/MongoFramework.Tests/MongoDbConnectionTests.cs b/tests/MongoFramework.Tests/MongoDbConnectionTests.cs index 9dd1b964..2d10bc33 100644 --- a/tests/MongoFramework.Tests/MongoDbConnectionTests.cs +++ b/tests/MongoFramework.Tests/MongoDbConnectionTests.cs @@ -16,7 +16,7 @@ public void ConnectionFromConnectionString() [TestMethod] public void NullUrlThrowsException() { - Assert.ThrowsException(() => MongoDbConnection.FromUrl(null)); + Assert.Throws(() => MongoDbConnection.FromUrl(null)); } } } diff --git a/tests/MongoFramework.Tests/MongoDbContextTenantTests.cs b/tests/MongoFramework.Tests/MongoDbContextTenantTests.cs index c8ea63cb..2cfee33d 100644 --- a/tests/MongoFramework.Tests/MongoDbContextTenantTests.cs +++ b/tests/MongoFramework.Tests/MongoDbContextTenantTests.cs @@ -202,7 +202,7 @@ public void AttachRejectsMismatchedEntity() var result = dbSet.AsNoTracking().FirstOrDefault(); result.TenantId = tenantId + "a"; - Assert.ThrowsException(() => context.Attach(result)); + Assert.Throws(() => context.Attach(result)); } [TestMethod] @@ -236,7 +236,7 @@ public void AttachRejectsMismatchedEntities() var result = dbSet.AsNoTracking().ToList(); result[0].TenantId = tenantId + "a"; - Assert.ThrowsException(() => context.AttachRange(result)); + Assert.Throws(() => context.AttachRange(result)); } } diff --git a/tests/MongoFramework.Tests/MongoDbSetTests.cs b/tests/MongoFramework.Tests/MongoDbSetTests.cs index b9948e26..d37d7d64 100644 --- a/tests/MongoFramework.Tests/MongoDbSetTests.cs +++ b/tests/MongoFramework.Tests/MongoDbSetTests.cs @@ -119,7 +119,7 @@ public void FindRequiresId() var context = new MongoDbContext(connection); var dbSet = new MongoDbSet(context); - Assert.ThrowsException(() => dbSet.Find(null)); + Assert.Throws(() => dbSet.Find(null)); } [TestMethod] @@ -190,7 +190,7 @@ public async Task FindAsyncRequiresId() var context = new MongoDbContext(connection); var dbSet = new MongoDbSet(context); - await Assert.ThrowsExceptionAsync(async () => await dbSet.FindAsync(null)); + await Assert.ThrowsAsync(async () => await dbSet.FindAsync(null)); } [TestMethod] diff --git a/tests/MongoFramework.Tests/MongoDbTenantSetTests.cs b/tests/MongoFramework.Tests/MongoDbTenantSetTests.cs index 7fdf5cab..b937e9a9 100644 --- a/tests/MongoFramework.Tests/MongoDbTenantSetTests.cs +++ b/tests/MongoFramework.Tests/MongoDbTenantSetTests.cs @@ -241,7 +241,7 @@ public void FindRequiresId() var context = new MongoDbTenantContext(connection, tenantId); var dbSet = new MongoDbTenantSet(context); - Assert.ThrowsException(() => dbSet.Find(null)); + Assert.Throws(() => dbSet.Find(null)); } [TestMethod] @@ -357,7 +357,7 @@ public async Task FindAsyncRequiresId() var context = new MongoDbTenantContext(connection, tenantId); var dbSet = new MongoDbTenantSet(context); - await Assert.ThrowsExceptionAsync(async () => await dbSet.FindAsync(null)); + await Assert.ThrowsAsync(async () => await dbSet.FindAsync(null)); } [TestMethod] @@ -384,7 +384,7 @@ public void SuccessfullyUpdateEntity() Assert.IsFalse(dbSet.Any(m => m.Description == "SuccessfullyUpdateEntity-Updated")); context.SaveChanges(); Assert.IsTrue(dbSet.Any(m => m.Description == "SuccessfullyUpdateEntity-Updated")); - Assert.IsTrue(dbSet.First(m => m.Description == "SuccessfullyUpdateEntity-Updated").TenantId == tenantId); + Assert.AreEqual(tenantId, dbSet.First(m => m.Description == "SuccessfullyUpdateEntity-Updated").TenantId); } @@ -407,7 +407,7 @@ public void SuccessfullyBlocksUpdateEntity() dbSet = new MongoDbTenantSet(context); entity.TenantId = "qweasd"; entity.Description = "SuccessfullyBlocksUpdateEntity-Updated"; - Assert.ThrowsException(() => dbSet.Update(entity)); + Assert.Throws(() => dbSet.Update(entity)); } @@ -432,7 +432,7 @@ public void SuccessfullyBlocksUpdateChangedEntity() //changing tenant ID after state is updated entity.TenantId = "qweasd"; - Assert.ThrowsException(() => context.SaveChanges()); + Assert.Throws(() => context.SaveChanges()); } @@ -499,7 +499,7 @@ public void SuccessfullyBlocksUpdateRange() entities[1].Description = "SuccessfullyBlocksUpdateRange.2-Updated"; entities[1].TenantId = "qweasd"; - Assert.ThrowsException(() => dbSet.UpdateRange(entities)); + Assert.Throws(() => dbSet.UpdateRange(entities)); } [TestMethod] @@ -547,7 +547,7 @@ public void SuccessfullyBlocksRemoveEntity() dbSet = new MongoDbTenantSet(context); - Assert.ThrowsException(() => dbSet.Remove(entity)); + Assert.Throws(() => dbSet.Remove(entity)); } @@ -611,7 +611,7 @@ public void SuccessfullyBlocksRemoveRange() entities[0].TenantId = "qweasd"; entities[1].TenantId = "qweasd"; - Assert.ThrowsException(() => dbSet.RemoveRange(entities)); + Assert.Throws(() => dbSet.RemoveRange(entities)); } @@ -770,10 +770,10 @@ public void SuccessfullyBlocksNulls() var context = new MongoDbTenantContext(connection, tenantId); var dbSet = new MongoDbTenantSet(context); - Assert.ThrowsException(() => dbSet.Add(null)); - Assert.ThrowsException(() => dbSet.AddRange(null)); - Assert.ThrowsException(() => dbSet.Update(null)); - Assert.ThrowsException(() => dbSet.UpdateRange(null)); + Assert.Throws(() => dbSet.Add(null)); + Assert.Throws(() => dbSet.AddRange(null)); + Assert.Throws(() => dbSet.Update(null)); + Assert.Throws(() => dbSet.UpdateRange(null)); } [TestMethod] @@ -817,7 +817,7 @@ public void BlocksDuplicatesByTenant() context2.SaveChanges(); dbSet.Add(new TestUniqueModel { UserName = "BlocksDuplicatesByTenant" }); - Assert.ThrowsException>(() => context.SaveChanges()); + Assert.Throws>(() => context.SaveChanges()); } [TestMethod] public void SuccessfullyLinqFindTracked() diff --git a/tests/MongoFramework.Tests/MongoFramework.Tests.csproj b/tests/MongoFramework.Tests/MongoFramework.Tests.csproj index 3b726c15..e1413fac 100644 --- a/tests/MongoFramework.Tests/MongoFramework.Tests.csproj +++ b/tests/MongoFramework.Tests/MongoFramework.Tests.csproj @@ -6,11 +6,16 @@ false + + + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs b/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs index 399b8692..88df27ff 100644 --- a/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs +++ b/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs @@ -53,8 +53,8 @@ public void ProfilingInsert() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsTrue(timings[0].CommandString.Contains("InsertOne")); - Assert.IsTrue(timings[0].CommandString.Contains("ProfilingInsert")); + StringAssert.Contains(timings[0].CommandString, "InsertOne"); + StringAssert.Contains(timings[0].CommandString, "ProfilingInsert"); } } @@ -77,8 +77,8 @@ public void ProfilingUpdate() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsTrue(timings[0].CommandString.Contains("UpdateOne")); - Assert.IsTrue(timings[0].CommandString.Contains("ProfilingUpdate-Updated")); + StringAssert.Contains(timings[0].CommandString, "UpdateOne"); + StringAssert.Contains(timings[0].CommandString, "ProfilingUpdate-Updated"); } } @@ -101,8 +101,8 @@ public void ProfilingDelete() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsTrue(timings[0].CommandString.Contains("DeleteOne")); - Assert.IsTrue(timings[0].CommandString.Contains(entity.Id)); + StringAssert.Contains(timings[0].CommandString, "DeleteOne"); + StringAssert.Contains(timings[0].CommandString, entity.Id); } } @@ -130,8 +130,8 @@ public void ProfilingRead() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsTrue(timings[0].CommandString.Contains("$skip")); - Assert.IsTrue(timings[0].CommandString.Contains("78")); + StringAssert.Contains(timings[0].CommandString, "$skip"); + StringAssert.Contains(timings[0].CommandString, "78"); } } @@ -161,7 +161,7 @@ public void ProfilingReadWithEnforcedSleep() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsTrue(timings[0].DurationMilliseconds > 1000); + Assert.IsGreaterThan(timings[0].DurationMilliseconds.GetValueOrDefault(), 1000); } } @@ -178,8 +178,8 @@ public void ProfilingIndex() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsTrue(timings[0].CommandString.Contains("TestIndex")); - Assert.IsTrue(timings[0].CommandString.Contains("IndexSpecificDescriptionField")); + StringAssert.Contains(timings[0].CommandString, "TestIndex"); + StringAssert.Contains(timings[0].CommandString, "IndexSpecificDescriptionField"); } } } diff --git a/tests/MongoFramework.Tests/Utilities/CheckTests.cs b/tests/MongoFramework.Tests/Utilities/CheckTests.cs index 4f7f904b..1dd1a800 100644 --- a/tests/MongoFramework.Tests/Utilities/CheckTests.cs +++ b/tests/MongoFramework.Tests/Utilities/CheckTests.cs @@ -14,49 +14,49 @@ public class CheckTest [TestMethod] public void Not_null_throws_when_arg_is_null() { - Assert.ThrowsException(() => Check.NotNull(null, "foo")); + Assert.Throws(() => Check.NotNull(null, "foo")); } [TestMethod] public void Not_null_throws_when_arg_name_empty() { - Assert.ThrowsException(() => Check.NotNull(null as object, string.Empty)); + Assert.Throws(() => Check.NotNull(null as object, string.Empty)); } [TestMethod] public void Not_empty_throws_when_empty() { - Assert.ThrowsException(() => Check.NotEmpty("", string.Empty)); + Assert.Throws(() => Check.NotEmpty("", string.Empty)); } [TestMethod] public void Not_empty_throws_when_whitespace() { - Assert.ThrowsException(() => Check.NotEmpty(" ", string.Empty)); + Assert.Throws(() => Check.NotEmpty(" ", string.Empty)); } [TestMethod] public void Not_empty_throws_when_parameter_name_null() { - Assert.ThrowsException(() => Check.NotEmpty(null, null)); + Assert.Throws(() => Check.NotEmpty(null, null)); } [TestMethod] public void Generic_Not_empty_throws_when_arg_is_empty() { - Assert.ThrowsException(() => Check.NotEmpty(Array.Empty(), "foo")); + Assert.Throws(() => Check.NotEmpty(Array.Empty(), "foo")); } [TestMethod] public void Generic_Not_empty_throws_when_arg_is_null() { - Assert.ThrowsException(() => Check.NotEmpty(null, "foo")); + Assert.Throws(() => Check.NotEmpty(null, "foo")); } [TestMethod] public void Generic_Not_empty_throws_when_arg_name_empty() { - Assert.ThrowsException(() => Check.NotEmpty(null, string.Empty)); + Assert.Throws(() => Check.NotEmpty(null, string.Empty)); } } } From 45514bc34e3776d26bcfdbb3ec3494373cfdf49e Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 12:14:54 -0500 Subject: [PATCH 4/8] fix typos --- .../Linq/LinqExtensions_SearchGeoTests.cs | 12 ++++++------ .../MiniProfilerDiagnosticListenerTests.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs b/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs index fed4104d..785f6603 100644 --- a/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs +++ b/tests/MongoFramework.Tests/Linq/LinqExtensions_SearchGeoTests.cs @@ -84,7 +84,7 @@ public void SearchGeoNearWithCustomDistanceField() Assert.AreNotEqual(0, results[0].CustomDistanceField); Assert.AreNotEqual(0, results[1].CustomDistanceField); - Assert.IsLessThan(results[0].CustomDistanceField, results[1].CustomDistanceField); + Assert.IsLessThan(results[1].CustomDistanceField, results[0].CustomDistanceField); Assert.IsNull(results[0].ExtraElements); } @@ -122,23 +122,23 @@ SearchGeoModel[] GetResults(double? maxDistance = null, double? minDistance = nu var results = GetResults(maxDistance: 3000000); Assert.AreEqual(3, results.Count()); - Assert.IsLessThan(results.Max(e => e.CustomDistanceField), 3000000); + Assert.IsLessThan(3000000, results.Max(e => e.CustomDistanceField)); results = GetResults(maxDistance: 600000); Assert.AreEqual(1, results.Count()); - Assert.IsLessThan(results.Max(e => e.CustomDistanceField), 600000); + Assert.IsLessThan(600000, results.Max(e => e.CustomDistanceField)); results = GetResults(maxDistance: 17000000); Assert.AreEqual(4, results.Count()); results = GetResults(minDistance: 600000); Assert.AreEqual(3, results.Count()); - Assert.IsGreaterThan(results.Min(e => e.CustomDistanceField), 600000); + Assert.IsGreaterThan(600000, results.Min(e => e.CustomDistanceField)); results = GetResults(maxDistance: 3000000, minDistance: 600000); Assert.AreEqual(2, results.Count()); - Assert.IsLessThan(results.Max(e => e.CustomDistanceField), 3000000); - Assert.IsGreaterThan(results.Min(e => e.CustomDistanceField), 600000); + Assert.IsLessThan(3000000, results.Max(e => e.CustomDistanceField)); + Assert.IsGreaterThan(600000, results.Min(e => e.CustomDistanceField)); } [TestMethod] diff --git a/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs b/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs index 88df27ff..cc6687c1 100644 --- a/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs +++ b/tests/MongoFramework.Tests/Profiling/MiniProfiler/MiniProfilerDiagnosticListenerTests.cs @@ -161,7 +161,7 @@ public void ProfilingReadWithEnforcedSleep() Assert.IsTrue(profiler.Root.CustomTimings.ContainsKey("mongodb")); var timings = profiler.Root.CustomTimings["mongodb"]; - Assert.IsGreaterThan(timings[0].DurationMilliseconds.GetValueOrDefault(), 1000); + Assert.IsGreaterThan(1000, timings[0].DurationMilliseconds.GetValueOrDefault()); } } From c2939a508e3e811807130bd27d54f9b09ab1b4c1 Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 15:16:34 -0500 Subject: [PATCH 5/8] update to support newest MongoDB driver --- docs/MONGODB_DRIVER_3_MIGRATION.md | 97 ++++ .../MiniProfilerDiagnosticListener.cs | 13 +- .../Infrastructure/DriverAbstractionRules.cs | 5 + .../Linq/MongoFrameworkQueryProvider.cs | 520 ++++++++++++++++-- src/MongoFramework/Linq/LinqExtensions.cs | 4 +- src/MongoFramework/MongoDbConnection.cs | 3 +- src/MongoFramework/MongoFramework.csproj | 2 +- .../UpdateDefinitionHelperTests.cs | 2 +- .../Indexing/IndexModelBuilderTests.cs | 25 +- .../Serialization/DateOnlyTimeOnlyTests.cs | 366 ++++++++++++ 10 files changed, 985 insertions(+), 52 deletions(-) create mode 100644 docs/MONGODB_DRIVER_3_MIGRATION.md create mode 100644 tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs diff --git a/docs/MONGODB_DRIVER_3_MIGRATION.md b/docs/MONGODB_DRIVER_3_MIGRATION.md new file mode 100644 index 00000000..3aebfbde --- /dev/null +++ b/docs/MONGODB_DRIVER_3_MIGRATION.md @@ -0,0 +1,97 @@ +# MongoDB.Driver 3.x Migration Guide + +This document describes the changes when upgrading MongoFramework to use MongoDB.Driver 3.6.0 (from 2.21.0). + +## Overview + +MongoDB.Driver 3.0 removes the LINQ2 provider entirely and makes LINQ3 the only option. MongoFramework has been updated to work seamlessly with LINQ3. + +## LINQ3 Compatibility + +All common LINQ patterns work correctly with LINQ3: + +| Pattern | Example | Status | +|---------|---------|--------| +| `Any()` with predicate | `dbSet.Any(x => x.Name == "test")` | **Supported** | +| `FirstOrDefault()` with predicate | `dbSet.FirstOrDefault(x => x.Id == id)` | **Supported** | +| `First()` with predicate | `dbSet.First(x => x.Id == id)` | **Supported** | +| `SingleOrDefault()` with predicate | `dbSet.SingleOrDefault(x => x.Id == id)` | **Supported** | +| `Single()` with predicate | `dbSet.Single(x => x.Id == id)` | **Supported** | +| `Where().FirstOrDefault()` | `dbSet.Where(x => x.Id == id).FirstOrDefault()` | **Supported** | +| `Count()` with predicate | `dbSet.Count(x => x.Active)` | **Supported** | + +## Breaking Changes + +### 1. DateTime Handling + +MongoDB.Driver 3.x returns `DateTime` values in UTC. For consistent round-tripping, use UTC explicitly: + +```csharp +// Recommended: Use UTC for dates +var date = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); +``` + +Dates stored with `DateTimeKind.Unspecified` will be treated as local time, converted to UTC for storage, and returned as UTC. + +### 2. GUID Serialization + +MongoDB.Driver 3.x requires explicit `GuidRepresentation`. MongoFramework now configures `GuidRepresentation.Standard` (UUID binary subtype 4) by default for cross-platform compatibility. + +If you were using a different GUID representation, you may need to migrate existing data or configure a different serializer. + +### 3. Render() Method Signature (Internal) + +If you use MongoDB driver APIs directly, the `Render()` method on definition builders now requires `RenderArgs`: + +```csharp +// Before +definition.Render(serializer, registry); + +// After +var renderArgs = new RenderArgs(serializer, registry); +definition.Render(renderArgs); +``` + +### 4. IMongoClient is now IDisposable + +`MongoClient` now implements `IDisposable`. MongoFramework's `MongoDbConnection.Dispose()` properly disposes the underlying client. + +### 5. Index Options + +The `Background` property on `CreateIndexOptions` is deprecated (since MongoDB 4.2). Index builds are now automatically optimized by the server. + +## Migration Steps + +1. Update the MongoFramework NuGet package to the version using MongoDB.Driver 3.6.0 +2. Review any code that uses `DateTime` values - ensure UTC is used for consistent behavior +3. Run your test suite - most LINQ queries should work without changes +4. Review any direct MongoDB driver API usage for `Render()` signature changes + +## Troubleshooting + +### ExpressionNotSupportedException + +If you encounter `ExpressionNotSupportedException`, it indicates a query pattern that LINQ3 cannot translate to the aggregation pipeline. Common solutions: + +1. **Simplify the expression** - Break complex queries into simpler steps +2. **Use supported alternatives** - Some projections may need adjustment +3. **Fetch and filter in-memory** - For edge cases, fetch results first then filter + +Example: +```csharp +// If a complex expression fails, try fetching first +var results = dbSet.Where(x => x.Category == "Active").ToArray(); +var filtered = results.Where(x => ComplexCondition(x)); +``` + +### DateTime Mismatch + +If dates appear shifted by your timezone offset: +- Ensure you're using `DateTimeKind.Utc` when creating dates +- Consider using `DateTimeOffset` for timezone-aware timestamps + +## Resources + +- [MongoDB Driver Upgrade Guide](https://www.mongodb.com/docs/drivers/csharp/current/reference/upgrade/v3/) +- [LINQ3 Documentation](https://www.mongodb.com/docs/drivers/csharp/current/aggregation/linq/) +- [Breaking Changes in v3.0](https://www.mongodb.com/docs/drivers/csharp/v3.0/reference/release-notes/) diff --git a/src/MongoFramework.Profiling.MiniProfiler/MiniProfilerDiagnosticListener.cs b/src/MongoFramework.Profiling.MiniProfiler/MiniProfilerDiagnosticListener.cs index 7daeab00..289bbcb4 100644 --- a/src/MongoFramework.Profiling.MiniProfiler/MiniProfilerDiagnosticListener.cs +++ b/src/MongoFramework.Profiling.MiniProfiler/MiniProfilerDiagnosticListener.cs @@ -93,18 +93,20 @@ private string GetWriteModelAsString(WriteModel writeModel) else if (writeModel is UpdateOneModel updateModel) { var serializer = BsonSerializer.LookupSerializer(); + var renderArgs = new RenderArgs(serializer, BsonSerializer.SerializerRegistry); return new BsonDocument { - { "Filter", updateModel.Filter.Render(serializer, BsonSerializer.SerializerRegistry) }, - { "Update", updateModel.Update.Render(serializer, BsonSerializer.SerializerRegistry) } + { "Filter", updateModel.Filter.Render(renderArgs) }, + { "Update", updateModel.Update.Render(renderArgs) } }.ToString(); } else if (writeModel is DeleteOneModel deleteModel) { var serializer = BsonSerializer.LookupSerializer(); + var renderArgs = new RenderArgs(serializer, BsonSerializer.SerializerRegistry); return new BsonDocument { - { "Filter", deleteModel.Filter.Render(serializer, BsonSerializer.SerializerRegistry) } + { "Filter", deleteModel.Filter.Render(renderArgs) } }.ToString(); } else @@ -129,11 +131,12 @@ private void OnNextIndexCommand(IndexDiagnosticCommand command private string GetIndexModelAsString(CreateIndexModel indexModel) { var serializer = BsonSerializer.LookupSerializer(); + var renderArgs = new RenderArgs(serializer, BsonSerializer.SerializerRegistry); var indexOptions = indexModel.Options; return new BsonDocument { - { "Keys", indexModel.Keys.Render(serializer, BsonSerializer.SerializerRegistry) }, - { "Options", new { indexOptions.Name, indexOptions.Unique, indexOptions.Background }.ToBsonDocument() } + { "Keys", indexModel.Keys.Render(renderArgs) }, + { "Options", new { indexOptions.Name, indexOptions.Unique }.ToBsonDocument() } }.ToString(); } } diff --git a/src/MongoFramework/Infrastructure/DriverAbstractionRules.cs b/src/MongoFramework/Infrastructure/DriverAbstractionRules.cs index e5ea0623..93db276d 100644 --- a/src/MongoFramework/Infrastructure/DriverAbstractionRules.cs +++ b/src/MongoFramework/Infrastructure/DriverAbstractionRules.cs @@ -17,6 +17,11 @@ public static void ApplyRules() RegisterSerializer(new DecimalSerializer(BsonType.Decimal128)); RegisterSerializer(new NullableSerializer(new DecimalSerializer(BsonType.Decimal128))); + // MongoDB.Driver 3.x requires explicit GuidRepresentation (default is Unspecified which throws) + // Use Standard (UUID binary subtype 4) for cross-platform compatibility + RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); + RegisterSerializer(new NullableSerializer(new GuidSerializer(GuidRepresentation.Standard))); + BsonSerializer.RegisterSerializationProvider(TypeDiscoverySerializationProvider.Instance); } diff --git a/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs b/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs index 65841d71..23c6aff5 100644 --- a/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs +++ b/src/MongoFramework/Infrastructure/Linq/MongoFrameworkQueryProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -39,7 +39,9 @@ public MongoFrameworkQueryProvider(IMongoFrameworkQueryProvider provide public Expression GetBaseExpression() { var collection = GetCollection(); - return Expression.Constant(collection.AsQueryable(), typeof(IMongoQueryable)); + var queryable = collection.AsQueryable(); + // Use the actual queryable type so the driver can recognize it + return Expression.Constant(queryable, queryable.GetType()); } public IQueryable CreateQuery(Expression expression) @@ -52,7 +54,100 @@ public IQueryable CreateQuery(Expression expression) return new MongoFrameworkQueryable(this, expression); } + /// + /// Returns true if the connection has a diagnostic listener that requires aggregation-based execution + /// to properly capture query profiling. + /// + private bool HasDiagnosticsEnabled() + { + return Connection.DiagnosticListener != null && !(Connection.DiagnosticListener is NoOpDiagnosticListener); + } + public object Execute(Expression expression) + { + // If there's a PreStage (e.g., for $geoNear, multi-tenant filtering), we must use + // aggregation-based execution to properly apply it. + // Also use aggregation path when diagnostics are enabled to properly capture query profiling. + if (PreStage != null || HasDiagnosticsEnabled()) + { + return ExecuteViaAggregation(expression); + } + + // In MongoDB.Driver 3.x, extract the driver's queryable and execute through it + var queryableAndProvider = ExtractQueryableFromExpression(expression); + + if (queryableAndProvider.queryable != null) + { + var driverQueryable = queryableAndProvider.queryable; + var driverProvider = queryableAndProvider.provider; + + // If the expression is just a ConstantExpression (the base queryable), + // enumerate the queryable directly + if (expression is ConstantExpression) + { + return EnumerateAndProcess(driverQueryable); + } + + // Rebase the expression to use the driver's queryable (handles MongoFrameworkQueryable wrappers) + var rebasedExpression = RebaseExpression(expression, driverQueryable); + + // Execute through the driver's provider directly + var resultType = expression.Type; + var executeMethod = driverProvider.GetType() + .GetMethods() + .FirstOrDefault(m => m.Name == "Execute" && m.IsGenericMethodDefinition); + + if (executeMethod != null) + { + try + { + var genericExecute = executeMethod.MakeGenericMethod(resultType); + var result = genericExecute.Invoke(driverProvider, new object[] { rebasedExpression }); + + // Process entities if result is a single entity + if (result is TEntity entity) + { + EntityProcessors.ProcessEntity(entity, Connection); + } + else if (result is IEnumerable entities) + { + // Materialize and process + var list = new List(); + foreach (var e in entities) + { + EntityProcessors.ProcessEntity(e, Connection); + list.Add(e); + } + return list; + } + + return result; + } + catch (TargetInvocationException ex) + { + var innerEx = ex.InnerException; + // If the inner exception is a business logic exception, throw it + // If it's an expression translation exception, fall back to aggregation + if (innerEx is InvalidOperationException || + innerEx is ArgumentException || + innerEx is ArgumentNullException) + { + throw innerEx; + } + // For other exceptions (expression not supported, etc.), fall back + } + } + } + + // Fallback: Use aggregation-based execution + return ExecuteViaAggregation(expression); + } + + /// + /// Executes the expression using aggregation pipeline. Required when PreStage is set + /// or when direct driver delegation fails. + /// + private object ExecuteViaAggregation(Expression expression) { var model = GetExecutionModel(expression); var outputType = model.Serializer.ValueType; @@ -80,18 +175,124 @@ public object Execute(Expression expression) } } + /// + /// Replaces the source ConstantExpression in an expression tree with the driver queryable's own expression. + /// + private static Expression RebaseExpression(Expression expression, IQueryable driverQueryable) + { + return new QueryableExpressionReplacer(driverQueryable.Expression).Visit(expression); + } + + private class QueryableExpressionReplacer : System.Linq.Expressions.ExpressionVisitor + { + private readonly Expression _replacement; + + public QueryableExpressionReplacer(Expression replacement) + { + _replacement = replacement; + } + + protected override Expression VisitConstant(ConstantExpression node) + { + // Check if this is a queryable constant + if (node.Value != null) + { + var valueType = node.Value.GetType(); + var providerProperty = valueType.GetProperty("Provider"); + if (providerProperty != null) + { + // This is a queryable, replace it with the driver's expression + return _replacement; + } + } + return base.VisitConstant(node); + } + } + public TResult Execute(Expression expression) { return (TResult)Execute(expression); } public object ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) + { + // If there's a PreStage (e.g., for $geoNear, multi-tenant filtering), we must use + // aggregation-based execution to properly apply it. + // Also use aggregation path when diagnostics are enabled to properly capture query profiling. + if (PreStage != null || HasDiagnosticsEnabled()) + { + return ExecuteAsyncViaAggregation(expression, cancellationToken); + } + + // In MongoDB.Driver 3.x, delegate to the driver's provider for async execution + var queryableAndProvider = ExtractQueryableFromExpression(expression); + + if (queryableAndProvider.queryable != null) + { + var driverQueryable = queryableAndProvider.queryable; + var driverProvider = queryableAndProvider.provider; + + // If the expression is just a ConstantExpression (the base queryable), + // return an async enumerable that enumerates the driver's queryable + if (expression is ConstantExpression) + { + return EnumerateAndProcessAsync(driverQueryable, cancellationToken); + } + + // Rebase the expression to use the driver's queryable + var rebasedExpression = RebaseExpression(expression, driverQueryable); + + // Execute through the driver's provider directly + var resultType = expression.Type; + var executeMethod = driverProvider.GetType() + .GetMethods() + .FirstOrDefault(m => m.Name == "Execute" && m.IsGenericMethodDefinition); + + if (executeMethod != null) + { + try + { + var genericExecute = executeMethod.MakeGenericMethod(resultType); + var syncResult = genericExecute.Invoke(driverProvider, new object[] { rebasedExpression }); + + // Process entities + if (syncResult is TEntity entity) + { + EntityProcessors.ProcessEntity(entity, Connection); + } + + // Return as ValueTask + return CreateValueTask(syncResult, resultType); + } + catch (TargetInvocationException ex) + { + var innerEx = ex.InnerException; + // If the inner exception is a business logic exception, throw it + // If it's an expression translation exception, fall back to aggregation + if (innerEx is InvalidOperationException || + innerEx is ArgumentException || + innerEx is ArgumentNullException) + { + throw innerEx; + } + // For other exceptions (expression not supported, etc.), fall back + } + } + } + + // Fallback to aggregation-based execution + return ExecuteAsyncViaAggregation(expression, cancellationToken); + } + + /// + /// Executes the expression asynchronously using aggregation pipeline. Required when PreStage is set + /// or when direct driver delegation fails. + /// + private object ExecuteAsyncViaAggregation(Expression expression, CancellationToken cancellationToken) { var model = GetExecutionModel(expression, true); var outputType = model.Serializer.ValueType; - //aka. ExecuteModelAsync(model, cancellationToken) - Expression executor = Expression.Call( Expression.Constant(this), nameof(ExecuteModelAsync), @@ -119,48 +320,299 @@ private IMongoCollection GetCollection() private AggregateExecutionModel GetExecutionModel(Expression expression, bool isAsync = false) { - //Use the official driver to do the heavy lifting on the query translation - var underlyingProvider = GetCollection().AsQueryable().Provider; - var providerType = underlyingProvider.GetType(); //Type: MongoQueryProviderImpl (internal) - var translatedQuery = providerType.GetMethod("Translate", BindingFlags.NonPublic | BindingFlags.Instance) - .Invoke(underlyingProvider, new[] { expression }); //Type: QueryableTranslation (internal) - var translatedQueryType = translatedQuery.GetType(); - - //We can't cast to AggregateQueryableExecutionModel directly as we don't have generic parameter T - //While it may be TEntity, it could also be something else - var underlyingExecutionModel = translatedQueryType.GetProperty("Model").GetValue(translatedQuery) as QueryableExecutionModel; - var modelType = underlyingExecutionModel.GetType(); //Assumed type: AggregateQueryableExecutionModel - - //Retrieve the stages from reflection - var expressionStages = modelType.GetProperty(nameof(AggregateQueryableExecutionModel.Stages)) - .GetValue(underlyingExecutionModel) as IEnumerable; - - //Retreve the serializer from reflection - var serializer = modelType.GetProperty(nameof(AggregateQueryableExecutionModel.OutputSerializer)) - .GetValue(underlyingExecutionModel) as IBsonSerializer; + // MongoDB.Driver 3.x: Use LINQ3 translation path + // Extract the underlying queryable and provider from the expression's source + var (driverQueryable, underlyingProvider) = ExtractQueryableFromExpression(expression); + if (underlyingProvider == null) + { + driverQueryable = GetCollection().AsQueryable(); + underlyingProvider = driverQueryable.Provider; + } + + // Rebase the expression to use the driver's queryable before translation + // This is critical - the driver's translator won't recognize MongoFrameworkQueryable + var rebasedExpression = RebaseExpression(expression, driverQueryable); + + // Use reflection to access the internal TranslateAndGetExecutionModel method + // which handles the expression translation properly + var providerType = underlyingProvider.GetType(); + + // Try to find and use the TranslateExpressionToAggregateQueryPipeline method or similar + // This is a more direct way to get the pipeline stages from the driver + var driverAssembly = typeof(MongoClient).Assembly; + + // Get the Translate method from the provider itself (if available in 3.x) + var translateMethod = providerType.GetMethod("Translate", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + if (translateMethod != null) + { + // Driver 3.x might still have a Translate method on the provider + var translatedQuery = translateMethod.Invoke(underlyingProvider, new[] { rebasedExpression }); + var translatedQueryType = translatedQuery.GetType(); + + // Try to get the execution model or pipeline from the translated query + var modelProperty = translatedQueryType.GetProperty("Model"); + if (modelProperty != null) + { + var executionModel = modelProperty.GetValue(translatedQuery); + var modelType = executionModel.GetType(); + + var stagesProperty = modelType.GetProperty("Stages"); + var serializerProperty = modelType.GetProperty("OutputSerializer"); + + var expressionStages = stagesProperty?.GetValue(executionModel) as IEnumerable + ?? Array.Empty(); + var serializer = serializerProperty?.GetValue(executionModel) as IBsonSerializer; + + if (PreStage != null) + { + expressionStages = new[] { PreStage }.Concat(expressionStages); + } + + var result = new AggregateExecutionModel + { + Stages = expressionStages, + Serializer = serializer + }; + + // Get result transformer + var resultTransformerProperty = translatedQueryType.GetProperty("ResultTransformer"); + var resultTransformer = resultTransformerProperty?.GetValue(translatedQuery); + if (resultTransformer != null) + { + result.ResultTransformer = ResultTransformers.Transform(expression, serializer.ValueType, isAsync) as LambdaExpression; + } + + return result; + } + } + + // Fallback: Use the static ExpressionToExecutableQueryTranslator.Translate method + var translatorType = driverAssembly.GetType( + "MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators.ExpressionToExecutableQueryTranslator"); + + if (translatorType == null) + { + throw new InvalidOperationException("Could not find ExpressionToExecutableQueryTranslator type. Ensure you are using MongoDB.Driver 3.x or later."); + } + + var getTranslationOptionsMethod = providerType.GetMethod("GetTranslationOptions", BindingFlags.NonPublic | BindingFlags.Instance); + var translationOptions = getTranslationOptionsMethod?.Invoke(underlyingProvider, null); + + var resultType = GetResultType(expression); + + var translateMethods = translatorType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) + .Where(m => m.Name == "Translate" && m.IsGenericMethodDefinition); + var translateMethodDefinition = translateMethods.FirstOrDefault(); + + if (translateMethodDefinition == null) + { + throw new InvalidOperationException("Could not find Translate method on ExpressionToExecutableQueryTranslator"); + } + + var genericTranslateMethod = translateMethodDefinition.MakeGenericMethod(typeof(TEntity), resultType); + var executableQuery = genericTranslateMethod.Invoke(null, new[] { underlyingProvider, rebasedExpression, translationOptions }); + var executableQueryType = executableQuery.GetType(); + + var pipelineProperty = executableQueryType.GetProperty("Pipeline", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var pipeline = pipelineProperty.GetValue(executableQuery); + var pipelineType = pipeline.GetType(); + + var outputSerializerProperty = pipelineType.GetProperty("OutputSerializer", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var serializer2 = outputSerializerProperty.GetValue(pipeline) as IBsonSerializer; + + var astProperty = pipelineType.GetProperty("Ast", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + var ast = astProperty.GetValue(pipeline); + var astType = ast.GetType(); + + var renderMethod = astType.GetMethod("Render", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null); + var renderedPipeline = renderMethod.Invoke(ast, null) as BsonValue; + + IEnumerable expressionStages2; + if (renderedPipeline is BsonArray bsonArray) + { + expressionStages2 = bsonArray.Select(v => v.AsBsonDocument).ToArray(); + } + else + { + expressionStages2 = Array.Empty(); + } if (PreStage != null) { - expressionStages = new[] { PreStage }.Concat(expressionStages); + expressionStages2 = new[] { PreStage }.Concat(expressionStages2); } - var result = new AggregateExecutionModel + var result2 = new AggregateExecutionModel { - Stages = expressionStages, - Serializer = serializer + Stages = expressionStages2, + Serializer = serializer2 }; - //Get the result transforming lambda (allows things like FirstOrDefault, Count, Average etc to work properly) - var resultTransformer = translatedQueryType.GetProperty("ResultTransformer").GetValue(translatedQuery); //Type: Mixed (implements IResultTransformer (internal)) - if (resultTransformer != null) + // Determine if this is a scalar query (First, Count, Any, etc.) + // In LINQ3, we detect this by checking if the expression is a MethodCallExpression with a scalar method + if (expression is MethodCallExpression methodCall) + { + var methodName = methodCall.Method.Name; + var scalarMethods = new[] { "First", "FirstOrDefault", "Single", "SingleOrDefault", "Last", "LastOrDefault", + "Count", "LongCount", "Any", "All", "Sum", "Average", "Min", "Max", "ElementAt", "ElementAtOrDefault" }; + + if (scalarMethods.Contains(methodName)) + { + result2.ResultTransformer = ResultTransformers.Transform(expression, serializer2.ValueType, isAsync) as LambdaExpression; + } + } + + return result2; + } + + /// + /// Determines the result element type from the expression. + /// For IQueryable<T> expressions, returns T. + /// For scalar methods like Count(), returns the appropriate type. + /// + private static Type GetResultType(Expression expression) + { + var expressionType = expression.Type; + + // Check if it's IQueryable or IEnumerable + if (expressionType.IsGenericType) + { + var genericDef = expressionType.GetGenericTypeDefinition(); + if (genericDef == typeof(IQueryable<>) || genericDef == typeof(IEnumerable<>) || genericDef == typeof(IOrderedQueryable<>)) + { + return expressionType.GetGenericArguments()[0]; + } + } + + // For scalar results (Count, Sum, etc.), the type is the expression's type itself + // but we need to find the element type of the source + if (expression is MethodCallExpression methodCall && methodCall.Arguments.Count > 0) + { + // Get the source type from the first argument + var sourceType = methodCall.Arguments[0].Type; + if (sourceType.IsGenericType) + { + var sourceGenericDef = sourceType.GetGenericTypeDefinition(); + if (sourceGenericDef == typeof(IQueryable<>) || sourceGenericDef == typeof(IEnumerable<>) || sourceGenericDef == typeof(IOrderedQueryable<>)) + { + return sourceType.GetGenericArguments()[0]; + } + } + } + + // Fallback to the expression type + return expressionType; + } + + /// + /// Extracts the underlying MongoDB driver's IQueryable and IQueryProvider from an expression tree. + /// This method unwraps MongoFrameworkQueryable to find the actual driver queryable. + /// + private static (IQueryable queryable, IQueryProvider provider) ExtractQueryableFromExpression(Expression expression) + { + // Walk down the expression tree to find the root ConstantExpression + var current = expression; + while (current != null) { - result.ResultTransformer = ResultTransformers.Transform(expression, serializer.ValueType, isAsync) as LambdaExpression; + if (current is ConstantExpression constant) + { + var value = constant.Value; + if (value != null) + { + // Check if this is a MongoFrameworkQueryable - if so, dig into its expression + // to find the actual driver queryable + if (value is IMongoFrameworkQueryable frameworkQueryable) + { + // Recursively extract from the framework queryable's expression + // which should contain the driver's queryable + return ExtractQueryableFromExpression(frameworkQueryable.Expression); + } + + // Check if this is the driver's queryable (IMongoQueryable or similar) + if (value is IQueryable queryable) + { + // Make sure it's not another MongoFrameworkQueryable + if (!(queryable.Provider is IMongoFrameworkQueryProvider)) + { + return (queryable, queryable.Provider); + } + } + } + } - //Note: In the future this can change from the initial reflection to a `TryTransform` function where it checks the expression itself - // The reason we are doing this method first is to weed out the bugs and any core missing functionality. + if (current is MethodCallExpression methodCall && methodCall.Arguments.Count > 0) + { + // The first argument of Queryable extension methods is the source + current = methodCall.Arguments[0]; + } + else + { + break; + } } - return result; + return (null, null); + } + + /// + /// Creates a ValueTask<T> with the given result value, handling null values correctly. + /// + private static object CreateValueTask(object result, Type resultType) + { + // Use a generic method to properly create the ValueTask + var method = typeof(MongoFrameworkQueryProvider) + .GetMethod(nameof(CreateValueTaskGeneric), BindingFlags.NonPublic | BindingFlags.Static) + .MakeGenericMethod(resultType); + return method.Invoke(null, new[] { result }); + } + + private static ValueTask CreateValueTaskGeneric(object result) + { + return new ValueTask((T)result); + } + + /// + /// Legacy method for compatibility - extracts just the provider. + /// + private static IQueryProvider ExtractProviderFromExpression(Expression expression) + { + return ExtractQueryableFromExpression(expression).provider; + } + + /// + /// Enumerates the driver's queryable and processes each entity. + /// Used when the expression is just the base queryable (not a method call). + /// + private IEnumerable EnumerateAndProcess(IQueryable driverQueryable) + { + foreach (var item in driverQueryable) + { + if (item is TEntity entity) + { + EntityProcessors.ProcessEntity(entity, Connection); + yield return entity; + } + } + } + + /// + /// Async version of EnumerateAndProcess. + /// + private async IAsyncEnumerable EnumerateAndProcessAsync(IQueryable driverQueryable, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // MongoDB driver's IMongoQueryable implements IAsyncCursorSource, but for simplicity + // we use sync enumeration here (wrapped in async). For true async, we'd need to use + // the driver's ToCursorAsync method. + foreach (var item in driverQueryable) + { + cancellationToken.ThrowIfCancellationRequested(); + if (item is TEntity entity) + { + EntityProcessors.ProcessEntity(entity, Connection); + yield return entity; + } + } } private IEnumerable ExecuteModel(AggregateExecutionModel model) diff --git a/src/MongoFramework/Linq/LinqExtensions.cs b/src/MongoFramework/Linq/LinqExtensions.cs index 549c0b0d..3e2e83f9 100644 --- a/src/MongoFramework/Linq/LinqExtensions.cs +++ b/src/MongoFramework/Linq/LinqExtensions.cs @@ -76,13 +76,13 @@ public static IQueryable SearchGeoNear(this IMon { var entitySerializer = BsonSerializer.LookupSerializer(); var keyExpressionField = new ExpressionFieldDefinition(targetField); - var keyStringField = keyExpressionField.Render(entitySerializer, BsonSerializer.SerializerRegistry); + var keyStringField = keyExpressionField.Render(new RenderArgs(entitySerializer, BsonSerializer.SerializerRegistry)); var distanceFieldName = "Distance"; if (distanceResultField != null) { var distanceResultExpressionField = new ExpressionFieldDefinition(distanceResultField); - var distanceResultStringField = distanceResultExpressionField.Render(entitySerializer, BsonSerializer.SerializerRegistry); + var distanceResultStringField = distanceResultExpressionField.Render(new RenderArgs(entitySerializer, BsonSerializer.SerializerRegistry)); distanceFieldName = distanceResultStringField.FieldName; } diff --git a/src/MongoFramework/MongoDbConnection.cs b/src/MongoFramework/MongoDbConnection.cs index 03f94288..0ae6358d 100644 --- a/src/MongoFramework/MongoDbConnection.cs +++ b/src/MongoFramework/MongoDbConnection.cs @@ -26,7 +26,6 @@ public IMongoClient Client { var settings = MongoClientSettings.FromUrl(Url); ConfigureSettings?.Invoke(settings); - settings.LinqProvider = MongoDB.Driver.Linq.LinqProvider.V2; InternalClient = new MongoClient(settings); } @@ -76,6 +75,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { + // In MongoDB.Driver 3.0+, IMongoClient implements IDisposable + (InternalClient as IDisposable)?.Dispose(); InternalClient = null; IsDisposed = true; } diff --git a/src/MongoFramework/MongoFramework.csproj b/src/MongoFramework/MongoFramework.csproj index 91f56ad9..6934a2bc 100644 --- a/src/MongoFramework/MongoFramework.csproj +++ b/src/MongoFramework/MongoFramework.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/MongoFramework.Tests/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperTests.cs b/tests/MongoFramework.Tests/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperTests.cs index b57c659f..94ce40d5 100644 --- a/tests/MongoFramework.Tests/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperTests.cs @@ -30,7 +30,7 @@ private BsonDocument RenderDefinition(UpdateDefinition definit { var serializerRegistry = BsonSerializer.SerializerRegistry; var documentSerializer = serializerRegistry.GetSerializer(); - return definition.Render(documentSerializer, serializerRegistry).AsBsonDocument; + return definition.Render(new RenderArgs(documentSerializer, serializerRegistry)).AsBsonDocument; } [TestMethod] diff --git a/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs b/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs index 616115cf..4cd2a233 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Indexing/IndexModelBuilderTests.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; using MongoDB.Driver.GeoJsonObjectModel; using MongoFramework.Attributes; using MongoFramework.Infrastructure.Indexing; @@ -10,6 +13,12 @@ namespace MongoFramework.Tests.Infrastructure.Indexing.Processors [TestClass] public class IndexModelBuilderTests : TestBase { + private static BsonDocument RenderKeys(IndexKeysDefinition keys) + { + var serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + return keys.Render(new RenderArgs(serializer, BsonSerializer.SerializerRegistry)); + } + public class IndexNamingModel { [Index(IndexSortOrder.Ascending)] @@ -119,7 +128,7 @@ public void AppliesIndexSortOrder() Assert.AreEqual(2, indexModel.Count()); - var indexBsonDocument = indexModel.Select(m => m.Keys.Render(null, null)).ToArray(); + var indexBsonDocument = indexModel.Select(m => RenderKeys(m.Keys)).ToArray(); Assert.AreEqual(1, indexBsonDocument[0]["AscendingIndex"]); Assert.AreEqual(-1, indexBsonDocument[1]["DescendingIndex"]); } @@ -144,7 +153,7 @@ public void CompoundIndex() var compoundIndex = indexModel.FirstOrDefault(); Assert.AreEqual("MyCompoundIndex", compoundIndex.Options.Name); - var indexBsonDocument = compoundIndex.Keys.Render(null, null); + var indexBsonDocument = RenderKeys(compoundIndex.Keys); Assert.AreEqual("FirstPriority", indexBsonDocument.ElementAt(0).Name); Assert.AreEqual("SecondPriority", indexBsonDocument.ElementAt(1).Name); @@ -161,7 +170,7 @@ public void NestedCompoundIndex() var compoundIndex = indexModel.FirstOrDefault(); Assert.AreEqual("MyCompoundIndex", compoundIndex.Options.Name); - var indexBsonDocument = compoundIndex.Keys.Render(null, null); + var indexBsonDocument = RenderKeys(compoundIndex.Keys); Assert.AreEqual("ChildModel.FirstPriority", indexBsonDocument.ElementAt(0).Name); Assert.AreEqual("SecondPriority", indexBsonDocument.ElementAt(1).Name); @@ -174,7 +183,7 @@ public void MultikeyIndex() Assert.AreEqual(3, indexModel.Count()); - var results = indexModel.Select(i => i.Keys.Render(null, null).ElementAt(0)); + var results = indexModel.Select(i => RenderKeys(i.Keys).ElementAt(0)); Assert.IsTrue(results.Any(e => e.Name == "ChildEnumerable.ChildId")); Assert.IsTrue(results.Any(e => e.Name == "ChildArray.ChildId")); Assert.IsTrue(results.Any(e => e.Name == "ChildList.ChildId")); @@ -187,7 +196,7 @@ public void TextIndex() Assert.AreEqual(1, indexModel.Count()); - var results = indexModel.Select(i => i.Keys.Render(null, null)).FirstOrDefault(); + var results = indexModel.Select(i => RenderKeys(i.Keys)).FirstOrDefault(); Assert.IsTrue(results.Any(e => e.Name == "SomeTextField" && e.Value == "text")); Assert.IsTrue(results.Any(e => e.Name == "AnotherTextField" && e.Value == "text")); } @@ -199,7 +208,7 @@ public void Geo2dSphereIndex() Assert.AreEqual(1, indexModel.Count()); - var results = indexModel.Select(i => i.Keys.Render(null, null)).FirstOrDefault(); + var results = indexModel.Select(i => RenderKeys(i.Keys)).FirstOrDefault(); Assert.IsTrue(results.Any(e => e.Name == "SomeCoordinates" && e.Value == "2dsphere")); } @@ -208,7 +217,7 @@ public void AppliesTenantConstraint() { var indexModel = IndexModelBuilder.BuildModel(); - var indexBsonDocument = indexModel.First().Keys.Render(null, null); + var indexBsonDocument = RenderKeys(indexModel.First().Keys); Assert.AreEqual(1, indexBsonDocument["TenantId"].AsInt32); Assert.AreEqual(1, indexBsonDocument["UniqueIndex"].AsInt32); @@ -221,7 +230,7 @@ public void MultipleIndexesOnSingleProperty() Assert.AreEqual(2, indexModel.Count()); - var results = indexModel.Select(i => i.Keys.Render(null, null)); + var results = indexModel.Select(i => RenderKeys(i.Keys)); Assert.IsTrue(results.Any(e => e.Contains("MyCustomField") && e.ElementCount == 1)); Assert.IsTrue(results.Any(e => e.Contains("MyCustomField") && e.Contains("OtherField") && e.ElementCount == 2)); } diff --git a/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs b/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs new file mode 100644 index 00000000..12a4ecc8 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs @@ -0,0 +1,366 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MongoFramework.Tests.Infrastructure.Serialization +{ + [TestClass] + public class DateOnlyTimeOnlyTests : TestBase + { + public class DateOnlyModel + { + public string Id { get; set; } + public DateOnly Date { get; set; } + public DateOnly? NullableDate { get; set; } + } + + public class TimeOnlyModel + { + public string Id { get; set; } + public TimeOnly Time { get; set; } + public TimeOnly? NullableTime { get; set; } + } + + public class CombinedModel + { + public string Id { get; set; } + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public DateOnly? NullableDate { get; set; } + public TimeOnly? NullableTime { get; set; } + } + + [TestMethod] + public void DateOnly_InsertAndQueryBack() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testDate = new DateOnly(2024, 6, 15); + var entity = new DateOnlyModel + { + Date = testDate, + NullableDate = new DateOnly(2024, 12, 25) + }; + + dbSet.Add(entity); + context.SaveChanges(); + + // Query back + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testDate, result.Date); + Assert.AreEqual(new DateOnly(2024, 12, 25), result.NullableDate); + } + + [TestMethod] + public void DateOnly_NullableWithNullValue() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testDate = new DateOnly(2024, 1, 1); + var entity = new DateOnlyModel + { + Date = testDate, + NullableDate = null + }; + + dbSet.Add(entity); + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testDate, result.Date); + Assert.IsNull(result.NullableDate); + } + + [TestMethod] + public void DateOnly_QueryWithPredicate() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var targetDate = new DateOnly(2024, 7, 4); + dbSet.Add(new DateOnlyModel { Date = new DateOnly(2024, 1, 1) }); + dbSet.Add(new DateOnlyModel { Date = targetDate }); + dbSet.Add(new DateOnlyModel { Date = new DateOnly(2024, 12, 31) }); + + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Date == targetDate); + + Assert.IsNotNull(result); + Assert.AreEqual(targetDate, result.Date); + } + + [TestMethod] + public void DateOnly_QueryWithComparison() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + dbSet.Add(new DateOnlyModel { Date = new DateOnly(2024, 1, 1) }); + dbSet.Add(new DateOnlyModel { Date = new DateOnly(2024, 6, 15) }); + dbSet.Add(new DateOnlyModel { Date = new DateOnly(2024, 12, 31) }); + + context.SaveChanges(); + + var midYear = new DateOnly(2024, 6, 1); + var results = dbSet.Where(x => x.Date > midYear).ToArray(); + + Assert.AreEqual(2, results.Length); + } + + [TestMethod] + public void TimeOnly_InsertAndQueryBack() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testTime = new TimeOnly(14, 30, 45); + var entity = new TimeOnlyModel + { + Time = testTime, + NullableTime = new TimeOnly(23, 59, 59) + }; + + dbSet.Add(entity); + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testTime, result.Time); + Assert.AreEqual(new TimeOnly(23, 59, 59), result.NullableTime); + } + + [TestMethod] + public void TimeOnly_NullableWithNullValue() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testTime = new TimeOnly(9, 0, 0); + var entity = new TimeOnlyModel + { + Time = testTime, + NullableTime = null + }; + + dbSet.Add(entity); + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testTime, result.Time); + Assert.IsNull(result.NullableTime); + } + + [TestMethod] + public void TimeOnly_QueryWithPredicate() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var targetTime = new TimeOnly(12, 0, 0); + dbSet.Add(new TimeOnlyModel { Time = new TimeOnly(8, 0, 0) }); + dbSet.Add(new TimeOnlyModel { Time = targetTime }); + dbSet.Add(new TimeOnlyModel { Time = new TimeOnly(18, 0, 0) }); + + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Time == targetTime); + + Assert.IsNotNull(result); + Assert.AreEqual(targetTime, result.Time); + } + + [TestMethod] + public void TimeOnly_QueryWithComparison() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + dbSet.Add(new TimeOnlyModel { Time = new TimeOnly(8, 0, 0) }); + dbSet.Add(new TimeOnlyModel { Time = new TimeOnly(12, 0, 0) }); + dbSet.Add(new TimeOnlyModel { Time = new TimeOnly(18, 0, 0) }); + + context.SaveChanges(); + + var noon = new TimeOnly(12, 0, 0); + var results = dbSet.Where(x => x.Time >= noon).ToArray(); + + Assert.AreEqual(2, results.Length); + } + + [TestMethod] + public void TimeOnly_WithMilliseconds() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testTime = new TimeOnly(10, 30, 45, 123); + var entity = new TimeOnlyModel + { + Time = testTime + }; + + dbSet.Add(entity); + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testTime.Hour, result.Time.Hour); + Assert.AreEqual(testTime.Minute, result.Time.Minute); + Assert.AreEqual(testTime.Second, result.Time.Second); + Assert.AreEqual(testTime.Millisecond, result.Time.Millisecond); + } + + [TestMethod] + public void Combined_DateOnlyAndTimeOnly() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testDate = new DateOnly(2024, 6, 15); + var testTime = new TimeOnly(14, 30, 0); + + var entity = new CombinedModel + { + Date = testDate, + Time = testTime, + NullableDate = new DateOnly(2025, 1, 1), + NullableTime = new TimeOnly(9, 0, 0) + }; + + dbSet.Add(entity); + context.SaveChanges(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testDate, result.Date); + Assert.AreEqual(testTime, result.Time); + Assert.AreEqual(new DateOnly(2025, 1, 1), result.NullableDate); + Assert.AreEqual(new TimeOnly(9, 0, 0), result.NullableTime); + } + + [TestMethod] + public async Task DateOnly_AsyncInsertAndQueryBack() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testDate = new DateOnly(2024, 3, 14); + var entity = new DateOnlyModel + { + Date = testDate + }; + + dbSet.Add(entity); + await context.SaveChangesAsync(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testDate, result.Date); + } + + [TestMethod] + public async Task TimeOnly_AsyncInsertAndQueryBack() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var testTime = new TimeOnly(16, 45, 30); + var entity = new TimeOnlyModel + { + Time = testTime + }; + + dbSet.Add(entity); + await context.SaveChangesAsync(); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + + Assert.IsNotNull(result); + Assert.AreEqual(testTime, result.Time); + } + + [TestMethod] + public void DateOnly_Update() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var entity = new DateOnlyModel + { + Date = new DateOnly(2024, 1, 1) + }; + + dbSet.Add(entity); + context.SaveChanges(); + + // Update + entity.Date = new DateOnly(2024, 12, 31); + dbSet.Update(entity); + context.SaveChanges(); + + // Verify + ResetMongoDb(); + context = new MongoDbContext(connection); + dbSet = new MongoDbSet(context); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + Assert.AreEqual(new DateOnly(2024, 12, 31), result.Date); + } + + [TestMethod] + public void TimeOnly_Update() + { + var connection = TestConfiguration.GetConnection(); + var context = new MongoDbContext(connection); + var dbSet = new MongoDbSet(context); + + var entity = new TimeOnlyModel + { + Time = new TimeOnly(8, 0, 0) + }; + + dbSet.Add(entity); + context.SaveChanges(); + + // Update + entity.Time = new TimeOnly(20, 0, 0); + dbSet.Update(entity); + context.SaveChanges(); + + // Verify + ResetMongoDb(); + context = new MongoDbContext(connection); + dbSet = new MongoDbSet(context); + + var result = dbSet.FirstOrDefault(x => x.Id == entity.Id); + Assert.AreEqual(new TimeOnly(20, 0, 0), result.Time); + } + } +} From 2b35a534a578b04ea81655fafe32b67c6ede4dc0 Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 15:30:20 -0500 Subject: [PATCH 6/8] update build file --- .github/workflows/build.yml | 40 ++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c1190e8..c83c9a17 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,16 +29,13 @@ jobs: strategy: matrix: os: [windows-latest, macOS-latest] - mongodb: ['4.4', '5.0', '6.0'] + mongodb: ['7.0', '8.0'] include: - - os: ubuntu-20.04 - mongodb: '4.4' - ubuntu: 'focal' - - os: ubuntu-20.04 - mongodb: '5.0' - ubuntu: 'focal' - os: ubuntu-latest - mongodb: '6.0' + mongodb: '7.0' + ubuntu: 'jammy' + - os: ubuntu-latest + mongodb: '8.0' ubuntu: 'jammy' steps: - name: Configure MongoDB (MacOS) @@ -48,11 +45,8 @@ jobs: brew update brew install mongodb-community@${{matrix.mongodb}} brew services start mongodb-community@${{matrix.mongodb}} - - name: Configure MongoDB (Ubuntu (20.04)) - if: matrix.os == 'ubuntu-20.04' - run: sudo apt remove mongodb-org - - name: Configure MongoDB (Ubuntu (All)) - if: matrix.os == 'ubuntu-latest' || matrix.os == 'ubuntu-20.04' + - name: Configure MongoDB (Ubuntu) + if: matrix.os == 'ubuntu-latest' run: | wget -qO - https://www.mongodb.org/static/pgp/server-${{matrix.mongodb}}.asc | gpg --dearmor | sudo tee /usr/share/keyrings/mongodb.gpg > /dev/null echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb.gpg ] https://repo.mongodb.org/apt/ubuntu ${{matrix.ubuntu}}/mongodb-org/${{matrix.mongodb}} multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-${{matrix.mongodb}}.list @@ -70,7 +64,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup dotnet SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x - name: Install dependencies @@ -82,9 +76,9 @@ jobs: - name: Pack run: dotnet pack --no-build -c Release /p:PackageOutputPath=${{env.BUILD_ARTIFACT_PATH}} - name: Publish artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: ${{matrix.os}} + name: ${{matrix.os}}-mongo${{matrix.mongodb}} path: ${{env.BUILD_ARTIFACT_PATH}} coverage: @@ -95,18 +89,18 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Download coverage reports - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Install ReportGenerator tool run: dotnet tool install -g dotnet-reportgenerator-globaltool - name: Prepare coverage reports run: reportgenerator -reports:*/coverage/*/coverage.cobertura.xml -targetdir:./ -reporttypes:Cobertura - name: Upload coverage report - uses: codecov/codecov-action@v3.1.4 + uses: codecov/codecov-action@v4 with: file: Cobertura.xml fail_ci_if_error: false - name: Save combined coverage report as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: coverage-report path: Cobertura.xml @@ -123,9 +117,9 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Download build' - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: 'ubuntu-latest' + name: 'ubuntu-latest-mongo8.0' - name: 'Add NuGet source' run: dotnet nuget add source https://nuget.pkg.github.com/TurnerSoftware/index.json --name GitHub --username Turnerj --password ${{secrets.GITHUB_TOKEN}} --store-password-in-clear-text - name: 'Upload NuGet package' @@ -141,8 +135,8 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Download build' - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: 'ubuntu-latest' + name: 'ubuntu-latest-mongo8.0' - name: 'Upload NuGet package' run: dotnet nuget push *.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{secrets.NUGET_API_KEY}} From b28fc3b9726034369684bdb5c3bdd5255d76e13f Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 16:20:14 -0500 Subject: [PATCH 7/8] added benchmark tests --- .gitignore | 6 +++ README.md | 13 +++++ .../Infrastructure/EntityEntryContainer.cs | 9 +++- .../Mapping/EntityDefinition.cs | 21 +++++++- .../UpdateDefinitionHelperBenchmark.cs | 2 +- .../EntityCollectionGetEntryBenchmark.cs | 48 +++++++++++++++++++ .../Indexing/IndexModelBuilderBenchmark.cs | 2 +- .../Internal/GenericMethodInvokeBenchmark.cs | 2 +- .../Infrastructure/Linq/LinqBenchmark.cs | 2 +- .../Mapping/EntityMappingBenchmark.cs | 2 +- .../TypeDiscovery_FindTypeBenchmark.cs | 2 +- .../MongoDbSetComparisonBenchmark.cs | 2 +- .../SerializationComparisonBenchmark.cs | 2 +- 13 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 tests/MongoFramework.Benchmarks/Infrastructure/EntityCollectionGetEntryBenchmark.cs diff --git a/.gitignore b/.gitignore index fa002543..8db733a3 100644 --- a/.gitignore +++ b/.gitignore @@ -258,3 +258,9 @@ paket-files/ # MacOS .DS_Store + +benchmark-results-v2/ + +benchmark-results-v3/ + +BenchmarkDotNet.Artifacts/ diff --git a/README.md b/README.md index c41cfc4b..e01f5155 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,19 @@ Some of the major features include: MongoFramework is currently built on-top of the official MongoDB C# driver. +## Performance + +MongoFramework with MongoDB.Driver 3.x and .NET 10 delivers significant performance improvements: + +| Category | Speed Improvement | Memory Reduction | +|----------|-------------------|------------------| +| LINQ Operations | ~66% faster | ~55% less | +| Entity Mapping | ~53% faster | ~10% less | +| Serialization | ~47% faster | ~6% less | +| Database Operations | ~84% faster | ~26% less | + +Benchmarks performed comparing MongoDB.Driver 2.x (.NET 6) vs 3.x (.NET 10). See [benchmark results](tests/MongoFramework.Benchmarks) for details. + ## Licensing and Support MongoFramework is licensed under the MIT license. It is free to use in personal and commercial projects. diff --git a/src/MongoFramework/Infrastructure/EntityEntryContainer.cs b/src/MongoFramework/Infrastructure/EntityEntryContainer.cs index 7002aba1..0424ccae 100644 --- a/src/MongoFramework/Infrastructure/EntityEntryContainer.cs +++ b/src/MongoFramework/Infrastructure/EntityEntryContainer.cs @@ -46,11 +46,16 @@ public EntityEntry GetEntry(TCollectionBase entity) var entityDefinition = EntityMapping.GetOrCreateDefinition(collectionType); var entityId = entityDefinition.GetIdValue(entity); var defaultIdValue = entityDefinition.GetDefaultId(); + var isDefaultId = Equals(entityId, defaultIdValue); + foreach (var entry in entries) { - if (Equals(entityId, defaultIdValue) && ReferenceEquals(entry.Entity, entity)) + if (isDefaultId) { - return entry; + if (ReferenceEquals(entry.Entity, entity)) + { + return entry; + } } else { diff --git a/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs b/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs index da034cff..91e9e034 100644 --- a/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs +++ b/src/MongoFramework/Infrastructure/Mapping/EntityDefinition.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq.Expressions; using System.Reflection; namespace MongoFramework.Infrastructure.Mapping; @@ -25,9 +26,27 @@ public sealed record PropertyDefinition public PropertyInfo PropertyInfo { get; init; } public string ElementName { get; init; } + private Func getValueDelegate; + public object GetValue(object entity) { - return PropertyInfo.GetValue(entity); + if (getValueDelegate is null) + { + var parameter = Expression.Parameter(typeof(object), "entity"); + var lambda = Expression.Lambda>( + Expression.Convert( + Expression.MakeMemberAccess( + Expression.Convert(parameter, PropertyInfo.DeclaringType), + PropertyInfo + ), + typeof(object) + ), + parameter + ); + getValueDelegate = lambda.Compile(); + } + + return getValueDelegate(entity); } public void SetValue(object entity, object value) diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperBenchmark.cs index 5e2ba93c..8c8a81da 100644 --- a/tests/MongoFramework.Benchmarks/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/Infrastructure/DefinitionHelpers/UpdateDefinitionHelperBenchmark.cs @@ -8,7 +8,7 @@ namespace MongoFramework.Benchmarks.Infrastructure.DefinitionHelpers { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class UpdateDefinitionHelperBenchmark { [Benchmark] diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/EntityCollectionGetEntryBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/EntityCollectionGetEntryBenchmark.cs new file mode 100644 index 00000000..c748c603 --- /dev/null +++ b/tests/MongoFramework.Benchmarks/Infrastructure/EntityCollectionGetEntryBenchmark.cs @@ -0,0 +1,48 @@ +using BenchmarkDotNet.Attributes; +using MongoFramework.Infrastructure; + +namespace MongoFramework.Benchmarks.Infrastructure +{ + [MemoryDiagnoser] + public class EntityCollectionGetEntryBenchmark + { + public class BenchmarkModel + { + public string Id { get; set; } + public string Name { get; set; } + } + + [Params(100, 1000, 10000)] + public int NumberOfEntries { get; set; } + + private EntityEntryContainer container; + private BenchmarkModel[] entities; + + [GlobalSetup] + public void Setup() + { + container = new EntityEntryContainer(); + entities = new BenchmarkModel[NumberOfEntries]; + + for (var i = 0; i < NumberOfEntries; i++) + { + entities[i] = new BenchmarkModel + { + Id = i.ToString(), + Name = $"Entity {i}" + }; + } + } + + [Benchmark] + public void SetEntityState() + { + for (var i = 0; i < NumberOfEntries; i++) + { + container.SetEntityState(entities[i], EntityEntryState.Added); + } + + container.Clear(); + } + } +} diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/Indexing/IndexModelBuilderBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/Indexing/IndexModelBuilderBenchmark.cs index e1c33007..9f5a1c41 100644 --- a/tests/MongoFramework.Benchmarks/Infrastructure/Indexing/IndexModelBuilderBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/Infrastructure/Indexing/IndexModelBuilderBenchmark.cs @@ -9,7 +9,7 @@ namespace MongoFramework.Benchmarks.Infrastructure.Indexing { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class IndexModelBuilderBenchmark { public class FlatIndexModel diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/Internal/GenericMethodInvokeBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/Internal/GenericMethodInvokeBenchmark.cs index 3907f233..6a62231a 100644 --- a/tests/MongoFramework.Benchmarks/Infrastructure/Internal/GenericMethodInvokeBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/Infrastructure/Internal/GenericMethodInvokeBenchmark.cs @@ -7,7 +7,7 @@ namespace MongoFramework.Benchmarks.Infrastructure.Internal { - [ShortRunJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [ShortRunJob, MemoryDiagnoser] public class GenericMethodInvokeBenchmark { [Benchmark] diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/Linq/LinqBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/Linq/LinqBenchmark.cs index 4ab2f666..a84a52b1 100644 --- a/tests/MongoFramework.Benchmarks/Infrastructure/Linq/LinqBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/Infrastructure/Linq/LinqBenchmark.cs @@ -6,7 +6,7 @@ namespace MongoFramework.Benchmarks.Infrastructure.Linq { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class LinqBenchmark { private IMongoDbConnection Connection { get; set; } diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/Mapping/EntityMappingBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/Mapping/EntityMappingBenchmark.cs index abf61819..e9a03139 100644 --- a/tests/MongoFramework.Benchmarks/Infrastructure/Mapping/EntityMappingBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/Infrastructure/Mapping/EntityMappingBenchmark.cs @@ -8,7 +8,7 @@ namespace MongoFramework.Benchmarks.Infrastructure.Mapping { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class EntityMappingBenchmark { public class PersonModel diff --git a/tests/MongoFramework.Benchmarks/Infrastructure/Serialization/TypeDiscovery_FindTypeBenchmark.cs b/tests/MongoFramework.Benchmarks/Infrastructure/Serialization/TypeDiscovery_FindTypeBenchmark.cs index fa60cbf1..22ce5358 100644 --- a/tests/MongoFramework.Benchmarks/Infrastructure/Serialization/TypeDiscovery_FindTypeBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/Infrastructure/Serialization/TypeDiscovery_FindTypeBenchmark.cs @@ -4,7 +4,7 @@ namespace MongoFramework.Benchmarks.Infrastructure.Serialization { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class TypeDiscovery_FindTypeBenchmark { private class LocalClass { } diff --git a/tests/MongoFramework.Benchmarks/MongoDbSetComparisonBenchmark.cs b/tests/MongoFramework.Benchmarks/MongoDbSetComparisonBenchmark.cs index a6119cdd..939c3b73 100644 --- a/tests/MongoFramework.Benchmarks/MongoDbSetComparisonBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/MongoDbSetComparisonBenchmark.cs @@ -6,7 +6,7 @@ namespace MongoFramework.Benchmarks { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class MongoDbSetComparisonBenchmark { private class TestModel diff --git a/tests/MongoFramework.Benchmarks/SerializationComparisonBenchmark.cs b/tests/MongoFramework.Benchmarks/SerializationComparisonBenchmark.cs index c706e6b2..dcd0449e 100644 --- a/tests/MongoFramework.Benchmarks/SerializationComparisonBenchmark.cs +++ b/tests/MongoFramework.Benchmarks/SerializationComparisonBenchmark.cs @@ -8,7 +8,7 @@ namespace MongoFramework.Benchmarks { - [SimpleJob(RuntimeMoniker.Net50), MemoryDiagnoser] + [MemoryDiagnoser] public class SerializationComparisonBenchmark { class ExampleClass From 808935a79abb06ac20cb4dcdc297984a54b66c80 Mon Sep 17 00:00:00 2001 From: John Campion Jr Date: Sat, 31 Jan 2026 16:42:45 -0500 Subject: [PATCH 8/8] update initial net10 --- .github/renovate.json | 2 +- .github/workflows/build.yml | 39 +------------- .github/workflows/publish-nuget.yml | 51 +++++++++++++++++++ .github/workflows/release-please.yml | 20 ++++++++ .release-please-manifest.json | 3 ++ README.md | 22 ++++---- release-please-config.json | 33 ++++++++++++ src/Directory.Build.props | 51 ++++++++++--------- src/Directory.Build.targets | 5 ++ .../Mapping/EntityMappingTests.cs | 2 +- .../Serialization/DateOnlyTimeOnlyTests.cs | 4 +- 11 files changed, 155 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/publish-nuget.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json create mode 100644 src/Directory.Build.targets diff --git a/.github/renovate.json b/.github/renovate.json index 1f4a9608..5db72dd6 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,6 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "github>TurnerSoftware/.github:renovate-shared" + "config:recommended" ] } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c83c9a17..1a6bed2f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,6 @@ on: push: branches: [ main ] pull_request: - release: - types: [ published ] concurrency: group: '${{github.workflow}} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' @@ -72,7 +70,7 @@ jobs: - name: Build run: dotnet build --no-restore -c Release - name: Test with Coverage - run: dotnet test --no-restore --logger trx --results-directory ${{env.BUILD_ARTIFACT_PATH}}/coverage --collect "XPlat Code Coverage" --settings CodeCoverage.runsettings /p:SkipBuildVersioning=true + run: dotnet test --no-restore --logger trx --results-directory ${{env.BUILD_ARTIFACT_PATH}}/coverage --collect "XPlat Code Coverage" --settings CodeCoverage.runsettings - name: Pack run: dotnet pack --no-build -c Release /p:PackageOutputPath=${{env.BUILD_ARTIFACT_PATH}} - name: Publish artifacts @@ -105,38 +103,3 @@ jobs: name: coverage-report path: Cobertura.xml - push-to-github-packages: - name: 'Push GitHub Packages' - needs: build - if: github.ref == 'refs/heads/main' || github.event_name == 'release' - environment: - name: 'GitHub Packages' - url: https://github.com/TurnerSoftware/MongoFramework/packages - permissions: - packages: write - runs-on: ubuntu-latest - steps: - - name: 'Download build' - uses: actions/download-artifact@v4 - with: - name: 'ubuntu-latest-mongo8.0' - - name: 'Add NuGet source' - run: dotnet nuget add source https://nuget.pkg.github.com/TurnerSoftware/index.json --name GitHub --username Turnerj --password ${{secrets.GITHUB_TOKEN}} --store-password-in-clear-text - - name: 'Upload NuGet package' - run: dotnet nuget push *.nupkg --api-key ${{secrets.GH_PACKAGE_REGISTRY_API_KEY}} --source GitHub --skip-duplicate - - push-to-nuget: - name: 'Push NuGet Packages' - needs: build - if: github.event_name == 'release' - environment: - name: 'NuGet' - url: https://www.nuget.org/packages/MongoFramework - runs-on: ubuntu-latest - steps: - - name: 'Download build' - uses: actions/download-artifact@v4 - with: - name: 'ubuntu-latest-mongo8.0' - - name: 'Upload NuGet package' - run: dotnet nuget push *.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{secrets.NUGET_API_KEY}} diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml new file mode 100644 index 00000000..65dfe35e --- /dev/null +++ b/.github/workflows/publish-nuget.yml @@ -0,0 +1,51 @@ +# Note this will only work with Release-Please if you have given it a PAT. +# The default GITHUB_TOKEN does not have permission to run workflows on releases +name: Publish to NuGet + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + id-token: write # Required for OIDC + contents: read + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + release: + runs-on: ubuntu-latest + environment: + name: 'NuGet' + url: https://www.nuget.org/packages/MongoFramework + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup DotNet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore -c Release + + - name: Pack NuGet + run: dotnet pack --no-build -c Release -o nupkg + + # Get a short-lived NuGet API key + - name: NuGet login (OIDC) + uses: NuGet/login@v1 + id: login + with: + user: ${{ secrets.NUGET_USER }} + + - name: Push NuGet + run: dotnet nuget push nupkg/*.nupkg -k ${{ steps.login.outputs.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..dcadfb08 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,20 @@ +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +name: release-please + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..d4f6f299 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "3.0.0" +} diff --git a/README.md b/README.md index e01f5155..524875bb 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,20 @@ ![Icon](images/icon.png) # MongoFramework -An "Entity Framework"-like interface for MongoDB -![Build](https://img.shields.io/github/actions/workflow/status/TurnerSoftware/mongoframework/build.yml?branch=main) -[![Codecov](https://img.shields.io/codecov/c/github/turnersoftware/mongoframework/main.svg)](https://codecov.io/gh/TurnerSoftware/MongoFramework) -[![NuGet](https://img.shields.io/nuget/v/MongoFramework.svg)](https://www.nuget.org/packages/MongoFramework/) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/62fa31c90bf94f3d8e201b9684a7a4ca)](https://www.codacy.com/app/Turnerj/MongoFramework) +![Build](https://img.shields.io/github/actions/workflow/status/jcamp-code/MongoFramework/build.yml?branch=main) +[![Codecov](https://img.shields.io/codecov/c/github/jcamp-code/MongoFramework/main.svg)](https://codecov.io/gh/jcamp-code/MongoFramework) +[![NuGet](https://img.shields.io/nuget/v/jcamp.MongoFramework.svg)](https://www.nuget.org/packages/jcamp.MongoFramework/) +## Fork +Note this is a fork from the original, updated for latest .NET versions and packages. + +Version has been set to v3 to indicate compatibility with the v3 MongoDB CS driver. + +An "Entity Framework"-like interface for MongoDB + + ## Overview MongoFramework tries to bring some of the nice features from Entity Framework into the world of MongoDB. @@ -38,14 +44,10 @@ MongoFramework with MongoDB.Driver 3.x and .NET 10 delivers significant performa Benchmarks performed comparing MongoDB.Driver 2.x (.NET 6) vs 3.x (.NET 10). See [benchmark results](tests/MongoFramework.Benchmarks) for details. -## Licensing and Support +## Licensing MongoFramework is licensed under the MIT license. It is free to use in personal and commercial projects. -There are [support plans](https://turnersoftware.com.au/support-plans) available that cover all active [Turner Software OSS projects](https://github.com/TurnerSoftware). -Support plans provide private email support, expert usage advice for our projects, priority bug fixes and more. -These support plans help fund our OSS commitments to provide better software for everyone. - ## MongoFramework Extensions These extensions are official packages that enhance the functionality of MongoFramework, integrating it with other systems and tools. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..1f19a6ab --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,33 @@ +{ + "packages": { + ".": { + "changelog-path": "CHANGELOG.md", + "release-type": "simple", + "draft": false, + "prerelease": false + } + }, + "plugins": ["sentence-case"], + "changelog-sections": [ + { "type": "feat", "section": "✨ Features" }, + { "type": "feature", "section": "✨ Features" }, + { "type": "fix", "section": "🐛 Bug Fixes" }, + { "type": "perf", "section": "⚡️ Performance Improvements" }, + { "type": "revert", "section": "⏪️ Reverts" }, + { "type": "docs", "section": "📝 Documentation" }, + { "type": "style", "section": "🎨 Styles" }, + { "type": "chore", "section": "🏡 Miscellaneous Chores" }, + { "type": "refactor", "section": "♻️ Code Refactoring", "hidden": true }, + { "type": "test", "section": "✅ Tests", "hidden": true }, + { "type": "build", "section": "📦️ Build System", "hidden": true }, + { "type": "ci", "section": "🤖 Continuous Integration", "hidden": true } + ], + "extra-files": [ + { + "type": "xml", + "path": "src/Directory.Build.props", + "xpath": "//Project/PropertyGroup/Version" + } + ], + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index eafe6553..82c0642b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,31 +1,32 @@ + + 3.0.0 + MongoFramework - - MongoFramework + Turner Software/JohnCampion - Turner Software + true + MIT + icon.png + https://github.com/jcamp-code/MongoFramework + mongo;mongodb;data;database;orm - $(AssemblyName) - true - MIT - icon.png - https://github.com/TurnerSoftware/MongoFramework - mongo;mongodb;data;database;orm + + true + true + embedded - - true - true - embedded + Latest + + + + - Latest - - - - - - - - - - - \ No newline at end of file + + + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 00000000..3a30dc72 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,5 @@ + + + jcamp.$(AssemblyName) + + diff --git a/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityMappingTests.cs b/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityMappingTests.cs index a44290c1..ef72059b 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityMappingTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Mapping/EntityMappingTests.cs @@ -17,7 +17,7 @@ public class MappingLockModel /// A potentially common issue for web application startup, this tests that multiple threads /// can map a class at the same time without concurrency issues. /// - /// Relates to: https://github.com/TurnerSoftware/MongoFramework/issues/7 + /// Relates to: https://github.com/jcamp-code/MongoFramework/issues/7 /// [TestMethod] public void MappingLocks() diff --git a/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs b/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs index 12a4ecc8..abdb2c54 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Serialization/DateOnlyTimeOnlyTests.cs @@ -116,7 +116,7 @@ public void DateOnly_QueryWithComparison() var midYear = new DateOnly(2024, 6, 1); var results = dbSet.Where(x => x.Date > midYear).ToArray(); - Assert.AreEqual(2, results.Length); + Assert.HasCount(2, results); } [TestMethod] @@ -203,7 +203,7 @@ public void TimeOnly_QueryWithComparison() var noon = new TimeOnly(12, 0, 0); var results = dbSet.Where(x => x.Time >= noon).ToArray(); - Assert.AreEqual(2, results.Length); + Assert.HasCount(2, results); } [TestMethod]