Home > Software engineering >  How to detect the length of array property using reflection only if it actually is an array?
How to detect the length of array property using reflection only if it actually is an array?

Time:01-02

I'm converting an object based on reflection like this.

obj.GetType().GetProperties()

It produces the expected list of properties.

{System.Reflection.PropertyInfo[4]}
[0]: {System.String Name}
[1]: {System.Guid[] Ids}
[2]: {System.String[] Tags}
[3]: {System.Nullable`1[System.Boolean] Status}

Then, I want to clear the list of properties not in use, so I add the condition that a value of a property needs to differ from null.

obj.GetType().GetProperties()
  .Where(a => a.GetValue(obj) != null)

It produces the expected result for atomary fields but not the arrays (as they're undefinably not null, only empty).

{System.Linq.Enumerable.WhereArrayIterator<System.Reflection.PropertyInfo>}
[0]: {System.String Name}
[1]: {System.Guid[] Ids}
[2]: {System.String[] Tags}

I'm not sure how I should go about detecting non-null but empty array properties in order to exclude those. I can't simply cast to an array (as discussed here) because not every property is an array.

The closest approach I have is this piece of "work", which seems to be rather... undesired. It's got this distinct code smell going on.

obj.GetType().GetProperties()
  .Where(a => a.GetValue(obj) != null 
    && !(a.GetValue(obj) is Array && (a.GetValue(obj) as Array).Length == 0))

And as I try to create something with it using Select(a=>a.GetValue(obj)) it gets even clunkier and more obviously in desperate need of improvement. I also noticed that whenever the array isn't empty, my mapping fails producing System.String[], which will probably require me to additionally clunkify the code.

CodePudding user response:

I can't think of a solution that's significantly different to yours, fundamentally it feels like it's a custom logic to determine whether a property value is "empty" or not.

Perhaps a pattern matched switch expression might make the code a little cleaner, however, for example:

var obj = new MyObject
{
    Name = "Test",
    Ids = new Guid[0],
    Tags = new[] { "t1" },
    Status = null
};

var values = obj.GetType().GetProperties()
    .Select(p => p.GetValue(obj))
    .Where(o => o switch {
        Array array => array.Length > 0,
        _ => o != null
    });

CodePudding user response:

You can simplify the condition by using pattern matching. It allows you to call GetValue only once

p => p.GetValue(obj) is not null and not IList { Count: 0 }

or with the slightly shorter version

p => p.GetValue(obj) is not (null or IList { Count: 0 })

I tested for IList instead of array. This includes arrays and also ArrayList and List<T>. Note that an array exposes its Length through the explicitly implemented IList.Count property as well. "explicitly implemented" means that this member is hidden or private unless view directly through the interface.

You could even test for the ICollection interface instead. This would include even more collections.

IList { Count: 0 } is a property pattern which tests whether the object is an IList having a Count of 0.

Together:

var result = obj.GetType().GetProperties()
  .Where(p => p.GetValue(obj) is not (null or IList { Count: 0 }));

Note that this uses logical patterns introduced in C# 9.0.

CodePudding user response:

Unfortunately, the code smell when working with this blind reflection is largely unavoidable (to the best of my knowledge) and rarely condenses into a tight block cleanly.

Your mapping seems to be sensitive to the exact property type given the string[] output, alluding to it being unable to unwrap collections without assistance.

To address both issues, I'd suggest a verbose enumerable helper where the collection handling specifics are immediately apparent to any future maintainers.

static IEnumerable<(Type Type, IEnumerable Values)> GetPopulatedPropertyData(object obj)
{
    foreach (var prop in obj.GetType().GetProperties())
    {
        var value = prop.GetValue(obj);
        if (value is Array a)
        {
            if (a.Length == 0) { continue; }
            yield return (prop.PropertyType.GetElementType()!, a);
        }
        // Future collections added in reverse hierarchial order here
        else if (value is not null)
        {
            yield return (prop.PropertyType, new[] { value });
        }
    }
}

This should facilitate mapping by making the correct type available while keeping the majority of the ugly code in its own box.

  • Related