3

I need to programmatically recognize when an indexer occurs within an expression, but the resulting expression tree is not what I expected.

class IndexedPropertiesTest
{
    static void Main( string[] args ) { new IndexedPropertiesTest(); }

    public string this[int index]
    {
        get { return list[index]; }
        set { list[index] = value; }
    }
    List<string> list = new List<string>();

    public IndexedPropertiesTest()
    {
        Test( () => this[0] );
    }

    void Test( Expression<Func<string>> expression )
    {
        var nodeType = expression.Body.NodeType;
        var methodName = ((MethodCallExpression)expression.Body).Method.Name;
    }
}

In the above code, nodeType is "Call" and methodName is "get_Item". Why? Shouldn't expression.Body be equivalent to Expression.Property( Expression.Constant( this ), "Item", Expression.Constant( 0 ) )? That's what I expected.

I need the ability to detect an indexer in a very general way - given just about any expression. This mangling of the intended expression tree compromises my ability to do that. Relying on the method name being "get_Item" is far too brittle. Plus, IndexerNameAttribute may have been used to rename the indexer property anyway.

So is there anyway to get the compiler to generate the intended expression tree? Please don't suggest manually building the expression, as that is not an option. Or is there any way to programmatically be sure that what I have is an indexer?

HappyNomad
  • 4,458
  • 4
  • 36
  • 55
  • 2
    `I need to programmatically recognize when an indexer occurs within an expression` are you sure? Because not all .NET languages even have a construct called an indexer. That's why the method get_Item is created internally. What would you expect to happen if your code gets called from VB.NET? – nvoigt Jul 14 '15 at 18:00
  • @nvoigt VB.NET has *multiple* indexers, which I'd like my code to recognize as well. So for langauges that *do* have indexers, why doesn't the compiler generate an `IndexExpression`? How are languages that lack indexers important to this question? I think what's important is that the underlying reflection model includes indexed properties as seen at [Property.GetValue](https://msdn.microsoft.com/en-us/library/b05d59ty.aspx). – HappyNomad Jul 14 '15 at 19:28

2 Answers2

3

I think you have your answer: That's the way the C# compiler works.

I translated your code into VB.NET. Unhelpfully enough, VB.NET doesn't call the method get_Item, rather it calls it by the name you give it. In the below example, that ends up with get_MyDefaultProperty.

Sub Main
    Dim x as IndexedPropertiesTest = New IndexedPropertiesTest()

End Sub

' Define other methods and classes here
Class IndexedPropertiesTest

    Private list as New List(Of String) From { "a" }
    Default Property MyDefaultProperty(index as Integer) as String
        Get
            Return list(index)
        End Get
        Set(value as String)
            list(index) = value
        End Set
    End Property

    Public Sub New
        Test( Function() Me(0))
    End Sub

    Public Sub Test(expression as Expression(Of Func(Of String)))

        Dim nodeType as ExpressionType = expression.Body.NodeType
        Dim methodName as String = CType(expression.Body, MethodCallExpression).Method.Name

        'expression.Dump() 'Using LINQPad

    End Sub

End Class

However, all is not lost: You can write a Visitor to try to stuff the get_Item call back into an IndexExpression. I started on it here:

public class PropertyFixerVisitor : ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.Name.StartsWith("get_"))
        {
            var possibleProperty = node.Method.Name.Substring(4);
            var properties = node.Method.DeclaringType.GetProperties()
                .Where(p => p.Name == possibleProperty);

            //HACK: need to filter out for overriden properties, multiple parameter choices, etc.
            var property = properties.FirstOrDefault();
            if (property != null)
                return Expression.Property(node.Object, possibleProperty, node.Arguments.ToArray());
            return base.VisitMethodCall(node);

        }
        else
            return base.VisitMethodCall(node);
    }
}

You can then safely modify your Test method as so:

void Test(Expression<Func<string>> expression)
{
    var visitor = new PropertyFixerVisitor();
    var modExpr = (Expression<Func<string>>)visitor.Visit(expression);

    var indexExpression = (modExpr.Body as IndexExpression); //Not Null
}
Shlomo
  • 14,102
  • 3
  • 28
  • 43
  • 1
    Your `Visitor` code helped get me on track to overcoming the issue. Only thing missing is an explanation as to why the compiler mutilates indexers. But I suppose only Microsoft can answer that. – HappyNomad Jul 16 '15 at 13:42
  • I was rather curious myself frankly. I looked, and [this answer](http://stackoverflow.com/questions/3340500/how-does-one-create-a-net-expression-with-nodetype-of-expressiontype-index) turns up a clue: [`IndexExpression`](https://msdn.microsoft.com/en-us/library/system.linq.expressions.indexexpression.aspx) didn't exist in .NET 3.5, it was one of the many `Expression` classes added to 4.0. Those added expression classes (like `AssignExpression`) where never integrated into the compiler. You can't for example use an assignment in an expression, you have to build it using Expression classes. – Shlomo Jul 16 '15 at 14:18
0

I've recently faced the same problem and ended up with the following solution (given your example):

void Test(Expression<Func<string>> expression)
{
    if (expression.Body.NodeType == ExpressionType.Call)
    {
        var callExpression = (MethodCallExpression)expression.Body;
        var getMethod = callExpression.Method;
        var indexer = getMethod.DeclaringType.GetProperties()
            .FirstOrDefault(p => p.GetGetMethod() == getMethod);
        if (indexer == null)
        {
            // Not indexer access
        }
        else
        {
            // indexer is a PropertyInfo accessed by expression
        }
    }
}

So, basically, instead of relying on indexer to be named in a particular way, I rely on the following:

  1. Two objects of MethodInfo can be compared to equality (or unequality) (operator == and operator != are both implemented for MethodInfo).
  2. The indexer has a get method. In general case a property may have no get method but such a property cannot be used to construct an expression with lambda syntax in the first place.
  3. Access to non-indexer-property yields MemberExpression instead of MethodCallExpression (and even if it wouldn't, PropertyInfo representing simple property can always be distinguished from one representing indexer with GetIndexParameters method, since all indexers have at least one parameter).

This approach also works if multiple overloads of indexer exst in a class, since each of them has its own MethodInfo and only one will be equal to the one, used in the expression.

Note: The method above will neither work with private indexer nor with indexer with private get method. To generalize the approach one should use correct overloads of GetProperties and GetGetMethod:

// ...
var indexer = getMethod.DeclaringType.GetProperties(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
    .FirstOrDefault(p => p.GetGetMethod(nonPublic: true) == getMethod);
// ...

Hope that will help someone.