Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass LINQ expression to another QueryProvider

I have a simple custom QueryProvider that takes an expression, translates it to SQL and queries an sql database.

I want to create a small cache in the QueryProvider that stores commonly accessed objects so retrieval can happen without a database hit.

The QueryProvider has the method

public object Execute(System.Linq.Expressions.Expression expression)
{
    /// Builds an SQL statement from the expression, 
    /// executes it and returns matching objects
}

The cache sits as a field in this QueryProvider class and is a simple generic List.

If I use the List.AsQueryable method and pass the above expression into the List.AsQueryable's Provider's Execute method it doesn't work as desired. It looks like when an expression gets compiled the initial QueryProvider becomes an integral part.

Is it possible to pass an expression to a subsequent QueryProvider and execute the expression as desired?

The calling code looks vaguely as follows:

public class QueryProvider<Entity>()
{
    private List<TEntity> cache = new List<Entity>();

    public object Execute(System.Linq.Expressions.Expression expression)
    {
        /// check whether expression expects single or multiple result
        bool isSingle = true;

        if (isSingle)
        {
            var result = this.cache.AsQueryable<Entity>().Provider.Execute(expression);
            if (result != null) 
                return result;
        }

        /// cache failed, hit database
        var qt = new QueryTranslator();
        string sql = qt.Translate(expression);
        /// .... hit database
    }
} 

It doesn't return an error, instead it gets stuck in loop where this same provider is called over and over again.

Here's some more code showing what I'm trying to do:

Collection:

class Collection<Entity>
{

    internal List<Entity> cacheOne { get; private set; }
    internal Dictionary<Guid, Entity> cacheTwo { get; private set; }

    internal Collection()
    {
        this.cacheOne = new List<Entity>();
        this.cacheTwo = new Dictionary<Guid, Entity>();
    }

    public IQueryable<Entity> Query()
    {
        return new Query<Entity>(this.cacheOne, this.cacheTwo);
    }

}

Query:

class Query<Entity> : IQueryable<Entity>
{
    internal Query(List<Entity> cacheOne, Dictionary<Guid, Entity> cacheTwo)
    {
        this.Provider = new QueryProvider<Entity>(cacheOne, cacheTwo);
        this.Expression = Expression.Constant(this);
    }

    internal Query(IQueryProvider provider, Expression expression)
    {
        this.Provider = provider;
        if (expression != null)
            this.Expression = expression;
    }

    public IEnumerator<Entity> GetEnumerator()
    {
        return this.Provider.Execute<IEnumerator<Entity>>(this.Expression);
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public Type ElementType
    {
        get { return typeof(Entity); }
    }

    public System.Linq.Expressions.Expression Expression { get; private set; }

    public IQueryProvider Provider { get; private set; }
}

QueryProvider:

class QueryProvider<Entity> : IQueryProvider
{

    private List<Entity> cacheOne;
    private Dictionary<Guid, Entity> cacheTwo;

    internal QueryProvider(List<Entity> cacheOne, Dictionary<Guid, Entity> cacheTwo)
    {
        this.cacheOne = cacheOne;
        this.cacheTwo = cacheTwo;   
    }

    public IQueryable<TElement> CreateQuery<TElement>(System.Linq.Expressions.Expression expression)
    {
        return new Query<TElement>(this, expression);
    }

    public IQueryable CreateQuery(System.Linq.Expressions.Expression expression)
    {
        throw new NotImplementedException();
    }

    public TResult Execute<TResult>(System.Linq.Expressions.Expression expression)
    {
        return (TResult)this.Execute(expression);
    }

    public object Execute(System.Linq.Expressions.Expression expression)
    {
        Iterator<Entity> iterator = new Iterator<Entity>(expression, cacheOne, cacheTwo);
        return (iterator as IEnumerable<Entity>).GetEnumerator();
    }
}

Iterator:

class Iterator<Entity> : IEnumerable<Entity>
{
    private Expression expression;
    private List<Entity> cacheOne;
    private Dictionary<Guid, Entity> cacheTwo;

    internal Iterator(Expression expression, List<Entity> cacheOne, Dictionary<Guid, Entity> cacheTwo)
    {
        this.expression = expression;
        this.cacheOne = cacheOne;
        this.cacheTwo = cacheTwo;
    }

    public IEnumerator<Entity> GetEnumerator()
    {
        foreach (var result in (IEnumerable<Entity>)this.cacheOne.AsQueryable<Entity>().Provider.Execute(expression))
        {
            yield return result;
        }

        foreach (var more in (IEnumerable<Entity>)this.cacheTwo.Values.AsQueryable<Entity>().Provider.Execute(expression))
        {
            yield return more;
        }
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

Program:

class Program
{
    static void Main(string[] args)
    {
        /// Create collection + caches
        var collection = new Collection<Giraffe>();
        collection.cacheOne.AddRange(new Giraffe[] {
            new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(2011, 03, 21), Height = 192, Name = "Percy" },
            new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(2005, 12, 25), Height = 188, Name = "Santa" },
            new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(1999, 04, 01), Height=144, Name="Clown" }
        });
        var cachetwo = new List<Giraffe>(new Giraffe[] {
            new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(1980, 03,03), Height = 599, Name="Big Ears" },
            new Giraffe() { Id = Guid.NewGuid(), DateOfBirth = new DateTime(1985, 04, 02), Height= 209, Name="Pug" }
        });
        foreach (var giraffe in cachetwo)
            collection.cacheTwo.Add(giraffe.Id, giraffe);

        /// Iterate through giraffes born before a certain date
        foreach (var result in collection.Query().Where(T => T.DateOfBirth < new DateTime(2006, 01, 01)))
        {
            Console.WriteLine(result.Name);
        }

    }
}

Giraffe:

class Giraffe
{
    public Guid Id { get; set; }
    public string Name { get; set;  }
    public long Height { get; set; }
    public DateTime DateOfBirth { get; set; }
}

Special cases e.g. SingleAndDefault, etc are left out. The part I'm wanting to work happens in Iterator, where it first of all executes the List's QueryProvider before executing the Dictionary's.

One of the two Queryable objects might be a database, or something else.

like image 260
Anthony Avatar asked May 22 '12 14:05

Anthony


1 Answers

No, a query does not become bound to a provider. That's why you have the IQueryable interface: it provides both the Expression and the Provider, so LINQ can call the provider to execute the expression.

The problem in your implementation is in the way Query<Entity> represents itself: you're setting the root expression to Expression.Constant(this), where this is the query (not the collection).

So when you execute the query with LINQ-to-Objects, it will call GetEnumerator on Query<>, which then calls LINQ-to-Objects to execute Expression, which has a root expression Expression.Constant(this) (of type Query<>), and LINQ-to-Objects then iterates this root expression by calling GetEnumerator on this Query<>, etc.

The problem lies in

(IEnumerable<Entity>)this.cacheOne.AsQueryable<Entity>().Provider.Execute(expression)

which is basically equal to

new Entity[0].AsQueryable().Provider.Execute(expression)

or

linqToObjectsProvider.Execute(expression)

The Provider returned by a query is not linked to the source (this.cacheOne), so you're just re-executing the expression, not querying over your cache.

What's wrong with the following?

class Collection<Entity>
{
    ...

    public IQueryable<Entity> Query()
    {
        return this.cacheOne.Concat(this.cacheTwo.Values).AsQueryable();
    }
}

Note that Concat uses delayed evaluation, so only when you execute the query are cacheOne and cacheTwo concatenated and then manipulated using the additional LINQ operators.

(In which case, I'd make Collection<Entity> an IQueryablewithExpressionequal toExpression.Constant(this.cacheOne.Concat(this.cacheTwo.Values))`. I think you can do away with all the other classes.)


Original answer

However, I don't think this way of piggy-backing LINQ to Objects will ever be able to do what you think it should.

At the very least, you should keep the original query provider so you can call that one when you have a cache miss. If you don't, and use your own query provider (you did not show the code you are using to do the actual call), your query provider one will call itself again, and again.

So you'll need to create a CachingQueryProvider and a CachingQuery:

class CachingQuery<T> : IQueryable<T>
{
    private readonly CachingQueryProvider _provider;
    private readonly Expression _expression;

    public CachingQuery(CachingQueryProvider provider, Expression expression)
    {
        _provider = provider;
        _expression = expression;
    }

    // etc.
}

class CachingQueryProvider : IQueryProvider
{
    private readonly IQueryProvider _original;

    public CachingQueryProvider(IQueryProvider original)
    {
        _original = original;
    }

    // etc.
}

public static class CachedQueryable
{
    public static IQuerable<T> AsCached(this IQueryable<T> source)
    {
        return new CachingQuery<T>(
             new CachingQueryProvider(source.Provider), 
             source.Expression);
    }
}

Also if you want to cache a result, you'll need to materialize the result before you cache it, otherwise you cache the query, not the result. And the result itself should never be executed again, as it is already the data you should return.

The direction I would head into is as follows:

class CachingQueryProvider : IQueryProvider
{
    public object Execute(Expression expression)
    {
        var key = TranslateExpressionToCacheKey(expression);

        object cachedValue;
        if (_cache.TryGetValue(key, out cachedValue))
            return cachedValue;

        object result = _originalProvider.Execute(expression);

        // Won't compile because we don't know T at compile time
        IEnumerable<T> sequence = result as IEnumerable<T>;
        if (sequence != null && !(sequence is ICollection<T>)) 
        {
            result = sequence.ToList<T>();
        }

        _cache[key] = result; 

        return result;
    }
}

For the part marked as Won't compile, you'll have to do some reflection trickery.

And caution: string implements IEnumerable, so be careful not to try to materialize a single string result value.

like image 172
Ruben Avatar answered Oct 21 '22 23:10

Ruben