From e202381ad6ccbeaeeb7deec1c9df5ce0da9f3499 Mon Sep 17 00:00:00 2001 From: adelinowona Date: Fri, 21 Feb 2025 21:19:00 -0500 Subject: [PATCH 01/11] CSHARP-5505: Add $geoNear stage aggregation builders --- src/MongoDB.Driver/AggregateFluent.cs | 7 + src/MongoDB.Driver/AggregateFluentBase.cs | 8 + .../Core/Operations/GeoNearOperation.cs | 190 ----- src/MongoDB.Driver/GeoNearOptions.cs | 64 ++ src/MongoDB.Driver/IAggregateFluent.cs | 12 + .../IAggregateFluentExtensions.cs | 20 + .../Ast/Stages/AstGeoNearStage.cs | 4 +- .../PipelineDefinitionBuilder.cs | 20 + .../PipelineStageDefinitionBuilder.cs | 61 +- .../AggregateGeoNearTests.cs | 183 +++++ .../Core/Operations/GeoNearOperationTests.cs | 651 ------------------ .../PipelineDefinitionBuilderTests.cs | 51 +- .../PipelineStageDefinitionBuilderTests.cs | 63 ++ 13 files changed, 487 insertions(+), 847 deletions(-) delete mode 100644 src/MongoDB.Driver/Core/Operations/GeoNearOperation.cs create mode 100644 src/MongoDB.Driver/GeoNearOptions.cs create mode 100644 tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs delete mode 100644 tests/MongoDB.Driver.Tests/Core/Operations/GeoNearOperationTests.cs diff --git a/src/MongoDB.Driver/AggregateFluent.cs b/src/MongoDB.Driver/AggregateFluent.cs index 90bd9823ce1..0d1d60adbad 100644 --- a/src/MongoDB.Driver/AggregateFluent.cs +++ b/src/MongoDB.Driver/AggregateFluent.cs @@ -129,6 +129,13 @@ public override IAggregateFluent Facet( { return WithPipeline(_pipeline.Facet(facets, options)); } + + public override IAggregateFluent GeoNear( + TPoint near, + GeoNearOptions options = null) + { + return WithPipeline(_pipeline.GeoNear(near, options)); + } public override IAggregateFluent GraphLookup( IMongoCollection from, diff --git a/src/MongoDB.Driver/AggregateFluentBase.cs b/src/MongoDB.Driver/AggregateFluentBase.cs index 51381f930bd..0dd1736aea3 100644 --- a/src/MongoDB.Driver/AggregateFluentBase.cs +++ b/src/MongoDB.Driver/AggregateFluentBase.cs @@ -124,6 +124,14 @@ public virtual IAggregateFluent Facet( throw new NotImplementedException(); } + /// + public virtual IAggregateFluent GeoNear( + TPoint near, + GeoNearOptions options = null) where TPoint : class + { + throw new NotImplementedException(); + } + /// public virtual IAggregateFluent GraphLookup( IMongoCollection from, 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..1c507f523b9 --- /dev/null +++ b/src/MongoDB.Driver/GeoNearOptions.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. + */ + +namespace MongoDB.Driver +{ + /// + /// Represents options for the $geoNear stage. + /// + public class GeoNearOptions + { + /// + /// Gets or sets the distance field. Required if querying a time-series collection. + /// + public string DistanceField { get; set; } + + /// + /// Gets or sets the distance multiplier. + /// + public double? DistanceMultiplier { get; set; } + + /// + /// Gets or sets the output field that identifies the location used to calculate the distance. + /// + public string 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 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/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index dfb0b821ec8..768355cbb07 100644 --- a/src/MongoDB.Driver/IAggregateFluent.cs +++ b/src/MongoDB.Driver/IAggregateFluent.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; namespace MongoDB.Driver @@ -175,6 +176,17 @@ IAggregateFluent Facet( IEnumerable> facets, AggregateFacetOptions options = null); + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// 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 fluent aggregate interface. + IAggregateFluent GeoNear( + TPoint near, + GeoNearOptions options = null) where TPoint : class; + /// /// Appends a $graphLookup stage to the pipeline. /// diff --git a/src/MongoDB.Driver/IAggregateFluentExtensions.cs b/src/MongoDB.Driver/IAggregateFluentExtensions.cs index c19b94ec667..b696d8fd207 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,25 @@ public static IAggregateFluent Facet( return aggregate.AppendStage(PipelineStageDefinitionBuilder.Facet(facets)); } + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the new result. + /// The type of the result. + /// The type of the point. This could be a , a 2d array or embedded document. + /// The aggregate. + /// The point for which to find the closest documents. + /// The options. + /// The fluent aggregate interface. + public static IAggregateFluent GeoNear( + this IAggregateFluent aggregate, + TPoint near, + GeoNearOptions options = null) where TPoint : class + { + 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..8c0631e2a25 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 @@ -483,6 +484,25 @@ public static PipelineDefinition For(IBsonSerializer(inputSerializer); } + + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// 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 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, + TPoint near, + GeoNearOptions options = null) where TPoint : class + { + 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..7d244619682 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -15,14 +15,15 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Linq.Expressions; using System.Reflection; 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; @@ -604,6 +605,64 @@ 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. + public static PipelineStageDefinition GeoNear( + TPoint near, + GeoNearOptions options = null) where TPoint : class + { + Ensure.IsNotNull(near, nameof(near)); + + const string operatorName = "$geoNear"; + var stage = new DelegatedPipelineStageDefinition( + operatorName, + args => + { + ClientSideProjectionHelper.ThrowIfClientSideProjection(args.DocumentSerializer, operatorName); + var pointSerializer = args.SerializerRegistry.GetSerializer(); + var outputRenderArgs = args.WithNewDocumentType(args.SerializerRegistry.GetSerializer()); + var geoNearOptions = new BsonDocument + { + { "near", pointSerializer.ToBsonValue(near)}, + { "distanceField", options?.DistanceField, options?.DistanceField != null }, + { "maxDistance", () => options?.MaxDistance.Value, options?.MaxDistance != null }, + { "minDistance", () => options?.MinDistance.Value, options?.MinDistance != null }, + { "distanceMultiplier", () => options?.DistanceMultiplier.Value, options?.DistanceMultiplier != null }, + { "key", options?.Key, options?.Key != null }, + { "query", options?.Query?.Render(outputRenderArgs), options?.Query != null }, + { "includeLocs", options?.IncludeLocs, options?.IncludeLocs != null }, + { "spherical", () => options?.Spherical.Value, options?.Spherical != null } + }; + + var outputSerializer = args.SerializerRegistry.GetSerializer(); + 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 point. This could be a , a 2d array or embedded document. + /// The point for which to find the closest documents. + /// The options. + /// The stage. + public static PipelineStageDefinition GeoNear( + TPoint near, + GeoNearOptions options = null) where TPoint : class + { + 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..d0972e8c81c --- /dev/null +++ b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs @@ -0,0 +1,183 @@ +/* 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.Collections.Generic; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.GeoJsonObjectModel; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests +{ + public class AggregateGeoNearTests : IntegrationTest + { + public AggregateGeoNearTests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void GeoNear_with_GeoJsonPoint_should_return_expected_result() + { + var collection = Fixture.Collection; + + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); + + 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"); + } + + [Fact] + public void GeoNear_omitting_distanceField_should_return_expected_result() + { + RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); + + var collection = Fixture.Collection; + + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); + + 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_with_legacyCoordinates_should_return_expected_result() + { + var collection = Fixture.Collection; + + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); + + var result = collection + .Aggregate() + .GeoNear( + new[] { -73.99279, 40.719296 }, + new GeoNearOptions + { + DistanceField = "Distance", + MaxDistance = 2, + Key = "LegacyCoordinateLocation", + Query = Builders.Filter.Eq(p => p.Category, + "Parks"), + Spherical = true + }) + .ToList(); + + result.Count.Should().Be(2); + result[0].Name.Should().Be("Sara D. Roosevelt Park"); + result[1].Name.Should().Be("Central Park"); + } + + [Fact] + public void GeoNear_using_pipeline_should_return_expected_result() + { + RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); + + var collection = Fixture.Collection; + + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); + + var pipeline = new EmptyPipelineDefinition() + .GeoNear( + new[] { -73.99279, 40.719296 }, + new GeoNearOptions + { + MaxDistance = 2, + Key = "LegacyCoordinateLocation", + Query = Builders.Filter.Eq(p => p.Category, + "Parks"), + Spherical = true + }); + + var result = collection.Aggregate(pipeline).ToList(); + + result.Count.Should().Be(2); + result[0].Name.Should().Be("Sara D. Roosevelt Park"); + result[1].Name.Should().Be("Central Park"); + } + + public class Place + { + public ObjectId Id { get; set; } + public string Name { get; set; } + public GeoJsonPoint GeoJsonPointLocation { get; set; } + public double[] LegacyCoordinateLocation { get; set; } + public string Category { get; set; } + } + + public class PlaceResult : Place + { + public double Distance { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData { get; } = + [ + 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" + } + ]; + } + } +} \ 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..966558e30e9 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; @@ -106,11 +106,56 @@ public void ChangeStreamSplitLargeEvent_should_throw_when_pipeline_is_null() argumentNullException.ParamName.Should().Be("pipeline"); } + [Theory] + [MemberData(nameof(GeoNearTestData))] + public void GeoNear_should_add_the_expected_stage(TPoint point, string expectedStage) where TPoint : class + { + var pipeline = new EmptyPipelineDefinition(); + + var result = pipeline.GeoNear( + point, + new GeoNearOptions + { + DistanceField = "calculatedDistance" + }); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages.Count.Should().Be(1); + stages[0].Should().Be(expectedStage); + } + + public static object[][] GeoNearTestData => + [ + [GeoJson.Point(GeoJson.Geographic(34, 67)), """{ "$geoNear" : { "near" : { "type" : "Point", "coordinates" : [34.0, 67.0] }, "distanceField" : "calculatedDistance" } }"""], + [new []{34.0, 67.0}, """{ "$geoNear" : { "near" : [34.0, 67.0], "distanceField" : "calculatedDistance" } }"""], + [new BsonDocument { { "long", 34.0}, { "lat", 67.0} }, """{ "$geoNear" : { "near" : { "long" : 34.0, "lat" : 67.0 }, "distanceField" : "calculatedDistance" } }"""] + ]; + [Fact] - public void Lookup_should_throw_when_pipeline_is_null() + public void GeoNear_should_throw_when_pipeline_is_null() { - RequireServer.Check(); + PipelineDefinition pipeline = null; + + var exception = Record.Exception(() => pipeline.GeoNear(new []{1, 2})); + 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..4e72ffb771b 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,68 @@ 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( + new []{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_embedded_document_should_return_the_expected_result() + { + var result = + PipelineStageDefinitionBuilder.GeoNear( + new BsonDocument + { + { "long", 34.0}, + { "lat", 67.0} + }); + + var stage = RenderStage(result); + stage.Document.Should().Be("""{ "$geoNear" : { "near" : { "long" : 34.0, "lat" : 67.0 } } }"""); + } + + [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 GraphLookup_with_many_to_one_parameters_should_return_expected_result() { From e665118c4d51f106333d7128c1e73d8adb2f5a3d Mon Sep 17 00:00:00 2001 From: adelinowona Date: Fri, 21 Feb 2025 21:49:56 -0500 Subject: [PATCH 02/11] remove misc usings statement --- src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 7d244619682..ea7dfa07a73 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; using System.Linq.Expressions; using System.Reflection; From f10115245f669d2b5ad64806014de6f1468bbe0d Mon Sep 17 00:00:00 2001 From: adelinowona Date: Tue, 25 Feb 2025 10:43:02 -0500 Subject: [PATCH 03/11] PR comments --- src/MongoDB.Driver/GeoNearOptions.cs | 6 +- .../AggregateGeoNearTests.cs | 146 ++++++++---------- 2 files changed, 69 insertions(+), 83 deletions(-) diff --git a/src/MongoDB.Driver/GeoNearOptions.cs b/src/MongoDB.Driver/GeoNearOptions.cs index 1c507f523b9..933f1e6ad76 100644 --- a/src/MongoDB.Driver/GeoNearOptions.cs +++ b/src/MongoDB.Driver/GeoNearOptions.cs @@ -21,12 +21,13 @@ namespace MongoDB.Driver public class GeoNearOptions { /// - /// Gets or sets the distance field. Required if querying a time-series collection. + /// 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 string DistanceField { get; set; } /// - /// Gets or sets the distance multiplier. + /// Gets or sets the factor to multiply all distances returned by the query. /// public double? DistanceMultiplier { get; set; } @@ -59,6 +60,5 @@ public class GeoNearOptions /// 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/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs index d0972e8c81c..bc074fbc14e 100644 --- a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs +++ b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs @@ -13,12 +13,10 @@ * limitations under the License. */ -using System.Collections.Generic; using FluentAssertions; using MongoDB.Bson; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.GeoJsonObjectModel; -using MongoDB.Driver.TestHelpers; using Xunit; namespace MongoDB.Driver.Tests @@ -31,9 +29,11 @@ public AggregateGeoNearTests(ClassFixture fixture) } [Fact] - public void GeoNear_with_GeoJsonPoint_should_return_expected_result() + public void GeoNear_omitting_distanceField_should_return_expected_result() { - var collection = Fixture.Collection; + RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); + + var collection = Fixture.GeoCollection; collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); @@ -41,12 +41,11 @@ public void GeoNear_with_GeoJsonPoint_should_return_expected_result() .Aggregate() .GeoNear( GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), - new GeoNearOptions + new GeoNearOptions { - DistanceField = "Distance", MaxDistance = 2, Key = "GeoJsonPointLocation", - Query = Builders.Filter.Eq(p => p.Category, + Query = Builders.Filter.Eq(p => p.Category, "Parks"), Spherical = true }) @@ -57,86 +56,63 @@ public void GeoNear_with_GeoJsonPoint_should_return_expected_result() } [Fact] - public void GeoNear_omitting_distanceField_should_return_expected_result() + public void GeoNear_using_pipeline_should_return_expected_result() { RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); - var collection = Fixture.Collection; + var collection = Fixture.GeoCollection; - collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); - var result = collection - .Aggregate() + var pipeline = new EmptyPipelineDefinition() .GeoNear( - GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), + new[] { -73.99279, 40.719296 }, new GeoNearOptions { - MaxDistance = 2, - Key = "GeoJsonPointLocation", + MaxDistance = 0.000313917534, + Key = "LegacyCoordinateLocation", Query = Builders.Filter.Eq(p => p.Category, "Parks"), Spherical = true - }) - .ToList(); + }); + + 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_legacyCoordinates_should_return_expected_result() + [Theory] + [MemberData(nameof(GeoTestData))] + public void GeoNear_with_legacyCoordinates_should_return_expected_result(TPoint point, bool usingLegacyCoord) where TPoint : class { - var collection = Fixture.Collection; - - collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); + var collection = Fixture.GeoCollection; var result = collection .Aggregate() .GeoNear( - new[] { -73.99279, 40.719296 }, + point, new GeoNearOptions { DistanceField = "Distance", - MaxDistance = 2, - Key = "LegacyCoordinateLocation", + MaxDistance = usingLegacyCoord ? 0.000313917534 : 2, + Key = usingLegacyCoord ? "LegacyCoordinateLocation" : "GeoJsonPointLocation", Query = Builders.Filter.Eq(p => p.Category, "Parks"), Spherical = true }) .ToList(); - result.Count.Should().Be(2); + result.Count.Should().Be(1); result[0].Name.Should().Be("Sara D. Roosevelt Park"); - result[1].Name.Should().Be("Central Park"); } - [Fact] - public void GeoNear_using_pipeline_should_return_expected_result() - { - RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); - - var collection = Fixture.Collection; - - collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); - - var pipeline = new EmptyPipelineDefinition() - .GeoNear( - new[] { -73.99279, 40.719296 }, - new GeoNearOptions - { - MaxDistance = 2, - Key = "LegacyCoordinateLocation", - Query = Builders.Filter.Eq(p => p.Category, - "Parks"), - Spherical = true - }); - - var result = collection.Aggregate(pipeline).ToList(); - - result.Count.Should().Be(2); - result[0].Name.Should().Be("Sara D. Roosevelt Park"); - result[1].Name.Should().Be("Central Park"); - } + public static object[][] GeoTestData => + [ + [new[] { -73.99279, 40.719296 }, true], + [new BsonDocument {{"long", -73.99279}, {"lat", 40.719296}}, true], + [GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), false] + ]; public class Place { @@ -152,32 +128,42 @@ public class PlaceResult : Place public double Distance { get; set; } } - public sealed class ClassFixture : MongoCollectionFixture - { - protected override IEnumerable InitialData { get; } = - [ - 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" - } - ]; + 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 From 77606ef5d504243ed25f7ca18bbdc8187031d755 Mon Sep 17 00:00:00 2001 From: adelinowona Date: Tue, 4 Mar 2025 11:56:31 -0500 Subject: [PATCH 04/11] address pr comments --- src/MongoDB.Driver/AggregateFluent.cs | 21 ++++- src/MongoDB.Driver/AggregateFluentBase.cs | 24 +++++- src/MongoDB.Driver/IAggregateFluent.cs | 33 +++++++- .../IAggregateFluentExtensions.cs | 19 ----- .../PipelineDefinitionBuilder.cs | 53 +++++++++++-- .../PipelineStageDefinitionBuilder.cs | 52 ++++++++++--- .../AggregateGeoNearTests.cs | 77 ++++++++++++++----- .../PipelineDefinitionBuilderTests.cs | 55 +++++++++---- .../PipelineStageDefinitionBuilderTests.cs | 30 +++++++- 9 files changed, 283 insertions(+), 81 deletions(-) diff --git a/src/MongoDB.Driver/AggregateFluent.cs b/src/MongoDB.Driver/AggregateFluent.cs index 0d1d60adbad..8ed06218f0e 100644 --- a/src/MongoDB.Driver/AggregateFluent.cs +++ b/src/MongoDB.Driver/AggregateFluent.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 @@ -130,11 +131,25 @@ public override IAggregateFluent Facet( return WithPipeline(_pipeline.Facet(facets, options)); } - public override IAggregateFluent GeoNear( - TPoint near, + public override IAggregateFluent GeoNear( + GeoJsonPoint near, GeoNearOptions options = null) { - return WithPipeline(_pipeline.GeoNear(near, options)); + return WithPipeline(_pipeline.GeoNear(near, options)); + } + + public override IAggregateFluent GeoNear( + TCoordinates[] near, + GeoNearOptions options = null) + { + return WithPipeline(_pipeline.GeoNear(near, options)); + } + + public override IAggregateFluent GeoNear( + BsonDocument near, + GeoNearOptions options = null) + { + return WithPipeline(_pipeline.GeoNear(near, options)); } public override IAggregateFluent GraphLookup( diff --git a/src/MongoDB.Driver/AggregateFluentBase.cs b/src/MongoDB.Driver/AggregateFluentBase.cs index 0dd1736aea3..e6fdb907f46 100644 --- a/src/MongoDB.Driver/AggregateFluentBase.cs +++ b/src/MongoDB.Driver/AggregateFluentBase.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; namespace MongoDB.Driver @@ -125,9 +126,26 @@ public virtual IAggregateFluent Facet( } /// - public virtual IAggregateFluent GeoNear( - TPoint near, - GeoNearOptions options = null) where TPoint : class + public virtual IAggregateFluent GeoNear( + GeoJsonPoint near, + GeoNearOptions options = null) + where TCoordinates : GeoJsonCoordinates + { + throw new NotImplementedException(); + } + + /// + public virtual IAggregateFluent GeoNear( + TCoordinates[] near, + GeoNearOptions options = null) + { + throw new NotImplementedException(); + } + + /// + public virtual IAggregateFluent GeoNear( + BsonDocument near, + GeoNearOptions options = null) { throw new NotImplementedException(); } diff --git a/src/MongoDB.Driver/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index 768355cbb07..6ebd0aed83f 100644 --- a/src/MongoDB.Driver/IAggregateFluent.cs +++ b/src/MongoDB.Driver/IAggregateFluent.cs @@ -179,13 +179,38 @@ IAggregateFluent Facet( /// /// Appends a $geoNear stage to the pipeline. /// - /// The type of the point. This could be a , a 2d array or embedded document. + /// The type of the new result. + /// The type of the coordinates for the point. + /// The point for which to find the closest documents. + /// The options. + /// The fluent aggregate interface. + IAggregateFluent GeoNear( + GeoJsonPoint near, + GeoNearOptions options = null) + where TCoordinates : GeoJsonCoordinates; + + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the new result. + /// The type of the coordinates for the point. + /// The point for which to find the closest documents. + /// The options. + /// The fluent aggregate interface. + IAggregateFluent GeoNear( + TCoordinates[] near, + GeoNearOptions options = null); + + /// + /// Appends a $geoNear stage to the pipeline. + /// + /// The type of the new result. /// The point for which to find the closest documents. /// The options. /// The fluent aggregate interface. - IAggregateFluent GeoNear( - TPoint near, - GeoNearOptions options = null) where TPoint : class; + IAggregateFluent GeoNear( + BsonDocument near, + GeoNearOptions options = null); /// /// Appends a $graphLookup stage to the pipeline. diff --git a/src/MongoDB.Driver/IAggregateFluentExtensions.cs b/src/MongoDB.Driver/IAggregateFluentExtensions.cs index b696d8fd207..fbd30d245cc 100644 --- a/src/MongoDB.Driver/IAggregateFluentExtensions.cs +++ b/src/MongoDB.Driver/IAggregateFluentExtensions.cs @@ -253,25 +253,6 @@ public static IAggregateFluent Facet( return aggregate.AppendStage(PipelineStageDefinitionBuilder.Facet(facets)); } - /// - /// Appends a $geoNear stage to the pipeline. - /// - /// The type of the new result. - /// The type of the result. - /// The type of the point. This could be a , a 2d array or embedded document. - /// The aggregate. - /// The point for which to find the closest documents. - /// The options. - /// The fluent aggregate interface. - public static IAggregateFluent GeoNear( - this IAggregateFluent aggregate, - TPoint near, - GeoNearOptions options = null) where TPoint : class - { - 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/PipelineDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs index 8c0631e2a25..55a33e949ab 100644 --- a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs @@ -489,19 +489,60 @@ public static PipelineDefinition For(IBsonSerializer /// The type of the input documents. + /// The type of the intermediate documents. /// The type of the output documents. - /// The type of the point. This could be a , a 2d array or embedded document. + /// 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, - TPoint near, - GeoNearOptions options = null) where TPoint : class + 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 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, + TCoordinates[] near, + GeoNearOptions options = null) + { + 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, + BsonDocument near, + GeoNearOptions options = null) { Ensure.IsNotNull(pipeline, nameof(pipeline)); - return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); } /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index ea7dfa07a73..0c92223c18a 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -614,9 +614,9 @@ public static PipelineStageDefinition Facet( /// The point for which to find the closest documents. /// The options. /// The stage. - public static PipelineStageDefinition GeoNear( + internal static PipelineStageDefinition GeoNear( TPoint near, - GeoNearOptions options = null) where TPoint : class + GeoNearOptions options = null) where TPoint : class { Ensure.IsNotNull(near, nameof(near)); @@ -627,7 +627,6 @@ public static PipelineStageDefinition GeoNear(); - var outputRenderArgs = args.WithNewDocumentType(args.SerializerRegistry.GetSerializer()); var geoNearOptions = new BsonDocument { { "near", pointSerializer.ToBsonValue(near)}, @@ -636,7 +635,7 @@ public static PipelineStageDefinition GeoNear options?.MinDistance.Value, options?.MinDistance != null }, { "distanceMultiplier", () => options?.DistanceMultiplier.Value, options?.DistanceMultiplier != null }, { "key", options?.Key, options?.Key != null }, - { "query", options?.Query?.Render(outputRenderArgs), options?.Query != null }, + { "query", options?.Query?.Render(args), options?.Query != null }, { "includeLocs", options?.IncludeLocs, options?.IncludeLocs != null }, { "spherical", () => options?.Spherical.Value, options?.Spherical != null } }; @@ -652,15 +651,50 @@ public static PipelineStageDefinition GeoNear /// The type of the input documents. - /// The type of the point. This could be a , a 2d array or embedded document. + /// 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( - TPoint near, - GeoNearOptions options = null) where TPoint : class + public static PipelineStageDefinition GeoNear( + GeoJsonPoint near, + GeoNearOptions options = null) + where TCoordinates : GeoJsonCoordinates + { + return GeoNear, TOutput>(near, options); + } + + /// + /// 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( + TCoordinates[] near, + GeoNearOptions options = null) + { + 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 $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( + BsonDocument near, + GeoNearOptions options = null) { - return GeoNear(near, options); + Ensure.That(near.ElementCount, len => len is >= 2 and <= 3, nameof(near), "Legacy coordinates document should have 2 or 3 coordinates."); + return GeoNear(near, options); } /// diff --git a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs index bc074fbc14e..4a3cb660546 100644 --- a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs +++ b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs @@ -39,7 +39,7 @@ public void GeoNear_omitting_distanceField_should_return_expected_result() var result = collection .Aggregate() - .GeoNear( + .GeoNear( GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), new GeoNearOptions { @@ -58,17 +58,16 @@ public void GeoNear_omitting_distanceField_should_return_expected_result() [Fact] public void GeoNear_using_pipeline_should_return_expected_result() { - RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); - var collection = Fixture.GeoCollection; collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); var pipeline = new EmptyPipelineDefinition() - .GeoNear( + .GeoNear( new[] { -73.99279, 40.719296 }, new GeoNearOptions { + DistanceField = "Distance", MaxDistance = 0.000313917534, Key = "LegacyCoordinateLocation", Query = Builders.Filter.Eq(p => p.Category, @@ -82,22 +81,21 @@ public void GeoNear_using_pipeline_should_return_expected_result() result[0].Name.Should().Be("Sara D. Roosevelt Park"); } - [Theory] - [MemberData(nameof(GeoTestData))] - public void GeoNear_with_legacyCoordinates_should_return_expected_result(TPoint point, bool usingLegacyCoord) where TPoint : class + [Fact] + public void GeoNear_with_array_legacy_coordinates_should_return_expected_result() { var collection = Fixture.GeoCollection; var result = collection .Aggregate() - .GeoNear( - point, - new GeoNearOptions + .GeoNear( + new[] { -73.99279, 40.719296 }, + new GeoNearOptions { DistanceField = "Distance", - MaxDistance = usingLegacyCoord ? 0.000313917534 : 2, - Key = usingLegacyCoord ? "LegacyCoordinateLocation" : "GeoJsonPointLocation", - Query = Builders.Filter.Eq(p => p.Category, + MaxDistance = 0.000313917534, + Key = "LegacyCoordinateLocation", + Query = Builders.Filter.Eq(p => p.Category, "Parks"), Spherical = true }) @@ -107,12 +105,53 @@ public void GeoNear_with_legacyCoordinates_should_return_expected_result result[0].Name.Should().Be("Sara D. Roosevelt Park"); } - public static object[][] GeoTestData => - [ - [new[] { -73.99279, 40.719296 }, true], - [new BsonDocument {{"long", -73.99279}, {"lat", 40.719296}}, true], - [GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), false] - ]; + [Fact] + public void GeoNear_with_BsonDocument_legacy_coordinates_should_return_expected_result() + { + var collection = Fixture.GeoCollection; + + var result = collection + .Aggregate() + .GeoNear( + new BsonDocument {{"long", -73.99279}, {"lat", 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"); + } public class Place { diff --git a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs index 966558e30e9..d16572b2d13 100644 --- a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs @@ -106,14 +106,13 @@ public void ChangeStreamSplitLargeEvent_should_throw_when_pipeline_is_null() argumentNullException.ParamName.Should().Be("pipeline"); } - [Theory] - [MemberData(nameof(GeoNearTestData))] - public void GeoNear_should_add_the_expected_stage(TPoint point, string expectedStage) where TPoint : class + [Fact] + public void GeoNear_with_geojson_point_should_add_the_expected_stage() { var pipeline = new EmptyPipelineDefinition(); - var result = pipeline.GeoNear( - point, + var result = pipeline.GeoNear( + GeoJson.Point(GeoJson.Geographic(34, 67)), new GeoNearOptions { DistanceField = "calculatedDistance" @@ -121,22 +120,50 @@ public void GeoNear_should_add_the_expected_stage(TPoint point, string e var stages = RenderStages(result, BsonDocumentSerializer.Instance); stages.Count.Should().Be(1); - stages[0].Should().Be(expectedStage); + 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" } }"""); } - public static object[][] GeoNearTestData => - [ - [GeoJson.Point(GeoJson.Geographic(34, 67)), """{ "$geoNear" : { "near" : { "type" : "Point", "coordinates" : [34.0, 67.0] }, "distanceField" : "calculatedDistance" } }"""], - [new []{34.0, 67.0}, """{ "$geoNear" : { "near" : [34.0, 67.0], "distanceField" : "calculatedDistance" } }"""], - [new BsonDocument { { "long", 34.0}, { "lat", 67.0} }, """{ "$geoNear" : { "near" : { "long" : 34.0, "lat" : 67.0 }, "distanceField" : "calculatedDistance" } }"""] - ]; + [Fact] + public void GeoNear_with_embedded_doc_should_add_the_expected_stage() + { + var pipeline = new EmptyPipelineDefinition(); + + var result = pipeline.GeoNear( + new BsonDocument { { "long", 34.0}, { "lat", 67.0} }, + new GeoNearOptions + { + DistanceField = "calculatedDistance" + }); + + var stages = RenderStages(result, BsonDocumentSerializer.Instance); + stages.Count.Should().Be(1); + stages[0].Should().Be("""{ "$geoNear" : { "near" : { "long" : 34.0, "lat" : 67.0 }, "distanceField" : "calculatedDistance" } }"""); + } [Fact] public void GeoNear_should_throw_when_pipeline_is_null() { PipelineDefinition pipeline = null; - var exception = Record.Exception(() => pipeline.GeoNear(new []{1, 2})); + var exception = Record.Exception(() => + pipeline.GeoNear([1.0, 2.0])); exception.Should().BeOfType() .Which.ParamName.Should().Be("pipeline"); @@ -147,7 +174,7 @@ public void GeoNear_should_throw_when_near_point_is_null() { var pipeline = new EmptyPipelineDefinition(); - var exception = Record.Exception(() => pipeline.GeoNear(null)); + var exception = Record.Exception(() => pipeline.GeoNear(null)); exception.Should().BeOfType() .Which.ParamName.Should().Be("near"); diff --git a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs index 4e72ffb771b..7983ae62429 100644 --- a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs @@ -137,8 +137,8 @@ public void ChangeStreamSplitLargeEvent_should_return_the_expected_result() [Fact] public void GeoNear_with_array_should_return_the_expected_result() { - var result = PipelineStageDefinitionBuilder.GeoNear( - new []{34.0, 67.0}, + var result = PipelineStageDefinitionBuilder.GeoNear( + [34.0, 67.0], new GeoNearOptions { DistanceField = "calculatedDistance", @@ -170,7 +170,7 @@ public void GeoNear_with_embedded_document_should_return_the_expected_result() [Fact] public void GeoNear_with_geojson_point_should_return_the_expected_result() { - var result = PipelineStageDefinitionBuilder.GeoNear( + var result = PipelineStageDefinitionBuilder.GeoNear( GeoJson.Point(GeoJson.Geographic(34, 67)), new GeoNearOptions { @@ -189,12 +189,34 @@ public void GeoNear_with_geojson_point_should_return_the_expected_result() public void GeoNear_with_no_options_should_return_the_expected_result() { var result = - PipelineStageDefinitionBuilder.GeoNear>( + 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(new BsonDocument + { + { "x", 34.0}, + { "y", 67.0}, + { "z", 25.0}, + { "w", 57.0} + }); + }); + } [Fact] public void GraphLookup_with_many_to_one_parameters_should_return_expected_result() From c84c06fb8d895a139679f0486c50141d62df4648 Mon Sep 17 00:00:00 2001 From: adelinowona Date: Tue, 4 Mar 2025 11:57:34 -0500 Subject: [PATCH 05/11] use record --- src/MongoDB.Driver/GeoNearOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/GeoNearOptions.cs b/src/MongoDB.Driver/GeoNearOptions.cs index 933f1e6ad76..5be58e27b5b 100644 --- a/src/MongoDB.Driver/GeoNearOptions.cs +++ b/src/MongoDB.Driver/GeoNearOptions.cs @@ -18,7 +18,7 @@ namespace MongoDB.Driver /// /// Represents options for the $geoNear stage. /// - public class GeoNearOptions + public record GeoNearOptions { /// /// Gets or sets the output field that contains the calculated distance. Required if querying a time-series collection. From 5985760d3653fc3050a50e450c927c4c80ff145e Mon Sep 17 00:00:00 2001 From: adelinowona Date: Tue, 4 Mar 2025 13:52:37 -0500 Subject: [PATCH 06/11] fix null error --- src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs | 7 ++++--- .../PipelineStageDefinitionBuilderTests.cs | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 0c92223c18a..2cc8b1a1248 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -616,10 +616,8 @@ public static PipelineStageDefinition Facet( /// The stage. internal static PipelineStageDefinition GeoNear( TPoint near, - GeoNearOptions options = null) where TPoint : class + GeoNearOptions options = null) { - Ensure.IsNotNull(near, nameof(near)); - const string operatorName = "$geoNear"; var stage = new DelegatedPipelineStageDefinition( operatorName, @@ -661,6 +659,7 @@ public static PipelineStageDefinition GeoNear options = null) where TCoordinates : GeoJsonCoordinates { + Ensure.IsNotNull(near, nameof(near)); return GeoNear, TOutput>(near, options); } @@ -677,6 +676,7 @@ public static PipelineStageDefinition GeoNear 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); } @@ -693,6 +693,7 @@ public static PipelineStageDefinition GeoNear( BsonDocument near, GeoNearOptions options = null) { + Ensure.IsNotNull(near, nameof(near)); Ensure.That(near.ElementCount, len => len is >= 2 and <= 3, nameof(near), "Legacy coordinates document should have 2 or 3 coordinates."); return GeoNear(near, options); } diff --git a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs index 7983ae62429..0e00feb77a7 100644 --- a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs @@ -205,6 +205,12 @@ public void GeoNear_with_wrong_legacy_coordinates_should_throw_exception() PipelineStageDefinitionBuilder.GeoNear([34.0, 67.0, 23.0, 34.5]); }); + Assert.Throws(() => + { + var result = + PipelineStageDefinitionBuilder.GeoNear([34.0]); + }); + Assert.Throws(() => { var result = From 04ddd36cd9d28f180a1d4da968a4c86d357e6b9b Mon Sep 17 00:00:00 2001 From: adelinowona Date: Tue, 18 Mar 2025 15:17:35 -0400 Subject: [PATCH 07/11] remove geoNearCommand feature --- src/MongoDB.Driver/Core/Misc/Feature.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/MongoDB.Driver/Core/Misc/Feature.cs b/src/MongoDB.Driver/Core/Misc/Feature.cs index 34dc72e57c8..e0b4dfec461 100644 --- a/src/MongoDB.Driver/Core/Misc/Feature.cs +++ b/src/MongoDB.Driver/Core/Misc/Feature.cs @@ -63,7 +63,6 @@ public class Feature private static readonly Feature __filterLimit = new Feature("FilterLimit", WireVersion.Server60); private static readonly Feature __findAllowDiskUse = new Feature("FindAllowDiskUse", WireVersion.Server44); private static readonly Feature __findProjectionExpressions = new Feature("FindProjectionExpressions", WireVersion.Server44); - private static readonly Feature __geoNearCommand = new Feature("GeoNearCommand", WireVersion.Zero, WireVersion.Server42); private static readonly Feature __getField = new Feature("GetField", WireVersion.Server50); private static readonly Feature __getMoreComment = new Feature("GetMoreComment", WireVersion.Server44); private static readonly Feature __groupCommand = new Feature("GroupCommand", WireVersion.Zero, WireVersion.Server42); @@ -288,11 +287,6 @@ public class Feature /// public static Feature FindProjectionExpressions => __findProjectionExpressions; - /// - /// Gets the geoNear command feature. - /// - public static Feature GeoNearCommand => __geoNearCommand; - /// /// Gets the getField feature. /// From 7c8e1de2b14cd88a4056fc6ef87d57dafba4a27b Mon Sep 17 00:00:00 2001 From: adelinowona Date: Wed, 2 Apr 2025 15:54:30 -0400 Subject: [PATCH 08/11] move geoNear methods to IAggregateFluentExtensions.cs --- src/MongoDB.Driver/AggregateFluent.cs | 22 ----------- src/MongoDB.Driver/AggregateFluentBase.cs | 26 ------------- src/MongoDB.Driver/IAggregateFluent.cs | 37 ------------------ .../IAggregateFluentExtensions.cs | 39 +++++++++++++++++++ 4 files changed, 39 insertions(+), 85 deletions(-) diff --git a/src/MongoDB.Driver/AggregateFluent.cs b/src/MongoDB.Driver/AggregateFluent.cs index 8ed06218f0e..90bd9823ce1 100644 --- a/src/MongoDB.Driver/AggregateFluent.cs +++ b/src/MongoDB.Driver/AggregateFluent.cs @@ -20,7 +20,6 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; -using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; namespace MongoDB.Driver @@ -130,27 +129,6 @@ public override IAggregateFluent Facet( { return WithPipeline(_pipeline.Facet(facets, options)); } - - public override IAggregateFluent GeoNear( - GeoJsonPoint near, - GeoNearOptions options = null) - { - return WithPipeline(_pipeline.GeoNear(near, options)); - } - - public override IAggregateFluent GeoNear( - TCoordinates[] near, - GeoNearOptions options = null) - { - return WithPipeline(_pipeline.GeoNear(near, options)); - } - - public override IAggregateFluent GeoNear( - BsonDocument near, - GeoNearOptions options = null) - { - return WithPipeline(_pipeline.GeoNear(near, options)); - } public override IAggregateFluent GraphLookup( IMongoCollection from, diff --git a/src/MongoDB.Driver/AggregateFluentBase.cs b/src/MongoDB.Driver/AggregateFluentBase.cs index e6fdb907f46..51381f930bd 100644 --- a/src/MongoDB.Driver/AggregateFluentBase.cs +++ b/src/MongoDB.Driver/AggregateFluentBase.cs @@ -19,7 +19,6 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; -using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; namespace MongoDB.Driver @@ -125,31 +124,6 @@ public virtual IAggregateFluent Facet( throw new NotImplementedException(); } - /// - public virtual IAggregateFluent GeoNear( - GeoJsonPoint near, - GeoNearOptions options = null) - where TCoordinates : GeoJsonCoordinates - { - throw new NotImplementedException(); - } - - /// - public virtual IAggregateFluent GeoNear( - TCoordinates[] near, - GeoNearOptions options = null) - { - throw new NotImplementedException(); - } - - /// - public virtual IAggregateFluent GeoNear( - BsonDocument near, - GeoNearOptions options = null) - { - throw new NotImplementedException(); - } - /// public virtual IAggregateFluent GraphLookup( IMongoCollection from, diff --git a/src/MongoDB.Driver/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index 6ebd0aed83f..dfb0b821ec8 100644 --- a/src/MongoDB.Driver/IAggregateFluent.cs +++ b/src/MongoDB.Driver/IAggregateFluent.cs @@ -19,7 +19,6 @@ using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization; -using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; namespace MongoDB.Driver @@ -176,42 +175,6 @@ IAggregateFluent Facet( IEnumerable> facets, AggregateFacetOptions options = null); - /// - /// Appends a $geoNear stage to the pipeline. - /// - /// The type of the new result. - /// The type of the coordinates for the point. - /// The point for which to find the closest documents. - /// The options. - /// The fluent aggregate interface. - IAggregateFluent GeoNear( - GeoJsonPoint near, - GeoNearOptions options = null) - where TCoordinates : GeoJsonCoordinates; - - /// - /// Appends a $geoNear stage to the pipeline. - /// - /// The type of the new result. - /// The type of the coordinates for the point. - /// The point for which to find the closest documents. - /// The options. - /// The fluent aggregate interface. - IAggregateFluent GeoNear( - TCoordinates[] near, - GeoNearOptions options = null); - - /// - /// Appends a $geoNear stage to the pipeline. - /// - /// The type of the new result. - /// The point for which to find the closest documents. - /// The options. - /// The fluent aggregate interface. - IAggregateFluent GeoNear( - BsonDocument near, - GeoNearOptions options = null); - /// /// Appends a $graphLookup stage to the pipeline. /// diff --git a/src/MongoDB.Driver/IAggregateFluentExtensions.cs b/src/MongoDB.Driver/IAggregateFluentExtensions.cs index fbd30d245cc..18aa157dd6e 100644 --- a/src/MongoDB.Driver/IAggregateFluentExtensions.cs +++ b/src/MongoDB.Driver/IAggregateFluentExtensions.cs @@ -253,6 +253,45 @@ 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 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, + TCoordinates[] near, + GeoNearOptions options = null) + { + Ensure.IsNotNull(aggregate, nameof(aggregate)); + return aggregate.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + } + /// /// Appends a $graphLookup stage to the pipeline. /// From 62337404227154c55a47b5c2ee15af471f8bb430 Mon Sep 17 00:00:00 2001 From: adelinowona Date: Fri, 4 Apr 2025 15:27:11 -0400 Subject: [PATCH 09/11] Address pr comments --- src/MongoDB.Driver/GeoNearOptions.cs | 29 +++--- .../IAggregateFluentExtensions.cs | 13 ++- .../PipelineDefinitionBuilder.cs | 34 ++----- .../PipelineStageDefinitionBuilder.cs | 56 ++++------- .../AggregateGeoNearTests.cs | 94 +++++++------------ .../PipelineDefinitionBuilderTests.cs | 37 ++------ .../PipelineStageDefinitionBuilderTests.cs | 83 ++++++++-------- 7 files changed, 136 insertions(+), 210 deletions(-) diff --git a/src/MongoDB.Driver/GeoNearOptions.cs b/src/MongoDB.Driver/GeoNearOptions.cs index 5be58e27b5b..5211fa35719 100644 --- a/src/MongoDB.Driver/GeoNearOptions.cs +++ b/src/MongoDB.Driver/GeoNearOptions.cs @@ -13,49 +13,56 @@ * limitations under the License. */ +using MongoDB.Bson.Serialization; + namespace MongoDB.Driver { /// /// Represents options for the $geoNear stage. /// - public record GeoNearOptions + 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 string DistanceField { get; set; } - + 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 string IncludeLocs { get; set; } - + 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; } - + public FilterDefinition Query { get; set; } + /// /// Gets or sets the spherical option which determines how to calculate the distance between two points. /// diff --git a/src/MongoDB.Driver/IAggregateFluentExtensions.cs b/src/MongoDB.Driver/IAggregateFluentExtensions.cs index 18aa157dd6e..a9e47c2a3d3 100644 --- a/src/MongoDB.Driver/IAggregateFluentExtensions.cs +++ b/src/MongoDB.Driver/IAggregateFluentExtensions.cs @@ -263,10 +263,10 @@ public static IAggregateFluent Facet( /// The point for which to find the closest documents. /// The options. /// The fluent aggregate interface. - public static IAggregateFluent GeoNear( + public static IAggregateFluent GeoNear( this IAggregateFluent aggregate, GeoJsonPoint near, - GeoNearOptions options = null) + GeoNearOptions options = null) where TCoordinates : GeoJsonCoordinates { Ensure.IsNotNull(aggregate, nameof(aggregate)); @@ -278,18 +278,17 @@ public static IAggregateFluent GeoNear /// 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( + public static IAggregateFluent GeoNear( this IAggregateFluent aggregate, - TCoordinates[] near, - GeoNearOptions options = null) + double[] near, + GeoNearOptions options = null) { Ensure.IsNotNull(aggregate, nameof(aggregate)); - return aggregate.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + return aggregate.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); } /// diff --git a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs index 55a33e949ab..7330dda1951 100644 --- a/src/MongoDB.Driver/PipelineDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineDefinitionBuilder.cs @@ -484,7 +484,7 @@ public static PipelineDefinition For(IBsonSerializer(inputSerializer); } - + /// /// Appends a $geoNear stage to the pipeline. /// @@ -499,33 +499,13 @@ public static PipelineDefinition For(IBsonSerializer GeoNear( this PipelineDefinition pipeline, GeoJsonPoint near, - GeoNearOptions options = null) + 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 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, - TCoordinates[] near, - GeoNearOptions options = null) - { - Ensure.IsNotNull(pipeline, nameof(pipeline)); - return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); } - + /// /// Appends a $geoNear stage to the pipeline. /// @@ -538,11 +518,11 @@ public static PipelineDefinition GeoNearA new pipeline with an additional stage. public static PipelineDefinition GeoNear( this PipelineDefinition pipeline, - BsonDocument near, - GeoNearOptions options = null) + double[] near, + GeoNearOptions options = null) { Ensure.IsNotNull(pipeline, nameof(pipeline)); - return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); + return pipeline.AppendStage(PipelineStageDefinitionBuilder.GeoNear(near, options)); } /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 2cc8b1a1248..2340e8880f9 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -604,7 +604,7 @@ public static PipelineStageDefinition Facet( { return Facet((IEnumerable>)facets); } - + /// /// Creates a $geoNear stage. /// @@ -616,7 +616,8 @@ public static PipelineStageDefinition Facet( /// The stage. internal static PipelineStageDefinition GeoNear( TPoint near, - GeoNearOptions options = null) + GeoNearOptions options = null) + // where TPoint is either a GeoJsonPoint or a legacy coordinate array { const string operatorName = "$geoNear"; var stage = new DelegatedPipelineStageDefinition( @@ -625,26 +626,27 @@ internal static PipelineStageDefinition GeoNear(); + var outputSerializer = options?.OutputSerializer ?? args.GetSerializer(); + var outputRenderArgs = args.WithNewDocumentType(outputSerializer); var geoNearOptions = new BsonDocument { { "near", pointSerializer.ToBsonValue(near)}, - { "distanceField", options?.DistanceField, options?.DistanceField != null }, - { "maxDistance", () => options?.MaxDistance.Value, options?.MaxDistance != null }, - { "minDistance", () => options?.MinDistance.Value, options?.MinDistance != null }, - { "distanceMultiplier", () => options?.DistanceMultiplier.Value, options?.DistanceMultiplier != null }, + { "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, options?.IncludeLocs != null }, - { "spherical", () => options?.Spherical.Value, options?.Spherical != null } + { "includeLocs", options?.IncludeLocs?.Render(outputRenderArgs).FieldName, options?.IncludeLocs != null }, + { "spherical", options?.Spherical, options?.Spherical != null } }; - - var outputSerializer = args.SerializerRegistry.GetSerializer(); + return new RenderedPipelineStageDefinition(operatorName, new BsonDocument(operatorName, geoNearOptions), outputSerializer); }); - + return stage; } - + /// /// Creates a $geoNear stage. /// @@ -656,31 +658,13 @@ internal static PipelineStageDefinition GeoNearThe stage. public static PipelineStageDefinition GeoNear( GeoJsonPoint near, - GeoNearOptions options = null) + 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 type of the coordinates for the point. - /// The point for which to find the closest documents. - /// The options. - /// The stage. - public static PipelineStageDefinition GeoNear( - TCoordinates[] 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 $geoNear stage. /// @@ -690,12 +674,12 @@ public static PipelineStageDefinition GeoNearThe options. /// The stage. public static PipelineStageDefinition GeoNear( - BsonDocument near, - GeoNearOptions options = null) + double[] near, + GeoNearOptions options = null) { Ensure.IsNotNull(near, nameof(near)); - Ensure.That(near.ElementCount, len => len is >= 2 and <= 3, nameof(near), "Legacy coordinates document should have 2 or 3 coordinates."); - return GeoNear(near, options); + 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); } /// diff --git a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs index 4a3cb660546..9ae42c415c9 100644 --- a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs +++ b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs @@ -14,7 +14,7 @@ */ using FluentAssertions; -using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.GeoJsonObjectModel; using Xunit; @@ -27,21 +27,21 @@ 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; - + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); - + var result = collection .Aggregate() - .GeoNear( + .GeoNear( GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), - new GeoNearOptions + new GeoNearOptions { MaxDistance = 2, Key = "GeoJsonPointLocation", @@ -50,22 +50,22 @@ public void GeoNear_omitting_distanceField_should_return_expected_result() 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; - + collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); - + var pipeline = new EmptyPipelineDefinition() - .GeoNear( - new[] { -73.99279, 40.719296 }, - new GeoNearOptions + .GeoNear( + [-73.99279, 40.719296], + new GeoNearOptions { DistanceField = "Distance", MaxDistance = 0.000313917534, @@ -74,47 +74,23 @@ public void GeoNear_using_pipeline_should_return_expected_result() "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( - new[] { -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_BsonDocument_legacy_coordinates_should_return_expected_result() - { - var collection = Fixture.GeoCollection; - + var result = collection .Aggregate() - .GeoNear( - new BsonDocument {{"long", -73.99279}, {"lat", 40.719296}}, - new GeoNearOptions + .GeoNear( + [-73.99279, 40.719296], + new GeoNearOptions { DistanceField = "Distance", MaxDistance = 0.000313917534, @@ -124,21 +100,21 @@ public void GeoNear_with_BsonDocument_legacy_coordinates_should_return_expected_ 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( + .GeoNear( GeoJson.Point(GeoJson.Geographic(-73.99279, 40.719296)), - new GeoNearOptions + new GeoNearOptions { DistanceField = "Distance", MaxDistance = 2, @@ -148,29 +124,31 @@ public void GeoNear_with_GeoJsonPoint_should_return_expected_result() Spherical = true }) .ToList(); - + result.Count.Should().Be(1); result[0].Name.Should().Be("Sara D. Roosevelt Park"); } - + + [BsonIgnoreExtraElements] public class Place { - public ObjectId Id { get; set; } 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"); @@ -197,7 +175,7 @@ protected override void InitializeFixture() Category = "Stadiums" } ]); - + GeoCollection.Indexes.CreateOne( new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); GeoCollection.Indexes.CreateOne( diff --git a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs index d16572b2d13..47b0dc17ca2 100644 --- a/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineDefinitionBuilderTests.cs @@ -111,50 +111,33 @@ public void GeoNear_with_geojson_point_should_add_the_expected_stage() { var pipeline = new EmptyPipelineDefinition(); - var result = pipeline.GeoNear( + var result = pipeline.GeoNear( GeoJson.Point(GeoJson.Geographic(34, 67)), - new GeoNearOptions + 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( + var result = pipeline.GeoNear( [34.0, 67.0], - new GeoNearOptions + 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_with_embedded_doc_should_add_the_expected_stage() - { - var pipeline = new EmptyPipelineDefinition(); - var result = pipeline.GeoNear( - new BsonDocument { { "long", 34.0}, { "lat", 67.0} }, - new GeoNearOptions - { - DistanceField = "calculatedDistance" - }); - var stages = RenderStages(result, BsonDocumentSerializer.Instance); stages.Count.Should().Be(1); - stages[0].Should().Be("""{ "$geoNear" : { "near" : { "long" : 34.0, "lat" : 67.0 }, "distanceField" : "calculatedDistance" } }"""); + stages[0].Should().Be("""{ "$geoNear" : { "near" : [34.0, 67.0], "distanceField" : "calculatedDistance" } }"""); } [Fact] @@ -163,18 +146,18 @@ public void GeoNear_should_throw_when_pipeline_is_null() PipelineDefinition pipeline = null; var exception = Record.Exception(() => - pipeline.GeoNear([1.0, 2.0])); + 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)); + var exception = Record.Exception(() => pipeline.GeoNear(null)); exception.Should().BeOfType() .Which.ParamName.Should().Be("near"); diff --git a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs index 0e00feb77a7..03772ccb529 100644 --- a/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/PipelineStageDefinitionBuilderTests.cs @@ -137,9 +137,9 @@ public void ChangeStreamSplitLargeEvent_should_return_the_expected_result() [Fact] public void GeoNear_with_array_should_return_the_expected_result() { - var result = PipelineStageDefinitionBuilder.GeoNear( + var result = PipelineStageDefinitionBuilder.GeoNear( [34.0, 67.0], - new GeoNearOptions + new GeoNearOptions { DistanceField = "calculatedDistance", MaxDistance = 3, @@ -147,32 +147,28 @@ public void GeoNear_with_array_should_return_the_expected_result() 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_embedded_document_should_return_the_expected_result() - { - var result = - PipelineStageDefinitionBuilder.GeoNear( - new BsonDocument - { - { "long", 34.0}, - { "lat", 67.0} - }); - + var stage = RenderStage(result); - stage.Document.Should().Be("""{ "$geoNear" : { "near" : { "long" : 34.0, "lat" : 67.0 } } }"""); + 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( + var result = PipelineStageDefinitionBuilder.GeoNear( GeoJson.Point(GeoJson.Geographic(34, 67)), - new GeoNearOptions + new GeoNearOptions { DistanceField = "calculatedDistance", MaxDistance = 3, @@ -180,47 +176,46 @@ public void GeoNear_with_geojson_point_should_return_the_expected_result() 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 } }"""); + 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 = + 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]); + var result = + PipelineStageDefinitionBuilder.GeoNear([34.0, 67.0, 23.0, 34.5]); }); - - Assert.Throws(() => - { - var result = - PipelineStageDefinitionBuilder.GeoNear([34.0]); - }); - + Assert.Throws(() => { - var result = - PipelineStageDefinitionBuilder.GeoNear(new BsonDocument - { - { "x", 34.0}, - { "y", 67.0}, - { "z", 25.0}, - { "w", 57.0} - }); + var result = + PipelineStageDefinitionBuilder.GeoNear([34.0]); }); } From 47b81ce93bf4b899a3300b03c9516f461eafdeb7 Mon Sep 17 00:00:00 2001 From: adelinowona Date: Fri, 11 Apr 2025 05:08:29 -0400 Subject: [PATCH 10/11] pr comments --- src/MongoDB.Driver/Core/Misc/Feature.cs | 6 ++++++ src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs | 2 +- tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs | 6 ------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/MongoDB.Driver/Core/Misc/Feature.cs b/src/MongoDB.Driver/Core/Misc/Feature.cs index e0b4dfec461..34dc72e57c8 100644 --- a/src/MongoDB.Driver/Core/Misc/Feature.cs +++ b/src/MongoDB.Driver/Core/Misc/Feature.cs @@ -63,6 +63,7 @@ public class Feature private static readonly Feature __filterLimit = new Feature("FilterLimit", WireVersion.Server60); private static readonly Feature __findAllowDiskUse = new Feature("FindAllowDiskUse", WireVersion.Server44); private static readonly Feature __findProjectionExpressions = new Feature("FindProjectionExpressions", WireVersion.Server44); + private static readonly Feature __geoNearCommand = new Feature("GeoNearCommand", WireVersion.Zero, WireVersion.Server42); private static readonly Feature __getField = new Feature("GetField", WireVersion.Server50); private static readonly Feature __getMoreComment = new Feature("GetMoreComment", WireVersion.Server44); private static readonly Feature __groupCommand = new Feature("GroupCommand", WireVersion.Zero, WireVersion.Server42); @@ -287,6 +288,11 @@ public class Feature /// public static Feature FindProjectionExpressions => __findProjectionExpressions; + /// + /// Gets the geoNear command feature. + /// + public static Feature GeoNearCommand => __geoNearCommand; + /// /// Gets the getField feature. /// diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 2340e8880f9..68604d863df 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -614,7 +614,7 @@ public static PipelineStageDefinition Facet( /// The point for which to find the closest documents. /// The options. /// The stage. - internal static PipelineStageDefinition GeoNear( + private static PipelineStageDefinition GeoNear( TPoint near, GeoNearOptions options = null) // where TPoint is either a GeoJsonPoint or a legacy coordinate array diff --git a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs index 9ae42c415c9..0b80fb3af78 100644 --- a/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs +++ b/tests/MongoDB.Driver.Tests/AggregateGeoNearTests.cs @@ -34,9 +34,6 @@ public void GeoNear_omitting_distanceField_should_return_expected_result() RequireServer.Check().VersionGreaterThanOrEqualTo("8.1.0"); var collection = Fixture.GeoCollection; - - collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2DSphere(p => p.GeoJsonPointLocation))); - var result = collection .Aggregate() .GeoNear( @@ -59,9 +56,6 @@ public void GeoNear_omitting_distanceField_should_return_expected_result() public void GeoNear_using_pipeline_should_return_expected_result() { var collection = Fixture.GeoCollection; - - collection.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Geo2D(p => p.LegacyCoordinateLocation))); - var pipeline = new EmptyPipelineDefinition() .GeoNear( [-73.99279, 40.719296], From 0eb74b87831cf93d9bf086e4c0bb8961effc452f Mon Sep 17 00:00:00 2001 From: adelinowona Date: Fri, 11 Apr 2025 05:31:31 -0400 Subject: [PATCH 11/11] reverse making Geonear method private --- src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs index 68604d863df..2340e8880f9 100644 --- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs +++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs @@ -614,7 +614,7 @@ public static PipelineStageDefinition Facet( /// The point for which to find the closest documents. /// The options. /// The stage. - private static PipelineStageDefinition GeoNear( + internal static PipelineStageDefinition GeoNear( TPoint near, GeoNearOptions options = null) // where TPoint is either a GeoJsonPoint or a legacy coordinate array