-2

Could someone assist with this .NET Linq query issue. It should work, but it doesn't and fails to find the element. The query should result in a value = 106492, but just returns null. Tnx so much!

Here's the .Net Fiddle code:

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

public class Program
{
    public static void Main()
    {
        String rxml = @"<TestMessage>
          <TestIdentification>
            <TestID>106491</TestID>
            <TestType>TESTID1</TestType>
          </TestIdentification>
          <TestIdentification>
            <TestID>106492</TestID>
            <TestType>TESTID2</TestType>
          </TestIdentification>
          <TestIdentification>
            <TestID>106493</TestID>
            <TestType>TESTID3</TestType>
          </TestIdentification>
          <TestIdentification>
            <TestID>106494</TestID>
            <TestType>TESTID4</TestType>
          </TestIdentification>
          </TestMessage>
          ";

        XElement xml = XElement.Parse(rxml);

        // Read all response fields responseLog class
        var testObj = (from el in xml.DescendantsAndSelf()
                       select new TestClassObj
                       {
                            TestID = el.Elements("TestIdentification").Elements("TestType").
                                       Where( c => c.Value == "TESTID2" ).Elements("TestID").
                                       Where( c => !string.IsNullOrWhiteSpace(c.Value)).FirstOrDefault().Value,
                     }).FirstOrDefault();

    if( testObj != null )
        Console.WriteLine(testObj.TestID);

    }
}

public class TestClassObj
{
    public String TestID{ get; set; }
    public String TestText{ get; set; }
}
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Not the same question at all. I need an answer to this problem. –  Oct 01 '18 at 23:25
  • Well, I already know that much =) It's the Where( c => !string.IsNullOrWhiteSpace(c.Value)).FirstOrDefault().Value that is failing, but I don't know why, it seems like a bug to me since it should produce the correct value, but doesn't. Please remove the duplicate notification since it's not a duplicate. I changed the title. –  Oct 01 '18 at 23:34
  • Thanks, yeah it's not that at all. Using Descendants produces the same results. I actually need DescendantsAndSelf in my actual code since I don't have a root xml element and this seems to work without it except for this search. Hopefully, someone will have the right answer for me. Thanks for the suggestions though. –  Oct 01 '18 at 23:41
  • Using LINQPad, `xml.DescendantsAndSelf().Elements("TestIdentification").Elements("TestType").Where(c => c.Value == "TESTID2").Dump();` shows `TESTID2`. Additional `Elements` calls from there are over-filtering down to nothing. – Jonathon Chase Oct 02 '18 at 00:14
  • 1
    Your LINQ is looking for an element `TestID` that is a *child of* the `TestType` element. But there aren't any-- they are peers, not parent/child. – John Wu Oct 02 '18 at 00:14
  • So, is there a way correct this search? I need the TestID element value for a particular TestType element type. Trying to this all in the same line is probably not the best approach, but there should be a way I think to pull it off. –  Oct 02 '18 at 00:22
  • It's not clear to me why you've got the "outer" search part at all. The "inner" query is only going to be useful on the root element, and the inner query doesn't really use `TestClassObj` except as a way of determining TestID. A single query that just resolves to a string would be simpler, IMO. Your comment on IVSoftware's answer suggests that some aspects of your code can't be changed, but it's not clear what you mean, and that constraint isn't in the question. – Jon Skeet Oct 02 '18 at 01:00

3 Answers3

3

First, I removed the "outer" query part of:

(from el in xml.DescendantsAndSelf()
 select new TestClassObj { ... }).FirstOrDefault()

Given that you're selecting TestIdentification elements which only occur directly beneath the root element, that query is pointless - it will only find anything for the root element.

Next, I've removed the usage of your TestClassObj - you're only using it as a way of wrapping a string, so you might as well just use the string directly.

Next, change the focus of the query: you're not trying to find a TestType element, you're trying to find a TestIdentification element which has the right TestType child. You can then find the TestID element of the same TestIdentification element.

Next, I've used casts from XElement to string rather than using the Value property. The cast will just return null when it's called on a null element, rather than throwing a NullReferenceException. It's a very useful approach in LINQ to XML when you want to just ignore elements which don't have the expected child element/attribute.

Here's what's left:

    var testId = xml.Elements("TestIdentification")
                    .Where(x => (string) x.Element("TestType") == "TESTID2")
                    .Select(x => (string) x.Element("TestID"))
                    .FirstOrDefault();

    Console.WriteLine(testId);

That prints out 106492 as expected.

An alternative would be to find the right TestType element, then navigate to its parent and find its TestID child:

var testId = xml.Elements("TestIdentification")
                .Elements("TestType")                      
                .Where(x => (string) x == "TESTID2")
                .Select(x => (string) x.Parent.Element("TestID"))
                .FirstOrDefault();

Console.WriteLine(testId);

That works just as well; use whichever form you find easiest to work with.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Jon, this works exactly how I needed. Thank you so much! I plugged this query into my code and it worked great. I didn't realize about the string casting, that's a great tip. Thanks again for taking the time to help me solve it. –  Oct 02 '18 at 01:42
0

You could try letting System.Xml.Linq do a little bit more of the work for you. Something like this might be a little simpler.

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

public class Program
{
    public static void Main()
    {
        String rxml = @"<TestMessage>
          <TestIdentification>
            <TestID>106491</TestID>
            <TestType>TESTID1</TestType>
          </TestIdentification>
          <TestIdentification>
            <TestID>106492</TestID>
            <TestType>TESTID2</TestType>
          </TestIdentification>
          <TestIdentification>
            <TestID>106493</TestID>
            <TestType>TESTID3</TestType>
          </TestIdentification>
          <TestIdentification>
            <TestID>106494</TestID>
            <TestType>TESTID4</TestType>
          </TestIdentification>
          </TestMessage>
          ";

        XElement xml = XElement.Parse(rxml);

        // Let Xml.Linq pick out the "TestIdentification" XElements
        foreach (var tid in xml.Descendants("TestIdentification"))
        {
            TestClassObj testObj = new TestClassObj(tid.Element("TestID").Value, tid.Element("TestType").Value);
            Console.WriteLine(testObj);
        }
        Console.WriteLine("Any Key to Continue...");
        Console.ReadKey();
    }
}

public class TestClassObj
{
    // Make sure we can construct a TestClassObj on-demand
    public TestClassObj(string testID, string testText)
    {
        TestID = testID;
        TestText = testText;
    }
    public String TestID { get; set; }
    public String TestText { get; set; }
    // Define how we would like the object displayed
    public override string ToString()
    {
        return "TestID:" + TestID + " TestText:" + TestText;
    }
}

enter image description here

IVSoftware
  • 5,732
  • 2
  • 12
  • 23
  • I appreciate you looking at this, but I need to keep the query that I have above since my main project has a ton of queries tied to a class and this is the only query that doesn't work and I don't why. When I add the Elements("TestID"). Where( c => !string.IsNullOrWhiteSpace(c.Value)).FirstOrDefault().Value on the end it doesn't find the value. It should work I would think. Any ideas? –  Oct 01 '18 at 23:59
  • I see. Ok, let me think about that for a second. – IVSoftware Oct 02 '18 at 00:04
  • Cool tnx. I've spent 2-3Hrs on this and it seems so simple =) I finally decided to create some test code to see if someone smarter than I could solve it. If I do this TestID = el.Elements("TestIdentification").Elements("TestType"). Where( c => c.Value == "TESTID2" ).Elements("TestID"). .FirstOrDefault().Value the TestID = "TESTID2" so it's the last search it doesn't like =) –  Oct 02 '18 at 00:12
  • I see one possible problem: You take a .FirstOrDefault and then reference .Value from that. So if the .FirstOrDefault() renders to the default case, then you will be dereferencing: null.Value and that will throw the exception – IVSoftware Oct 02 '18 at 00:16
  • el.Elements("TestIdentification").Elements("TestType").Where(c => c.Value == "TESTID2") evaluates to this XElement: TESTID2 and this has no XAttributes of its own. So when you take .Elements("TestID") from that you get the null. – IVSoftware Oct 02 '18 at 00:37
0

You want to use Element instead of Elements on TestIdentification's children and combine the filter.

var testObj = (from el in xml.DescendantsAndSelf().Elements("TestIdentification")
               where el.Element("TestType").Value == "TESTID2" && !string.IsNullOrWhiteSpace(el.Element("TestID")?.Value)
               select new TestClassObj { TestID = el.Element("TestID").Value })
               .FirstOrDefault();

Console.WriteLine(testObj.TestID); // 106492
Jonathon Chase
  • 9,396
  • 21
  • 39