In light of trailmax's comment below, yes, the original answer below does load all data into memory since it's exposing chained enumerables.
There are two issues that arise when trying to chain IQueryable
calls together.
- EF does not know how to translate the resulting
.Invoke()
call
- If you try to maintain a naming convention like
.Where()
, you will end up picking the wrong extension since IDeletable
only exposes a single column on which you can build a predicate.
The following relies on the LinqKit library to chain expressions and the syntax is no longer fluid, but it does avoid any immediate execution.
var users = context.Users.WhereIsNotDeleted(x => x.Id > 0).ToList();
public static class Extension
{
public static IEnumerable<T> WhereIsNotDeleted<T>(this IQueryable<T> source,
Expression<Func<T, bool>> predicate) where T : IDeletable
{
var query = source.AsExpandable().Where(x => !x.IsDeleted);
return query.Where(predicate);
}
}
Assuming that IDeletable
guarantees IsDeleted
exists, you can create an extension method that always performs this check for you.
public interface IDeletable
{
bool IsDeleted { get; set; }
}
public static class Extension
{
public static IEnumerable<T> WhereNotDeleted<T>(this IEnumerable<T> source,
Func<T, bool> predicate) where T : IDeletable
{
return source.Where(x => !x.IsDeleted).Where(predicate);
}
}
In the following simple test, only two records are returned since the third has been soft-deleted.
void Main()
{
var x = new List<Test>();
x.Add(new Test{ Number = "one" });
x.Add(new Test{ Number = "two" });
x.Add(new Test{ Number = "three", IsDeleted = true });
var y = x.WhereNotDeleted(a => a != null);
y.Count().Dump();
}
public class Test : IDeletable
{
public string Number { get; set; }
public bool IsDeleted { get; set; }
}
In light of your comment, if you wanted it to work with EXISTING LINQ extension methods, you wouldn't actually be able to, since you'd run into call ambiguity.
If you wanted to call .Where()
and have it translate to your condition ONLY for your objects that implement IDeletable
, you would need to wrap each LINQ extension method.
public static class Extension
{
public static IEnumerable<IDeletable> Where(this IEnumerable<IDeletable> source,
Func<IDeletable, bool> predicate)
{
return System.Linq.Enumerable.Where(source, (x => predicate(x) && !x.IsDeleted));
}
}
In these extension methods you have to call the base LINQ methods via instance invocation instead of as an extension method, else you'll run into a stack overflow exception.