将LINQ表达式传递给另一个QueryProvider

时间:2012-05-22 14:23:45

标签: c# linq linq-expressions

我有一个简单的自定义QueryProvider,它接受一个表达式,将其转换为SQL并查询一个sql数据库。

我想在QueryProvider中创建一个小缓存,用于存储常用对象,这样就可以在没有数据库命中的情况下进行检索。

QueryProvider具有方法

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

缓存作为此QueryProvider类中的字段而存在,是一个简单的通用List。

如果我使用List.AsQueryable方法并将上述表达式传递给List.AsQueryable的提供程序的Execute方法,那么它根本无法工作。看起来当表达式被编译时,初始的QueryProvider成为不可或缺的部分。

是否可以将表达式传递给后续的QueryProvider并根据需要执行表达式?

调用代码看起来模糊如下:

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
    }
} 

它不会返回错误,而是会被一遍又一遍地调用同一个提供程序的循环卡住。

这里还有一些代码,展示了我尝试做的事情:

收集:

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);
    }

}

查询:

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();
    }
}

迭代器:

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();
    }
}

程序:

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);
        }

    }
}

长颈鹿:

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

特殊情况,例如SingleAndDefault等被遗漏了。我想要工作的部分发生在Iterator中,它首先执行List的QueryProvider,然后再执行Dictionary#s。

两个Queryable对象中的一个可能是数据库,或其他东西。

1 个答案:

答案 0 :(得分:6)

不,查询不会绑定到提供程序。这就是你拥有IQueryable接口的原因:它提供了Expression和Provider,因此LINQ可以调用提供程序来执行表达式。

您的实施中的问题是Query<Entity>代表自己的方式:您将根表达式设置为Expression.Constant(this),其中this查询 (不是收藏品)。

因此,当您使用LINQ-to-Objects执行查询时,它将在GetEnumerator上调用Query<>,然后调用LINQ-to-Objects执行Expression,其中包含Expression.Constant(this)根表达式Query<>(类型为GetEnumerator),然后LINQ-to-Objects通过调用此Query<>等上的(IEnumerable<Entity>)this.cacheOne.AsQueryable<Entity>().Provider.Execute(expression) 来迭代此根表达式。

问题在于

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

基本上等于

linqToObjectsProvider.Execute(expression)

this.cacheOne

查询返回的提供程序链接到源(class Collection<Entity> { ... public IQueryable<Entity> Query() { return this.cacheOne.Concat(this.cacheTwo.Values).AsQueryable(); } } ),因此您只是重新执行表达式,而不是查询缓存。

以下是什么问题?

Concat

请注意,Collection<Entity>使用延迟评估,因此只有在执行查询时,才会将cacheOne和cacheTwo连接起来,然后使用其他LINQ运算符进行操作。

(在这种情况下,我会使with成为一个IQueryable equal to表达式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); } } Expression.Constant(this.cacheOne.Concat(this.cacheTwo.Values))`。我认为你可以取消所有其他课程。)


原始回答

但是,我不认为这种支持LINQ to Objects的方法能够做到你认为应该做的事情。

至少,您应该保留原始查询提供程序,以便在缓存未命中时调用该提供程序。如果不这样做,并使用您自己的查询提供程序(您没有显示用于执行实际调用的代码),您的查询提供程序将再次调用自身,并再次调用。

所以你需要创建一个CachingQueryProvider 一个CachingQuery:

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;
    }
}

此外,如果要缓存结果,则需要在缓存之前实现结果,否则缓存查询,而不是结果。结果本身永远不应该再次执行,因为它已经是您应该返回的数据。

我将进入的方向如下:

Won't compile

对于标记为{{1}}的部分,您将不得不做一些反思技巧。

请注意:string实现IEnumerable,所以要小心 not 以尝试实现单个字符串结果值。