Construyendo una expresión indexada a partir de otra expresión

asp.net-mvc c# expression-trees lambda

Pregunta

Guión

public class Element {
   public int Id {get;set;}
}

public class ViewModel {
   public IList<Element> Elements{get;set;}
}

Tengo un método con un parámetro de tipo Expression<Func<Element, int>> , que se parece a m => m.Id

Me gustaria transformar

m => m.Id (donde m es un elemento)

a

x => x.Elements[0].Id donde x es un ViewModel, y 0 es un parámetro "índice"

Lo que tengo ahora (por supuesto, es genérico, quité la parte genérica para mayor claridad)

public static class Helpers {
    public static Expression<Func<ViewModel, int>> BuildExpressionArrayFromExpression(
                this Expression<Func<Element, int>> expression,
                ViewModel model,
                int index = 0, 
                string bindingPropertyName = "Elements"//the name of the "List" property in ViewModel class
                ) 
    {
       var parameter = Expression.Parameter(typeof(ViewModel), "x");
       var viewModelProperty = model.GetType().GetProperty(bindingPropertyName);
       Expression member = parameter;//x => x
       member = Expression.Property(member, viewModelProperty);//x => x.Elements

       var test1 =  Expression.Property(member, "Item", new Expression[]{Expression.Constant(index)});
       //x => x.Elements.Item[0], and I don't want Item

       var test2 = Expression.Call(member, viewModelProperty.PropertyType.GetMethod("get_Item"), new Expression[] {Expression.Constant(index)});
       //x 0> x.Elements.get_Item(0), and I don't want get_Item(0)

       //code to add Id property to expression, not problematic
       return Expression.Lambda<Func<ViewModel, int>(member, parameter);
    }
}

EDITAR

Necesito x => x.Elements[0] y no x => x.Elements.Item[0] , porque la expresión resultante debe llamarse con un InputExtensions.TextBoxFor(<myIndexedExpression>)

Imagina una clase como esa

public class Test {
  public int Id {get;set;}
  public IList<Element> Elements {get;set;}
}

y una acción posterior

[HttpPost]
public ActionResult Edit(Test model) {
 bla bla bla.
}

Si los atributos de nombre de mis entradas no están bien generados, entonces tengo problemas de enlace (model.Elements está vacío en mi Acción posterior).

El atributo del nombre de mi entrada debe ser

Elements[0]PropertyName

y me dan (dependiendo de mis intentos)

PropertyName

o (tal vez no exacto, intento reproducir este caso)

Elements.Item[0].PropertyName

EDIT2

También probé una solución diferente, trabajando con ViewData.TemplateInfo.HtmlFieldPrefix, pero luego obtengo

Elements.[0].PropertyName

(y Elements_ 0 _PropertyName como Id).

El primer punto no es deseado en nombre, y el primer "doble guión bajo" debe ser uno simple en id.

Realmente utilizo esta solución, trabajando con regex (argh) para eliminar los no deseados. y _, pero me gustaría evitar esto.

Respuesta aceptada

Esto es solo una cuestión de la representación de cadena del árbol de expresión, y no puedes cambiar eso. La expresión del árbol que estás construyendo está bien. Puedes ver el mismo efecto si usas una expresión lambda para construir el árbol de expresiones:

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

class Test
{
    public static void Main()
    {
        Expression<Func<List<string>, string>> expression = list => list[0];
        Console.WriteLine(expression);
    }
}

Salida:

list => list.get_Item(0)

Me sorprendería mucho que el problema al que te enfrentas sea el resultado de llamar a ToString() en el árbol de expresiones. En lugar de decirnos el resultado que cree que necesita y la vaga justificación de "Lo necesito por razones de enlace de MVC", debe explicar qué es lo que realmente está mal. Sospecho fuertemente que el problema no es donde crees que está.


Respuesta popular

Expression<Func<Element, int>> expr1 =
    m => m.Id;
Expression<Func<ViewModel, Element>> expr2 =
    x => x.Elements[0];

Expression<Func<ViewModel, int>> result =
    expr1.ComposeWith(expr2);

Resultado:

expr1 = m => m.Id
expr2 = x => x.Elements.get_Item(0)
result = x => x.Elements.get_Item(0).Id

Reemplaza el parámetro de expr1 ( m ) con el cuerpo de expr2 ( x.Elements[0] ), y reemplaza el parámetro de entrada con el de expr2 ( x ).

Método de extensión ComposeWith :

public static class FunctionalExtensions
{
    public static Expression<Func<TInput,TResult>> ComposeWith<TInput,TParam,TResult>(
        this Expression<Func<TParam,TResult>> left, Expression<Func<TInput,TParam>> right)
    {
        var param = left.Parameters.Single();

        var visitor = new ParameterReplacementVisitor(p => {
            if (p == param)
            {
                return right.Body;
            }
            return null;
        });

        return Expression.Lambda<Func<TInput,TResult>>(
            visitor.Visit(left.Body),
            right.Parameters.Single());
    }

    private class ParameterReplacementVisitor : ExpressionVisitor
    {
        private Func<ParameterExpression, Expression> _replacer;

        public ParameterReplacementVisitor(Func<ParameterExpression, Expression> replacer)
        {
            _replacer = replacer;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var replaced = _replacer(node);
            return replaced ?? node;
        }
    }
}


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow