다음을 통해 공유


LINQ Reduces Line Counts and Makes Code “Pop”

Just as soon as I learn something, I blog it.  This has a potential pitfall – sometimes I blog something and then learn that my approach was incorrect.  But I’m not too awfully proud – I just blog again and tell you my new lessons learned.

This blog is inactive.
New blog: EricWhite.com/blog

Blog TOCNearly two years ago, I wrote some LINQ to XML code to use reflection take an object graph composed of anonymous types, and transform it into an XML tree.  You can find that blog post here.  In my ignorance, I wrote this code in the imperative style.  This presents a good opportunity to compare and contrast that old, imperative code with the functional approach.  This contrast shines a spotlight on some of the benefits that we can gain from the functional approach.

The new code to effect the transformation is:

  • Shorter – 12 lines vs. 22 lines.
  • Far easier to read, provided you have the proper education.
  • More robust – the new version fixes a bug in the old code.  After I rewrote the code, the bug was patently obvious to me, as well as the fix.

Here is the old code (don’t spend too much time reading it):

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;

class Program
{
static void ObjectGraphToXElement(XElement parent, object o)
{
MemberInfo[] members =
o.GetType().GetMembers(BindingFlags.Public |
BindingFlags.Instance);
foreach (MemberInfo m in members)
{
PropertyInfo p = m as PropertyInfo;
if (p != null)
{
Type t = p.PropertyType;
object val = p.GetValue(o, null);
if (val != null)
{
if (t.IsValueType || t == typeof(string))
parent.Add(new XElement(m.Name, val));
else
{
XElement newParent = new XElement(m.Name);
parent.Add(newParent);
foreach (var v in (val as IEnumerable))
ObjectGraphToXElement(newParent, v);
}
}
}
}
}

static void Main(string[] args)
{
var objectTree = new
{
Name = "Eric",
Age = 46
};

XElement xmlTree = new XElement("Root");
ObjectGraphToXElement(xmlTree, objectTree);
Console.WriteLine(xmlTree);
}
}

When run, it produces this output:

<Root>
<Name>Eric</Name>
<Age>46</Age>
</Root>

Here is the new version:

using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

class Program
{
static object ObjectGraphToXElement(string name, object o)
{
if (o == null) return null;
if (o.GetType().IsValueType || o.GetType() == typeof(string))
return new XElement(name, o);
if (o.GetType().IsArray)
return ((object[])o).Select(z => ObjectGraphToXElement(name, z));
return new XElement(name, o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.OfType<PropertyInfo>()
.Select(z => ObjectGraphToXElement(z.Name, z.GetValue(o, null))));
}

static void Main(string[] args)
{
var objectTree = new
{
Name = "Eric",
Age = 46
};

XElement xmlTree = (XElement)ObjectGraphToXElement("Root", objectTree);
Console.WriteLine(xmlTree);
}
}

Explanation of the Code

If asked, here is how I would describe the code in ObjectGraphToXElement:

There are three pretty simple if/then statements.

If the object passed in as an argument is null, then return null.  This takes advantage of the fact that it is valid to pass null as content to the XElement constructor.  You can find more information on this here.

If the object is a value type or a string, then return a new XElement that contains the value.

If the object is an array, then return a collection of XElements that contain the contents of the items of the array, calling ObjectGraphToXElement recursively.  (The original code didn’t handle this case.)

Otherwise, return a new XElement that contains the object and its properties, calling ObjectGraphToXElement recursively.

ObjectGraphToXElement returns object, as sometimes it returns an XElement object, and sometimes it returns a collection of XElement objects.  This parallels the XElement constructors, which also take object, for the same benefit in composability.

After a little while, it becomes very easy to see the flow of types through a query.  We can examine this statement:

return new XElement(name, o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.OfType<PropertyInfo>()
.Select(z => ObjectGraphToXElement(z.Name, z.GetValue(o, null))));

The query that we want to read is:

o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.OfType<PropertyInfo>()
.Select(z => ObjectGraphToXElement(z.Name, z.GetValue(o, null)))

The first part of the query is:

o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)

This is just plain old .NET programming.

The code then dots into the OfType<T> extension method:

o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.OfType<PropertyInfo>()

This takes the collection returned by the GetMembers method, and returns a new collection of every item in the source collection that is the specified type (PropertyInfo).

The code then dots into the Select extension method:

o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.OfType<PropertyInfo>()
.Select(z => ObjectGraphToXElement(z.Name, z.GetValue(o, null)))

This tells me that the collection resulting from the call to OfType will be processed using the projection extension method (which is Select).  For a more detailed explanation of projection, see this page.

The projection for each item in the source collection will be the return value of the recursive call to the ObjectGraphToXElement method.  The use of the Name property (z.Name), and the use of the GetValue method (z.GetValue(o, null)) are common operations when using reflection.

This topic also gives a detailed explanation of following the flow of types through a query.

Sometimes I try to explain to other programmers the new “eye” that I have for this type of code.  It is easier to read.  The intent of the code “pops” out at me.

I've written a pile of code in the past, including software systems of 20,000, 50,000, and >100,000 lines of code.  I haven’t written such a system using the functional style.  However, I’ve written a number of programs using this approach that are a few hundred lines of code, and my experience remains the same – reading the code is easier than reading imperative code.  Of course, these programs would be much longer if written in the imperative style.

Points of Possible Failure

Another of the benefits of code written in the functional style is that there are fewer points of possible failure.  In the imperative code, there are various places where the code tests an object against null.  These are places where if the code isn’t written properly, the code has the potential to throw an exception.  The functional approach reduces these – the use of the OfType<T> method takes care of creating a strongly typed collection without needing to use the “as” operator.  You write the query, it returns a collection that contains zero or more items.  It’s a simpler model with fewer opportunities to make mistakes.

Learning about Functional Programming

If the above code doesn’t “pop” for you, I encourage you to spend a couple of days going through my Functional Programming Tutorial.

Practical Use of this Code

The transformation presented in this post has practical uses.  It is pretty reasonable to use the above code to create XML trees.  The following code shows a more elaborate use of the code:

using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Linq;
using System.Xml.Linq;

class Program
{
static object ObjectGraphToXElement(string name, object o)
{
if (o == null) return null;
if (o.GetType().IsValueType || o.GetType() == typeof(string))
return new XElement(name, o);
if (o.GetType().IsArray)
return ((object[])o).Select(z => ObjectGraphToXElement(name, z));
return new XElement(name, o.GetType()
.GetMembers(BindingFlags.Public | BindingFlags.Instance)
.OfType<PropertyInfo>()
.Select(z => ObjectGraphToXElement(z.Name, z.GetValue(o, null))));
}

static void Main(string[] args)
{
var PurchaseOrder = new
{
PurchaseOrderNumber = "99503",
OrderDate = DateTime.Parse("1999-10-20"),
Adresses = new
{
Address = new[] {
new {
AddressType = "Shipping",
Name = "Alice Smith",
Street = "123 Maple Street",
City = "Mill Valley",
State = "CA",
Zip = "90952",
Country = "USA"
},
new {
AddressType = "Billing",
Name = "Robert Smith",
Street = "8 Oak Avenue",
City = "Old Town",
State = "PA",
Zip = "95819",
Country = "USA",
}
}
},
Comment = "Hurry, my lawn is going wild",
Items = new
{
Item = new[] {
new {
PartNumber = "872-AA",
ProductName = "Lawnmower",
Quantity = 1,
USPrice = 148.95,
Comment = "Confirm this is electric",
ShipDate = DateTime.MinValue
},
new {
PartNumber = "926-AA",
ProductName = "Baby Monitor",
Quantity = 2,
USPrice = 39.98,
Comment = (string)null,
ShipDate = DateTime.Parse("1999-05-21")
}
},
},
};

XElement po = (XElement)ObjectGraphToXElement("PurchaseOrder", PurchaseOrder);
Console.WriteLine(po);
}
}

This code produces the following output:

<PurchaseOrder>
<PurchaseOrderNumber>99503</PurchaseOrderNumber>
<OrderDate>1999-10-20T00:00:00</OrderDate>
<Adresses>
<Address>
<AddressType>Shipping</AddressType>
<Name>Alice Smith</Name>
<Street>123 Maple Street</Street>
<City>Mill Valley</City>
<State>CA</State>
<Zip>90952</Zip>
<Country>USA</Country>
</Address>
<Address>
<AddressType>Billing</AddressType>
<Name>Robert Smith</Name>
<Street>8 Oak Avenue</Street>
<City>Old Town</City>
<State>PA</State>
<Zip>95819</Zip>
<Country>USA</Country>
</Address>
</Adresses>
<Comment>Hurry, my lawn is going wild</Comment>
<Items>
<Item>
<PartNumber>872-AA</PartNumber>
<ProductName>Lawnmower</ProductName>
<Quantity>1</Quantity>
<USPrice>148.95</USPrice>
<Comment>Confirm this is electric</Comment>
<ShipDate>0001-01-01T00:00:00</ShipDate>
</Item>
<Item>
<PartNumber>926-AA</PartNumber>
<ProductName>Baby Monitor</ProductName>
<Quantity>2</Quantity>
<USPrice>39.98</USPrice>
<ShipDate>1999-05-21T00:00:00</ShipDate>
</Item>
</Items>
</PurchaseOrder>

Using an initialized object graph and then transforming in this fashion is a way to create an XML tree with a minimum of syntactic noise, and at the expense of a little performance.

Code is attached.

ObjectGraphToXElement.cs

Comments

  • Anonymous
    August 07, 2008
    Just a small hint for performance: Instead of using GetMembers(BindingFlags.Public | BindingFlags.Instance).OfType<PropertyInfo>(), why not use GetProperties() to retrieve only properties and not fields and methods?

  • Anonymous
    August 18, 2008
    I've tried this: var obj = new {    Name = "bubi",    Marks = new { Mark = new[] { 1, 2, 3 } } }; And I get a casting exception from int[] to object[]. How can we improve the function to work also with value type arrays?

  • Anonymous
    August 20, 2008
    The comment has been removed

  • Anonymous
    November 14, 2008
    I was able to get this to work with value type arrays. Given:            var objectTree = new            {                Name = "Eric",                Age = 46,                List = new [] {1,2,3,4}            }; The methods below return: <Root>  <Name>Eric</Name>  <Age>46</Age>  <List>    <Item>1</Item>    <Item>2</Item>    <Item>3</Item>    <Item>4</Item>  </List> </Root> Here's the code: public static XElement ObjectGraphToXElement<T>(string name, T o) {    if (Equals(o, default(T))) return null;    var type = o.GetType();    if (type.IsValueType ||        type == typeof (string))        return new XElement(name, o);    if (type.IsArray)        return ArrayToXElement(o, name);    return new XElement(name, type                                  .GetProperties(BindingFlags.Public | BindingFlags.Instance)                                  .Select(z => ObjectGraphToXElement(z.Name, z.GetValue(o, null)))); } private static XElement ArrayToXElement<T>(T o, string name) {    XElement element = new XElement(XName.Get(name));    Array array = o as Array;    foreach (var item in array)    {        element.Add(ObjectGraphToXElement("Item", item));    }    return element; }