Home > Enterprise >  How can i do DRY for many similar joins in LINQ?
How can i do DRY for many similar joins in LINQ?

Time:09-17

I have an interface with ten fields and i often join two Queryables of this interface. Most of the time all of the ten fields have to be equal but sometimes not. Right now i solve this by having a lot of static extensionmethods but they all look kinda the same and with every variant i add i fear i will add an error (Wrong fields etc...).

public static class MyJoins
{
public static IQueryable<T> JoinOnAll<T, TA, TB>(this IQueryable<TA> query, IQueryable<TB> otherQuery)
    where TA : IMyInterface where TB : IMyInterface where T : class, IJoinInterface<TA, TB>, new()
{
    return query.Join(otherQuery,
        a => new { a.F1, a.F2, a.F3, a.F4, a.F5, a.F6, a.F7, a.F8, a.F9, a.F10 },
        b => new { b.F1, b.F2, b.F3, b.F4, b.F5, b.F6, b.F7, b.F8, b.F9, b.F10 },
        (t, p) => new T { AA = a, BB = b });
}

public static IQueryable<T> JoinOnAllButF2<T, TA, TB>(this IQueryable<TA> query, IQueryable<TB> otherQuery)
    where TA : IMyInterface where TB : IMyInterface where T : class, IJoinInterface<TA, TB>, new()
{
    return query.Join(otherQuery,
        a => new { a.F1,       a.F3, a.F4, a.F5, a.F6, a.F7, a.F8, a.F9, a.F10 },
        b => new { b.F1,       b.F3, b.F4, b.F5, b.F6, b.F7, b.F8, b.F9, b.F10 },
        (t, p) => new T { AA = a, BB = b });
}

public static IQueryable<T> JoinOnAllButF4F7<T, TA, TB>(this IQueryable<TA> query, IQueryable<TB> otherQuery)
    where TA : IMyInterface where TB : IMyInterface where T : class, IJoinInterface<TA, TB>, new()
{
    return query.Join(otherQuery,
        a => new { a.F1, a.F2, a.F3,       a.F5, a.F6,       a.F8, a.F9, a.F10 },
        b => new { b.F1, b.F2, b.F3,       b.F5, b.F6,       b.F8, b.F9, b.F10 },
        (t, p) => new T { AA = a, BB = b });
}
//and many more of this
}

I there a way to pass the fields to compare on as a parameter? (I think there is not) Are there other ways to solve this mass of nearly duplicated code?

----How i would use this-------------------------

I mainly use it in a way where i split querys, join them and then concat them:

IQueryable a = ......
IQueryable b = ......
var firstPart = a.Where(MyExpression).JoinOnAll(b);
var secondPart = a.Where(OtherExpression).JoinOnAllButF2(b);
var thirdPart = a.Where(AnotherExpression).JoinOnAllButF1F2F8(b);
var joinResult = firstPart.Concat(secondpart).Concat(thirdPart);
var joinResultFiltered = joinResult.WHere(AndAnotherExpression);
return joinResultFiltered;

I have many functions like this but the Expressions and joins are always different.

Additional Info because someone in the Commetns asked:

The interfaces are basically just this

public interface IMyInterface{
    public string F1 {get;set;}
    public string F2 {get;set;}
    // up to F10
}

CodePudding user response:

How about something like this:

public static class MyJoins
{
    private sealed class ReplacementVisitor : ExpressionVisitor
    {
        public ReplacementVisitor(LambdaExpression source, Expression toFind, Expression replaceWith)
        {
            SourceParameters = source.Parameters;
            ToFind = toFind;
            ReplaceWith = replaceWith;
        }
        
        private IReadOnlyCollection<ParameterExpression> SourceParameters { get; }
        private Expression ToFind { get; }
        private Expression ReplaceWith { get; }
        
        private Expression ReplaceNode(Expression node) => node == ToFind ? ReplaceWith : node;

        protected override Expression VisitConstant(ConstantExpression node) => ReplaceNode(node);

        protected override Expression VisitBinary(BinaryExpression node)
        {
            var result = ReplaceNode(node);
            if (result == node) result = base.VisitBinary(node);
            return result;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (SourceParameters.Contains(node)) return ReplaceNode(node);
            return SourceParameters.FirstOrDefault(p => p.Name == node.Name) ?? node;
        }
        
    }
    
    private static Expression<Func<T, object>> BuildJoinFields<T>(LambdaExpression fn) where T : IMyInterface
    {
        var p = Expression.Parameter(typeof(T), "p");
        var visitor = new ReplacementVisitor(fn, fn.Parameters[0], p);
        var body = visitor.Visit(fn.Body);
        return Expression.Lambda<Func<T, object>>(body, p);
    }
    
    public static IQueryable<T> JoinOn<T, TA, TB>(
        this IQueryable<TA> query, 
        IQueryable<TB> otherQuery, 
        Expression<Func<IMyInterface, object>> fieldsToJoinOn)
        where TA : IMyInterface
        where TB : IMyInterface,
        where T : class, IJoinInterface<TA, TB>, new()
    {
        Expression<Func<TA, object>> aFields = BuildJoinFields<TA>(fieldsToJoinOn);
        Expression<Func<TB, object>> bFields = BuildJoinFields<TB>(fieldsToJoinOn);
        return query.Join(otherQuery, aFields, bFields, (a, b) => new T { AA = a, BB = b });
    }
}
query.JoinOn(otherQuery, x => new { x.F1, x.F2, x.F3, ... })

Although I'm not convinced the compiler will be able to infer the type for type parameter T.

CodePudding user response:

Another solution. It does not care about interfaces, join key is based on outer type. And if property not found inner type, it will throw exception.

public static class QueryableExtensions
{
    public interface IJoinInterface<TA, TB>
    {
        public TA AA { get; set; }
        public TB BB { get; set; }
    }
    class JoinHandler<TA, TB> : IJoinInterface<TA, TB>
    {
        public TA AA { get; set; }
        public TB BB { get; set; }
    }

    public static IQueryable<IJoinInterface<TA, TB>> JoinOn<TA, TB, TKey>(
        this IQueryable<TA> outer, 
        IQueryable<TB> inner, 
        Expression<Func<TA, TKey>> joinKey)
    {
        var innerParam = Expression.Parameter(typeof(TB), "inner");
        var innerKey   = BuildKey(joinKey, innerParam);

        Expression<Func<TA, TB, IJoinInterface<TA, TB>>> resultExpression = (a, b) => new JoinHandler<TA, TB> {AA = a, BB = b};

        var queryExpression = Expression.Call(typeof(Queryable), nameof(Queryable.Join),
            new[] { typeof(TA), typeof(TB), typeof(TKey), typeof(IJoinInterface<TA, TB>) }, outer.Expression, inner.Expression,
            joinKey, innerKey, resultExpression);

        return outer.Provider.CreateQuery<IJoinInterface<TA, TB>>(queryExpression);
    }

    private static LambdaExpression BuildKey(LambdaExpression source, ParameterExpression param)
    {
        var body = new MemberReplacer(source.Parameters[0], param).Visit(source.Body);
        return Expression.Lambda(body, param);
    }

    class MemberReplacer : ExpressionVisitor
    {
        public MemberReplacer(ParameterExpression sourceParam, ParameterExpression destParam)
        {
            SourceParam = sourceParam;
            DestParam   = destParam;
        }

        public ParameterExpression SourceParam { get; }
        public ParameterExpression DestParam { get; }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression == SourceParam)
            {
                if (DestParam.Type == SourceParam.Type)
                    return node.Update(DestParam);

                var destProp = DestParam.Type.GetProperty(node.Member.Name);
                if (destProp == null)
                    throw new ArgumentException($"Type '{DestParam.Type.Name}' has no property '{node.Member.Name}'.");

                return Expression.MakeMemberAccess(DestParam, destProp);
            }

            return base.VisitMember(node);
        }
    }
}

Solution is closer to previous answer, but more universal and do not care about specific interfaces realizations.

  • Related