Прививка LINQ на библиотеку C # 2 - PullRequest
2 голосов
/ 07 апреля 2010

Я пишу слой доступа к данным.У него будут клиенты C # 2 и C # 3, поэтому я собираюсь на основе фреймворка 2.0.Несмотря на то, что я поощряю использование хранимых процедур, я все же пытаюсь предоставить довольно полную возможность выполнять специальные запросы.У меня это работает довольно хорошо, уже.

Для удобства клиентов C # 3 я пытаюсь обеспечить как можно большую совместимость с синтаксисом запросов LINQ.Джон Скит заметил , что выражения запросов LINQ имеют утку, поэтому у меня нет , чтобы иметь IQueryable и IQueryProvider (или IEnumerable<T>) для их использования.Я просто должен предоставить методы с правильными сигнатурами.

Итак, я получил Select, Where, OrderBy, OrderByDescending, ThenBy и ThenByDescending.Мне нужна помощь с Join и GroupJoin.У меня они работают, но только для одного соединения.

Краткий скомпилированный пример того, что у меня есть, это:

// .NET 2.0 doesn't define the Func<...> delegates, so let's define some workalikes
delegate TResult FakeFunc<T, TResult>(T arg);
delegate TResult FakeFunc<T1, T2, TResult>(T1 arg1, T2 arg2);

abstract class Projection{
    public static Condition operator==(Projection a, Projection b){
        return new EqualsCondition(a, b);
    }
    public static Condition operator!=(Projection a, Projection b){
        throw new NotImplementedException();
    }
}
class ColumnProjection : Projection{
    readonly Table  table;
    readonly string columnName;

    public ColumnProjection(Table table, string columnName){
        this.table      = table;
        this.columnName = columnName;
    }
}
abstract class Condition{}
class EqualsCondition : Condition{
    readonly Projection a;
    readonly Projection b;

    public EqualsCondition(Projection a, Projection b){
        this.a = a;
        this.b = b;
    }
}
class TableView{
    readonly Table        table;
    readonly Projection[] projections;

    public TableView(Table table, Projection[] projections){
        this.table       = table;
        this.projections = projections;
    }
}
class Table{
    public Projection this[string columnName]{
        get{return new ColumnProjection(this, columnName);}
    }

    public TableView Select(params Projection[] projections){
        return new TableView(this, projections);
    }
    public TableView Select(FakeFunc<Table, Projection[]> projections){
        return new TableView(this, projections(this));
    }
    public Table     Join(Table other, Condition condition){
        return new JoinedTable(this, other, condition);
    }
    public TableView Join(Table inner,
                          FakeFunc<Table, Projection> outerKeySelector,
                          FakeFunc<Table, Projection> innerKeySelector,
                          FakeFunc<Table, Table, Projection[]> resultSelector){
        Table join = new JoinedTable(this, inner,
            new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
        return join.Select(resultSelector(this, inner));
    }
}
class JoinedTable : Table{
    readonly Table     left;
    readonly Table     right;
    readonly Condition condition;

    public JoinedTable(Table left, Table right, Condition condition){
        this.left      = left;
        this.right     = right;
        this.condition = condition;
    }
}

Это позволяет мне использовать довольно приличный синтаксис в C #2:

Table table1 = new Table();
Table table2 = new Table();

TableView result =
    table1
    .Join(table2, table1["ID"] == table2["ID"])
    .Select(table1["ID"], table2["Description"]);

Но еще более приятный синтаксис в C # 3:

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    select new[]{t1["ID"], t2["Description"]};

Это работает хорошо и дает мне результаты, идентичные первому случаю.Проблема в том, что я хочу присоединиться к третьей таблице.

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    join t3 in table3 on t1["ID"] equals t3["ID"]
    select new[]{t1["ID"], t2["Description"], t3["Foo"]};

Теперь я получаю сообщение об ошибке (Не удается неявно преобразовать тип 'AnonymousType # 1' в 'Projection []'), возможно, из-за второго соединенияпытается соединить третью таблицу с анонимным типом, содержащим первые две таблицы.Этот анонимный тип, конечно же, не имеет Join метода.

Есть какие-нибудь подсказки, как я могу это сделать?

Ответы [ 2 ]

2 голосов
/ 07 апреля 2010

Это очень интересный дизайн, мне нравится!Как вы говорите, проблема в том, что ваше определение метода Join слишком конкретное.Основное различие между вашим определением и определением в LINQ заключается в следующем:

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    /* cut */, Func<TOuter, TInner, TResult> resultSelector)

public TableView Join(
     /* cut */, FakeFunc<Table, Table, Projection[]> resultSelector)

Когда LINQ компилирует запрос с несколькими предложениями join, он вызывает их в последовательности и генерирует resultSelector для первогоодин автоматически - и сгенерированный код возвращает простой анонимный тип, содержащий элементы из обеих исходных таблиц.Итак, если я прав, в вашем случае сгенерированный анонимный тип будет выглядеть следующим образом:

new { t1 : Projection; t2 : Projection }

Это, к сожалению, несовместимо с Projection[] (хотя семантически ,разница не большая)Боюсь, что единственный способ решить эту проблему - использовать динамическое приведение типов и рефлексию.

  • Вам необходимо изменить Join, чтобы он имел параметр общего типа TResult, используемый в resultSelector.

  • В методе Join вы запускаете TResult res = resultSelector(...), а затем вам нужно что-то сделать со значением res.

  • Если res is Projection[], то вы можете использовать существующий код (это тот случай, который будет использоваться в запросе, содержащем одно предложение join)

  • В другом случае resявляется анонимным типом, подобным приведенному выше.Это означает, что вам нужно использовать отражение, чтобы получить значения свойств типа и превратить их в массив значений Projection (а затем сделать то же самое, что вы делаете сейчас).

Я не пытался реализовать это, но я думаю, что это может сработать ...

0 голосов
/ 07 апреля 2010

Следующее немного длинно. Если вы заинтересованы только в том, чтобы заставить это работать, и не заботитесь о том, почему или как, то перейдите к последним двум разделам кода.


Томас Петричек ответ был верным направлением, но только на полпути туда. TResult из resultSelector действительно должно быть общим. Соединения действительно связаны друг с другом, а промежуточные результаты содержат анонимный тип, состоящий из левой и правой частей каждого объединения (они называются внешними и внутренними, соответственно).

Давайте посмотрим на мой запрос ранее:

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    join t3 in table3 on t1["ID"] equals t3["ID"]
    select new[]{t1["ID"], t2["Description"], t3["Foo"]};

Это переводится примерно так:

var intermediate =
    table1.Join(
        table2, t1=>t1["ID"], t2=>t2["ID"],
        (t1, t2)=>new{t1=t1, t2=t2}
    );
TableView result =
    intermediate.Join(
        table3, anon=>anon.t1["ID"], t3=>t3["ID"],
        (anon, t3)=>new[]{anon.t1["ID"], anon.t2["ID"], t3["Foo"]}
    );

Добавление дополнительного соединения делает шаблон более четким:

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    join t3 in table3 on t1["ID"] equals t3["ID"]
    join t4 in table4 on t1["ID"] equals t4["ID"]
    select new[]{t1["ID"], t2["Description"], t3["Foo"], t4["Bar"]};

Это примерно переводит на:

var intermediate1 =
    table1.Join(
        table2, t1=>t1["ID"], t2=>t2["ID"],
        (t1, t2)=>new{t1=t1, t2=t2}
    );
var intermediate2 =
    intermediate1.Join(
        table3, anon1=>anon1.t1["ID"], t3=>t3["ID"],
        (anon1, t3)=>new{anon1=anon1, t3=t3}
    );                 
TableView result =
    intermediate2.Join(
        table4, anon2=>anon2.anon1.t1["ID"], t4=>t4["ID"],
        (anon2, t3)=>new[]{
            anon2.anon1.t1["ID"], anon2.anon1.t2["ID"],
            anon2.t3["Foo"], t4["Bar"]
        }
    );

Таким образом, возвращаемое значение resultSelector будет двумя разными вещами. Для окончательного соединения результатом является список выбора, и это тот случай, который я уже обрабатывал. Для каждого другого соединения это будет анонимный тип, содержащий объединенные таблицы, которым присваиваются имена в соответствии с псевдонимами, которые я назначил им в запросе. LINQ, по-видимому, заботится о косвенности в анонимном типе, шагая по мере необходимости.

Стало ясно, что мне нужен не один, а два Join метода. Тот, который у меня работал просто отлично для финального соединения, и мне нужно было добавить еще один для промежуточных соединений. Помните, что метод, который у меня уже был, возвращает TableView:

public TableView Join(Table inner,
                      FakeFunc<Table, Projection> outerKeySelector,
                      FakeFunc<Table, Projection> innerKeySelector,
                      FakeFunc<Table, Table, Projection[]> resultSelector){
    Table join = new JoinedTable(this, inner,
        new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
    return join.Select(resultSelector(this, inner));
}

Теперь мне нужно было добавить тот, который возвращал что-нибудь с помощью метода Join, чтобы его можно было вызывать в цепочке:

public Table Join<T>(Table inner,
                     FakeFunc<Table, Projection> otherKeySelector,
                     FakeFunc<Table, Projection> innerKeySelector,
                     FakeFunc<Table, Table, T> resultSelector){
    Table join = new JoinedTable(this, inner,
        new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
    // calling resultSelector(this, inner) would give me the anonymous type,
    // but what would I do with it?
    return join;
}

Добавление этого метода сделало мои объединения почти работающими. Наконец-то я могу объединить три или более таблиц, но потерял псевдонимы:

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    join t3 in table3 on t1["ID"] equals t3["ID"]
                       // ^  error, 't1' isn't a member of 'Table'
    select new[]{t1["ID"], t2["Description"], t3["Foo"]};
               // ^         ^  error, 't1' & 't2' aren't members of 'Table'

Я почти остановился здесь. В конце концов, я мог бы обойти это, исключив эти потерянные псевдонимы:

TableView result =
    from t1 in table1
    join t2 in table2 on t1["ID"] equals t2["ID"]
    join t3 in table3 on table1["ID"] equals t3["ID"]
    select new[]{table1["ID"], table2["Description"], t3["Foo"]};

Это скомпилировало, запустило и дало ожидаемый результат. Woo-Hoo! Успех, вроде. Потеря псевдонимов была не идеальной, хотя. В реальном запросе таблицы могут быть более сложными:

TableView result =
    from t1 in table1
    join t2 in (
        from t in table2
        where t["Amount"] > 20
        select new[]{t["ID"], t["Description"]
    ).AsSubQuery() on t1["ID"] equals t2["ID"]
    join t3 in table3 on t1["ID"] equals t3["ID"]
    select new[]{table1["ID"], t2["Description"], t3["Foo"]};
                             // ^ error, 't2' isn't a member of 'Table'

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

После того, как я достаточно раз увидел сообщение "t1" не является членом таблицы ", я наконец понял, что секрет был в параметре outerKeySelector, равном Join. LINQ просто искал свойство с именем t1 (или что-то еще), которое было бы частью аргумента этой лямбды. Мои outerKeySelector параметры были объявлены так:

FakeFunc<Table, Projection> outerKeySelector

Класс Table определенно не имеет свойства с именем t1 или какого-либо другого псевдонима. Как я могу добавить это свойство? Если бы я использовал C # 4, я мог бы, возможно, использовать dynamic, чтобы сделать это, но если бы я использовал C # 4, тогда весь дизайн был бы другим (и я планирую повторить это позже в C # 4, для. Только для клиентов NET 4.0, использующих все преимущества динамической типизации для предоставления проекций столбцов в качестве фактических свойств таблиц). Однако в .NET 2.0 у меня нет динамических типов. Так, как я мог сделать тип, который имел псевдонимы таблицы в качестве свойств?

Какая минутка. Держи телефон. resultSelector уже возвращает мне один! Каким-то образом мне нужно держаться за этот объект и передать его outerKeySelector в следующем соединении. Но как? Я не могу просто сохранить его в своем классе JoinedTable, потому что этот класс не будет знать, как его разыграть.

Вот когда он ударил меня. Мне нужен общий промежуточный класс для хранения этих промежуточных результатов соединения. Он будет хранить ссылку на экземпляр JoinedTable, описывающую фактическое соединение, а также ссылку на этот анонимный тип, содержащий псевдонимы. Эврика!

Окончательная, полностью функциональная версия кода добавляет класс:

class IntermediateJoin<T>{
    readonly JoinedTable table;
    readonly T           aliases;

    public IntermediateJoin(JoinedTable table, T aliases){
        this.table   = table;
        this.aliases = aliases;
    }

    public TableView Join(Table inner,
                          FakeFunc<T, Projection> outerKeySelector,
                          FakeFunc<Table, Projection> innerKeySelector,
                          FakeFunc<T, Table, Projection[]> resultSelector){
        var join = new JoinedTable(table, inner,
            new EqualsCondition(outerKeySelector(aliases), innerKeySelector(inner)));
        return join.Select(resultSelector(aliases, inner));
    }
    public IntermediateJoin<U> Join<U>(Table inner,
                                       FakeFunc<T, Projection> outerKeySelector,
                                       FakeFunc<Table, Projection> innerKeySelector,
                                       FakeFunc<T, Table, U> resultSelector){
        var join = new JoinedTable(table, inner,
            new EqualsCondition(outerKeySelector(aliases), innerKeySelector(inner)));
        var newAliases = resultSelector(aliases, inner);
        return new IntermediateJoin<U>(join, newAliases);
    }
}

и этот метод для Table класса:

public IntermediateJoin<T> Join<T>(Table inner,
                      FakeFunc<Table, Projection> outerKeySelector,
                      FakeFunc<Table, Projection> innerKeySelector,
                      FakeFunc<Table, Table, T> resultSelector){
    var join = new JoinedTable(this, inner,
        new EqualsCondition(outerKeySelector(this), innerKeySelector(inner)));
    var x = resultSelector(this, inner);
    return new IntermediateJoin<T>(join, x);
}

Это обеспечивает полностью функциональный синтаксис объединения! *

Еще раз спасибо Томашу Петричеку за то, что он нашел время, чтобы прочитать и понять мой вопрос, и дал мне вдумчивый ответ.

* GroupJoin и SelectManyеще впереди, но я думаю, что знаю секреты достаточно хорошо, чтобы справиться с ними, сейчас.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...