/// <summary> /// gets an expression that specifies a property to be tested /// </summary> /// <param name="filter"></param> /// <param name="paramExp"></param> /// <param name="value"></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> /// <returns></returns> private static Expression GetFilteredValue(ReadFilter filter, ParameterExpression paramExp, object value = null) { return(WrapIntoDbFunction( filter, Expression.Convert( Expression.Constant(value ?? filter.Value), Expression.Property(paramExp, filter.Property).Type ) )); }
/// <summary> /// Reads applications that are visible to an organisation and can be linked to it - returns apps that require auth and apps that are non-public (specific to that very org) /// </summary> /// <param name="dbCtx"></param> /// <param name="sort"></param> /// <param name="filter"></param> /// <param name="start"></param> /// <param name="limit"></param> /// <returns></returns> public async Task <Tuple <IEnumerable <Application>, int> > GetOrganisationLinkableApps(DbContext dbCtx, string sort = null, string filter = null, int start = 0, int limit = 25) { //first need to get objects for an organisation, and then add an extra filter with object guids var orgObjIds = await GetOrganisationObjectIdsAsync <Application>(dbCtx); //do make sure there is something to read! var filters = filter.ExtJsJsonFiltersToReadFilters(); //return all the apps var preFilter = new ReadFilter { Property = nameof(Application.RequiresAuth), Value = true, Operator = "==", ExactMatch = true, //make the whole filter exact AndJoin = true, NestedFilters = new List <ReadFilter> { //need common apps new ReadFilter { Property = nameof(Application.IsCommon), Value = true }, //but not the dashboard. users always can access the dashboard! new ReadFilter { Property = nameof(Application.IsDefault), Value = false } } }; //if there are org specific apps always return them in addition to the ones filtered by default if (orgObjIds.Any()) { preFilter = new ReadFilter { Property = "Uuid", Value = orgObjIds.AsReadFilterList(), Operator = "in", ExactMatch = true, AndJoin = false, NestedFilters = new List <ReadFilter> { preFilter } }; } filters.Add(preFilter); var app = new Application(); var apps = await app.ReadAsync(dbCtx, sort.ExtJsJsonSortersToReadSorters(), filters, start, limit); if (!apps.Any()) { return(null); } var count = await apps.First().ReadCountAsync(dbCtx, filters); return(new Tuple <IEnumerable <Application>, int>(apps, count)); }
/// <summary> /// Gets a filter expression for a specified filter /// </summary> /// <param name="filter"></param> /// <param name="targetType"></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") { Guid guid; if (!Guid.TryParse(filter.Value, out 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."); } filter.Operator = "=="; filter.Value = guid; 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 //Check if model property exists; if not this is a bad, bad request... var propertyToFilterBy = targetType.GetProperties() .FirstOrDefault( p => string.Equals(p.Name, filter.Property, StringComparison.CurrentCultureIgnoreCase)); if (propertyToFilterBy == null) { throw new BadRequestException( $"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 orineted 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 shoudl implement IJsonSerialisableType. 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(IJsonSerialisableType))) { //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), "serialised" //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 BadRequestException( $"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; if (Expression.Property(paramExp, filter.Property).Type == 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()) //); Guid guid; if (!Guid.TryParse((string)item, out guid)) { continue; } inExpression = Expression.Equal( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp, guid) ); } else { inExpression = Expression.Equal( GetFilteredProperty(filter, paramExp), GetFilteredValue(filter, paramExp) ); } //join the filters for each item in a list with OR filterExpression = filterExpression == null ? inExpression : Expression.OrElse(filterExpression, inExpression); } } catch (Exception ex) { throw new BadRequestException("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) ); } //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 BadRequestException("Property: " + propertyToFilterBy.Name + ", " + ex.Message, ex.InnerException); } } // If no expression generated then throw exception if (filterExpression == null) { throw new BadRequestException( $"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); filterExpression = filter.AndJoin ? Expression.AndAlso(filterExpression, nested) : Expression.OrElse(filterExpression, nested); } } return(filterExpression); }