Продолжается подписка на наши издания! Вы не забыли подписаться?

LINQ: создаем IQueryable-провайдер

Автор: Мэтт Уоррен (Matt Warren)
Опубликовано: 28.04.2009

По новой технологии Microsoft LINQ написано уже немало статей, записей в богах и т.д. Однако почти все они являются очередной вариацией на тему «введение в...». Увы, более-менее детальную информацию удается добыть только в форумах, но там она отрывочна, и искать ее очень непросто. К счастью, даже в нашу эпоху интеллектуальной жадности находятся люди, готовые бескорыстно делиться информацией, например, Matt Warren, один из разработчиков LINQ, написавший серию сообщений в своем блоге (вернее, статей, сообщениями их можно назвать только по месту публикации – http://blogs.msdn.com/mattwar/) о создании LINQ-провайдеров, и предоставивший нам разрешение на их публикацию. Этот материал будет полезен не только тем, кто собирается написать свой провайдер, но и просто программистам, желающим понимать, как работают технологии, которые они используют в работе.

Часть I. Повторно используемые базовые классы, реализующие IQueryable

Это первое из серии сообщений о том, как создать LINQ IQueryable-провайдер. Каждое следующее сообщение будет базироваться на предыдущем.

Я уже некоторое время собирался начать серию сообщений о создании LINQ-провайдеров, использующих IQueryable. Люди довольно давно просят у меня советов, как через внутреннюю электронную почту Microsoft, так и на форумах, или же взламывая шифрование и связываясь со мной напрямую. В основном я, конечно, отвечал: «Я работаю над примером, который все вам покажет», – давая им знать, что скоро все раскроется. Однако вместо простого размещения здесь полного примера я счел правильным разобрать все по шагам – это позволит мне копать глубже и разъяснить все происходящее вместо того, чтобы вывалить на вас все сразу и предоставить разбираться самостоятельно.

Первое, что я должен сказать – IQueryable в Beta 2 изменился. Это уже не один интерфейс, а два: IQueryable and IQueryProvider. Давайте разберемся с этим перед тем, как перейти к их реализации.

Используя в Visual Studio ‘go to definition’, вы получите что-то типа следующего:

  public interface IQueryable : IEnumerable { Type ElementType { get; }
    Expression Expression { get; }
    IQueryProvider Provider { get; }
  }
  public interface IQueryable<T> : IEnumerable<T>, IQueryable, IEnumerable 
  { ... }

Конечно, IQueryable больше не выглядит особо интересным; все самое интересное перенесли в новый интерфейс IQueryProvider. Однако на IQueryable все еще стоит посмотреть. Как вы можете заметить, все, что есть в IQueryable – три свойства, доступных только для чтения. Первое содержит тип элемента (или ‘T’ в IQueryable<T>). Важно отметить, что все классы, реализующие IQueryable, должны также реализовать IQueryable<T> для некоторого T, и наоборот. Обобщенный IQueryable<T> вы будете использовать чаще всего (в сигнатурах методов и тому подобном). Не обобщенный IQueryable существует в основном для динамического создания запросов.

Второе свойство содержит выражение, соответствующее запросу. В это смысл существования IQueryable. Реальный «запрос» в IQueryable – это выражение, представляющее запрос в виде дерева операторов и вызовов методов LINQ. Это часть IQueryable, которую ваш провайдер обязан содержать, чтобы делать что-либо полезное. Если посмотреть глубже, можно увидеть, что вся инфраструктура IQueryable (включая версию стандартных операторов запросов LINQ System.Linq.Queryable) – просто механизм для автоматического конструирования узлов дерева выражений за вас. Когда вы используете метод Queryable.Where для наложения фильтра на IQueryable, он просто строит для вас новый IQueryable, добавляя узел с вызовом метода в конец запроса. Не верите? Попробуйте сами, и вы убедитесь, что это так.

Осталось последнее свойство, которое дает нам экземпляр нового интерфейса IQueryProvider. Мы сделали вот что – переместили все методы, реализующие создание новых объектов, реализующих IQueryable, и их выполнение, в отдельный интерфейс, который более логично представляет истинного провайдера.

public interface IQueryProvider 
{
  IQueryable CreateQuery(Expression expression);
  IQueryable<TElement> CreateQuery<TElement>(Expression expression);
  object Execute(Expression expression);
  TResult Execute<TResult>(Expression expression);
} 

Посмотрев на интерфейс IQueryProvider, вы можете подумать: «К чему все эти методы?» Правда состоит в том, что на самом деле там только две операции, CreateQuery и Execute, в generic- и не- generic форме. Generic-формы чаще всего используются при прямом создании запросов на языке программирования и имеют более высокую производительность, поскольку можно избежать рефлексии при создании экземпляров.

Метод CreateQuery делает точно то, что и следует из его названия. Он создает новый экземпляр IQueryable-запроса, основываясь на указанном дереве выражений. Когда кто-то вызывает этот метод, он, в сущности, просит ваш провайдер создать новый экземпляр объекта, реализующего IQueryable, который при перечислении вызовет провайдер запросов и обработает указанное выражение запроса. Queryable-форма стандартных операторов запросов использует этот метод для конструирования новых IQueryable, которые остаются ассоциированными с вашим провайдером. Заметьте, что вызывающая сторона может передать любое дерево выражений, которое позволяет данный API. Оно может даже быть некорректным для вашего провайдера. Однако единственное, что должно быть верно – само выражение должно быть типизированным и возвращать/порождать корректно типизированный IQueryable. Как видите, IQueryable содержит выражение, описывающее кусок кода (т.е. его AST – прим.ред.), который, если превратить его в реальный код и исполнить, заново сконструирует тот же самый IQueryable (или его эквивалент).

Метод Execute – это точка входа в провайдер для реального исполнения выражений запросов. Возможность явно выполнить вместо того, чтобы полагаться на IEnumerable.GetEnumerator(), важна, так как позволяет исполнять выражения, которые не обязательно порождают последовательности. Например, запрос “myquery.Count()” порождает одно целое число. Дерево выражений для этого запроса – это вызов метода Count, возвращающего целое. Метод Queryable.Count (как и другие агрегаты и т.п.) используют этот метод для исполнения запроса «прямо сейчас».

Что ж, пока не слишком страшно, правда? Вы легко можете реализовать все эти методы, верно? Конечно да, но зачем беспокоиться? Я сделаю это за вас. Пожалуй, кроме метода Execute. Как он реализуется, я покажу вам в следующих сообщениях.

Начнем с IQuerayble. Поскольку этот интерфейс разделили на два, стало возможно реализовать IQuerayble всего один раз и использовать эту реализацию в любом провайдере. Я сейчас реализую Query<T>, реализующий IQueryable<T> и все остальное:

  public class Query<T> : IQueryable<T>, IQueryable, IEnumerable<T>, 
    IEnumerable, IOrderedQueryable<T>, IOrderedQueryable 
  {
    QueryProvider provider;
    Expression expression;
 
    public Query(QueryProvider provider) 
    {
      if (provider == null) 
      {
        throw new ArgumentNullException("provider");
      }
      this.provider = provider;
      this.expression = Expression.Constant(this);
    }
 
    public Query(QueryProvider provider, Expression expression) 
    {
      if (provider == null) 
      {
        throw new ArgumentNullException("provider");
      }
      if (expression == null) 
      {
        throw new ArgumentNullException("expression");
      }
      if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type)) 
      {
        throw new ArgumentOutOfRangeException("expression");
      }
      this.provider = provider; 
      this.expression = expression;
    }
 
    Expression IQueryable.Expression 
    {
      get { return this.expression; 
    }
  }
 
    Type IQueryable.ElementType 
    {
      get { return typeof(T); }
    }
 
    IQueryProvider IQueryable.Provider 
    {
      get { return this.provider; }
    }
 
    public IEnumerator<T> GetEnumerator() 
    {
      return(
       (IEnumerable<T>)this.provider.Execute(this.expression)).GetEnumerator();
    }
 
    IEnumerator IEnumerable.GetEnumerator() 
    {
      return(
        (IEnumerable)this.provider.Execute(this.expression)).GetEnumerator();
    }
 
    public override string ToString() 
    {
      return this.provider.GetQueryText(this.expression);
    }
  }

Как вы можете видеть, реализация IQueryable прямолинейна. Этот маленький объект всего лишь хранит ссылку на дерево выражений и экземпляр провайдера.

ОК, теперь я должен показать вам какой-нибудь провайдер. Я реализовал абстрактный базовый класс QueryProvider, на который выше ссылался Query<T>. Реальный провайдер можно просто унаследовать от этого класса и реализовать в нем метод Execute.

  public abstract class QueryProvider : IQueryProvider 
  {
    protected QueryProvider() 
    { ... }

    IQueryable<S> IQueryProvider.CreateQuery<S>(Expression expression) 
    {
      return new Query<S>(this, expression);
    }

    IQueryable IQueryProvider.CreateQuery(Expression expression) 
    {
      Type elementType = TypeSystem.GetElementType(expression.Type);
      try 
      {
        return(
          IQueryable)Activator.CreateInstance(typeof(Query<>).MakeGenericType(
            elementType), new object[] { this, expression });
      }
      catch (TargetInvocationException tie) 
      {
        throw tie.InnerException;
      }
    }

    S IQueryProvider.Execute<S>(Expression expression) 
    {
      return (S)this.Execute(expression);
    }

    object IQueryProvider.Execute(Expression expression) 
    {
      return this.Execute(expression);
    }

    public abstract string GetQueryText(Expression expression);
    public abstract object Execute(Expression expression);
  }

Я реализовал интерфейс IQueryProvider в базовом классе QueryProvider. Методы CreateQuery создают новые экземпляры Query<T>, а методы Execute передают исполнение великому, но пока не реализованному методу Execute.

Вы можете думать об этом коде как о заглушке, которую нужно написать, начиная создавать IQueryable-провайдер. Реальные действия выполняются в методе Execute.

Именно с этого я и начну в следующий раз.

Класс TypeSystem

Похоже, я забыл определить маленький хелпер-класс, который использовала моя реализация, вот он:

  internal static class TypeSystem 
  {
    internal static Type GetElementType(Type seqType) 
    {
      Type ienum = FindIEnumerable(seqType);
      if (ienum == null) return seqType;
      return ienum.GetGenericArguments()[0];
    }
    private static Type FindIEnumerable(Type seqType) 
    {
      if (seqType == null || seqType == typeof(string))
        return null;
      if (seqType.IsArray)
        return typeof(IEnumerable<>).MakeGenericType(seqType.GetElementType());
      if (seqType.IsGenericType) 
      {
        foreach (Type arg in seqType.GetGenericArguments()) 
        {
          Type ienum = typeof(IEnumerable<>).MakeGenericType(arg);
          if (ienum.IsAssignableFrom(seqType)) {
            return ienum;
          }
        }
      }
      Type[] ifaces = seqType.GetInterfaces();
      if (ifaces != null && ifaces.Length > 0) 
      {
        foreach (Type iface in ifaces) 
        {
          Type ienum = FindIEnumerable(iface);
          if (ienum != null) return ienum;
        }
      }
      if (seqType.BaseType != null && seqType.BaseType != typeof(object)) 
      {
        return FindIEnumerable(seqType.BaseType);
      }
      return null;
    }
  }

Да, я знаю. В этом хелпере больше кода, чем во всем сообщении. Такие дела... :)

Часть II. Where и многократно используемый посетитель дерева выражений

Это второе сообщение в серии, посвященной созданию LINQ-провайдеров с использованием интерфейса IQueryable.

Теперь, когда я заложил фундамент, определив многократно используемые реализации IQueryable и IQueryProvider, а именно – Query<T> и QueryProvider, я собираюсь создать такой провайдер, который на самом деле что-то делает. Как я уже говорил, провайдер запросов на самом деле занят исполнением небольшого количества «кода», представленного в виде дерева выражений вместо IL. Конечно, он не должен исполнять его в традиционном смысле этого слова. Например, LINQ to SQL транслирует выражения запроса в SQL и отправляет их серверу для исполнения.

Приведенный ниже пример должен работать во многом наподобие LINQ to SQL, то есть транслировать и исполнять запросы к ADO-провайдеру. Здесь я должен оговориться, пример ни в каком смысле не является полноценным провайдером. Я собираюсь обрабатывать только операцию Where, и даже не стану пытаться делать что-нибудь сложнее, чем позволить предикату содержать ссылку на поле и несколько простых операторов. В будущем я могу расширить провайдер, но пока он служит только для иллюстрации. Пожалуйста, не рассматривайте его как код промышленного качества.

Провайдер в основном должен делать две вещи: 1) транслировать запрос в текст SQL-команд и 2) транслировать результат исполнения команд в объекты.

Транслятор запросов

Транслятор запросов должен просто посетить каждый узел в дереве выражений и транслировать поддерживаемые операции в текст, используя StringBuilder. Для ясности будем считать, что есть класс ExpressionVisitor, который реализует паттерн Посетитель (Visitor) для узлов Expression (я обещаю привести его в конце этого сообщения, а пока поверьте мне на слово).

internal class QueryTranslator : ExpressionVisitor 
{
  StringBuilder sb;

  internal QueryTranslator() 
  { ... }

  internal string Translate(Expression expression) 
  {
    this.sb = new StringBuilder();
    this.Visit(expression);
    return this.sb.ToString();
  }

  private static Expression StripQuotes(Expression e) 
  {
    while (e.NodeType == ExpressionType.Quote) 
    {
      e = ((UnaryExpression)e).Operand;
    }
    return e;
  }

  protected override Expression VisitMethodCall(MethodCallExpression m) 
  {
    if (m.Method.DeclaringType == typeof(Queryable) 
      && m.Method.Name == "Where")
    {
      sb.Append("SELECT * FROM (");
      this.Visit(m.Arguments[0]);
      sb.Append(") AS T WHERE ");
      LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
      this.Visit(lambda.Body);
      return m;
    }
    throw new NotSupportedException(string.Format(
      "The method '{0}' is not supported", m.Method.Name));
  }

  protected override Expression VisitUnary(UnaryExpression u)
  {
    switch (u.NodeType) 
    {
      case ExpressionType.Not:
        sb.Append(" NOT ");
        this.Visit(u.Operand);
        break;
      default:
        throw new NotSupportedException(string.Format(
          "The unary operator '{0}' is not supported", u.NodeType));
    }
    return u;
  }

  protected override Expression VisitBinary(BinaryExpression b)
  {
    sb.Append("(");
    this.Visit(b.Left);
    switch (b.NodeType) 
    {
      case ExpressionType.And:
        sb.Append(" AND ");
        break;
      case ExpressionType.Or:
        sb.Append(" OR");
        break;
      case ExpressionType.Equal:
        sb.Append(" = ");
        break;
      case ExpressionType.NotEqual:
        sb.Append(" <> ");
        break;
      case ExpressionType.LessThan:
        sb.Append(" < ");
        break;
      case ExpressionType.LessThanOrEqual:
        sb.Append(" <= ");
        break;
      case ExpressionType.GreaterThan:
        sb.Append(" > ");
        break;
      case ExpressionType.GreaterThanOrEqual:
        sb.Append(" >= ");
        break;
      default:
        throw new NotSupportedException(
          string.Format(
            "The binary operator '{0}' is not supported", b.NodeType));
    }
    this.Visit(b.Right);
    sb.Append(")");
    return b;
  }

  protected override Expression VisitConstant(ConstantExpression c) 
  {
    IQueryable q = c.Value as IQueryable;
    if (q != null) 
    {
      // assume constant nodes w/ IQueryables are table references
      sb.Append("SELECT * FROM ");
      sb.Append(q.ElementType.Name);
    }
    else if (c.Value == null) 
    {
      sb.Append("NULL");
    }
    else {
      switch (Type.GetTypeCode(c.Value.GetType())) 
      {
        case TypeCode.Boolean:
          sb.Append(((bool)c.Value) ? 1 : 0);
          break;
        case TypeCode.String:
          sb.Append("'");
          sb.Append(c.Value);
          sb.Append("'");
          break;
        case TypeCode.Object:
          throw new NotSupportedException(
            string.Format(
            "The constant for '{0}' is not supported", c.Value));
        default:
          sb.Append(c.Value);
          break;
      }
    }
    return c;
  }

  protected override Expression VisitMemberAccess(MemberExpression m) 
  {
    if (m.Expression != null 
      && m.Expression.NodeType == ExpressionType.Parameter)
    {
      sb.Append(m.Member.Name);
      return m;
    }
    throw new NotSupportedException(
      string.Format("The member '{0}' is not supported", m.Member.Name));
  }
}
 

Самое большее, что я ожидаю увидеть в дереве выражений – это узел вызова метода с аргументами, ссылающимися на источник (аргумент 0) и предикат (аргумент 1). Посмотрите на приведенный выше метод VisitMethodCall. Я явно обрабатываю случай метода Queryable.Where, генерируя “SELECT * FROM (“, рекурсивно посещая источник и затем присоединяя ”) AS T WHERE “, после чего посещаю предикат. Это позволяет другим операторам запроса, имеющимся в исходном выражении, быть вложенными в подзапросы. Я не обрабатываю другие операторы, но если имеется несколько вызовов Where, я могу их обработать. Не имеет значения, какой псевдоним таблицы используется (я выбрал «Т»), поскольку я все равно не собираюсь генерировать ссылок на нее. Более полноценный провайдер, конечно, обязан это делать.

Здесь есть маленький вспомогательный метод StripQuotes. Его задача – вырезать любые узлы ExpressionType.Quote из аргументов метода (которые могут встретиться), чтобы я смог получить чистое лямбда-выражение, которое мне и нужно.

Методы VisitUnary и VisitBinary прямолинейны. Они просто вводят корректный текст для тех унарных и бинарных операторов, которые я собираюсь поддерживать. Интересный момент в трансляции касается метода VisitConstant. Как видите, ссылки на таблицы у меня – это просто корневые IQueryable. Если узел константы содержит один или несколько экземпляров Query<T>, я просто считаю их представляющими корневую таблицу и присоединяю “SELECT * FROM”, а следом – имя таблицы, которое является просто именем типа элемента запроса. Остальная трансляция узлов констант работает с настоящими константами. Заметьте, эти константы добавляются в текст команды как литеральные значения. Обратите внимание, что этот код совершенно не учитывает возможности атак типа SQL Injection, от которых должен защищать настоящие провайдеры.

Наконец, VisitMemberAccess считает, что все обращения к полям или свойствам являются ссылками на колонки в тексте команды. Так ли это на самом деле, никак не проверяется. Предполагается, что имя поля или свойства соответствует имени колонки в БД.

Для класса ‘Customers’ с полями, соответствующими именам колонок в БД Northwind, этот транслятор запросов генерирует примерно такие запросы:

Query<Customers> customers = ...;
IQueryable<Customers> q = customers.Where(c => c.City == "London");

==>
“SELECT * FROM (SELECT *FROM Customers) AS T WHERE (city = ‘London’)”

Читатель объектов – ObjectReader

Работа читателя объектов состоит в том, чтобы превратить результаты SQL-запроса в объекты. Я собираюсь создать простой класс, принимающий DbDataReader и тип ‘T’, и заставить его реализовать IEnumerable<T>. В этой реализации нет никаких завитушек. Она будет работать только на запись в поля объектов через рефлексию. Имена полей должны соответствовать именам колонок в DataReader, а типы должны соответствовать тем, которые DataReader считает правильными.

internal class ObjectReader<T> : IEnumerable<T>, 
  IEnumerable where T : class, new() 
{
  Enumerator enumerator;

  internal ObjectReader(DbDataReader reader) 
  {
    this.enumerator = new Enumerator(reader);
  }

  public IEnumerator<T> GetEnumerator() 
  {
    Enumerator e = this.enumerator;
    if (e == null) {
      throw new InvalidOperationException("Cannot enumerate more than once");
    }
    this.enumerator = null;
    return e;
  }

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

  class Enumerator : IEnumerator<T>, IEnumerator, IDisposable 
  {
    DbDataReader reader;
    FieldInfo[] fields;
    int[] fieldLookup;
    T current;

    internal Enumerator(DbDataReader reader) 
    {
      this.reader = reader;
      this.fields = typeof(T).GetFields();
    }

    public T Current 
    {
      get { return this.current; }
    }

    object IEnumerator.Current 
    {
      get { return this.current; }
    }

    public bool MoveNext() 
    {
      if (this.reader.Read()) 
      {
        if (this.fieldLookup == null) 
        {
          this.InitFieldLookup();
        }
        T instance = new T();
        for (int i = 0, n = this.fields.Length; i < n; i++) 
        {
          int index = this.fieldLookup[i];
          if (index >= 0) {
            FieldInfo fi = this.fields[i];
            if (this.reader.IsDBNull(index)) 
            {
              fi.SetValue(instance, null);
            }
            else 
            {
              fi.SetValue(instance, this.reader.GetValue(index));
            }
          }
        }
        this.current = instance;
        return true;
      }
      return false;
    }

    public void Reset() 
    { ... }

    public void Dispose() 
    {
      this.reader.Dispose();
    }

    private void InitFieldLookup() 
    {
      Dictionary<string, int> map = 
       new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
      for (int i = 0, n = this.reader.FieldCount; i < n; i++) 
      {
        map.Add(this.reader.GetName(i), i);
      }
      this.fieldLookup = new int[this.fields.Length];
      for (int i = 0, n = this.fields.Length; i < n; i++) 
      {
        int index;
        if (map.TryGetValue(this.fields[i].Name, out index)) 
        {
          this.fieldLookup[i] = index;
        }
        else 
        {
          this.fieldLookup[i] = -1;
        }
      }
    }
  }
}

ObjectReader создает новый экземпляр типа Т для каждой записи, считанной DbDataReader. Он использует рефлексию, а конкретно метод FieldInfo.SetValue (а зря – это очень медленно; лучше с помощью рефлексии создать делегаты и использовать их для считывании данных – прим.ред.), чтобы присвоить значения каждому из полей объекта. При создании ObjectReader создает экземпляр вложенного класса Enumerator. Этот перечислитель выдается при вызове метода GetEnumerator. Поскольку DataReader нельзя перезагрузить и выполнить заново, перечислитель может быть запрошен только один раз. Если GetEnumerator вызывается второй раз, генерируется исключение.

ObjectReader терпимо относится к порядку полей. Поскольку QueryTranslator строит запросы, используя “SELECT *”, это необходимо, так как код никак не может узнать, какая колонка появится в результате первой. Заметьте, что в общем не рекомендуется использовать “SELECT *” в промышленном коде. Не забывайте, что это просто пример, в общих чертах показывающий, как собрать LINQ-провайдер. Чтобы учитывать различные последовательности колонок, точная последовательность обнаруживается во время выполнения, когда первая запись считывается в DataReader. Функция InitFieldLookup строит соответствие названия и порядкового номера колонки, а затем строит таблицу соответствий полей объекта и их номеров – fieldLookup.

Провайдер

Имея эти два куска (и класс из предыдущего сообщения) довольно просто объединить их в IQueryable LINQ-провайдер.

public class DbQueryProvider : QueryProvider 
{
  DbConnection connection;

  public DbQueryProvider(DbConnection connection) 
  {
    this.connection = connection;
  }

  public override string GetQueryText(Expression expression) 
  {
    return this.Translate(expression);
  }

  public override object Execute(Expression expression) 
  {
    DbCommand cmd = this.connection.CreateCommand();
    cmd.CommandText = this.Translate(expression);
    DbDataReader reader = cmd.ExecuteReader();
    Type elementType = TypeSystem.GetElementType(expression.Type);
    return Activator.CreateInstance(
      typeof(ObjectReader<>).MakeGenericType(elementType),
      BindingFlags.Instance | BindingFlags.NonPublic, null,
      new object[] { reader },
      null);
  }

  private string Translate(Expression expression) 
  {
    return new QueryTranslator().Translate(expression);
  }
} 

Как видите, создание провайдера теперь – это всего лишь упражнение по объединению двух кусков. GetQueryText просто использует QueryTranslator, чтобы произвести текст команды. Метод Execute использует QueryTranslator и ObjectReader для создания объекта DbCommand, его выполнения и возврата результата в виде IEnumerable.

Испытания

Теперь, имея провайдер, можно его испытать. Поскольку в основном я следую модели LINQ to SQL, я определю класс для таблицы Customers, Контекст (класс Northwind), который в курсе насчет таблиц (корневых запросов), и маленькую программу, использующую их.

public class Customers 
{
  public string CustomerID;
  public string ContactName;
  public string Phone;
  public string City;
  public string Country;
}

public class Orders 
{
  public int OrderID;
  public string CustomerID;
  public DateTime OrderDate;
}

public class Northwind 
{
  public Query<Customers> Customers;
  public Query<Orders> Orders;

  public Northwind(DbConnection connection) 
  {
    QueryProvider provider = new DbQueryProvider(connection);
    this.Customers = new Query<Customers>(provider);
    this.Orders = new Query<Orders>(provider);
  }
}


class Program {
  static void Main(string[] args) {
    string constr = @"…";
    using (SqlConnection con = new SqlConnection(constr)) {
      con.Open();
      Northwind db = new Northwind(con);

      IQueryable<Customers> query = 
         db.Customers.Where(c => c.City == "London");

      Console.WriteLine("Query:\n{0}\n", query);

      var list = query.ToList();
      foreach (var item in list) {
        Console.WriteLine("Name: {0}", item.ContactName);
      }

      Console.ReadLine();
    }
  }
}

Теперь, если запустить получившийся код, мы получим следующее (заметьте, что вам придется ввести в эту программу собственную строку подключения):

Query:
SELECT * FROM (SELECT * FROM Customers) AS T WHERE (City = 'London')

Name: Thomas Hardy
Name: Victoria Ashworth
Name: Elizabeth Brown
Name: Ann Devon
Name: Simon Crowther
Name: Hari Kumar

Прекрасно, это мне и нужно было. Обожаю, когда планы выполняются. :)

Вот оно, граждане. Вот он, IQueryable LINQ-провайдер. Или хотя бы его грубое подобие. Конечно, ваш провайдер будет делать куда больше, чем мой. Он будет обрабатывать все крайние случаи и подавать кофе.

Но у меня есть еще кое что, см. часть 3.

Приложение – класс ExpressionVisitor

Мне кажется, что я получил на порядок больше вопросов по этому классу, чем по помощи в создании провайдера запросов. В System.Linq.Expressions есть класс ExpressionVisitor, однако он предназначен для внутреннего использования, а не для прямого употребления, как вам бы этого хотелось. Если вы будете действительно громко вопить, может, вы и заставите нас в следующей версии сделать его открытым.

ExpressionVisitor – это моя реализация (классического) паттерна Посетитель. В этом варианте есть единственный класс-посетитель, который перенаправляет вызовы общей функции Visit в вызовы специфичных методов VisitXXX, соответствующих разным типам узлов. Заметьте, что не каждому типа узла соответствует свой метод, например, все бинарные операторы рассматриваются в одном методе VisitBinary. Сами узлы не участвуют напрямую в процессе посещения. Они рассматриваются просто как данные. Причина этого в том, что количество посетителей не ограничено. Вы можете написать свой собственный. Поэтому в классах узлов нет никакой семантики посещений – она вся в посетителях. Поведение посетителя по умолчанию для узла ХХХ заключено в версии базового класса VisitXXX.

Все методы VisitXXX возвращают узел. Узлы дерева выражений неизменяемы. Чтобы изменить дерево, нужно создать новое. По умолчанию методы VisitXXX будут создавать новый узел, если любое из его поддеревьев изменилось. Если изменений нет, возвращаются те же узлы. Таким образом, если вы изменяете узел (создавая новый узел) глубоко по дереву, остальное дерево будет перестроено автоматически.

Вот код. Радуйтесь. :)

public abstract class ExpressionVisitor 
{
  protected ExpressionVisitor() 
  { ... }

  protected virtual Expression Visit(Expression exp) 
  {
    if (exp == null)
      return exp;
    switch (exp.NodeType) 
    {
      case ExpressionType.Negate:
      case ExpressionType.NegateChecked:
      case ExpressionType.Not:
      case ExpressionType.Convert:
      case ExpressionType.ConvertChecked:
      case ExpressionType.ArrayLength:
      case ExpressionType.Quote:
      case ExpressionType.TypeAs:
        return this.VisitUnary((UnaryExpression)exp);
      case ExpressionType.Add:
      case ExpressionType.AddChecked:
      case ExpressionType.Subtract:
      case ExpressionType.SubtractChecked:
      case ExpressionType.Multiply:
      case ExpressionType.MultiplyChecked:
      case ExpressionType.Divide:
      case ExpressionType.Modulo:
      case ExpressionType.And:
      case ExpressionType.AndAlso:
      case ExpressionType.Or:
      case ExpressionType.OrElse:
      case ExpressionType.LessThan:
      case ExpressionType.LessThanOrEqual:
      case ExpressionType.GreaterThan:
      case ExpressionType.GreaterThanOrEqual:
      case ExpressionType.Equal:
      case ExpressionType.NotEqual:
      case ExpressionType.Coalesce:
      case ExpressionType.ArrayIndex:
      case ExpressionType.RightShift:
      case ExpressionType.LeftShift:
      case ExpressionType.ExclusiveOr:
        return this.VisitBinary((BinaryExpression)exp);
      case ExpressionType.TypeIs:
        return this.VisitTypeIs((TypeBinaryExpression)exp);
      case ExpressionType.Conditional:
        return this.VisitConditional((ConditionalExpression)exp);
      case ExpressionType.Constant:
        return this.VisitConstant((ConstantExpression)exp);
      case ExpressionType.Parameter:
        return this.VisitParameter((ParameterExpression)exp);
      case ExpressionType.MemberAccess:
        return this.VisitMemberAccess((MemberExpression)exp);
      case ExpressionType.Call:
        return this.VisitMethodCall((MethodCallExpression)exp);
      case ExpressionType.Lambda:
        return this.VisitLambda((LambdaExpression)exp);
      case ExpressionType.New:
        return this.VisitNew((NewExpression)exp);
      case ExpressionType.NewArrayInit:
      case ExpressionType.NewArrayBounds:
        return this.VisitNewArray((NewArrayExpression)exp);
      case ExpressionType.Invoke:
        return this.VisitInvocation((InvocationExpression)exp);
      case ExpressionType.MemberInit:
        return this.VisitMemberInit((MemberInitExpression)exp);
      case ExpressionType.ListInit:
        return this.VisitListInit((ListInitExpression)exp);
      default:
        throw new Exception(
          string.Format("Unhandled expression type: '{0}'", exp.NodeType));
    }
  }

  protected virtual MemberBinding VisitBinding(MemberBinding binding) 
  {
    switch (binding.BindingType) 
    {
      case MemberBindingType.Assignment:
        return this.VisitMemberAssignment((MemberAssignment)binding);
      case MemberBindingType.MemberBinding:
        return this.VisitMemberMemberBinding((MemberMemberBinding)binding);
      case MemberBindingType.ListBinding:
        return this.VisitMemberListBinding((MemberListBinding)binding);
      default:
        throw new Exception(
          string.Format("Unhandled binding type '{0}'", binding.BindingType));
    }
  }

  protected virtual ElementInit VisitElementInitializer(
    ElementInit initializer)
  {
    ReadOnlyCollection<Expression> arguments = 
      this.VisitExpressionList(initializer.Arguments);
    if (arguments != initializer.Arguments) 
    {
      return Expression.ElementInit(initializer.AddMethod, arguments);
    }
    return initializer;
  }

  protected virtual Expression VisitUnary(UnaryExpression u) 
  {
    Expression operand = this.Visit(u.Operand);
    if (operand != u.Operand) 
    {
      return Expression.MakeUnary(u.NodeType, operand, u.Type, u.Method);
    }
    return u;
  }

  protected virtual Expression VisitBinary(BinaryExpression b) 
  {
    Expression left = this.Visit(b.Left);
    Expression right = this.Visit(b.Right);
    Expression conversion = this.Visit(b.Conversion);
    if (left != b.Left || right != b.Right || conversion != b.Conversion) 
    {
      if (b.NodeType == ExpressionType.Coalesce && b.Conversion != null)
        return Expression.Coalesce(
          left, right, conversion as LambdaExpression);
      else
        return Expression.MakeBinary(
          b.NodeType, left, right, b.IsLiftedToNull, b.Method);
    }
    return b;
  }

  protected virtual Expression VisitTypeIs(TypeBinaryExpression b)
  {
    Expression expr = this.Visit(b.Expression);
    if (expr != b.Expression) 
    {
      return Expression.TypeIs(expr, b.TypeOperand);
    }
    return b;
  }

  protected virtual Expression VisitConstant(ConstantExpression c) 
  {
    return c;
  }

  protected virtual Expression VisitConditional(ConditionalExpression c)
  {
    Expression test = this.Visit(c.Test);
    Expression ifTrue = this.Visit(c.IfTrue);
    Expression ifFalse = this.Visit(c.IfFalse);
    if (test != c.Test || ifTrue != c.IfTrue || ifFalse != c.IfFalse) 
    {
      return Expression.Condition(test, ifTrue, ifFalse);
    }
    return c;
  }

  protected virtual Expression VisitParameter(ParameterExpression p) 
  {
    return p;
  }

  protected virtual Expression VisitMemberAccess(MemberExpression m) 
  {
    Expression exp = this.Visit(m.Expression);
    if (exp != m.Expression) 
    {
      return Expression.MakeMemberAccess(exp, m.Member);
    }
    return m;
  }

  protected virtual Expression VisitMethodCall(MethodCallExpression m)
  {
    Expression obj = this.Visit(m.Object);
    IEnumerable<Expression> args = this.VisitExpressionList(m.Arguments);
    if (obj != m.Object || args != m.Arguments) 
    {
      return Expression.Call(obj, m.Method, args);
    }
    return m;
  }

  protected virtual ReadOnlyCollection<Expression> 
    VisitExpressionList(ReadOnlyCollection<Expression> original) 
  {
    List<Expression> list = null;
    for (int i = 0, n = original.Count; i < n; i++) 
    {
      Expression p = this.Visit(original[i]);
      if (list != null) 
      {
        list.Add(p);
      }
      else if (p != original[i]) 
      {
        list = new List<Expression>(n);
        for (int j = 0; j < i; j++) 
        {
          list.Add(original[j]);
        }
        list.Add(p);
      }
    }
    if (list != null) 
    {
      return list.AsReadOnly();
    }
    return original;
  }

  protected virtual MemberAssignment 
    VisitMemberAssignment(MemberAssignment assignment)
  {
    Expression e = this.Visit(assignment.Expression);
    if (e != assignment.Expression) {
      return Expression.Bind(assignment.Member, e);
    }
    return assignment;
  }

  protected virtual MemberMemberBinding 
    VisitMemberMemberBinding(MemberMemberBinding binding) 
  {
    IEnumerable<MemberBinding> bindings = 
      this.VisitBindingList(binding.Bindings);
    if (bindings != binding.Bindings) 
    {
      return Expression.MemberBind(binding.Member, bindings);
    }
    return binding;
  }

  protected virtual MemberListBinding 
    VisitMemberListBinding(MemberListBinding binding) 
  {
    IEnumerable<ElementInit> initializers = 
      this.VisitElementInitializerList(binding.Initializers);
    if (initializers != binding.Initializers) 
    {
      return Expression.ListBind(binding.Member, initializers);
    }
    return binding;
  }

  protected virtual IEnumerable<MemberBinding> 
    VisitBindingList(ReadOnlyCollection<MemberBinding> original) 
  {
    List<MemberBinding> list = null;
    for (int i = 0, n = original.Count; i < n; i++) 
    {
      MemberBinding b = this.VisitBinding(original[i]);
      if (list != null) 
      {
        list.Add(b);
      }
      else if (b != original[i]) 
      {
        list = new List<MemberBinding>(n);
        for (int j = 0; j < i; j++) 
        {
          list.Add(original[j]);
        }
        list.Add(b);
      }
    }
    if (list != null)
      return list;
    return original;
  }

  protected virtual IEnumerable<ElementInit> 
    VisitElementInitializerList(ReadOnlyCollection<ElementInit> original) 
  {
    List<ElementInit> list = null;
    for (int i = 0, n = original.Count; i < n; i++) 
    {
      ElementInit init = this.VisitElementInitializer(original[i]);
      if (list != null) 
      {
        list.Add(init);
      }
      else if (init != original[i]) 
      {
        list = new List<ElementInit>(n);
        for (int j = 0; j < i; j++) 
        {
          list.Add(original[j]);
        }
        list.Add(init);
      }
    }
    if (list != null)
      return list;
    return original;
  }

  protected virtual Expression VisitLambda(LambdaExpression lambda) 
  {
    Expression body = this.Visit(lambda.Body);
    if (body != lambda.Body) 
    {
      return Expression.Lambda(lambda.Type, body, lambda.Parameters);
    }
    return lambda;
  }

  protected virtual NewExpression VisitNew(NewExpression nex) 
  {
    IEnumerable<Expression> args = this.VisitExpressionList(nex.Arguments);
    if (args != nex.Arguments) 
    {
      if (nex.Members != null)
        return Expression.New(nex.Constructor, args, nex.Members);
      else
        return Expression.New(nex.Constructor, args);
    }
    return nex;
  }

  protected virtual Expression VisitMemberInit(MemberInitExpression init) 
  {
    NewExpression n = this.VisitNew(init.NewExpression);
    IEnumerable<MemberBinding> bindings = this.VisitBindingList(init.Bindings);
    if (n != init.NewExpression || bindings != init.Bindings) 
    {
      return Expression.MemberInit(n, bindings);
    }
    return init;
  }

  protected virtual Expression VisitListInit(ListInitExpression init) 
  {
    NewExpression n = this.VisitNew(init.NewExpression);
    IEnumerable<ElementInit> initializers = 
      this.VisitElementInitializerList(init.Initializers);
    if (n != init.NewExpression || initializers != init.Initializers) 
    {
      return Expression.ListInit(n, initializers);
    }
    return init;
  }

  protected virtual Expression VisitNewArray(NewArrayExpression na) 
  {
    IEnumerable<Expression> exprs = this.VisitExpressionList(na.Expressions);
    if (exprs != na.Expressions) 
    {
      if (na.NodeType == ExpressionType.NewArrayInit) 
      {
        return Expression.NewArrayInit(na.Type.GetElementType(), exprs);
      }
      else 
      {
        return Expression.NewArrayBounds(na.Type.GetElementType(), exprs);
      }
    }
    return na;
  }

  protected virtual Expression VisitInvocation(InvocationExpression iv) 
  {
    IEnumerable<Expression> args = this.VisitExpressionList(iv.Arguments);
    Expression expr = this.Visit(iv.Expression);
    if (args != iv.Arguments || expr != iv.Expression) 
    {
      return Expression.Invoke(expr, args);
    }
    return iv;
  }
}

Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.

Copyright © 1994-2016 ООО "К-Пресс"