Skip to content

Commit f65de36

Browse files
authored
Feature/stringlike value indexing (#138)
* String like value-types * removing extra param * fixing tests * addressing some PR comments
1 parent d5efa84 commit f65de36

File tree

9 files changed

+381
-52
lines changed

9 files changed

+381
-52
lines changed

src/Redis.OM/Common/ExpressionTranslator.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq.Expressions;
55
using System.Reflection;
66
using System.Text;
7+
using System.Text.Json.Serialization;
78
using Redis.OM.Aggregation;
89
using Redis.OM.Aggregation.AggregationPredicates;
910
using Redis.OM.Modeling;
@@ -260,9 +261,17 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
260261
/// <returns>The index field type.</returns>
261262
internal static SearchFieldType DetermineIndexFieldsType(MemberInfo member)
262263
{
263-
if (member is PropertyInfo info && TypeDeterminationUtilities.IsNumeric(info.PropertyType))
264+
if (member is PropertyInfo info)
264265
{
265-
return SearchFieldType.NUMERIC;
266+
if (TypeDeterminationUtilities.IsNumeric(info.PropertyType))
267+
{
268+
return SearchFieldType.NUMERIC;
269+
}
270+
271+
if (info.PropertyType.IsEnum)
272+
{
273+
return TypeDeterminationUtilities.GetSearchFieldFromEnumProperty(info);
274+
}
266275
}
267276

268277
return SearchFieldType.TAG;
@@ -518,6 +527,12 @@ private static string BuildQueryFromExpression(Expression exp)
518527
return operandString;
519528
}
520529

530+
if (exp is MemberExpression member && member.Type == typeof(bool))
531+
{
532+
var property = ExpressionParserUtilities.GetOperandString(exp);
533+
return $"{property}:{{true}}";
534+
}
535+
521536
throw new ArgumentException("Unparseable Lambda Body detected");
522537
}
523538

@@ -561,6 +576,30 @@ private static string TranslateBinaryExpression(BinaryExpression binExpression)
561576
sb.Append(predicate);
562577
sb.Append(")");
563578
}
579+
else if (binExpression.Left is UnaryExpression uni)
580+
{
581+
member = (MemberExpression)uni.Operand;
582+
var attr = member.Member.GetCustomAttributes(typeof(JsonConverterAttribute)).FirstOrDefault() as JsonConverterAttribute;
583+
if (attr != null && attr.ConverterType == typeof(JsonStringEnumConverter))
584+
{
585+
if (int.TryParse(rightContent, out int ordinal))
586+
{
587+
rightContent = Enum.ToObject(member.Type, ordinal).ToString();
588+
}
589+
}
590+
else
591+
{
592+
if (!int.TryParse(rightContent, out _))
593+
{
594+
rightContent = ((int)Enum.Parse(member.Type, rightContent)).ToString();
595+
}
596+
}
597+
598+
var predicate = BuildQueryPredicate(binExpression.NodeType, leftContent, rightContent, member);
599+
sb.Append("(");
600+
sb.Append(predicate);
601+
sb.Append(")");
602+
}
564603
else
565604
{
566605
throw new ArgumentException("Left side of expression must be a member of the search class");

src/Redis.OM/Modeling/RedisSchemaField.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Globalization;
44
using System.Linq;
55
using System.Reflection;
6+
using System.Text.Json.Serialization;
67

78
namespace Redis.OM.Modeling
89
{
@@ -45,6 +46,10 @@ internal static string[] SerializeArgsJson(this PropertyInfo info, int remaining
4546
if (!TypeDeterminationUtilities.IsNumeric(innerType)
4647
&& innerType != typeof(string)
4748
&& innerType != typeof(GeoLoc)
49+
&& innerType != typeof(Ulid)
50+
&& innerType != typeof(bool)
51+
&& innerType != typeof(Guid)
52+
&& !innerType.IsEnum
4853
&& !IsTypeIndexableArray(innerType))
4954
{
5055
int cascadeDepth = remainingDepth == -1 ? attr.CascadeDepth : remainingDepth;
@@ -62,7 +67,7 @@ internal static string[] SerializeArgsJson(this PropertyInfo info, int remaining
6267
ret.Add(!string.IsNullOrEmpty(attr.PropertyName) ? $"{pathPrefix}{attr.PropertyName}{pathPrefix}" : $"{pathPrefix}{info.Name}{pathPostFix}");
6368
ret.Add("AS");
6469
ret.Add(!string.IsNullOrEmpty(attr.PropertyName) ? $"{aliasPrefix}{attr.PropertyName}" : $"{aliasPrefix}{info.Name}");
65-
ret.AddRange(CommonSerialization(attr, innerType));
70+
ret.AddRange(CommonSerialization(attr, innerType, info));
6671
}
6772
}
6873
}
@@ -85,7 +90,7 @@ internal static string[] SerializeArgs(this PropertyInfo info)
8590

8691
var ret = new List<string> { !string.IsNullOrEmpty(attr.PropertyName) ? attr.PropertyName : info.Name };
8792
var innerType = Nullable.GetUnderlyingType(info.PropertyType);
88-
ret.AddRange(CommonSerialization(attr, innerType ?? info.PropertyType));
93+
ret.AddRange(CommonSerialization(attr, innerType ?? info.PropertyType, info));
8994
return ret.ToArray();
9095
}
9196

@@ -97,6 +102,7 @@ private static IEnumerable<string> SerializeIndexFromJsonPaths(PropertyInfo pare
97102
var path = attribute.JsonPath;
98103
var propertyNames = path!.Split('.').Skip(1).ToArray();
99104
var type = parentInfo.PropertyType;
105+
PropertyInfo propertyInfo = parentInfo;
100106
foreach (var name in propertyNames)
101107
{
102108
var childProperty = type.GetProperty(name);
@@ -105,19 +111,21 @@ private static IEnumerable<string> SerializeIndexFromJsonPaths(PropertyInfo pare
105111
throw new RedisIndexingException($"{path} not found in {parentInfo.Name} object graph.");
106112
}
107113

114+
propertyInfo = childProperty;
108115
type = childProperty.PropertyType;
109116
}
110117

111118
indexArgs.Add($"{prefix}{parentInfo.Name}{path.Substring(1)}");
112119
indexArgs.Add("AS");
113120
indexArgs.Add($"{parentInfo.Name}_{string.Join("_", propertyNames)}");
114121
var underlyingType = Nullable.GetUnderlyingType(type);
115-
indexArgs.AddRange(CommonSerialization(attribute, underlyingType ?? type));
122+
indexArgs.AddRange(CommonSerialization(attribute, underlyingType ?? type, propertyInfo));
116123
return indexArgs;
117124
}
118125

119-
private static string GetSearchFieldType(SearchFieldType typeEnum, Type declaredType)
126+
private static string GetSearchFieldType(Type declaredType, SearchFieldAttribute attr, PropertyInfo propertyInfo)
120127
{
128+
var typeEnum = attr.SearchFieldType;
121129
if (typeEnum != SearchFieldType.INDEXED)
122130
{
123131
return typeEnum.ToString();
@@ -128,12 +136,17 @@ private static string GetSearchFieldType(SearchFieldType typeEnum, Type declared
128136
return "GEO";
129137
}
130138

139+
if (declaredType.IsEnum)
140+
{
141+
return propertyInfo.GetCustomAttributes(typeof(JsonConverterAttribute)).FirstOrDefault() is JsonConverterAttribute converter && converter.ConverterType == typeof(JsonStringEnumConverter) ? "TAG" : "NUMERIC";
142+
}
143+
131144
return TypeDeterminationUtilities.IsNumeric(declaredType) ? "NUMERIC" : "TAG";
132145
}
133146

134-
private static string[] CommonSerialization(SearchFieldAttribute attr, Type declaredType)
147+
private static string[] CommonSerialization(SearchFieldAttribute attr, Type declaredType, PropertyInfo propertyInfo)
135148
{
136-
var searchFieldType = GetSearchFieldType(attr.SearchFieldType, declaredType);
149+
var searchFieldType = GetSearchFieldType(declaredType, attr, propertyInfo);
137150
var ret = new List<string> { searchFieldType };
138151
if (attr is SearchableAttribute text)
139152
{
@@ -157,7 +170,7 @@ private static string[] CommonSerialization(SearchFieldAttribute attr, Type decl
157170

158171
if (searchFieldType == "TAG" && attr is IndexedAttribute tag)
159172
{
160-
if (tag.Separator != ',')
173+
if (tag.Separator != ',' && !declaredType.IsEnum)
161174
{
162175
ret.Add("SEPARATOR");
163176
ret.Add(tag.Separator.ToString());

src/Redis.OM/Modeling/TypeDeterminationUtilities.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using System.Text.Json.Serialization;
36

47
namespace Redis.OM.Modeling
58
{
@@ -62,7 +65,16 @@ internal static SearchFieldType GetSearchFieldType(Type type)
6265
return SearchFieldType.TAG;
6366
}
6467

65-
throw new ArgumentException("Unrecognized Index type, can only index numerics, GeoLoc, or String");
68+
throw new ArgumentException("Unrecognized Index type, can only index numerics, GeoLoc, strings, ULIDs, GUIDs, Enums, and Booleans");
6669
}
70+
71+
/// <summary>
72+
/// Determines SearchFieldType of provided property info.
73+
/// </summary>
74+
/// <param name="info">The PropertyInfo to check.</param>
75+
/// <returns>The Search field type.</returns>
76+
internal static SearchFieldType GetSearchFieldFromEnumProperty(PropertyInfo info) =>
77+
info.GetCustomAttributes<JsonConverterAttribute>().FirstOrDefault() is JsonConverterAttribute converter
78+
&& converter.ConverterType == typeof(JsonStringEnumConverter) ? SearchFieldType.TAG : SearchFieldType.NUMERIC;
6779
}
6880
}

src/Redis.OM/RedisObjectHandler.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ internal static IDictionary<string, string> BuildHashSet(this object obj)
312312
hash.Add(propertyName, val.ToString());
313313
}
314314
}
315+
else if (type.IsEnum)
316+
{
317+
var val = property.GetValue(obj);
318+
hash.Add(propertyName, ((int)val).ToString());
319+
}
315320
else if (type == typeof(DateTimeOffset))
316321
{
317322
var val = (DateTimeOffset)property.GetValue(obj);
@@ -390,7 +395,7 @@ private static string SendToJson(IDictionary<string, string> hash, Type t)
390395

391396
ret += $"\"{propertyName}\":{hash[propertyName].ToLower()},";
392397
}
393-
else if (type.IsPrimitive || type == typeof(decimal))
398+
else if (type.IsPrimitive || type == typeof(decimal) || type.IsEnum)
394399
{
395400
if (!hash.ContainsKey(propertyName))
396401
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Redis.OM.Unit.Tests.RediSearchTests
2+
{
3+
public enum AnEnum
4+
{
5+
one=0,
6+
two=1,
7+
three=2
8+
}
9+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Text.Json.Serialization;
3+
using Redis.OM.Modeling;
4+
5+
namespace Redis.OM.Unit.Tests.RediSearchTests
6+
{
7+
[Document(StorageType = StorageType.Json)]
8+
public class ObjectWithStringLikeValueTypes
9+
{
10+
[Indexed]
11+
public Ulid Ulid { get; set; }
12+
13+
[Indexed]
14+
public bool Boolean { get; set; }
15+
16+
[Indexed]
17+
public Guid Guid { get; set; }
18+
19+
[Indexed]
20+
[JsonConverter(typeof(JsonStringEnumConverter))]
21+
public AnEnum AnEnum { get; set; }
22+
23+
[Indexed]
24+
public AnEnum AnEnumAsInt { get; set; }
25+
}
26+
27+
[Document]
28+
public class ObjectWithStringLikeValueTypesHash
29+
{
30+
[Indexed]
31+
public Ulid Ulid { get; set; }
32+
33+
[Indexed]
34+
public bool Boolean { get; set; }
35+
36+
[Indexed]
37+
public Guid Guid { get; set; }
38+
39+
[Indexed]
40+
public AnEnum AnEnum { get; set; }
41+
}
42+
}

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

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Redis.OM.Contracts;
88
using Redis.OM.Modeling;
99
using Redis.OM.Searching;
10+
using Redis.OM.Searching.Query;
1011
using Xunit;
1112

1213
namespace Redis.OM.Unit.Tests.RediSearchTests
@@ -504,5 +505,93 @@ public async Task FindByKeyAsync()
504505
Assert.NotNull(alsoBob);
505506
Assert.Equal("Bob",person.Name);
506507
}
508+
509+
[Fact]
510+
public async Task SearchByUlid()
511+
{
512+
var ulid = Ulid.NewUlid();
513+
var obj = new ObjectWithStringLikeValueTypes
514+
{
515+
Ulid = ulid
516+
};
517+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_connection);
518+
var key = await collection.InsertAsync(obj);
519+
var alsoObj = await collection.FirstOrDefaultAsync(x => x.Ulid == ulid);
520+
Assert.NotNull(alsoObj);
521+
}
522+
523+
[Fact]
524+
public async Task SearchByBoolean()
525+
{
526+
var obj = new ObjectWithStringLikeValueTypes
527+
{
528+
Boolean = true
529+
};
530+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_connection);
531+
var key = await collection.InsertAsync(obj);
532+
var alsoObj = await collection.FirstOrDefaultAsync(x => x.Boolean == true);
533+
Assert.NotNull(alsoObj);
534+
alsoObj = await collection.FirstOrDefaultAsync(x => x.Boolean);
535+
Assert.NotNull(alsoObj);
536+
}
537+
538+
[Fact]
539+
public async Task SearchByBooleanFalse()
540+
{
541+
var obj = new ObjectWithStringLikeValueTypes
542+
{
543+
Boolean = false
544+
};
545+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_connection);
546+
var key = await collection.InsertAsync(obj);
547+
var alsoObj = await collection.FirstOrDefaultAsync(x => x.Boolean == false);
548+
Assert.NotNull(alsoObj);
549+
alsoObj = await collection.FirstOrDefaultAsync(x => !x.Boolean);
550+
Assert.NotNull(alsoObj);
551+
}
552+
553+
[Fact]
554+
public async Task TestSearchByStringEnum()
555+
{
556+
var obj = new ObjectWithStringLikeValueTypes() {AnEnum = AnEnum.two, AnEnumAsInt = AnEnum.three};
557+
await _connection.SetAsync(obj);
558+
var anEnum = AnEnum.two;
559+
var three = AnEnum.three;
560+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_connection);
561+
var result = await collection.Where(x => x.AnEnum == AnEnum.two).ToListAsync();
562+
Assert.NotEmpty(result);
563+
result = await collection.Where(x => x.AnEnum == anEnum).ToListAsync();
564+
Assert.NotEmpty(result);
565+
result = await collection.Where(x => x.AnEnum == obj.AnEnum).ToListAsync();
566+
Assert.NotEmpty(result);
567+
result = await collection.Where(x => (int)x.AnEnumAsInt > 1).ToListAsync();
568+
Assert.NotEmpty(result);
569+
result = await collection.Where(x => x.AnEnumAsInt > AnEnum.two).ToListAsync();
570+
Assert.NotEmpty(result);
571+
result = await collection.Where(x => x.AnEnumAsInt == AnEnum.three).ToListAsync();
572+
Assert.NotEmpty(result);
573+
result = await collection.Where(x => x.AnEnumAsInt == three).ToListAsync();
574+
Assert.NotEmpty(result);
575+
result = await collection.Where(x => x.AnEnumAsInt == obj.AnEnumAsInt).ToListAsync();
576+
Assert.NotEmpty(result);
577+
}
578+
579+
[Fact]
580+
public async Task TestSearchByStringEnumHash()
581+
{
582+
var obj = new ObjectWithStringLikeValueTypesHash() {AnEnum = AnEnum.two};
583+
await _connection.SetAsync(obj);
584+
var anEnum = AnEnum.two;
585+
var collection = new RedisCollection<ObjectWithStringLikeValueTypesHash>(_connection);
586+
var result = await collection.Where(x => x.AnEnum == AnEnum.two).ToListAsync();
587+
Assert.NotEmpty(result);
588+
Assert.Equal(AnEnum.two, result.First().AnEnum);
589+
result = await collection.Where(x => x.AnEnum == anEnum).ToListAsync();
590+
Assert.NotEmpty(result);
591+
Assert.Equal(AnEnum.two, result.First().AnEnum);
592+
result = await collection.Where(x => x.AnEnum == obj.AnEnum).ToListAsync();
593+
Assert.NotEmpty(result);
594+
Assert.Equal(AnEnum.two, result.First().AnEnum);
595+
}
507596
}
508597
}

0 commit comments

Comments
 (0)