At the bottom is an example of a left join implementation. It will work for normal lists and arrays but it will not work with LINQ to entities since I use Expression.Invoke()
.
What I'm trying to achieve is modifying/wrapping the resultSelector
input to accept a single instance of the anonymous class 'a
(used by the leftJoin
queryable) instead of two separate parameters.
Inspecting resultSelector
it seems I will want to create a modified version of it that has one parameter of type 'a
and two arguments extracted from the properties Outer
and Inner
on 'a
.
How do I go about doing this modification? How do I change Arguments
?
[TestClass]
public class LeftJoinTests
{
public class Outer
{
public int Key { get; }
public int? ForeignKey { get; }
public Outer(int key, int? foreignKey)
{
Key = key;
ForeignKey = foreignKey;
}
}
public class Inner
{
public int Key { get; }
public string Data { get; }
public Inner(int key, string data)
{
Key = key;
Data = data;
}
}
[TestMethod]
public void LeftJoinTest()
{
var outers = new []
{
new Outer(1, 1),
new Outer(2, 2),
new Outer(3, 3),
new Outer(4, null),
};
var inners = new []
{
new Inner(5, "5"),
new Inner(2, "2"),
new Inner(1, "1")
};
var leftJoin = LeftJoin(outers.AsQueryable(), inners.AsQueryable(), o => o.ForeignKey, i => i.Key, (oooo, iiii) => new { Outer = oooo, Inner = iiii }).ToArray();
Assert.AreEqual(4, leftJoin.Length);
Assert.AreSame(outers[0], leftJoin[0].Outer);
Assert.AreSame(outers[1], leftJoin[1].Outer);
Assert.AreSame(outers[2], leftJoin[2].Outer);
Assert.AreSame(outers[3], leftJoin[3].Outer);
Assert.AreSame(inners[2], leftJoin[0].Inner);
Assert.AreSame(inners[1], leftJoin[1].Inner);
Assert.IsNull(leftJoin[2].Inner);
Assert.IsNull(leftJoin[3].Inner);
}
public IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
IQueryable<TOuter> outer,
IQueryable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner, TResult>> resultSelector)
{
var leftJoin = outer.GroupJoin(
inner,
outerKeySelector,
innerKeySelector,
(o, i) => new
{
Outer = o,
Inners = i
}).SelectMany(
oi => oi.Inners.DefaultIfEmpty(),
(oi, i) => new
{
oi.Outer,
Inner = i
}
);
// Break the anonymous type of the left join into the two parameters needed for the resultSelector
var anonymousType = leftJoin.GetType().GetGenericArguments()[0];
var parameter = Expression.Parameter(anonymousType, "oi");
var outerProperty = Expression.Property(parameter, "Outer");
var outerLambda = Expression.Lambda(outerProperty, parameter);
var innerProperty = Expression.Property(parameter, "Inner");
var innerLambda = Expression.Lambda(innerProperty, parameter);
var wrapper = Expression.Lambda(Expression.Invoke(resultSelector, new Expression[] {
Expression.Invoke(outerLambda, parameter),
Expression.Invoke(innerLambda, parameter)
}), parameter);
Expression<Func<TAnonymous, TResult>> Cast<TAnonymous>(Expression expression, IQueryable<TAnonymous> queryable)
{
return expression as Expression<Func<TAnonymous, TResult>>;
}
var typeSafeWrapper = Cast(wrapper, leftJoin);
return leftJoin.Select(typeSafeWrapper);
}
}