4

I recently came across a post from the infamous Jon Skeet regarding the use of LINQ to XML. This particular snippet of code caught my eye:

// Customers is a List<Customer>
XElement customersElement = new XElement("customers",
    customers.Select(c => new XElement("customer", //This line is "magic"
        new XAttribute("name", c.Name),
        new XAttribute("lastSeen", c.LastOrder)
        new XElement("address",
            new XAttribute("town", c.Town),
            new XAttribute("firstline", c.Address1),
            // etc
    ));

I decided to test it myself in my application where I had a foreach loop set up like so:

foreach (var kvp in m_jobs) {    //m_jobs is a Dictionary<string,Job>
    m_xmlDoc.Root.Element("SCHED_TABLE").Add(
        kvp.Value.GenerateXmlNode())
    );
}

I modifed to:

m_xmlDoc.Root.Element("SCHED_TABLE").Add(
    m_jobs.Select(job => job.Value.GenerateXmlNode())
};

Where GenerateXmlNode() is a method that genrates the appropriate XML mark-up for a particular job item. I wasn't sure what would happen but lo and behold it worked exactly as my foreach loop did. What I don't quite understand is WHY?! Also, is this considered an "abuse" or a "feature" of LINQ?

Edit for clarity: I know that .Select will return an IEnumerable containing exactly what I asked for but I'm not expressly enumerating it. I understand how .Add works because it accepts a variable number of arguments but again, I don't expressly enumerate to pass those arguments. So... how does it still work?

Kittoes0124
  • 4,930
  • 3
  • 26
  • 47
  • 3
    Why what? What's surprising with this code, given what `XElement.Add` is advertised to do? – Jon Feb 19 '13 at 19:46
  • It's cool certainly, but it shouldn't be surprising. This is how LINQ behaves and how it's expected to behave. – Joel Etherton Feb 19 '13 at 19:47
  • 2
    @JoelEtherton Actually, it is surprising. The method overload is `Add(Object)`, not `Add(IEnumerable)`. How would you know it expects a sequence or a single item, and not just a single item? – Servy Feb 19 '13 at 19:48
  • I don't get why this is so magical to you. That is how the `.Select` statement work. – Automatico Feb 19 '13 at 19:48
  • @Cort3z It's not the `Select` that's magical, it's the `Add` method. – Servy Feb 19 '13 at 19:49
  • @Servy: I disagree: http://msdn.microsoft.com/en-us/library/system.xml.linq.xelement.add.aspx – Joel Etherton Feb 19 '13 at 19:49
  • Note to OP: the amount of confusion indicates the you should edit for clarity - what is surprising and why? – Paul Bellora Feb 19 '13 at 19:50
  • 4
    @JoelEtherton What's your point? Generally it should be obvious from the public API of a method whether it accepts a sequence or a single item. It is *very* weird and unexpected to have a parameter of type `object` that determines if the type implements `IEnumerable` and acts differently if it does. I know I didn't expect that, I expected an additional overload of the method of `pubic void Add(IEnumerable objects)`. – Servy Feb 19 '13 at 19:55
  • 1
    @Servy: My apologies. I pasted the wrong link. This link describes why it is unsurprising: http://msdn.microsoft.com/en-us/library/bb943882.aspx. If you RTM, you'll see it indicates complex content can be passed in: `Any type that implements IEnumerable`. – Joel Etherton Feb 19 '13 at 20:07
  • @JoelEtherton Thanks for the link. Definitely easy to understand after reading but it's definitely unintuitive if just going off of the API. – Kittoes0124 Feb 19 '13 at 20:13
  • @JoelEtherton The fact that you need to find a page like that to understand how to use the method is specifically what makes the design of this method confusing. For almost any other method the public API alone is enough information. – Servy Feb 19 '13 at 20:17

4 Answers4

7

The XElement.Add method will look something like this under the hood:

public void Add(object content)
{
    if (content is IEnumerable)
    {
        foreach (object child in (IEnumerable)content)
            Add(child);
    }
    else
    {
        //process individual element
    }
}

So while it's not clear from the public interface of Add, you can pass it either a sequence of items, or a single item, and it will determine which it is at runtime and act accordingly.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • Interesting, so this only works because Add() supports it under the hood then and I shouldn't expect this to work in other areas? – Kittoes0124 Feb 19 '13 at 19:59
  • 1
    @Kittoes, Yes it is because Add in this class is so special. Please try not to write such code yourself based on this single case - there is absolutely no way to figure this behavior based on method signature. – Alexei Levenkov Feb 19 '13 at 20:04
  • @AlexeiLevenkov Exactly my thoughts, had I seen an IEnumerable in the method signature then it would've been extremely obvious what's going on. – Kittoes0124 Feb 19 '13 at 20:14
5

No magic; the Add method accepts either a object or a params object[] - and internally it simply checks each input for a range of common scenarios, including IEnumerable etc. It then simply unrolls the sequence, adding the child elements / attributes that it discovers. LINQ returns (in this scenario) an IEnumerable sequence from Select, which makes it entire usable.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
2

Select is a LINQ extension method that can be applied to any type implementing IEnumerable<T>. As parameter it accepts a delegate or a lambda expression (here a lambda expression). This lambda expression defines an ad-hoc function which is applied to each element of the collection. These elements are represented by c here. Select yields an IEnumerable<U> where U is the type of the items returned by the lambda expression. In other words Select transforms elements of type T to elements of type U by the means of the lambda expression (here Customers to XElements).

Since the second argument to the XElement constructor accepts enumerations as well, this "magic" is possible.

public XElement(
    XName name,
    Object content
)

The content can be a IEnumerable<XElement> among other things. The System.Xml.Linq namespace is very flexible. There is also an implicit conversion from string to XName allowing you to pass a string as first argument.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • `"Since the second argument to the XElement constructor accepts enumerations as well, this "magic" is possible."` He's not calling the XElement constructor, he's calling the `Add` method, which doesn't accept an enumeration as it's parameter. – Servy Feb 19 '13 at 20:05
  • The question is about Add, and there is no constructor that accepts enumeration (`params`, but not `IEnumerable`). – Alexei Levenkov Feb 19 '13 at 20:07
  • The "magic" happens in the first code snippet where no `Add` is called. `XElement` has a constructor overload: `public XElement( XName name, Object content)`. This `content` can be an `IEnumerable`. – Olivier Jacot-Descombes Feb 19 '13 at 20:10
  • @OlivierJacot-Descombes The second and third snippet have absolutely nothing to do with the third. I am specifically using the `.Select` in the `.Add` statement and despite the fact that Add() doesn't have an overload accepting an IEnumerable the code runs just fine. After decompiling System.Xml.Linq in dotPeek I can see that the XContainer object contains the Add() function which works exactly as described in the answer above. It checks to see if the object passed is an enumerable or an array (amongst several other things). – Kittoes0124 Feb 19 '13 at 20:18
  • OK, the comment `'This line is "magic"'` should be in the third snippet then and not in the first. I described the "magic" of the line marked with "magic". – Olivier Jacot-Descombes Feb 20 '13 at 13:11
1

This question, in fact, is composed of three whys. Before the Magic's Biggest Secrets Finally Revealed, we would first need Breaking the Magician's Code, so that we can see through the questions:

Func<Customer, XElement> selector=
    c => {
        var xe=new XElement("address",
            new XAttribute("town", c.Town),
            new XAttribute("firstline", c.Address1)
            // , etc
            );

        return
            new XElement("customer", // This line is a part of the "magic"
                new XAttribute("name", c.Name),
                new XAttribute("lastSeen", c.LastOrder),
                xe
                );
    };

XElement customersElement=new XElement("customers", customers.Select(selector)); // This line is another part of the "magic"

The Customer class is assumed within fields or properties of LastOrder, Address1, Town and Name. Following are the divided questions and answers:

Q1: With the code snippet which caught your eye, why the members of element in an IEnumerable can be accessed without your expressly enumerating?

A1: The element is passed with lambda expression. That is, the argument passed to Select is a delegate. Thus, you can access the members of element passed with c. And the constructor we invoke inside the delegate is the overload of XElement(XName name, params object[] content), which you can pass objects either XAttribute or XElement.

Q2: With the snippets in two different syntax of your code, why the statement of Add with a projected enumerable of Select works like calling the constructor of XElement with a selected enumerable, produces the equivalent result as the code use foreach?

A2: The IEnumerable or IEnumerable<T> is passed as an object either the overloading constructor of XElement and Add method which inherited from XContainer.

Q3: According to Q2, why IEnumerable can still be enumerated in the case of it has been passed as an object?

A3: An instance of a class always is a(n) instance of its class, even though you pass it with a bigger type, generic type or interface. Although here we do not encounter the problems of interface, I would suggest to take a look of [this answer] to get the idea of that. Inside the code of XContainer.Add(object content), the passed argument of an IEnumerable would be processed with the following code:

IEnumerable enumerable=content as IEnumerable;

if(enumerable!=null) {
    foreach(object element in enumerable) {
        this.Add(element);
    }
}
else {
    this.AddString(GetStringValue(content));
}

However, if content is an array, instead of as IEnumerable it would be processed with following code(perhaps for gaining performence):

object[] objArray=content as object[];

if(objArray!=null) {
    foreach(object element in objArray) {
        this.Add(element);
    }
}

You might wonder why even passing an array would invoke XContainer.Add(object content), and it is because of the overloading of Add for array is:

public void Add(params object[] content) {
    this.Add(content);
}

Now, you know how it's done.

Community
  • 1
  • 1
Ken Kin
  • 4,503
  • 3
  • 38
  • 76