14 Stimmen

Übergeben Sie den LINQ-Ausdruck an einen anderen QueryProvider

Ich habe einen einfachen benutzerdefinierten QueryProvider, der einen Ausdruck nimmt, ihn in SQL übersetzt und eine SQL-Datenbank abfragt.

Ich möchte einen kleinen Cache im QueryProvider erstellen, der häufig abgerufene Objekte speichert, sodass ein Abruf ohne Datenbankzugriff erfolgen kann.

Der QueryProvider hat die Methode

public object Execute(System.Linq.Expressions.Expression expression)
{
    /// Erstellt eine SQL-Anweisung aus dem Ausdruck, führt sie aus und gibt übereinstimmende Objekte zurück.
}

Der Cache liegt als Feld in dieser QueryProvider-Klasse vor und ist eine einfache generische Liste.

Wenn ich die List.AsQueryable-Methode verwende und den obigen Ausdruck in die Execute-Methode des Providers von List.AsQueryable übergebe, funktioniert es nicht wie gewünscht. Es sieht so aus, als ob der ursprüngliche QueryProvider ein integraler Bestandteil wird, wenn ein Ausdruck kompiliert wird.

Ist es möglich, einen Ausdruck an einen nachfolgenden QueryProvider zu übergeben und den Ausdruck wie gewünscht auszuführen?

Der Aufrufcode sieht ungefähr wie folgt aus:

public class QueryProvider()
{
    private List cache = new List();

    public object Execute(System.Linq.Expressions.Expression expression)
    {
        /// Überprüft, ob der Ausdruck ein einzelnes oder mehrfaches Ergebnis erwartet.
        bool isSingle = true;

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

        /// Cache fehlgeschlagen, Datenbankabfrage
        var qt = new QueryTranslator();
        string sql = qt.Translate(expression);
        /// .... Datenbankabfrage
    }
} 

Es wird kein Fehler zurückgegeben, stattdessen bleibt es in einer Schleife stecken, in der dieser Provider immer wieder aufgerufen wird.

Hier ist etwas weiterer Code, der zeigt, was ich versuche zu tun:

Sammlung:

class Collection
{

    internal List cacheOne { get; private set; }
    internal Dictionary cacheTwo { get; private set; }

    internal Collection()
    {
        this.cacheOne = new List();
        this.cacheTwo = new Dictionary();
    }

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

}

Abfrage:

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

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

    public IEnumerator GetEnumerator()
    {
        return this.Provider.Execute>(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 : IQueryProvider
{

    private List cacheOne;
    private Dictionary cacheTwo;

    internal QueryProvider(List cacheOne, Dictionary cacheTwo)
    {
        this.cacheOne = cacheOne;
        this.cacheTwo = cacheTwo;   
    }

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

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

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

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

Iterator:

class Iterator : IEnumerable
{
    private Expression expression;
    private List cacheOne;
    private Dictionary cacheTwo;

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

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

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

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

Programm:

class Program
{
    static void Main(string[] args)
    {
        /// Erstelle Sammlung + Caches
        var collection = new Collection();
        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(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);

        /// Iteriere durch Giraffen, die vor einem bestimmten Datum geboren wurden
        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; }
}

Spezialfälle wie SingleAndDefault usw. werden ausgelassen. Der Teil, der funktionieren soll, liegt im Iterator, wo zunächst der QueryProvider der Liste und dann der des Dictionaries ausgeführt wird.

Eines der beiden Queryable-Objekte könnte eine Datenbank oder etwas anderes sein.

7voto

Ruben Punkte 14679

Nein, eine Abfrage wird nicht an einen Anbieter gebunden. Deshalb haben Sie das IQueryable-Interface: Es stellt sowohl den Ausdruck als auch den Anbieter bereit, damit LINQ den Anbieter aufrufen kann, um den Ausdruck auszuführen.

Das Problem in Ihrer Implementierung liegt darin, wie Query sich selbst darstellt: Sie setzen den Wurzelausdruck auf Expression.Constant(this), wobei this die Abfrage (nicht die Sammlung) ist.

Wenn Sie die Abfrage mit LINQ-to-Objects ausführen, wird GetEnumerator auf Query<> aufgerufen, der dann LINQ-to-Objects aufruft, um Expression auszuführen, das einen Wurzelausdruck Expression.Constant(this) (vom Typ Query<>) hat, und LINQ-to-Objects durchläuft dann diesen Wurzelausdruck, indem es GetEnumerator auf dieser Query<> aufruft usw.

Das Problem liegt in

(IEnumerable)this.cacheOne.AsQueryable().Provider.Execute(expression)

das im Grunde genommen gleich ist wie

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

oder

linqToObjectsProvider.Execute(expression)

Der vom Abfrage zurückgegebene Anbieter ist nicht mit der Quelle (this.cacheOne) verbunden, daher führen Sie nur den Ausdruck erneut aus, ohne über Ihren Cache abzufragen.

Was ist falsch an folgendem?

class Collection
{
    ...

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

Beachten Sie, dass Concat eine verzögerte Auswertung verwendet, sodass cacheOne und cacheTwo nur dann verkettet und dann mit den zusätzlichen LINQ-Operatoren manipuliert werden, wenn Sie die Abfrage ausführen.

(In diesem Fall würde ich Collection an IQueryablewithExpressionequal toExpression.Constant(this.cacheOne.Concat(this.cacheTwo.Values))` machen. Ich denke, Sie können alle anderen Klassen loswerden.)


Ursprüngliche Antwort

Dennoch glaube ich nicht, dass dieser Ansatz, LINQ an Objekte anzuhängen, jemals das tun wird, was Sie glauben, dass er tun sollte.

Zumindest sollten Sie den ursprünglichen Abfrageanbieter beibehalten, damit Sie diesen aufrufen können, wenn Sie einen Cache-Miss haben. Wenn Sie das nicht tun und Ihren eigenen Abfrageanbieter verwenden (Sie haben den Code, den Sie für den tatsächlichen Aufruf verwenden, nicht gezeigt), wird Ihr Abfrageanbieter sich selbst wieder und wieder aufrufen.

Sie müssen also ein CachingQueryProvider und einen CachingQuery erstellen:

class CachingQuery : IQueryable
{
    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 AsCached(this IQueryable source)
    {
        return new CachingQuery(
             new CachingQueryProvider(source.Provider), 
             source.Expression);
    }
}

Wenn Sie ein Ergebnis zwischenspeichern möchten, müssen Sie das Ergebnis vorher materialisieren, sonst speichern Sie die Abfrage, nicht das Ergebnis. Und das Ergebnis selbst sollte nie wieder ausgeführt werden, da es bereits die Daten ist, die Sie zurückgeben sollten.

Die Richtung, in die ich gehen würde, lautet wie folgt:

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

        // Wird nicht kompilieren, da wir T zur Kompilierzeit nicht kennen
        IEnumerable sequence = result as IEnumerable;
        if (sequence != null && !(sequence is ICollection)) 
        {
            result = sequence.ToList();
        }

        _cache[key] = result; 

        return result;
    }
}

Für den mit Wird nicht kompilieren markierten Teil müssen Sie einige Reflektionstricks anwenden.

Und Vorsicht: string implementiert IEnumerable, also achten Sie darauf, nicht zu versuchen, einen einzelnen Stringergebniswert zu materialisieren.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X