Creating a dynamic query using IQueryable - c#

I'm trying to iterate for over an string array and dynamically create a IQueryable query. Its pretty straightforward but here's where I'm stuck
var query = context.QuestionsMetaDatas.AsQueryable();
var keywords=new List<string>(){ "Test1","Test2" };
foreach(var key in keywords)
{
query=query.Where(a=>a.Text.Contains(key));
}
Now the problem is that when the query gets generated its compiles to
select * from QuestionsMetaDatas where Text Like "Test1" AND Text Like "Test2"
Instead of AND I wanted the query to generate OR...Now how can I achieve this?

I have used predicate builder like Raphael suggested, it's just one file to include in your project, then your example becomes:
var keywords=new List<string>(){ "Test1","Test2" };
var predicate = PredicateBuilder.False<QuestionsMetaDatas>();
foreach (var key in keywords)
{
predicate = predicate.Or (a => a.Text.Contains (key));
}
var query = context.QuestionsMetaDatas.AsQueryable().Where(predicate);
Generating the OR query your are looking for.

Have you tried contains the other way?
var keywords=new List<int>(){ "Test1","Test2" };
query=query.Where(a=>keywords.Contains(a));
this is like an IN clause

you could look at predicate builder, or build your own expression (here a possble solution with a static extension method on IQueryable<QuestionsMetadatas> )
public static IQueryable<QuestionsMetaDatas> FilterText(this IQueryable<QuestionsMetaDatas> queryable, IEnumerable<string> keywords)
{
var entityType = typeof(QuestionsMetaDatas);
var parameter = Expression.Parameter(entityType, "a");
var containsMethod = typeof(string).GetMethod("Contains"
, new[] { typeof(string) });
var propertyExpression = Expression.Property(parameter, "Text");
Expression body = Expression.Constant(false);
foreach (var keyword in keywords)
{
var innerExpression = Expression.Call(propertyExpression, containsMethod, Expression.Constant(keyword));
body = Expression.OrElse(body, innerExpression);
}
var lambda = Expression.Lambda<Func<QuestionsMetaDatas, bool>>(body, new[] { parameter });
return queryable.Where(lambda);
}
where lambda will look like that :
a => ((False OrElse a.Text.Contains("firstKeyWord")) OrElse a.Text.Contains("secondKeyWord"))
and usage is
var query = context.QuestionsMetaDatas.AsQueryable();
var keywords=new List<string>(){ "Test1","Test2" };
query = query.FilterText(keywords);
or shorter
var query = context.QuestionsMetaDatas.AsQueryable().FilterText(new[]{"Test1", "Test2"});

Related

Search a term in multiple columns

I'd like to create an extension to search terms in multiple columns.
Terms are separated with space, each term must appears to at least one given column.
Here what I've done so far:
public static IQueryable<TSource> SearchIn<TSource>(this IQueryable<TSource> query,
string searchText,
Expression<Func<TSource, string>> expression,
params Expression<Func<TSource, string>>[] expressions)
{
if (string.IsNullOrWhiteSpace(searchText))
{
return query;
}
// Concat expressions
expressions = new[] { expression }.Concat(expressions).ToArray();
// Format search text
var formattedSearchText = searchText.FormatForSearch();
var searchParts = formattedSearchText.Replace('\'', ' ').Split(' ');
// Initialize expression
var pe = Expression.Parameter(typeof(TSource), "entity");
var predicateBody = default(Expression);
// Search in each expressions, put OR in between
foreach (var expr in expressions)
{
var exprBody = default(Expression);
// Search for each words, put AND in between
foreach (var searchPart in searchParts)
{
// Create property or field expression
var left = Expression.PropertyOrField(pe, ((MemberExpression)expr.Body).Member.Name);
// Create the constant expression with current word
var search = Expression.Constant(searchPart, typeof(string));
// Create the contains function
var contains = Expression.Call(left, typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), search);
// Check if there already a predicate body
if (exprBody == null)
{
exprBody = contains;
}
else
{
exprBody = Expression.And(exprBody, contains);
}
}
if (predicateBody == null)
{
predicateBody = exprBody;
}
else
{
predicateBody = Expression.OrElse(predicateBody, exprBody);
}
}
// Build the where method expression
var whereCallExpression = Expression.Call(
typeof(Queryable),
nameof(Queryable.Where),
new Type[] { query.ElementType },
query.Expression,
Expression.Lambda<Func<TSource, bool>>(predicateBody, new ParameterExpression[] { pe }));
// Apply the condition to the query and return it
return query.Provider.CreateQuery<TSource>(whereCallExpression);
}
It works well as long as given expressions are simple:
// It works well
query.SearchIn("foo", x => x.Column1, x => x.Column2);
But it does not work when trying to navigate through navigation properties:
// Not working
query.SearchIn("foo", x => x.Nav1.Column1);
It gives me an exception.
'Column1' is not a member of type 'Nav1'.
I understand the problem but I can't find the solution to pass through Nav1.
I need help with this one.
Instead of parsing lambda expression body just call it with given parameter:
var left = Expression.Invoke(expr, pe);
However it works only in EF Core.
In EF6 you would need to get property or field of each nested member like this:
var left = expr.Body.ToString()
.Split('.')
.Skip(1) //skip the original parameter name
.Aggregate((Expression)pe, (a, c) => Expression.PropertyOrField(a, c));
It will work only for simple lambdas like:
x => x.Prop1.Nav1
If that's not enough you would need some more advanced parsing algorithm with ExpressionVisitor for example.

Building a Linq to EF query to a variable list of keywords in a variable number of columns?

I am trying to come up with a utility method to build a Linq Query or Linq Predicate to add to an Linq to EF query to do search for all terms in a list of terms in a variable number of columns.
I am trying to use PredicateBuilder to build the where clause. With one search term and a fixed list of columns it is relatively easy.
The pseudo code that I am trying to work up looks like this so far:
private static Predicate<Project> CreateDynamicSearch(IEnumerable<strings> searchableColumns, string[] searchTerms)
{
var predicate = PredicateBuilder.True<Project>();
foreach (var columnName in searchableColumns)
{
foreach (var term in searchTerms)
{
predicate = predicate.And(a => a.**columnName**.Contains(term));
}
predicate = predicate.Or(predicate);
}
return predicate;
}
The biggest issue I have is handling the expression for the columnName. Previous advice was to use an expression tree but I do not understand how that works into this scenario.
** Update **
I've taken the code as you have it after the update. It builds but when I actually make the call it errors on the Extension.Property(param,columnName); line, with the error Instance property 'Name' is not defined for type 'System.Func`2[Myclass,System.Boolean]' message. The columnName = "Name"
** Update 2 **
The way it's called:
var test = CreateDynamicSearch<Func<Project, bool>>(searchCols, searchTerms);
You can build expression for predicate yourself, in this case it's relatively easy:
private static Expression<Func<T, bool>> CreateDynamicSearch<T>(IEnumerable<string> searchableColumns, string[] searchTerms) {
// start with true, since we combine with AND
// and true AND anything is the same as just anything
var predicate = PredicateBuilder.True<T>();
foreach (var columnName in searchableColumns) {
// start with false, because we combine with OR
// and false OR anything is the same as just anything
var columnFilter = PredicateBuilder.False<T>();
foreach (var term in searchTerms) {
// a =>
var param = Expression.Parameter(typeof(T), "a");
// a => a.ColumnName
var prop = Expression.Property(param, columnName);
// a => a.ColumnName.Contains(term)
var call = Expression.Call(prop, "Contains", new Type[0], Expression.Constant(term));
columnFilter = columnFilter.Or(Expression.Lambda<Func<T, bool>>(call, param));
}
predicate = predicate.And(columnFilter);
}
return predicate;
}
In response to comment
I was just curious if there was some way you could combine the
expression created by Expression.Property(param, columnName) with the
one the compiler generates for (string s) -> s.Contains(term)
You can do that with (for example) like this:
// a =>
var param = Expression.Parameter(typeof(T), "a");
// a => a.ColumnName
var prop = Expression.Property(param, columnName);
// s => s.Contains(term)
Expression<Func<string, bool>> contains = (string s) => s.Contains(term);
// extract body - s.Contains(term)
var containsBody = (MethodCallExpression)contains.Body;
// replace "s" parameter with our property - a.ColumnName.Contains(term)
// Update accepts new target as first parameter (old target in this case is
// "s" parameter and new target is "a.ColumnName")
// and list of arguments (in this case it's "term" - we don't need to update that).
//
var call = containsBody.Update(prop, containsBody.Arguments);
columnFilter = columnFilter.Or(Expression.Lambda<Func<T, bool>>(call, param));

Convert foreach to linq expression with select

I have expression:
var newValues = MetarDecoder.Decode(telegram)
.OfType<MeteoParameter<decimal>>()
.Select(parameter => MeteoParameterFactory
.Create(parameter.ParameterId, parameter.DateTime.ToLocalTime(), parameter.Status, parameter.Value))
.ToList();
MeteoParameterFactory cannot be changed for some reasons, just take it as it is.
MeteoParameter also have string Info property.
I need to copy Info from old parameter to MeteoParameterFactory.Create() result.
Without LINQ it looks like:
var val = MetarDecoder.Decode(telegram).OfType<MeteoParameter<decimal>>().ToList();
foreach (var param in val)
{
var parameter = MeteoParameterFactory.Create(param.ParameterId, param.DateTime.ToLocalTime(), param.Status, param.Value);
parameter.Info = param.Info;
newValues.Add(parameter);
}
So, is there any way to add this part in LINQ expression shown below?
In Select you can create an anonymous function that returns the parameter created inside of it.
var newValues = MetarDecoder.Decode(telegram)
.OfType<MeteoParameter<decimal>>()
.Select(param => {
var parameter = MeteoParameterFactory.Create(param.ParameterId, param.DateTime.ToLocalTime(), param.Status, param.Value);
parameter.Info = param.Info;
return parameter;
}).ToList();
var val = MetarDecoder
.Decode(telegram)
.OfType<MeteoParameter<decimal>>()
.ToList()
.ForEach(param =>
{
var parameter = MeteoParameterFactory.Create(param.ParameterId, param.DateTime.ToLocalTime(), param.Status, param.Value);
parameter.Info = param.Info;
newValues.Add(parameter);
});

Building Expression Tree Using a Parameter's Indexer

Given a class that has a property that is a Dictionary
public class Product
{
public Dictionary<string, string> Attributes { get { return attributes; } }
private Dictionary<string, string> attributes = new Dictionary<string, string>();
}
I want to be able to match products in a list of products based on criteria that are retrieved from a data store that are in the format of
Brand == Tyco
Color != Blue
My current approach is to construct an expression from the filter, and then pass that expression as the parameter to a LINQ Where method call like so
products = products.Where(myConstructedExpression);
where myConstructedExpression would normally be a lamda expression that looks like
p => p.Attributes[attribute] == value
I have assembled the following code for testing purposes, but it always fails the call to lambda.Compile() regardless of what I have tried for he left expression.
Dictionary<string, ExpressionType> expressionType = new Dictionary<string, ExpressionType>();
expressionType.Add("==", ExpressionType.Equal);
expressionType.Add("!=", ExpressionType.NotEqual);
string filter = "Brand == Tyco";
string[] fields = filter.Split(' ');
string attribute = fields[0];
string op = fields[1];
string value = fields[2];
Product product = new Product();
product.Attributes.Add("Brand", "Tyco");
var parameter = Expression.Parameter(typeof(Product), "p");
var left = /***** THIS IS WHAT I AM FAILING TO CONSTRUCT PROPERLY ********/
var right = Expression.Constant(value);
var operation = Expression.MakeBinary(expressionType[op], left, right);
var lambda = Expression.Lambda<Func<Product, bool>>(operation, parameter);
var result = lambda.Compile()(product);
Questions
Is this even a reasonable approach, and, if so,
How do I construct the left expression?
So to get p => p.Attributes["Brand"] <someoperator> "Tyco", you can do this.
The "trick", to work with indexed types, is to use their Item property (you could also work with the get_item method)
var parameter = Expression.Parameter(typeof(Product), "p");
Expression left = Expression.Property(parameter, "Attributes");
left = Expression.Property(left, "Item", new Expression[] { Expression.Constant(attribute) });
EDIT
the version with the IDictionary.ContainsKey(<value>) test
really step by step, but I think this makes things clearer at first.
//left part of lambda, p
var parameter = Expression.Parameter(typeof(Product), "p");
//right part
//p.Attributes
Expression left = Expression.Property(parameter, "Attributes");
var method = typeof(IDictionary<string, string>).GetMethod("ContainsKey");
//p.Attributes.ContainsKey("Brand");
Expression containsExpression = Expression.Call(left, method, Expression.Constant(attribute));
//p.Attributes.Item["Brand"]
Expression keyExpression= Expression.Property(left, "Item", new Expression[] { Expression.Constant(attribute) });
//"Tyco"
var right = Expression.Constant(value);
//{p => IIF(p.Attributes.ContainsKey("Brand"), (p.Attributes.Item["Brand"] == "Tyco"), False)}
Expression operation = Expression.Condition(
containsExpression,
Expression.MakeBinary(expressionType[op], keyExpression, right),
Expression.Constant(false));
var lambda = Expression.Lambda<Func<Product, bool>>(operation, parameter);

How to produce a Subquery using non-generic Lambda

How would you translate the following generic Lambda function into a lambda expression :
context.AssociateWith<Product>(p => p.Regions.Where(r => r.Country == 'Canada')
I'm trying to create a full lambda expression without any <T> or direct call. Something like :
void AddFilter(ITable table, MetaDataMember relation)
{
var tableParam = Expression.Parameter(table.ElementType, "e");
var prop = Expression.Property(tableParam, relation.Name);
var func = typeof(Func<,>).MakeGenericType(table.ElementType, relation.type)
var exp = Expression.Lambda(func, prop, tableParam);
}
This will produce e.Regions... but I'm unable to get the Where part from there...
I know I'm very late in the game with my answer and likely this is not the exact solution you are looking for (still uses the frequently), but maybe it will help you and others building their expression:
/*
example: Session.Query.Where(m => m.Regions.Where(f => f.Name.Equals("test")))
*/
var innerItem = Expression.Parameter(typeof(MyInnerClass), "f");
var innerProperty = Expression.Property(innerItem, "Name");
var innerMethod = typeof(string).GetMethod("Equals", new[] { typeof(string) });
var innerSearchExpression = Expression.Constant(searchString, typeof(string));
var innerMethodExpression = Expression.Call(innerProperty, innerMethod, new[] { innerSearchExpression });
var innerLambda = Expression.Lambda<Func<MyInnerClass, bool>>(innerMethodExpression, innerItem);
var outerItem = Expression.Parameter(typeof(MyOuterClass), "m");
var outerProperty = Expression.Property(outerItem, info.Name);
/* calling a method extension defined in Enumerable */
var outerMethodExpression = Expression.Call(typeof(Enumerable), "Where", new[] { typeof(MyInnerClass) }, outerProperty, innerLambda);
var outerLambda = Expression.Lambda<Func<MyOuterClass, bool>>(outerMethodExpression, outerItem);
query = query.Where(outerLambda);
Based on an answer posted here: Creating a Linq expression dynamically containing a subquery.
Try this, it's not pretty but it gives you a valid expression for the whole structure. You could define the inner lambda as an expression but you would still have to compile it before you could pass it to Where(), so for the purposes of this answer it seems redundant.
Expression<Func<Product, IEnumerable<Region>>> getRegions =
p => p.Regions.Where(r => r.Country == "Canada");

Categories