Cómo usar una expresión configurar una propiedad anidada?

.net c# expression-trees reflection

Pregunta

Así que tengo un código que establece una propiedad en un objeto. Este código es de una clase de validación interna que estamos usando en pruebas unitarias. Así que el código puede ser suministrado algo como

private static void SetDeepValue(object targetObject, Expression<Func<string>> propertyToSet, object valueToSet)
        {
            var underlyingProperty = ((PropertyInfo)((MemberExpression)propertyToSet.Body).Member);
            underlyingProperty.SetValue(targetObject, valueToSet);
        }

Este código se usa en un entorno de tipo de prueba unitaria, donde podemos hacer llamadas como

foreach (string currentTestCaseValue in TestCaseSets)
{
     BusinessObject myCustomer = new BusinessObject();
     SetDeepValue(myCustomer, ()=>myCustomer.FirstName,currentTestCaseValue);
     ValidateBusinessRules(myCustomer);
}

(código simplificado por brevedad / complejidad)

Sin embargo, ahora, debido a algunas refactorizaciones, nos quedamos con algo como:

foreach (string currentTestCaseValue in TestCaseSets)
    {
         BusinessObject myCustomer = new BusinessObject();
         SetDeepValue(myCustomer, ()=>myCustomer.NameInfo.First,currentTestCaseValue);
         ValidateBusinessRules(myCustomer);
    }

Cuando este código se ejecuta, obtenemos el error:

El objeto no coincide con el tipo de destino.

Sospecho que está intentando llamar a la First propiedad en BusinessObject , en lugar de la NameInfo . ¿Cómo puedo modificar mi código para manejar este caso 'anidado'?

Respuesta popular

Ahora que nos dio un ejemplo, es bastante fácil hacerlo. Sería inútil compilar la expresión de cualquier manera, porque no podemos reutilizarla, por lo que solo ralentizaría el método. Es más fácil recorrer la "cadena" de captadores y usar la reflexión para acceder a su valor. El método que escribí es compatible con ambos campos (a menudo utilizados como campos de readonly ) y propiedades.

public static void SetDeepValue<T>(object notUsed, Expression<Func<T>> propertyToSet, T valueToSet)
{
    List<MemberInfo> members = new List<MemberInfo>();

    Expression exp = propertyToSet.Body;
    ConstantExpression ce = null;

    // There is a chain of getters in propertyToSet, with at the 
    // beginning a ConstantExpression. We put the MemberInfo of
    // these getters in members and the ConstantExpression in ce

    while (exp != null)
    {
        MemberExpression mi = exp as MemberExpression;

        if (mi != null)
        {
            members.Add(mi.Member);
            exp = mi.Expression;
        }
        else
        {
            ce = exp as ConstantExpression;

            if (ce == null)
            {
                // We support only a ConstantExpression at the base
                // no function call like
                // () => myfunc().A.B.C
                throw new NotSupportedException();
            }

            break;
        }
    }

    if (members.Count == 0)
    {
        // We need at least a getter
        throw new NotSupportedException();
    }

    // Now we must walk the getters (excluding the last).
    // From the ConstantValue ce we take the base object
    object targetObject = ce.Value;

    // We have to walk the getters from last (most inner) to second
    // (the first one is the one we have to use as a setter)
    for (int i = members.Count - 1; i >= 1; i--)
    {
        PropertyInfo pi = members[i] as PropertyInfo;

        if (pi != null)
        {
            targetObject = pi.GetValue(targetObject);
        }
        else
        {
            FieldInfo fi = (FieldInfo)members[i];
            targetObject = fi.GetValue(targetObject);
        }
    }

    // The first one is the getter we treat as a setter
    {
        PropertyInfo pi = members[0] as PropertyInfo;

        if (pi != null)
        {
            pi.SetValue(targetObject, valueToSet);
        }
        else
        {
            FieldInfo fi = (FieldInfo)members[0];
            fi.SetValue(targetObject, valueToSet);
        }
    }
}

Lo usas así:

A a = new A();

SetDeepValue(a, () => a.B.C.Value, "Foo");

Tenga en cuenta que SetDeepValue no necesita, ni usa, targetObject , porque puede descubrirlo en la cadena de captadores:

SetDeepValue(myCustomer, ()=>myCustomer.FirstName, currentTestCaseValue);

Aquí tienes ()=>myCustomer .

Hubiera sido necesario si la llamada estuviera en el formulario.

SetDeepValue(myCustomer, x=>x.FirstName, currentTestCaseValue);

Incluso te daré un método que usa este segundo formato de Expression :

public static void SetDeepValue<TObject, T>(TObject target, Expression<Func<TObject, T>> propertyToSet, T valueToSet)
{
    List<MemberInfo> members = new List<MemberInfo>();

    Expression exp = propertyToSet.Body;

    // There is a chain of getters in propertyToSet, with at the 
    // beginning a ParameterExpression. We put the MemberInfo of
    // these getters in members. We don't really need the 
    // ParameterExpression

    while (exp != null)
    {
        MemberExpression mi = exp as MemberExpression;

        if (mi != null)
        {
            members.Add(mi.Member);
            exp = mi.Expression;
        }
        else
        {
            ParameterExpression pe = exp as ParameterExpression;

            if (pe == null)
            {
                // We support only a ParameterExpression at the base
                throw new NotSupportedException();
            }

            break;
        }
    }

    if (members.Count == 0)
    {
        // We need at least a getter
        throw new NotSupportedException();
    }

    // Now we must walk the getters (excluding the last).
    object targetObject = target;

    // We have to walk the getters from last (most inner) to second
    // (the first one is the one we have to use as a setter)
    for (int i = members.Count - 1; i >= 1; i--)
    {
        PropertyInfo pi = members[i] as PropertyInfo;

        if (pi != null)
        {
            targetObject = pi.GetValue(targetObject);
        }
        else
        {
            FieldInfo fi = (FieldInfo)members[i];
            targetObject = fi.GetValue(targetObject);
        }
    }

    // The first one is the getter we treat as a setter
    {
        PropertyInfo pi = members[0] as PropertyInfo;

        if (pi != null)
        {
            pi.SetValue(targetObject, valueToSet);
        }
        else
        {
            FieldInfo fi = (FieldInfo)members[0];
            fi.SetValue(targetObject, valueToSet);
        }
    }
}

Puedes comparar los dos para ver las diferencias.



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