/// <summary> /// gets an expression that specifies a property to be tested /// </summary> /// <param name="filter"></param> /// <param name="paramExp"></param> /// <returns></returns> private static Expression GetFilteredProperty(ReadFilter filter, ParameterExpression paramExp) { return(WrapIntoDbFunction( filter, Expression.Property(paramExp, filter.Property) )); }
/// <summary> /// Wraps an expression into a single param db fn /// </summary> /// <param name="filter"></param> /// <param name="e"></param> /// <returns></returns> private static Expression WrapIntoDbFunction(ReadFilter filter, Expression e) { if (!string.IsNullOrEmpty(filter.DbFn)) { return(Expression.Call(typeof(DbFunctions), filter.DbFn, Type.EmptyTypes, e)); } return(e); }
/// <summary> /// Gets an expression that specifies the value to be tested against /// </summary> /// <param name="filter"></param> /// <param name="paramExp"></param> /// <param name="value"></param> /// <param name="forceValue">Whether or not should force value or try to use filter value if null</param> /// <returns></returns> private static Expression GetFilteredValue(ReadFilter filter, ParameterExpression paramExp, object value = null, bool forceValue = false) { return(WrapIntoDbFunction( filter, Expression.Convert( Expression.Constant(forceValue ? value : value ?? filter.Value), Expression.Property(paramExp, filter.Property).Type ) )); }
/// <summary> /// Gets a filter expression for a specified filter /// </summary> /// <param name="filter"></param> /// <param name="targetType"></param> /// <param name="paramExp"></param> /// <returns></returns> private static Expression GetFilterExpression(ReadFilter filter, Type targetType, ParameterExpression paramExp) { Expression filterExpression = null; //adjust "guid" filter types to just "==", so they're filtered the same way if (filter.Operator == "guid" || filter.Operator == "!guid") { var nullableGuid = default(Guid?); if (!string.IsNullOrEmpty((string)filter.Value)) { if (!Guid.TryParse((string)filter.Value, out var guid)) { throw new ArgumentException($"Filter on {filter.Property} with a value of {filter.Value} is specified as a guid filter; the value is nto parsable to guid though."); } ; nullableGuid = guid; } filter.Operator = filter.Operator == "guid" ? "==" : "!="; filter.Value = nullableGuid; filter.ExactMatch = true; } //if filter operator is not defined make it "==" for bools, "like" for strings and "eq" for numbers and dates //this is mainly because when filtering directly on store, operator is not sent and may be null if (string.IsNullOrEmpty(filter.Operator)) { if (filter.Value is string) { filter.Operator = "like"; } else if (filter.Value is bool) { filter.Operator = "=="; } else if (filter.Value.IsNumeric() || filter.Value is DateTime) { filter.Operator = "eq"; } } //TODO - support some more filter operators such as =, <, <=, >, >=, notin, !=; this should be simply just a minor modification of the conditions below var propertyToFilterBy = targetType.GetProperties() .FirstOrDefault( p => string.Equals(p.Name, filter.Property, StringComparison.CurrentCultureIgnoreCase)); if (filter.Operator != "nested") { //Check if model property exists; if not this is a bad, bad request... if (string.IsNullOrEmpty(filter.Property)) { throw new ArgumentException( $"The property to filter on has not been declared!"); } if (propertyToFilterBy == null) { throw new ArgumentException( $"The property {targetType}.{filter.Property} is not defined for the model!"); } } //work out what sort of filtering should be applied for a property... //string filter if (filter.Operator == "like" && filter.Value is string) { //ExtJs sends string filters ike this: //{"operator":"like","value":"some value","property":"name"} //Note: //In some cases complex types are used to simplify interaction with them in an object oriented way (so through properties) and at the same time //such types are stored as a single json string entry in the database //in such case a type should implement IJsonSerializableType. I so it should be possible to filter such type as it was a string //(which it indeed is on the db side) if (propertyToFilterBy.PropertyType.GetInterfaces().Contains(typeof(Cartomatic.Utils.JsonSerializableObjects.IJsonSerializable))) { //This will call to lower on the property that the filter applies to; //pretty much means call ToLower on the property - something like p => p.ToLower() var toLower = Expression.Call( Expression.Property( //this specify the property to filter by on the param object specified earlier - so p => p.Property Expression.Property(paramExp, filter.Property), "serialized" //and we dig deeper here to reach p.Property.Serialised ), typeof(string).GetMethod("ToLower", Type.EmptyTypes) //this is the method to be called on the property specified above ); //finally need to assemble an equivaluent of p.Property.ToLower().Contains(filter.Value) filterExpression = Expression.Call(toLower, "Contains", null, Expression.Constant(((string)filter.Value.ToString()).ToLower(), typeof(string))); } else { //this should be a string //make sure the type of the property to filter by is ok if (propertyToFilterBy.PropertyType != typeof(string)) { throw new ArgumentException( $"The property {targetType}.{propertyToFilterBy.Name} is not of 'string' type."); } //This will call to lower on the property that the filter applies to; //pretty much means call ToLower on the property - something like p => p.ToLower() var toLower = Expression.Call( Expression.Property(paramExp, filter.Property), //this specify the property to filter by on the param object specified earlier - so p => p.Property typeof(string).GetMethod("ToLower", Type.EmptyTypes) //this is the method to be called on the property specified above ); //finally need to assemble an equivaluent of p.Property.ToLower().Contains(filter.Value) filterExpression = Expression.Call(toLower, "Contains", null, Expression.Constant(((string)filter.Value.ToString()).ToLower(), typeof(string))); } } //range filter else if (filter.Operator == "in" && filter.Value is JArray) { //{"operator":"in","value":[11,18],"property":"name"} try { var json = ((string)filter.Value.ToString()).Trim().TrimStart('{').TrimEnd('}'); var list = JsonConvert.DeserializeObject <List <object> >(json); foreach (var item in list) { Expression inExpression; var underlyingType = Nullable.GetUnderlyingType(Expression.Property(paramExp, filter.Property).Type); if ( Expression.Property(paramExp, filter.Property).Type == typeof(Guid) || underlyingType == typeof(Guid) ) { //property to string //var toStr = Expression.Call( // GetFilteredProperty(filter, paramExp), // typeof(Guid).GetMethod("ToString", Type.EmptyTypes) //); ////and then lowercase //var toLower = Expression.Call(typeof(string).GetMethod("ToString", Type.EmptyTypes)); //inExpression = Expression.Call(toLower, // typeof(string).GetMethod("Equals", new[] {typeof(string)}), // Expression.Constant(item.ToString().ToLower()) //); //make sure can squize in nulls as filtered values var nullableGuid = default(Guid?); if (!string.IsNullOrEmpty((string)item)) { if (!Guid.TryParse((string)item, out var guid)) { continue; } nullableGuid = guid; } inExpression = Expression.Equal( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp, nullableGuid, !nullableGuid.HasValue) ); } else { inExpression = Expression.Equal( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp, item) ); } //join the filters for each item in a list with OR filterExpression = filterExpression == null ? inExpression : Expression.OrElse(filterExpression, inExpression); } } catch (Exception ex) { throw new ArgumentException("Property: " + propertyToFilterBy.Name + ", " + ex.Message, ex.InnerException); } } //boolean filter //else if (filter.Operator == "==" && filter.Value is bool) else if (filter.Operator == "==") { //{"operator":"==","value":true,"property":"name"} //if (propertyToFilterBy.PropertyType != typeof(bool)) // throw new BadRequestException($"The property { targetType }.{ propertyToFilterBy.Name} is not of 'bool' type."); //filterExpression = Expression.Call(Expression.Property(expType, filter.Property), "Equals", null, Expression.Constant(filter.Value, typeof(bool))); filterExpression = Expression.Equal( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp) ); } else if (filter.Operator == "!=") { //{"operator":"==","value":true,"property":"name"} //if (propertyToFilterBy.PropertyType != typeof(bool)) // throw new BadRequestException($"The property { targetType }.{ propertyToFilterBy.Name} is not of 'bool' type."); //filterExpression = Expression.Call(Expression.Property(expType, filter.Property), "Equals", null, Expression.Constant(filter.Value, typeof(bool))); filterExpression = Expression.NotEqual( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp) ); } //Lower than / greater than / equals; applies to numbers and dates else if ((filter.Operator == "lt" || filter.Operator == "gt" || filter.Operator == "eq") && (filter.Value.IsNumeric() || filter.Value is DateTime)) { //{"operator":"lt","value":10,"property":"name"} //{"operator":"gt","value":10,"property":"name"} //{"operator":"eq","value":10,"property":"name"} //{"operator":"lt","value":"2016-05-08T22:00:00.000Z","property":"name"} //{"operator":"gt","value":"2016-05-08T22:00:00.000Z","property":"name"} //{"operator":"eq","value":"2016-05-08T22:00:00.000Z","property":"name"} try { switch (filter.Operator) { case "eq": filterExpression = Expression.Equal( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp) ); break; case "lt": filterExpression = Expression.LessThan( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp) ); break; case "gt": filterExpression = Expression.GreaterThan( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp) ); break; } } catch (Exception ex) { throw new ArgumentException("Property: " + propertyToFilterBy.Name + ", " + ex.Message, ex.InnerException); } } // If no expression generated then throw exception if (filter.Operator != "nested" && filterExpression == null) { throw new ArgumentException( $"Filter operator: {filter.Operator} for type: {filter.Value.GetType()} is not implemented (property: {propertyToFilterBy.Name} should be {propertyToFilterBy.PropertyType})"); } //check if there are nested filters and process them the same way as itself! if (filter.NestedFilters?.Count > 0) { foreach (var nestedFilter in filter.NestedFilters) { var nested = GetFilterExpression(nestedFilter, targetType, paramExp); if (filterExpression == null && filter.Operator == "nested") { //a nested filter is a way of specifying a nested filter that gets glued to containing property //according to the AndJoin value and internally looks like this //( filter1 and/orelse filter2) //and or else depends on the value of AndJoin set on the nested filter type filterExpression = nested; continue; } //else - this should have thrown just before we examined the nested filters property //nothing to join! //this is a rare scenario when someone declared a nested filter but it was of 'nested' type and //had an empty nestedFilters array. in this case, no filter expression could be created if (nested == null) { continue; } filterExpression = filter.AndJoin ? Expression.AndAlso(filterExpression, nested) : Expression.OrElse(filterExpression, nested); } } return(filterExpression); }