开发者

Linq to XML Simple Query

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


If you are having difficulty with the whole LINQ to XML thing, I'd suggest taking a slight step back and just look at linq in general. I believe that linq is very much worth the effort and will repay you many times over--particularly as we migrate to the manycore world.

I'd suggest that you forget the XML part for a moment and just think about what linq is doing--after you get what linq is about, then it will make the specialized stuff for XML easier to grok. It helps me to think about linq with these two things in mind:

  1. It is expressing standard 'operators' (extension methods) on anything that implements the interface IEnumerable<T> In other words, linq is just a method call. Some of the methods take in code (Func), some (First()) do not.
  2. this allows you to be less concerned with the guts of how these operators give you the result, and more of just declaring your intent.

It's difficult for us imperative language folks to let go of the minute (over)specification of how to get a result. What linq is basically giving us is a way to declare we want a result without saying exactly how to get it. This is why, IMO, linq is so important to learn, for it is this exact 'decoupling' of intent from the exact 'how' that is the really cool part of linq. Basically it grafts in functional language concepts into poor old imperative C#.

A helpful learning aid for LINQ is to write the C# imperative code that would do the same thing. Do this a few times, and suddenly you will see the pattern that linq does for you. So, for example, consider a 'linq-to-object' declaration.

string[] names= new string[] { "kevin", "tristan", jen" };
var result = names.where(n => n.length() > 3);

the analog in C# for the linq query would be:

List<string> list = new List<string>();
foreach(string n in names)
{
    if (n.length > 3) list.add(n);
}

I think it is fairly easy to see in this example how linq is doing the same thing as the foreach (it is actually very close to this in the reality)--but you don't have all the fluff that is just compiler goo. There is less goo and more intent in the linq version. Another way of putting this is that linq is doing the boring boilerplate stuff for you implicitly allowing your code to just show the interesting part.

In functional speak, the where() method is a higher order function. This just means that it is a function that itself takes or returns a function. Where() takes a lambda--the interesting part of the loop. The lambda is an anonymous function in C#--so Where() takes a function, thus it is higher order. I mention this because this concept is so powerful and key to understanding linq and grants insight into how linq is actually a whole new programmatic model (functional programming) bolted into C#. Getting this 'higher-order-ness' is very helpful to understanding linq.

Working with the straight List<T> queries such as the one above, I think, is the best way to wrap your head around how linq works. Afer you find yourself feeling comfortable with the straignt L-2-O queries, then look again at the XML stuff--I think you will find they will then make more sense.

With the Reactive framework due out in .NET 4.0, we will see linq expand to not only 'pull' queries but to 'push' scenarios too. I.e: the collection will fire code when it changes. It is my belief that the concepts of functional programming (of which linq is C#'s tunnel into) are the most likely bread and butter ways we will handle concurrency and parallel problems, so it is very important to learn them--not just to make our code more concise.


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 this XML is nested further down, you'll need more Select/Where combinations.


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;


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(...)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜