Search code examples
c#expressionexpression-trees

C# merge/combine expressions


With the following example classes (which could map to a database table):

class Package
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public bool Enabled { get; set; }

    public static Expression<Func<Package, bool>> IsActive =>
        x => x.Enabled;
}

class Schedule
{
    public DateTimeOffset Start { get; set; }
    public DateTimeOffset End { get; set; }
    public bool Enabled { get; set; }

    public static Expression<Func<Schedule, bool>> IsActive =>
        x => x.Enabled && x.Start > DateTimeOffset.Now;
}

and the following (which could be a join table):

class SchedulePackage
{
    public Package Package { get; init; }
    public Schedule Schedule { get; init; }
}

How can I merge the two expressions with AndAlso? An active SchedulePackage is one that both its package and schedule are active (whatever those mean). So, in order to avoid code duplication, it's better to reuse the IsActive logic of each entity.

Let's say I have the following:

public static Expression<Func<SchedulePackage, bool>> IsActive =>
    x => x.Package.Enabled && x.Schedule.Enabled && x.Schedule.Start > DateTimeOffset.Now;

Now if I update the package logic

public static Expression<Func<Package, bool>> IsActive =>
    x => x.Enabled && x.Price > 0;

I have to update SchedulePackage as well:

public static Expression<Func<SchedulePackage, bool>> IsActive =>
    x => x.Package.Enabled && x.Package.Price > 0 && x.Schedule.Enabled && x.Schedule.Start > DateTimeOffset.Now;

Solution

  • In the context of Entity Framework (or other LINQ to Entities applications) it is generally not possible for one expression to invoke another for reasons explained in "The LINQ expression node type 'Invoke' is not supported in LINQ to Entities" - stumped!. Instead what you can do is subclass ExpressionVisitor to create modified copies of the bodies of Schedule.IsActive and Package.IsActive that take SchedulePackage.Schedule and SchedulePackage.Package as inputs, then compose them with a binary && expression for the final result.

    To accomplish this, first create the following extension methods for composing functional expressions:

    public static partial class ExpressionExtensions
    {
        // Compose two Func<Tx, Ty> expressions with compatible generic parameters into a third.
        public static Expression<Func<T1, TResult>> Compose<T1, T2, TResult>(this Expression<Func<T2, TResult>> outer, Expression<Func<T1, T2>> inner) =>
            Expression.Lambda<Func<T1, TResult>>(
                new ParameterReplacer((outer.Parameters[0], inner.Body)).Visit(outer.Body), 
                false, inner.Parameters[0]);    
    
        // Compose a Func<T2, T3, TResult> expression with compatible Func<T1, T2> and Func<T1, T3> expressions to obtain a Func<T1, TResult> expression.
        public static Expression<Func<T1, TResult>> Compose<T1, T2, T3, TResult>(this Expression<Func<T2, T3, TResult>> outer, Expression<Func<T1, T2>> inner1, Expression<Func<T1, T3>> inner2)
        {
            var inner2body = new ParameterReplacer((inner2.Parameters[0], (Expression)inner1.Parameters[0])).Visit(inner2.Body);
            return Expression.Lambda<Func<T1, TResult>>(
                new ParameterReplacer((outer.Parameters[0], inner1.Body), (outer.Parameters[1], inner2body)).Visit(outer.Body), 
                false, inner1.Parameters[0]);   
        }
    }
    
    class ParameterReplacer : ExpressionVisitor
    {
        // Replace formal parameters (e.g. of a lambda body) with some containing expression in scope.
        readonly Dictionary<ParameterExpression, Expression> parametersToReplace;
        public ParameterReplacer(params (ParameterExpression parameter, Expression replacement) [] parametersToReplace) =>
            this.parametersToReplace = parametersToReplace.ToDictionary(p => p.parameter, p => p.replacement);
        protected override Expression VisitParameter(ParameterExpression p) => 
            parametersToReplace.TryGetValue(p, out var e) ? e : base.VisitParameter(p);
    }
    

    And now you can write SchedulePackage.IsActive as follows:

    static Lazy<Expression<Func<SchedulePackage, bool>>> IsActiveExpression = new(static () => {
        var left = Package.IsActive.Compose((SchedulePackage sp) => sp.Package);
        var right = Schedule.IsActive.Compose((SchedulePackage sp) => sp.Schedule);
        Expression<Func<bool, bool, bool>> binary = (b1, b2) => b1 && b2;
        return binary.Compose(left, right);
    });
    
    public static Expression<Func<SchedulePackage, bool>> IsActive => IsActiveExpression.Value;
    

    The resulting expression looks like:

    sp => (sp.Package.Enabled AndAlso (sp.Schedule.Enabled AndAlso (sp.Schedule.Start > DateTimeOffset.Now)))
    

    Note that the inner IsActive expressions have been inlined rather than invoked.

    I am lazily constructing the expression once and reusing it purely for performance reasons.

    Demo fiddle here.