views:

279

answers:

4

I am not getting this Linq thing. I can write complex SQL queries and have written a few xpaths. I am trying to learn Linq to XML and cannot get past my first try despite poring over every terse example I can Google.

Given XML:

<Manufacturer ManufacturerName="Acme">
 <Model ModelName="RobotOne">
  <CommandText CommandName="MoveForward">MVFW</CommandText>
  <CommandText CommandName="MoveBack">MVBK</CommandText>

Query input is "Acme", "RobotOne", "MoveBack", I want output "MVBK"

Not sure if this is the best way to construct the XML how would I do it with elements instead of attributes? There are a few manufacturers and models and lots of codes

+1  A: 

Assuming that you're using this initialization:

string xml = @"
<Manufacturer ManufacturerName='Acme'>
 <Model ModelName='RobotOne'>
  <CommandText CommandName='MoveForward'>MVFW</CommandText>
  <CommandText CommandName='MoveBack'>MVBK</CommandText>
</Model>
</Manufacturer>";

XElement topElement = XElement.Parse(xml);

You can get the result via the following LINQ query:

string commandText = topElement.Elements("Model")
    .Where(element => (string)element.Attribute("ModelName") == "RobotOne")
    .Elements("CommandText")
    .Where(element => (string)element.Attribute("CommandName") == "MoveBack")
    .Select(element => element.Value)
    .FirstOrDefault();

If you this XML is nested further down, you'll need more Select/Where combinations.

micahtan
I follow what you are doing but when I loaded this into LINQPad it returns null. If I take out the OrDefault it says "InvalidOperationException: Sequence contains no elements" Typical terse and misleading Microsoft error message, the sequence does contain elements. Something else is wrong but it is not telling me what. I was hoping to learn how to write this as a query but for now I just want to get anything to work. I think I'll switch to Java (just kidding)
Phil06
The FirstOrDefault call picks the first value of the IEnumerable, or the default value of the type if IEnumerable (in this case string) if there are no values. Most likely you're running into problems b/c topElement is not initialized correctly. I'll add my initialization code to the example.
micahtan
That's it! I'm off and running. I'm still interested in writing as a query, it can't be as hard as the response below, can it?
Phil06
The query syntax is a different side of the same coin. See http://msdn.microsoft.com/en-us/library/bb397947.aspx. It's generally straightforward to convert one to the other. That being said, I come from an XPath background, and one of the main benefits to LINQ is the ability to write queries against different sources (objects/XML) using the same syntax. If you're just using XML, you may want to consider sticking w/XPath unless you find performance to be better with LINQ, or you just want to learn it...
micahtan
+2  A: 
Kevin Won
+1. really good answer!
Otaku
I guess I need to do that. I thought Linq was just pig latin SQL but it is not. It doesn't help that you can write things two ways, query or dot-notation or a mix of the two. I learn by looking at worked out examples but because Linq is new there aren't many around. I am flummoxed by a simple Linq query that I could write on one line in either SQL or xpath.
Phil06
A: 

I've expanded your XML and demonstrate how to get the MoveBack command text. I've also added an example to grab the robot models for a particular manufacturer and list each robot's commands. The first example is broken down to demonstrate how to walk the XML structure to get an element at a time. The second example is done in one query. Of course this depends on how well you know your data. You should use SingleOrDefault and check for null before using a result if you expect it not to exist. Also, checking for attributes is important if they don't exist. This code assumes the XML is complete.

Regarding the structure of the XML it looks fine. Keeping the CommandText generic allows different commands to be supported. If the commands are always the same they could be their own elements. You could make the model name its own element, but leaving it as is - as an attribute - makes sense.

string input = @"<root>
    <Manufacturer ManufacturerName=""Acme"">
        <Model ModelName=""RobotOne"">
            <CommandText CommandName=""MoveForward"">MVFW</CommandText>
            <CommandText CommandName=""MoveBack"">MVBK</CommandText>
        </Model>
        <Model ModelName=""RobotTwo"">
            <CommandText CommandName=""MoveRight"">MVRT</CommandText>
            <CommandText CommandName=""MoveLeft"">MVLT</CommandText>
        </Model>
    </Manufacturer>
    <Manufacturer ManufacturerName=""FooBar Inc."">
        <Model ModelName=""Johnny5"">
            <CommandText CommandName=""FireLaser"">FL</CommandText>
            <CommandText CommandName=""FlipTVChannels"">FTVC</CommandText>
        </Model>
        <Model ModelName=""Optimus"">
            <CommandText CommandName=""FirePlasmaCannon"">FPC</CommandText>
            <CommandText CommandName=""TransformAndRollout"">TAL</CommandText>
        </Model>
    </Manufacturer>
</root>";
var xml = XElement.Parse(input);

// get the Manufacturer elements, then filter on the one named "Acme".
XElement acme = xml.Elements("Manufacturer")
                   .Where(element => element.Attribute("ManufacturerName").Value == "Acme")
                   .Single(); // assuming there's only one Acme occurrence.

// get Model elements, filter on RobotOne name, get CommandText elements, filter on MoveBack, select single element
var command = acme.Elements("Model")
                  .Where(element => element.Attribute("ModelName").Value == "RobotOne")
                  .Elements("CommandText")
                  .Where(c => c.Attribute("CommandName").Value == "MoveBack")
                  .Single();

// command text value
string result = command.Value;
Console.WriteLine("MoveBack command: " + result);

// one unbroken query to list each FooBar Inc. robot and their commands
var query = xml.Elements("Manufacturer")
               .Where(element => element.Attribute("ManufacturerName").Value == "FooBar Inc.")
               .Elements("Model")
               .Select(model => new {
                    Name = model.Attribute("ModelName").Value,
                    Commands = model.Elements("CommandText")
                                    .Select(c => new {
                                        CommandName = c.Attribute("CommandName").Value,
                                        CommandText = c.Value
                                    })
               });

foreach (var robot in query)
{
    Console.WriteLine("{0} commands:", robot.Name);
    foreach (var c in robot.Commands)
    {
        Console.WriteLine("{0}: {1}", c.CommandName, c.CommandText);
    }
    Console.WriteLine();
}

If you decide to use an XDocument instead you'll need to use the Root: xml.Root.Elements(...)

Ahmad Mageed
That is quite verbose compared to a one line xpath statement. Is Linq to XML really that complicated?
Phil06
@Phil06 it's not too complicated although problems do occur when the XML isn't consistent and null checking has to be made. I can agree with it being verbose and some people share the same sentiment: http://stackoverflow.com/questions/1442585/is-it-just-me-i-find-linq-to-xml-to-be-sort-of-cumbersome-compared-to-xpath However, .NET only supports XPath 1.0 so LINQ can make up for the scenarios where XPath 2.0 functions would've been useful without resorting to installing some 3rd party library that provides those features.
Ahmad Mageed
Thanks. I've been able to run some queries now. LinqPad is great for learning, it was just rejecting everything I wrote for awhile because I didn't get it. Now I'm gettina a feel for it.
Phil06
+1  A: 

Not quite the answer, but would not XPath make the code a little bit easier?

  var result1 = XDocument
            .Load("test.xml")
            .XPathSelectElements("/Manufacturer[@ManufacturerName='Acme']/Model[@ModelName='RobotOne']/CommandText[@CommandName='MoveBack']")
            .FirstOrDefault().Value;
Arve
I already know how to write xpath. I'm trying to learn Linq to XML. All the examples I find have single where clause and focus on returning a list. I need multiple where clauses and want to return a single value.
Phil06