How to access property of anonymous type in C#?

后端 未结 5 1072
南旧
南旧 2020-11-28 02:40

I have this:

List nodes = new List(); 

nodes.Add(
new {
    Checked     = false,
    depth       = 1,
    id          = \"div_\"         


        
      
      
      
5条回答
  •  孤独总比滥情好
    2020-11-28 03:01

    The accepted answer correctly describes how the list should be declared and is highly recommended for most scenarios.

    But I came across a different scenario, which also covers the question asked. What if you have to use an existing object list, like ViewData["htmlAttributes"] in MVC? How can you access its properties (they are usually created via new { @style="width: 100px", ... })?

    For this slightly different scenario I want to share with you what I found out. In the solutions below, I am assuming the following declaration for nodes:

    List nodes = new List();
    
    nodes.Add(
    new
    {
        Checked = false,
        depth = 1,
        id = "div_1" 
    });
    
    

    1. Solution with dynamic

    In C# 4.0 and higher versions, you can simply cast to dynamic and write:

    if (nodes.Any(n => ((dynamic)n).Checked == false))
        Console.WriteLine("found not checked element!");
    

    Note: This is using late binding, which means it will recognize only at runtime if the object doesn't have a Checked property and throws a RuntimeBinderException in this case - so if you try to use a non-existing Checked2 property you would get the following message at runtime: "'<>f__AnonymousType0' does not contain a definition for 'Checked2'".

    2. Solution with reflection

    The solution with reflection works both with old and new C# compiler versions. For old C# versions please regard the hint at the end of this answer.

    Background

    As a starting point, I found a good answer here. The idea is to convert the anonymous data type into a dictionary by using reflection. The dictionary makes it easy to access the properties, since their names are stored as keys (you can access them like myDict["myProperty"]).

    Inspired by the code in the link above, I created an extension class providing GetProp, UnanonymizeProperties and UnanonymizeListItems as extension methods, which simplify access to anonymous properties. With this class you can simply do the query as follows:

    if (nodes.UnanonymizeListItems().Any(n => (bool)n["Checked"] == false))
    {
        Console.WriteLine("found not checked element!");
    }
    

    or you can use the expression nodes.UnanonymizeListItems(x => (bool)x["Checked"] == false).Any() as if condition, which filters implicitly and then checks if there are any elements returned.

    To get the first object containing "Checked" property and return its property "depth", you can use:

    var depth = nodes.UnanonymizeListItems()
                 ?.FirstOrDefault(n => n.Contains("Checked")).GetProp("depth");
    

    or shorter: nodes.UnanonymizeListItems()?.FirstOrDefault(n => n.Contains("Checked"))?["depth"];

    Note: If you have a list of objects which don't necessarily contain all properties (for example, some do not contain the "Checked" property), and you still want to build up a query based on "Checked" values, you can do this:

    if (nodes.UnanonymizeListItems(x => { var y = ((bool?)x.GetProp("Checked", true)); 
                                          return y.HasValue && y.Value == false;}).Any())
    {
        Console.WriteLine("found not checked element!");
    }
    

    This prevents, that a KeyNotFoundException occurs if the "Checked" property does not exist.


    The class below contains the following extension methods:

    • UnanonymizeProperties: Is used to de-anonymize the properties contained in an object. This method uses reflection. It converts the object into a dictionary containing the properties and its values.
    • UnanonymizeListItems: Is used to convert a list of objects into a list of dictionaries containing the properties. It may optionally contain a lambda expression to filter beforehand.
    • GetProp: Is used to return a single value matching the given property name. Allows to treat not-existing properties as null values (true) rather than as KeyNotFoundException (false)

    For the examples above, all that is required is that you add the extension class below:

    public static class AnonymousTypeExtensions
    {
        // makes properties of object accessible 
        public static IDictionary UnanonymizeProperties(this object obj)
        {
            Type type = obj?.GetType();
            var properties = type?.GetProperties()
                   ?.Select(n => n.Name)
                   ?.ToDictionary(k => k, k => type.GetProperty(k).GetValue(obj, null));
            return properties;
        }
        
        // converts object list into list of properties that meet the filterCriteria
        public static List UnanonymizeListItems(this List objectList, 
                        Func, bool> filterCriteria=default)
        {
            var accessibleList = new List();
            foreach (object obj in objectList)
            {
                var props = obj.UnanonymizeProperties();
                if (filterCriteria == default
                   || filterCriteria((IDictionary)props) == true)
                { accessibleList.Add(props); }
            }
            return accessibleList;
        }
    
        // returns specific property, i.e. obj.GetProp(propertyName)
        // requires prior usage of AccessListItems and selection of one element, because
        // object needs to be a IDictionary
        public static object GetProp(this object obj, string propertyName, 
                                     bool treatNotFoundAsNull = false)
        {
            try 
            {
                return ((System.Collections.Generic.IDictionary)obj)
                       ?[propertyName];
            }
            catch (KeyNotFoundException)
            {
                if (treatNotFoundAsNull) return default(object); else throw;
            }
        }
    }
    
    

    Hint: The code above is using the null-conditional operators, available since C# version 6.0 - if you're working with older C# compilers (e.g. C# 3.0), simply replace ?. by . and ?[ by [ everywhere (and do the null-handling traditionally by using if statements or catch NullReferenceExceptions), e.g.

    var depth = nodes.UnanonymizeListItems()
                .FirstOrDefault(n => n.Contains("Checked"))["depth"];
    

    As you can see, the null-handling without the null-conditional operators would be cumbersome here, because everywhere you removed them you have to add a null check - or use catch statements where it is not so easy to find the root cause of the exception resulting in much more - and hard to read - code.

    If you're not forced to use an older C# compiler, keep it as is, because using null-conditionals makes null handling much easier.

    Note: Like the other solution with dynamic, this solution is also using late binding, but in this case you're not getting an exception - it will simply not find the element if you're referring to a non-existing property, as long as you keep the null-conditional operators.

    What might be useful for some applications is that the property is referred to via a string in solution 2, hence it can be parameterized.

    提交回复
    热议问题