0

I need to iterate through the entire structure of a class. I'm interrogating the objects sent into my WCF methods and I presently have an overload of ToString() in each of my classes that list all properties and their values. This works; but is hardcoded and requires updates each time we add properties to the class.

Current solution is VB but next version will be C# - hence both tags.

The class may be comprised of primitive types only or the class may be comprised of other objects. Iterating through a simple class is not a problem.

I am having trouble identifying when a property is actually another class. So, given the example below, I can iterate through Appointment and Patient and dump the values of each of their properties.

I am stuck trying to iterate through PatientAppointment. I've scoured MSDN and SO, tried countless properties within the type, etc. to no avail.

Public Class PatientAppointment
    Public Property Pat As Patient
    Public Property Appt As Appointment
End Class

Public Class Appointment
    Public Property ApptProp1 As String
    Public Property ApptProp2 As Integer
End Class

Public Class Patient
    Public Property PatProp1 As String
    Public Property PatProp2 As Integer
End Class
Donny McCoy
  • 130
  • 7
  • And what error are you getting? – BradleyDotNET Oct 16 '14 at 22:27
  • 1
    How exactly do you want to iterate over `PatientAppointment`? Do you want to iterate recursively (iterate over each property of the `PatientAppointment` as well as each property of `Pat` and `Appt`)? Or do you just want to know if the property is a class or not? – AJ Richardson Oct 16 '14 at 22:33
  • - I'm not receiving an error. – Donny McCoy Oct 16 '14 at 22:39
  • 1
    what are you trying to do that Reflection is necessary? – Ňɏssa Pøngjǣrdenlarp Oct 16 '14 at 22:40
  • aj_r, would like to know if it's a class so I can recurse it. Problem is, every property is a class (string, int, etc). – Donny McCoy Oct 16 '14 at 22:40
  • 1
    @DonnyMcCoy There are ways to filter those: http://stackoverflow.com/questions/2442534/how-to-test-if-type-is-primitive – TyCobb Oct 16 '14 at 22:52
  • most alternatives are going to have the same drawback; a custom attribute applied to the class might short circuit things a little. it only breaks when you add a Type not a property; I am trying to think how to do it with an Interface which isnt hokey – Ňɏssa Pøngjǣrdenlarp Oct 16 '14 at 22:56
  • Thanks TyCobb, I'm good on that; but I thought that some types are not primitives. I'm not in the office now so I don't have the url of where I read that; but I read somewhere that using IsPrimitive might not always work as expected. – Donny McCoy Oct 16 '14 at 23:37
  • Plutonix, I just found this as a related suggestion on the right side of SO... Swear I didn't find this in an earlier search... I'm not sure I like this though, it is similar to another idea I saw which queried the namespace (I thought). Risk there is it could be gamed since you can put "system" in your namespace or assembly. http://stackoverflow.com/questions/20814790/how-to-get-properties-from-nested-object-using-reflection-and-recursion?rq=1 – Donny McCoy Oct 16 '14 at 23:41
  • No, I was thinking more along the lines of an Attribute so you could know when you found one of your own Types. An attribute which returns info like names and such has the same drawbacks you mentioned - maintenance. Although a dictionary to cache the ones you have already identified would likely optimize things. – Ňɏssa Pøngjǣrdenlarp Oct 16 '14 at 23:57

2 Answers2

0

It sounds like maybe you are looking for a way to identify your Types with some level of certainty. One way might be to use a custom attribute which will be easy to find and filter using Reflection.

Public Class RealMcCoy
    Inherits Attribute

    Public Property IsReal As Boolean 

    Public Sub New(b as Boolean)
      IsReal = b
    End Sub
End Class

<RealMcCoy(True)>      
Public Class Patient
    ...
End Class

<RealMcCoy(True)>   
Public Class Appointment
   ...
End Class

Now when iterating properties, check if it is the RealMcCoy to see if it is one you want/need to drill into. Since the Attribute will be on the Type, there is an extra step to get each property's Type and poll that

    Dim props As PropertyInfo() = myFoo.GetType.GetProperties
    Dim pt As Type

    For Each pi As PropertyInfo In props
        pt = pi.PropertyType         ' get the property type

        Dim attr() As RealMcCoy =
                DirectCast(pt.GetCustomAttributes(GetType(RealMcCoy), True), RealMcCoy())
        If attr.Length > 0 Then
            ' bingo, baby...optional extra test:
            If attr(0).IsReal Then
                Console.Beep()
            End If
        Else
            ' skip this prop - its not a real mccoy
        End If
    Next
End Sub

It breaks if you add a Type and dont add the Attribute, but it is less breakable than having to update each Types for their constituent properties. A fake interface would be easier to query, but has the same drawback.

I am not sure I understand the issue of "gaming" things - are you afraid that some other Types will get picked up? Attributes would be hard to "game" since they are compiled into the assembly, still the above could be used to return a GUID (maybe one attached to the assembly?) rather than bool to provide some reassurance.

Absolute certainty will be hard to come by.


The RealMcCoy attribute would probably not be applied to your top level types (PatientAppointment) just the types (classes) which will be used as properties in other types. The object being a way to identify these easily.

Depending on how it is used, a dictionary or hashtable of TypeName-PropertyName pairs already identified as RealMcCoys might be useful so the whole Reflection process can be short circuited. Rather than adding on the fly, you might be able to build the list in its entirety up front as shown in this answer - see the RangeManager.BuildPropMap procedure.

I am not so sure about the inheritance approach since you might want to actually use inheritance somewhere. An interface might work better: initially, its mere existence could be the trigger at the start, but could also be used to provide a service.


Simple test case:

' B and C classes are not tagged
Friend Class FooBar
    Public Property Prop1 As PropA
    Public Property Prop2 As PropB
    Public Property Prop3 As PropC
    Public Property Prop4 As PropD
    Public Property Prop5 As PropE
End Class

Add a line to the for loop:

Dim f As New FooBar
' use instance:
Dim props As PropertyInfo() = f.GetType.GetProperties
Dim pt As Type

For Each pi As PropertyInfo In props
    pt = pi.PropertyType

    Dim attr() As RealMcCoy =
            DirectCast(pt.GetCustomAttributes(GetType(RealMcCoy), True), RealMcCoy())

    Console.WriteLine("Prop Name: {0},  prop type: {1}, IsRealMcCoy? {2}",
            pi.Name, pt.Name, If(attr.Length > 0, "YES!", "no"))
Next

Output:

Prop Name: Prop1  prop type: PropA IsRealMcCoy? YES!  
Prop Name: Prop2  prop type: PropB IsRealMcCoy? no  
Prop Name: Prop3  prop type: PropC IsRealMcCoy? no 
Prop Name: Prop4  prop type: PropD IsRealMcCoy? YES! 
Prop Name: Prop5  prop type: PropE IsRealMcCoy? YES!
Community
  • 1
  • 1
Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
  • another way would be to make every single class used inherit from a `McCoy` base class and simply check if a type `IsAssignableFrom` as the signal – Ňɏssa Pøngjǣrdenlarp Oct 17 '14 at 00:50
  • Hi Plutonix, I'm playing with this now. I also like the IsAssignableFrom idea and may use that in the C# version as I plan to inherit from multiple classes in that version. So anyway, I have a question on your sample. Where you say dim attr() as mccoy, what should mccoy really be? I tried realmccoy but I'm getting an empty list for attr(). – Donny McCoy Oct 17 '14 at 12:50
  • I made an edit - It should be `RealMcCoy` - I was trying to avoid `RealMcCoy.IsRealMcCoy` and dropped some chars. There will obviously be many props on a type which do *not* have the attr, but those which do have it should show up. That is working/tested code (now). – Ňɏssa Pøngjǣrdenlarp Oct 17 '14 at 13:12
  • given the attribute is on the **Type** which is used as a property, there is an extra step which I added to the answer (note the **pt** to get `attr()`). Sorry, I originally tested it using it directly on properties (like in that RangeAttribute thing). – Ňɏssa Pøngjǣrdenlarp Oct 17 '14 at 13:30
  • Plutonix, I really appreciate your help. I'm working on the GetValue part now, as the method is different based on a property or a property of a property (recursive). I apologize for the delay in marking your answer - nothing but meetings Friday afternoon. Might you have any guidance on casting the property info object into its real object? Edit to add that I'm missing something in my direct cast so I keep getting the dreaded objects don't match exception. – Donny McCoy Oct 18 '14 at 13:18
  • np on the "delay" - there were small errors in the first versions anyway. Yes, you can drill into the type.prop to get type.prop.value, *but* both our examples are wrong: we are not instancing these goofy class props: `Public Property Pat As NEW Patient` and for me `Public Property Prop1 As NEW PropA` - they ARE instance members. HOPEFULLY that is as deep as it goes, but I have to think there is a simpler way, just dont know what you are doing in the broader picture. You need to use `InvokeMember` to get the instance prop then AGAIN to get the current value. Post a new Q if you need help. – Ňɏssa Pøngjǣrdenlarp Oct 18 '14 at 14:33
  • Type.InvokeMember - that was the missing piece! Bloody brilliant Plutonix! – Donny McCoy Oct 20 '14 at 14:24
  • are those classes used anywhere in the normal fashion or are they just transport mechanisms? If the latter, a propertybag type thing might be easier. An Interface could do what RealMcCoy does here and then a GetPropertyBag method could cough up all the property-values in a dictionary. – Ňɏssa Pøngjǣrdenlarp Oct 20 '14 at 14:39
0

Many thanks to Plutonix! Here is my final function. If my answer supercedes Plutonix's answer then I'll need to adjust this as he fully deserves credit for getting me to this point.

I've tested it to 8 levels of nested types. Rudimentary exception handling due to this being a prototype only.

    Function IterateClass(ByVal o As Object, ByVal topLevelFlag As Boolean, ByVal indentLevel As Integer) As String

    Dim retVal As New StringBuilder

    Try

        '' Iterate top level of supplied type ''
        '' Query each property for custom attribute ADMICompositeClassAttribute ''
        '' If exists and set to true then we're dealing with a base class that we need to break down recursively ''
        '' If not then immediately dump this type's properties ''


        '' Build header of output ''
        If topLevelFlag Then

            '' <<EXCERPTED>> ''

        End If


        '' We start walking through the hierarchy here, no matter how much we recurse we still need to do this each time ''
        Dim properties_info As PropertyInfo()
        properties_info = o.GetType().GetProperties()

        For Each p As PropertyInfo In properties_info
            Dim propName As String = p.Name
            Dim propTypeList As List(Of String) = p.PropertyType.ToString().Split(".").ToList()
            Dim propType As String = propTypeList(propTypeList.Count - 1).Replace("[", "").Replace("]", "")
            Dim pValue As Object = Nothing

            '' We check this type for custom attribute ADMICompositeClassAttribute ''
            '' Is this type a composite? ''
            Dim oAttr() As ADMICompositeClassAttribute = p.PropertyType.GetCustomAttributes(GetType(ADMICompositeClassAttribute), False)
            Dim indentString As String = String.Concat(Enumerable.Repeat(" ", indentLevel))
            If oAttr.Length > 0 AndAlso oAttr(0).IsComposite Then

                '' This is a nested type, recurse it ''
                Dim dynType As Type = p.PropertyType()
                Dim dynObj As Object = Activator.CreateInstance(dynType)

                '' <<EXCERPTED ''

            Else

                pValue = p.GetValue(o, Nothing)

            End If

        Next


    Catch ex As Exception
        retVal.AppendLine(ex.ToString())

    End Try
    Return retVal.ToString()


End Function
Donny McCoy
  • 130
  • 7