Using an existing expression/func to build out another expression/func

c# entity-framework-core expression generics lambda

Question

I want to be able to use a generic method to select a property and pass that into the Any() method.

private List<TModel> _models;

public bool Any<TModel, TProperty>(
  Expression<Func<TModel, TProperty>> propertySelector,
  TModel model)
{
  // ....
}
// OR
public bool Any<TModel, TProperty>(
  Func<TModel, TProperty> propertySelector,
  TModel model)
{
  // ....
}

I'm not sure how to take the propertySelector and use it with the Any() on a List<TModel>.

This is close, but I'm missing something:

_models.Any(m => propertySelector(m) == propertySeletor(model));

Operator '==' cannot be applied to operands of type 'TProperty' and 'TProperty'

What am I missing here?

The question is more of a contrived example, as the Expression will ultimately be consumed by to build a query.

1
1
9/11/2019 12:38:14 AM

Accepted Answer

The Any methods in your question look like they were intended to be extension methods on a source sequence but they were missing the parameter for the target object.

Conceptually, this is what I believe you were looking to do:

public static bool Any<TEntity, TProperty>(this IEnumerable<TEntity> source, Func<TEntity, TProperty> selector, TEntity other)
{
    if (source is null)
        throw new ArgumentNullException(nameof(source));
    if (selector is null)
        throw new ArgumentNullException(nameof(selector));
    if (other == null)
        throw new ArgumentNullException(nameof(other));

    TProperty otherProperty = selector(other);

    return source.Any(item => EqualityComparer<TProperty>.Default.Equals(selector(item), otherProperty));
}

Now, that'll work on objects in memory, but since you have the tag, we'll need something that deals with expressions.

To do that, we must first evaluate the property to get the value against which we'll be comparing each entity's selected property. In memory, we just call selector(other) and we're done. Since we have to deal with an expression now, we need to compile the expression first. That's fairly simple.

The next step is to build an expression that represents selector(item) == otherValue. Fortunately, we don't need to replace the parameter since we're not trying to fold two separate lambdas into the same expression. We can simply reuse selector's first parameter. We then call Expression.Invoke with the selector and its parameter, which will be translated into the column reference in the SQL.

Finally, we build the lambda that we can pass along to the built-in Any method.

public static bool Any<TEntity, TProperty>(this IQueryable<TEntity> source, Expression<Func<TEntity, TProperty>> selectorExpression, TEntity other)
{
    if (source is null)
        throw new ArgumentNullException(nameof(source));
    if (selectorExpression is null)
        throw new ArgumentNullException(nameof(selectorExpression));
    if (other == null)
        throw new ArgumentNullException(nameof(other));

    ParameterExpression itemParameter = selectorExpression.Parameters[0];
    ConstantExpression otherValue = Expression.Constant(selectorExpression.Compile()(other), typeof(TProperty));

    BinaryExpression equalExpression = Expression.Equal(Expression.Invoke(selectorExpression, itemParameter), otherValue);

    Expression<Func<TEntity, bool>> predicate = Expression.Lambda<Func<TEntity, bool>>(equalExpression, itemParameter);

    return source.Any(predicate);
}

In my own testing, using SQL Profile to confirm, I was able to get the condition to evaluate in the SQL query. After changing the property selection expression, the query changed accordingly.

1
9/11/2019 2:19:06 AM

Popular Answer

If the question is how to build Expression<Func<TModel, bool>> from Expression<Func<TModel, TProperty>> and TModel representing equal predicate, it could be done like this:

// Expression<Func<TModel, TProperty>> propertySelector
// TModel model
var parameter = propertySelector.Parameters[0];
var left = propertySelector.Body;
var right = Expression.Invoke(propertySelector, Expression.Constant(model));
var body = Expression.Equal(left, right);
var predicate = Expression.Lambda<Func<TModel, bool>>(body, parameter);

The essential parts are the Expression.Equal (the expression equivalent of == operator) and the expression for invoking the property selector on the passed object instance.

In case the query provider does not support invocation expressions, it could be replaced with

var right = propertySelector.Body.ReplaceParameter(
    propertySelector.Parameters[0],
    Expression.Constant(model));

where ReplaceParameter is the usual ExpressionVisitor based helper for replacing ParameterExpression with another arbitrary expression (pretty much like string.Replace, but with expressions):

public static partial class ExpressionUtils
{
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
        => new ParameterReplacer { Source = source, Target = target }.Visit(expression);

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
            => node == Source ? Target : node;
    }
} 


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow