In MVC4 , I am providing Search box to the user to search any value in Table.
So I am implementing Generic Filter Condition at server side in C#
Need a help to combine multiple expressions to form single expression
Expression<Func<T, bool>>
For Example
Table Columns
MenuText, Role Name (Role.Name mapping), ActionName
Now If user entered in search box for ABC , which can be in any of the rows in shown columns, need to filter.
Model
public class Menu
{
public string MenuText {get;set;}
public Role Role {get;set;}
public string ActionName {get;set;}
}
public class Role
{
public string Name {get;set;}
}
So far I have implemented
/// <summary>
/// string[] properties property.Name (MenuText, ActionName), including deeper Mapping names such as (Role.Name)
/// </summary>
public static Expression<Func<T, bool>> FilterKey<T>(string filterText, params string[] properties)
{
ParameterExpression parameter = Expression.Parameter(typeof (T));
Expression[] propertyExpressions = properties.Select(
x => !string.IsNullOrEmpty(x) ? GetDeepPropertyExpression(parameter, x) : null).ToArray();
Expression<Func<T, bool>> predicate = PredicateBuilder.False<T>();
foreach (Expression expression in propertyExpressions)
{
var toLower = Expression.Call(expression, typeof(string).GetMethod("ToLower", System.Type.EmptyTypes));
var like = Expression.Call(toLower, typeof(string).GetMethod("Contains"), Expression.Constant(filterText.ToLower()));
//TODO: Combine expressions to form single Expression<Func<T, bool>> expression
}
return predicate;
}
/// <summary>
/// To Get Deeper Properties such as Role.Name Expressions
/// </summary>
private static Expression GetDeepPropertyExpression(Expression initialInstance, string property)
{
Expression result = null;
foreach (string propertyName in property.Split('.'))
{
Expression instance = result ?? initialInstance;
result = Expression.Property(instance, propertyName);
}
return result;
}
I have created a few search IQueryable extension methods that you should be able to use
Full blog post is here:
http://jnye.co/Posts/6/c%23-generic-search-extension-method-for-iqueryable
GitHub project is here (has a couple of extra extensions for OR searches:
https://github.com/ninjanye/SearchExtensions
public static class QueryableExtensions
{
public static IQueryable<T> Search<T>(this IQueryable<T> source, Expression<Func<T, string>> stringProperty, string searchTerm)
{
if (String.IsNullOrEmpty(searchTerm))
{
return source;
}
// The below represents the following lamda:
// source.Where(x => x.[property] != null
// && x.[property].Contains(searchTerm))
//Create expression to represent x.[property] != null
var isNotNullExpression = Expression.NotEqual(stringProperty.Body, Expression.Constant(null));
//Create expression to represent x.[property].Contains(searchTerm)
var searchTermExpression = Expression.Constant(searchTerm);
var checkContainsExpression = Expression.Call(stringProperty.Body, typeof(string).GetMethod("Contains"), searchTermExpression);
//Join not null and contains expressions
var notNullAndContainsExpression = Expression.AndAlso(isNotNullExpression, checkContainsExpression);
var methodCallExpression = Expression.Call(typeof(Queryable),
"Where",
new Type[] { source.ElementType },
source.Expression,
Expression.Lambda<Func<T, bool>>(notNullAndContainsExpression, stringProperty.Parameters));
return source.Provider.CreateQuery<T>(methodCallExpression);
}
}
This allows you to write something like:
string searchTerm = "test";
var results = context.Menu.Search(menu => menu.MenuText, searchTerm).ToList();
//OR for Role name
string searchTerm = "test";
var results = context.Menu.Search(menu => menu.Role.Name, searchTerm).ToList();
You might also find the following posts useful:
Search extension method that allows search accros multiple properties:
http://jnye.co/Posts/7/generic-iqueryable-or-search-on-multiple-properties-using-expression-trees
Search extension method that allows multiple or search terms on a property:
http://jnye.co/Posts/8/generic-iqueryable-or-search-for-multiple-search-terms-using-expression-trees
Thanks to NinjaNye , I have borrowed BuildOrExpression which resolved my problem
Here is the solution
public static Expression<Func<T, bool>> FilterKey<T>(string filterText, params string[] properties)
{
ParameterExpression parameter = Expression.Parameter(typeof (T));
Expression[] propertyExpressions = properties.Select(
x => !string.IsNullOrEmpty(x) ? GetDeepPropertyExpression(parameter, x) : null).ToArray();
Expression like= propertyExpressions.Select(expression => Expression.Call(expression, typeof (string).GetMethod("ToLower", Type.EmptyTypes))).Select(toLower => Expression.Call(toLower, typeof (string).GetMethod("Contains"), Expression.Constant(filterText.ToLower()))).Aggregate<MethodCallExpression, Expression>(null, (current, ex) => BuildOrExpression(current, ex));
return Expression.Lambda<Func<T, bool>>(like, parameter);
}
private static Expression BuildOrExpression(Expression existingExpression, Expression expressionToAdd)
{
if (existingExpression == null)
{
return expressionToAdd;
}
//Build 'OR' expression for each property
return Expression.OrElse(existingExpression, expressionToAdd);
}
private static Expression GetDeepPropertyExpression(Expression initialInstance, string property)
{
Expression result = null;
foreach (string propertyName in property.Split('.'))
{
Expression instance = result ?? initialInstance;
result = Expression.Property(instance, propertyName);
}
return result;
}
Related
For a generic filter I am implementing, I need to modify the following to accept the column name which will be searched in the Where method, something like:
public IQueryable<TEntity> GetEntities(string val)
{
TEntity entity = _DbContext.Set<TEntity>()
.Where(e => e.Col1.Contains(val));
return entities;
}
to be changed to
public IQueryable<TEntity> GetEntities(string val, string colName)
{
TEntity entity = _DbContext.Set<TEntity>()
.WhereContains(val, colName);
return entities;
}
colName is the name of a string column.
I looked at https://blog.jeremylikness.com/blog/dynamically-build-linq-expressions/ but could not modify the example there for my needs. The answer should be in the form of
public static IQueryable<TEntity> WhereContains<TEntity>(this IQueryable<TEntity> query, string value, string colName)
where TEntity : class
{
...
...
}
But I cant make it work...
OK, found a good reference and was able to modify it:
public static IQueryable<T> TextFilter<T>(IQueryable<T> source, string[] colNames, string[] terms)
{
if (colNames.Length == 0) return source;
// T is a compile-time placeholder for the element type of the query.
Type elementType = typeof(T);
// Get all the properties on this specific type for colNames.
List<PropertyInfo> props = new List<PropertyInfo>();
for (int i = 0; i < colNames.Length; i++)
{
PropertyInfo prop = elementType.GetProperties().Where(x => x.PropertyType == typeof(string) && x.Name.ToLower() == colNames[i].ToLower()).FirstOrDefault();
if (prop == null) { return source; }
props.Add(prop);
}
// Get the right overload of String.Contains. Can be replaced e.g. with "Contains"
MethodInfo containsMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!;
// Create a parameter for the expression tree:
// the 'x' in 'x => x.PropertyName.Contains("term")'
// The type of this parameter is the query's element type
ParameterExpression prm = Expression.Parameter(elementType);
// Map each property to an expression tree node
List<Expression> expressions = new List<Expression>();
for (int i = 0; i < colNames.Length; i++)
{
expressions.Add(
Expression.Call(
Expression.Property(
prm,
props.ElementAt(i)
),
containsMethod,
Expression.Constant(terms[i])
)
);
}
// Combine all the resultant expression nodes using ||
Expression body = expressions
.Aggregate(
(prev, current) => Expression.And(prev, current)
);
// Wrap the expression body in a compile-time-typed lambda expression
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, prm);
// Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
return source.Where(lambda);
}
I was not able to make this work as an extension, so calling it is done with:
var qry= QueryableExtensions.TextFilter(_crudApiDbContext.Set<TEntity>()
.Where(entity => entity.someColumn==someValue),
filters.Keys.ToArray(), filters.Values.ToArray());
List<TEntity> entities = await qry
.Skip(pageSize * (page - 1)).Take(pageSize).ToListAsync();
I have a function like this:
public CountryDto FindCountryByName(string name)
{
Country country = _countryRepository.GetAll().Where(g => g.Name.ToLower().Trim() == name.ToLower().Trim()).FirstOrDefault();
var dto = _mapper.Map<Country, CountryDto>(country);
return dto;
}
and it's referred to GetAll-function in the GenericRepository
public IEnumerable<T> GetAll()
{
return table.ToList();
}
Is it possible creating a function like this (in the GenericRepository)?
public IEnumerable<T> FindByName(string objname, string name)
{
return table.Where(t => t.GetType(objname) == name);
}
By example
Country country = _countryRepository.FindByName("CountryName", name);
and
AlbumTrack track = _albumtrackRepository.FindByName("SongTitle", songTitle);
I don't remember where I found this function, I'm using it to generate LINQ queries based on given property name, please note that you can change the search function "Equals" to "Contains" or "StartsWith" :
///<summary>
/// Create query that search in given property of class T for matching results with value
/// </summary>
public static IQueryable<T> CreateSearchQuery<T>(IQueryable<T> queryable, string PropertyName, string value) where T : class
{
IQueryable<T> query = queryable;
List<Expression> expressions = new List<Expression>();
ParameterExpression parameter = Expression.Parameter(typeof(T), "p");
MethodInfo Equals_Method = typeof(string).GetMethod("Equals", new[] { typeof(string) });
MethodInfo ToString_Method = typeof(object).GetMethod("ToString");
//Iterate through all properties Except inherited ones
foreach (PropertyInfo prop in typeof(T).GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.PropertyType == typeof(string) //properties of type string
|| x.Name == PropertyName))
{
ConstantExpression value_expression = Expression.Constant(value, typeof(string));
MemberExpression member_expression = Expression.PropertyOrField(parameter, prop.Name);
//Combine ToString() and Equals() methods
MethodCallExpression callChain = Expression.Call(Expression.Call(member_expression, ToString_Method), Equals_Method, value_expression);
expressions.Add(callChain);
}
if (expressions.Count == 0)
return query;
Expression or_expression = expressions[0];
for (int i = 1; i < expressions.Count; i++)
{
or_expression = Expression.OrElse(or_expression, expressions[i]);
}
Expression<Func<T, bool>> expression = Expression.Lambda<Func<T, bool>>(or_expression, parameter);
return query.Where(expression);
}
I am trying to perform a query against a list to give immediate results using an expression that is set elsewhere in the code, while a second thread goes off and uses it to get a full set of results from a database in a Linq query.
I know that the expression itself is OK as when I send it over the wire to the server side and apply it against an IQueryable then it will work. However, when applied on the client side the following error is produced:
System.InvalidOperationException: 'variable 'item' of type 'MissionControlSuite.Shared.Interface.Model.IBox' referenced from scope '', but it is not defined'
This code that performs the filtering:
public abstract class GridWithPaging<T> where T : class
{
List<T> _items
public Expression<Func<T, bool>> Filter { get; set; }
public ObservableCollection<IGridFilter<T>> Filters { get; set; }
// more code skipped for breveity
public void FiltersChanged(object sender, EventArgs e)
{
Expression<Func<T, bool>> combined = item => true;
foreach (var filter in Filters)
combined = Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(filter.Expression.Body, combined.Body),
combined.Parameters.Single());
Filter = combined;
NotifyPropertyChanged("Filter");
}
public void AddFilter(IGridFilter<T> filter)
{
Filters.Add(filter);
NotifyPropertyChanged("Filters");
}
private void DoFiltering(int start)
{
var newView = _items.Skip(start);
if(Filter != null)
newView = newView.AsQueryable().Where(Filter);
//some more code that acts on the filtered list
}
}
The expression is set elsewhere like this:
Expression<Func<IBox, bool>> expression = item => item.BoxId.Contains(v);
var filter = new GridFilter<IBox>()
{
Name = string.Format("{0} contains {1}", Property, Value),
Expression = expression
};
Grid.AddFilter(filter);
This answer will help you: Combining two expressions (Expression<Func<T, bool>>)
To summarize: The problem is that the parameters, although with the same name, are not the same instance of the ParameterExpression - which is included in the comment to that answer.
The solution is to use a Visitor (code copied over from linked post):
public static Expression<Func<T, bool>> AndAlso<T>(
this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var parameter = Expression.Parameter(typeof (T));
var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
var left = leftVisitor.Visit(expr1.Body);
var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
var right = rightVisitor.Visit(expr2.Body);
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(left, right), parameter);
}
private class ReplaceExpressionVisitor
: ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue;
public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
}
public override Expression Visit(Expression node)
{
if (node == _oldValue)
return _newValue;
return base.Visit(node);
}
}
Also note: the ORM provider (or another IQueryable you use) probably works because it translates that to text directly, whereas executing (Invokeing) this LambdaExpression causes the error as it's more strict, in a way. Some IQueryables can also accept simple string parameters and parse that on their own, which should indicate that they have a wider range of acceptable inputs.
I have a query and I need to apply n filters to it. However I have to use expressions. So far I have expression constructed for each filter, which works pretty good. Issue is I want to join these filters into one expression so I could use it as parameter for LINQ's .Where().
Filter code:
//Filters could slightly differ in functionality
private Expression<Func<T, bool>> StringPropertyContains<T>(Filter filter)
{
if (filter == null)
{
throw new ArgumentNullException(nameof(filter));
}
if (typeof(T).GetProperties().FirstOrDefault(pi => pi.Name.Equals(filter.PropertyName, StringComparison.OrdinalIgnoreCase)) == null)
{
throw new ArgumentNullException($"Type {typeof(T)} does not contain property {filter.PropertyName}");
}
var propertyInfo = typeof(T).GetProperty(filter.PropertyName);
var param = Expression.Parameter(typeof(T));
var member = Expression.MakeMemberAccess(param, propertyInfo);
var constant = Expression.Constant(filter.Value, typeof(filter.Type));
var methodInfo = typeof(filter.Type).GetMethod(filter.Method, new Type[] { typeof(filter.Type) });
var body = Expression.Call(member, methodInfo, constant);
return Expression.Lambda<Func<T, bool>>(body, param);
}
private Expression<Func<T, bool>> Filter<T>(T t)
{
//TODO join filters while each filter should have its different parameter
//the parameter is currently constructed in the object and is accessible via property Filter[] ParsedFilter
}
Filter class:
internal class Filter
{
public string Type { get; }
public string Method { get; }
public string PropertyName { get; }
public string Value { get; }
}
Desired usage:
IQueryable<T> q = query.Where(this.Filter)
To be honest I have pretty bad headache with this issue because I am pretty new to expressions. So thanks in regards for any help.
You can not pass a class as argument to a Where clause however you can use Linq Expressions to acheive what you want:
Expression<Func<T, bool>> filter = q => q.type == filter.Type && q.Value == filter.Value;
var x = query.Where(filter);
Although very similar to another question. Other question
I am wondering how to do that and support dot notation with nested objects. My current extension to IQueryable looks like this.
public static IQueryable<T> WherePropStringContains<T>(this IQueryable<T> query, string propertyName, string contains)
{
var parameter = Expression.Parameter(typeof(T), "x");
var propertyExpression = Expression.Property(parameter, propertyName);
var method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(contains, typeof(string));
var containsExpression = Expression.Call(propertyExpression, method, someValue);
var lmd = Expression.Lambda<Func<T, bool>>(containsExpression, parameter);
return query.Where(lmd);
}
I would like for the property name to support something like "User.Name.First" which if i wasn't using string and generic might look like query.where(x => x.User.Name.First.Contains(contains)
Thanks
Does this look similar to what you're looking for?
public static class Extensions {
public static IQueryable<T> WherePropStringContains<T>(this IQueryable<T> queryable, String name, String value) {
IEnumerable<String> propertyNames = name.Split('.');
return queryable.Where(x => ObjectChainHasProperties(x, propertyNames, value));
}
private static bool ObjectChainHasProperties(Object obj, IEnumerable<String> propertyNames, String value) {
IEnumerator<String> enumerator = propertyNames.GetEnumerator();
while (obj != null && enumerator.MoveNext()) {
obj = obj.GetType().GetProperties()
.Where(p => p.Name == enumerator.Current)
.FirstOrDefault()?
.GetValue(obj);
}
return (obj as String) == value;
}
}