diff --git a/src/MongoDB.Driver/Core/Operations/GeoNearOperation.cs b/src/MongoDB.Driver/Core/Operations/GeoNearOperation.cs deleted file mode 100644 index 1b9a14f1dd9..00000000000 --- a/src/MongoDB.Driver/Core/Operations/GeoNearOperation.cs +++ /dev/null @@ -1,190 +0,0 @@ -/* Copyright 2015-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.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Driver.Core.Bindings; -using MongoDB.Driver.Core.Connections; -using MongoDB.Driver.Core.Misc; -using MongoDB.Driver.Core.WireProtocol.Messages.Encoders; - -namespace MongoDB.Driver.Core.Operations -{ - internal sealed class GeoNearOperation : IReadOperation - { - private Collation _collation; - private readonly CollectionNamespace _collectionNamespace; - private double? _distanceMultiplier; - private BsonDocument _filter; - private bool? _includeLocs; - private int? _limit; - private double? _maxDistance; - private TimeSpan? _maxTime; - private readonly MessageEncoderSettings _messageEncoderSettings; - private readonly BsonValue _near; - private ReadConcern _readConcern = ReadConcern.Default; - private readonly IBsonSerializer _resultSerializer; - private bool? _spherical; - private bool? _uniqueDocs; - - public GeoNearOperation(CollectionNamespace collectionNamespace, BsonValue near, IBsonSerializer resultSerializer, MessageEncoderSettings messageEncoderSettings) - { - _collectionNamespace = Ensure.IsNotNull(collectionNamespace, nameof(collectionNamespace)); - _near = Ensure.IsNotNull(near, nameof(near)); - _resultSerializer = Ensure.IsNotNull(resultSerializer, nameof(resultSerializer)); - _messageEncoderSettings = Ensure.IsNotNull(messageEncoderSettings, nameof(messageEncoderSettings)); - } - - public Collation Collation - { - get { return _collation; } - set { _collation = value; } - } - - public CollectionNamespace CollectionNamespace - { - get { return _collectionNamespace; } - } - - public double? DistanceMultiplier - { - get { return _distanceMultiplier; } - set { _distanceMultiplier = value; } - } - - public BsonDocument Filter - { - get { return _filter; } - set { _filter = value; } - } - - public bool? IncludeLocs - { - get { return _includeLocs; } - set { _includeLocs = value; } - } - - public int? Limit - { - get { return _limit; } - set { _limit = value; } - } - - public double? MaxDistance - { - get { return _maxDistance; } - set { _maxDistance = value; } - } - - public TimeSpan? MaxTime - { - get { return _maxTime; } - set { _maxTime = Ensure.IsNullOrInfiniteOrGreaterThanOrEqualToZero(value, nameof(value)); } - } - - public MessageEncoderSettings MessageEncoderSettings - { - get { return _messageEncoderSettings; } - } - - public BsonValue Near - { - get { return _near; } - } - - public ReadConcern ReadConcern - { - get { return _readConcern; } - set { _readConcern = Ensure.IsNotNull(value, nameof(value)); } - } - - public IBsonSerializer ResultSerializer - { - get { return _resultSerializer; } - } - - public bool? Spherical - { - get { return _spherical; } - set { _spherical = value; } - } - - public bool? UniqueDocs - { - get { return _uniqueDocs; } - set { _uniqueDocs = value; } - } - - public BsonDocument CreateCommand(ConnectionDescription connectionDescription, ICoreSession session) - { - var readConcern = ReadConcernHelper.GetReadConcernForCommand(session, connectionDescription, _readConcern); - return new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", _near }, - { "limit", () => _limit.Value, _limit.HasValue }, - { "maxDistance", () => _maxDistance.Value, _maxDistance.HasValue }, - { "query", _filter, _filter != null }, - { "spherical", () => _spherical.Value, _spherical.HasValue }, - { "distanceMultiplier", () => _distanceMultiplier.Value, _distanceMultiplier.HasValue }, - { "includeLocs", () => _includeLocs.Value, _includeLocs.HasValue }, - { "uniqueDocs", () => _uniqueDocs.Value, _uniqueDocs.HasValue }, - { "maxTimeMS", () => MaxTimeHelper.ToMaxTimeMS(_maxTime.Value), _maxTime.HasValue }, - { "collation", () => _collation.ToBsonDocument(), _collation != null }, - { "readConcern", readConcern, readConcern != null } - }; - } - - public TResult Execute(IReadBinding binding, CancellationToken cancellationToken) - { - Ensure.IsNotNull(binding, nameof(binding)); - using (var channelSource = binding.GetReadChannelSource(cancellationToken)) - using (var channel = channelSource.GetChannel(cancellationToken)) - using (var channelBinding = new ChannelReadBinding(channelSource.Server, channel, binding.ReadPreference, binding.Session.Fork())) - { - var operation = CreateOperation(channel, channelBinding); - return operation.Execute(channelBinding, cancellationToken); - } - } - - public async Task ExecuteAsync(IReadBinding binding, CancellationToken cancellationToken) - { - Ensure.IsNotNull(binding, nameof(binding)); - using (var channelSource = await binding.GetReadChannelSourceAsync(cancellationToken).ConfigureAwait(false)) - using (var channel = await channelSource.GetChannelAsync(cancellationToken).ConfigureAwait(false)) - using (var channelBinding = new ChannelReadBinding(channelSource.Server, channel, binding.ReadPreference, binding.Session.Fork())) - { - var operation = CreateOperation(channel, channelBinding); - return await operation.ExecuteAsync(channelBinding, cancellationToken).ConfigureAwait(false); - } - } - - private ReadCommandOperation CreateOperation(IChannel channel, IBinding binding) - { - var command = CreateCommand(channel.ConnectionDescription, binding.Session); - return new ReadCommandOperation( - _collectionNamespace.DatabaseNamespace, - command, - _resultSerializer, - _messageEncoderSettings) - { - RetryRequested = false - }; - } - } -} diff --git a/src/MongoDB.Driver/GeoNearOptions.cs b/src/MongoDB.Driver/GeoNearOptions.cs new file mode 100644 index 00000000000..5211fa35719 --- /dev/null +++ b/src/MongoDB.Driver/GeoNearOptions.cs @@ -0,0 +1,71 @@ +/* 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 MongoDB.Bson.Serialization; + +namespace MongoDB.Driver +{ + /// + /// Represents options for the $geoNear stage. + /// + public class GeoNearOptions + { + /// + /// Gets or sets the output field that contains the calculated distance. Required if querying a time-series collection. + /// Optional for non-time series collections in MongoDB 8.1+ + /// + public FieldDefinition DistanceField { get; set; } + + /// + /// Gets or sets the factor to multiply all distances returned by the query. + /// + public double? DistanceMultiplier { get; set; } + + /// + /// Gets or sets the output field that identifies the location used to calculate the distance. + /// + public FieldDefinition IncludeLocs { get; set; } + + /// + /// Gets or sets the geospatial indexed field used when calculating the distance. + /// + public string Key { get; set; } + + /// + /// Gets or sets the max distance from the center point that the documents can be. + /// + public double? MaxDistance { get; set; } + + /// + /// Gets or sets the min distance from the center point that the documents can be. + /// + public double? MinDistance { get; set; } + + /// + /// Gets or sets the output serializer. + /// + public IBsonSerializer OutputSerializer { get; set; } + + /// + /// Gets or sets the query that limits the results to the documents that match the query. + /// + public FilterDefinition Query { get; set; } + + /// + /// Gets or sets the spherical option which determines how to calculate the distance between two points. + /// + public bool? Spherical { get; set; } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/IAggregateFluentExtensions.cs b/src/MongoDB.Driver/IAggregateFluentExtensions.cs index c19b94ec667..a9e47c2a3d3 100644 --- a/src/MongoDB.Driver/IAggregateFluentExtensions.cs +++ b/src/MongoDB.Driver/IAggregateFluentExtensions.cs @@ -22,6 +22,7 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.GeoJsonObjectModel; namespace MongoDB.Driver { @@ -252,6 +253,44 @@ public static IAggregateFluent Facet( return aggregate.AppendStage(PipelineStageDefinitionBuilder.Facet(facets)); } + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the result. + /// The type of the new result. + /// The type of the coordinates for the point. + /// The aggregate. + /// The point for which to find the closest documents. + /// The options. + /// The fluent aggregate interface. + public static IAggregateFluent GeoNear( + this IAggregateFluent aggregate, + GeoJsonPoint near, + GeoNearOptions options = null) + where TCoordinates : GeoJsonCoordinates + { + Ensure.IsNotNull(aggregate, nameof(aggregate)); + return aggregate.AppendStage(PipelineStageDefinitionBuilder.GeoNear, TNewResult>(near, options)); + } + + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the result. + /// The type of the new result. + /// The aggregate. + /// The point for which to find the closest documents. + /// The options. + /// The fluent aggregate interface. + public static IAggregateFluent GeoNear( + this IAggregateFluent aggregate, + double[] near, + GeoNearOptions options = null) + { + Ensure.IsNotNull(aggregate, nameof(aggregate)); + return aggregate.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + } + /// /// Appends a $graphLookup stage to the pipeline. /// diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstGeoNearStage.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstGeoNearStage.cs index 2d4dae4ba61..9a5945a4a6d 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstGeoNearStage.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstGeoNearStage.cs @@ -45,7 +45,7 @@ public AstGeoNearStage( string key) { _near = Ensure.IsNotNull(near, nameof(near)); - _distanceField = Ensure.IsNotNull(distanceField, nameof(distanceField)); + _distanceField = distanceField; _spherical = spherical; _maxDistance = maxDistance; _query = query; @@ -80,7 +80,7 @@ public override BsonValue Render() { "$geoNear", new BsonDocument { { "near", _near }, - { "distanceField", _distanceField }, + { "distanceField", _distanceField, _distanceField != null }, { "spherical", () => _spherical.Value, _spherical.HasValue }, { "maxDistance", () => _maxDistance.Value, _maxDistance.HasValue }, { "query", _query, _query != null }, diff --git a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs index 4aae84c5732..7330dda1951 100644 --- a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs @@ -20,6 +20,7 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; namespace MongoDB.Driver @@ -484,6 +485,46 @@ public static PipelineDefinition For(IBsonSerializer(inputSerializer); } + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the input documents. + /// The type of the intermediate documents. + /// The type of the output documents. + /// The type of the coordinates for the point. + /// The pipeline. + /// The point for which to find the closest documents. + /// The options. + /// A new pipeline with an additional stage. + public static PipelineDefinition GeoNear( + this PipelineDefinition pipeline, + GeoJsonPoint near, + GeoNearOptions options = null) + where TCoordinates : GeoJsonCoordinates + { + Ensure.IsNotNull(pipeline, nameof(pipeline)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + } + + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the input documents. + /// The type of the intermediate documents. + /// The type of the output documents. + /// The pipeline. + /// The point for which to find the closest documents. + /// The options. + /// A new pipeline with an additional stage. + public static PipelineDefinition GeoNear( + this PipelineDefinition pipeline, + double[] near, + GeoNearOptions options = null) + { + Ensure.IsNotNull(pipeline, nameof(pipeline)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + } + /// /// Appends a $graphLookup stage to the pipeline. /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 53f6017f6e8..2340e8880f9 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -21,8 +21,8 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; -using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Linq; using MongoDB.Driver.Linq.Linq3Implementation; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters; @@ -605,6 +605,83 @@ public static PipelineStageDefinition Facet( return Facet((IEnumerable>)facets); } + /// + /// Creates a $geoNear stage. + /// + /// The type of the input documents. + /// The type of the output documents. + /// The type of the point. This could be a , a 2d array or embedded document. + /// The point for which to find the closest documents. + /// The options. + /// The stage. + internal static PipelineStageDefinition GeoNear( + TPoint near, + GeoNearOptions options = null) + // where TPoint is either a GeoJsonPoint or a legacy coordinate array + { + const string operatorName = "$geoNear"; + var stage = new DelegatedPipelineStageDefinition( + operatorName, + args => + { + ClientSideProjectionHelper.ThrowIfClientSideProjection(args.DocumentSerializer, operatorName); + var pointSerializer = args.SerializerRegistry.GetSerializer(); + var outputSerializer = options?.OutputSerializer ?? args.GetSerializer(); + var outputRenderArgs = args.WithNewDocumentType(outputSerializer); + var geoNearOptions = new BsonDocument + { + { "near", pointSerializer.ToBsonValue(near)}, + { "distanceField", options?.DistanceField?.Render(outputRenderArgs).FieldName, options?.DistanceField != null }, + { "maxDistance", options?.MaxDistance, options?.MaxDistance != null }, + { "minDistance", options?.MinDistance, options?.MinDistance != null }, + { "distanceMultiplier", options?.DistanceMultiplier, options?.DistanceMultiplier != null }, + { "key", options?.Key, options?.Key != null }, + { "query", options?.Query?.Render(args), options?.Query != null }, + { "includeLocs", options?.IncludeLocs?.Render(outputRenderArgs).FieldName, options?.IncludeLocs != null }, + { "spherical", options?.Spherical, options?.Spherical != null } + }; + + return new RenderedPipelineStageDefinition(operatorName, new BsonDocument(operatorName, geoNearOptions), outputSerializer); + }); + + return stage; + } + + /// + /// Creates a $geoNear stage. + /// + /// The type of the input documents. + /// The type of the output documents. + /// The type of the coordinates for the point. + /// The point for which to find the closest documents. + /// The options. + /// The stage. + public static PipelineStageDefinition GeoNear( + GeoJsonPoint near, + GeoNearOptions options = null) + where TCoordinates : GeoJsonCoordinates + { + Ensure.IsNotNull(near, nameof(near)); + return GeoNear, TOutput>(near, options); + } + + /// + /// Creates a $geoNear stage. + /// + /// The type of the input documents. + /// The type of the output documents. + /// The point for which to find the closest documents. + /// The options. + /// The stage. + public static PipelineStageDefinition GeoNear( + double[] near, + GeoNearOptions options = null) + { + Ensure.IsNotNull(near, nameof(near)); + Ensure.That(near.Length, len => len is >= 2 and <= 3, nameof(near), "Legacy coordinates array should have 2 or 3 coordinates."); + return GeoNear(near, options); + } + /// /// Creates a $graphLookup stage. /// diff --git a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs new file mode 100644 index 00000000000..0b80fb3af78 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs @@ -0,0 +1,180 @@ +/* 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 FluentAssertions; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.GeoJsonObjectModel; +using Xunit; + +namespace MongoDB.Driver.Tests +{ + public class AggregateGeoNearTests : IntegrationTest + { + public AggregateGeoNearTests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void GeoNear_omitting_distanceField_should_return_expected_result() + { + RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); + + var collection = Fixture.GeoCollection; + var result = collection + .Aggregate() + .GeoNear( + GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), + new GeoNearOptions + { + MaxDistance = 2, + Key = "GeoJsonPointLocation", + Query = Builders.Filter.Eq(p => p.Category, + "Parks"), + Spherical = true + }) + .ToList(); + + result.Count.Should().Be(1); + result[0].Name.Should().Be("Sara D. Roosevelt Park"); + } + + [Fact] + public void GeoNear_using_pipeline_should_return_expected_result() + { + var collection = Fixture.GeoCollection; + var pipeline = new EmptyPipelineDefinition() + .GeoNear( + [-73.99279, 40.719296], + new GeoNearOptions + { + DistanceField = "Distance", + MaxDistance = 0.000313917534, + Key = "LegacyCoordinateLocation", + Query = Builders.Filter.Eq(p => p.Category, + "Parks"), + Spherical = true + }); + + var result = collection.Aggregate(pipeline).ToList(); + + result.Count.Should().Be(1); + result[0].Name.Should().Be("Sara D. Roosevelt Park"); + } + + [Fact] + public void GeoNear_with_array_legacy_coordinates_should_return_expected_result() + { + var collection = Fixture.GeoCollection; + + var result = collection + .Aggregate() + .GeoNear( + [-73.99279, 40.719296], + new GeoNearOptions + { + DistanceField = "Distance", + MaxDistance = 0.000313917534, + Key = "LegacyCoordinateLocation", + Query = Builders.Filter.Eq(p => p.Category, + "Parks"), + Spherical = true + }) + .ToList(); + + result.Count.Should().Be(1); + result[0].Name.Should().Be("Sara D. Roosevelt Park"); + } + + [Fact] + public void GeoNear_with_GeoJsonPoint_should_return_expected_result() + { + var collection = Fixture.GeoCollection; + + var result = collection + .Aggregate() + .GeoNear( + GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), + new GeoNearOptions + { + DistanceField = "Distance", + MaxDistance = 2, + Key = "GeoJsonPointLocation", + Query = Builders.Filter.Eq(p => p.Category, + "Parks"), + Spherical = true + }) + .ToList(); + + result.Count.Should().Be(1); + result[0].Name.Should().Be("Sara D. Roosevelt Park"); + } + + [BsonIgnoreExtraElements] + public class Place + { + public string Name { get; set; } + public GeoJsonPoint GeoJsonPointLocation { get; set; } + public double[] LegacyCoordinateLocation { get; set; } + public string Category { get; set; } + } + + [BsonIgnoreExtraElements] + public class PlaceResult : Place + { + [BsonElement("dist")] + public double Distance { get; set; } + } + + public sealed class ClassFixture : MongoDatabaseFixture + { + public IMongoCollection GeoCollection { get; private set; } + + protected override void InitializeFixture() + { + GeoCollection = CreateCollection("geoCollection"); + GeoCollection.InsertMany([ + new() + { + Name = "Central Park", + GeoJsonPointLocation = GeoJson.Point(GeoJson.Geographic(-73.97, 40.77)), + LegacyCoordinateLocation = [-73.97, 40.77], + Category = "Parks" + }, + new() + { + Name = "Sara D. Roosevelt Park", + GeoJsonPointLocation = GeoJson.Point(GeoJson.Geographic(-73.9928, 40.7193)), + LegacyCoordinateLocation = [-73.9928, 40.7193], + Category = "Parks" + }, + new() + { + Name = "Polo Grounds", + GeoJsonPointLocation = GeoJson.Point(GeoJson.Geographic(-73.9375, 40.8303)), + LegacyCoordinateLocation = [-73.9375, 40.8303], + Category = "Stadiums" + } + ]); + + GeoCollection.Indexes.CreateOne( + new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); + GeoCollection.Indexes.CreateOne( + new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); + } + } + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Core/Operations/GeoNearOperationTests.cs b/tests/MongoDB.Driver.Tests/Core/Operations/GeoNearOperationTests.cs deleted file mode 100644 index 93de6ccd054..00000000000 --- a/tests/MongoDB.Driver.Tests/Core/Operations/GeoNearOperationTests.cs +++ /dev/null @@ -1,651 +0,0 @@ -/* Copyright 2013-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.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver.Core.Clusters; -using MongoDB.Driver.Core.Misc; -using MongoDB.Driver.Core.TestHelpers; -using MongoDB.Driver.Core.TestHelpers.XunitExtensions; -using MongoDB.TestHelpers.XunitExtensions; -using Xunit; - -namespace MongoDB.Driver.Core.Operations -{ - public class GeoNearOperationTests : OperationTestBase - { - private readonly BsonValue _near = new BsonArray { 1, 2 }; - private readonly IBsonSerializer _resultSerializer = BsonDocumentSerializer.Instance; - - [Fact] - public void constructor_should_initialize_instance() - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.CollectionNamespace.Should().BeSameAs(_collectionNamespace); - subject.Near.Should().BeSameAs(_near); - subject.ResultSerializer.Should().BeSameAs(_resultSerializer); - subject.MessageEncoderSettings.Should().BeSameAs(_messageEncoderSettings); - - subject.Collation.Should().BeNull(); - subject.DistanceMultiplier.Should().NotHaveValue(); - subject.Filter.Should().BeNull(); - subject.IncludeLocs.Should().NotHaveValue(); - subject.Limit.Should().NotHaveValue(); - subject.MaxDistance.Should().NotHaveValue(); - subject.MaxTime.Should().NotHaveValue(); - subject.ReadConcern.Should().BeSameAs(ReadConcern.Default); - subject.Spherical.Should().NotHaveValue(); - subject.UniqueDocs.Should().NotHaveValue(); - } - - [Fact] - public void Constructor_should_throw_when_collectionNamespace_is_null() - { - var exception = Record.Exception(() => new GeoNearOperation(null, _near, _resultSerializer, _messageEncoderSettings)); - - var argumentNullException = exception.Should().BeOfType().Subject; - argumentNullException.ParamName.Should().Be("collectionNamespace"); - } - - [Fact] - public void Constructor_should_throw_when_near_is_null() - { - var exception = Record.Exception(() => new GeoNearOperation(_collectionNamespace, null, _resultSerializer, _messageEncoderSettings)); - - var argumentNullException = exception.Should().BeOfType().Subject; - argumentNullException.ParamName.Should().Be("near"); - } - - [Fact] - public void Constructor_should_throw_when_resultSerializer_is_null() - { - var exception = Record.Exception(() => new GeoNearOperation(_collectionNamespace, _near, null, _messageEncoderSettings)); - - var argumentNullException = exception.Should().BeOfType().Subject; - argumentNullException.ParamName.Should().Be("resultSerializer"); - } - - [Fact] - public void Constructor_should_throw_when_messageEncoderSettings_is_null() - { - var exception = Record.Exception(() => new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, null)); - - var argumentNullException = exception.Should().BeOfType().Subject; - argumentNullException.ParamName.Should().Be("messageEncoderSettings"); - } - - [Theory] - [ParameterAttributeData] - public void Collation_get_and_set_should_work( - [Values(null, "en_US", "fr_CA")] - string locale) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - var value = locale == null ? null : new Collation(locale); - - subject.Collation = value; - var result = subject.Collation; - - result.Should().BeSameAs(value); - } - - [Theory] - [ParameterAttributeData] - public void DistanceMultiplier_get_and_set_should_work( - [Values(null, 1.0, 2.0)] - double? value) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.DistanceMultiplier = value; - var result = subject.DistanceMultiplier; - - result.Should().Be(value); - } - - [Theory] - [ParameterAttributeData] - public void Filter_get_and_set_should_work( - [Values(null, "{ x : 1 }", "{ x : 2 }")] - string valueString) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - var value = valueString == null ? null : BsonDocument.Parse(valueString); - - subject.Filter = value; - var result = subject.Filter; - - result.Should().BeSameAs(value); - } - - [Theory] - [ParameterAttributeData] - public void IncludeLocs_get_and_set_should_work( - [Values(null, false, true)] - bool? value) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.IncludeLocs = value; - var result = subject.IncludeLocs; - - result.Should().Be(value); - } - - [Theory] - [ParameterAttributeData] - public void Limit_get_and_set_should_work( - [Values(null, 1, 2)] - int? value) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.Limit = value; - var result = subject.Limit; - - result.Should().Be(value); - } - - [Theory] - [ParameterAttributeData] - public void MaxDistance_get_and_set_should_work( - [Values(null, 1.0, 2.0)] - double? value) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.MaxDistance = value; - var result = subject.MaxDistance; - - result.Should().Be(value); - } - - [Theory] - [ParameterAttributeData] - public void MaxTime_get_and_set_should_work( - [Values(-10000, 0, 1, 10000, 99999)] long maxTimeTicks) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - var value = TimeSpan.FromTicks(maxTimeTicks); - - subject.MaxTime = value; - var result = subject.MaxTime; - - result.Should().Be(value); - } - - [Theory] - [ParameterAttributeData] - public void MaxTime_set_should_throw_when_value_is_invalid( - [Values(-10001, -9999, -1)] long maxTimeTicks) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - var value = TimeSpan.FromTicks(maxTimeTicks); - - var exception = Record.Exception(() => subject.MaxTime = value); - - var e = exception.Should().BeOfType().Subject; - e.ParamName.Should().Be("value"); - } - - [Theory] - [ParameterAttributeData] - public void ReadConcern_get_and_set_should_work( - [Values(ReadConcernLevel.Linearizable, ReadConcernLevel.Local)] - ReadConcernLevel level) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - var value = new ReadConcern(level); - - subject.ReadConcern = value; - var result = subject.ReadConcern; - - result.Should().Be(value); - } - - [Fact] - public void ReadConcern_set_should_throw_when_value_is_null() - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - var exception = Record.Exception(() => subject.ReadConcern = null); - - var argumentNulException = exception.Should().BeOfType().Subject; - argumentNulException.ParamName.Should().Be("value"); - } - - [Theory] - [ParameterAttributeData] - public void Spherical_get_and_set_should_work( - [Values(null, false, true)] - bool? value) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.Spherical = value; - var result = subject.Spherical; - - result.Should().Be(value); - } - - [Theory] - [ParameterAttributeData] - public void UniqueDocs_get_and_set_should_work( - [Values(null, false, true)] - bool? value) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - subject.UniqueDocs = value; - var result = subject.UniqueDocs; - - result.Should().Be(value); - } - - [Fact] - public void CreateCommand_should_return_expected_result() - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_Collation_is_set( - [Values(null, "en_US", "fr_CA")] - string locale) - { - var collation = locale == null ? null : new Collation(locale); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - Collation = collation - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "collation", () => collation.ToBsonDocument(), collation != null } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_DistanceMultiplier_is_set( - [Values(null, 1.0, 2.0)] - double? distanceMultiplier) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - DistanceMultiplier = distanceMultiplier - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "distanceMultiplier", () => distanceMultiplier.Value, distanceMultiplier.HasValue } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_Filter_is_set( - [Values(null, "{ x : 1 }", "{ x : 2 }")] - string filterString) - { - var filter = filterString == null ? null : BsonDocument.Parse(filterString); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - Filter = filter - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "query", () => filter, filter != null } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_IncludeLocs_is_set( - [Values(null, false, true)] - bool? includeLocs) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - IncludeLocs = includeLocs - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "includeLocs", () => includeLocs.Value, includeLocs.HasValue } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_Limit_is_set( - [Values(null, 1, 2)] - int? limit) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - Limit = limit - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "limit", () => limit.Value, limit.HasValue } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_MaxDistance_is_set( - [Values(null, 1.0, 2.0)] - double? maxDistance) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - MaxDistance = maxDistance - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "maxDistance", () => maxDistance.Value, maxDistance.HasValue } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [InlineData(-10000, 0)] - [InlineData(0, 0)] - [InlineData(1, 1)] - [InlineData(9999, 1)] - [InlineData(10000, 1)] - [InlineData(10001, 2)] - public void CreateCommand_should_return_expected_result_when_MaxTime_is_set(long maxTimeTicks, int expectedMaxTimeMS) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - MaxTime = TimeSpan.FromTicks(maxTimeTicks) - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "maxTimeMS", expectedMaxTimeMS } - }; - result.Should().Be(expectedResult); - result["maxTimeMS"].BsonType.Should().Be(BsonType.Int32); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_ReadConcern_is_set( - [Values(null, ReadConcernLevel.Linearizable, ReadConcernLevel.Local)] - ReadConcernLevel? level) - { - var readConcern = new ReadConcern(level); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - ReadConcern = readConcern - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "readConcern", () => readConcern.ToBsonDocument(), !readConcern.IsServerDefault } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_Spherical_is_set( - [Values(null, false, true)] - bool? spherical) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - Spherical = spherical - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "spherical", () => spherical.Value, spherical.HasValue } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_expected_result_when_UniqueDocs_is_set( - [Values(null, false, true)] - bool? uniqueDocs) - { - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - UniqueDocs = uniqueDocs - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(); - var session = OperationTestHelper.CreateSession(); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "uniqueDocs", () => uniqueDocs.Value, uniqueDocs.HasValue } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void CreateCommand_should_return_the_expected_result_when_using_causal_consistency( - [Values(null, ReadConcernLevel.Linearizable, ReadConcernLevel.Local)] - ReadConcernLevel? level) - { - var readConcern = new ReadConcern(level); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - ReadConcern = readConcern - }; - - var connectionDescription = OperationTestHelper.CreateConnectionDescription(supportsSessions: true); - var session = OperationTestHelper.CreateSession(true, new BsonTimestamp(100)); - - var result = subject.CreateCommand(connectionDescription, session); - - var expectedReadConcernDocument = readConcern.ToBsonDocument(); - expectedReadConcernDocument["afterClusterTime"] = new BsonTimestamp(100); - - var expectedResult = new BsonDocument - { - { "geoNear", _collectionNamespace.CollectionName }, - { "near", new BsonArray { 1, 2 } }, - { "readConcern", expectedReadConcernDocument } - }; - result.Should().Be(expectedResult); - } - - [Theory] - [ParameterAttributeData] - public void Execute_should_return_expected_result( - [Values(false, true)] - bool async) - { - RequireServer.Check().Supports(Feature.GeoNearCommand); - EnsureTestData(); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - var result = ExecuteOperation(subject, async); - - result["results"].AsBsonArray.Count.Should().Be(5); - result["results"].AsBsonArray.Select(i => i["dis"].ToDouble()).Should().BeInAscendingOrder(); - } - - [Theory] - [ParameterAttributeData] - public void Execute_should_return_expected_result_when_Collation_is_set( - [Values(false, true)] - bool caseSensitive, - [Values(false, true)] - bool async) - { - RequireServer.Check().Supports(Feature.GeoNearCommand); - EnsureTestData(); - var collation = new Collation("en_US", caseLevel: caseSensitive, strength: CollationStrength.Primary); - var filter = BsonDocument.Parse("{ x : 'x' }"); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings) - { - Collation = collation, - Filter = filter - }; - - var result = ExecuteOperation(subject, async); - - result["results"].AsBsonArray.Count.Should().Be(caseSensitive ? 2 : 5); - result["results"].AsBsonArray.Select(i => i["dis"].ToDouble()).Should().BeInAscendingOrder(); - } - - [Theory] - [ParameterAttributeData] - public void Execute_should_send_session_id_when_supported( - [Values(false, true)] bool async) - { - RequireServer.Check().Supports(Feature.GeoNearCommand); - EnsureTestData(); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - - VerifySessionIdWasSentWhenSupported(subject, "geoNear", async); - } - - [Theory] - [ParameterAttributeData] - public void Execute_should_throw_when_maxTime_is_exceeded( - [Values(false, true)] bool async) - { - RequireServer.Check().Supports(Feature.GeoNearCommand).ClusterTypes(ClusterType.Standalone, ClusterType.ReplicaSet); - var subject = new GeoNearOperation(_collectionNamespace, _near, _resultSerializer, _messageEncoderSettings); - subject.MaxTime = TimeSpan.FromSeconds(9001); - - using (var failPoint = FailPoint.ConfigureAlwaysOn(_cluster, _session, FailPointName.MaxTimeAlwaysTimeout)) - { - var exception = Record.Exception(() => ExecuteOperation(subject, failPoint.Binding, async)); - - exception.Should().BeOfType(); - } - } - - // helper methods - private void EnsureTestData() - { - RunOncePerFixture(() => - { - DropCollection(); - Insert(Enumerable.Range(1, 5).Select(id => new BsonDocument - { - { "_id", id }, - { "Location", new BsonArray { id, id + 1 } }, - { "x", (id % 2) == 0 ? "x" : "X" } // some lower case and some upper case - })); - CreateIndexes(new CreateIndexRequest(new BsonDocument("Location", "2d"))); - }); - } - } -} diff --git a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs index ec15ce0bc32..47b0dc17ca2 100644 --- a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs @@ -19,7 +19,7 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; using Moq; using Xunit; @@ -107,10 +107,65 @@ public void ChangeStreamSplitLargeEvent_should_throw_when_pipeline_is_null() } [Fact] - public void Lookup_should_throw_when_pipeline_is_null() + public void GeoNear_with_geojson_point_should_add_the_expected_stage() + { + var pipeline = new EmptyPipelineDefinition(); + + var result = pipeline.GeoNear( + GeoJson.Point(GeoJson.Geographic(34, 67)), + new GeoNearOptions + { + DistanceField = "calculatedDistance" + }); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages.Count.Should().Be(1); + stages[0].Should().Be("""{ "$geoNear" : { "near" : { "type" : "Point", "coordinates" : [34.0, 67.0] }, "distanceField" : "calculatedDistance" } }"""); + } + + [Fact] + public void GeoNear_with_array_should_add_the_expected_stage() + { + var pipeline = new EmptyPipelineDefinition(); + + var result = pipeline.GeoNear( + [34.0, 67.0], + new GeoNearOptions + { + DistanceField = "calculatedDistance" + }); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages.Count.Should().Be(1); + stages[0].Should().Be("""{ "$geoNear" : { "near" : [34.0, 67.0], "distanceField" : "calculatedDistance" } }"""); + } + + [Fact] + public void GeoNear_should_throw_when_pipeline_is_null() { - RequireServer.Check(); + PipelineDefinition pipeline = null; + + var exception = Record.Exception(() => + pipeline.GeoNear([1.0, 2.0])); + + exception.Should().BeOfType() + .Which.ParamName.Should().Be("pipeline"); + } + [Fact] + public void GeoNear_should_throw_when_near_point_is_null() + { + var pipeline = new EmptyPipelineDefinition(); + + var exception = Record.Exception(() => pipeline.GeoNear(null)); + + exception.Should().BeOfType() + .Which.ParamName.Should().Be("near"); + } + + [Fact] + public void Lookup_should_throw_when_pipeline_is_null() + { PipelineDefinition> pipeline = null; IMongoCollection collection = null; diff --git a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs index 71f1b962477..03772ccb529 100644 --- a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs @@ -21,6 +21,7 @@ using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.GeoJsonObjectModel; using Moq; using Xunit; @@ -133,6 +134,91 @@ public void ChangeStreamSplitLargeEvent_should_return_the_expected_result() stage.Document.Should().Be("{ $changeStreamSplitLargeEvent : { } }"); } + [Fact] + public void GeoNear_with_array_should_return_the_expected_result() + { + var result = PipelineStageDefinitionBuilder.GeoNear( + [34.0, 67.0], + new GeoNearOptions + { + DistanceField = "calculatedDistance", + MaxDistance = 3, + IncludeLocs = "usedLocation", + Spherical = true, + Query = new BsonDocument("testfield", "testvalue") + }); + + var stage = RenderStage(result); + stage.Document.Should().Be(""" + { + "$geoNear" : { + "near" : [34.0, 67.0], + "distanceField" : "calculatedDistance", + "maxDistance" : 3.0, + "query" : { "testfield" : "testvalue" }, + "includeLocs" : "usedLocation", + "spherical" : true + } + } + """); + } + + [Fact] + public void GeoNear_with_geojson_point_should_return_the_expected_result() + { + var result = PipelineStageDefinitionBuilder.GeoNear( + GeoJson.Point(GeoJson.Geographic(34, 67)), + new GeoNearOptions + { + DistanceField = "calculatedDistance", + MaxDistance = 3, + IncludeLocs = "usedLocation", + Spherical = true, + Query = new BsonDocument("testfield", "testvalue") + }); + + var stage = RenderStage(result); + stage.Document.Should().Be(""" + { + "$geoNear" : { + "near" : { "type" : "Point", "coordinates" : [34.0, 67.0] }, + "distanceField" : "calculatedDistance", + "maxDistance" : 3.0, + "query" : { "testfield" : "testvalue" }, + "includeLocs" : "usedLocation", + "spherical" : true + } + } + """); + } + + [Fact] + public void GeoNear_with_no_options_should_return_the_expected_result() + { + var result = + PipelineStageDefinitionBuilder.GeoNear( + GeoJson.Point(GeoJson.Geographic(34, 67))); + + var stage = RenderStage(result); + stage.Document.Should().Be("""{ "$geoNear" : { "near" : { "type" : "Point", "coordinates" : [34.0, 67.0] } } }"""); + } + + [Fact] + public void GeoNear_with_wrong_legacy_coordinates_should_throw_exception() + { + Assert.Throws(() => + { + var result = + PipelineStageDefinitionBuilder.GeoNear([34.0, 67.0, 23.0, 34.5]); + }); + + Assert.Throws(() => + { + var result = + PipelineStageDefinitionBuilder.GeoNear([34.0]); + }); + } + [Fact] public void GraphLookup_with_many_to_one_parameters_should_return_expected_result() {