C# PropertyInfo.GetValue() when the property is a SortedSet<T>

169 Views Asked by At

I refactored the code and added comments to make it clear. See pic of code and result

UPDATE: I know the info I provided was a bit flawed so I edited the question. Nevertheless, how does one get the value from the property when it's a SortedSet? When I try to iterate over the return from GetValue() it gives me an Argument Null Exception.

Is there a practical way to get the PropertyInfo.Name and PropertyInfo.GetValue() from a class instance when said property is a SortedSet<T>? For example:

The machine class looks like this:

public class Machine : IComparable, IComparable<Machine>
{
    [BsonId]
    public Guid Id { get; set; }

    [BsonElement]
    public SortedSet<SubAssembly> SubAssemblies { get; set; } = [];
}

I then pull all of the Machines from a DB and try to get the property names and values like so:

 foreach (var MachineInDB in db.GetRecordList<Machine>(MachinesTable))
 {
     PropertyInfo[] MachineClassProperties = MachineInDB.GetType().GetProperties();

     Console.WriteLine("\n");

     foreach (PropertyInfo MachineProperty in MachineClassProperties)
     {
         
          Console.WriteLine($"{MachineProperty.Name} => {MachineProperty.GetValue(MachineInDB)}");
            
          //this one will print the following when it gets to the problem property
          //SubAssemblies => System.Collections.Generic.SortedSet`1[SubAssembly]

          //If I try to iterate over the SortedSet property, I get an ArgumentNull exception. 
         
     }
 }

Every other property in the class prints fine with the above code. If I print each Machine from the DB using ToString() all of the SubAssemblies print perfectly, so no problem pulling the data from the database.

Somehow the PropertyInfo element is null when it try to use reflectance only on the 'SortedSet'. Does anyone know how to work around that? I bet that if I switch to a List<T> it would work fine but I really need it to be a SortedSet<T>.

I've also tried putting the return value into a 'var' and iterating over it but I end up with an ArgumentNull exception. This is how I know it's just not pulling the object with the GetValue() method.

Is it wrong to expect a 'SortedSet' object? What's the right way to do this?

Thank you!

BMET1

3

There are 3 best solutions below

0
John Wu On BEST ANSWER

In your screenshot example, you're passing an object to string.Join and expecting it to figure out that it is a collection and figure out which properties to output. It won't do that.

If you're explicit about what you want, e.g. by telling the compiler how to treat the object and which properties to output, there is no issue retrieving the SortedSet via Reflection.

If you don't want to cast anything, you can use Reflection on the item that is retrieved from the collection and iterate over its properties too.

public class SubAssembly : IComparable
{
    public string Name { get; set; }
    public int CompareTo(SubAssembly other) => Name.CompareTo(other.Name);
    public int CompareTo(object other) => Name.CompareTo((other as SubAssembly)?.Name);
}

public class Machine 
{
    public Guid Id { get; set; }

    public SortedSet<SubAssembly> SubAssemblies { get; set; } = new SortedSet<SubAssembly>();
}

public static void Main()
{
    var machine = new Machine
    {
        SubAssemblies = 
        {
            new SubAssembly
            {
                Name = "Foo"
            },
            new SubAssembly
            {
                Name = "Bar"
            }
        }
    };
    var propertyInfo = machine.GetType().GetProperty("SubAssemblies");
    var subAssemblies = propertyInfo.GetValue(machine);
    
    Console.WriteLine("This will contain a type string: " + string.Join(",", subAssemblies));
    Console.WriteLine("Here's your data: " + string.Join(",", ((SortedSet<SubAssembly>)subAssemblies).Select( x => x.Name)));

    //In case you don't want to cast anything
    foreach (var subAssemblyObject in subAssemblies as IEnumerable)
    {
        var type = subAssemblyObject.GetType();
        Console.WriteLine(string.Join(",", type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Select( x => x.Name + ": " + x.GetValue(subAssemblyObject))));
    }
}

Output:

This will contain a type string: System.Collections.Generic.SortedSet`1[SubAssembly]
Here's your data: Bar,Foo
Name: Bar
Name: Foo

See DotNetFiddle for working example.

1
Trey On

You don't even need to know the type to use GetValue since it can treat it as an object. I wrote this to test it real quick.

var m = new Machine();
m.SubAssemblies.Add(new SubAssembly("a"));
m.SubAssemblies.Add(new SubAssembly("c"));
m.SubAssemblies.Add(new SubAssembly("b"));

var machineType = m.GetType(); // or typeof(Machine)
var pi = machineType.GetProperty(nameof(Machine.SubAssemblies));

var value = pi.GetValue(m);

Console.WriteLine(value.ToString());

public class Machine : IComparable, IComparable<Machine>
{
    public Guid Id { get; set; }
    public SortedSet<SubAssembly> SubAssemblies { get; set; } = new();

    public Machine(Guid? id = null)
    {
        Id = id ?? Guid.NewGuid();
    }

    public int CompareTo(Machine other) => Id.CompareTo(other.Id);
    public int CompareTo(object obj) => throw new NotImplementedException();

    public override string ToString() => JsonConvert.SerializeObject(this, Formatting.Indented);
}

public class SubAssembly : IComparable, IComparable<SubAssembly>
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    
    public SubAssembly(string name, Guid? id = null)
    {
        Id = id ?? Guid.NewGuid();
        Name = name;
    }
    
    public int CompareTo(SubAssembly other) => Id.CompareTo(other.Id);
    public int CompareTo(object obj) => throw new NotImplementedException();

    public override string ToString() => JsonConvert.SerializeObject(this, Formatting.Indented);
}

Output:

System.Collections.Generic.SortedSet`1[SubAssembly]

It's worth noting that you can leave the value implicit var or if you want to specify the type you can use any implemented interface or a parent class of SortedSet<T> and you can view them here.

  • ISet<T>
  • ICollection<T>
  • IEnumerable<T>
  • IEnumerable
  • ICollection
  • IReadOnlyCollection<T>
  • IReadOnlySet<T>
  • ISerializable
  • IDeserializationCallback
0
BMET1 On

UPDATE: I can use as ICollection and it will still work. Close enough I guess.

I think I might have found a PARTIAL answer.

So this can't be done (or at least I can't find a way) with a generic approach. It seems that you have to know the class type no matter what, and statically type it worse of all. I tried Type typeOfClass = machineInDB.GetType(), typeof(machineInDB.SubAssemblies), etc.

subAssembliesProperty.GetValue(machineInDB) as SortedSet<SubAssembly>; is the only way it will work. If you have another solution please let me know because even if it works, it defeats the goal to create a generic way to deconstruct a class instance when one of the properties is a SortedSet<T>.

    int k = 0;
    foreach (var machineInDB in db.GetRecordList<Machine>(DBCollections.MACHINES.ToString()))
    {
        if (machineInDB != null)
        {
            var subAssembliesProperty = typeof(Machine).GetProperty("SubAssemblies");

            if (subAssembliesProperty != null)
            {
                var subAssemblies = subAssembliesProperty.GetValue(machineInDB) as SortedSet<SubAssembly>;

                int j = 0;

                if (subAssemblies != null)
                    foreach (var subAssembly in subAssemblies)
                    {
                        Console.WriteLine($"{k}{j} - {subAssembly}");
                        j++;
                    }
                else Console.WriteLine("It's Null");

                Console.WriteLine();
            }
        }
        k++;
    }