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 entity-framework-core to build a query.
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 entity-framework-core 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.
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;
}
}