Fun With Dynamic Objects (Doug Rothaus)

Fun With Dynamic Objects (Doug Rothaus)

Rate This
  • Comments 7

A while back, I remember being asked if there was a simple way to expose a source XML document as an object with properties. That is, if the root XML element had a child element <Name>Doug</Name>, then the object would have a Name property that was a string and returned “Doug”. The catch was that the XML document did not conform to a specific schema. Hence, you could not simply create an object with a Name property, because you did not know if the source document had a <Name> element. While there are ways to expose the XML data dynamically, you couldn’t quite do what was being asked.

Enter dynamic objects in Visual Basic 2010. Now, this is possible. This means that you can use the classes in the System.Dynamic namespace to create objects that expose properties and methods dynamically at run-time and solve the original problem. In this example, we will create an object that inherits the System.Dynamic.DynamicObject class. The DynamicObject class has methods that you can override to provide code specific to your implementation. When VB performs a late-bound request on an object that implements the DynamicObject class, it calls one of the methods of the DynamicObject class to obtain the result. Consider this simple example:

Imports System.Dynamic

 

Public Class SimpleObject

    Inherits DynamicObject

 

    Public Overrides Function TryGetMember(ByVal binder As GetMemberBinder,

                                           ByRef result As Object) As Boolean

 

        If binder.Name = "Address" Then

            result = "Value"

            Return True

        End If

 

        Return False

    End Function

End Class

 

Now consider this code that creates an instance of the object and accesses properties of that instance.

Module Module1

 

    Sub Main()

 

        Dim t As Object = New SimpleObject()

        Console.WriteLine(t.Address)

        Console.WriteLine(t.Street)

 

    End Sub

 

End Module

 

First, in order to ensure that requests made of the dynamic object are late-bound, we need to type the variable as Object. Then we make two late-bound requests for object properties: Address and Street. When you access the Address property, a call is made to the TryGetMember method of the DynamicObject class, which we have overridden. The GetMemberBinder object instance that is passed to our TryGetMember method contains the name of the requested member in its Name property. Our small sample matches that name and returns the string “Value”. When you access the Street property, a call is made to the TryGetMember method, the name does not match anything that our code supports, so the method returns False. False indicates an unsupported member, and an exception is thrown.

To solve our initial problem of “hiding” source XML and exposing an object instead, we can use the DynamicObject class in this same fashion. When a property is requested of our dynamic object, our code can search the XML source for a matching XML element name, and return the corresponding value as the property value. Let’s look at an example.

DynamicXmlObject example


Let’s jump into the code and we’ll talk about how the object behaves as we go. We start with a class that inherits DynamicObject. Let’s name it DynamicXmlObject.

Imports System.Dynamic

 

Public Class DynamicXmlObject

    Inherits DynamicObject

 

End Class

 

Constructors

We will add two constructors for the class. One that takes a path to an XML file as the source, and another that takes an XElement.  The constructor that takes an XElement is not just for user convenience. We’ll use this later when dealing with child elements that have attributes or children. Also for this class, we won’t expose a settable property for the source XML, so we’ll “disable” the empty constructor by making it Protected.

Imports System.Dynamic

Imports System.IO

 

 

Public Class DynamicXmlObject

    Inherits DynamicObject

 

    ' The source XML for this instance.

    Private p_Xml As XElement

 

 

    ' Create a new DynamicXmlObject with the specified source file.

    Public Sub New(ByVal filePath As String)

        If Not File.Exists(filePath) Then

            Throw New Exception("File does not exist.")

        End If

 

        p_Xml = XElement.Load(filePath)

    End Sub

 

 

    ' Create a new DynamicXmlObject with the specified source XElement.

    Public Sub New(ByVal xml As XElement)

        p_Xml = xml

    End Sub

 

 

    ' Disable the empty constructor.

    Protected Sub New()

 

    End Sub

 

 

End Class

 

Implementing GetDynamicMemberNames

This next piece of code is optional. I’ve added it so that our object supports reflection-like functionality.  You can expose the member names of your dynamic object using the GetDynamicMemberNames method of the DynamicObject class. Our sample object will return all of the child element names and attribute names of the current element. I’ve added a case-insensitive Distinct comparison so that only a single name is returned if multiple entries are found. The search looks at only the LocalName property of the XName for an element or attribute. XML namespaces are ignored. That is, <a:Name> and <b:Name> are considered to be duplicate element names.

 

    Public Overrides Function GetDynamicMemberNames() As IEnumerable(Of String)

        Dim names = (From e In p_Xml.Elements() Select e.Name.LocalName).Union(

                     From a In p_Xml.Attributes() Select a.Name.LocalName)

 

        Return (From n In names

                Order By n).Distinct(StringComparer.CurrentCultureIgnoreCase)

    End Function

 

Implementing TryGetMember

Now to the “meat” of the code. The beauty of it is that there’s not much to it. For this example, we will override the TryGetMember method of the DynamicObject class. Our TryGetMember method will be called when a dynamic property is requested on an instance of our object as in obj.PropertyName (use TryInvokeMember if your dynamic property takes one or more arguments). If the property is supported, that is, it exists in our source XML, then the TryGetMember method sets the ByRef result output parameter to the return value for the property and returns True. If the property is not found in our source XML, TryGetMember returns False.  A return value of False results in an exception being thrown, which I don’t want users to have to deal with. We’ll see later that the GetPropertyValue method returns an empty string for unsupported properties, which avoids an exception being thrown.

    Public Overrides Function TryGetMember(ByVal binder As GetMemberBinder,

                                           ByRef result As Object) As Boolean

        result = GetPropertyValue(binder.Name)

        Return If(result Is Nothing, False, True)

    End Function

 

I’ve factored the code that searches the XML into a separate method named GetPropertyValue and made the GetPropertyValue method public for testing/verification purposes. You can use the GetPropertyValue method in conjunction with the GetDynamicMemberNames method to loop through all of the possible properties exposed by the object at run time.

Next we’ll add the GetPropertyValue method. GetPropertyValue performs a case-insensitive search of attribute and child element names and ignores any XML namespaces like the GetDynamicMemberNames method. If multiple matches are found, then GetPropertyValue returns a list of DynamicXmlObject. If only one match is found, then a single DynamicXmlObject is returned. You may be asking, “Why return objects as DynamicXmlObject? Why not just return string values.” The reason is that a child XML element might also have children or attributes, so we need to expose those as child properties as well as in obj.PropertyName.ChildPropertyName. I’ve made the GetPropertyValue method Public so that it can be used to test for a member name passed as a string.

You’ll notice that, if a matching attribute name is found, the attribute and value are returned as an XElement. This enables the attribute to be used as the source for a new DynamicXmlObject.

    Public Function GetPropertyValue(ByVal propertyName As String) As Object

        Dim searchName = UCase(propertyName)

 

        If searchName = "COUNT" Then Return 1

 

        ' Variable for the return value.

        Dim result As Object

 

        Dim resultList = ((From e In p_Xml.Elements()

                           Where UCase(e.Name.LocalName) = searchName).Union(

                             From a In p_Xml.Attributes()

                             Where UCase(a.Name.LocalName) = searchName

                             Select <<%= propertyName %>><%= a.Value %></>)).ToList()

 

        ' No matches found.

        If resultList.Count = 0 Then Return ""

 

        ' If only one object is found, return an instance of a DynamicXmlObject. Otherwise

        ' return a list of DynamicXmlObject.

        If resultList.Count = 1 Then

            result = New DynamicXmlObject(resultList(0))

        Else

            result = (From e In resultList Select New DynamicXmlObject(e)).ToList()

        End If

 

        Return result

    End Function

 

Notice that the code also checks for a variable name of Count. I added this to simplify things for calling code. This way, instead of having to check the type of the property to determine if it is a list of objects or a single object, you can just check the Count property. If the dynamic property returns a list, then the Count property is handled by the list. However, if the Count property is requested on a single instance of our dynamic object, we need to return a value.

Type Conversions

You could consider this code optional, but I’d be sure to include it. The final bits of code to add ensure that our object can be converted to other types in a convenient manner. This can be early-bound requests by the compiler, or late-bound requests at run-time. The most common use of this conversion code will be to output the value from our object as a string or concatenate the results of our dynamic object with other strings.

First, we’ll override the ToString method of our object to return the value of the source XML element of the object instance (remember that we converted attributes to elements so that they could be used as the source of an instance of our dynamic object). In the case where our object has attributes or child elements, the value returned will strip out any XML (XElement.Value works like the XmlElement.InnerText property). This is convenient when working with mixed XML content such as: <ContactInfo>The contact’s name is <name>Doug</name> and phone number is <phoneNumber>111-222-3333</phoneNumber></ContactInfo>. You can access dynObject.ContactInfo.name or dynObject.ContactInfo.phoneNumber to get the specific values, or you can call dynObject.ContactInfo.ToString() and get back “The contact’s name is Doug and phone number is 111-222-3333”.

    Public Overrides Function ToString() As String

        Return p_Xml.Value

    End Function

 

For late-bound conversions, we need to implement the TryConvert method of the DynamicObject class. If user code calls the CTypeDynamic method (new for VB 2010) to perform a late-bound type conversion, then our implementation of the TryConvert method is called. I’ve written the TryConvert method to return the results of the ToString method if converting our object to a string. For other requested conversion, TryConvert will also check and see if the source XML has no attributes or child elements. If so, TryConvert will return a late-bound conversion of the element value to the requested type. This allows the user to convert numeric or date values such as <ID>1234</ID> or <BirthDate>1/1/2010</BirthDate>.

    Public Overrides Function TryConvert(ByVal binder As ConvertBinder,

                                         ByRef result As Object) As Boolean

        If binder.Type = GetType(String) Then

            result = Me.ToString()

            Return True

        End If

 

        ' For elements without attributes or child elements, convert the element value.

        If p_Xml.Elements().Count = 0 AndAlso p_Xml.Attributes().Count = 0 Then

            Try

                result = CTypeDynamic(p_Xml.Value, binder.Type)

                Return True

            Catch

            End Try

        End If

 

        ' Pass all other conversions to the base class.

        Return MyBase.TryConvert(binder, result)

    End Function

 

For compile-time conversions, we need to implement IConvertible. Because our primary use for the object is to return string values, I’ve specified the type code as string. I’ve forwarded all other conversions other than string to the Convert class, for strongly-typed conversions. First, add the following to the beginning of the class after Inherits DynamicObject.

    Implements IConvertible

 

Then, add the specific implementations for the IConvertible interface. It’s a bit of code “bloat”, but it makes the user experience with the object far more convenient.

    Public Function GetTypeCode() As TypeCode Implements IConvertible.GetTypeCode

        Return TypeCode.String

    End Function

 

    Public Function ToBoolean(ByVal provider As IFormatProvider) As Boolean Implements IConvertible.ToBoolean

        Return Convert.ToBoolean(p_Xml.Value, provider)

    End Function

 

    Public Function ToByte(ByVal provider As IFormatProvider) As Byte Implements IConvertible.ToByte

        Return Convert.ToByte(p_Xml.Value, provider)

    End Function

 

    Public Function ToChar(ByVal provider As IFormatProvider) As Char Implements IConvertible.ToChar

        Return Convert.ToChar(p_Xml.Value)

    End Function

 

    Public Function ToDateTime(ByVal provider As IFormatProvider) As Date Implements IConvertible.ToDateTime

        Return Convert.ToDateTime(p_Xml.Value, provider)

    End Function

 

    Public Function ToDecimal(ByVal provider As IFormatProvider) As Decimal Implements IConvertible.ToDecimal

        Return Convert.ToDecimal(p_Xml.Value, provider)

    End Function

 

    Public Function ToDouble(ByVal provider As IFormatProvider) As Double Implements IConvertible.ToDouble

        Return Convert.ToDouble(p_Xml.Value, provider)

    End Function

 

    Public Function ToInt16(ByVal provider As IFormatProvider) As Short Implements IConvertible.ToInt16

        Return Convert.ToInt16(p_Xml.Value, provider)

    End Function

 

    Public Function ToInt32(ByVal provider As IFormatProvider) As Integer Implements IConvertible.ToInt32

        Return Convert.ToInt32(p_Xml.Value, provider)

    End Function

 

    Public Function ToInt64(ByVal provider As IFormatProvider) As Long Implements IConvertible.ToInt64

        Return Convert.ToInt64(p_Xml.Value, provider)

    End Function

 

    Public Function ToSByte(ByVal provider As IFormatProvider) As SByte Implements IConvertible.ToSByte

        Return Convert.ToSByte(p_Xml.Value, provider)

    End Function

 

    Public Function ToSingle(ByVal provider As IFormatProvider) As Single Implements IConvertible.ToSingle

        Return Convert.ToSingle(p_Xml.Value, provider)

    End Function

 

    Public Function ToString1(ByVal provider As IFormatProvider) As String Implements IConvertible.ToString

        Return Me.ToString()

    End Function

 

    Public Function ToType(ByVal conversionType As Type, ByVal provider As IFormatProvider) As Object Implements IConvertible.ToType

        Return Convert.ChangeType(p_Xml.Value, conversionType, provider)

    End Function

 

    Public Function ToUInt16(ByVal provider As IFormatProvider) As UShort Implements IConvertible.ToUInt16

        Return Convert.ToUInt16(p_Xml.Value, provider)

    End Function

 

    Public Function ToUInt32(ByVal provider As IFormatProvider) As UInteger Implements IConvertible.ToUInt32

        Return Convert.ToUInt32(p_Xml.Value, provider)

    End Function

 

    Public Function ToUInt64(ByVal provider As IFormatProvider) As ULong Implements IConvertible.ToUInt64

        Return Convert.ToUInt64(p_Xml.Value, provider)

    End Function

 

 

Using the Dynamic Object

That’s it for the dynamic object. To see it in action, you can code things like…

      Dim sampleXml = <SampleRoot>

                            <Person ID="1">

                                <FirstName>Doug</FirstName>

                                <LastName>Rothaus</LastName>

                                <StartDate>4/5/2002</StartDate>

                                <Rating>3.782</Rating>

                            </Person>

                            <person ID="2">

                                <FirstName>Rajesh</FirstName>

                                <MiddleName>M.</MiddleName>

                                <LastName>Patel</LastName>

                                <EmailAddress>rajeshp@example.com</EmailAddress>

                            </person>

                            <Person id="3">

                                <FirstName>Maira</FirstName>

                                <LastName>Wenzel</LastName>

                                <Company>

                                    <CompanyName>Microsoft</CompanyName>

                                </Company>

                            </Person>

                        </SampleRoot>

 

        Dim dynamicData As Object = New DynamicXmlObject(sampleXml)

 

        Console.WriteLine("Person.Count = " & dynamicData.Person.Count)

        Console.WriteLine("Person(1).MiddleName = " & dynamicData.Person(1).MiddleName)

        Console.WriteLine("Person(1).ID = " & dynamicData.Person(1).ID)

        Console.WriteLine("Person(2).Company.CompanyName = " &

                          dynamicData.Person(2).Company.CompanyName)

 

        ' This next property doesn't exist, so an empty string is returned.

        Console.WriteLine("Person(0).Middle = " & dynamicData.Person(0).Middle)

 

 

        ' Convert values to types other than string.

        Dim personID = CTypeDynamic(Of Integer)(dynamicData.Person(0).ID)

        Console.WriteLine("Person(0).ID As Integer = " & personID)

 

        Dim startDate = CTypeDynamic(dynamicData.Person(0).StartDate, GetType(DateTime))

        Console.WriteLine("Person(0).StartDate As DateTime = " &

                          FormatDateTime(startDate, DateFormat.LongDate))

 

        Dim ratingInt = CTypeDynamic(dynamicData.Person(0).Rating, GetType(Integer))

        Dim ratingDbl = CTypeDynamic(Of Double)(dynamicData.Person(0).Rating)

        Console.WriteLine("Person(0).Rating As Integer = " & ratingInt)

        Console.WriteLine("Person(0).Rating As Double = " & ratingDbl)

 

        ' Strongly-typed conversion.

        Dim startDate2 As DynamicXmlObject = dynamicData.Person(0).StartDate

        Dim sDate As DateTime = startDate2.ToDateTime(My.Application.Culture.DateTimeFormat)

        Console.WriteLine("Culture aware date conversion = " & sDate.ToString())

 

 

        ' If you added the GetMemberNames method, you can run this code.

 

        Dim person As DynamicXmlObject = dynamicData.Person(2)

 

        For Each name In person.GetDynamicMemberNames()

            Console.WriteLine(name & " = " & person.GetPropertyValue(name).ToString())

        Next

 

Why not just use XML Literals?

As a disclaimer/caveat/note, I need to mention that all of this functionality is available using XML literals and no dynamic objects. Plus XML literals provide additional support for descendant searches, XML namespaces, XML intellisense support, and so on. This example abstracts the XML for the user so that they don’t need to know anything about XML. In doing so, you lose some of the XML functionality.

 

 

Leave a Comment
  • Please add 5 and 6 and type the answer here:
  • Post
  • Hi Doug

    Just came across this article while looking for  a solution to my problem. Hope you could guide me well.

    In the process of implementing a wrapper for web sessions (i.e. HttpContext.Current.Session) for my library, I got hit by two problems:

    1. Having "Option Strict On" vomits while attempting to use the object. e.g.

    Dim ws As Object = New MyLibrary.WebSessionWrapperClass

    ws.UserName = "Username"  (this throws the compiler to disallow late binding)

    2. I have to instantiate the class everytime I need to access the session thru my wrapper, thus defeating my intentions to obtain friendly access to the web session contents.

    What could be the right (politically correct) way of achieving my objective?

    Thanks / @bhi

  • Is your Session state configured with a server rather than InProc?

  • Nopes. It is InProc. I don't really need a state server / SQL server for our apps. We don't rely too much on session state, just a few handy bits of info which can be easily re-populated, if required.

    And yes, I was able to solve my problem #2 yesternight. I did that by using a singleton instance pattern instead of a completely shared (static) class and methods. It works like a charm!

    My problem #1 still remains however.

    I have to switch "Off" the "Option Strict" to make it work. But, this then leaves me vulnerable to possible nightmares of debugging an accidental narrow conversion scenario. Besides, I might slip in an occasional VB specific code (like forgetting to CChar a string character) and resulting in non-CLS-compliant code.

    I feel safe and secure when "Option Strict" is on. However, am at a loss to find a way out with Dynamic Objects.

  • In your project properties on the "Compile" tab you can configure the individual parts of the Option Strict setting project-wide including which aspects are legal, and which are warnings/errors. If you want to allow late-binding (Dynamic) without allowing implicit conversions set the project setting to Strict On and then set the "Late binding; call could fail at runtime" setting to either warning or none. This will allow you to use late-binding project wide without allowing implicit conversions.

    -ADG

  • I may be missing some details, but I don't think Dynamic objects is the solution. Is there a reason you can't just cast the object with Option Strict On?

    Dim ws As MyLibrary.WebSessionWrapperClass = _

     TryCast(Session("mySession"), MyLibrary.WebSessionWrapperClass)

    When I work with Session objects, I prefer to get the Session data into a page-scope variable for the duration of the page, and save at the end of the page code if necessary.

  • Hmmm..

    Thanks ADG, I think that should suffice just fine! I have checked it and it does serve my purpose well. Thanks again. It was right there, staring at my face all the time and I didn't pay attention!!

    VBTeam/Doug: Yes, I know it is an overkill for session obejcts. But, I wanted to put Dynamic to test and some use in my current project. Moreover, this provides me with a way to use session variables fluently.

    So rather than having this splattered across all pages / library code:

    CInt(HttpContext.Current.Session("SomeNumber"))

    I can now have:

    WS.Current.SomeNumber.ToInt

    Where "Current" is the Shared (static) method that instantiates and returns an object of WS class.

    Thank you guys again for all the help.

  • Another pattern you might try using that I've used in the past is to create a base class for all (or most) pages in your web project (see P of EAA: Layer Supertype).

    What I do is make my own class called Page that Inherits System.Web.UI.Page and then make sure all the aspx code-behind classes inherit from my Page class. This way you get all of the normal behavior of a normal ASPX page but you get a class you can control in between the basic ASP.NET Page and all of your pages where you can add common functionality so you don't have to duplicate it on all of your pages.

    For example, if you did this in your project you could Shadow the Session property in *your* Page class and define a new Session property that always returns your strongly-typed Session object. You'd only have to do it once and all of your pages could just say Session.SomeNumber without having to worry about the plumbing or calling into Shared singletons. And if you have common page validation logic to run OnLoad of the page you can control that in one place :)

    -ADG

Page 1 of 1 (7 items)