diff --git a/src/Redis.OM/Modeling/RedisSchemaField.cs b/src/Redis.OM/Modeling/RedisSchemaField.cs index ee7ab785..b4e2214d 100644 --- a/src/Redis.OM/Modeling/RedisSchemaField.cs +++ b/src/Redis.OM/Modeling/RedisSchemaField.cs @@ -12,6 +12,18 @@ namespace Redis.OM.Modeling /// internal static class RedisSchemaField { + internal static bool IsComplexType(Type type) + { + return !TypeDeterminationUtilities.IsNumeric(type) + && type != typeof(string) + && type != typeof(GeoLoc) + && type != typeof(Ulid) + && type != typeof(bool) + && type != typeof(Guid) + && !type.IsEnum + && !IsTypeIndexableArray(type); + } + /// /// gets the schema field args serialized for json. /// @@ -35,24 +47,17 @@ internal static string[] SerializeArgsJson(this PropertyInfo info, int remaining var ret = new List(); foreach (var attr in attributes) { + int cascadeDepth = remainingDepth == -1 ? attr.CascadeDepth : remainingDepth; if (attr.JsonPath != null) { - ret.AddRange(SerializeIndexFromJsonPaths(info, attr)); + ret.AddRange(SerializeIndexFromJsonPaths(info, attr, pathPrefix, aliasPrefix, cascadeDepth)); } else { var innerType = Nullable.GetUnderlyingType(info.PropertyType) ?? info.PropertyType; - if (!TypeDeterminationUtilities.IsNumeric(innerType) - && innerType != typeof(string) - && innerType != typeof(GeoLoc) - && innerType != typeof(Ulid) - && innerType != typeof(bool) - && innerType != typeof(Guid) - && !innerType.IsEnum - && !IsTypeIndexableArray(innerType)) + if (IsComplexType(innerType)) { - int cascadeDepth = remainingDepth == -1 ? attr.CascadeDepth : remainingDepth; if (cascadeDepth > 0) { foreach (var property in info.PropertyType.GetProperties()) @@ -96,7 +101,7 @@ internal static string[] SerializeArgs(this PropertyInfo info) private static bool IsTypeIndexableArray(Type t) => t == typeof(string[]) || t == typeof(bool[]) || t == typeof(List) || t == typeof(List); - private static IEnumerable SerializeIndexFromJsonPaths(PropertyInfo parentInfo, SearchFieldAttribute attribute, string prefix = "$.") + private static IEnumerable SerializeIndexFromJsonPaths(PropertyInfo parentInfo, SearchFieldAttribute attribute, string prefix = "$.", string aliasPrefix = "", int remainingDepth = -1) { var isCollection = false; var indexArgs = new List(); @@ -128,12 +133,29 @@ private static IEnumerable SerializeIndexFromJsonPaths(PropertyInfo pare type = childProperty.PropertyType; } - var arrayStr = isCollection ? "[*]" : string.Empty; - indexArgs.Add($"{prefix}{parentInfo.Name}{arrayStr}{path.Substring(1)}"); - indexArgs.Add("AS"); - indexArgs.Add($"{parentInfo.Name}_{string.Join("_", propertyNames)}"); - var underlyingType = Nullable.GetUnderlyingType(type); - indexArgs.AddRange(CommonSerialization(attribute, underlyingType ?? type, propertyInfo)); + var underlyingType = Nullable.GetUnderlyingType(type) ?? type; + if (!IsComplexType(underlyingType)) + { + var arrayStr = isCollection ? "[*]" : string.Empty; + indexArgs.Add($"{prefix}{parentInfo.Name}{arrayStr}{path.Substring(1)}"); + indexArgs.Add("AS"); + indexArgs.Add($"{aliasPrefix}{parentInfo.Name}_{string.Join("_", propertyNames)}"); + indexArgs.AddRange(CommonSerialization(attribute, underlyingType, propertyInfo)); + } + else + { + int cascadeDepth = remainingDepth == -1 ? attribute.CascadeDepth : remainingDepth; + if (cascadeDepth > 0) + { + foreach (var property in propertyInfo.PropertyType.GetProperties()) + { + var pathPrefix = $"{prefix}{parentInfo.Name}{path.Substring(1)}."; + var alias = $"{aliasPrefix}{parentInfo.Name}_{string.Join("_", propertyNames)}_"; + indexArgs.AddRange(property.SerializeArgsJson(cascadeDepth - 1, pathPrefix, alias)); + } + } + } + return indexArgs; } diff --git a/src/Redis.OM/Searching/RedisCollection.cs b/src/Redis.OM/Searching/RedisCollection.cs index 6b849c98..e6e4a90b 100644 --- a/src/Redis.OM/Searching/RedisCollection.cs +++ b/src/Redis.OM/Searching/RedisCollection.cs @@ -476,7 +476,11 @@ public T First(Expression> expression) query.Limit = new SearchLimit { Number = 1, Offset = 0 }; var res = _connection.Search(query); var result = res.Documents.FirstOrDefault(); - SaveToStateManager(result.Key, result.Value); + if (result.Key != null) + { + SaveToStateManager(result.Key, result.Value); + } + return result.Value; } diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs index 551ede8f..5134f165 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchFunctionalTests.cs @@ -1008,6 +1008,36 @@ public void TestIntSelects() collection.Delete(obj); } + [Fact] + public void TestComplexObjectsWithMixedNesting() + { + var obj = new ComplexObjectWithCascadeAndJsonPath + { + InnerCascade = new InnerObject() + { + InnerInnerCascade = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 }, + InnerInnerCollection = new[] { new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 } }, + InnerInnerJson = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 } + }, + InnerJson = new InnerObject() + { + InnerInnerCascade = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 }, + InnerInnerCollection = new[] { new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 } }, + InnerInnerJson = new InnerInnerObject() { Arr = new[] { "hello" }, Tag = "World", Num = 42 } + } + }; + + var collection = new RedisCollection(_connection); + + collection.Insert(obj); + + Assert.NotNull(collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCascade.Tag == "World")); + Assert.NotNull(collection.FirstOrDefault(x=> x.InnerCascade.InnerInnerCascade.Num == 42)); + Assert.NotNull(collection.FirstOrDefault(x=> x.InnerCascade.InnerInnerCollection.Any(x=>x.Tag == "World"))); + Assert.NotNull(collection.FirstOrDefault(x=>x.InnerJson.InnerInnerCascade.Tag == "World")); + Assert.NotNull(collection.FirstOrDefault(x=>x.InnerJson.InnerInnerCascade.Arr.Contains("hello"))); + } + [Fact] public void TestUpdateWithQuotes() { diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs index 55d63a4b..36767f40 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs @@ -2888,5 +2888,71 @@ public void TestNullableEnumQueries() "100" )); } + + [Fact] + public void TestMixedNestingIndexCreation() + { + _mock.Setup(x => x.Execute(It.IsAny(), It.IsAny())) + .Returns("OK"); + + _mock.Object.CreateIndex(typeof(ComplexObjectWithCascadeAndJsonPath)); + + _mock.Verify(x => x.Execute( + "FT.CREATE", + $"{nameof(ComplexObjectWithCascadeAndJsonPath).ToLower()}-idx", + "ON", + "Json", + "PREFIX", + "1", + $"Redis.OM.Unit.Tests.{nameof(ComplexObjectWithCascadeAndJsonPath)}:", + "SCHEMA", "$.InnerCascade.InnerInnerJson.Tag", "AS", "InnerCascade_InnerInnerJson_Tag", "TAG", "SEPARATOR", "|", + "$.InnerCascade.InnerInnerCascade.Tag", "AS", "InnerCascade_InnerInnerCascade_Tag", "TAG", "SEPARATOR", "|", + "$.InnerCascade.InnerInnerCascade.Num", "AS", "InnerCascade_InnerInnerCascade_Num", "NUMERIC", + "$.InnerCascade.InnerInnerCascade.Arr[*]", "AS", "InnerCascade_InnerInnerCascade_Arr", "TAG", "SEPARATOR", "|", + "$.InnerCascade.InnerInnerCollection[*].Tag", "AS", "InnerCascade_InnerInnerCollection_Tag", "TAG", "SEPARATOR", "|", + "$.InnerJson.InnerInnerCascade.Tag", "AS", "InnerJson_InnerInnerCascade_Tag", "TAG", "SEPARATOR", "|", + "$.InnerJson.InnerInnerCascade.Num", "AS", "InnerJson_InnerInnerCascade_Num", "NUMERIC", + "$.InnerJson.InnerInnerCascade.Arr[*]", "AS", "InnerJson_InnerInnerCascade_Arr", "TAG", "SEPARATOR", "|")); + } + + [Fact] + public void TestMixedNestingQuerying() + { + _mock.Setup(x => x.Execute(It.IsAny(), It.IsAny())) + .Returns(_mockReply); + + var collection = new RedisCollection(_mock.Object); + + collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCascade.Tag == "hello"); + + _mock.Verify(x => x.Execute( + "FT.SEARCH", + "complexobjectwithcascadeandjsonpath-idx", + "(@InnerCascade_InnerInnerCascade_Tag:{hello})", + "LIMIT", + "0", + "1" + )); + + collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCascade.Num == 5); + _mock.Verify(x => x.Execute( + "FT.SEARCH", + "complexobjectwithcascadeandjsonpath-idx", + "(@InnerCascade_InnerInnerCascade_Num:[5 5])", + "LIMIT", + "0", + "1" + )); + + collection.FirstOrDefault(x => x.InnerCascade.InnerInnerCollection.Any(x=>x.Tag == "hello")); + _mock.Verify(x => x.Execute( + "FT.SEARCH", + "complexobjectwithcascadeandjsonpath-idx", + "(@InnerCascade_InnerInnerCollection_Tag:{hello})", + "LIMIT", + "0", + "1" + )); + } } } \ No newline at end of file diff --git a/test/Redis.OM.Unit.Tests/RedisSetupCollection.cs b/test/Redis.OM.Unit.Tests/RedisSetupCollection.cs index 27e4de6c..30f699a0 100644 --- a/test/Redis.OM.Unit.Tests/RedisSetupCollection.cs +++ b/test/Redis.OM.Unit.Tests/RedisSetupCollection.cs @@ -26,6 +26,7 @@ public RedisSetup() Connection.CreateIndex(typeof(ObjectWithDateTime)); Connection.CreateIndex(typeof(ObjectWithDateTimeHash)); Connection.CreateIndex(typeof(PersonWithNestedArrayOfObject)); + Connection.CreateIndex(typeof(ComplexObjectWithCascadeAndJsonPath)); Connection.CreateIndex(typeof(BasicJsonObjectTestSave)); } @@ -61,6 +62,7 @@ public void Dispose() Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithDateTime)); Connection.DropIndexAndAssociatedRecords(typeof(ObjectWithDateTimeHash)); Connection.DropIndexAndAssociatedRecords(typeof(PersonWithNestedArrayOfObject)); + Connection.DropIndexAndAssociatedRecords(typeof(ComplexObjectWithCascadeAndJsonPath)); Connection.DropIndexAndAssociatedRecords(typeof(BasicJsonObjectTestSave)); } } diff --git a/test/Redis.OM.Unit.Tests/Serialization/ComplexObjectWithCascadeAndJsonPath.cs b/test/Redis.OM.Unit.Tests/Serialization/ComplexObjectWithCascadeAndJsonPath.cs new file mode 100644 index 00000000..7d7319e2 --- /dev/null +++ b/test/Redis.OM.Unit.Tests/Serialization/ComplexObjectWithCascadeAndJsonPath.cs @@ -0,0 +1,24 @@ +using Redis.OM.Modeling; + +namespace Redis.OM.Unit.Tests; + +[Document(StorageType = StorageType.Json)] +public class ComplexObjectWithCascadeAndJsonPath +{ + [Indexed(CascadeDepth = 2)] public InnerObject InnerCascade { get; set; } + [Indexed(JsonPath = "$.InnerInnerCascade", CascadeDepth = 2)] public InnerObject InnerJson { get; set; } +} + +public class InnerObject +{ + [Indexed(JsonPath = "$.Tag")] public InnerInnerObject InnerInnerJson { get; set; } + [Indexed(CascadeDepth = 1)] public InnerInnerObject InnerInnerCascade { get; set; } + [Indexed(JsonPath = "$.Tag")] public InnerInnerObject[] InnerInnerCollection { get; set; } +} + +public class InnerInnerObject +{ + [Indexed] public string Tag { get; set; } + [Indexed] public int Num { get; set; } + [Indexed] public string[] Arr { get; set; } +} \ No newline at end of file