0

I have this XML string of "booms":

<booms>
  <boom>
    <name>John</name>
    <address>New York City</address>
  </boom>

  <boom>
    <name>Daniel</name>
    <address>Los Angeles</address>
  </boom>

  <boom>
    <name>Joe</name>
    <address>Chicago</address>
  </boom>
</booms>

I also have this LINQ C# code

//string xmlString = ...;
XDocument document = XDocument.Load(new StringReader(xmlString));

var booms = from boomElement in document.Descendants("boom")
            let boolChildren = (from boomElementChild in boomElement.Elements()
                                select String.Format("{0}: {1}",
                                                     boomElementChild.Name.LocalName,
                                                     boomElementChild.Value))
            select String.Join(Environment.NewLine, boolChildren);
var result = String.Join(Environment.NewLine + Environment.NewLine, booms);

that turns the XML into this string:

name: John
address: New York City

name: Daniel
address: Los Angeles

name: Joe
address: Chicago

Question:

How can I change the LINQ to filter out booms that satisfy some condition? For example, how can I filter out booms that have an address containing "New"? In this case, this would give the string:

name: John
address: New York City

The code should not be limited to only a "contains" filter though.

roger.james
  • 1,478
  • 1
  • 11
  • 23

3 Answers3

1

If the conditions are limited to equal.

Dictionary<string, string> conditions = new Dictionary<string, string> { { "name", "John" } };

XDocument document = XDocument.Load(new StringReader(xmlString));

var booms = from boomElement in document.Descendants("boom")
            where conditions.All(condition => (string)boomElement.Element(condition.Key) == condition.Value)  // Where is used to filter the result
            let boomChildren = (from boomElementChild in boomElement.Elements()
                                select String.Format("{0}: {1}",
                                                     boomElementChild.Name.LocalName,
                                                     boomElementChild.Value))
            select String.Join(Environment.NewLine, boomChildren);
var result = String.Join(Environment.NewLine + Environment.NewLine, booms);

If it is not limited to equal (contain, equal, <, >) you have to create a structure that will represent the condition.

// I've made Condition an abstract class to super any kind of condition.
// Just derive this class with the condition you want (And, Or, Equal, <=, IsNumber, ...)
public abstract class Condition
{
    // A condition is defined by this method. Because a condition is basically: "Does the specified value satisfy the condition?"
    public abstract bool Satisfy(string valueToTest);  
}

// This is the first example of condition.
// I wanted to make the condition immutable (readonly) not to be able to change them.
// So, all parameters of the condition are set during the construction.
public sealed class EqualCondition : Condition
{
    private readonly string value;
    public string Value { get { return value; } }

    public EqualCondition(string value)
    {
        this.value = value;
    }

    public override bool Satisfy(string valueToTest)
    {
        return value == valueToTest;  // Equals condition...
    }
}
public sealed class ContainCondition : Condition
{
    private readonly string value;
    public string Value { get { return value; } }

    public ContainCondition(string value)
    {
        this.value = value;
    }

    public override bool Satisfy(string valueToTest)
    {
        return valueToTest.Contains(valueToTest);  // Contains condition
    }
}

// The dictionary is used to list the conditions applied to each element.
Dictionary<string, Condition> conditions = new Dictionary<string, Condition> { { "name", new EqualCondition("John") } };  
XDocument document = XDocument.Load("test.xml");

var booms = from boomElement in document.Descendants("boom")
            // The next line check where all conditions are satisfied for the corresponding elements
            where conditions.All(condition => condition.Value.Satisfy((string)boomElement.Element(condition.Key)))
            let boomChildren = (from boomElementChild in boomElement.Elements()
                                select String.Format("{0}: {1}",
                                                        boomElementChild.Name.LocalName,
                                                        boomElementChild.Value))
            select String.Join(Environment.NewLine, boomChildren);
var result = String.Join(Environment.NewLine + Environment.NewLine, booms);
Cédric Bignon
  • 12,892
  • 3
  • 39
  • 51
  • Can you provide an example of such a general structure? For instance, imagine address being address*es* - a comma-separated string of substrings (multiple addresses) - and I want to match one of those substrings. Is that possible? – roger.james Jul 23 '13 at 16:00
  • 1
    @roger.james If I'm understanding you right, sure. It's just String comparison; you could likely just use `address.Split(",")`. To find addresses matching a condition, use `address.Split(",").Where(...)`. Apologies for not always matching the expressive LINQ format, some of us visualize these problems differently. – Katana314 Jul 23 '13 at 16:04
  • @Katana314 I don't follow you. Where do you see ","? – Cédric Bignon Jul 23 '13 at 16:12
  • 1
    @CédricBignon It was a request from his comment. "For instance, imagine address being address*es* - a comma-seperated string of substrings". A large, extendable class-based system may be what he's looking for, but I was just pointing out how he would do it inline in the LINQ expression. – Katana314 Jul 23 '13 at 16:14
1

I would strongly type this:

public class Boom
{
    string Name { get; set; }
    string Address { get; set; }
    public override string ToString()
    {
        return string.Format("Name: {0}{1}Address: {2}, Name, Environment.NewLine, Address);
    }
}

So your query changes to this:

XDocument document = XDocument.Load(new StringReader(xmlString));
var booms = 
    document.Descendants("boom")
            .Select(x => new Boom { Name = x.Element("name").Value,
                                    Address = x.Element("address").Value })
            .Where(b => /*filter here!*/);
It'sNotALie.
  • 22,289
  • 12
  • 68
  • 103
  • In fact, this question is the second part for http://stackoverflow.com/questions/17810102/extracting-from-xml-string. Where he asked in a comment to support any kind of elements in the `boom` element. Not only `name` or `address`. – Cédric Bignon Jul 23 '13 at 16:24
  • @CédricBignon I wasn't aware of that, and as the OP hasn't pointed it out in his OP it's not confirmed. Maybe his requirements have changed? – It'sNotALie. Jul 23 '13 at 16:28
  • I don't know. At least, now, he has the choice. – Cédric Bignon Jul 23 '13 at 16:29
0

Haven't tested this, but try this:

//string xmlString = ...;
XDocument document = XDocument.Load(new StringReader(xmlString));

var booms = from boomElement in document.Descendants("boom").Where(x => true)
            let boolChildren = (from boomElementChild in boomElement.Elements()
                                select String.Format("{0}: {1}",
                                                     boomElementChild.Name.LocalName,
                                                     boomElementChild.Value))
            select String.Join(Environment.NewLine, boolChildren);
var result = String.Join(Environment.NewLine + Environment.NewLine, booms);

Replace true with your test on x...

Martin Milan
  • 6,346
  • 2
  • 32
  • 44