|
5 | 5 | using System.Reflection; |
6 | 6 | using System.Text; |
7 | 7 | using System.Text.RegularExpressions; |
| 8 | +using Redis.OM.Aggregation; |
8 | 9 | using Redis.OM.Aggregation.AggregationPredicates; |
9 | 10 | using Redis.OM.Modeling; |
10 | 11 | using Redis.OM.Searching.Query; |
@@ -155,21 +156,117 @@ internal static RedisGeoFilter TranslateGeoFilter(MethodCallExpression exp) |
155 | 156 | return new RedisGeoFilter(memberOperand, longitude, latitude, radius, unit); |
156 | 157 | } |
157 | 158 |
|
| 159 | + /// <summary> |
| 160 | + /// Gets the search field name from member expression. Will climb back up to the parent node and build the alias. |
| 161 | + /// Which will be all the names in the path to the expression seperated by an underscore. e.g. Address_City. |
| 162 | + /// </summary> |
| 163 | + /// <param name="member">The member expression to pull the serach field name from.</param> |
| 164 | + /// <returns>The alias to search for.</returns> |
| 165 | + internal static string GetSearchFieldNameFromMember(MemberExpression member) |
| 166 | + { |
| 167 | + var stack = GetMemberChain(member); |
| 168 | + var topMember = stack.Peek(); |
| 169 | + var memberPath = stack.Select(x => x.Name).ToArray(); |
| 170 | + |
| 171 | + if (topMember == member.Member) |
| 172 | + { |
| 173 | + var searchField = member.Member.GetCustomAttributes().Where(x => x is SearchFieldAttribute).Cast<SearchFieldAttribute>().FirstOrDefault(); |
| 174 | + if (searchField != null && !string.IsNullOrEmpty(searchField.PropertyName)) |
| 175 | + { |
| 176 | + return searchField.PropertyName; |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + return string.Join("_", memberPath); |
| 181 | + } |
| 182 | + |
| 183 | + /// <summary> |
| 184 | + /// Gets the chain of members down to the currently accessed member. |
| 185 | + /// </summary> |
| 186 | + /// <param name="memberExpression">The member expression being accessed.</param> |
| 187 | + /// <returns>The chain of members down to the currently accessed member, e.g. if a Person's |
| 188 | + /// Address.City was being accessed a stack with Address at the top and City at the bottom would be returned.</returns> |
| 189 | + internal static Stack<MemberInfo> GetMemberChain(MemberExpression memberExpression) |
| 190 | + { |
| 191 | + var memberStack = new Stack<MemberInfo>(); |
| 192 | + memberStack.Push(memberExpression.Member); |
| 193 | + |
| 194 | + var parentExpression = memberExpression.Expression; |
| 195 | + while (parentExpression is MemberExpression parentMember) |
| 196 | + { |
| 197 | + if (parentMember.Member.Name == nameof(AggregationResult<object>.RecordShell)) |
| 198 | + { |
| 199 | + break; |
| 200 | + } |
| 201 | + |
| 202 | + memberStack.Push(parentMember.Member); |
| 203 | + parentExpression = parentMember.Expression; |
| 204 | + } |
| 205 | + |
| 206 | + return memberStack; |
| 207 | + } |
| 208 | + |
| 209 | + /// <summary> |
| 210 | + /// Gets the Search Field type for the member. |
| 211 | + /// </summary> |
| 212 | + /// <param name="memberExpression">the member expression.</param> |
| 213 | + /// <returns>the <see cref="SearchFieldAttribute"/>.</returns> |
| 214 | + internal static SearchFieldAttribute? DetermineSearchAttribute(MemberExpression memberExpression) |
| 215 | + { |
| 216 | + var memberChain = GetMemberChain(memberExpression); |
| 217 | + SearchFieldAttribute? attr; |
| 218 | + do |
| 219 | + { |
| 220 | + var memberInfo = memberChain.Pop(); |
| 221 | + attr = memberInfo |
| 222 | + .GetCustomAttributes() |
| 223 | + .Where(x => x is SearchFieldAttribute) |
| 224 | + .Cast<SearchFieldAttribute>() |
| 225 | + .FirstOrDefault(x => x.JsonPath?.Split('.').Last() == memberExpression.Member.Name); |
| 226 | + } |
| 227 | + while (attr == null && memberChain.Any()); |
| 228 | + |
| 229 | + if (attr == null) |
| 230 | + { |
| 231 | + attr = memberExpression.Member.GetCustomAttributes().Where(x => x is SearchFieldAttribute).Cast<SearchFieldAttribute>().FirstOrDefault(); |
| 232 | + } |
| 233 | + |
| 234 | + return attr; |
| 235 | + } |
| 236 | + |
158 | 237 | private static string GetOperandStringForMember(MemberExpression member) |
159 | 238 | { |
160 | | - var searchField = member.Member.GetCustomAttribute<SearchFieldAttribute>(); |
| 239 | + var memberPath = new List<string>(); |
| 240 | + var parentExpression = member.Expression; |
| 241 | + while (parentExpression is MemberExpression parentMember) |
| 242 | + { |
| 243 | + memberPath.Add(parentMember.Member.Name); |
| 244 | + parentExpression = parentMember.Expression; |
| 245 | + } |
| 246 | + |
| 247 | + memberPath.Add(member.Member.Name); |
| 248 | + |
| 249 | + var searchField = member.Member.GetCustomAttributes().Where(x => x is SearchFieldAttribute).Cast<SearchFieldAttribute>().FirstOrDefault(); |
161 | 250 | if (searchField == null) |
162 | 251 | { |
163 | 252 | if (member.Expression is not ConstantExpression c) |
164 | 253 | { |
165 | | - return Expression.Lambda(member).Compile().DynamicInvoke().ToString(); |
| 254 | + try |
| 255 | + { |
| 256 | + return Expression.Lambda(member).Compile().DynamicInvoke().ToString(); |
| 257 | + } |
| 258 | + catch (Exception ex) |
| 259 | + { |
| 260 | + throw new InvalidOperationException( |
| 261 | + $"Could not retrieve value from {member.Member.Name}, most likely, it is not properly decorated in the model defining the index.", ex); |
| 262 | + } |
166 | 263 | } |
167 | 264 |
|
168 | 265 | var val = GetValue(member.Member, c.Value); |
169 | 266 | return val.ToString(); |
170 | 267 | } |
171 | 268 |
|
172 | | - var propertyName = string.IsNullOrEmpty(searchField.PropertyName) ? member.Member.Name : searchField.PropertyName; |
| 269 | + var propertyName = GetSearchFieldNameFromMember(member); |
173 | 270 | return $"@{propertyName}"; |
174 | 271 | } |
175 | 272 |
|
@@ -400,14 +497,26 @@ private static IEnumerable<BinaryExpression> SplitBinaryExpression(BinaryExpress |
400 | 497 |
|
401 | 498 | private static string TranslateContainsStandardQuerySyntax(MethodCallExpression exp) |
402 | 499 | { |
403 | | - if (exp.Object is not MemberExpression member) |
| 500 | + MemberExpression? expression = null; |
| 501 | + if (exp.Object is MemberExpression) |
404 | 502 | { |
405 | | - throw new ArgumentException("String that Contains is called on must be a member of an indexed class"); |
| 503 | + expression = exp.Object as MemberExpression; |
406 | 504 | } |
| 505 | + else if (exp.Arguments.FirstOrDefault() is MemberExpression) |
| 506 | + { |
| 507 | + expression = exp.Arguments.FirstOrDefault() as MemberExpression; |
| 508 | + } |
| 509 | + |
| 510 | + if (expression == null) |
| 511 | + { |
| 512 | + throw new InvalidOperationException($"Could not parse query for Contains"); |
| 513 | + } |
| 514 | + |
| 515 | + var type = Nullable.GetUnderlyingType(expression.Type) ?? expression.Type; |
407 | 516 |
|
408 | | - var memberName = GetOperandStringForMember(member); |
409 | | - var literal = GetOperandStringForQueryArgs(exp.Arguments[0]); |
410 | | - return $"{memberName}:{literal}"; |
| 517 | + var memberName = GetOperandStringForMember(expression); |
| 518 | + var literal = GetOperandStringForQueryArgs(exp.Arguments.Last()); |
| 519 | + return (type == typeof(string)) ? $"{memberName}:{literal}" : $"{memberName}:{{{literal}}}"; |
411 | 520 | } |
412 | 521 | } |
413 | 522 | } |
0 commit comments