Ayende @ Rahien

Refunds available at head office

Disabling Linq

For a lot of things, Linq is really nice. But there is one very important problem with Linq. It make it very easy to do some really stupid things, because it is so easy to intermix in memory operations with remote operations. One of the most common problems that we run into with the RavenDB API was this:

session.LuceneQuery<User>("Users/ByName")
            .Where(x=>x.Name == username)
            .FirstOrDefault();

The problem is that LuceneQuery is not the Linq endpoint to RavenDB. It is the lower level API that we use to query RavenDB, and the Linq API is built on top of that. The problem is that it is very easy to write code like that, and it works, for small amounts of data. What this code actually does is load the first page of information from the database and do the filtering in memory.

Obviously, this is not ideal, but this is a pretty common mistake. We are currently thinking of ways to drastically simplify the API, but in the meantime, something had to be done about this.

LuceneQuery returns an instance of IDocumentQuery:

public interface IDocumentQuery<T> : IEnumerable<T>

That was good, because that meant that we had a hook to place stop the user from making this mistake. The way Linq works, it first looks for an instance method and only afterward it will look for extension methods. That allowed me to define the following:

[Obsolete(@"
You cannot issue an in memory filter - such as Where(x=>x.Name == ""Ayende"") - on IDocumentQuery. 
This is likely a bug, because this will execute the filter in memory, rather than in RavenDB.
Consider using session.Query<T>() instead of session.LuceneQuery<T>. The session.Query<T>() method fully supports Linq queries, while session.LuceneQuery<T>() is intended for lower level API access.
If you really want to do in memory filtering on the data returned from the query, you can use: session.LuceneQuery<T>().ToList().Where(x=>x.Name == ""Ayende"")
", true)]
IEnumerable<T> Where(Func<T, bool> predicate);

Now, the code above it going to generate a very descriptive compilation error, and the entire class of bugs just went out the window.

Comments

tobi
10/10/2010 10:52 AM by
tobi

Creative.

gunteman
10/10/2010 02:23 PM by
gunteman

Good solution, not destroying the api but very clearly pointing the consumer in a better direction.

Omer Mor
10/10/2010 06:37 PM by
Omer Mor

How about stop deriving IDocumentQuery from IEnumerable, and adding an .AsEnumerable() method to it?

This will make moving into the IEnumerable monad more explicit.

Ayende Rahien
10/10/2010 06:42 PM by
Ayende Rahien

Omer, but that would mean that we couldn't do things like:

First(), Single(), Count, etc

Scooletz
10/10/2010 07:01 PM by
Scooletz

A descriptive obsolescence as in NH. Maybe you should allow these queries to be executed from another interface, like ImProbablyMakingAMistakeSession or sth like this?

Neil Mosafi
10/10/2010 07:03 PM by
Neil Mosafi

But all the other methods mentioned take predicates to filter on, so it's the same thing. I would never write:

.Where(x => x.Name == "ayende").FirstOrDefault();

But

.FirstOrDefault(x => x.Name == "ayende");

There are so many extension methods, do you really want to Obsolete them all? Think I agree with Omer

Omer Mor
10/10/2010 09:13 PM by
Omer Mor

Oren,

As Neil observed, there are many LINQ operators that can filter.

Even Single() with no predicate is filtering, and as such will cause a full query to hit the DB, only to be filtered back into a single item.

You should rethink this.

If you do want to use linq-to-objects, you'll just have to add a single .AsObservable() method and then you could go run with your scissors. :-)

Indranil
10/12/2010 03:05 PM by
Indranil

Only works if you are referebcing the LuceneQuery through a IDocumentQuery. If you happen to have a reference to an IEnumerable which happens to be a LuceneQuery then the LINQ extension method will still get used.

Comments have been closed on this topic.