Uso de la reflexión en C # para obtener propiedades de un objeto anidado

82

Dados los siguientes objetos:

public class Customer {
    public String Name { get; set; }
    public String Address { get; set; }
}

public class Invoice {
    public String ID { get; set; }
    public DateTime Date { get; set; }
    public Customer BillTo { get; set; }
}

Me gustaría usar la reflexión para pasar por el Invoicey obtener la Namepropiedad de a Customer. Esto es lo que busco, asumiendo que este código funcionaría:

Invoice inv = GetDesiredInvoice();  // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo.Address");
Object val = info.GetValue(inv, null);

Por supuesto, esto falla ya que "BillTo.Address" no es una propiedad válida de la Invoiceclase.

Entonces, intenté escribir un método para dividir la cadena en pedazos en el punto y caminar sobre los objetos buscando el valor final que me interesaba. Funciona bien, pero no me siento del todo cómodo con él:

public Object GetPropValue(String name, Object obj) {
    foreach (String part in name.Split('.')) {
        if (obj == null) { return null; }

        Type type = obj.GetType();
        PropertyInfo info = type.GetProperty(part);
        if (info == null) { return null; }

        obj = info.GetValue(obj, null);
    }
    return obj;
}

¿Alguna idea sobre cómo mejorar este método o una mejor manera de resolver este problema?

EDITAR después de publicar, vi algunas publicaciones relacionadas ... Sin embargo, no parece haber una respuesta que aborde específicamente esta pregunta. Además, aún me gustaría recibir comentarios sobre mi implementación.

jheddings
fuente
solo por curiosidad, si tu GetDesiredInvoicete devuelve un objeto de tipo, Invoice¿por qué no usarlo inv.BillTo.Namedirectamente?
ram
De hecho, estoy usando esto de manera un poco diferente, simplemente simplificado para mi ejemplo. Cojo un objeto y lo paso a un procesador que lo fusiona con una plantilla para imprimir.
jheddings
Simplemente se sintió un poco de "fuerza bruta" y parecía que debería haber una mejor manera. Sin embargo, a partir de las respuestas hasta ahora, parece que no estaba totalmente equivocado.
jheddings
3
Pensé que estaba loco por hacer esto, pero parece que alguien ha tenido el mismo problema que yo. Por cierto
Marcello Grechi Lins
También consulte mi respuesta a otro tema para usar estos como métodos de extensión: stackoverflow.com/questions/1196991/…
jheddings

Respuestas:

12

De hecho, creo que tu lógica está bien. Personalmente, probablemente lo cambiaría para que pase el objeto como primer parámetro (que está más en línea con PropertyInfo.GetValue, por lo que es menos sorprendente).

También probablemente lo llamaría algo más como GetNestedPropertyValue, para que sea obvio que busca en la pila de propiedades.

Reed Copsey
fuente
Buen llamado a reordenar los parámetros y el cambio de nombre sugerido.
itowlson
Gracias por los comentarios ... He incluido ambas sugerencias en mi implementación. Terminé convirtiéndolo en un método de extensión en la Objectclase, lo que refuerza el punto de reordenar los parámetros.
jheddings
¿Por qué es la respuesta aceptada? No me ayuda a obtener la propiedad de un objeto anidado como pidió el OP.
Levitikon
@Levitikon El segundo conjunto de código del OP muestra una forma decente de hacer lo que se le pidió. Ese era mi punto de vista. No hay nada de malo en el código que se publica en la propia respuesta.
Reed Copsey
27

Utilizo el siguiente método para obtener los valores de las propiedades (clases anidadas) como

"Propiedad"

"Nombre de la calle"

"Dirección.País.Nombre"

    public static object GetPropertyValue(object src, string propName)
    {
        if (src == null) throw new ArgumentException("Value cannot be null.", "src");
        if (propName == null) throw new ArgumentException("Value cannot be null.", "propName");

        if(propName.Contains("."))//complex type nested
        {
            var temp = propName.Split(new char[] { '.' }, 2);
            return GetPropertyValue(GetPropertyValue(src, temp[0]), temp[1]);
        }
        else
        {
            var prop = src.GetType().GetProperty(propName);
            return prop != null ? prop.GetValue(src, null) : null;
        }
    }

Aquí está el violín: https://dotnetfiddle.net/PvKRH0

DevT
fuente
Si la propiedad es nula, esto no funcionará, debe verificar si src es nulo al principio antes de trabajar.
Furtiro
@Furtiro sí, claro, no puede funcionar si src (o propName) es nulo. Agregué la excepción de lanzamiento. Gracias
DevT
Encantado de ayudar ! Pero no funciona para mí con una propiedad de anidación triple, se detiene después de 2, ¡código triste pero excelente!
Furtiro
@Furtiro eso es extraño, tengo que trabajar como pueden ver en el violín que acabo de agregar al post. Eche un vistazo, tal vez pueda encontrar su problema.
DevT
@DevT Hola, ¿Tendrías el mismo enfoque para hacer que GetProperty funcione cuando usamos clases anidadas? es decir, var property = type.GetProperty (sortProperty); falla con la clase anidada (ya que obtenemos un resultado nulo), creo que su solución puede responder esto. (Para obtener detalles completos, la solución proporcionada aquí stackoverflow.com/questions/11336713/… falla con la clase anidada)
Kynao
13

Sé que llego un poco tarde a la fiesta y, como dijeron otros, su implementación está bien
... para casos de uso simples .
Sin embargo, he desarrollado una biblioteca que resuelve exactamente ese caso de uso, Pather.CSharp .
También está disponible como paquete Nuget .

Su clase principal es Resolvercon su Resolvemétodo.
Le pasa un objeto y la ruta de la propiedad , y devolverá el valor deseado .

Invoice inv = GetDesiredInvoice();  // magic method to get an invoice
var resolver = new Resolver();
object result = resolver.Resolve(inv, "BillTo.Address");

Pero también puede resolver rutas de propiedades más complejas , incluido el acceso a matrices y diccionarios.
Así, por ejemplo, si su Customertenían varias direcciones

public class Customer {
    public String Name { get; set; }
    public IEnumerable<String> Addresses { get; set; }
}

puede acceder al segundo usando Addresses[1].

Invoice inv = GetDesiredInvoice();  // magic method to get an invoice
var resolver = new Resolver();
object result = resolver.Resolve(inv, "BillTo.Addresses[1]");
Domysee
fuente
2
¿Cómo maneja esto los objetos nulos para propiedades anidadas, es decir, NullObject.Id con NullObject siendo nulo en la factura, por ejemplo?
AVFC_Bubble88
10

Tienes que acceder al objeto REAL en el que necesitas usar la reflexión. Esto es lo que quiero decir:

En lugar de esto:

Invoice inv = GetDesiredInvoice();  // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo.Address");
Object val = info.GetValue(inv, null);

Haga esto (editado según el comentario):

Invoice inv = GetDesiredInvoice();  // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo");
Customer cust = (Customer)info.GetValue(inv, null);

PropertyInfo info2 = cust.GetType().GetProperty("Address");
Object val = info2.GetValue(cust, null);

Mire esta publicación para obtener más información: Uso de la reflexión para establecer una propiedad de una propiedad de un objeto

Gabriel McAdams
fuente
Gracias por la respuesta ... Sé cómo obtener el valor de una propiedad de primer nivel, pero me preguntaba cómo obtener una anidada. En mi aplicación real, no tengo acceso al objeto real.
jheddings
1
Necesita obtener propiedades anidadas directamente. También estoy en una situación en la que "Factura" es T y tengo una cadena de la ruta "Propiedad.Propiedad.Propiedad". No puedo jugar con cada propiedad.
Levitikon
Esto lo hizo por mí. Tampoco estaba teniendo suerte con BillTo.Address. Tengo curiosidad por saber si la configuración de la propiedad funciona de la misma manera.
Yusif_Nurizade
7

Con la esperanza de no sonar demasiado tarde para la fiesta, me gustaría agregar mi solución: definitivamente use la recursividad en esta situación

public static Object GetPropValue(String name, object obj, Type type)
    {
        var parts = name.Split('.').ToList();
        var currentPart = parts[0];
        PropertyInfo info = type.GetProperty(currentPart);
        if (info == null) { return null; }
        if (name.IndexOf(".") > -1)
        {
            parts.Remove(currentPart);
            return GetPropValue(String.Join(".", parts), info.GetValue(obj, null), info.PropertyType);
        } else
        {
            return info.GetValue(obj, null).ToString();
        }
    }
Roger l
fuente
6

No explica la fuente de su "incomodidad", pero su código básicamente me parece correcto.

Lo único que cuestionaría es el manejo de errores. Devuelve nulo si el código intenta atravesar una referencia nula o si el nombre de la propiedad no existe. Esto oculta errores: es difícil saber si devolvió nulo porque no hay un cliente BillTo, o porque lo escribió mal "BilTo.Address" ... o porque hay un cliente BillTo y su dirección es nula. Dejaría que el método se bloquee y se queme en estos casos, solo dejaría escapar la excepción (o tal vez envolverla en una más amigable).

itowlson
fuente
3

Aquí hay otra implementación que omitirá una propiedad anidada si es un enumerador y continuará más profundamente. Las propiedades del tipo cadena no se ven afectadas por la comprobación de enumeración.

public static class ReflectionMethods
{
    public static bool IsNonStringEnumerable(this PropertyInfo pi)
    {
        return pi != null && pi.PropertyType.IsNonStringEnumerable();
    }

    public static bool IsNonStringEnumerable(this object instance)
    {
        return instance != null && instance.GetType().IsNonStringEnumerable();
    }

    public static bool IsNonStringEnumerable(this Type type)
    {
        if (type == null || type == typeof(string))
            return false;
        return typeof(IEnumerable).IsAssignableFrom(type);
    }

    public static Object GetPropValue(String name, Object obj)
    {
        foreach (String part in name.Split('.'))
        {
            if (obj == null) { return null; }
            if (obj.IsNonStringEnumerable())
            {
                var toEnumerable = (IEnumerable)obj;
                var iterator = toEnumerable.GetEnumerator();
                if (!iterator.MoveNext())
                {
                    return null;
                }
                obj = iterator.Current;
            }
            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }
}

basado en esta pregunta y en

Cómo saber si una PropertyInfo es una colección de Berryl

Utilizo esto en un proyecto MVC para ordenar dinámicamente mis datos simplemente pasando la propiedad para ordenar por ejemplo:

result = result.OrderBy((s) =>
                {
                    return ReflectionMethods.GetPropValue("BookingItems.EventId", s);
                }).ToList();

donde BookingItems es una lista de objetos.

erevosgr
fuente
2
> Get Nest properties e.g., Developer.Project.Name
private static System.Reflection.PropertyInfo GetProperty(object t, string PropertName)
            {
                if (t.GetType().GetProperties().Count(p => p.Name == PropertName.Split('.')[0]) == 0)
                    throw new ArgumentNullException(string.Format("Property {0}, is not exists in object {1}", PropertName, t.ToString()));
                if (PropertName.Split('.').Length == 1)
                    return t.GetType().GetProperty(PropertName);
                else
                    return GetProperty(t.GetType().GetProperty(PropertName.Split('.')[0]).GetValue(t, null), PropertName.Split('.')[1]);
            }
Mohamed.Abdo
fuente
1
   if (info == null) { /* throw exception instead*/ } 

De hecho, lanzaría una excepción si solicitan una propiedad que no existe. De la forma en que lo tiene codificado, si llamo a GetPropValue y devuelve nulo, no sé si eso significa que la propiedad no existía, o que la propiedad existía pero su valor era nulo.

AaronLS
fuente
Además, mueva la comprobación de que obj sea nulo fuera del bucle.
Kevin Brock
Lo siento, no vi el uso repetido de obj. No es una buena práctica de programación cambiar los parámetros, esto puede generar confusión en el futuro. Utilice una variable diferente para que el parámetro obj recorra el bucle.
Kevin Brock
Kevin: Para usar una variable diferente, tendría que asignarla a obj al final o hacer que el método sea recursivo. Personalmente, no creo que esto sea un problema (aunque un buen comentario sería bueno ...)
Reed Copsey
1
@Levitikon El OP declaró "¿Alguna idea sobre cómo mejorar este método o una mejor manera de resolver este problema?". Por lo tanto, esto es una respuesta y no un comentario, porque el OP pidió mejoras, lo cual es una mejora.
AaronLS
1
    public static string GetObjectPropertyValue(object obj, string propertyName)
    {
        bool propertyHasDot = propertyName.IndexOf(".") > -1;
        string firstPartBeforeDot;
        string nextParts = "";

        if (!propertyHasDot)
            firstPartBeforeDot = propertyName.ToLower();
        else
        {
            firstPartBeforeDot = propertyName.Substring(0, propertyName.IndexOf(".")).ToLower();
            nextParts = propertyName.Substring(propertyName.IndexOf(".") + 1);
        }

        foreach (var property in obj.GetType().GetProperties())
            if (property.Name.ToLower() == firstPartBeforeDot)
                if (!propertyHasDot)
                    if (property.GetValue(obj, null) != null)
                        return property.GetValue(obj, null).ToString();
                    else
                        return DefaultValue(property.GetValue(obj, null), propertyName).ToString();
                else
                    return GetObjectPropertyValue(property.GetValue(obj, null), nextParts);
        throw new Exception("Property '" + propertyName.ToString() + "' not found in object '" + obj.ToString() + "'");
    }
BarbaBabak
fuente
1
Describa en qué se basa su sulution, cómo ayudará al OP es una buena práctica.
DontVoteMeDown
1

Quería compartir mi solución, aunque puede que sea demasiado tarde. Esta solución es principalmente para verificar si existe la propiedad anidada. Pero se puede modificar fácilmente para devolver el valor de la propiedad si es necesario.

private static PropertyInfo _GetPropertyInfo(Type type, string propertyName)
        {
            //***
            //*** Check if the property name is a complex nested type
            //***
            if (propertyName.Contains("."))
            {
                //***
                //*** Get the first property name of the complex type
                //***
                var tempPropertyName = propertyName.Split(".", 2);
                //***
                //*** Check if the property exists in the type
                //***
                var prop = _GetPropertyInfo(type, tempPropertyName[0]);
                if (prop != null)
                {
                    //***
                    //*** Drill down to check if the nested property exists in the complex type
                    //***
                    return _GetPropertyInfo(prop.PropertyType, tempPropertyName[1]);
                }
                else
                {
                    return null;
                }
            }
            else
            {
                return type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
            }
        }

Tuve que referirme a algunas publicaciones para encontrar esta solución. Creo que esto funcionará para múltiples tipos de propiedades anidadas.

HarshitGindra
fuente
0

Mi conexión a Internet se cayó cuando necesito resolver el mismo problema, así que tuve que 'reinventar la rueda':

static object GetPropertyValue(Object fromObject, string propertyName)
{
    Type objectType = fromObject.GetType();
    PropertyInfo propInfo = objectType.GetProperty(propertyName);
    if (propInfo == null && propertyName.Contains('.'))
    {
        string firstProp = propertyName.Substring(0, propertyName.IndexOf('.'));
        propInfo = objectType.GetProperty(firstProp);
        if (propInfo == null)//property name is invalid
        {
            throw new ArgumentException(String.Format("Property {0} is not a valid property of {1}.", firstProp, fromObject.GetType().ToString()));
        }
        return GetPropertyValue(propInfo.GetValue(fromObject, null), propertyName.Substring(propertyName.IndexOf('.') + 1));
    }
    else
    {
        return propInfo.GetValue(fromObject, null);
    }
}

Estoy bastante seguro de que esto resuelve el problema para cualquier cadena que use para el nombre de la propiedad, independientemente del grado de anidación, siempre que todo sea una propiedad.

MalibuCusser
fuente
-7

Tratar inv.GetType().GetProperty("BillTo+Address");

RAM
fuente