Combine Lambda Expressions using custom operator - c#

I'm implementing a search page where the user can filter the result using a bunch of different filters. I need to pass a Lambda Expression to the search layer in order for the filtering to take place.
My problem is that I don't know how to build a Lambda expression dynamically.
I've tried combining Expressions using AndAlso() but that doesn't work since my Lambda expressions doesn't return a bool.
So I'm guessing I need to implement a ExpressionVisitor and that's where I'm lost right now.
// The custom operator for AFilterType
public static AFilterType operator &(AFilterType first, AFilterType second)
{
return new AndFilter(AFilterType.GetFilters<AndFilter>(first, second));
}
// Here's a simplified version of what I'm trying to do
var filterInput = new FilterInput() { FirstName = "John", LastName = "Doe" };
// Using Match() which is a AFilterType method
Expression<Func<Person, AFilterType>> firstNameFilterExpression = x => x.firstName.Match(filterInput.FirstName);
Expression<Func<Person, AFilterType>> lastNameFilterExpression = x => x.LastName.Match(filterInput.LastName);
// How can I combine those 2 expressions into 1 single Expression at runtime using the custom operator '&' (not the bool '&&').
// Combined Expression should be like this.
Expression<Func<Person, AFilterType>> combinedFilterExpression = x => x.firstName.Match(filterInput.FirstName) & x.LastName.Match(filterInput.LastName);

I've had the same problem once and I solved it using LinqKit and a little bit of reflection (I've used this in EntityFramework project, but it can be adapted to other types, if desired). I will try to post my stripped code below (hope its not too long).
Predisposition: Inclusion of LinqKit (https://www.nuget.org/packages/LinqKit or via NuGet) version 1.1.7.2 or higher.
The code consists of several files in subdirectories:
Interfaces
Framework
Extensions
Interfaces\IPredicateParser.cs
using System;
using System.Collections.Generic;
namespace LambdaSample.Interfaces
{
// Used to defined IPredicateParser for parsing predicates
public interface IPredicateParser
{
bool Parse(string text, bool rangesAllowed, Type definedType);
List<IPredicateItem> Items { get; }
}
}
Interfaces\IPredicateItem.cs
namespace LambdaSample.Interfaces
{
public interface IPredicateItem
{
bool IsValid { get; }
}
}
Framework\PredicateItemSingle.cs
using LambdaSample.Interfaces;
namespace LambdaSample.Framework
{
/// <summary>
/// Item for single predicate (e.g. "44")
/// </summary>
public class PredicateItemSingle : IPredicateItem
{
public PredicateItemSingle()
{
}
public bool IsValid => Value != null;
public object Value { get; set; }
}
}
Framework\PredicateItemRange.cs
using LambdaSample.Interfaces;
namespace LambdaSample.Framework
{
/// <summary>
/// Item for range predicates (e.g. "1-5")
/// </summary>
public class PredicateItemRange : IPredicateItem
{
public PredicateItemRange()
{
}
public bool IsValid => Value1 != null && Value2 != null;
public object Value1 { get; set; }
public object Value2 { get; set; }
}
}
Framework\PredicateParser.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using LambdaSample.Extensions;
using LambdaSample.Interfaces;
namespace LambdaSample.Framework
{
/// <summary>
/// Simple parser for text used in search fields for
/// searching through records or any values
/// </summary>
public class PredicateParser : IPredicateParser
{
private enum RangeType
{
None,
From,
To
}
public PredicateParser()
{
Items = new List<IPredicateItem>();
}
public bool Parse(string text, bool rangesAllowed, Type definedType)
{
Items.Clear();
if (string.IsNullOrWhiteSpace(text))
return true;
var result = true;
var items = text.Split(',');
foreach (var item in items)
{
object val1, val2;
bool isRange;
var ranges = item.Split('-');
if (rangesAllowed && ranges.Length == 2) // Range is only when ranges are allowed and length is 2, otherwise its single value.
{
object val1Temp, val2Temp;
if (ParseValue(ranges[0], definedType, RangeType.From, out isRange, out val1, out val1Temp) &&
ParseValue(ranges[1], definedType, RangeType.To, out isRange, out val2, out val2Temp))
{
Items.Add(new PredicateItemRange { Value1 = val1, Value2 = val2, });
}
else
{
result = false;
}
}
else
{
if (ParseValue(item, definedType, RangeType.None, out isRange, out val1, out val2))
{
if (isRange)
{
Items.Add(new PredicateItemRange { Value1 = val1, Value2 = val2, });
}
else
{
Items.Add(new PredicateItemSingle { Value = val1, });
}
}
else
{
result = false;
}
}
}
return result;
}
private bool ParseValue(string value, Type definedType, RangeType rangeType, out bool isRange, out object result, out object result2)
{
result = null;
result2 = null;
isRange = false;
if (string.IsNullOrWhiteSpace(value))
return false;
// Enums are also treated like ints!
if (definedType == typeof(int) || definedType.IsEnum)
{
int val;
if (!int.TryParse(value, out val))
return false;
result = val;
return true;
}
if (definedType == typeof(long))
{
long val;
if (!long.TryParse(value, out val))
return false;
result = val;
return true;
}
if (definedType == typeof(decimal))
{
decimal val;
if (!decimal.TryParse(value, NumberStyles.Number ^ NumberStyles.AllowThousands, new CultureInfo("sl-SI"), out val))
return false;
result = val;
return true;
}
if (definedType == typeof(DateTime))
{
int year, month, yearMonth;
if (value.Length == 4 && int.TryParse(value, out year) && year >= 1000 && year <= 9999) // If only year, we set whole year's range (e.g. 2015 ==> 2015-01-01 00:00:00.0000000 - 2015-12-31 23:59:59.9999999
{
// Default datetime for From range and if no range
result = new DateTime(year, 1, 1);
switch (rangeType)
{
case RangeType.None:
result2 = ((DateTime)result).AddYears(1).AddMilliseconds(-1);
isRange = true;
break;
case RangeType.To:
result = ((DateTime)result).AddYears(1).AddMilliseconds(-1);
break;
}
return true;
}
if (value.Length == 6 && int.TryParse(value, out yearMonth) && yearMonth >= 100001 && yearMonth <= 999912) // If only year and month, we set whole year's range (e.g. 201502 ==> 2015-02-01 00:00:00.0000000 - 2015-02-28 23:59:59.9999999
{
year = Convert.ToInt32(yearMonth.ToString().Substring(0, 4));
month = Convert.ToInt32(yearMonth.ToString().Substring(4, 2));
// Default datetime for From range and if no range
result = new DateTime(year, month, 1);
switch (rangeType)
{
case RangeType.None:
result2 = ((DateTime)result).AddMonths(1).AddMilliseconds(-1);
isRange = true;
break;
case RangeType.To:
result = ((DateTime)result).AddMonths(1).AddMilliseconds(-1);
break;
}
return true;
}
DateTime val;
if (!value.ParseDateTimeEx(CultureInfo.InvariantCulture, out val))
{
return false;
}
if (val.Hour == 0 && val.Minute == 0)
{
// No hours and minutes specified, searching whole day or to the end of the day.
// If this is no range, we make it a range
result = new DateTime(val.Year, val.Month, val.Day);
switch (rangeType)
{
case RangeType.None:
result2 = ((DateTime)result).AddDays(1).AddMilliseconds(-1);
isRange = true;
break;
case RangeType.To:
result = ((DateTime)result).AddDays(1).AddMilliseconds(-1);
break;
}
return true;
}
result = val;
return true;
}
if (definedType == typeof(string))
{
result = value;
return true;
}
return false;
}
public List<IPredicateItem> Items { get; private set; }
}
}
Extensions\StringExtensions.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace LambdaSample.Extensions
{
public static class StringExtensions
{
private static List<string> GetValidDateTimeFormats()
{
var dateFormats = new[]
{
"dd.MM.yyyy",
"yyyy-MM-dd",
"yyyyMMdd",
}.ToList();
var timeFormats = new[]
{
"HH:mm:ss.fff",
"HH:mm:ss",
"HH:mm",
}.ToList();
var result = (from dateFormat in dateFormats
from timeFormat in timeFormats
select $"{dateFormat} {timeFormat}").ToList();
return result;
}
public static bool ParseDateTimeEx(this string #this, CultureInfo culture, out DateTime dateTime)
{
if (culture == null)
{
culture = CultureInfo.InvariantCulture;
}
if (DateTime.TryParse(#this, culture, DateTimeStyles.None, out dateTime))
return true;
var dateTimeFormats = GetValidDateTimeFormats();
if (DateTime.TryParseExact(#this, dateTimeFormats.ToArray(), culture, DateTimeStyles.None, out dateTime))
return true;
return false;
}
}
}
Extensions\ObjectExtensions.cs
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace LambdaSample.Extensions
{
public static class ObjectExtensions
{
/// <summary>
/// Build Filter Dictionary<string,string> used in ExpressionExtensions.BuildPredicate to build
/// predicates for Predicate Builder based on class's properties values. Filters are then used
/// by PredicateParser, which converts them to appropriate types (DateTime, int, decimal, etc.)
/// </summary>
/// <param name="this">Object to build dictionary from</param>
/// <param name="includeNullValues">Includes null values in dictionary</param>
/// <returns>Dictionary with string keys and string values</returns>
public static Dictionary<string, string> ToFilterDictionary(this object #this, bool includeNullValues)
{
var result = new Dictionary<string, string>();
if (#this == null || !#this.GetType().IsClass)
return result;
// First, generate Dictionary<string, string> from #this by using reflection
var props = #this.GetType().GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(#this);
if (value == null && !includeNullValues)
continue;
// If value already is a dictionary add items from this dictionary
var dictValue = value as IDictionary;
if (dictValue != null)
{
foreach (var key in dictValue.Keys)
{
var valueTemp = dictValue[key];
if (valueTemp == null && !includeNullValues)
continue;
result.Add(key.ToString(), valueTemp != null ? valueTemp.ToString() : null);
}
continue;
}
// If property ends with list, check if list of generics
if (prop.Name.EndsWith("List", false, CultureInfo.InvariantCulture))
{
var propName = prop.Name.Remove(prop.Name.Length - 4, 4);
var sb = new StringBuilder();
var list = value as IEnumerable;
if (list != null)
{
foreach (var item in list)
{
if (item == null)
continue;
if (sb.Length > 0)
sb.Append(",");
sb.Append(item.ToString());
}
result.Add(propName, sb.ToString());
}
continue;
}
var str = value != null ? value.ToString() : null;
result.Add(prop.Name, str);
}
return result;
}
}
}
Extensions\ExpressionExtensions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using LambdaSample.Framework;
using LambdaSample.Interfaces;
using LinqKit;
namespace LambdaSample.Extensions
{
public static class ExpressionExtensions
{
private static readonly MethodInfo StringContainsMethod = typeof(string).GetMethod(#"Contains", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, null);
private static readonly MethodInfo StringStartsWithMethod = typeof(string).GetMethod(#"StartsWith", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, null);
private static readonly MethodInfo StringEndsWithMethod = typeof(string).GetMethod(#"EndsWith", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(string) }, null);
private static readonly MethodInfo ObjectEquals = typeof(object).GetMethod(#"Equals", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(object) }, null);
//private static readonly MethodInfo BooleanEqualsMethod = typeof(bool).GetMethod(#"Equals", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(bool) }, null);
/// <summary>
/// Build a predicate with linq clauses, taking searchCriteria object's properties to define where conditions.
/// </summary>
/// <typeparam name="TDbType">Type of entity to build predicate for</typeparam>
/// <param name="searchCriteria">Object which contains criteria for predicate</param>
/// <param name="predicateParser">Implementation of predicate parser that will parse predicates as string</param>
/// <param name="includeNullValues">Determines whether null values are included when constructing query</param>
/// <returns></returns>
public static Expression<Func<TDbType, bool>> BuildPredicate<TDbType>(object searchCriteria, IPredicateParser predicateParser, bool includeNullValues)
{
var filterDictionary = searchCriteria.ToFilterDictionary(includeNullValues);
return BuildPredicate<TDbType>(filterDictionary, predicateParser);
}
public static Expression<Func<TDbType, bool>> BuildPredicate<TDbType>(Dictionary<string, string> searchCriteria, IPredicateParser predicateParser)
{
var predicateOuter = PredicateBuilder.New<TDbType>(true);
var predicateErrorFields = new List<string>();
var dict = searchCriteria;// as Dictionary<string, string>;
if (dict == null || !dict.Any())
return predicateOuter;
var searchFields = typeof(TDbType).GetProperties();
foreach (var searchField in searchFields)
{
// Get the name of the DB field, which may not be the same as the property name.
var dbFieldName = GetDbFieldName(searchField);
var dbType = typeof(TDbType);
var dbFieldMemberInfo = dbType.GetMember(dbFieldName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).SingleOrDefault();
if (dbFieldMemberInfo == null || !dict.ContainsKey(dbFieldMemberInfo.Name))
continue;
var predicateValue = dict[dbFieldMemberInfo.Name];
if (predicateValue == null)
continue;
var rangesAllowed = searchField.PropertyType != typeof(string);
if (!predicateParser.Parse(predicateValue, rangesAllowed, searchField.PropertyType))
{
predicateErrorFields.Add(dbFieldMemberInfo.Name);
continue;
}
if (!predicateParser.Items.Any())
continue;
var predicateInner = BuildInnerPredicate<TDbType>(predicateParser, searchField, dbFieldMemberInfo);
if (predicateInner == null)
continue;
predicateOuter = predicateOuter.And(predicateInner);
}
return predicateOuter;
}
private static Expression<Func<TDbType, bool>> BuildInnerPredicate<TDbType>(IPredicateParser predicateParser, PropertyInfo searchField, MemberInfo dbFieldMemberInfo)
{
var dbType = typeof(TDbType);
// Create an "x" as TDbType
var dbTypeParameter = Expression.Parameter(dbType, #"x");
// Get at x.firstName
var dbFieldMember = Expression.MakeMemberAccess(dbTypeParameter, dbFieldMemberInfo);
Expression<Func<TDbType, bool>> predicateInner = null;
foreach (var predicateItem in predicateParser.Items)
{
var predicateItemSingle = predicateItem as PredicateItemSingle;
var predicateItemRange = predicateItem as PredicateItemRange;
if (predicateItemSingle != null)
{
// Create the MethodCallExpression like x.firstName.Contains(criterion)
if (searchField.PropertyType == typeof(string))
{
var str = predicateItemSingle.Value as string ?? "";
var startsWithAsterisk = str.StartsWith("*");
var endsWithAsterisk = str.EndsWith("*");
str = str.Trim('*').Trim();
MethodCallExpression callExpression;
if (startsWithAsterisk && !endsWithAsterisk)
{
callExpression = Expression.Call(dbFieldMember, StringEndsWithMethod, new Expression[] { Expression.Constant(str) });
}
else if (!startsWithAsterisk && endsWithAsterisk)
{
callExpression = Expression.Call(dbFieldMember, StringStartsWithMethod, new Expression[] { Expression.Constant(str) });
}
else
{
callExpression = Expression.Call(dbFieldMember, StringContainsMethod, new Expression[] { Expression.Constant(str) });
}
predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(Expression.Lambda(callExpression, dbTypeParameter) as Expression<Func<TDbType, bool>>);
}
else
{
if (dbFieldMember.Type.IsEnum)
{
if (!dbFieldMember.Type.IsEnumDefined(predicateItemSingle.Value))
continue;
var enumValue = (int)predicateItemSingle.Value;
if (enumValue <= 0)
continue;
var enumObj = Enum.ToObject(dbFieldMember.Type, (int)predicateItemSingle.Value);
predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(Expression.Lambda<Func<TDbType, bool>>(Expression.Equal(dbFieldMember, Expression.Constant(enumObj)), new[] { dbTypeParameter }));
}
else
{
predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(Expression.Lambda<Func<TDbType, bool>>(Expression.Equal(dbFieldMember, Expression.Constant(predicateItemSingle.Value)), new[] { dbTypeParameter }));
}
}
}
else if (predicateItemRange != null)
{
var predicateRange = PredicateBuilder.New<TDbType>(true);
predicateRange = predicateRange.And(Expression.Lambda<Func<TDbType, bool>>(Expression.GreaterThanOrEqual(dbFieldMember, Expression.Constant(predicateItemRange.Value1)), new[] { dbTypeParameter }));
predicateRange = predicateRange.And(Expression.Lambda<Func<TDbType, bool>>(Expression.LessThanOrEqual(dbFieldMember, Expression.Constant(predicateItemRange.Value2)), new[] { dbTypeParameter }));
predicateInner = (predicateInner ?? PredicateBuilder.New<TDbType>(false)).Or(predicateRange);
}
}
return predicateInner;
}
private static string GetDbFieldName(PropertyInfo propertyInfo)
{
var dbFieldName = propertyInfo.Name;
// TODO: Can put custom logic here, to obtain another field name if desired.
return dbFieldName;
}
}
}
Usage
Let's say we have the DbPerson class that holds our data:
public class DbPerson
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public int Age { get; set; }
}
And beside that DbPerson class we have a class that represents our filter for the DbPerson objects:
public class DbPersonFilter
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string BirthDate { get; set; }
public string Age { get; set; }
}
Notice how the names of properties for base class DbPerson and DbPersonFilter are the same. This is important because a lot of code above requires that naming convention is consistent. Type of properties, however are not the same. This is because for filter we can set ranges to search, not just one value. Later there will be some samples to see how this works.
Now, let us fill our "database" with simple data. We use this method:
private List<DbPerson> GenerateTestDb()
{
var result = new List<DbPerson>
{
new DbPerson { Id = 1,FirstName = "John", LastName = "Doe", BirthDate = new DateTime(1963, 6, 14), Age = 53 },
new DbPerson { Id = 2,FirstName = "Jane", LastName = "Hunt", BirthDate = new DateTime(1972, 1, 16), Age = 44 },
new DbPerson { Id = 3,FirstName = "Aaron", LastName = "Pitch", BirthDate = new DateTime(1966, 7, 31), Age = 50 },
};
return result;
}
Using clauses for our sample applications are the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using LambdaSample.Extensions;
using LambdaSample.Framework;
using LinqKit;
Now, lets create some btnTest in our WinForms application (of course, you would you this in your app, whatever may be):
private void btnTest_Click(object sender, EventArgs e)
{
// Load sample database into db (db is actually List<DbPerson>)
var db = GenerateTestDb();
// Create filter looking for FirstName is "John"
var filterValues = new DbPersonFilter
{
FirstName = "John",
};
// Build PredicateParser which it used to parse predicates inside ExpressionExtensions.
var predicateParser = new PredicateParser();
// Build predicate...
var predicate1 = PredicateBuilder.New(ExpressionExtensions.BuildPredicate<DbPerson>(filterValues, predicateParser, true));
// And search for items...
var items1 = db.AsQueryable().AsExpandable().Where(predicate1).ToList();
// Create filter to look for items where Id is between 1 and 2
filterValues = new DbPersonFilter
{
Id = "1-2",
};
// Build predicate...
var predicate2 = PredicateBuilder.New(ExpressionExtensions.BuildPredicate<DbPerson>(filterValues, predicateParser, true));
// And search for items...
var items2 = db.AsQueryable().AsExpandable().Where(predicate2).ToList();
// Create filter to look for items where Age is 44
filterValues = new DbPersonFilter
{
Age = "44",
};
// Build predicate...
var predicate3 = PredicateBuilder.New(ExpressionExtensions.BuildPredicate<DbPerson>(filterValues, predicateParser, true));
// And search for items...
var items3 = db.AsQueryable().AsExpandable().Where(predicate3).ToList();
}
Hope this helps. Code should be self-explanatory, since comments are not included everywhere. If you have any more questions, do ask.
NOTE: .AsExpandable() is and extension method of LinqKit, to use PredicateBuilder inside Where extension method.

I deliberatly left out details about the actual domain in which I'm working. This was an attempt to make my question more generic and focused on Expressions.
But as it turned out there was a FilterExpressionParser available in the API I'm using (Episerver FIND) which came in handy.
So here's a function that builds and applies a composite filter.
private void MechanicalPropertiesFilter(SteelNavigatorForm form, ref ITypeSearch<SteelGradeVariantPage> search)
{
FilterExpressionParser filterExpressionParser = new FilterExpressionParser(SearchClient.Instance.Conventions);
Filter combinedFilter = null;
// Dimension
if (form.DimensionThickness > 0)
{
var dimensionFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.DimensionInMillimeterMin.LessThan(form.DimensionThickness)
& m.DimensionInMillimeterMax.GreaterThan(form.DimensionThickness));
combinedFilter = (combinedFilter == null) ? dimensionFilter : combinedFilter & dimensionFilter;
}
// Yield strength
if (form.YieldStrengthMin > 0)
{
var yieldStrengthFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.YieldStrengh.GreaterThan(form.YieldStrengthMin));
combinedFilter = (combinedFilter == null) ? yieldStrengthFilter : combinedFilter & yieldStrengthFilter;
}
// Tensile strength
if (form.TensileStrengthMin > 0 | form.TensileStrengthMax > 0)
{
var tensileStrengthMin = (form.TensileStrengthMin == 0) ? double.MinValue : form.TensileStrengthMin;
var tensileStrengthMax = (form.TensileStrengthMax == 0) ? double.MaxValue : form.TensileStrengthMax;
var tensileStrengthFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.TensileStrengthMin.InRangeInclusive(tensileStrengthMin, tensileStrengthMax) | m.TensileStrengthMax.InRangeInclusive(tensileStrengthMin, tensileStrengthMax));
combinedFilter = (combinedFilter == null) ? tensileStrengthFilter : combinedFilter & tensileStrengthFilter;
}
// Elongation
if (form.Elongation > 0)
{
var elongationFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.ElongationA5Percentage.GreaterThan(form.Elongation));
combinedFilter = (combinedFilter == null) ? elongationFilter : combinedFilter & elongationFilter;
}
// Hardness
if (form.HardnessMin > 0 || form.HardnessMax > 0)
{
var max = (form.HardnessMax == 0) ? double.MaxValue : form.HardnessMax;
var hardnessFilter = filterExpressionParser.GetFilter<MechanicalProperties>(m => m.HardnessScaleGuid.Match(form.HardnessMethod) & (
m.HardnessMin.InRangeInclusive(form.HardnessMin, max)
| m.HardnessMax.InRangeInclusive(form.HardnessMin, max)));
combinedFilter = (combinedFilter == null) ? hardnessFilter : combinedFilter & hardnessFilter;
}
if (combinedFilter != null)
{
NestedFilterExpression<SteelGradeVariantPage, MechanicalProperties> mechanicalFilterExpression = new NestedFilterExpression<SteelGradeVariantPage, MechanicalProperties>(v => v.MechanicalProperties, ((MechanicalProperties item) => combinedFilter), search.Client.Conventions);
search = search.Filter(mechanicalFilterExpression.NestedFilter);
}
}

Related

Prevent joining table if no field selected in GraphQL

I'm new to GraphQL.
Currently, I have a query definition which searches for students and their participated classes:
var studentsQueryArguments = new QueryArguments();
studentsQueryArguments.Add(new QueryArgument<ListGraphType<IntGraphType>> { Name = "ids", Description = "Student indexes." });
studentsQueryArguments.Add(new QueryArgument<RangeModelType<double?, double?>> {Name = "age", Description = "Age range of student."});
Field<ListGraphType<StudentType>>(
"students",
arguments: studentsQueryArguments,
resolve: context =>
{
var students = relationalDbContext.Students.AsQueryable();
var classes = relationalDbContext.Classes.AsQueryable();
var participatedClasses = relationalDbContext.StudentInClasses.AsQueryable();
var ids = context.GetArgument<List<int>>("ids");
var age = context.GetArgument<RangeModel<double?, double?>>("age");
if (ids != null)
students = students.Where(x => ids.Contains(x.Id));
if (age != null)
{
var from = age.From;
var to = age.To;
if (from != null)
students = students.Where(x => x.Age >= from);
if (to != null)
students = students.Where(x => x.Age <= to);
}
var results = (from student in students
select new StudentViewModel
{
Id = student.Id,
Age = student.Age,
FullName = student.FullName,
Photo = student.Photo,
Classes = from participatedClass in participatedClasses
from oClass in classes
where participatedClass.StudentId == student.Id &&
participatedClass.ClassId == oClass.Id
select new ClassViewModel
{
Id = oClass.Id,
ClosingHour = oClass.ClosingHour,
Name = oClass.Name,
OpeningHour = oClass.OpeningHour
}
});
return results;
});
In my code above, I join Student and Class.
With the query
{
students(ids: [1, 2, 3]) {
id
age
classes {
name
openingHour
closingHour
}
}
}
Students and their classes are returned. That is ok.
What I want is when I use this query:
{
students(ids: [1, 2, 3]) {
id
age
}
}
My app will not join Student with Class, and just return Student information only.
Is it possible ?
Thanks,
I made a super cool extension method that returns whether the selection is included in a GraphQL query.
Include the navigation property to your query if it exists in the selections.
Extension Method
using System;
using System.Linq;
using GraphQL.Language.AST;
using GraphQL.Types;
public static class ContextExtensions
{
/// <summary>
/// Returns true if the given fieldSelector exists in the selection of the query.
/// </summary>
/// <param name="context">The working context</param>
/// <param name="fieldSelector">The query of the field selector. For example items:organizationUnits:displayName</param>
/// <param name="namespaceSeperator">The seperator character of the fieldSelector. Default is :</param>
/// <returns></returns>
public static bool HasSelectionField(this ResolveFieldContext<object> context, string fieldSelector, char namespaceSeperator = ':')
{
if (string.IsNullOrWhiteSpace(fieldSelector))
{
return false;
}
if (context.SubFields == null)
{
return false;
}
var fragments = fieldSelector.Split(new[] { namespaceSeperator }, StringSplitOptions.RemoveEmptyEntries);
if (fragments.Length == 1)
{
return context.SubFields.ContainsKey(fragments[0]);
}
if (context.SubFields[fragments[0]] == null)
{
return false;
}
if (context.SubFields[fragments[0]].SelectionSet == null)
{
return false;
}
if (context.SubFields[fragments[0]].SelectionSet.Selections == null)
{
return false;
}
var selections = context.SubFields[fragments[0]].SelectionSet.Selections;
for (var i = 1; i < fragments.Length; i++)
{
if (selections == null)
{
return false;
}
var field = selections.Select(selection => (Field)selection).FirstOrDefault(f => f.Name == fragments[i]);
if (field == null)
{
return false;
}
if (i == fragments.Length - 1)
{
return true;
}
selections = field.SelectionSet?.Selections;
}
return true;
}
}
Usage
protected override async Task<PagedResultDto<UserDto>> Resolve(ResolveFieldContext<object> context)
{
var total_count_exists = context.HasSelectionField("totalCount"); //true
var items_name_exists = context.HasSelectionField("items:name"); //true
var items_roles_name_exists = context.HasSelectionField("items:roles:name"); //true
var items_organizationUnits_displayName_exists = context.HasSelectionField("items:organizationUnits:displayName"); //true
var items_organizationUnits_xyz_exists = context.HasSelectionField("items:organizationUnits:xyz"); //false
}
Sample Query
query MyQuery {
users(id: 1) {
totalCount
items {
name
surname
roles {
id
name
displayName
}
organizationUnits {
id
code
displayName
}
}
}
}

How to convent viewmodel to Expression<Func<T,bool>>?

Piggybacking off of a very similar question...
I need to generate an Expression from a ViewModel to pass as a search predicate for IQueryable.Where. I need to be able to include/exclude query parameters based on what is provided by the user. Example:
public class StoresFilter
{
public int[] Ids { get; set; }
[StringLength(150)]
public string Name { get; set; }
[StringLength(5)]
public string Abbreviation { get; set; }
[Display(Name = "Show all")]
public bool ShowAll { get; set; } = true;
public Expression<Func<Store, bool>> ToExpression()
{
List<Expression<Func<Store, bool>>> expressions = new List<Expression<Func<Store, bool>>>();
if (Ids != null && Ids.Length > 0)
{
expressions.Add(x => Ids.Contains(x.Id));
}
if (Name.HasValue())
{
expressions.Add(x => x.Name.Contains(Name));
}
if (Abbreviation.HasValue())
{
expressions.Add(x => x.Abbreviation.Contains(Abbreviation));
}
if (!ShowAll)
{
expressions.Add(x => x.Enabled == true);
}
if (expressions.Count == 0)
{
return x => true;
}
// how to combine list of expressions into composite expression???
return compositeExpression;
}
}
Is there a simple way to build a composite expression from a list of expressions? Or do I need to go through the process of manually building out the expression using ParameterExpression, Expression.AndAlso, ExpressionVisitor, etc?
You should not build and combine Expressions, but instead of this you should do it through IQuerable<Store> via .Where chain. Moreover, source.Expression will contain desired expression:
public IQueryable<Store> ApplyFilter(IQueryable<Store> source)
{
if (Ids != null && Ids.Length > 0)
source = source.Where(x => Ids.Contains(x.Id));
if (Name.HasValue())
source = source.Where(x => x.Name.Contains(Name));
if (Abbreviation.HasValue())
source = source.Where(x => x.Abbreviation.Contains(Abbreviation));
if (!ShowAll)
source = source.Where(x => x.Enabled == true);
//or return source.Expression as you wanted
return source;
}
Usage:
var filter = new StoresFilter { Name = "Market" };
var filteredStores = filter.ApplyFilter(context.Stores).ToList();
void Main()
{
var store = new Store
{
Id = 1,
Abbreviation = "ABC",
Enabled = true,
Name = "DEF"
};
var filter = new Filter<Store>
{
Ids = new HashSet<int>(new [] {1,2,3,4}),
Abbreviation = "GFABC",
Enabled = true,
Name = "SDEFGH",
ShowAll = false
}
var expression = filter.ToExpression(store);
var parameterType = Expression.Parameter(typeof(Store), "obj");
// Generate Func from the Expression Tree
Func<Store,bool> func = Expression.Lambda<Func<Store,bool>>(expression,parameterType).Compile();
}
public class Store
{
public int Id {get; set;}
public string Name {get; set;}
public string Abbreviation { get; set; }
public bool Enabled { get; set; }
}
public class Filter<T> where T : Store
{
public HashSet<int> Ids { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
public bool Enabled {get; set;}
public bool ShowAll { get; set; } = true;
public Expression ToExpression(T data)
{
var parameterType = Expression.Parameter(typeof(T), "obj");
var expressionList = new List<Expression>();
if (Ids != null && Ids.Count > 0)
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Id");
ConstantExpression idConstantExpression = Expression.Constant(data.Id, typeof(int));
MethodInfo filtersMethodInfo = typeof(HashsetExtensions).GetMethod("Contains", new[] { typeof(HashSet<int>), typeof(int) });
var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);
expressionList.Add(methodCallExpression);
}
if (!string.IsNullOrEmpty(Name))
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Name");
ConstantExpression idConstantExpression = Expression.Constant(data.Name, typeof(string));
MethodInfo filtersMethodInfo = typeof(StringExtensions).GetMethod("Contains", new[] { typeof(string), typeof(string) });
var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);
expressionList.Add(methodCallExpression);
}
if (!string.IsNullOrEmpty(Abbreviation))
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Abbreviation");
ConstantExpression idConstantExpression = Expression.Constant(data.Abbreviation, typeof(string));
MethodInfo filtersMethodInfo = typeof(StringExtensions).GetMethod("Contains", new[] { typeof(string), typeof(string) });
var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);
expressionList.Add(methodCallExpression);
}
if (!ShowAll)
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Enabled");
var binaryExpression = Expression.Equal(idExpressionColumn, Expression.Constant(true, typeof(bool)));
expressionList.Add(binaryExpression);
}
if (expressionList.Count == 0)
{
expressionList.Add(BinaryExpression.Constant(true));
}
// Aggregate List<Expression> data into single Expression
var returnExpression = expressionList.Skip(1).Aggregate(expressionList.First(), (expr1,expr2) => Expression.And(expr1,expr2));
return returnExpression;
// Generate Func<T,bool> - Expression.Lambda<Func<T,bool>>(returnExpression,parameterType).Compile();
}
}
public static class StringExtensions
{
public static bool Contains(this string source, string subString)
{
return source?.IndexOf(subString, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
public static class HashsetExtensions
{
public static bool Contains(this HashSet<string> source, string subString)
{
return source.Contains(subString,StringComparer.OrdinalIgnoreCase);
}
}
How it works ?
Only in simple equality cases you can use BinaryExpression like Expression.Equal, Expression.GreaterThan, which is shown for the property like "ShowAll"
For other cases like string / Array / List Contains, you need extension method, which can take two types and provide the result. A separate Contains for string to make it case neutral. Also for collection Hashset has a better choice, it has O(1) time complexity, unlike O(N) for an array
We use MethodCallExpression to call the extension methods
Finally we aggreagte all the expressions, which can be compiled to create Func<T,bool>
In case you need something like x => true, then BinaryExpression.Constant(true) is sufficient
I have provided a Sample implementation using the Store class that you have defined

Get requested column distinct values from list

I have a list with multiple columns. I want to filter the list based on the requested column name (column name will come as a parameter) with distinct values.
IList<obj1> objTemp= new List<obj1>();
for (int i = 0; i < 15; i++)
{
obj1 temp= new obj1();
temp.Name = "Name" + i;
temp.Age= "Age" + i;
temp.Company= "Company" + i;
objTemp.Add(temp);
}
var distinctTypeIDs = objTemp.Select(x => x.**{my requested column}**).Distinct();
You can use reflection for getting desired property by it's name:
var distinctTypeIDs = objTemp.Select(x => x.GetType().GetProperty("requested_column").GetValue(x))
.Distinct();
I've always been a fan of "mapping" a column to an anonymous method responsible for retrieving the contents of that column:
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
Console.WriteLine("Hello World");
var items = new List<SomeObject> {new SomeObject { Age = 10, Name = "Daniel", Company = "InCycle" },
{new SomeObject { Age = 20, Name = "Not Daniel", Company = "Not InCycle" }
}};
var result = Filter<int>(items, "Age");
Console.WriteLine(result.Last());
}
public static IEnumerable<T> Filter<T>(IEnumerable<SomeObject> items, string filterCriteria)
{
var mappings = new Dictionary<string, Func<IEnumerable<SomeObject>, IEnumerable<T>>>
{
{ "Age", filterItems => filterItems.Select(item => item.Age).Distinct().Cast<T>() },
{ "Name", filterItems => filterItems.Select(item => item.Name).Distinct().Cast<T>() },
{ "Company", filterItems => filterItems.Select(item => item.Company).Distinct().Cast<T>() }
};
return mappings[filterCriteria](items);
}
}
public class SomeObject
{
public int Age {get;set;}
public string Name {get;set;}
public string Company {get; set;}
}
The downside to this approach is that if you add additional properties, you could forget to add them to the filtering. Expressions are a solid approach as well.
One way is to use a method like this.
private IList<obj1> SortListAccordingToParameter(string filter)
{
if(filter == "Name")
return objTemp.Select(x => x.Name).Distinct();
else if(filter == "Age")
return objTemp.Select(x => x.Age).Distinct();
else if(filter == "Company")
return objTemp.Select(x => x.Company).Distinct();
}
If you know the type of the property you will be searching for, you could use expressions.
string propName = "Age";
var paramExpression = Expression.Parameter(typeof(Obj1));
// o =>
var memberExpression = Expression.Property(paramExpression, propName);
// o => o.Age
var lambdaExpression = Expression.Lambda<Func<Obj1, string>>(memberExpression, paramExpression);
// (o => o.Age)
var compiled = lambdaExpression.Compile();
IList<Obj1> objTemp = new List<Obj1>();
for (var i = 0; i < 15; i++) {
Obj1 temp = new Obj1();
temp.Name = "Name" + i;
temp.Age = "Age" + i;
temp.Company = "Company" + i;
objTemp.Add(temp);
}
var results = objTemp.Select(compiled);
// equivalent to objTemp.Select(o => o.Age), plus a delegate call and the time to
// compile the lambda.
I would probably wrap this up in a static class, like this:
static class Gen<TModel, TProp> {
public static Func<TModel, TProp> SelectorExpr(string propertyName) {
var pExpr = Expression.Parameter(typeof (TModel));
var mExpr = Expression.Property(pExpr, propertyName);
var lExpr = Expression.Lambda<Func<TModel, TProp>>(mExpr, pExpr);
return lExpr.Compile();
}
}
that way you can write your selector like:
var results = objTemp.Select(Gen<Obj1, string>.SelectorExpr(propName));
That seems a bit more clear to me what it is I'm doing, especially if I'm reading expression DOM code I wrote 6 months after.
public class Test
{
public string name { get; set; }
public string age { get; set; }
public string contact { get; set; }
public Test getName(string name)
{
List<Test> testList = new List<Test>();
testList.Add(new Test { name = "Developer", age = "24", contact = "99009900990" });
testList.Add(new Test { name = "Tester", age = "30", contact = "009900990099" });
return testList.Where(c => c.name == name).FirstOrDefault();
}
}
static void Main(string[] args)
{
Test testObj = new Test();
Test selectedObj = testObj.getName("Developer");
}

Searching all public variables of objects in List<T> for specific string

I'm trying to see if it is possible to search all public variables that have a .ToString() method of any given object with Linq.
Basically I want to supply two paramaters, a List<T> to search and a string and get returned a List<T> of objects that had one or more public variables that contained my string.
I can get all public properties of an object using
Type objectType = _data[0].GetType();
var properties = objectType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
var fields = objectType.GetFields(BindingFlags.Public | BindingFlags.Instance)
But I'm stuck on how I should use this to search the list of objects without looping over all objects and all their properties.
Here is a complete example:
(Note: I will edit this answer to take into account ToString() override)
EDIT: Done
public class Program
{
public class ToStringNotOverrided0 { }
public int Int { get; set; }
public string str;
public ToStringNotOverrided0 toStringNotOverrided;
public static IEnumerable<T> getMatches<T>(List<T> list, string search) {
if (search == null)
throw new ArgumentNullException("search");
return list.Select(x => new
{
X = x,
Props = x.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public),
Fields = x.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public),
})
.Where(x => x.Props.Any(p =>
{
var val = p.GetValue(x.X, null);
return val != null
&& val.GetType().GetMethod("ToString", Type.EmptyTypes).DeclaringType == val.GetType()
&& val.ToString().Contains(search);
})
|| x.Fields.Any(p =>
{
var val = p.GetValue(x.X);
return val != null
&& val.GetType().GetMethod("ToString", Type.EmptyTypes).DeclaringType == val.GetType()
&& val.ToString().Contains(search);
}))
.Select(x => x.X);
}
static void Main(string[] args)
{
List<Program> list = new List<Program>{
new Program { Int = 0, str = "foo bar" , toStringNotOverrided = new ToStringNotOverrided0()},
new Program { Int = 54, str = "foo 0 bar" , toStringNotOverrided = new ToStringNotOverrided0()},
new Program { Int = 12, str = "foo bar" , toStringNotOverrided = new ToStringNotOverrided0()},
new Program { Int = 720, str = "foo bar" , toStringNotOverrided = new ToStringNotOverrided0() }
};
foreach (var item in getMatches(list, "0"))
{
Debug.WriteLine("Int = " + item.Int
+ ", str = " + item.str
+ ", toStringNotOverrided = " + item.toStringNotOverrided);
}
}
}
var type = objectToCheck.GetType();
bool isMethodExist = type.GetMethod(methodName) != null;
if you have List<T>
var type = _data.GetType().GetGenericArguments()[0];
bool isMethodExist = type.GetMethod(methodName) != null;

How to filter collection by any property with reflection?

I have IEnumerable collection. I want to create such method:
public IEnumerable<object> Try_Filter(IEnumerable<object> collection, string property_name, string value)
{
//If object has property with name property_name,
// return collection.Where(c => c.Property_name == value)
}
Is it possible? I'm using C# 4.0.
Thanks!
Try this:
public IEnumerable<object> Try_Filter(IEnumerable<object> collection,
string property_name, string value)
{
var objTypeDictionary = new Dictionary<Type, PropertyInfo>();
var predicateFunc = new Func<Object, String, String, bool>((obj, propName, propValue) => {
var objType = obj.GetType();
PropertyInfo property = null;
if(!objTypeDictionary.ContainsKey(objType))
{
property = objType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(prop => prop.Name == propName);
objTypeDictionary[objType] = property;
} else {
property = objTypeDictionary[objType];
}
if(property != null && property.GetValue(obj, null).ToString() == propValue)
return true;
return false;
});
return collection.Where(obj => predicateFunc(obj, property_name, value));
}
Tested with:
class a
{
public string t { get; set;}
}
var lst = new List<Object> { new a() { t = "Hello" }, new a() { t = "HeTherello" }, new a() { t = "Hello" } };
var result = Try_Filter(lst, "t", "Hello");
result.Dump();
Although, this will be very slow for large collections

Categories

Resources