Last active
December 9, 2022 13:40
-
-
Save markotny/92b41e84e8e7c84416846825fd445c55 to your computer and use it in GitHub Desktop.
IQueryable LeftJoin extension
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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