diff --git a/src/MongoDB.Driver.Core/Core/Operations/FindOneAndDeleteOperation.cs b/src/MongoDB.Driver.Core/Core/Operations/FindOneAndDeleteOperation.cs index ef02349b8fa..a30817efe41 100644 --- a/src/MongoDB.Driver.Core/Core/Operations/FindOneAndDeleteOperation.cs +++ b/src/MongoDB.Driver.Core/Core/Operations/FindOneAndDeleteOperation.cs @@ -127,12 +127,14 @@ public BsonDocument Sort // methods internal override BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription, long? transactionNumber) { - var maxWireVersion = connectionDescription.MaxWireVersion; - if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(maxWireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged)) + var wireVersion = connectionDescription.MaxWireVersion; + FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion); + + if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(wireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged)) { if (_hint != null) { - throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(maxWireVersion)} does not support hints."); + throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(wireVersion)} does not support hints."); } } diff --git a/src/MongoDB.Driver.Core/Core/Operations/FindOneAndReplaceOperation.cs b/src/MongoDB.Driver.Core/Core/Operations/FindOneAndReplaceOperation.cs index 3e9497f0609..723bed3ef98 100644 --- a/src/MongoDB.Driver.Core/Core/Operations/FindOneAndReplaceOperation.cs +++ b/src/MongoDB.Driver.Core/Core/Operations/FindOneAndReplaceOperation.cs @@ -181,12 +181,14 @@ public BsonDocument Sort // methods internal override BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription, long? transactionNumber) { - var maxWireVersion = connectionDescription.MaxWireVersion; - if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(maxWireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged)) + var wireVersion = connectionDescription.MaxWireVersion; + FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion); + + if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(wireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged)) { if (_hint != null) { - throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(maxWireVersion)} does not support hints."); + throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(wireVersion)} does not support hints."); } } diff --git a/src/MongoDB.Driver.Core/Core/Operations/FindOneAndUpdateOperation.cs b/src/MongoDB.Driver.Core/Core/Operations/FindOneAndUpdateOperation.cs index cdb180a061b..ead4db0ff4a 100644 --- a/src/MongoDB.Driver.Core/Core/Operations/FindOneAndUpdateOperation.cs +++ b/src/MongoDB.Driver.Core/Core/Operations/FindOneAndUpdateOperation.cs @@ -196,12 +196,14 @@ public BsonValue Update // methods internal override BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription, long? transactionNumber) { - var maxWireVersion = connectionDescription.MaxWireVersion; - if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(maxWireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged)) + var wireVersion = connectionDescription.MaxWireVersion; + FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion); + + if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(wireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged)) { if (_hint != null) { - throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(maxWireVersion)} does not support hints."); + throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(wireVersion)} does not support hints."); } } diff --git a/src/MongoDB.Driver.Core/Core/Operations/FindOperation.cs b/src/MongoDB.Driver.Core/Core/Operations/FindOperation.cs index c6d2d0f057c..b2b9a906910 100644 --- a/src/MongoDB.Driver.Core/Core/Operations/FindOperation.cs +++ b/src/MongoDB.Driver.Core/Core/Operations/FindOperation.cs @@ -469,6 +469,9 @@ public BsonDocument Sort /// public BsonDocument CreateCommand(ConnectionDescription connectionDescription, ICoreSession session) { + var wireVersion = connectionDescription.MaxWireVersion; + FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion); + var firstBatchSize = _firstBatchSize ?? (_batchSize > 0 ? _batchSize : null); var isShardRouter = connectionDescription.HelloResult.ServerType == ServerType.ShardRouter; diff --git a/src/MongoDB.Driver.Core/Core/Operations/FindProjectionChecker.cs b/src/MongoDB.Driver.Core/Core/Operations/FindProjectionChecker.cs new file mode 100644 index 00000000000..6728ef7cf38 --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Operations/FindProjectionChecker.cs @@ -0,0 +1,64 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Core.Operations +{ + internal static class FindProjectionChecker + { + internal static void ThrowIfAggregationExpressionIsUsedWhenNotSupported(BsonDocument projection, int wireVersion) + { + if (projection == null || Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + return; + } + + foreach (var specification in projection) + { + ThrowIfAggregationExpressionIsUsed(specification); + } + + static void ThrowIfAggregationExpressionIsUsed(BsonElement specification) + { + if (IsAggregationExpression(specification.Value)) + { + var specificationAsDocument = new BsonDocument(specification); + throw new NotSupportedException($"The projection specification {specificationAsDocument} uses an aggregation expression and is not supported with find on servers prior to version 4.4."); + } + } + + static bool IsAggregationExpression(BsonValue value) + { + return value.BsonType switch + { + BsonType.Boolean => false, + _ when value.IsNumeric => false, + _ when value is BsonDocument documentValue => + documentValue.ElementCount == 1 && documentValue.GetElement(0).Name switch + { + "$elemMatch" => false, + "$meta" => false, + "$slice" => false, + _ => true, + }, + _ => true + }; + } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4734Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4734Tests.cs new file mode 100644 index 00000000000..ae2cdeddd51 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4734Tests.cs @@ -0,0 +1,224 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Linq; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira +{ + public class CSharp4734Tests : Linq3IntegrationTest + { + [Theory] + [InlineData("{ X : false }", "{ _id : 1 }")] + [InlineData("{ X : true }", "{ _id : 1, X : 2 }")] + [InlineData("{ X : 0 }", "{ _id : 1 }")] + [InlineData("{ X : 1 }", "{ _id : 1, X : 2 }")] + [InlineData("{ X : -1 }", "{ _id : 1, X : 2 }")] + [InlineData("{ X : { $numberLong : 0 } }", "{ _id : 1 }")] + [InlineData("{ X : { $numberLong : 1 } }", "{ _id : 1, X : 2 }")] + [InlineData("{ X : { $numberLong : -1 } }", "{ _id : 1, X : 2 }")] + [InlineData("{ X : 0.0 }", "{ _id : 1 }")] + [InlineData("{ X : 1.0 }", "{ _id : 1, X : 2 }")] + [InlineData("{ X : -1.0 }", "{ _id : 1, X : 2 }")] + public void Find_with_projections_that_should_work_on_all_server_versions(string projection, string expectedResult) + { + var collection = GetCollection("{ _id : 1, X : 2 }"); + + var find = collection + .Find("{}") + .Project(projection); + + var result = find.Single(); + result.Should().Be(expectedResult); + } + + [Theory] + [InlineData("{}", "{ A : { $slice : 2 } }", "{ _id : 1, A : [1, 2] }")] + [InlineData("{}", "{ A : { $slice : -2 } }", "{ _id : 1, A : [2, 3] }")] + [InlineData("{}", "{ A : { $slice : [1, 2] } }", "{ _id : 1, A : [2, 3] }")] + [InlineData("{}", "{ A : { $slice : [-3, 2] } }", "{ _id : 1, A : [1, 2] }")] + [InlineData("{}", "{ A : { $elemMatch : { $gt : 1 } } }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : true }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : 1 }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : -1 }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : { $numberLong : 1 } }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : { $numberLong : -1 } }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : 1.0 }", "{ _id : 1, A : [2] }")] + [InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : -1.0 }", "{ _id : 1, A : [2] }")] + public void Find_with_array_projection_that_should_work_on_all_server_versions(string filter, string projection, string expectedResult) + { + var collection = GetCollection("{ _id : 1, A : [1, 2, 3] }"); + + var find = collection + .Find(filter) + .Project(projection); + + var result = find.Single(); + result.Should().BeEquivalentTo(expectedResult); // order of result elements varies by server version + } + + [Theory] + [InlineData("{ $text : { $search : 'coffee' } }", "{ score : { $meta : 'textScore' } }", "{ _id : 1, subject : 'coffee', score : 1.0 }")] + public void Find_with_meta_projection_that_should_work_on_all_server_versions(string filter, string projection, string expectedResult) + { + var collection = GetCollection("{ _id : 1, subject : 'coffee' }"); + var keyDefinition = new IndexKeysDefinitionBuilder().Text("subject"); + var indexModel = new CreateIndexModel(keyDefinition); + collection.Indexes.CreateOne(indexModel); + + var find = collection + .Find(filter) + .Project(projection); + + var result = find.Single(); + result.Should().Be(expectedResult); + } + + [Theory] + [InlineData("{ X : 'abc' }", "{ _id : 1, X : 'abc' }")] + [InlineData("{ X : '$Y' }", "{ _id : 1, X : 3 }")] + [InlineData("{ X : { $add : ['$X', '$Y'] } }", "{ _id : 1, X : 5 }")] + [InlineData("{ X : { $literal : true } }", "{ _id : 1, X : true }")] + [InlineData("{ X : { $literal : 1 } }", "{ _id : 1, X : 1 }")] + [InlineData("{ X : { $literal : { $numberLong : 1 } } }", "{ _id : 1, X : { $numberLong : 1 } }")] + [InlineData("{ X : { $literal : 1.0 } }", "{ _id : 1, X : 1.0 }")] + public void Find_with_projections_that_should_work_only_on_servers_newer_than_44(string projection, string expectedResult) + { + var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }"); + + var find = collection + .Find("{}") + .Project(projection); + + var wireVersion = CoreTestConfiguration.MaxWireVersion; + if (Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + var result = find.Single(); + result.Should().BeEquivalentTo(expectedResult); // order of result elements varies by server version + } + else + { + var exception = Record.Exception(() => find.Single()); + exception.Should().BeOfType(); + exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4."); + } + } + + [Theory] + [InlineData("{ X : { Y : 1 } }", "{ _id : 1, X : { Y : 2 } }")] + [InlineData("{ X : { Y : 0 } }", "{ _id : 1, X : { Z : 3 } }")] + [InlineData("{ X : { Y : { $numberLong : 1 } } }", "{ _id : 1, X : { Y : 2 } }")] + [InlineData("{ X : { Y : 1.0 } }", "{ _id : 1, X : { Y : 2 } }")] + public void Find_with_nested_field_projections_that_should_work_only_on_servers_newer_than_44(string projection, string expectedResult) + { + var collection = GetCollection("{ _id : 1, X : { Y : 2, Z : 3 } }"); + + var find = collection + .Find("{}") + .Project(projection); + + var wireVersion = CoreTestConfiguration.MaxWireVersion; + if (Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + var result = find.Single(); + result.Should().BeEquivalentTo(expectedResult); // order of result elements varies by server version + } + else + { + var exception = Record.Exception(() => find.Single()); + exception.Should().BeOfType(); + exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4."); + } + } + + [Fact] // it is sufficient to test only one projection because the rest are tested using Find + public void FindOneAndDelete_with_projections_that_should_work_only_on_servers_newer_than_44() + { + var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }"); + var filter = "{ _id : 1 }"; + var options = new FindOneAndDeleteOptions { Projection = "{ Z : '$Y' }" }; + + var wireVersion = CoreTestConfiguration.MaxWireVersion; + if (Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + var result = collection.FindOneAndDelete(filter, options); + result.Should().BeEquivalentTo("{ _id : 1, Z : 3 }"); // order of result elements varies by server version + } + else + { + var exception = Record.Exception(() => collection.FindOneAndDelete(filter, options)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4."); + } + } + + [Fact] // it is sufficient to test only one projection because the rest are tested using Find + public void FindOneAndReplace_with_projections_that_should_work_only_on_servers_newer_than_44() + { + var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }"); + var filter = "{ _id : 1 }"; + var replacement = BsonDocument.Parse("{ _id : 1, X : 4, Y : 5 }"); + var options = new FindOneAndReplaceOptions { Projection = "{ Z : '$Y' }", ReturnDocument = ReturnDocument.After }; + + var wireVersion = CoreTestConfiguration.MaxWireVersion; + if (Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + var result = collection.FindOneAndReplace(filter, replacement, options); + result.Should().BeEquivalentTo("{ _id : 1, Z : 5 }"); // order of result elements varies by server version + } + else + { + var exception = Record.Exception(() => collection.FindOneAndReplace(filter, replacement, options)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4."); + } + } + + [Fact] // it is sufficient to test only one projection because the rest are tested using Find + public void FindOneAndUpdate_with_projections_that_should_work_only_on_servers_newer_than_44() + { + var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }"); + var filter = "{ _id : 1 }"; + var update = "{ $inc : { Y : 1 } }"; + var options = new FindOneAndUpdateOptions { Projection = "{ Z : '$Y' }", ReturnDocument = ReturnDocument.After }; + + var wireVersion = CoreTestConfiguration.MaxWireVersion; + if (Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + var result = collection.FindOneAndUpdate(filter, update, options); + result.Should().BeEquivalentTo("{ _id : 1, Z : 4 }"); // order of result elements varies by server version + } + else + { + var exception = Record.Exception(() => collection.FindOneAndUpdate(filter, update, options)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4."); + } + } + + private IMongoCollection GetCollection(params string[] documents) + { + var collection = GetCollection("test"); + CreateCollection( + collection, + documents.Select(BsonDocument.Parse)); + return collection; + } + } +}