Skip to content

Commit eac42b3

Browse files
authored
Fixed contains for guid/ulid/enum (#279)
1 parent 316be86 commit eac42b3

File tree

3 files changed

+246
-6
lines changed

3 files changed

+246
-6
lines changed

src/Redis.OM/Common/ExpressionParserUtilities.cs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Globalization;
45
using System.Linq;
56
using System.Linq.Expressions;
67
using System.Reflection;
78
using System.Text;
9+
using System.Text.Json.Serialization;
810
using System.Text.RegularExpressions;
911
using Redis.OM.Aggregation;
1012
using Redis.OM.Aggregation.AggregationPredicates;
@@ -70,16 +72,17 @@ internal static string GetOperandString(MethodCallExpression exp)
7072
/// Gets the operand string from a search.
7173
/// </summary>
7274
/// <param name="exp">expression.</param>
75+
/// <param name="treatEnumsAsInt">Treat enum as an integer.</param>
7376
/// <returns>the operand string.</returns>
7477
/// <exception cref="ArgumentException">thrown if expression is un-parseable.</exception>
75-
internal static string GetOperandStringForQueryArgs(Expression exp)
78+
internal static string GetOperandStringForQueryArgs(Expression exp, bool treatEnumsAsInt = false)
7679
{
7780
return exp switch
7881
{
7982
ConstantExpression constExp => $"{constExp.Value}",
80-
MemberExpression member => GetOperandStringForMember(member),
83+
MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt),
8184
MethodCallExpression method => TranslateMethodStandardQuerySyntax(method),
82-
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand),
85+
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, treatEnumsAsInt),
8386
_ => throw new ArgumentException("Unrecognized Expression type")
8487
};
8588
}
@@ -266,7 +269,7 @@ internal static string EscapeTagField(string text)
266269
return sb.ToString();
267270
}
268271

269-
private static string GetOperandStringForMember(MemberExpression member)
272+
private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false)
270273
{
271274
var memberPath = new List<string>();
272275
var parentExpression = member.Expression;
@@ -291,17 +294,30 @@ private static string GetOperandStringForMember(MemberExpression member)
291294
if (dependencyChain.Last().Expression is ConstantExpression c)
292295
{
293296
var resolved = c.Value;
297+
294298
for (var i = dependencyChain.Count; i > 0; i--)
295299
{
296300
var expr = dependencyChain[i - 1];
297301
resolved = GetValue(expr.Member, resolved);
298302
}
299303

304+
var resolvedType = resolved.GetType();
305+
300306
if (resolved is IEnumerable<string> strings)
301307
{
302308
return string.Join("|", strings);
303309
}
304310

311+
if (resolved is IEnumerable<Guid> guids)
312+
{
313+
return string.Join("|", guids);
314+
}
315+
316+
if (resolved is IEnumerable<Ulid> ulids)
317+
{
318+
return string.Join("|", ulids);
319+
}
320+
305321
if (resolved is IEnumerable<int?> ints)
306322
{
307323
var sb = new StringBuilder();
@@ -315,6 +331,43 @@ private static string GetOperandStringForMember(MemberExpression member)
315331
return sb.ToString();
316332
}
317333

334+
if (resolvedType.IsArray || resolvedType.GetInterfaces().Contains(typeof(IEnumerable)))
335+
{
336+
var asEnumerable = (IEnumerable)resolved;
337+
var elementType = resolvedType.GetElementType();
338+
if (elementType == null)
339+
{
340+
elementType = resolvedType.GenericTypeArguments.FirstOrDefault();
341+
}
342+
343+
if (elementType != null && elementType.IsEnum)
344+
{
345+
if (treatEnumsAsInt)
346+
{
347+
var sb = new StringBuilder();
348+
sb.Append('|');
349+
foreach (var item in asEnumerable)
350+
{
351+
var asInt = (int)item;
352+
sb.Append($"[{asInt} {asInt}]|");
353+
}
354+
355+
sb.Remove(sb.Length - 1, 1);
356+
return sb.ToString();
357+
}
358+
else
359+
{
360+
var strs = new List<string>();
361+
foreach (var item in asEnumerable)
362+
{
363+
strs.Add(item.ToString());
364+
}
365+
366+
return string.Join("|", strs);
367+
}
368+
}
369+
}
370+
318371
return ValueToString(resolved);
319372
}
320373

@@ -621,9 +674,10 @@ private static string TranslateContainsStandardQuerySyntax(MethodCallExpression
621674

622675
type = Nullable.GetUnderlyingType(propertyExpression.Type) ?? propertyExpression.Type;
623676
memberName = GetOperandStringForMember(propertyExpression);
624-
literal = GetOperandStringForQueryArgs(valuesExpression);
677+
var treatEnumsAsInts = type.IsEnum && !(propertyExpression.Member.GetCustomAttributes(typeof(JsonConverterAttribute)).FirstOrDefault() is JsonConverterAttribute converter && converter.ConverterType == typeof(JsonStringEnumConverter));
678+
literal = GetOperandStringForQueryArgs(valuesExpression, treatEnumsAsInts);
625679

626-
if ((type == typeof(string) || type == typeof(string[]) || type == typeof(List<string>)) && attribute is IndexedAttribute)
680+
if ((type == typeof(string) || type == typeof(string[]) || type == typeof(List<string>) || type == typeof(Guid) || type == typeof(Ulid) || (type.IsEnum && !treatEnumsAsInts)) && attribute is IndexedAttribute)
627681
{
628682
return $"({memberName}:{{{EscapeTagField(literal).Replace("\\|", "|")}}})";
629683
}

test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,22 @@ public async Task TestMultipleContains()
837837
Assert.Contains(people, x =>x.Id == person2.Id);
838838
}
839839

840+
[Fact]
841+
public async Task TestMultipleContainsGuid()
842+
{
843+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_connection);
844+
var objectList = Enumerable.Range(1, 10).Select(x => new ObjectWithStringLikeValueTypes() { Guid = Guid.NewGuid() }).ToList();
845+
foreach (var item in objectList)
846+
{
847+
await collection.InsertAsync(item);
848+
}
849+
850+
var ids = objectList.Select(x => x.Guid);
851+
var objects = await collection.Where(x => ids.Contains(x.Guid)).ToListAsync();
852+
853+
Assert.Equal(ids, objects.Select(x => x.Guid));
854+
}
855+
840856
[Fact]
841857
public async Task TestShouldFailForSave()
842858
{

test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,5 +2609,175 @@ public void SearchWithEmptyAny()
26092609

26102610
Assert.True(any);
26112611
}
2612+
2613+
[Fact]
2614+
public void SearchGuidFieldContains()
2615+
{
2616+
var guid1 = Guid.NewGuid();
2617+
var guid2 = Guid.NewGuid();
2618+
var guid3 = Guid.NewGuid();
2619+
var guid1Str = ExpressionParserUtilities.EscapeTagField(guid1.ToString());
2620+
var guid2Str = ExpressionParserUtilities.EscapeTagField(guid2.ToString());
2621+
var guid3Str = ExpressionParserUtilities.EscapeTagField(guid3.ToString());
2622+
var potentialFieldValues = new Guid[] { guid1, guid2, guid3 };
2623+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2624+
.Returns(_mockReply);
2625+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.Contains(x.Guid));
2626+
collection.ToList();
2627+
_mock.Verify(x => x.Execute(
2628+
"FT.SEARCH",
2629+
"objectwithstringlikevaluetypes-idx",
2630+
$"(@Guid:{{{guid1Str}|{guid2Str}|{guid3Str}}})",
2631+
"LIMIT",
2632+
"0",
2633+
"100"));
2634+
}
2635+
2636+
[Fact]
2637+
public void SearchUlidFieldContains()
2638+
{
2639+
var ulid1 = Ulid.NewUlid();
2640+
var ulid2 = Ulid.NewUlid();
2641+
var ulid3 = Ulid.NewUlid();
2642+
2643+
var potentialFieldValues = new Ulid[] { ulid1, ulid2, ulid3 };
2644+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2645+
.Returns(_mockReply);
2646+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.Contains(x.Ulid));
2647+
collection.ToList();
2648+
_mock.Verify(x => x.Execute(
2649+
"FT.SEARCH",
2650+
"objectwithstringlikevaluetypes-idx",
2651+
$"(@Ulid:{{{ulid1}|{ulid2}|{ulid3}}})",
2652+
"LIMIT",
2653+
"0",
2654+
"100"));
2655+
}
2656+
2657+
[Fact]
2658+
public void SearchEnumFieldContains()
2659+
{
2660+
var enum1 = AnEnum.one;
2661+
var enum2 = AnEnum.two;
2662+
var enum3 = AnEnum.three;
2663+
2664+
var potentialFieldValues = new AnEnum[] { enum1, enum2, enum3 };
2665+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2666+
.Returns(_mockReply);
2667+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.Contains(x.AnEnum));
2668+
collection.ToList();
2669+
_mock.Verify(x => x.Execute(
2670+
"FT.SEARCH",
2671+
"objectwithstringlikevaluetypes-idx",
2672+
$"(@AnEnum:{{one|two|three}})",
2673+
"LIMIT",
2674+
"0",
2675+
"100"));
2676+
}
2677+
2678+
[Fact]
2679+
public void SearchNumericEnumFieldContains()
2680+
{
2681+
var enum1 = AnEnum.one;
2682+
var enum2 = AnEnum.two;
2683+
var enum3 = AnEnum.three;
2684+
2685+
var potentialFieldValues = new AnEnum[] { enum1, enum2, enum3 };
2686+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2687+
.Returns(_mockReply);
2688+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.Contains(x.AnEnumAsInt));
2689+
collection.ToList();
2690+
_mock.Verify(x => x.Execute(
2691+
"FT.SEARCH",
2692+
"objectwithstringlikevaluetypes-idx",
2693+
"@AnEnumAsInt:[0 0]|@AnEnumAsInt:[1 1]|@AnEnumAsInt:[2 2]",
2694+
"LIMIT",
2695+
"0",
2696+
"100"));
2697+
}
2698+
2699+
[Fact]
2700+
public void SearchEnumFieldContainsList()
2701+
{
2702+
var enum1 = AnEnum.one;
2703+
var enum2 = AnEnum.two;
2704+
var enum3 = AnEnum.three;
2705+
2706+
var potentialFieldValues = new List<AnEnum> { enum1, enum2, enum3 };
2707+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2708+
.Returns(_mockReply);
2709+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.Contains(x.AnEnum));
2710+
collection.ToList();
2711+
_mock.Verify(x => x.Execute(
2712+
"FT.SEARCH",
2713+
"objectwithstringlikevaluetypes-idx",
2714+
$"(@AnEnum:{{one|two|three}})",
2715+
"LIMIT",
2716+
"0",
2717+
"100"));
2718+
}
2719+
2720+
[Fact]
2721+
public void SearchNumericEnumFieldContainsList()
2722+
{
2723+
var enum1 = AnEnum.one;
2724+
var enum2 = AnEnum.two;
2725+
var enum3 = AnEnum.three;
2726+
2727+
var potentialFieldValues = new List<AnEnum> { enum1, enum2, enum3 };
2728+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2729+
.Returns(_mockReply);
2730+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.Contains(x.AnEnumAsInt));
2731+
collection.ToList();
2732+
_mock.Verify(x => x.Execute(
2733+
"FT.SEARCH",
2734+
"objectwithstringlikevaluetypes-idx",
2735+
"@AnEnumAsInt:[0 0]|@AnEnumAsInt:[1 1]|@AnEnumAsInt:[2 2]",
2736+
"LIMIT",
2737+
"0",
2738+
"100"));
2739+
}
2740+
2741+
[Fact]
2742+
public void SearchEnumFieldContainsListAsProperty()
2743+
{
2744+
var enum1 = AnEnum.one;
2745+
var enum2 = AnEnum.two;
2746+
var enum3 = AnEnum.three;
2747+
2748+
var potentialFieldValues = new {list = new List<AnEnum> { enum1, enum2, enum3 }};
2749+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2750+
.Returns(_mockReply);
2751+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.list.Contains(x.AnEnum));
2752+
collection.ToList();
2753+
_mock.Verify(x => x.Execute(
2754+
"FT.SEARCH",
2755+
"objectwithstringlikevaluetypes-idx",
2756+
$"(@AnEnum:{{one|two|three}})",
2757+
"LIMIT",
2758+
"0",
2759+
"100"));
2760+
}
2761+
2762+
[Fact]
2763+
public void SearchNumericEnumFieldContainsListAsProperty()
2764+
{
2765+
var enum1 = AnEnum.one;
2766+
var enum2 = AnEnum.two;
2767+
var enum3 = AnEnum.three;
2768+
2769+
var potentialFieldValues = new {list = new List<AnEnum> { enum1, enum2, enum3 }};
2770+
_mock.Setup(x => x.Execute(It.IsAny<string>(), It.IsAny<string[]>()))
2771+
.Returns(_mockReply);
2772+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_mock.Object).Where(x => potentialFieldValues.list.Contains(x.AnEnumAsInt));
2773+
collection.ToList();
2774+
_mock.Verify(x => x.Execute(
2775+
"FT.SEARCH",
2776+
"objectwithstringlikevaluetypes-idx",
2777+
"@AnEnumAsInt:[0 0]|@AnEnumAsInt:[1 1]|@AnEnumAsInt:[2 2]",
2778+
"LIMIT",
2779+
"0",
2780+
"100"));
2781+
}
26122782
}
26132783
}

0 commit comments

Comments
 (0)