Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variance in Expression<Func<T,bool>>

Just a quick and short one, this time. Func<T,TResult> is contravariant (EDIT : The Type Parameter T is). Now, I don't work with Func<T,TResult>, but rather with Expression<Func<T,TResult>>, and seem to have reached a dead end. UPDATE - FULL CODE SAMPLE :

public interface IColoredObject
{
    string Color { get; }
}

public class Item : IColoredObject
{
    public string Color { get; set; }

    public double Price { get; set; }
}

public partial class MainWindow : Window
{
    private IList<Item> _items;

    public IList<Item> Items
    {
        get
        {
            if (_items == null)
            {
                _items = new List<Item>();
                _items.Add(new Item() { Color = "black" });
                _items.Add(new Item() { Color = "blue" });
                _items.Add(new Item() { Color = "red" });
            }
            return _items;
        }
    }

    public MainWindow()
    {
        InitializeComponent();
        Expression<Func<IColoredObject, bool>> filter = x => x.Color == "black";
        Item i = Get(filter);
    }

    public Item Get(Expression<Func<Item, bool>> filter)
    {
        return Items.AsQueryable().Where(filter).FirstOrDefault();
    }
}

The call is made using an Expression<Func<IColoredObject, bool>> as argument and should, if I haven't misunderstood contravariance, work, because IColoredObject is less derived that Item.

What I get is a conversion Exception saying something like

cannot convert

System.Linq.Expressions.Expression`1[System.Func`2[MyNs.IColoredObject,System.Boolean]]

To

System.Linq.Expressions.Expression`1[System.Func`2[MyNs.Item,System.Boolean]]

Is there any way of fixing this and getting it to work?

EDIT:

Since there's some inaccuracy in what I've said, here's more background. Code sample updated. Furthermore, I checked what MSDN said about Func<T, TRes>:

public Item GetFunc(Func<Item, bool> filter)
{
    return Items.AsQueryable().Where(filter).FirstOrDefault();
}

As indicated by MS, this can be used with a contravariant Type param, as listed below:

 Func<IColoredObject, bool> filterFunc = x => x.Color == "black";
 GetFunc(filterFunc);

Which again makes me wonder why this works for Func<T, TRes> but not for Expression<Func<T, TRes>>...

FINALLY...

The checked answer was selected because it is what I eventually did. As I said somewhere in the comments below, the Get-Method utilizes NHibernate to fetch data. But obviously NHibernate has a feature of accepting queries over an interface and auto-selecting the types that implement the interface. This does not solve the issue itself, but as you can read below, there is not real solution, since what encountered here was expected behaviour.

like image 273
Sebastian Edelmeier Avatar asked Jul 12 '12 13:07

Sebastian Edelmeier


1 Answers

Expression<TDelegate> is a class so it can't have variance for generic parameters. There is also a big distinction between Delegate and Expression<Delegate>. While you can pass Item object into Func<IColoredObject, bool> and thus can convert Func<IColoredObject, bool> into Func<Item, bool>, Expression<Func<Item, bool>> is just like the code of the method taking Item and returning bool. You can analyze and change this code adding Item specific methods and properties, obviously this would not be possible for code working with IColoredObject.

It might be possible to change all entries of IColoredObject parameter in the Expression<Func<IColoredObject, bool>> into Item object using ExpressionVisitor. Below is a visitor which performs such conversion in simple cases (i.e. no explicit interface implementations). Probably, there is much more simple solution to your problem, but it's hard to find it without knowing further details.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

interface IGizmo
{
    bool Frobnicate();
}

class Gizmo : IGizmo
{
    public bool Frobnicate()
    {
        Console.WriteLine("Gizmo was frobnicated!");

        return true;
    }
}

public sealed class DelegateConversionVisitor : ExpressionVisitor
{
    IDictionary<ParameterExpression, ParameterExpression> parametersMap;

    public static Expression<Func<T2, TResult>> Convert<T1, T2, TResult>(Expression<Func<T1, TResult>> expr)
    {
        var parametersMap = expr.Parameters
            .Where(pe => pe.Type == typeof(T1))
            .ToDictionary(pe => pe, pe => Expression.Parameter(typeof(T2)));

        var visitor = new DelegateConversionVisitor(parametersMap);
        var newBody = visitor.Visit(expr.Body);

        var parameters = expr.Parameters.Select(visitor.MapParameter);

        return Expression.Lambda<Func<T2, TResult>>(newBody, parameters);
    }

    public DelegateConversionVisitor(IDictionary<ParameterExpression, ParameterExpression> parametersMap)
    {
        this.parametersMap = parametersMap;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return base.VisitParameter(this.MapParameter(node));
    }

    private ParameterExpression MapParameter(ParameterExpression source)
    {
        var target = source;
        this.parametersMap.TryGetValue(source, out target);

        return target;
    }
}

class Program
{
    static void Main()
    {
        Expression<Func<IGizmo, bool>> expr = g => g.Frobnicate();

        var e2 = DelegateConversionVisitor.Convert<IGizmo, Gizmo, bool>(expr);

        var gizmo = new Gizmo();
        e2.Compile()(gizmo);
    }
}

like image 87
Konstantin Oznobihin Avatar answered Sep 28 '22 15:09

Konstantin Oznobihin