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