Apply inner expression filter on IQueryable - c#

Suppose I have the following objects:
public class Source { public string Name; }
public class Target { public string Name; }
public class Result
{
public Source SourceObj;
public Target TargetObj;
}
Now, obtaining IQueryable<Result> from somewhere I would like to prepare expression filter for it just having Target filter as expression: Expression<Func<Target, bool>> filter. The filter method signature looks like this:
public Expression<Func<Result, bool>> Filter(IQueryable<Result> collection, Expression<Func<Target, bool>> targetFilter)
{
in result expression: "in given collection select items where their property TargetObj satisfies targetFilter"
}
Any suggestions will be very appreciated. Thanks

I'm not entierly sure I understood your goal correctly, but here's what I think is probably the thing you want.
With the linq Select, you can map an element from your source collection. So here we are mapping the result to the target and then apply your target filter.
public IQueryable<Target> GetFilteredTargets(IQueryable<Result> collection, Expression<Func<Target, bool>> targetFilter)
{
return collection.Select(result => result.Target).Where(targetFilter);
}

If you really want an expression as a return for your Filter method, you don't need IQueryable<Result> collection.
public Expression<Func<Result, bool>> Filter(Expression<Func<Target, bool>> targetFilter)
{
Func<Target, bool> func = targetFilter.Compile();
Expression<Func<Result, bool>> resultExpression = r => func(r.TargetObj);
return resultExpression;
}
usage:
var results = new List<Result>()
{
new Result() { TargetObj = new Target() { Name = "A" }},
new Result() { TargetObj = new Target() { Name = "B" }},
new Result() { TargetObj = new Target() { Name = "C" }},
};
var expression = Filter(r => r.Name == "B");
var func = expression.Compile();
var filteredResults = results.Where(r => func(r));
The sample filtered result here would return the target with B. It would make more sense for me to use Linq Where directly based on my understanding of your goal.
EDIT: Since you've added a new factor into your question here is how you can add the target filter to the base filter.
public Expression<Func<Result, bool>> Filter(Expression<Func<Result, bool>> baseFilter, Expression<Func<Target, bool>> targetFilter)
{
Func<Result, bool> baseFunc = baseFilter.Compile();
Func<Target, bool> targetFunc = targetFilter.Compile();
Expression<Func<Result, bool>> resultExpression = r => baseFunc(r) && targetFunc(r.TargetObj);
return resultExpression;
}

Related

Filter IQueryable which contains (%like%) any of the items in the List<string>

I want to filter an IQueryable/List using List without hitting the database. The IQueryable result should include all result containing any of the strings in the List and the length of the list is unspecified.
myQueryable = myQueryable.Where(filelist => filelist.Location.Contains(filterList[0]) || filelist.Location.Contains(filterList[1]) || filelist.Location.Contains(filterList[N])...);
I'm using ASP.NET Core 3 and will use the IQueryable to hit the databse in a later phase using Entity Framework.
I've tried these two codes which did not work and my IQuery works fine if i exclude my trial codes (for filtering the attribute, location).
workOrders = workOrders.Where(filelist => filterList.Contains(filelist.Location)); //Returns only exact match
workOrders = workOrders.Where(filelist => filterList.Any(filter => filelist.Location.ToUpperInvariant().Contains(filter.ToUpperInvariant()))); //Returns error
Expression<Func<Workorder, bool>> predicate = filelist => false;
foreach (var filter in filterList)
{
Expression<Func<Workorder, bool>> orPredicate = filelist =>
filter.Contains(filelist.Location);
var body = Expression.Or(predicate.Body, orPredicate.Body);
predicate = Expression.Lambda<Func<Workorder, bool>>(body,
predicate.Parameters[0]);
}
workOrders = workOrders.Where(predicate); //Returns Error
class Workorder //Database Model
{
public string SiteId { get; set; }
public string Location { get; set; }
}
List<string> filterList //List to be used as filter
{
"filterOne",
"filterTwo",
"filterN"
};
The second code i ran gives me an error which i can get nothing out of.
System.InvalidOperationException: When called from 'VisitLambda', rewriting a node of type 'System.Linq.Expressions.ParameterExpression' must return a non-null value of the same type. Alternatively, override 'VisitLambda' and change it to not visit children of this type.
Third code i ran gives me this error,
System.InvalidOperationException: Operation is not valid due to the current state of the object.
at Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateWhere(ShapedQueryExpression source, LambdaExpression predicate)
I have definitely not tested this... but I don't think you can do it in a one-liner if you pretend EF to parse it afterwards.
However, something like this should be fine:
Expression<Func<WorkOrder, bool>> predicate = filelist => false;
foreach(var filter in filterList) {
Expression<Func<WorkOrder, bool>> orPredicate = filelist => filter.Contains(filelist.Location);
var body = Expression.Or(predicate.Body, orPredicate.Body);
predicate = Expression.Lambda<Func<WorkOrder,bool>>(body, predicate.Parameters[0]);
}
And then:
myQueryable = myQueryable.Where(predicate);
We are basically just telling it:
.Where(filelist => false || filterList[0].Contains(filelist.Location) || filterList[1].Contains(filelist.Location) || ...);
Edit
Still untested on EF, but at least the syntax seems to be ok: https://dotnetfiddle.net/Htr7Zr
It took me a while but i used your example to construct an expression tree that worked.
List<filterList> filterList //List to be used as filter
{
"filterOne",
"filterTwo",
"filterN"
}
class Workorder //Database Model
{
public string SiteId { get; set; }
public string Location { get; set; }
}
static Expression<Func<T, bool>> GetExpression<T>(string propertyName, List<string> propertyValue)
{
Expression orExpression = null;
var parameterExp = Expression.Parameter(typeof(T), "type");
var propertyExp = Expression.Property(parameterExp, propertyName);
foreach (string searchTerm in propertyValue)
{
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(searchTerm, typeof(string));
var containsMethodExp = Expression.Call(propertyExp, method, someValue);
if(orExpression == null) //to handle intial phase when left expression = null
{
orExpression = Expression.Call(propertyExp, method, someValue);
}else
{
orExpression = Expression.Or(orExpression, containsMethodExp);
}
return Expression.Lambda<Func<T, bool>>(orExpression, parameterExp);
}
myQueryable = myQueryable.Where(GetExpression<Workorder>(property, filterList));
This Equals:
myQueryable = myQueryable.Where(filelist => filelist.Location.Contains(filterList[0]) || filelist.Location.Contains(filterList[1]) || filelist.Location.Contains(filterList[N])...);

How to solve LINQ to Entity query duplication when the queries only differ by a property?

I have two DbSets, Foo and Bar. Foo has an identifying string property, FooName, and Bar has an identifying string property, BarName.
I am designing a very simple search feature, where a user's query term can either be equal to, or contained in the identifying name.
So I have two methods (heavily simplified):
public ActionView SearchFoo(string query)
{
var equalsQuery = db.Foo.Where(f => f.FooName.Equals(query));
var containsQuery = db.Foo.Where(f => f.FooName.Contains(query)).Take(10); // Don't want too many or else a search for "a" would yield too many results
var result = equalsQuery.Union(containsQuery).ToList();
... // go on to return a view
}
public ActionView SearchBar(string query)
{
var equalsQuery = db.Bar.Where(f => f.BarName.Equals(query));
var containsQuery = db.Bar.Where(f => f.BarName.Contains(query)).Take(10); // Don't want too many or else a search for "a" would yield too many results
var result = equalsQuery.Union(containsQuery).ToList();
... // go on to return a view
}
Clearly I want some helper method like so:
public IList<T> Search<T>(string query, DbSet<T> set)
{
var equalsQuery = set.Where(f => ???.Equals(query));
var containsQuery = set.Where(f => ???.Contains(query)).Take(10); // Don't want too many or else a search for "a" would yield too many results
var result = equalsQuery.Union(containsQuery).ToList();
... // go on to return a view
}
I originally tried to add a Func<T, string> to the Search parameters, where I could use f => f.FooName and b => b.BarName respectively, but LINQ to Entities doesn't support a lambda expression during the execution of the query.
I've been scratching my head as to how I can extract this duplication.
You can achieve this with Expression<Funt<T,string>>
public IList<T> Search<T>(string query, DbSet<T> set, Expression<Func<T, string>> propExp)
{
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
ConstantExpression someValue = Expression.Constant(query, typeof(string));
MethodCallExpression containsMethodExp =
Expression.Call(propExp.Body, method, someValue);
var e = (Expression<Func<T, bool>>)
Expression.Lambda(containsMethodExp, propExp.Parameters.ToArray());
var containsQuery = set.Where(e).Take(10);
BinaryExpression equalExpression = Expression.Equal(propExp.Body, someValue);
e = (Expression<Func<T, bool>>)
Expression.Lambda(equalExpression, propExp.Parameters.ToArray());
var equalsQuery = set.Where(e);
var result = equalsQuery.Union(containsQuery).ToList();
}
Then you'll call it:
Search ("myValue", fooSet, foo=>foo.FooName);
if you can have a static method, then you could have it as an extension method:
public static IList<T> Search<T>(this DbSet<T> set,
string query, Expression<Func<T, string>> propExp)
And call it:
FooSet.Search ("myValue", foo=>foo.FooName);
Here's one way to do it.
First you need a helper method to generate the Expression for you:
private Expression<Func<T, bool>> GetExpression<T>(string propertyName, string propertyValue, string operatorName)
{
var parameterExp = Expression.Parameter(typeof(T));
var propertyExp = Expression.Property(parameterExp, propertyName);
MethodInfo method = typeof(string).GetMethod(operatorName, new[] { typeof(string) });
var someValue = Expression.Constant(propertyValue, typeof(string));
var methodExp = Expression.Call(propertyExp, method, someValue);
return Expression.Lambda<Func<T, bool>>(methodExp, parameterExp);
}
This is how you can use this method, propertyName would be FooName and BarName:
public IList<T> Search<T>(string propertyName, string query, DbSet<T> set)
{
var equalsQuery = set.Where(GetExpression<T>(propertyName, query, "Equals"));
var containsQuery = set.Where(GetExpression<T>(propertyName, query, "Contains")).Take(10); // Don't want too many or else a search for "a" would yield too many results
var result = equalsQuery.Union(containsQuery).ToList();
return result;
}
You can create interface:
public interface IName
{
string Name { get; set; }
}
Then implicitly implement IName interface in both entities.
public class Bar : IName { ... }
public class Foo : IName { ... }
And then change your method as:
public IList<T> SearchByName<T>(string query, DbSet<T> set)
where T: class, IName
{
var equalsQuery = set.Where(f => f.Name.Equals(query));
var containsQuery = set.Where(f => f.Name.Contains(query)).Take(10); // Don't want too many or else a search for "a" would yield too many results
var result = equalsQuery.Union(containsQuery).ToList();
... // go on to return a view
}
You could overide your ToString() method and use that in the query
public class foo
{
public string FooName
{
get;
set;
}
public override string ToString()
{
return FooName;
}
}
public class Bar
{
public string BarName
{
get;
set;
}
public override string ToString()
{
return BarName;
}
}
public IList<T> Search<T>(string query, DbSet<T> set)
{
var equalsQuery = set.AsEnumerable().Where(f => f.ToString().Equals(query));
var containsQuery = set.AsEnumerable().Where(f => f.ToString().Contains(query)).Take(10);
var result = equalsQuery.Union(containsQuery).ToList(); . . . // go on to return a view
}

How to join multiple MethodCallExpressions into one Expression?

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);

How to set the lambda if the properties won't be known until runtime?

I'm trying to make a generic method that I can use anywhere within my application.
Here is my method:
public T GetEntry(ObjectId id)
{
IMongoCollection<T> collections = db.GetCollection<T>(database);
var getObj = collections.Find( ).FirstOrDefault(); // something like x => x.id == id
return getObj;
}
Find takes a lambda expression that will specify the constraints but I can't set it because the properties won't be known until runtime. How do I set it up?
Do you want to change the search expression at the caller? In that case it's probably easiest to just pass in the expression from the caller. Something like this:
public T GetEntry<T>(ObjectId id, Expression<Func<T, bool>> predicate)
{
IMongoCollection<T> collections = db.GetCollection<T>(database);
var getObj = collections.Find(predicate).FirstOrDefault(); // Call passed in predicate
return getObj;
}
Then when you call the function you can do something like:
var person = GetEntry<Person>(id, person => person.Id == id);
You can use Interface for this solution
public Interface IEntity
{
int Id {set ;get}
}
class EntryManagement<T> where T : IEntity
{
public T GetEntry(ObjectId id)
{
IMongoCollection<T> collections = db.GetCollection<T>(database);
var getObj = collections.Find(x => x.Id == id).FirstOrDefault();
return getObj;
}
}
or you can create your lambda expression dynamically at runtime
public T GetEntry(ObjectId id)
{
IMongoCollection<T> collections = db.GetCollection<T>(database);
var parameterExpression = Expression.Parameter(typeof(T), "object");
var propertyOrFieldExpression = Expression.PropertyOrField(parameterExpression, "Id");
var equalityExpression = Expression.Equal(propertyOrFieldExpression, Expression.Constant(id, typeof(int)));
var lambdaExpression = Expression.Lambda<Func<T, bool>>(equalityExpression, parameterExpression);
var getObj = collections.Find(lambdaExpression).FirstOrDefault();
return getObj;
}

Linq parametrizable group by with 2 properties

I'm trying to do a LinQ request with a group by where a parameter is parametrizable by an Expression ( (Expression<Func<CompanyModel,TKey>> myGroupingProperty) and the other one is hard coded. But even if my code compile I get an error that linq does not support lambdas. Would you have any idea how to do this request?
Here is the code sample:
public List<timelineResult> getTimelinebyCompany<TKey>(Expression<Func<CompanyModel,TKey>> myGroupingProperty, Filter item)
{
int cntToBeSureThatTheQueryExecuteAtLeastOneTime = 0;
List<timelineResult> toto = new List<timelineResult>();
using (var db = new fintechDbContext())
{
while (cntToBeSureThatTheQueryExecuteAtLeastOneTime == 0)
{
toto = (from p in db.companyDBSET
select p).GroupBy(p=> new {p.Founded_Year, myGroupingProperty})
.Select(o => new timelineResult{ year = o.Key.Founded_Year, cluster = o.myGroupingProperty.ToString(), count = o.Count() })
.OrderBy(o => o.year).ToList();
cntToBeSureThatTheQueryExecuteAtLeastOneTime++;
}
}
return toto;
}
What you are seeking for is doable, but not the way you tried because the passed lambda expression cannot be used directly inside another lambda expression.
You should start first by creating a generic class to hold the grouping key. It's similar to system provided Tuple, but has parameterless constructor and simple property get/setters to conform to the EF projection rules:
public class GroupKey<K1, K2>
{
public K1 Key1 { get; set; }
public K2 Key2 { get; set; }
}
Then you need to build dynamically lambda expression like this
Expression<Func<T, K1, K2>> keySelector = x =>
new GroupKey<K1, K2> { Key1 = x.Prop1, Key2 = x.Prop2 };
In order to do that, you'll need some Expression helpers:
public static class ExpressionUtils
{
public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
{
return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
}
class ParameterReplacer : ExpressionVisitor
{
public ParameterExpression Source;
public Expression Target;
protected override Expression VisitParameter(ParameterExpression node)
{
return node == Source ? Target : base.VisitParameter(node);
}
}
}
and you can encapsulate the grouping part in a custom extension method:
public static class QueryableExtensions
{
public static IQueryable<IGrouping<GroupKey<K1, K2>, T>> GroupByPair<T, K1, K2>(this IQueryable<T> source, Expression<Func<T, K1>> keySelector1, Expression<Func<T, K2>> keySelector2)
{
var parameter = keySelector1.Parameters[0];
var key1 = keySelector1.Body;
var key2 = keySelector2.Body.ReplaceParameter(keySelector2.Parameters[0], parameter);
var keyType = typeof(GroupKey<K1, K2>);
var keySelector = Expression.Lambda<Func<T, GroupKey<K1, K2>>>(
Expression.MemberInit(
Expression.New(keyType),
Expression.Bind(keyType.GetProperty("Key1"), key1),
Expression.Bind(keyType.GetProperty("Key2"), key2)),
parameter);
return source.GroupBy(keySelector);
}
}
Finally, the essential part of your method becomes like this:
toto = db.companyDBSET
.GroupByPair(p => p.Founded_Year, myGroupingProperty)
.Select(g => new timelineResult
{
year = g.Key.Key1,
cluster = g.Key.Key2.ToString(),
count = g.Count()
})
.OrderBy(o => o.year)
.ToList();

Categories