3

I'm writing a collection of LINQ 2 SQL extension methods. Right now, I have a bunch of duplicate methods, one working on an IEnumerable collection and the other on an IQueryable collection. For example

public static IQueryable<X> LimitVisible(this IQueryable<X> items) {
    return items.Where(x => !x.Hidden && !x.Parent.Hidden);
}

public static IEnumerable<X> LimitVisible(this IEnumerable<X> items) {
    return items.Where(x => !x.Hidden && !x.Parent.Hidden);
}

I think I need this duplication because I want to be able to retain the IQueryable behavior in some cases, so that execution is deferred to the database. Is this a correct assumption or would a single IEnumerable method (plus casting the output to IQueryable) work? If I do need both, I don't like the duplication, so I want to combine the two using the generic method

public static T LimitVisible<T>(this T items) where T : IEnumerable<X> {
    return (T)items.Where(x => !x.Hidden && !x.Parent.Hidden);
}

This works except that I have other types than X (say Y and Z) that I also want to write LimitVisible functions for. But I can't write

public static T LimitVisible<T>(this T items) where T : IEnumerable<Y> {
    return (T)items.Where(y => !y.Hidden && !y.Parent.Hidden);
}

because you can overload based on generics. I could put these methods in different classes, but that doesn't seem like the right solution.

Any suggestions? Maybe I'm making incorrect assumptions and there's no need for an IQueryable specific version in the first place.

Edit: Another Option

Here's another pattern I've used in the past to avoid duplication

private static readonly Expression<Func<X, bool>> F = x => !x.Hidden && !x.Parent.Hidden;

public static IEnumerable<X> LimitVisible(this IEnumerable<X> e) {
    return e.Select(W.Compile());
}

public static IQueryable<X> LimitVisible(this IQueryable<X> q) {
    return q.Select(W);
}

Are there any dangers with this?

2
  • Does X, Y, Z have a common interface or base class for the Hidden and Parent properties? Commented Oct 29, 2012 at 17:01
  • No, they are different tables in the database that both happen to have Hidden fields. Commented Oct 29, 2012 at 17:19

2 Answers 2

2

Really there's no good way around having two methods. The IQueryable Where takes an Expression<Func<T,bool>> as a parameter while the IEnumerable takes a Func<T,bool> as a parameter. Thanks to the magic of lambdas the code looks identical for the caller, but the compiler actually does vastly different things with the two different bits of code.

Sign up to request clarification or add additional context in comments.

2 Comments

Sorry, that was a typo, which I just fixed.
@JordanCrittenden Removed that section of my answer, but the rest is still relevant.
1

The answer depends on your usage: if you do not mind bringing the data set into memory in order to perform the query, then keeping only the IEnumerable<T> override will be fine.

However, the inevitable consequence of this decision is that your queries will be forced into memory the moment that you use LimitVisible<T>. This may or may not be what you want, and might force you into coding patterns that you would rather avoid. For example,

var firstHundred = ctx
    .SalesOrders
    .LimitVisible()                     // Happens in memory
    .Where(ord => ord.UserId == myUser) // Also happens in memory
    .Take(100);

will perform a lot worse with a single IEnumerable<T> than with an IQueryable<T> overide availalbe, because all sales orders will be retrieved into memory one-by-one before determining their visibility, rather than checking the condition on the server side. If filtering by user ID could eliminate the majority of sales orders on the server side, the performance difference of this equivalent query could be orders of magnitude better:

var firstHundred = ctx
    .SalesOrders
    .Where(ord => ord.UserId == myUser) // Happens in RDBMS
    .LimitVisible()                     // Happens in memory
    .Take(100);

If you plan to use your code only by yourself and you do not mind being watchful for situations when there is bad performance for no obvious reason, you can keep one overload. If you plan to let others use your library, I strongly suggest keeping both overrides.

EDIT : To speed up your alternative implementation, pre-compile your expression, like this:

private static readonly Expression<Func<X, bool>> F = x => !x.Hidden && !x.Parent.Hidden;
private static readonly Predicate<X> CompiledF = (Predicate<X>)F.Compile();

public static IEnumerable<X> LimitVisible(this IEnumerable<X> e) {
    return e.Select(CompiledF);
}

public static IQueryable<X> LimitVisible(this IQueryable<X> q) {
    return q.Select(F);
}

6 Comments

You confirmed my suspicions. I definitely don't want to bring everything into memory. The problem with having two methods is that it's easy to accidentally update the code for one but not both.
@JordanCrittenden Unfortunately, this is probably the easiest way to go, without writing a lot of complex code to reuse LINQ's expressions inside IQueryable<T> implementations.
Yeah, I just edited to show the alternative. I think the alternative is inefficient though, due to the Compile call
@JordanCrittenden You can easily fix it, though, by creating a static readonly version of the compiled lambda (take a look at the edit).
@ErenErsönmez If you use IEnumerator methods it won't load them eagarly, but the point is that it will, eventually (i.e. when iterated) load all of the source items into memory, then filter them, and output the results. If you used IQueryable inputs then the output of the filter is what needs to be brought into memory. The key there is that all items that this Where filters out won't be brought into memory (ever). It also means that subsequent calls happen at the DB, not in memory, and they could do all sorts of complex tasks (i.e. group bys, joins, etc.) or do lots more filtering.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.