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..b4686e3a815 100644 --- a/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs +++ b/src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs @@ -18,8 +18,10 @@ using System.Linq; using MongoDB.Bson; 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 +44,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 +79,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 +109,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; @@ -118,38 +125,37 @@ private protected override BsonDocument RenderArguments(RenderArgs ar internal sealed class EqualsSearchDefinition : OperatorSearchDefinition { - private readonly BsonValue _value; + private readonly TField _value; public EqualsSearchDefinition(FieldDefinition path, TField value, SearchScoreDefinition score) : base(OperatorType.Equals, path, score) { - _value = ToBsonValue(value); + _value = value; } - private protected override BsonDocument RenderArguments(RenderArgs args) => - new("value", _value); - - private static BsonValue ToBsonValue(TField value) => - value switch + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) + { + BsonValue serializedValue; + if (_useConfiguredSerializers) { - 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() - }; + var valueSerializer = fieldSerializer switch + { + null => args.SerializerRegistry.GetSerializer(), + IBsonArraySerializer => ArraySerializerHelper.GetItemSerializer(fieldSerializer), + _ => fieldSerializer + }; + + serializedValue = SerializationHelper.SerializeValue(valueSerializer, _value); + } + else + { + serializedValue = ToBsonValue(_value); + } + + return new BsonDocument("value", serializedValue); + } } internal sealed class ExistsSearchDefinition : OperatorSearchDefinition @@ -172,7 +178,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 +205,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,13 +229,15 @@ 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 { - private readonly BsonArray _values; + private readonly TField[] _values; public InSearchDefinition( SearchPathDefinition path, @@ -234,36 +246,32 @@ public InSearchDefinition( : 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 static BsonValue ToBsonValue(TField value) => - value switch + private protected override BsonDocument RenderArguments( + RenderArgs args, + IBsonSerializer fieldSerializer) + { + BsonValue serializedValues; + if (_useConfiguredSerializers) { - 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() - }; + var arraySerializer = fieldSerializer switch + { + null => new ArraySerializer(args.SerializerRegistry.GetSerializer()), + IBsonArraySerializer => fieldSerializer, + _ => new ArraySerializer((IBsonSerializer)fieldSerializer) + }; + + serializedValues = SerializationHelper.SerializeValue(arraySerializer, _values); + } + else + { + serializedValues = new BsonArray(_values.Select(ToBsonValue)); + } + + return new BsonDocument("value", serializedValues); + } } internal sealed class MoreLikeThisSearchDefinition : OperatorSearchDefinition @@ -276,7 +284,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 +315,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 +340,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,8 +361,9 @@ 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 } @@ -360,7 +373,7 @@ private protected override BsonDocument RenderArguments(RenderArgs ar internal sealed class RangeSearchDefinition : OperatorSearchDefinition { private readonly SearchRangeV2 _range; - + public RangeSearchDefinition( SearchPathDefinition path, SearchRangeV2 range, @@ -370,48 +383,39 @@ public RangeSearchDefinition( _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) + BsonValue serializedMin; + BsonValue serializedMax; + if (_useConfiguredSerializers) { - min = ToBsonValue(_range.Min.Value); - minInclusive = _range.Min.Inclusive; + var valueSerializer = fieldSerializer switch + { + null => args.SerializerRegistry.GetSerializer(), + IBsonArraySerializer => ArraySerializerHelper.GetItemSerializer(fieldSerializer), + _ => fieldSerializer + }; + + serializedMin = _range.Min == null ? null : SerializationHelper.SerializeValue(valueSerializer, _range.Min.Value); + serializedMax = _range.Max == null ? null : SerializationHelper.SerializeValue(valueSerializer, _range.Max.Value); } - - if (_range.Max != null) + else { - max = ToBsonValue(_range.Max.Value); - maxInclusive = _range.Max.Inclusive; + serializedMin = _range.Min == null ? null : ToBsonValue(_range.Min.Value); + serializedMax = _range.Max == null ? null : ToBsonValue(_range.Max.Value); } - - return new() + + var minInclusive = _range.Min?.Inclusive ?? false; + var maxInclusive = _range.Max?.Inclusive ?? false; + + return new BsonDocument { - { minInclusive ? "gte" : "gt", min, min != null }, - { maxInclusive ? "lte" : "lt", max, max != null } + { minInclusive ? "gte" : "gt", serializedMin, serializedMin != null }, + { maxInclusive ? "lte" : "lt", serializedMax, serializedMax != null } }; } - - 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 +434,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 +453,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 +477,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 +502,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..e9097798c1e 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,32 @@ public static implicit operator SearchDefinition(string json) => json != null ? new JsonSearchDefinition(json) : null; } + /// + /// Extension methods for SearchDefinition. + /// + public static class SearchDefinitionExtensions + { + /// + /// Determines whether to use the configured serializers for the specified . + /// When set to true (the default value), the configured serializers will be used to serialize the values of certain Atlas Search operators, such as "Equals", "In" and "Range". + /// If set to false, then a default conversion will be used. + /// + /// The type of the document. + /// The search definition instance. + /// Whether to use the configured serializers or not. + /// The same instance with default serialization enabled. + public static SearchDefinition UseConfiguredSerializers(this SearchDefinition searchDefinition, bool useConfiguredSerializers) + { + if (searchDefinition is not OperatorSearchDefinition operatorSearchDefinition) + { + throw new NotSupportedException($"{nameof(UseConfiguredSerializers)} cannot be used with SearchDefinition type: {searchDefinition.GetType()}."); + } + + operatorSearchDefinition.SetUseConfiguredSerializers(useConfiguredSerializers); + return operatorSearchDefinition; + } + } + /// /// A search definition based on a BSON document. /// @@ -123,9 +151,7 @@ private protected enum OperatorType QueryString, Range, Regex, - Search, Span, - Term, Text, Wildcard } @@ -135,6 +161,8 @@ private protected enum OperatorType protected readonly SearchPathDefinition _path; protected readonly SearchScoreDefinition _score; + protected bool _useConfiguredSerializers = true; + private protected OperatorSearchDefinition(OperatorType operatorType) : this(operatorType, null) { @@ -156,13 +184,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 SetUseConfiguredSerializers(bool useConfiguredSerializers) + { + _useConfiguredSerializers = useConfiguredSerializers; + } + + 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..7e06af49c7b 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 @@ -286,21 +288,73 @@ public void Equals_should_render_supported_type( 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), + $"{{ equals: {{ path: 'x', value: {valueRendered} }} }}"); + + var scoreBuilder = new SearchScoreDefinitionBuilder(); + AssertRendered( + subject.Equals("x", value, scoreBuilder.Constant(1)), + $"{{ equals: {{ path: 'x', value: {valueRendered}, score: {{ constant: {{ value: 1 }} }} }} }}"); + } + AssertRendered( - subject.Equals("x", value), + subjectTyped.Equals(fieldExpression, value), + $"{{ equals: {{ path: '{fieldRendered}', value: {valueRendered} }} }}"); + } + + [Theory] + [MemberData(nameof(EqualsWithConfiguredSerializersSetToFalseSupportedTypesTestData))] + public void Equals_with_configured_serializers_false_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. + AssertRendered( + subject.Equals("x", value).UseConfiguredSerializers(false), $"{{ equals: {{ path: 'x', value: {valueRendered} }} }}"); var scoreBuilder = new SearchScoreDefinitionBuilder(); AssertRendered( - subject.Equals("x", value, scoreBuilder.Constant(1)), + subject.Equals("x", value, scoreBuilder.Constant(1)).UseConfiguredSerializers(false), $"{{ equals: {{ path: 'x', value: {valueRendered}, score: {{ constant: {{ value: 1 }} }} }} }}"); AssertRendered( - subjectTyped.Equals(fieldExpression, value), + subjectTyped.Equals(fieldExpression, value).UseConfiguredSerializers(false), $"{{ equals: {{ path: '{fieldRendered}', value: {valueRendered} }} }}"); } public static object[][] EqualsSupportedTypesTestData => new[] + { + 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" } + }; + + public static object[][] EqualsWithConfiguredSerializersSetToFalseSupportedTypesTestData => new[] { new object[] { true, "true", Exp(p => p.Retired), "ret" }, new object[] { (sbyte)1, "1", Exp(p => p.Int8), nameof(Person.Int8), }, @@ -320,22 +374,48 @@ 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) + [Fact] + public void Equals_should_use_correct_serializers_when_using_attributes_and_expression_path() { - var subject = CreateSubject(); - Record.Exception(() => subject.Equals("x", value)).Should().BeOfType(); + var testGuid = Guid.Parse("01020304-0506-0708-090a-0b0c0d0e0f10"); + var subjectTyped = CreateSubject(); - var subjectTyped = CreateSubject(); - Record.Exception(() => subjectTyped.Equals(fieldExpression, value)).Should().BeOfType(); + AssertRendered( + subjectTyped.Equals(t => t.DefaultGuid, testGuid), + """{ "equals" : { "value" : { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, "path" : "DefaultGuid" } } """); + + AssertRendered( + subjectTyped.Equals(t => t.StringGuid, testGuid), + """{ "equals" : { "value" : "01020304-0506-0708-090a-0b0c0d0e0f10", "path" : "StringGuid" } }"""); } - public static object[][] EqualsUnsupportedTypesTestData => new[] + [Fact] + public void Equals_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), + """{ "equals" : { "value" : { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, "path" : "DefaultGuid" } } """); + + AssertRendered( + subjectTyped.Equals("StringGuid", testGuid), + """{ "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).UseConfiguredSerializers(false), + """{ "equals" : { "value" : "01020304-0506-0708-090a-0b0c0d0e0f10", "path" : "UndefinedRepresentationGuid" } }"""); + } [Fact] public void Exists() @@ -504,24 +584,54 @@ public void In(T[] fieldValues, string[] fieldsRendered) $"{{ in: {{ path: 'x', value: [{string.Join(",", fieldsRendered)}] }} }}"); } + [Theory] + [MemberData(nameof(InWithConfiguredSerializersSetToFalseTestData))] + public void InWithConfiguredSerializersSetToFalse(T[] fieldValues, string[] fieldsRendered) + { + var subject = CreateSubject(); + + AssertRendered( + subject.In("x", fieldValues).UseConfiguredSerializers(false), + $"{{ in: {{ path: 'x', value: [{string.Join(",", fieldsRendered)}] }} }}"); + } + + public static readonly object[][] InWithConfiguredSerializersSetToFalseTestData = + { + 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[][] 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[] { """{ "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] @@ -531,21 +641,45 @@ public void In_typed( string[] fieldValuesRendered, Expression> fieldExpression, string fieldNameRendered) + { + var subjectTyped = 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( + subjectTyped.In("x", fieldValues), + $"{{ in: {{ path: 'x', value: {fieldValuesArray} }} }}"); + } + + AssertRendered( + subjectTyped.In(fieldExpression, fieldValues), + $"{{ in: {{path: '{fieldNameRendered}', value: {fieldValuesArray} }} }}"); + } + + [Theory] + [MemberData(nameof(InTypedWithConfiguredSerializersSetToFalseTestData))] + public void InWithConfiguredSerializersSetToFalse_typed( + T[] fieldValues, + string[] fieldValuesRendered, + Expression> fieldExpression, + string fieldNameRendered) { var subject = CreateSubject(); var fieldValuesArray = $"[{string.Join(",", fieldValuesRendered)}]"; AssertRendered( - subject.In("x", fieldValues), + subject.In("x", fieldValues).UseConfiguredSerializers(false), $"{{ in: {{ path: 'x', value: {fieldValuesArray} }} }}"); AssertRendered( - subject.In(fieldExpression, fieldValues), + subject.In(fieldExpression, fieldValues).UseConfiguredSerializers(false), $"{{ 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) }, @@ -559,22 +693,31 @@ public void In_typed( 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[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" }, Exp(p => p.DateTimeOffset), nameof(Person.DateTimeOffset)}, + 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) }, - 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[][] InTypedWithConfiguredSerializersSetToFalseTestData = { - 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[] { "ISODate(\"0001-01-01T00:00:00Z\")", "ISODate(\"9999-12-31T23:59:59.999Z\")" }, 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) }, + new object[] { new object[] { (byte)1, (short)2, (int)3 }, new[] { "1", "2", "3" }, Exp(p => p.Object), nameof(Person.Object) } + }; [Fact] public void In_should_throw_when_values_are_invalid() @@ -588,34 +731,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_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]), + """{ "in" : { "value" : [{ "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }, { "$binary" : { "base64" : "AQIDBAUGBwgJCgsMDQ4PEA==", "subType" : "04" } }], "path" : "DefaultGuid" } } """); + + AssertRendered( + subjectTyped.In(t => t.StringGuid, [testGuid, testGuid]), + """{ "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_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]), + """{ "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]), + """{ "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_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]), + """{ "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 +1046,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(RangeWithConfiguredSerializersSetToFalseSupportedTypesTestData))] + public void Range_with_configured_serializers_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)).UseConfiguredSerializers(false), + $"{{ range: {{ path: 'age', gte: {minRendered}, lt: {maxRendered} }} }}"); AssertRendered( - subjectTyped.Range(fieldExpression, SearchRangeV2Builder.Gte(min).Lt(max)), + subjectTyped.Range(fieldExpression, SearchRangeBuilder.Gte(min).Lt(max)).UseConfiguredSerializers(false), $"{{ range: {{ path: '{fieldRendered}', gte: {minRendered}, lt: {maxRendered} }} }}"); } @@ -917,35 +1109,75 @@ 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,"""{ "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) } + }; + + public static object[][] RangeWithConfiguredSerializersSetToFalseSupportedTypesTestData => new[] + { + 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, "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) + [Fact] + public void Range_should_use_correct_serializers_when_using_attributes_and_expression_path() { - var subject = CreateSubject(); - Record.Exception(() => subject.Range("age", SearchRangeV2Builder.Gte(value).Lt(value)).Render(new RenderArgs())).Should().BeOfType(); + var testLong = 23; + var subjectTyped = CreateSubject(); - var subjectTyped = CreateSubject(); - Record.Exception(() => subjectTyped.Range(fieldExpression, SearchRangeV2Builder.Gte(value).Lt(value)).Render(new RenderArgs())).Should().BeOfType(); + AssertRendered( + subjectTyped.Range(t => t.DefaultLong, new SearchRange(testLong, null, false, false )), + """{"range" :{ "gt" : 23, "path" : "DefaultLong" }}"""); + + AssertRendered( + subjectTyped.Range(t => t.StringLong, new SearchRange(testLong, null, false, false )), + """{"range":{ "gt" : "23", "path" : "StringLong" }}"""); } - public static object[][] RangeUnsupportedTypesTestData => new[] + [Fact] + public void Range_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 )), + """{"range" :{ "gt" : 23, "path" : "DefaultLong" }}"""); + + AssertRendered( + subjectTyped.Range("StringLong", new SearchRange(testLong, null, false, false )), + """{"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 +1489,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 +1545,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; } + } } }