diff --git a/src/MongoDB.Driver/FieldValueSerializerHelper.cs b/src/MongoDB.Driver/FieldValueSerializerHelper.cs index 16d95460897..68880f7fe18 100644 --- a/src/MongoDB.Driver/FieldValueSerializerHelper.cs +++ b/src/MongoDB.Driver/FieldValueSerializerHelper.cs @@ -141,18 +141,6 @@ public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSer return ConvertIfPossibleSerializer.Create(valueType, fieldType, fieldSerializer, serializerRegistry); } - public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType, object value) - { - if (!valueType.GetTypeInfo().IsValueType && value == null) - { - return fieldSerializer; - } - else - { - return GetSerializerForValueType(fieldSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false); - } - } - // private static methods private static bool HasStringRepresentation(IBsonSerializer serializer) { @@ -313,7 +301,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati } } - internal class IEnumerableSerializer : SerializerBase> + internal class IEnumerableSerializer : SerializerBase>, IBsonArraySerializer { private readonly IBsonSerializer _itemSerializer; @@ -351,6 +339,12 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati bsonWriter.WriteEndArray(); } } + + public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo) + { + serializationInfo = new BsonSerializationInfo(null, _itemSerializer, typeof(TItem)); + return true; + } } internal class NullableEnumConvertingSerializer : SerializerBase> where TFrom : struct where TTo : struct diff --git a/src/MongoDB.Driver/IAggregateFluent.cs b/src/MongoDB.Driver/IAggregateFluent.cs index dfb0b821ec8..e8d825c66e6 100644 --- a/src/MongoDB.Driver/IAggregateFluent.cs +++ b/src/MongoDB.Driver/IAggregateFluent.cs @@ -404,7 +404,6 @@ IAggregateFluent Lookup SetWindowFields( AggregateExpressionDefinition, TWindowFields> output); - //TODO If I add a parameter here, then this would be a binary breaking change /// /// Appends a $search stage to the pipeline. /// diff --git a/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs index 753d8c452b6..2c0ca7e8df5 100644 --- a/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs +++ b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs @@ -17,9 +17,12 @@ using System.Collections.Generic; using System.Linq; using MongoDB.Bson; +using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.GeoJsonObjectModel; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; namespace MongoDB.Driver.Search { @@ -42,8 +45,9 @@ public AutocompleteSearchDefinition( _fuzzy = fuzzy; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "query", _query.Render() }, { "tokenOrder", _tokenOrder.ToCamelCase(), _tokenOrder != SearchAutocompleteTokenOrder.Any }, @@ -76,7 +80,9 @@ public CompoundSearchDefinition( _minimumShouldMatch = minimumShouldMatch; } - private protected override BsonDocument RenderArguments(RenderArgs args) + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) { return new() { @@ -104,7 +110,9 @@ public EmbeddedDocumentSearchDefinition(FieldDefinition args) + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) { // Add base path to all nested operator paths var pathPrefix = _path.Render(args).AsString; @@ -116,40 +124,42 @@ private protected override BsonDocument RenderArguments(RenderArgs ar } } - internal sealed class EqualsSearchDefinition : OperatorSearchDefinition + internal sealed class EqualsSearchDefinition : OperatorSearchDefinition { - private readonly BsonValue _value; + private readonly TValue _value; - public EqualsSearchDefinition(FieldDefinition path, TField value, SearchScoreDefinition score) + public EqualsSearchDefinition(FieldDefinition path, TValue value, SearchScoreDefinition score) : base(OperatorType.Equals, path, score) { - _value = ToBsonValue(value); + _value = value; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new("value", _value); + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) + { + if (!_useConfiguredSerialization) + { + return new BsonDocument("value", ToBsonValue(_value)); + } - private static BsonValue ToBsonValue(TField value) => - value switch + var valueSerializer = fieldSerializer switch { - bool v => (BsonBoolean)v, - sbyte v => (BsonInt32)v, - byte v => (BsonInt32)v, - short v => (BsonInt32)v, - ushort v => (BsonInt32)v, - int v => (BsonInt32)v, - uint v => (BsonInt64)v, - long v => (BsonInt64)v, - float v => (BsonDouble)v, - double v => (BsonDouble)v, - DateTime v => (BsonDateTime)v, - DateTimeOffset v => (BsonDateTime)v.UtcDateTime, - ObjectId v => (BsonObjectId)v, - Guid v => new BsonBinaryData(v, GuidRepresentation.Standard), - string v => (BsonString)v, - null => BsonNull.Value, - _ => throw new InvalidCastException() + null => args.SerializerRegistry.GetSerializer(), + IBsonArraySerializer => ArraySerializerHelper.GetItemSerializer(fieldSerializer), + _ => fieldSerializer }; + + var document = new BsonDocument(); + using var bsonWriter = new BsonDocumentWriter(document); + var context = BsonSerializationContext.CreateRoot(bsonWriter); + bsonWriter.WriteStartDocument(); + bsonWriter.WriteName("value"); + valueSerializer.Serialize(context, _value); + bsonWriter.WriteEndDocument(); + + return document; + } } internal sealed class ExistsSearchDefinition : OperatorSearchDefinition @@ -172,7 +182,9 @@ public FacetSearchDefinition(SearchDefinition @operator, IEnumerable< _facets = Ensure.IsNotNull(facets, nameof(facets)).ToArray(); } - private protected override BsonDocument RenderArguments(RenderArgs args) => + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "operator", _operator.Render(args) }, @@ -197,7 +209,9 @@ public GeoShapeSearchDefinition( _relation = relation; } - private protected override BsonDocument RenderArguments(RenderArgs args) => + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "geometry", _geometry.ToBsonDocument() }, @@ -219,51 +233,53 @@ public GeoWithinSearchDefinition( _area = Ensure.IsNotNull(area, nameof(area)); } - private protected override BsonDocument RenderArguments(RenderArgs args) => + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new(_area.Render()); } - internal sealed class InSearchDefinition : OperatorSearchDefinition + internal sealed class InSearchDefinition : OperatorSearchDefinition { - private readonly BsonArray _values; + private readonly TValue[] _values; public InSearchDefinition( SearchPathDefinition path, - IEnumerable values, + IEnumerable values, SearchScoreDefinition score) : base(OperatorType.In, path, score) { Ensure.IsNotNullOrEmpty(values, nameof(values)); - var array = new BsonArray(values.Select(ToBsonValue)); - - var bsonType = array[0].GetType(); - _values = Ensure.That(array, arr => arr.All(v => v.GetType() == bsonType), nameof(values), "All values must be of the same type."); + _values = values.ToArray(); } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new("value", _values); + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) + { + if (!_useConfiguredSerialization) + { + var values = new BsonArray(_values.Select(ToBsonValue)); + return new BsonDocument("value", values); + } - private static BsonValue ToBsonValue(TField value) => - value switch + var valueSerializer = fieldSerializer switch { - bool v => (BsonBoolean)v, - sbyte v => (BsonInt32)v, - byte v => (BsonInt32)v, - short v => (BsonInt32)v, - ushort v => (BsonInt32)v, - int v => (BsonInt32)v, - uint v => (BsonInt64)v, - long v => (BsonInt64)v, - float v => (BsonDouble)v, - double v => (BsonDouble)v, - decimal v => (BsonDecimal128)v, - DateTime v => (BsonDateTime)v, - DateTimeOffset v => (BsonDateTime)v.UtcDateTime, - string v => (BsonString)v, - ObjectId v => (BsonObjectId)v, - Guid v => new BsonBinaryData(v, GuidRepresentation.Standard), - _ => throw new InvalidCastException() + null => new ArraySerializer(args.SerializerRegistry.GetSerializer()), + IBsonArraySerializer => fieldSerializer, + _ => new ArraySerializer((IBsonSerializer)fieldSerializer) }; + + var document = new BsonDocument(); + using var bsonWriter = new BsonDocumentWriter(document); + var context = BsonSerializationContext.CreateRoot(bsonWriter); + bsonWriter.WriteStartDocument(); + bsonWriter.WriteName("value"); + valueSerializer.Serialize(context, _values); + bsonWriter.WriteEndDocument(); + + return document; + } } internal sealed class MoreLikeThisSearchDefinition : OperatorSearchDefinition @@ -276,7 +292,9 @@ public MoreLikeThisSearchDefinition(IEnumerable like) _like = Ensure.IsNotNull(like, nameof(like)).ToArray(); } - private protected override BsonDocument RenderArguments(RenderArgs args) + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) { var likeSerializer = typeof(TLike) switch { @@ -305,8 +323,9 @@ public NearSearchDefinition( _pivot = pivot; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "origin", _origin }, { "pivot", _pivot } @@ -329,8 +348,9 @@ public PhraseSearchDefinition( _slop = slop; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "query", _query.Render() }, { "slop", _slop, _slop != null } @@ -349,69 +369,81 @@ public QueryStringSearchDefinition(FieldDefinition defaultPath, strin _query = Ensure.IsNotNull(query, nameof(query)); } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "defaultPath", _defaultPath.Render(args) }, { "query", _query } }; } - internal sealed class RangeSearchDefinition : OperatorSearchDefinition + internal sealed class RangeSearchDefinition : OperatorSearchDefinition { - private readonly SearchRangeV2 _range; - + private readonly SearchRangeV2 _range; + public RangeSearchDefinition( SearchPathDefinition path, - SearchRangeV2 range, + SearchRangeV2 range, SearchScoreDefinition score) : base(OperatorType.Range, path, score) { _range = range; } - private protected override BsonDocument RenderArguments(RenderArgs args) + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) { - BsonValue min = null, max = null; - bool minInclusive = false, maxInclusive = false; - - if (_range.Min != null) + if (!_useConfiguredSerialization) { - min = ToBsonValue(_range.Min.Value); - minInclusive = _range.Min.Inclusive; + BsonValue min = null, max = null; + bool minInclusive = false, maxInclusive = false; + + if (_range.Min != null) + { + min = ToBsonValue(_range.Min.Value); + minInclusive = _range.Min.Inclusive; + } + + if (_range.Max != null) + { + max = ToBsonValue(_range.Max.Value); + maxInclusive = _range.Max.Inclusive; + } + + return new() + { + { minInclusive ? "gte" : "gt", min, min != null }, + { maxInclusive ? "lte" : "lt", max, max != null } + }; } - - if (_range.Max != null) + + var valueSerializer = fieldSerializer switch + { + null => args.SerializerRegistry.GetSerializer(), + IBsonArraySerializer => ArraySerializerHelper.GetItemSerializer(fieldSerializer), + _ => fieldSerializer + }; + + var document = new BsonDocument(); + using var bsonWriter = new BsonDocumentWriter(document); + var context = BsonSerializationContext.CreateRoot(bsonWriter); + bsonWriter.WriteStartDocument(); + if (_range.Min != null) { - max = ToBsonValue(_range.Max.Value); - maxInclusive = _range.Max.Inclusive; + bsonWriter.WriteName(_range.Min.Inclusive? "gte" : "gt"); + valueSerializer.Serialize(context, _range.Min.Value); } - - return new() + if (_range.Max is not null) { - { minInclusive ? "gte" : "gt", min, min != null }, - { maxInclusive ? "lte" : "lt", max, max != null } - }; + bsonWriter.WriteName(_range.Max.Inclusive? "lte" : "lt"); + valueSerializer.Serialize(context, _range.Max.Value); + } + bsonWriter.WriteEndDocument(); + + return document; } - - private static BsonValue ToBsonValue(TField value) => - value switch - { - sbyte v => (BsonInt32)v, - byte v => (BsonInt32)v, - short v => (BsonInt32)v, - ushort v => (BsonInt32)v, - int v => (BsonInt32)v, - uint v => (BsonInt64)v, - long v => (BsonInt64)v, - float v => (BsonDouble)v, - double v => (BsonDouble)v, - DateTime v => (BsonDateTime)v, - DateTimeOffset v => (BsonDateTime)v.UtcDateTime, - string v => (BsonString)v, - null => null, - _ => throw new InvalidCastException() - }; } internal sealed class RegexSearchDefinition : OperatorSearchDefinition @@ -430,8 +462,9 @@ public RegexSearchDefinition( _allowAnalyzedField = allowAnalyzedField; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "query", _query.Render() }, { "allowAnalyzedField", _allowAnalyzedField, _allowAnalyzedField }, @@ -448,8 +481,9 @@ public SpanSearchDefinition(SearchSpanDefinition clause) _clause = Ensure.IsNotNull(clause, nameof(clause)); } - private protected override BsonDocument RenderArguments(RenderArgs args) => - _clause.Render(args); + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => _clause.Render(args); } internal sealed class TextSearchDefinition : OperatorSearchDefinition @@ -471,8 +505,8 @@ public TextSearchDefinition( _synonyms = synonyms; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments(RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "query", _query.Render() }, { "fuzzy", () => _fuzzy.Render(), _fuzzy != null }, @@ -496,8 +530,8 @@ public WildcardSearchDefinition( _allowAnalyzedField = allowAnalyzedField; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new() + private protected override BsonDocument RenderArguments(RenderArgs args, + IBsonSerializer fieldSerializer) => new() { { "query", _query.Render() }, { "allowAnalyzedField", _allowAnalyzedField, _allowAnalyzedField }, diff --git a/src/MongoDB.Driver/Search/SearchDefinition.cs b/src/MongoDB.Driver/Search/SearchDefinition.cs index 3b4d23f720c..0d07d88a825 100644 --- a/src/MongoDB.Driver/Search/SearchDefinition.cs +++ b/src/MongoDB.Driver/Search/SearchDefinition.cs @@ -13,7 +13,9 @@ * limitations under the License. */ +using System; using MongoDB.Bson; +using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; namespace MongoDB.Driver.Search @@ -54,6 +56,31 @@ public static implicit operator SearchDefinition(string json) => json != null ? new JsonSearchDefinition(json) : null; } + /// + /// Extensions for SearchDefinition + /// + public static class SearchDefinitionExtensions + { + /// + /// Sets the use of the configured serialization for the specified . + /// When set to true, the configured serializers will be used to serialize the values of certain Atlas Search operators, such as "Equals", "In" and "Range". This will become the default behaviour in version 4.0 of the library. + /// If not enabled, then a default conversion will be used. + /// + /// The type of the document. + /// The search definition instance. + /// Whether to use the configured serialization or not. + /// The same instance with default serialization enabled. + /// Thrown if is not of a valid type/>. + public static SearchDefinition WithConfiguredSerialization(this SearchDefinition searchDefinition, bool useDefaultSerialization) + { + if (searchDefinition is not OperatorSearchDefinition op) + throw new InvalidOperationException("Default serialization cannot be used with the current SearchDefinition type"); + + op.SetUseConfiguredSerialization(useDefaultSerialization); + return searchDefinition; + } + } + /// /// A search definition based on a BSON document. /// @@ -123,9 +150,7 @@ private protected enum OperatorType QueryString, Range, Regex, - Search, Span, - Term, Text, Wildcard } @@ -135,6 +160,8 @@ private protected enum OperatorType protected readonly SearchPathDefinition _path; protected readonly SearchScoreDefinition _score; + protected bool _useConfiguredSerialization = false; + private protected OperatorSearchDefinition(OperatorType operatorType) : this(operatorType, null) { @@ -156,13 +183,53 @@ private protected OperatorSearchDefinition(OperatorType operatorType, SearchPath /// public override BsonDocument Render(RenderArgs args) { - var renderedArgs = RenderArguments(args); - renderedArgs.Add("path", () => _path.Render(args), _path != null); - renderedArgs.Add("score", () => _score.Render(args), _score != null); + BsonDocument renderedArgs; + + if (_path is null) + { + renderedArgs = RenderArguments(args, null); + } + else + { + var renderedPath = _path.RenderAndGetFieldSerializer(args, out var fieldSerializer); + renderedArgs = RenderArguments(args, fieldSerializer); + renderedArgs.Add("path", renderedPath); + } + renderedArgs.Add("score", () => _score.Render(args), _score != null); return new(_operatorType.ToCamelCase(), renderedArgs); } - private protected virtual BsonDocument RenderArguments(RenderArgs args) => new(); + private protected virtual BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) => new(); + + internal void SetUseConfiguredSerialization(bool useDefaultSerialization) + { + _useConfiguredSerialization = useDefaultSerialization; + } + + protected static BsonValue ToBsonValue(T value) => + value switch + { + bool v => (BsonBoolean)v, + sbyte v => (BsonInt32)v, + byte v => (BsonInt32)v, + short v => (BsonInt32)v, + ushort v => (BsonInt32)v, + int v => (BsonInt32)v, + uint v => (BsonInt64)v, + long v => (BsonInt64)v, + float v => (BsonDouble)v, + double v => (BsonDouble)v, + decimal v => (BsonDecimal128)v, + DateTime v => (BsonDateTime)v, + DateTimeOffset v => (BsonDateTime)v.UtcDateTime, + ObjectId v => (BsonObjectId)v, + Guid v => new BsonBinaryData(v, GuidRepresentation.Standard), + string v => (BsonString)v, + null => BsonNull.Value, + _ => throw new InvalidCastException() + }; } } diff --git a/src/MongoDB.Driver/Search/SearchPathDefinition.cs b/src/MongoDB.Driver/Search/SearchPathDefinition.cs index 3b7aad231f9..7ef9058f6ef 100644 --- a/src/MongoDB.Driver/Search/SearchPathDefinition.cs +++ b/src/MongoDB.Driver/Search/SearchPathDefinition.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.Linq; using MongoDB.Bson; +using MongoDB.Bson.Serialization; namespace MongoDB.Driver.Search { @@ -104,10 +105,25 @@ public static implicit operator SearchPathDefinition(List fie /// The render arguments. /// The rendered field. protected string RenderField(FieldDefinition fieldDefinition, RenderArgs args) + => RenderField(fieldDefinition, args, out _); + + internal virtual BsonValue RenderAndGetFieldSerializer( + RenderArgs args, + out IBsonSerializer fieldSerializer) + { + fieldSerializer = null; + return Render(args); + } + + internal string RenderField( + FieldDefinition fieldDefinition, + RenderArgs args, + out IBsonSerializer fieldSerializer) { var renderedField = fieldDefinition.Render(args); var prefix = args.PathRenderArgs.PathPrefix; + fieldSerializer = renderedField.FieldSerializer; return prefix == null ? renderedField.FieldName : $"{prefix}.{renderedField.FieldName}"; } } diff --git a/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs b/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs index 5c63b8430cc..b72b03dc716 100644 --- a/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs +++ b/src/MongoDB.Driver/Search/SearchPathDefinitionBuilder.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Linq.Expressions; using MongoDB.Bson; +using MongoDB.Bson.Serialization; using MongoDB.Driver.Core.Misc; namespace MongoDB.Driver.Search @@ -113,7 +114,7 @@ public AnalyzerSearchPathDefinition(FieldDefinition field, string ana } public override BsonValue Render(RenderArgs args) => - new BsonDocument() + new BsonDocument { { "value", RenderField(_field, args) }, { "multi", _analyzerName } @@ -144,6 +145,9 @@ public SingleSearchPathDefinition(FieldDefinition field) public override BsonValue Render(RenderArgs args) => RenderField(_field, args); + + internal override BsonValue RenderAndGetFieldSerializer(RenderArgs args, out IBsonSerializer fieldSerializer) + => RenderField(_field, args, out fieldSerializer); } internal sealed class WildcardSearchPathDefinition : SearchPathDefinition diff --git a/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs index 05bcbaf1829..ec649f4d435 100644 --- a/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs @@ -20,6 +20,7 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.GeoJsonObjectModel; using MongoDB.Driver.Search; using Xunit; @@ -249,7 +250,8 @@ public void EmbeddedDocument_typed() // Nested AssertRendered( - subjectFamily.EmbeddedDocument(p => p.Relatives, subjectFamily.EmbeddedDocument(p => p.Children, subjectPerson.Text(p => p.FirstName, "Alice"))), + subjectFamily.EmbeddedDocument(p => p.Relatives, + subjectFamily.EmbeddedDocument(p => p.Children, subjectPerson.Text(p => p.FirstName, "Alice"))), "{ embeddedDocument: { path : 'Relatives', operator : { embeddedDocument: { path : 'Relatives.Children', operator : { 'text' : { path: 'Relatives.Children.fn', query : 'Alice' } } } } } }"); // Multipath @@ -300,6 +302,36 @@ public void Equals_should_render_supported_type( $"{{ equals: {{ path: '{fieldRendered}', value: {valueRendered} }} }}"); } + [Theory] + [MemberData(nameof(EqualsWithConfiguredSerializerSupportedTypesTestData))] + public void Equals_with_configured_serializer_should_render_supported_type( + T value, + string valueRendered, + Expression> fieldExpression, + string fieldRendered) + { + var subject = CreateSubject(); + var subjectTyped = CreateSubject(); + + //When using an untyped query, the default GuidSerializer is used, and that will throw an exception + //because the GuidRepresentation is Unspecified. + if (typeof(T) != typeof(Guid)) + { + AssertRendered( + subject.Equals("x", value).WithConfiguredSerialization(true), + $"{{ equals: {{ path: 'x', value: {valueRendered} }} }}"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Equals("x", value, scoreBuilder.Constant(1)).WithConfiguredSerialization(true), + $"{{ equals: {{ path: 'x', value: {valueRendered}, score: {{ constant: {{ value: 1 }} }} }} }}"); + } + + AssertRendered( + subjectTyped.Equals(fieldExpression, value).WithConfiguredSerialization(true), + $"{{ equals: {{ path: '{fieldRendered}', value: {valueRendered} }} }}"); + } + public static object[][] EqualsSupportedTypesTestData => new[] { new object[] { true, "true", Exp(p => p.Retired), "ret" }, @@ -320,22 +352,68 @@ public void Equals_should_render_supported_type( new object[] { "Jim", "\"Jim\"", Exp(p => p.FirstName), "fn" } }; - [Theory] - [MemberData(nameof(EqualsUnsupportedTypesTestData))] - public void Equals_should_throw_on_unsupported_type(T value, Expression> fieldExpression) + public static object[][] EqualsWithConfiguredSerializerSupportedTypesTestData => new[] { - var subject = CreateSubject(); - Record.Exception(() => subject.Equals("x", value)).Should().BeOfType(); + new object[] { true, "true", Exp(p => p.Retired), "ret" }, + new object[] { (sbyte)1, "1", Exp(p => p.Int8), nameof(Person.Int8), }, + new object[] { (byte)1, "1", Exp(p => p.UInt8), nameof(Person.UInt8), }, + new object[] { (short)1, "1", Exp(p => p.Int16), nameof(Person.Int16) }, + new object[] { (ushort)1, "1", Exp(p => p.UInt16), nameof(Person.UInt16) }, + new object[] { (int)1, "1", Exp(p => p.Int32), nameof(Person.Int32) }, + new object[] { (uint)1, "1", Exp(p => p.UInt32), nameof(Person.UInt32) }, + new object[] { long.MaxValue, "NumberLong(\"9223372036854775807\")", Exp(p => p.Int64), nameof(Person.Int64) }, + new object[] { (float)1, "1", Exp(p => p.Float), nameof(Person.Float) }, + new object[] { (double)1, "1", Exp(p => p.Double), nameof(Person.Double) }, + new object[] { DateTime.MinValue, "ISODate(\"0001-01-01T00:00:00Z\")", Exp(p => p.Birthday), "dob" }, + new object[] { DateTimeOffset.MaxValue, """{ "DateTime" : { "$date" : "9999-12-31T23:59:59.999Z" }, "Ticks" : 3155378975999999999, "Offset" : 0 }""", Exp(p => p.DateTimeOffset), nameof(Person.DateTimeOffset) }, + new object[] { ObjectId.Empty, "{ $oid: '000000000000000000000000' }", Exp(p => p.Id), "_id" }, + new object[] { Guid.Empty, """{ "$binary" : { "base64" : "AAAAAAAAAAAAAAAAAAAAAA==", "subType" : "04" } }""", Exp(p => p.Guid), nameof(Person.Guid) }, + new object[] { null, "null", Exp(p => p.Name), nameof(Person.Name) }, + new object[] { "Jim", "\"Jim\"", Exp(p => p.FirstName), "fn" } + }; - var subjectTyped = CreateSubject(); - Record.Exception(() => subjectTyped.Equals(fieldExpression, value)).Should().BeOfType(); + [Fact] + public void Equals_with_configured_serializer_should_use_correct_serializers_when_using_attributes_and_expression_path() + { + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.Equals(t => t.DefaultGuid, testGuid).WithConfiguredSerialization(true), + """{ "equals" : { "value" : { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, "path" : "DefaultGuid" } } """); + + AssertRendered( + subjectTyped.Equals(t => t.StringGuid, testGuid).WithConfiguredSerialization(true), + """{ "equals" : { "value" : "01020304-0506-0708-090a-0b0c0d0e0f10", "path" : "StringGuid" } }"""); } - public static object[][] EqualsUnsupportedTypesTestData => new[] + [Fact] + public void Equals_with_configured_serializer_should_use_correct_serializers_when_using_attributes_and_string_path() { - new object[] { (ulong)1, Exp(p => p.UInt64) }, - new object[] { TimeSpan.Zero, Exp(p => p.TimeSpan) }, - }; + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.Equals("DefaultGuid", testGuid).WithConfiguredSerialization(true), + """{ "equals" : { "value" : { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, "path" : "DefaultGuid" } } """); + + AssertRendered( + subjectTyped.Equals("StringGuid", testGuid).WithConfiguredSerialization(true), + """{ "equals" : { "value" : "01020304-0506-0708-090a-0b0c0d0e0f10", "path" : "StringGuid" } }"""); + } + + [Fact(Skip = "This should only be run manually due to the use of BsonSerializer.RegisterSerializer")] + public void Equals_should_use_correct_serializers_when_using_serializer_registry() + { + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); + + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.Equals(t => t.UndefinedRepresentationGuid, testGuid).WithConfiguredSerialization(true), + """{ "equals" : { "value" : "01020304-0506-0708-090a-0b0c0d0e0f10", "path" : "UndefinedRepresentationGuid" } }"""); + } [Fact] public void Exists() @@ -504,24 +582,54 @@ public void In(T[] fieldValues, string[] fieldsRendered) $"{{ in: {{ path: 'x', value: [{string.Join(",", fieldsRendered)}] }} }}"); } + [Theory] + [MemberData(nameof(InWithConfiguredSerializationTestData))] + public void InWithConfiguredSerialization(T[] fieldValues, string[] fieldsRendered) + { + var subject = CreateSubject(); + + AssertRendered( + subject.In("x", fieldValues).WithConfiguredSerialization(true), + $"{{ in: {{ path: 'x', value: [{string.Join(",", fieldsRendered)}] }} }}"); + } + public static readonly object[][] InTestData = { - new object[] { new bool[] { true, false }, new[] { "true", "false" } }, - new object[] { new byte[] { 1, 2 }, new[] { "1", "2" } }, - new object[] { new sbyte[] { 1, 2 }, new[] { "1", "2" } }, - new object[] { new short[] { 1, 2 }, new[] { "1", "2" } }, - new object[] { new ushort[] { 1, 2 }, new[] { "1", "2" } }, - new object[] { new int[] { 1, 2 }, new[] { "1", "2" } }, - new object[] { new uint[] { 1, 2 }, new[] { "1", "2" } }, - new object[] { new long[] { long.MaxValue, long.MinValue }, new[] { "NumberLong(\"9223372036854775807\")", "NumberLong(\"-9223372036854775808\")" } }, - new object[] { new float[] { 1.5f, 2.5f }, new[] { "1.5", "2.5" } }, - new object[] { new double[] { 1.5, 2.5 }, new[] { "1.5", "2.5" } }, - new object[] { new decimal[] { 1.5m, 2.5m }, new[] { "NumberDecimal(\"1.5\")", "NumberDecimal(\"2.5\")" } }, - new object[] { new[] { "str1", "str2" }, new[] { "'str1'", "'str2'" } }, - new object[] { new[] { DateTime.MinValue, DateTime.MaxValue }, new[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" } }, - new object[] { new[] { DateTimeOffset.MinValue, DateTimeOffset.MaxValue }, new[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" } }, - new object[] { new[] { ObjectId.Empty, ObjectId.Parse("4d0ce088e447ad08b4721a37") }, new[] { "{ $oid: '000000000000000000000000' }", "{ $oid: '4d0ce088e447ad08b4721a37' }" } }, - new object[] { new object[] { (byte)1, (short)2, (int)3 }, new[] { "1", "2", "3" } } + new object[] { new bool[] { true, false }, new[] { "true", "false" } }, + new object[] { new byte[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new sbyte[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new short[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new ushort[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new int[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new uint[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new long[] { long.MaxValue, long.MinValue }, new[] { "NumberLong(\"9223372036854775807\")", "NumberLong(\"-9223372036854775808\")" } }, + new object[] { new float[] { 1.5f, 2.5f }, new[] { "1.5", "2.5" } }, + new object[] { new double[] { 1.5, 2.5 }, new[] { "1.5", "2.5" } }, + new object[] { new decimal[] { 1.5m, 2.5m }, new[] { "NumberDecimal(\"1.5\")", "NumberDecimal(\"2.5\")" } }, + new object[] { new[] { "str1", "str2" }, new[] { "'str1'", "'str2'" } }, + new object[] { new[] { DateTime.MinValue, DateTime.MaxValue }, new[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" } }, + new object[] { new[] { DateTimeOffset.MinValue, DateTimeOffset.MaxValue }, new[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" } }, + new object[] { new[] { ObjectId.Empty, ObjectId.Parse("4d0ce088e447ad08b4721a37") }, new[] { "{ $oid: '000000000000000000000000' }", "{ $oid: '4d0ce088e447ad08b4721a37' }" } }, + new object[] { new object[] { (byte)1, (short)2, (int)3 }, new[] { "1", "2", "3" } } + }; + + public static readonly object[][] InWithConfiguredSerializationTestData = + { + new object[] { new bool[] { true, false }, new[] { "true", "false" } }, + new object[] { new byte[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new sbyte[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new short[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new ushort[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new int[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new uint[] { 1, 2 }, new[] { "1", "2" } }, + new object[] { new long[] { long.MaxValue, long.MinValue }, new[] { "NumberLong(\"9223372036854775807\")", "NumberLong(\"-9223372036854775808\")" } }, + new object[] { new float[] { 1.5f, 2.5f }, new[] { "1.5", "2.5" } }, + new object[] { new double[] { 1.5, 2.5 }, new[] { "1.5", "2.5" } }, + new object[] { new decimal[] { 1.5m, 2.5m }, new[] { "NumberDecimal(\"1.5\")", "NumberDecimal(\"2.5\")" } }, + new object[] { new[] { "str1", "str2" }, new[] { "'str1'", "'str2'" } }, + new object[] { new[] { DateTime.MinValue, DateTime.MaxValue }, new[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" } }, + new object[] { new[] { DateTimeOffset.MinValue, DateTimeOffset.MaxValue }, new[] { """{ "DateTime" : { "$date" : { "$numberLong" : "-62135596800000" } }, "Ticks" : 0, "Offset" : 0 } """, """ { "DateTime" : { "$date" : "9999-12-31T23:59:59.999Z" }, "Ticks" : 3155378975999999999, "Offset" : 0 } """ } }, + new object[] { new[] { ObjectId.Empty, ObjectId.Parse("4d0ce088e447ad08b4721a37") }, new[] { "{ $oid: '000000000000000000000000' }", "{ $oid: '4d0ce088e447ad08b4721a37' }" } }, }; [Theory] @@ -532,20 +640,44 @@ public void In_typed( Expression> fieldExpression, string fieldNameRendered) { - var subject = CreateSubject(); + var subjectTyped = CreateSubject(); var fieldValuesArray = $"[{string.Join(",", fieldValuesRendered)}]"; AssertRendered( - subject.In("x", fieldValues), + subjectTyped.In("x", fieldValues), $"{{ in: {{ path: 'x', value: {fieldValuesArray} }} }}"); AssertRendered( - subject.In(fieldExpression, fieldValues), + subjectTyped.In(fieldExpression, fieldValues), + $"{{ in: {{path: '{fieldNameRendered}', value: {fieldValuesArray} }} }}"); + } + + [Theory] + [MemberData(nameof(InTypedWithConfiguredSerializationTestData))] + public void InWithConfiguredSerialization_typed( + T[] fieldValues, + string[] fieldValuesRendered, + Expression> fieldExpression, + string fieldNameRendered) + { + var subject = CreateSubject(); + var fieldValuesArray = $"[{string.Join(",", fieldValuesRendered)}]"; + + //There is no property called "x" where to pick up a properly configured GuidSerializer for the tests + if (typeof(T) != typeof(Guid)) + { + AssertRendered( + subject.In("x", fieldValues).WithConfiguredSerialization(true), + $"{{ in: {{ path: 'x', value: {fieldValuesArray} }} }}"); + } + + AssertRendered( + subject.In(fieldExpression, fieldValues).WithConfiguredSerialization(true), $"{{ in: {{path: '{fieldNameRendered}', value: {fieldValuesArray} }} }}"); } public static readonly object[][] InTypedTestData = - { + { new object[] { new bool[] { true, false }, new[] { "true", "false" }, Exp(p => p.Retired), "ret" }, new object[] { new byte[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.UInt8), nameof(Person.UInt8) }, new object[] { new sbyte[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.Int8), nameof(Person.Int8) }, @@ -565,16 +697,25 @@ public void In_typed( new object[] { new object[] { (byte)1, (short)2, (int)3 }, new[] { "1", "2", "3" }, Exp(p => p.Object), nameof(Person.Object) } }; - [Theory] - [MemberData(nameof(InUnsupportedTypesTestData))] - public void In_should_throw_on_unsupported_types(T value, Expression> fieldExpression) + public static readonly object[][] InTypedWithConfiguredSerializationTestData = { - var subject = CreateSubject(); - Record.Exception(() => subject.In("x", new[] { value } )).Should().BeOfType(); - - var subjectTyped = CreateSubject(); - Record.Exception(() => subjectTyped.In(fieldExpression, new[] { value })).Should().BeOfType(); - } + new object[] { new bool[] { true, false }, new[] { "true", "false" }, Exp(p => p.Retired), "ret" }, + new object[] { new byte[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.UInt8), nameof(Person.UInt8) }, + new object[] { new sbyte[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.Int8), nameof(Person.Int8) }, + new object[] { new short[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.Int16), nameof(Person.Int16) }, + new object[] { new ushort[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.UInt16), nameof(Person.UInt16) }, + new object[] { new int[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.Int32), nameof(Person.Int32) }, + new object[] { new uint[] { 1, 2 }, new[] { "1", "2" }, Exp(p => p.UInt32), nameof(Person.UInt32) }, + new object[] { new long[] { long.MaxValue, long.MinValue }, new[] { "NumberLong(\"9223372036854775807\")", "NumberLong(\"-9223372036854775808\")" }, Exp(p => p.Int64), nameof(Person.Int64) }, + new object[] { new float[] { 1.5f, 2.5f }, new[] { "1.5", "2.5" }, Exp(p => p.Float), nameof(Person.Float) }, + new object[] { new double[] { 1.5, 2.5 }, new[] { "1.5", "2.5" }, Exp(p => p.Double), nameof(Person.Double) }, + new object[] { new decimal[] { 1.5m, 2.5m }, new[] { "NumberDecimal(\"1.5\")", "NumberDecimal(\"2.5\")" }, Exp(p => p.Decimal), nameof(Person.Decimal) }, + new object[] { new[] { "str1", "str2" }, new[] { "'str1'", "'str2'" }, Exp(p => p.FirstName), "fn" }, + new object[] { new[] { DateTime.MinValue, DateTime.MaxValue }, new[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" }, Exp(p => p.Birthday), "dob" }, + new object[] { new[] { DateTimeOffset.MinValue, DateTimeOffset.MaxValue }, new[] { """{ "DateTime" : { "$date" : { "$numberLong" : "-62135596800000" } }, "Ticks" : 0, "Offset" : 0 } """, """ { "DateTime" : { "$date" : "9999-12-31T23:59:59.999Z" }, "Ticks" : 3155378975999999999, "Offset" : 0 } """ }, Exp(p => p.DateTimeOffset), nameof(Person.DateTimeOffset)}, + new object[] { new[] { ObjectId.Empty, ObjectId.Parse("4d0ce088e447ad08b4721a37") }, new[] { "{ $oid: '000000000000000000000000' }", "{ $oid: '4d0ce088e447ad08b4721a37' }" }, Exp(p => p.Id), "_id" }, + new object[] { new[] { Guid.Empty, Guid.Parse("b52af144-bc97-454f-a578-418a64fa95bf") }, new[] { """{ "$binary" : { "base64" : "AAAAAAAAAAAAAAAAAAAAAA==", "subType" : "04" } }""", """{ "$binary" : { "base64" : "tSrxRLyXRU+leEGKZPqVvw==", "subType" : "04" } }""" }, Exp(p => p.Guid), nameof(Person.Guid) }, + }; [Fact] public void In_should_throw_when_values_are_invalid() @@ -588,34 +729,70 @@ public void In_should_throw_when_values_are_invalid() Record.Exception(() => subjectTyped.In(p => p.Age, null)).Should().BeOfType(); } - public static object[][] InUnsupportedTypesTestData => new[] + [Fact] + public void In_with_configured_serialization_should_use_correct_serializers_when_using_attributes_and_expression_path() { - new object[] { (ulong)1, Exp(p => p.UInt64) }, - new object[] { TimeSpan.Zero, Exp(p => p.TimeSpan) }, - }; + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.In(t => t.DefaultGuid, [testGuid, testGuid]).WithConfiguredSerialization(true), + """{ "in" : { "value" : [{ "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }], "path" : "DefaultGuid" } } """); + + AssertRendered( + subjectTyped.In(t => t.StringGuid, [testGuid, testGuid]).WithConfiguredSerialization(true), + """{ "in" : { "value" : ["01020304-0506-0708-090a-0b0c0d0e0f10", "01020304-0506-0708-090a-0b0c0d0e0f10"], "path" : "StringGuid" } }"""); + } [Fact] - public void In_should_throw_when_values_are_not_of_same_type() + public void In_with_configured_serialization_should_use_correct_serializers_when_using_attributes_and_string_path() { - var values = new object[] { 1.5, 1 }; + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); - var subject = CreateSubject(); - Record.Exception(() => subject.In("x", values)).Should().BeOfType(); + AssertRendered( + subjectTyped.In("DefaultGuid", [testGuid, testGuid]).WithConfiguredSerialization(true), + """{ "in" : { "value" : [{ "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }], "path" : "DefaultGuid" } } """); - var subjectTyped = CreateSubject(); - Record.Exception(() => subjectTyped.In(p => p.Object, values)).Should().BeOfType(); + AssertRendered( + subjectTyped.In("StringGuid", [testGuid, testGuid]).WithConfiguredSerialization(true), + """{ "in" : { "value" : ["01020304-0506-0708-090a-0b0c0d0e0f10", "01020304-0506-0708-090a-0b0c0d0e0f10"], "path" : "StringGuid" } }"""); } - + + [Fact(Skip = "This should only be run manually due to the use of BsonSerializer.RegisterSerializer")] + public void In_with_configured_serialization_should_use_correct_serializers_when_using_serializer_registry() + { + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); + + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.In(t => t.UndefinedRepresentationGuid, [testGuid, testGuid]).WithConfiguredSerialization(true), + """{ "in" : { "value" : ["01020304-0506-0708-090a-0b0c0d0e0f10", "01020304-0506-0708-090a-0b0c0d0e0f10"], "path" : "UndefinedRepresentationGuid" } }"""); + } + [Fact] public void In_with_array_field_should_render_correctly() { var subjectTyped = CreateSubject(); - + AssertRendered( subjectTyped.In(p => p.Hobbies, ["dance", "ski"]), "{ in: { path: 'hobbies', value: ['dance', 'ski'] } }"); } + [Fact] + public void In_with_wildcard_path_should_render_correctly() + { + var subjectTyped = CreateSubject(); + + var path = new SearchPathDefinitionBuilder(); + AssertRendered( + subjectTyped.In(path.Wildcard("*"), ["dance"]), + "{ in: { path: { wildcard: '*'}, value: ['dance'] } }"); + } + [Fact] public void MoreLikeThis() { @@ -867,42 +1044,55 @@ public void QueryString_typed() [InlineData(1, 10, true, true, "gte: 1, lte: 10")] public void Range_should_render_correct_operator(int? min, int? max, bool minInclusive, bool maxInclusive, string rangeRendered) { - var searchRange = new SearchRange(min, max, minInclusive, maxInclusive); + var subject = CreateSubject(); + AssertRendered( + subject.Range("x", new SearchRange(min, max, minInclusive, maxInclusive)), + $"{{ range: {{ path: 'x', {rangeRendered} }} }}"); + } - var searchRangev2 = new SearchRangeV2( - min.HasValue ? new(min.Value, minInclusive) : null, - max.HasValue ? new(max.Value, maxInclusive) : null); - + [Theory] + [MemberData(nameof(RangeSupportedTypesTestData))] + public void Range_should_render_supported_types( + T min, + T max, + string minRendered, + string maxRendered, + Expression> fieldExpression, + string fieldRendered) + where T : struct, IComparable + { var subject = CreateSubject(); - - var searchRangeQuery = subject.Range("x", searchRange); - var searchRangeV2Query = subject.Range("x", searchRangev2); + var subjectTyped = CreateSubject(); - var expected = $"{{ range: {{ path: 'x', {rangeRendered} }} }}"; + AssertRendered( + subject.Range("age", SearchRangeBuilder.Gte(min).Lt(max)), + $"{{ range: {{ path: 'age', gte: {minRendered}, lt: {maxRendered} }} }}"); - AssertRendered(searchRangeQuery, expected); - AssertRendered(searchRangeV2Query, expected); + AssertRendered( + subjectTyped.Range(fieldExpression, SearchRangeBuilder.Gte(min).Lt(max)), + $"{{ range: {{ path: '{fieldRendered}', gte: {minRendered}, lt: {maxRendered} }} }}"); } [Theory] - [MemberData(nameof(RangeSupportedTypesTestData))] - public void Range_should_render_supported_types( + [MemberData(nameof(RangeWithConfiguredSerializationSupportedTypesTestData))] + public void Range_with_configured_serialization_should_render_supported_types( T min, T max, string minRendered, string maxRendered, Expression> fieldExpression, string fieldRendered) + where T : struct, IComparable { var subject = CreateSubject(); var subjectTyped = CreateSubject(); AssertRendered( - subject.Range("testField", SearchRangeV2Builder.Gte(min).Lt(max)), - $"{{ range: {{ path: 'testField', gte: {minRendered}, lt: {maxRendered} }} }}"); + subject.Range("age", SearchRangeBuilder.Gte(min).Lt(max)).WithConfiguredSerialization(true), + $"{{ range: {{ path: 'age', gte: {minRendered}, lt: {maxRendered} }} }}"); AssertRendered( - subjectTyped.Range(fieldExpression, SearchRangeV2Builder.Gte(min).Lt(max)), + subjectTyped.Range(fieldExpression, SearchRangeBuilder.Gte(min).Lt(max)).WithConfiguredSerialization(true), $"{{ range: {{ path: '{fieldRendered}', gte: {minRendered}, lt: {maxRendered} }} }}"); } @@ -917,35 +1107,79 @@ public void Range_should_render_supported_types( new object[] { long.MinValue, long.MaxValue, "NumberLong(\"-9223372036854775808\")", "NumberLong(\"9223372036854775807\")", Exp(p => p.Int64), nameof(Person.Int64) }, new object[] { (float)1, (float)2, "1", "2", Exp(p => p.Float), nameof(Person.Float) }, new object[] { (double)1, (double)2, "1", "2", Exp(p => p.Double), nameof(Person.Double) }, - new object[] { "A", "D", "'A'", "'D'", Exp(p => p.FirstName), "fn" }, new object[] { DateTime.MinValue, DateTime.MaxValue, "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")", Exp(p => p.Birthday), "dob" }, new object[] { DateTimeOffset.MinValue, DateTimeOffset.MaxValue, "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")", Exp(p => p.DateTimeOffset), nameof(Person.DateTimeOffset) } }; - [Theory] - [MemberData(nameof(RangeUnsupportedTypesTestData))] - public void Range_should_throw_on_unsupported_types(T value, Expression> fieldExpression) + public static object[][] RangeWithConfiguredSerializationSupportedTypesTestData => new[] { - var subject = CreateSubject(); - Record.Exception(() => subject.Range("age", SearchRangeV2Builder.Gte(value).Lt(value)).Render(new RenderArgs())).Should().BeOfType(); + new object[] { (sbyte)1, (sbyte)2, "1", "2", Exp(p => p.Int8), nameof(Person.Int8) }, + new object[] { (byte)1, (byte)2, "1", "2", Exp(p => p.UInt8), nameof(Person.UInt8) }, + new object[] { (short)1, (short)2, "1", "2", Exp(p => p.Int16), nameof(Person.Int16) }, + new object[] { (ushort)1, (ushort)2, "1", "2", Exp(p => p.UInt16), nameof(Person.UInt16) }, + new object[] { (int)1, (int)2, "1", "2", Exp(p => p.Int32), nameof(Person.Int32) }, + new object[] { (uint)1, (uint)2, "1", "2", Exp(p => p.UInt32), nameof(Person.UInt32) }, + new object[] { long.MinValue, long.MaxValue, "NumberLong(\"-9223372036854775808\")", "NumberLong(\"9223372036854775807\")", Exp(p => p.Int64), nameof(Person.Int64) }, + new object[] { (float)1, (float)2, "1", "2", Exp(p => p.Float), nameof(Person.Float) }, + new object[] { (double)1, (double)2, "1", "2", Exp(p => p.Double), nameof(Person.Double) }, + new object[] { DateTime.MinValue, DateTime.MaxValue, "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")", Exp(p => p.Birthday), "dob" }, + new object[] { DateTimeOffset.MinValue, DateTimeOffset.MaxValue,"""{ "DateTime" : { "$date" : { "$numberLong" : "-62135596800000" } }, "Ticks" : 0, "Offset" : 0 }""", """{ "DateTime" : { "$date" : "9999-12-31T23:59:59.999Z" }, "Ticks" : 3155378975999999999, "Offset" : 0 }""", Exp(p => p.DateTimeOffset), nameof(Person.DateTimeOffset) } + }; - var subjectTyped = CreateSubject(); - Record.Exception(() => subjectTyped.Range(fieldExpression, SearchRangeV2Builder.Gte(value).Lt(value)).Render(new RenderArgs())).Should().BeOfType(); + [Fact] + public void Range_with_configured_serialization_should_use_correct_serializers_when_using_attributes_and_expression_path() + { + var testLong = 23; + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.Range(t => t.DefaultLong, new SearchRange(testLong, null, false, false )) + .WithConfiguredSerialization(true), + """{"range" :{ "gt" : 23, "path" : "DefaultLong" }}"""); + + AssertRendered( + subjectTyped.Range(t => t.StringLong, new SearchRange(testLong, null, false, false )) + .WithConfiguredSerialization(true), + """{"range":{ "gt" : "23", "path" : "StringLong" }}"""); } - public static object[][] RangeUnsupportedTypesTestData => new[] + [Fact] + public void Range_with_configured_serialization_should_use_correct_serializers_when_using_attributes_and_string_path() { - new object[] { (ulong)1, Exp(p => p.UInt64) }, - new object[] { TimeSpan.Zero, Exp(p => p.TimeSpan) }, - }; - + var testLong = 23; + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.Range("DefaultLong", new SearchRange(testLong, null, false, false )) + .WithConfiguredSerialization(true), + """{"range" :{ "gt" : 23, "path" : "DefaultLong" }}"""); + + AssertRendered( + subjectTyped.Range("StringLong", new SearchRange(testLong, null, false, false )) + .WithConfiguredSerialization(true), + """{"range":{ "gt" : "23", "path" : "StringLong" }}"""); + } + + [Fact(Skip = "This should only be run manually due to the use of BsonSerializer.RegisterSerializer")] + public void Range_should_use_correct_serializers_when_using_serializer_registry() + { + BsonSerializer.RegisterSerializer(new Int64Serializer(BsonType.String)); + + var testLong = 23; + var subjectTyped = CreateSubject(); + + AssertRendered( + subjectTyped.Range(t => t.DefaultLong, new SearchRange(testLong, null, false, false )), + """{"range":{ "gt" : "23", "path" : "DefaultLong" }}"""); + } + [Fact] public void Range_with_array_field_should_render_correctly() { var subject = CreateSubject(); AssertRendered( - subject.Range(x => x.SalaryHistory, SearchRangeV2Builder.Gte(1000).Lt(2000)), + subject.Range(x => x.SalaryHistory, SearchRangeBuilder.Gte(1000).Lt(2000)), "{ range: { path: 'salaries', gte: 1000, lt: 2000 } }"); } @@ -1257,6 +1491,8 @@ public class Person : SimplePerson public float Float { get; set; } public double Double { get; set; } public decimal Decimal { get; set; } + + [BsonGuidRepresentation(GuidRepresentation.Standard)] public Guid Guid { get; set; } public DateTimeOffset DateTimeOffset { get; set; } public TimeSpan TimeSpan { get; set; } @@ -1311,5 +1547,21 @@ public class SimplestPerson [BsonElement("fn")] public string FirstName { get; set; } } + + public class AttributesTestClass + { + [BsonGuidRepresentation(GuidRepresentation.Standard)] + public Guid DefaultGuid { get; set; } + + [BsonRepresentation(BsonType.String)] + public Guid StringGuid { get; set; } + + public Guid UndefinedRepresentationGuid { get; set; } + + public long DefaultLong { get; set; } + + [BsonRepresentation(BsonType.String)] + public long StringLong { get; set; } + } } }