Skip to content

Commit 3ed97d9

Browse files
BorisDogDmitryLukyanov
authored andcommitted
CSHARP-4528: Extend Search.Equals to support number and date (#1043)
* CSHARP-4528: Extend Search.Equals to support number and date
1 parent 4284dde commit 3ed97d9

File tree

3 files changed

+103
-65
lines changed

3 files changed

+103
-65
lines changed

src/MongoDB.Driver/Search/OperatorSearchDefinitions.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,37 @@ Func<BsonArray> Render(List<SearchDefinition<TDocument>> searchDefinitions) =>
9191
}
9292
}
9393

94-
internal sealed class EqualsSearchDefinition<TDocument> : OperatorSearchDefinition<TDocument>
94+
internal sealed class EqualsSearchDefinition<TDocument, TField> : OperatorSearchDefinition<TDocument>
9595
{
9696
private readonly BsonValue _value;
9797

98-
public EqualsSearchDefinition(FieldDefinition<TDocument> path, BsonValue value, SearchScoreDefinition<TDocument> score)
98+
public EqualsSearchDefinition(FieldDefinition<TDocument> path, TField value, SearchScoreDefinition<TDocument> score)
9999
: base(OperatorType.Equals, path, score)
100100
{
101-
_value = value;
101+
_value = ToBsonValue(value);
102102
}
103103

104104
private protected override BsonDocument RenderArguments(IBsonSerializer<TDocument> documentSerializer, IBsonSerializerRegistry serializerRegistry) =>
105105
new("value", _value);
106+
107+
private static BsonValue ToBsonValue(TField value) =>
108+
value switch
109+
{
110+
bool v => (BsonBoolean)v,
111+
sbyte v => (BsonInt32)v,
112+
byte v => (BsonInt32)v,
113+
short v => (BsonInt32)v,
114+
ushort v => (BsonInt32)v,
115+
int v => (BsonInt32)v,
116+
uint v => (BsonInt64)v,
117+
long v => (BsonInt64)v,
118+
float v => (BsonDouble)v,
119+
double v => (BsonDouble)v,
120+
DateTime v => (BsonDateTime)v,
121+
DateTimeOffset v => (BsonDateTime)v.UtcDateTime,
122+
ObjectId v => (BsonObjectId)v,
123+
_ => throw new InvalidCastException()
124+
};
106125
}
107126

108127
internal sealed class ExistsSearchDefinition<TDocument> : OperatorSearchDefinition<TDocument>

src/MongoDB.Driver/Search/SearchDefinitionBuilder.cs

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,58 +73,34 @@ public SearchDefinition<TDocument> Autocomplete<TField>(
7373
/// <summary>
7474
/// Creates a search definition that queries for documents where an indexed field is equal
7575
/// to the specified value.
76+
/// Supported value types are boolean, numeric, ObjectId and date.
7677
/// </summary>
7778
/// <param name="path">The indexed field to search.</param>
7879
/// <param name="value">The value to query for.</param>
7980
/// <param name="score">The score modifier.</param>
8081
/// <returns>An equality search definition.</returns>
81-
public SearchDefinition<TDocument> Equals(
82-
FieldDefinition<TDocument, bool> path,
83-
bool value,
84-
SearchScoreDefinition<TDocument> score = null) =>
85-
new EqualsSearchDefinition<TDocument>(path, value, score);
86-
87-
/// <summary>
88-
/// Creates a search definition that queries for documents where an indexed field is equal
89-
/// to the specified value.
90-
/// </summary>
91-
/// <param name="path">The indexed field to search.</param>
92-
/// <param name="value">The value to query for.</param>
93-
/// <param name="score">The score modifier.</param>
94-
/// <returns>An equality search definition.</returns>
95-
public SearchDefinition<TDocument> Equals(
96-
FieldDefinition<TDocument, ObjectId> path,
97-
ObjectId value,
98-
SearchScoreDefinition<TDocument> score = null) =>
99-
new EqualsSearchDefinition<TDocument>(path, value, score);
100-
101-
/// <summary>
102-
/// Creates a search definition that queries for documents where an indexed field is equal
103-
/// to the specified value.
104-
/// </summary>
105-
/// <param name="path">The indexed field to search.</param>
106-
/// <param name="value">The value to query for.</param>
107-
/// <param name="score">The score modifier.</param>
108-
/// <returns>An equality search definition.</returns>
109-
public SearchDefinition<TDocument> Equals(
110-
Expression<Func<TDocument, bool>> path,
111-
bool value,
112-
SearchScoreDefinition<TDocument> score = null) =>
113-
Equals(new ExpressionFieldDefinition<TDocument, bool>(path), value, score);
82+
public SearchDefinition<TDocument> Equals<TField>(
83+
FieldDefinition<TDocument, TField> path,
84+
TField value,
85+
SearchScoreDefinition<TDocument> score = null)
86+
where TField : struct, IComparable<TField> =>
87+
new EqualsSearchDefinition<TDocument, TField>(path, value, score);
11488

11589
/// <summary>
11690
/// Creates a search definition that queries for documents where an indexed field is equal
11791
/// to the specified value.
92+
/// Supported value types are boolean, numeric, ObjectId and date.
11893
/// </summary>
11994
/// <param name="path">The indexed field to search.</param>
12095
/// <param name="value">The value to query for.</param>
12196
/// <param name="score">The score modifier.</param>
12297
/// <returns>An equality search definition.</returns>
123-
public SearchDefinition<TDocument> Equals(
124-
Expression<Func<TDocument, ObjectId>> path,
125-
ObjectId value,
126-
SearchScoreDefinition<TDocument> score = null) =>
127-
Equals(new ExpressionFieldDefinition<TDocument, ObjectId>(path), value, score);
98+
public SearchDefinition<TDocument> Equals<TField>(
99+
Expression<Func<TDocument, TField>> path,
100+
TField value,
101+
SearchScoreDefinition<TDocument> score = null)
102+
where TField : struct, IComparable<TField> =>
103+
Equals(new ExpressionFieldDefinition<TDocument, TField>(path), value, score);
128104

129105
/// <summary>
130106
/// Creates a search definition that tests if a path to a specified indexed field name

tests/MongoDB.Driver.Tests/Search/SearchDefinitionBuilderTests.cs

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
using System;
1717
using System.Collections.Generic;
18+
using System.Linq.Expressions;
1819
using FluentAssertions;
1920
using MongoDB.Bson;
2021
using MongoDB.Bson.Serialization;
@@ -176,41 +177,66 @@ public void Compound_typed()
176177
"{ compound: { must: [{ exists: { path: 'age' } }, { exists: { path: 'fn' } }, { exists: { path: 'ln' } }], mustNot: [{ exists: { path: 'ret' } }, { exists: { path: 'dob' } }] } }");
177178
}
178179

179-
[Fact]
180-
public void Equals()
180+
[Theory]
181+
[MemberData(nameof(EqualsSupportedTypesTestData))]
182+
public void Equals_should_render_supported_type<T>(
183+
T value,
184+
string valueRendered,
185+
Expression<Func<Person, T>> fieldExpression,
186+
string fieldRendered)
187+
where T : struct, IComparable<T>
181188
{
182189
var subject = CreateSubject<BsonDocument>();
190+
var subjectTyped = CreateSubject<Person>();
183191

184192
AssertRendered(
185-
subject.Equals("x", true),
186-
"{ equals: { path: 'x', value: true } }");
187-
AssertRendered(
188-
subject.Equals("x", ObjectId.Empty),
189-
"{ equals: { path: 'x', value: { $oid: '000000000000000000000000' } } }");
193+
subject.Equals("x", value),
194+
$"{{ equals: {{ path: 'x', value: {valueRendered} }} }}");
190195

191196
var scoreBuilder = new SearchScoreDefinitionBuilder<BsonDocument>();
192197
AssertRendered(
193-
subject.Equals("x", true, scoreBuilder.Constant(1)),
194-
"{ equals: { path: 'x', value: true, score: { constant: { value: 1 } } } }");
198+
subject.Equals("x", value, scoreBuilder.Constant(1)),
199+
$"{{ equals: {{ path: 'x', value: {valueRendered}, score: {{ constant: {{ value: 1 }} }} }} }}");
200+
201+
AssertRendered(
202+
subjectTyped.Equals(fieldExpression, value),
203+
$"{{ equals: {{ path: '{fieldRendered}', value: {valueRendered} }} }}");
195204
}
196205

197-
[Fact]
198-
public void Equals_typed()
206+
public static object[][] EqualsSupportedTypesTestData => new[]
199207
{
200-
var subject = CreateSubject<Person>();
208+
new object[] { true, "true", Exp(p => p.Retired), "ret" },
209+
new object[] { (sbyte)1, "1", Exp(p => p.Int8), nameof(Person.Int8), },
210+
new object[] { (byte)1, "1", Exp(p => p.UInt8), nameof(Person.UInt8), },
211+
new object[] { (short)1, "1", Exp(p => p.Int16), nameof(Person.Int16) },
212+
new object[] { (ushort)1, "1", Exp(p => p.UInt16), nameof(Person.UInt16) },
213+
new object[] { (int)1, "1", Exp(p => p.Int32), nameof(Person.Int32) },
214+
new object[] { (uint)1, "1", Exp(p => p.UInt32), nameof(Person.UInt32) },
215+
new object[] { long.MaxValue, "NumberLong(\"9223372036854775807\")", Exp(p => p.Int64), nameof(Person.Int64) },
216+
new object[] { (float)1, "1", Exp(p => p.Float), nameof(Person.Float) },
217+
new object[] { (double)1, "1", Exp(p => p.Double), nameof(Person.Double) },
218+
new object[] { DateTime.MinValue, "ISODate(\"0001-01-01T00:00:00Z\")", Exp(p => p.Birthday), "dob" },
219+
new object[] { DateTimeOffset.MaxValue, "ISODate(\"9999-12-31T23:59:59.999Z\")", Exp(p => p.DateTimeOffset), nameof(Person.DateTimeOffset) },
220+
new object[] { ObjectId.Empty, "{ $oid: '000000000000000000000000' }", Exp(p => p.Id), "_id" }
221+
};
201222

202-
AssertRendered(
203-
subject.Equals(x => x.Retired, true),
204-
"{ equals: { path: 'ret', value: true } }");
205-
AssertRendered(
206-
subject.Equals("Retired", true),
207-
"{ equals: { path: 'ret', value: true } }");
223+
[Theory]
224+
[MemberData(nameof(EqualsUnsupporteddTypesTestData))]
225+
public void Equals_should_throw_on_unsupported_type<T>(T value, Expression<Func<Person, T>> fieldExpression) where T : struct, IComparable<T>
226+
{
227+
var subject = CreateSubject<BsonDocument>();
228+
Record.Exception(() => subject.Equals("x", value)).Should().BeOfType<InvalidCastException>();
208229

209-
AssertRendered(
210-
subject.Equals(x => x.Id, ObjectId.Empty),
211-
"{ equals: { path: '_id', value: { $oid: '000000000000000000000000' } } }");
230+
var subjectTyped = CreateSubject<Person>();
231+
Record.Exception(() => subjectTyped.Equals(fieldExpression, value)).Should().BeOfType<InvalidCastException>();
212232
}
213233

234+
public static object[][] EqualsUnsupporteddTypesTestData => new[]
235+
{
236+
new object[] { (ulong)1, Exp(p => p.UInt64) },
237+
new object[] { TimeSpan.Zero, Exp(p => p.TimeSpan) },
238+
};
239+
214240
[Fact]
215241
public void Exists()
216242
{
@@ -928,8 +954,24 @@ private void AssertRendered<TDocument>(SearchDefinition<TDocument> query, BsonDo
928954

929955
private SearchDefinitionBuilder<TDocument> CreateSubject<TDocument>() => new SearchDefinitionBuilder<TDocument>();
930956

931-
private class Person : SimplePerson
957+
private static Expression<Func<Person, T>> Exp<T>(Expression<Func<Person, T>> expression) => expression;
958+
959+
public class Person : SimplePerson
932960
{
961+
public byte UInt8 { get; set; }
962+
public sbyte Int8 { get; set; }
963+
public short Int16 { get; set; }
964+
public ushort UInt16 { get; set; }
965+
public int Int32 { get; set; }
966+
public uint UInt32 { get; set; }
967+
public long Int64 { get; set; }
968+
public ulong UInt64 { get; set; }
969+
public float Float { get; set; }
970+
public double Double { get; set; }
971+
972+
public DateTimeOffset DateTimeOffset { get; set; }
973+
public TimeSpan TimeSpan { get; set; }
974+
933975
[BsonElement("age")]
934976
public int Age { get; set; }
935977

@@ -938,14 +980,15 @@ private class Person : SimplePerson
938980

939981
[BsonId]
940982
public ObjectId Id { get; set; }
983+
941984
[BsonElement("location")]
942985
public GeoJsonPoint<GeoJson2DGeographicCoordinates> Location { get; set; }
943986

944987
[BsonElement("ret")]
945988
public bool Retired { get; set; }
946989
}
947990

948-
private class SimplePerson
991+
public class SimplePerson
949992
{
950993
[BsonElement("fn")]
951994
public string FirstName { get; set; }
@@ -954,7 +997,7 @@ private class SimplePerson
954997
public string LastName { get; set; }
955998
}
956999

957-
private class SimplestPerson
1000+
public class SimplestPerson
9581001
{
9591002
[BsonElement("fn")]
9601003
public string FirstName { get; set; }

0 commit comments

Comments
 (0)