2010-04-07 18 views
2

我在寫數據訪問層。它將擁有C#2和C#3客戶端,所以我正在編譯2.0框架。儘管鼓勵使用存儲過程,但我仍然試圖提供一種相當完備的功能來執行臨時查詢。我已經很好地工作了。將LINQ嫁接到C#2庫

爲了方便C#3客戶端,我試圖儘可能多地提供與LINQ查詢語法的兼容性。 Jon Skeet noticed LINQ查詢表達式是鴨子鍵入的,所以我不要有一個IQueryableIQueryProvider(或IEnumerable<T>)使用它們。我只需提供具有正確簽名的方法。

所以我就SelectWhereOrderByOrderByDescendingThenByThenByDescending工作。我需要幫助的地方是JoinGroupJoin。我有他們的工作,但只有一個加入。

什麼,我有一個簡短的編譯的例子是這樣的:

// .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」隱式轉換爲「投影[]」),大概是因爲第二個連接正在嘗試加入第三個表包含前兩個匿名類型表。這種匿名類型當然沒有Join方法。

任何提示我如何做到這一點?

回答

2

這是一個非常有趣的設計,我喜歡它! 正如你所說,問題在於你對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,這樣它在resultSelector使用泛型類型參數TResult

  • Join方法中,您將運行TResult res = resultSelector(...),然後您需要使用res值進行操作。

  • 如果res is Projection[]那麼你可以使用現有的代碼(這是將在包含單個join子句的查詢中使用的情況下)

  • 在其他情況下,res是一個匿名類型像一個以上。這意味着您需要使用反射來獲取類型屬性的值,並將它們轉換爲一個Projection值的數組(然後執行與您現在所做的相同的操作)。

我沒有嘗試實現這一點,但我認爲它可能工作...

+0

謝謝你,你指出我的正確道路。考慮到你的建議,我開始用IEnumerables嘲笑事物,並在Reflector中四處探索。經過足夠的戳動,我想出了我需要做的事情。好消息:不需要運行時反射。不過,它需要額外的課程。我會在另一個答案中發佈完整的細節。不過,你會得到接受的答案和+1。 – 2010-04-07 06:28:30

+0

@P爸爸:我很高興我的答案有幫助 - 我沒有意識到你可以通過添加一個需要數組的重載來從匿名類型中恢復 - 這是一個很好的竅門! – 2010-04-07 13:24:17

0

以下是有點長。如果你只是想讓這個工作,而不關心爲什麼或如何,然後跳到最後兩個代碼部分。


托馬斯Petricek的answer是走對了方向,但只有約一半在那裏。 resultSelectorTResult確實需要是通用的。連接確實是鏈接的,中間結果包含由每個連接的左側和右側部分組成的匿名類型(分別稱爲外部和內部)。

讓我們看看我的查詢從之前:

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

這個編譯,運行併產生了預期結果。真厲害!成功,有點。雖然失去別名並不理想。在實際的查詢,該表可能會更復雜:

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(或其他)的屬性,這是該lambda參數的成員。我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); 
} 

這提供了全功能連接語法!*

再次感謝托馬斯佩特里切克花時間閱讀和理解我的問題,並給了我一個深思熟慮的答案。

*GroupJoinSelectMany的還在後面,但我想我知道的祕密不夠好應對這些,現在。