public LambdaExpression ToExpression(IEnumerable <PropertyFilter> propertyFilters, Type parameterType) { ParameterExpression parameter = Expression.Parameter(parameterType, "p"); if (propertyFilters == null || propertyFilters.Count() == 0) { return(Expression.Lambda(Expression.Constant(true), parameter)); } Expression resultCondition = null; foreach (var filter in propertyFilters) { if (string.IsNullOrEmpty(filter.Property)) { continue; } Expression memberAccess = null; foreach (var property in filter.Property.Split('.')) { var parentExpression = memberAccess ?? (Expression)parameter; if (parentExpression.Type.GetProperty(property) == null) { throw new ClientException("Invalid generic filter parameter: Type '" + parentExpression.Type.FullName + "' does not have property '" + property + "'."); } memberAccess = Expression.Property(parentExpression, property); } // Change the type of the parameter 'value'. It is necessary for comparisons. bool propertyIsNullableValueType = memberAccess.Type.IsGenericType && memberAccess.Type.GetGenericTypeDefinition() == typeof(Nullable <>); Type propertyBasicType = propertyIsNullableValueType ? memberAccess.Type.GetGenericArguments().Single() : memberAccess.Type; ConstantExpression constant; // Operations 'equal' and 'notequal' are supported for backward compatibility. if (new[] { "equals", "equal", "notequals", "notequal", "greater", "greaterequal", "less", "lessequal" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { // Constant value should be of same type as the member it is compared to. object convertedValue; if (filter.Value == null || propertyBasicType.IsAssignableFrom(filter.Value.GetType())) { convertedValue = filter.Value; } // Guid object's type was not automatically recognized when deserializing from JSON: else if (propertyBasicType == typeof(Guid) && filter.Value is string) { convertedValue = Guid.Parse(filter.Value.ToString()); } // DateTime object's type was not automatically recognized when deserializing from JSON: else if (propertyBasicType == typeof(DateTime) && filter.Value is string) { convertedValue = ParseJsonDateTime((string)filter.Value, filter.Property, propertyBasicType); } else if ((propertyBasicType == typeof(decimal) || propertyBasicType == typeof(int)) && filter.Value is string) { throw new FrameworkException($"Invalid JSON format of {propertyBasicType.Name} property '{filter.Property}'. Numeric value should not be passed as a string in JSON serialized object."); } else { convertedValue = Convert.ChangeType(filter.Value, propertyBasicType); } if (convertedValue == null && memberAccess.Type.IsValueType && !propertyIsNullableValueType) { Type nullableMemberType = typeof(Nullable <>).MakeGenericType(memberAccess.Type); memberAccess = Expression.Convert(memberAccess, nullableMemberType); } constant = Expression.Constant(convertedValue, memberAccess.Type); } else if (new[] { "startswith", "endswith", "contains", "notcontains" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { // Constant value should be string. constant = Expression.Constant(filter.Value.ToString(), typeof(string)); } else if (new[] { "datein", "datenotin" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { constant = null; } else if (new[] { "in", "notin" }.Contains(filter.Operation, StringComparer.OrdinalIgnoreCase)) { if (filter.Value == null) { throw new ClientException($"Invalid generic filter parameter for operation '{filter.Operation}' on {propertyBasicType.Name} property '{filter.Property}'." + $" The provided value is null, instead of an Array."); } // The list element should be of same type as the member it is compared to. var parameterMismatchInfo = new Lazy <string>(() => $"Invalid generic filter parameter for operation '{filter.Operation}' on {propertyBasicType.Name} property '{filter.Property}'." + $" The provided value type is '{filter.Value.GetType()}', instead of an Array of {propertyBasicType.Name}."); if (!(filter.Value is IEnumerable)) { throw new ClientException(parameterMismatchInfo.Value); } var list = (IEnumerable)filter.Value; // Guid object's type was not automatically recognized when deserializing from JSON: if (propertyBasicType == typeof(Guid) && list is IEnumerable <string> ) { list = ((IEnumerable <string>)list).Select(s => !string.IsNullOrEmpty(s) ? (Guid?)Guid.Parse(s) : null).ToList(); } // DateTime object's type was not automatically recognized when deserializing from JSON: if (propertyBasicType == typeof(DateTime) && list is IEnumerable <string> ) { list = ((IEnumerable <string>)list).Select(s => ParseJsonDateTime(s, filter.Property, propertyBasicType)).ToList(); } // Adjust the list element type to exactly match the property type: if (GetElementType(list) != memberAccess.Type) { if (list is IList && ((IList)list).Count == 0) { list = EmptyList(memberAccess.Type); } else if (propertyBasicType == typeof(Guid)) { AdjustListTypeNullable <Guid>(ref list, propertyIsNullableValueType); } else if (propertyBasicType == typeof(DateTime)) { AdjustListTypeNullable <DateTime>(ref list, propertyIsNullableValueType); } else if (propertyBasicType == typeof(int)) { AdjustListTypeNullable <int>(ref list, propertyIsNullableValueType); } else if (propertyBasicType == typeof(decimal)) { AdjustListTypeNullable <decimal>(ref list, propertyIsNullableValueType); } if (!(GetElementType(list).IsAssignableFrom(memberAccess.Type))) { throw new ClientException(parameterMismatchInfo.Value); } } constant = Expression.Constant(list, list.GetType()); } else { throw new ClientException($"Unsupported generic filter operation '{filter.Operation}' on a property."); } Expression expression; switch (filter.Operation.ToLower()) { case "equals": case "equal": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("EqualsCaseInsensitive"), memberAccess, constant); } else { expression = Expression.Equal(memberAccess, constant); } break; case "notequals": case "notequal": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("NotEqualsCaseInsensitive"), memberAccess, constant); } else { expression = Expression.NotEqual(memberAccess, constant); } break; case "greater": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsGreaterThen"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsGreaterThan"), memberAccess, constant); } else { expression = Expression.GreaterThan(memberAccess, constant); } break; case "greaterequal": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsGreaterThenOrEqual"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsGreaterThanOrEqual"), memberAccess, constant); } else { expression = Expression.GreaterThanOrEqual(memberAccess, constant); } break; case "less": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsLessThen"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsLessThan"), memberAccess, constant); } else { expression = Expression.LessThan(memberAccess, constant); } break; case "lessequal": if (propertyBasicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsLessThenOrEqual"), memberAccess, constant); } else if (propertyBasicType == typeof(Guid)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("GuidIsLessThanOrEqual"), memberAccess, constant); } else { expression = Expression.LessThanOrEqual(memberAccess, constant); } break; case "startswith": case "endswith": { Expression stringMember; if (propertyBasicType == typeof(string)) { stringMember = memberAccess; } else { var castMethod = typeof(DatabaseExtensionFunctions).GetMethod("CastToString", new[] { memberAccess.Type }); if (castMethod == null) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' is not supported on property type '" + propertyBasicType.Name + "'. There is no overload of 'DatabaseExtensionFunctions.CastToString' function for the type."); } stringMember = Expression.Call(castMethod, memberAccess); } string dbMethodName = filter.Operation.Equals("startswith", StringComparison.OrdinalIgnoreCase) ? "StartsWithCaseInsensitive" : "EndsWithCaseInsensitive"; expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod(dbMethodName), stringMember, constant); break; } case "contains": case "notcontains": { Expression stringMember; if (propertyBasicType == typeof(string)) { stringMember = memberAccess; } else { var castMethod = typeof(DatabaseExtensionFunctions).GetMethod("CastToString", new[] { memberAccess.Type }); if (castMethod == null) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' is not supported on property type '" + propertyBasicType.Name + "'. There is no overload of 'DatabaseExtensionFunctions.CastToString' function for the type."); } stringMember = Expression.Call(castMethod, memberAccess); } expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("ContainsCaseInsensitive"), stringMember, constant); if (filter.Operation.Equals("notcontains", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } case "datein": case "datenotin": { if (propertyBasicType != typeof(DateTime)) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' is not supported for property type '" + propertyBasicType.Name + "'. Expected property type 'DateTime'."); } var match = DateRangeRegex.Match(filter.Value.ToString()); if (!match.Success) { throw new FrameworkException("Generic filter operation '" + filter.Operation + "' expects format 'yyyy-mm-dd', 'yyyy-mm' or 'yyyy'. Value '" + filter.Value + "' has invalid format."); } DateTime?date1, date2; int year = int.Parse(match.Groups["y"].Value); if (string.IsNullOrEmpty(match.Groups["m"].Value)) { date1 = new DateTime(year, 1, 1); date2 = date1.Value.AddYears(1); } else { int month = int.Parse(match.Groups["m"].Value); if (string.IsNullOrEmpty(match.Groups["d"].Value)) { date1 = new DateTime(year, month, 1); date2 = date1.Value.AddMonths(1); } else { int day = int.Parse(match.Groups["d"].Value); date1 = new DateTime(year, month, day); date2 = date1.Value.AddDays(1); } } expression = Expression.AndAlso( Expression.GreaterThanOrEqual(memberAccess, Expression.Constant(date1, typeof(DateTime?))), Expression.LessThan(memberAccess, Expression.Constant(date2, typeof(DateTime?)))); if (filter.Operation.Equals("datenotin", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } case "in": case "notin": { Type collectionBasicType = typeof(IQueryable).IsAssignableFrom(constant.Type) ? typeof(Queryable) : typeof(Enumerable); Type collectionElement = GetElementType(constant.Type); var containsMethod = collectionBasicType.GetMethods() .Where(m => m.Name == "Contains" && m.GetParameters().Count() == 2) .Single() .MakeGenericMethod(collectionElement); Expression convertedMemberAccess = memberAccess.Type != collectionElement ? Expression.Convert(memberAccess, collectionElement) : memberAccess; expression = Expression.Call(containsMethod, constant, convertedMemberAccess); if (filter.Operation.Equals("notin", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } default: throw new FrameworkException("Unsupported generic filter operation '" + filter.Operation + "' on property (while generating expression)."); } resultCondition = resultCondition != null?Expression.AndAlso(resultCondition, expression) : expression; } return(Expression.Lambda(resultCondition, parameter)); }
public LambdaExpression ToExpression(IEnumerable <PropertyFilter> propertyFilters, Type parameterType) { ParameterExpression parameter = Expression.Parameter(parameterType, "p"); if (propertyFilters == null || propertyFilters.Count() == 0) { return(Expression.Lambda(Expression.Constant(true), parameter)); } Expression resultCondition = null; foreach (var criteria in propertyFilters) { if (string.IsNullOrEmpty(criteria.Property)) { continue; } Expression memberAccess = null; foreach (var property in criteria.Property.Split('.')) { var parentExpression = memberAccess ?? (Expression)parameter; if (parentExpression.Type.GetProperty(property) == null) { throw new ClientException("Invalid generic filter parameter: Type '" + parentExpression.Type.FullName + "' does not have property '" + property + "'."); } memberAccess = Expression.Property(parentExpression, property); } // Change the type of the parameter 'value'. it is necessary for comparisons (specially for booleans) bool memberIsNullableValueType = memberAccess.Type.IsGenericType && memberAccess.Type.GetGenericTypeDefinition() == typeof(Nullable <>); Type basicType = memberIsNullableValueType ? memberAccess.Type.GetGenericArguments().Single() : memberAccess.Type; ConstantExpression constant; // Operations 'equal' and 'notequal' are supported for backward compatibility. if (new[] { "equals", "equal", "notequals", "notequal", "greater", "greaterequal", "less", "lessequal" }.Contains(criteria.Operation, StringComparer.OrdinalIgnoreCase)) { // Constant value should be of same type as the member it is compared to. object convertedValue; if (criteria.Value == null || basicType.IsAssignableFrom(criteria.Value.GetType())) { convertedValue = criteria.Value; } else if (basicType == typeof(Guid) && criteria.Value is string) // Guid object's type was not automatically recognized when deserializing from JSON. { convertedValue = Guid.Parse(criteria.Value.ToString()); } else if (basicType == typeof(DateTime) && criteria.Value is string) // DateTime object's type was not automatically recognized when deserializing from JSON. { string dateString = "\"" + ((string)criteria.Value).Replace("/", "\\/") + "\""; using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(dateString))) { var serializer = new DataContractJsonSerializer(typeof(DateTime)); try { convertedValue = (DateTime)serializer.ReadObject(stream); } catch (SerializationException ex) { throw new ClientException("Invalid JSON format of " + basicType.Name + " property '" + criteria.Property + "'. " + ex.Message, ex); } } } else if ((basicType == typeof(decimal) || basicType == typeof(int)) && criteria.Value is string) { throw new FrameworkException("Invalid JSON format of " + basicType.Name + " property '" + criteria.Property + "'. Numeric value should not be passed as a string in JSON serialized object."); } else { convertedValue = Convert.ChangeType(criteria.Value, basicType); } if (convertedValue == null && memberAccess.Type.IsValueType && !memberIsNullableValueType) { Type nullableMemberType = typeof(Nullable <>).MakeGenericType(memberAccess.Type); memberAccess = Expression.Convert(memberAccess, nullableMemberType); } constant = Expression.Constant(convertedValue, memberAccess.Type); } else if (new[] { "startswith", "endswith", "contains", "notcontains" }.Contains(criteria.Operation, StringComparer.OrdinalIgnoreCase)) { // Constant value should be string. constant = Expression.Constant(criteria.Value.ToString(), typeof(string)); } else if (new[] { "datein", "datenotin" }.Contains(criteria.Operation, StringComparer.OrdinalIgnoreCase)) { constant = null; } else { throw new FrameworkException("Unsupported generic filter operation '" + criteria.Operation + "' on property."); } Expression expression; switch (criteria.Operation.ToLower()) { case "equals": case "equal": if (basicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("EqualsCaseInsensitive"), memberAccess, constant); } else { expression = Expression.Equal(memberAccess, constant); } break; case "notequals": case "notequal": if (basicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("NotEqualsCaseInsensitive"), memberAccess, constant); } else { expression = Expression.NotEqual(memberAccess, constant); } break; case "greater": if (basicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsGreaterThen"), memberAccess, constant); } else { expression = Expression.GreaterThan(memberAccess, constant); } break; case "greaterequal": if (basicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsGreaterThenOrEqual"), memberAccess, constant); } else { expression = Expression.GreaterThanOrEqual(memberAccess, constant); } break; case "less": if (basicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsLessThen"), memberAccess, constant); } else { expression = Expression.LessThan(memberAccess, constant); } break; case "lessequal": if (basicType == typeof(string)) { expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("IsLessThenOrEqual"), memberAccess, constant); } else { expression = Expression.LessThanOrEqual(memberAccess, constant); } break; case "startswith": case "endswith": { Expression stringMember; if (basicType == typeof(string)) { stringMember = memberAccess; } else { var castMethod = typeof(DatabaseExtensionFunctions).GetMethod("CastToString", new[] { memberAccess.Type }); if (castMethod == null) { throw new FrameworkException("Generic filter operation '" + criteria.Operation + "' is not supported on property type '" + basicType.Name + "'. There is no overload of 'DatabaseExtensionFunctions.CastToString' function for the type."); } stringMember = Expression.Call(castMethod, memberAccess); } string dbMethodName = criteria.Operation.Equals("startswith", StringComparison.OrdinalIgnoreCase) ? "StartsWithCaseInsensitive" : "EndsWithCaseInsensitive"; expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod(dbMethodName), stringMember, constant); break; } case "contains": case "notcontains": { Expression stringMember; if (basicType == typeof(string)) { stringMember = memberAccess; } else { var castMethod = typeof(DatabaseExtensionFunctions).GetMethod("CastToString", new[] { memberAccess.Type }); if (castMethod == null) { throw new FrameworkException("Generic filter operation '" + criteria.Operation + "' is not supported on property type '" + basicType.Name + "'. There is no overload of 'DatabaseExtensionFunctions.CastToString' function for the type."); } stringMember = Expression.Call(castMethod, memberAccess); } expression = Expression.Call(typeof(DatabaseExtensionFunctions).GetMethod("ContainsCaseInsensitive"), stringMember, constant); if (criteria.Operation.Equals("notcontains", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } case "datein": case "datenotin": { if (basicType != typeof(DateTime)) { throw new FrameworkException("Generic filter operation '" + criteria.Operation + "' is not supported for property type '" + basicType.Name + "'. Expected property type 'DateTime'."); } var match = DateRangeRegex.Match(criteria.Value.ToString()); if (!match.Success) { throw new FrameworkException("Generic filter operation '" + criteria.Operation + "' expects format 'yyyy-mm-dd', 'yyyy-mm' or 'yyyy'. Value '" + criteria.Value + "' has invalid format."); } DateTime?date1, date2; int year = int.Parse(match.Groups["y"].Value); if (string.IsNullOrEmpty(match.Groups["m"].Value)) { date1 = new DateTime(year, 1, 1); date2 = date1.Value.AddYears(1); } else { int month = int.Parse(match.Groups["m"].Value); if (string.IsNullOrEmpty(match.Groups["d"].Value)) { date1 = new DateTime(year, month, 1); date2 = date1.Value.AddMonths(1); } else { int day = int.Parse(match.Groups["d"].Value); date1 = new DateTime(year, month, day); date2 = date1.Value.AddDays(1); } } expression = Expression.AndAlso( Expression.GreaterThanOrEqual(memberAccess, Expression.Constant(date1, typeof(DateTime?))), Expression.LessThan(memberAccess, Expression.Constant(date2, typeof(DateTime?)))); if (criteria.Operation.Equals("datenotin", StringComparison.OrdinalIgnoreCase)) { expression = Expression.Not(expression); } break; } default: throw new FrameworkException("Unsupported generic filter operation '" + criteria.Operation + "' on property (while generating expression)."); } resultCondition = resultCondition != null?Expression.AndAlso(resultCondition, expression) : expression; } return(Expression.Lambda(resultCondition, parameter)); }