Skip to content

Instantly share code, notes, and snippets.

@markotny
Last active December 9, 2022 13:40
Show Gist options
  • Save markotny/92b41e84e8e7c84416846825fd445c55 to your computer and use it in GitHub Desktop.
Save markotny/92b41e84e8e7c84416846825fd445c55 to your computer and use it in GitHub Desktop.
IQueryable LeftJoin extension
using System.Linq.Expressions;
namespace LeftJoin;
public static class ExpressionExtensions
{
/// <inheritdoc cref="Compose{TFunc}"/>
public static Expression<Func<P1, TRes>> Compose<P1, TP1, TRes>(
this Expression<Func<TP1, TRes>> function,
Expression<Func<P1, TP1>> firstParamSelector)
{
return function.Compose<Func<P1, TRes>>(firstParamSelector);
}
/// <inheritdoc cref="Compose{TFunc}"/>
public static Expression<Func<P1, P2, TRes>> Compose<P1, P2, TP1, TP2, TRes>(
this Expression<Func<TP1, TP2, TRes>> function,
Expression<Func<P1, P2, TP1>> firstParamSelector,
Expression<Func<P1, P2, TP2>> secondParamSelector)
{
return function.Compose<Func<P1, P2, TRes>>(firstParamSelector, secondParamSelector);
}
/// <summary>
/// Provides parameters for <paramref name="function" /> expression creating new, combined expression of signature <typeparamref name="TFunc" />
/// Use <see cref="Compose{P1, TP1, TRes}" /> for functions with single argument mapping or <see cref="Compose{P1, P2, TP1, TP2, TRes}" /> for functions with two arguments.
/// </summary>
/// <typeparam name="TFunc">Resulting Func signature. Arguments must be the same as in <paramref name="paramSelectors"/> and return type must be the same as in <paramref name="function"/></typeparam>
/// <param name="function">Func accepting params provided by <paramref name="paramSelectors"/> and returning the same type as <typeparamref name="TFunc"/></param>
/// <param name="paramSelectors">Array of Func for each <paramref name="function"/> parameter. Must accept the same arguments as <typeparamref name="TFunc"/></param>
/// <returns>Expression <paramref name="function"/> with each parameter replaced by <paramref name="paramSelectors"/></returns>
/// <example>
/// <code>
/// Expression{Func{string, int}} target;
/// Expression{Func{int, int}} timesTwo = x =} x * 2;
/// Expression{Func{string, int}} toInt = x =} Int32.Parse(x);
/// target = timesTwo.Compose(toInt);
/// target.Compile()("4") == 8
/// </code>
/// </example>
private static Expression<TFunc> Compose<TFunc>(
this LambdaExpression function,
params LambdaExpression[] paramSelectors)
{
SubstExpressionVisitor visitor = new();
var parameters = paramSelectors.First().Parameters;
foreach (var selector in paramSelectors.Skip(1))
foreach (var (p, i) in parameters.WithIndex())
visitor.Substitute[selector.Parameters[i]] = p;
foreach (var (s, i) in paramSelectors.WithIndex())
visitor.Substitute[function.Parameters[i]] = visitor.Visit(s.Body);
return Expression.Lambda<TFunc>(visitor.Visit(function.Body), parameters);
}
}
internal class SubstExpressionVisitor : ExpressionVisitor
{
public Dictionary<Expression, Expression> Substitute = new();
protected override Expression VisitParameter(ParameterExpression node)
{
if (Substitute.TryGetValue(node, out var newValue))
{
return newValue;
}
return node;
}
}
using System.Linq.Expressions;
namespace LeftJoin;
public static class QueryableExtensions
{
public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
this IQueryable<TOuter> outer,
IQueryable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner?, TResult>> resultSelector)
{
return outer
.GroupJoin(inner,
outerKeySelector, innerKeySelector,
(outer, inner) => new Intermediate<TOuter, TInner> { Outer = outer, Inner = inner })
.SelectMany(
x => x.Inner.DefaultIfEmpty(),
resultSelector.Compose((Intermediate<TOuter, TInner> x, TInner? inner) => x.Outer, (x, inner) => inner));
}
internal class Intermediate<TOuter, TInner>
{
public TOuter Outer { get; set; } = default!;
public IEnumerable<TInner> Inner { get; set; } = default!;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment