Click here to Skip to main content
Licence CPOL
First Posted 28 May 2010
Views 21,494
Downloads 425
Bookmarked 59 times

A Multi-level C# 4.0 Dynamic Object

By | 18 Oct 2010 | Article
Creating a multi-level object using the dynamic features of C# 4.0

Introduction

The new C# 4.0 comes with the capability of having dynamic objects. Simply speaking, the objects that you use regularly are non-dynamic objects, and the compiler uses a behavior called early-binding by which it checks that the object's properties and methods exist at compile time. But when you use the new dynamic keyword to declare an object, you are instructing the compiler to use a late-binding mechanism, by which all those checks are postponed until run-time. In other words, when using a dynamic "type", the compiler understands that the actual type of the object you are using is not known until run-time, and that only then it is valid to access the known metadata about the object to perform the actual binding.

This feature permits you much more flexibility when solving a number of scenarios, as for example, interacting with scripting languages as JavaScript, Python, Ruby, and others. But it also permits you to write code against objects whose properties and methods can not be completely known when writing your program, or even worse, when they can change dynamically (as for instance, adding or removing members and properties) while the object is alive.

The latter is precisely what the ExpandoObject object solves. This class is part of the infrastructure provided by the C# 4.0 compiler to deal with these dynamic scenarios, along with the DynamicObject class, the System.Dynamic namespace classes, and the DLR - the Dynamic Language Runtime. But as soon as you start using it, you realize that the ExpandoObject class is tailored to support single-level objects, meaning that it does not really support members that can be dynamic objects themselves. This article describes the DeepObject class that permits you to instantiate multi-level dynamic objects, defined as dynamic objects whose members are themselves dynamic objects.

DeepObject's Basics

As an example, suppose that you have a program that handles franchise stores, and that your problem is that the information that you have about each store can vary (for instance, if the source of information is a set of XML files). In this scenario, to create a given object, you may want to write something like this:

dynamic obj = new DeepObject();
obj.Id = "ES001";
obj.Owner.FirstName = "Juan";
obj.Owner.LastName = "Garcia";
obj.Address.City = "Madrid";
obj.Address.Street = "Gran Via, 1";

The first thing to note in the example is that you declare your newly created DeepObject instance with the dynamic keyword. This is necessary, as said, to use the late-binding mechanism and to avoid errors that, otherwise if using early-binding, the compiler would raise.

Now, you are assigning a value to the first-level property Id. The basic idea is that the object keeps track internally of what properties have been accessed and the values assigned to them, creating entries each time a binding is performed, and intercepting the bindings to give back the values the properties have as needed. This is nothing new, and indeed the ExpandoObject class provides precisely this capability, as said before.

But now comes the fun: when you assign a value to the FirstName property of the Owner property, the DeepObject host notices that the first-level property doesn’t exist yet, and creates it as a new instance of DeepObject. Then, on this new dynamic member, it performs the binding as described above.

The original host is said to be a "level-0" object, because it has no parent. Each of its members are said to be "level-1" objects, the members of these members are said to be "level-2" objects, and so on. DeepObject is prepared to accept an arbitrary depth of levels, and, of course, there is no limit to the number of members that can exist in each level.

The string representation of a DeepObject instance wraps inside square brackets each member and its value (if exists). If a member has its own members, then inside the main brackets, each sub-member is wrapped with its own set of square brackets. So, if you write:

Console.WriteLine( “> {0}”, obj );

with the object in the above example, you get:

[Id:ES001], [Owner, [FirstName:Juan], [LastName:Garcia]], 
                    [Address, [City:Madrid], [Street:Gran Via, 1]]

You can see how the Id property is printed along with its value, and how the composite second level members (the Owner and Address in this example) expand themselves to include within its main brackets the sub-members that they host.

Finally, all of what have been said about properties can also be said about methods: you can set methods in a DeepObject instance just by simply assigning a delegate as the member’s value, as in the following example:

obj.Greetings = (Action<string>)( x => Console.WriteLine("Hello {0}", x ) );
obj.Greetings( "John" );

In this case, you want the object to have a Greetings( string name ) method that takes one string parameter, prints in the console the famous “Hello world“ thing, and returns nothing. You only have to assign a delegate as the value of the member (in this case created as an Action<string> to follow the signature we want for this method), and later you can call it the way to are used to for regular object’s methods.

Under the Hood

The DeepObject class comes with a rich set of methods that allow you to access its state, members, values, and operate with it. All of those methods have the "Deep" prefix in order not to collide with any existing method of its base class of any method you want to "inject" in a given instance.

Also, it is important to note that DeepObject has no public properties or fields, but only methods, and this is on purpose, to avoid colliding with your members' names. Yes, it uses internal members, but are private ones.

Creating a New DeepObject Instance

The constructor of DeepObject is as follows:

public DeepObject( string name = null, bool caseSensitive = true );

The first thing to note is that you can assign to each DeepObject instance a name if you wish. If none is assigned, null is used. The object's name can be retrieved using the object's DeepName() method.

Similarly, names can be changed or cleaned (just by passing null as the argument) using the DeepSetName( string name ) method, but only if the object is a "root" one (defined as an object that has itself no parent). If you try to change the name of a member, it is an error and the method raises an exception.

The second thing to note is the caseSensitive argument. By default, it is true, emulating the standard behavior of C# members. But there are scenarios (as for instance, when retrieving information from XML files or databases) where it has no sense to distinguish among the members using a case sensitive comparison type. For those cases, simply pass false as the value of this argument, and note that obj.Id, obj.ID, and obj.id are all referring to the same member. You can obtain the value of this flag using the object's DeepCaseSensitive() method.

Finally, as you will see below, that DeepObject instances are created automatically to host the object's members, and those new ones are created with the case sensitive value used in the host object.

Internal List of Members

As said, each member of a DeepObject instance is a DeepObject by itself. Internally, a list of members is maintained to keep track of the added members and the value each member holds, if any. DeepObject derives from the DynamicObject class provided by the .NET framework, and overrides its TryXXX() methods to intercept the binding requests, returning back the values assigned to each member, if exists, or creating and adding to the list new members as needed.

If you try to get the actual DeepObject instance of one of your object's members by using the standard binding mechanism, typically, you will get the value this member contains. To solve this, DeepObject provides you two methods:

public DeepObject DeepMember( string name, bool raise = true );
public DeepObject DeepMember( int n, bool raise = true );

Both return the DeepObject instance of the member you are referring to. The first version tries to find the member by its given name (and remember, using the case sensitive flag used when creating the host object). The second option just uses its ordinal position within the list of members.

A DeepObject object is said to be a "tree" if it has members. In this case, its DeepIsTree() method returns true, its DeepMembers() returns an enumeration that can be used to iterate all its members, and its DeepCount() returns the actual number of members hosted.

A member can access its parent using the DeepParent() method. An object with no parent is said to be a "level-0" object, each of its members are "level-1" objects, each of the members of those are "level-2" objects, and so on. The DeepLevel() method returns the level of its DeepObject instance.

To create a member, you can use the DeepAddMember() and DeepAddIndex() methods, and DeepRemoveMember() to eliminate one of its members. The DeepClearMembers() method removes them all.

Object's Values

Any DeepObject instance can host a value (and note that null is considered a valid value). When a value is set for a DeepObject object, its DeepHasValue() method returns true, and you can access the actual value using DeepValue(). You can clear its value using the DeepClearValue() method. And finally, you can directly set a DeepObject's value with its DeepSetValue() method.

A DeepObject is considered a value wrapper when it has a value assigned and simultaneously it is not a tree. In this case, its DeepIsWrapper() method returns true.

When using the standard binding mechanism to access a member's value, if it is a value wrapper, the actual value object (or null) is returned. If it is not a wrapper, for instance when it has members (regardless if it has a value assigned or not), then the actual DeepObject instance is returned. Please bear that in mind so you are not confused.

A Note on Members Accessed by Indexes

You can also refer to members by using indexes, for instance: obj.Member[ 1, "name", null ]. To handle these scenarios, the approach DeepObject uses is to give the new member a name built with a deterministic way of combining the actual values of each of the indexes used. You can obtain the name to be used by a given set of indexes by using the static DeepIndexesToString() method of the DeepObject class - this is useful when, for instance, you want to remove the member identified by those indexes.

Cloning a DeepObject

DeepObject implements the ICloneable interface to create a new instance with the same values and members of the original one - except its parent value, which is set to null. So the case sensitive flag is the same as the original object.

If it has a value assigned, and it is cloneable, than a clone is assigned as the new object's value instead of merely copying the reference. Otherwise, a plain copy is assigned. If you don't like this default, use its DeepCloneOnCopying property to modify this behavior, but also note that this flag is propagated down in the object's tree.

Finally, note that this process is iterated with each of the original object's members, so a new tree of members is created and injected into the newly cloned object if needed.

History

  • V2: October 12, 2010. New methods and features added, and some improvements
  • V1: May 28, 2010. Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Moises Barba



Spain Spain

Member

Moises Barba works for the Consulting division of a major multinational IT company.
While he has not to develop for a living nowadays, solving complex puzzles has been ever among his main interests - that's why he has spent his latest 20 years trying to combine his degree is in Theoretical Physics with his MBA... and he is still trying to figure how this two things can fit together.
Flying a lot across many countries, along with the long working days that are customary in consultancy, he can say that, after all, he lives in Spain (at least the weekends).

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
Generalhiiiiiiiiii Pingroupglory4448:58 15 Apr '11  
GeneralAmazing Pinmemberjapp277:45 10 Dec '10  
GeneralRe: Amazing Pinmembermbarbac21:19 10 Dec '10  
GeneralRe: Amazing Pinmemberwonderdelight3:36 18 Jan '11  
Generalerror PinmemberJohn Giblin17:36 15 Nov '10  
GeneralRe: error Pinmembermbarbac1:40 17 Nov '10  
GeneralRe: error PinmemberJohn Giblin12:32 18 Nov '10  
GeneralMy vote of 3 PinmemberBilal Haider22:27 27 Oct '10  
Generalbinding Pinmembercqwydz7:19 17 Oct '10  
GeneralRe: binding Pinmembermbarbac12:05 18 Oct '10  
GeneralDynamic Objects and Why.... Pinmemberxzz01953:30 15 Oct '10  
Generalok article PinmemberDonsw17:44 31 May '10  
GeneralRe: ok article PinmemberDmitri Nesteruk6:29 6 Jun '10  
GeneralVery useful PinmemberBill Seddon14:22 31 May '10  
GeneralRe: Very useful Pinmembershakil03040035:40 19 Oct '10  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web02 | 2.5.120517.1 | Last Updated 18 Oct 2010
Article Copyright 2010 by Moises Barba
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid