I have a Linq Expression, which may be altered depending on certain conditions. An example of what I would like to do (left blank the bit I am not sure about):
If you Get
method retrives the data and returns in memory objects the you could do so
Expression<Func<Project, bool>> filter = (Project p) => p.UserName == "Bob";
if(showArchived) {
filter = (Project p) => p.UserName == "Bob" && p.Archived;
}
IEnumerable<Project> projects = unitOfWork.ProjectRepository.Get(filter);
EDIT
Just to point out. When you use .ToList()
method it enumerates the Queryable
, i.e. makes a database request.
Get rid of ToList()
and you'll be just fine.
If I understand the question, then most likely here's the problem:
IEnumerable<Project> projects = unitOfWork.ProjectRepository.Get(filter);
Any work on projects
is going to be using Enumerable
, not Queryable
; it should probably be:
IQueryable<Project> projects = unitOfWork.ProjectRepository.Get(filter);
if(showArchived)
{
projects = projects.Where(p => p.Archived);
}
The latter is composable, and .Where
should work as you expect, building up a more restrictive query before sending it to the server.
Your other option is to rewrite the filter to combine before sending:
using System;
using System.Linq.Expressions;
static class Program
{
static void Main()
{
Expression<Func<Foo, bool>> filter1 = x => x.A > 1;
Expression<Func<Foo, bool>> filter2 = x => x.B > 2.5;
// combine two predicates:
// need to rewrite one of the lambdas, swapping in the parameter from the other
var rewrittenBody1 = new ReplaceVisitor(
filter1.Parameters[0], filter2.Parameters[0]).Visit(filter1.Body);
var newFilter = Expression.Lambda<Func<Foo, bool>>(
Expression.AndAlso(rewrittenBody1, filter2.Body), filter2.Parameters);
// newFilter is equivalent to: x => x.A > 1 && x.B > 2.5
}
}
class Foo
{
public int A { get; set; }
public float B { get; set; }
}
class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
}
Or re-written in a way to allow convenient usage:
using System;
using System.Linq.Expressions;
static class Program
{
static void Main()
{
Expression<Func<Foo, bool>> filter = x => x.A > 1;
bool applySecondFilter = true;
if(applySecondFilter)
{
filter = Combine(filter, x => x.B > 2.5);
}
var data = repo.Get(filter);
}
static Expression<Func<T,bool>> Combine<T>(Expression<Func<T,bool>> filter1, Expression<Func<T,bool>> filter2)
{
// combine two predicates:
// need to rewrite one of the lambdas, swapping in the parameter from the other
var rewrittenBody1 = new ReplaceVisitor(
filter1.Parameters[0], filter2.Parameters[0]).Visit(filter1.Body);
var newFilter = Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(rewrittenBody1, filter2.Body), filter2.Parameters);
return newFilter;
}
}
class Foo
{
public int A { get; set; }
public float B { get; set; }
}
class ReplaceVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public ReplaceVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
}
I think you want to combine filters this way:
var myFilters = new List<Expression<Func<Customer, bool>>>();
myFilters.Add(c => c.Name.StartsWith("B"));
myFilters.Add(c => c.Orders.Count() == 3);
if (stranded)
{
myFilters.Add(c => c.Friends.Any(f => f.Cars.Any())); //friend has car
}
Expression<Func<Customer, bool>> filter = myFilters.AndTheseFiltersTogether();
IEnumerable<Customer> thoseCustomers = Data.Get(filter);
This code will allow you to combine your filters.
public static Expression<Func<T, bool>> OrTheseFiltersTogether<T>(params Expression<Func<T, bool>>[] filters)
{
return filters.OrTheseFiltersTogether();
}
public static Expression<Func<T, bool>> OrTheseFiltersTogether<T>(this IEnumerable<Expression<Func<T, bool>>> filters)
{
if (!filters.Any())
{
Expression<Func<T, bool>> alwaysTrue = x => true;
return alwaysTrue;
}
Expression<Func<T, bool>> firstFilter = filters.First();
var body = firstFilter.Body;
var param = firstFilter.Parameters.ToArray();
foreach (var nextFilter in filters.Skip(1))
{
var nextBody = Expression.Invoke(nextFilter, param);
body = Expression.OrElse(body, nextBody);
}
Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(body, param);
return result;
}
public static Expression<Func<T, bool>> AndTheseFiltersTogether<T>(params Expression<Func<T, bool>>[] filters)
{
return filters.AndTheseFiltersTogether();
}
public static Expression<Func<T, bool>> AndTheseFiltersTogether<T>(this IEnumerable<Expression<Func<T, bool>>> filters)
{
if (!filters.Any())
{
Expression<Func<T, bool>> alwaysTrue = x => true;
return alwaysTrue;
}
Expression<Func<T, bool>> firstFilter = filters.First();
var body = firstFilter.Body;
var param = firstFilter.Parameters.ToArray();
foreach (var nextFilter in filters.Skip(1))
{
var nextBody = Expression.Invoke(nextFilter, param);
body = Expression.AndAlso(body, nextBody);
}
Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(body, param);
return result;
}
This all depends on how does ProjectRepository.Get()
behave and what it returns. The usual way (for example, LINQ to SQL does something like this) is that it returns a IQueryable<T>
and lets you (among other things) add more Where()
clauses before sending it to the server in the form of one SQL query, with all the Where()
clauses included. If this is the case, Mark's solution (use IQuerybale<T>
) will work for you.
But if the Get()
method executes the query based on the filter
immediately, you need to pass it the whole filter in the expression. To do that, you can use PredicateBuilder.