Pasar la expresión LINQ a otro QueryProvider

Tengo un QueryProvider personalizado simple que toma una expresión, la traduce a SQL y consulta una base de datos sql.

Quiero crear un pequeño caché en QueryProvider que almacene los objetos a los que se accede comúnmente para que la recuperación pueda ocurrir sin un golpe a la base de datos.

El QueryProvider tiene el método

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

El caché se encuentra como un campo en esta clase QueryProvider y es una lista genérica simple.

Si uso el método List.AsQueryable y paso la expresión anterior al método de ejecución del proveedor de List.AsQueryable, no funciona como se desea. Parece que cuando se compila una expresión, el QueryProvider inicial se convierte en una parte integral.

¿Es posible pasar una expresión a un QueryProvider posterior y ejecutar la expresión como se desee?

El código de llamada se ve vagamente como sigue:

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

No devuelve un error, sino que se atasca en un bucle en el que se llama a este mismo proveedor una y otra vez.

Aquí hay más código que muestra lo que estoy tratando de hacer:

Colección:

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

}

consulta:

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

proveedor de consultas:

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

Iterador:

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

Programa:

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

    }
}

Jirafa:

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

Los casos especiales, por ejemplo, SingleAndDefault, etc., se omiten. La parte que quiero trabajar ocurre en Iterator, donde primero ejecuta el QueryProvider de List antes de ejecutar el Dictionary.

Uno de los dos objetos Queryable podría ser una base de datos u otra cosa.

preguntado el 22 de mayo de 12 a las 15:05

¿Podrías agregar el código de llamada? -

¿Podría dar un ejemplo de la expresión Linq que está utilizando para llamar a su QueryProvider? (Estoy tratando de reconstruir su código localmente). Además, ¿también implementas la versión genérica de Execute? public TResult Execute<TResult>(System.Linq.Expressions.Expression expression) { ... } -

Actualizado con un ejemplo de la expresión LINQ y otro código en las clases Query, QueryProvider y Collection. -

1 Respuestas

No, una consulta no se vincula a un proveedor. Es por eso que tiene la interfaz IQueryable: proporciona tanto la expresión como el proveedor, por lo que LINQ puede llamar al proveedor para ejecutar la expresión.

El problema en su implementación está en la forma Query<Entity> se representa a sí mismo: está configurando la expresión raíz en Expression.Constant(this), Donde this es la pregunta (no la colección).

Entonces, cuando ejecute la consulta con LINQ-to-Objects, llamará GetEnumerator on Query<>, que luego llama a LINQ-to-Objects para ejecutar Expression, que tiene una expresión raíz Expression.Constant(this) (de tipo Query<>), y LINQ-to-Objects luego itera esta expresión raíz llamando GetEnumerator en esto Query<>, etc.

El problema radica en

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

que es básicamente igual a

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

or

linqToObjectsProvider.Execute(expression)

El proveedor devuelto por una consulta es no vinculado a la fuente (this.cacheOne), por lo que solo está volviendo a ejecutar la expresión, no consultando su caché.

¿Qué pasa con lo siguiente?

class Collection<Entity>
{
    ...

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

Tenga en cuenta que Concat utiliza la evaluación retrasada, por lo que solo cuando ejecuta la consulta se concatenan cacheOne y cacheTwo y luego se manipulan con los operadores LINQ adicionales.

(En cuyo caso, haría Collection<Entity> un IQueryablewithexpresiónequal toExpression.Constant(this.cacheOne.Concat(this.cacheTwo.Values))`. Creo que puedes acabar con todas las otras clases.)


Respuesta original

Sin embargo, no creo que esta forma de aprovechar LINQ to Objects alguna vez pueda hacer lo que cree que debería.

Como mínimo, debe mantener la original proveedor de consultas para que pueda llamar a ese cuando tenga una falla de caché. Si no lo hace y usa su propio proveedor de consultas (no mostró el código que está usando para hacer la llamada real), su proveedor de consultas se llamará a sí mismo una y otra vez.

Por lo tanto, deberá crear un CachingQueryProvider y una consulta de almacenamiento en caché:

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

Además, si desea almacenar en caché un resultado, deberá materializar el resultado antes lo almacena en caché, de lo contrario almacena en caché la consulta, no el resultado. Y el resultado en sí nunca debe volver a ejecutarse, ya que son los datos que debe devolver.

La dirección en la que me dirigiría es la siguiente:

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

Para la parte marcada como Won't compile, tendrás que hacer algunos trucos de reflexión.

Y precaución: la cadena implementa IEnumerable, así que tenga cuidado no para intentar materializar un único valor de resultado de cadena.

contestado el 30 de mayo de 12 a las 18:05

Gracias, Ruben, eso es útil, pero ¿por qué crees que aprovechar LINQ-to-Objects nunca hará lo que espero? Su solución alternativa es buena, pero tengo curiosidad por saber por qué no cree que el respaldo puede funcionar. - Antonio

LINQ to Objects está diseñado para enumerar colecciones en memoria (como matrices). Eso es lo único que puede hacer. Entonces, si deja que LINQ to Objects ejecute una consulta como from entity in table where ... select entity, preguntará table para devolver todos los elementos, y luego aplicar el where al resultado Y table utilizará su propio contexto de datos para hacerlo (y así ejecutar SELECT * FROM Table cada vez que lo use). Por lo tanto, debe ejecutar la consulta y transformar el resultado en una estructura en memoria y almacenarlo en caché. No veo dónde encaja L2O aquí. - Ruben

También IQueryProvider.Execute Siempre debe devolver el resultado de la consulta No debería devolver una representación intermedia. ¿Quizás esa es la confusión? - Ruben

@Ruben ¿Podría usarse IQueryProvider.PartialEval en este caso? - Totero

@Totero Eso es probablemente algo que necesita para calcular la clave de caché, porque todos los locales y campos en la expresión de consulta estarán representados por MemberExpressions. O estoy completa y completamente malinterpretando la pregunta original. Mi tren de pensamiento aquí es: ¿cómo puedo almacenar en caché una consulta aleatoria (con una entrada de caché separada por consulta distinta). Si desea almacenar en caché la tabla completa pero aplicar todas las operaciones de consulta en la memoria, necesitará una implementación completamente diferente. - Ruben

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.