[PL] Własny dostawca w LINQ (dla opornych) - cz. 1

Artykuł został rozbity na wiele części, poniżej spis treści:
I. Budowa zapytania
II. Podstawy analizy drzewa wyrażeń
III. Właściwa analiza drzewa wyrażeń
IV. Pobieranie i zwrot danych

-

Dużo się mówi i pisze o tym, że LINQ jest elastyczne i rozszerzalne. Sam powtarzam, że aby podpiąć się pod składnie LINQ z własnymi obiektami wystarczy poprawnie zaimplementować interfejs IQueryable. Zdanie brzmi jakby to była robota do porannej kawy, więc spróbujmy pójść na tę kawę. W tej serii artykułów chciałbym opisać jak takiego dostawcę stworzyć w możliwie najbardziej zrozumiały sposób. Takie artykuły już na pewno gdzieś na sieci, czytając jednak niektóre z nich uznałem, że temat wcale nie wygląda tak trywialnie, jak go opisują. Niniejszym spróbuję to zmienić. Dodatkowo poza publikacją na Code Guru, która luźno bazuje na artykułach z bloga Matta Warren'a (klasa ExpressionVisitor), nie ma znalazłem więcej publikacji po polsku. Mam więc nadzieję, iż poniższe treści komuś się przydadzą. Na zakończenie serii chciałbym podać parę przykładów zastosowań własnego dostawcy, co powinno być dobrą puentą dla całości. Zatem do dzieła.

Budowa zapytania

Jak wspomniałem początek jest banalny, tworzę sobie własną klasę zapytań, która implementuje wyżej wymieniony interfejs. Wygląda to mniej więcej tak:

   public class Query<T> : IQueryable<T>    {
        public IEnumerator<T> GetEnumerator()
        { … }
        IEnumerator IEnumerable.GetEnumerator()
        { … }
        public Type ElementType
        { … }
        public Expression Expression
        { … }
        public IQueryProvider Provider
        { … }
    }

Implementacje ciała tej klasy pozostawmy na moment pustą. Niech zwraca NotImplementedException(). Przyjrzyjmy się co zawiera wspomniany interfejs. W zasadzie można go podzielić na właściwego dostawce silnika zapytań (IQueryProvider), drzewo wyrażeń, typ elementu jaki zwraca wykonanie zapytania oraz enumeratory całkiem przydatne przy szperaniu w danych.

Przetestujmy naszą najprostszą implementację. Za wiele na pewno nie wykona poza zwróceniem wyjątków ale najistotniejsze jest to, że poniższy kod już się kompiluje i poprawnie jest interpretowany przez Visual Studio (chociażby w kontekście Intellisense)
Poniżej prosty i niezbyt wyrafinowany kod naszego testu:

    internal class Customer
    {
        public string FirstName;
        public string LastName;
        public int Age;
    }

class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Query<Customer> q = new Query<Customer>();

                var result = from c in q
                             where c.Age > 10
                             select c; 

                Console.WriteLine(result.ToString());
            }
            catch (Exception err)
            {
                Console.WriteLine("Error: \n" + err.ToString());
            }
            Console.ReadKey();
        }
    }

Tak jak się spodziewaliśmy powyższy kod wyrzuca wyjątki NotImplementedException. Pierwszy jaki się pojawił, powinien wystąpić przy próbie dostępu do naszego Query.Provider. Kontynuujmy zatem pracę nad implementacją tego interfejsu.

    public class BasicQueryProvider : IQueryProvider
    {
        public IQueryable<T> CreateQuery<T>(Expression expression)
        { … }
        public IQueryable CreateQuery(Expression expression)
        { … }
        public T Execute<T>(Expression expression)
        { … }
        public object Execute(Expression expression)
        { … }
    }

Ponownie ciała odpowiednich metod pozostają puste i to co możemy wyodrębnić to funkcje związane z tworzeniem i wykonaniem realnego zapytania. Układanka zaczyna być coraz bardziej logiczna.
Coraz bardziej i częściej widoczny w powyższym kodzie staje się tajemnicze Expression. Klasa ta pochodząca z System.Linq.Expressions to nic innego jak reprezentacja drzewa wyrażeń. Jak najbardziej potrzebne do poprawnego złożenia naszego wyrażenia. Warto się przyjrzeć jak ta klasa wygląda w object browserze. Opiszę ją i jej pochodne lepiej w dalszej części a teraz spróbujemy poprawnie zaimplementować obsługę dostawcy i wyrażeń:

    public class Query<T> : IQueryable<T>
    {
        BasicQueryProvider provider;
        Expression expression; 

public Query(BasicQueryProvider provider, Expression expression)
        {
            this.provider = provider;
            this.expression = expression;
        }
        public Query(BasicQueryProvider provider) {
            this.provider = provider;
            this.expression = Expression.Constant(this);
        } 

        public Expression Expression
        {
            get { return expression; }
        } 
        public IQueryProvider Provider
        {
            get { return provider; }
        }
        //reszta bez zmian
    }

Aby posunąć sprawy dalej do przodu powinniśmy dodać jeszcze implementację BasicQueryProvider.CreateQuery<t> w postaci:

        public IQueryable<T> CreateQuery<T>(Expression expression)
        {
            return new Query<T>(this, expression);
        }

Kompilacja i pierwszy sukces. Nie dość, że aplikacja nam się skompilowała to jeszcze nie wyrzuciła wyjątków. Niby super, ale nie do końca. Koniec końców nasz wynik niczego nie reprezentuje (poza typem naszej klasy Query).

Następne kroki to implementacja odpowiedniej interpretacji drzewa wyrażeń oraz realne wykonanie i co za tym idzie zwrócenie wyniku. To jednak opiszę w kolejnej części. Na ten moment, aby nie było wątpliwości podsumuję aktualne stadium kodu:

//BasicQueryProvider.cs
public class BasicQueryProvider : IQueryProvider
    {
        #region IQueryProvider Members

        public IQueryable<T> CreateQuery<T>(Expression expression)
        {
            return new Query<T>(this, expression);
        }

        public IQueryable CreateQuery(Expression expression)
        {
            throw new NotImplementedException();
        }

        public TRes Execute<TRes>(Expression expression)
        {
            throw new NotImplementedException();
        }

        public object Execute(Expression expression)
        {
            throw new NotImplementedException();
        }

        #endregion

    }
//Query.cs

    public class Query<T> : IQueryable<T>
    {
        BasicQueryProvider provider;
        Expression expression;

        public Query()
        {
            this.provider = new BasicQueryProvider();
            this.expression = Expression.Constant(this);
        }

        public Query(BasicQueryProvider provider, Expression expression)
        {
            this.provider = provider;
            this.expression = expression;
        }

        public Query(BasicQueryProvider provider) {
            this.provider = provider;
            this.expression = Expression.Constant(this);
        }

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            throw new NotImplementedException();
        }

        #endregion

        #region IQueryable Members

        public Type ElementType
        {
            get { throw new NotImplementedException(); }
        }

    public Expression Expression
        {
            get { return expression; }
        }

        public IQueryProvider Provider
        {
            get { return provider; }
        }

        #endregion

    }

//i nasz program.cs

internal class Customer
    {
        public Customer()
        {
            FirstName = "";
            LastName = "";
            Age = 0;
        }

        public string FirstName;
        public string LastName;
        public int Age;

    }

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Query<Customer> q = new Query<Customer>();

                var result = from c in q
                      where c.Age > 10
                             select c; 
                Console.WriteLine(result.ToString());
            }
            catch (Exception err)
            {
                Console.WriteLine("Error message\n" + err.ToString());
            }
            Console.ReadKey();
        }
    }

Dalszy ciąg implementacji własnego dostawcy zapytań Linq niedługo. W następnym artykule opiszę dokładnie mechanizm tłumaczenia drzewa wyrażeń na dowolną składnię jaką zapragniemy (chociażby T-SQL w przypadku SQL Server'a) oraz tłumaczenie wyników zgodny z oczekiwanym typem zwrotu. Pojawią się zatem nowe obiekty oraz istniejące BasicQueryProvider i Query<T> otrzymają konkretniejszą implementację niektórych metod. Nie powinno być też żadnych NotImplementedException w załączonym już kodzie.

Jeśli nic na temat własnych dostawców w Linq do tej pory nie wiedzieliśmy to po tym artykule powinniśmy wiedzieć:
1) Tworzenie własnego dostawcy to w zasadzie poprawna implementacja nie tylko IQueryable ale też IQueryProvider
2) Interfejs IQueryable zawiera informacje o zapytaniu w postaci typu zwracanego rezultatu, drzewa wyrażeń pomocnego w konstrukcji zapytania, enumeratory dostępu do danych w rezultacie i konkretnego dostawce wykonującego polecenia.
3) Interfejs IQueryProvider odpowiedzialny jest za konstrukcję konkretnej instancji zapytania oraz jego wykonanie i poprawne zwrócenie wyniku.

[Pytanie i prośba o komentarz: Jeśli macie jakiś szczególny kierunek, w którym ewolucja takiego własnego dostawcy powinna przebiec w moim wykonaniu to zapraszam do pisania]

Technorati Tagi: Polish posts,coding,.NET Framework,Linq