I’m writing a data access layer. It will have C# 2 and C# 3 clients, so I’m compiling against the 2.0 framework. Although encouraging the use of stored procedures, I’m still trying to provide a fairly complete ability to perform ad-hoc queries. I have this working fairly well, already.
For the convenience of C# 3 clients, I’m trying to provide as much compatibility with LINQ query syntax as I can. Jon Skeet noticed that LINQ query expressions are duck typed, so I don’t have to have an IQueryable and IQueryProvider (or IEnumerable<T>) to use them. I just have to provide methods with the correct signatures.
So I got Select, Where, OrderBy, OrderByDescending, ThenBy, and ThenByDescending working. Where I need help are with Join and GroupJoin. I’ve got them working, but only for one join.
A brief compilable example of what I have is this:
// .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;
}
}
This allows me to use a fairly decent syntax in C# 2:
Table table1 = new Table();
Table table2 = new Table();
TableView result =
table1
.Join(table2, table1["ID"] == table2["ID"])
.Select(table1["ID"], table2["Description"]);
But an even nicer syntax in C# 3:
TableView result =
from t1 in table1
join t2 in table2 on t1["ID"] equals t2["ID"]
select new[]{t1["ID"], t2["Description"]};
This works well and gives me identical results to the first case. The problem is if I want to join in a third table.
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"]};
Now I get an error (Cannot implicitly convert type ‘AnonymousType#1’ to ‘Projection[]’), presumably because the second join is trying to join the third table to an anonymous type containing the first two tables. This anonymous type, of course, doesn’t have a Join method.
Any hints on how I can do this?
This is a very interesting design, I like it!
As you’re saying, the problem is that your definition of the
Joinmethod is too specific. The key difference between your definition and the one in LINQ is following:When LINQ compiles a query with multiple
joinclauses, it calls them in a sequence and it generatesresultSelectorfor the first one automatically – and the generated code returns a simple anonymous type containing the elements from both of the source tables. So, if I’m correct, in your case, the generated anonymous type would look like this:This is unfortunately incompatible with
Projection[](even though semantically, the difference isn’t big). I’m afraid that the only way to solve this would be to use dynamic type casts and Reflection.You need to modify
Joinso that it has generic type parameterTResultused in theresultSelector.In the
Joinmethod you’d runTResult res = resultSelector(...)and then you need to do something with theresvalue.If
res is Projection[]then you can use your existing code (this is the case that will be used in a query that contains a singlejoinclause)resis an anonymous type like the one above. This means that you’ll need to use reflection to get values of the properties of the type and turn them into an array ofProjectionvalues (and then do the same thing as you’re doing now).I didn’t try implementing that, but I think it may work…