Retrieve SiteMapNode matching custom attribute using LINQ - c#

I am new to LINQ. I have an ordinary siteMap XML document with custom attributes. One of these attributes is: id
I would like to use LINQ to retrieve a single node matching the value of the custom attribute (id).
etc.
My attempt at the LINQ looks like this:
private SiteMapNode FindNodeById(SiteMapNodeCollection nodes, int siteMapNodeId)
{
var pageNode = from SiteMapNode node in nodes.Cast<SiteMapNode>()
where node["id"] == Convert.ToString(siteMapNodeId)
select node;
return (SiteMapNode)pageNode;
}
During debugging, pageNode becomes assigned with:
{System.Linq.Enumerable.WhereEnumerableIterator<System.Web.SiteMapNode>}
And on the return statement an InvalidCastException is thrown:
Unable to cast object of type 'WhereEnumerableIterator`1[System.Web.SiteMapNode]' to type 'System.Web.SiteMapNode'.
Any help is appreciated! :)
EDIT: I've re-posted this question in a clearer manner here: Re-worded Question
Thanks to Stefan for putting me on the right track!

You try to cast a IEnumerable<SiteMapNode> to a SiteMapNode. Use First to filter and return one node:
return nodes
.Cast<SiteMapNode>()
.First(node => node["id"] == Convert.ToString(siteMapNodeId));

pageNode is a sequence of nodes.
You want to call First() to get the first item in the sequence.

Related

Roslyn Find Same Node in Changed Document

As we all know Roslyn Syntax Trees are Immutable, so after making changes you need to get a new node.
I'm trying to update a document using the document editor, but I keep getting an error that the node is not found in the syntax tree.
public static T FindEquivalentNode<T>(this Document newDocument, T node)
where T : CSharpSyntaxNode
{
var root = newDocument.GetSyntaxRootAsync().Result;
return root.DescendantNodes().OfType<T>()
.FirstOrDefault(newNode => SyntaxFactory.AreEquivalent(newNode, node));
}
When I try to Call this again the document editor:
var newFieldDeclaration = documentEditor.GetChangedDocument().FindEquivalentNode(syntaxNode);
documentEditor.ReplaceNode(newFieldDeclaration, propertyDeclaration);
I get an error:
The node is not part of the tree
The newField Declaration is not null it find an equivalent field yet I still get this error, How Can I Replace this node?
You get the node because in your FindEquivalentNode method, you are doing that:
SyntaxFactory.AreEquivalent(newNode, node)
SyntaxFactory.AreEquivalent is not return true for "real" same nodes, but for nodes\tokens that are seems equal in their structure (with consideration of topLevel parameter).
Back to your question, if you want to call ReplaceNode you must have the "real" old node, so you wouldn't get an
The node is not part of the tree
To achive that, you have a few options, one of them is what #Tamas wrote in comments, use SyntaxAnnotations.
Example:
//Add annotation to node
var annotation = new SyntaxAnnotation("your_annotation", "some_data");
node = node .WithAdditionalAnnotations(annotation);
// Now you can change the tree as much you want
// And when you want to get the node from the changed tree
var annotatedNode = someNode.DescendantNodesAndSelf().
FirstOrDefault(n => n.GetAnnotations("your_annotation").Any())

Efficient Way to Parse XML

I find it puzzling to determine the best way to parse some XML. It seems they are so many possible ways and none have really clicked with me.
My current attempt looks something like this:
XElement xelement = XElement.Parse(xmlText);
var name = xelement.Element("Employee").Attribute("name").Value;
So, this works. But it throws an exception if either the "Employee" element or the "name" attribute is missing. I don't want to throw an exception.
Exploring some examples available online, I see code like this:
XElement xelement = XElement.Load("..\\..\\Employees.xml");
IEnumerable<XElement> employees = xelement.Elements();
Console.WriteLine("List of all Employee Names :");
foreach (var employee in employees)
{
Console.WriteLine(employee.Element("Name").Value);
}
This would seem to suffer from the exact same issue. If the "Name" element does not exist, Element() returns null and there is an error calling the Value property.
I need a number of blocks like the first code snippet above. Is there a simple way to have it work and not throw an exception if some data is missing?
You can use the combination of the explicit string conversion from XAttribute to string (which will return null if the operand is null) and the FirstOrDefault method:
var name = xelement.Elements("Employee")
.Select(x => (string) x.Attribute("name"))
.FirstOrDefault();
That will be null if either there's no such element (because the sequence will be empty, and FirstOrDefault() will return null) or there's an element without the attribute (in which case you'll get a sequence with a null element, which FirstOrDefault will return).
I often use extension methods in cases like this as they work even if the reference is null. I use a slightly modified version of the extension method's from Anders Abel's very good blog posting from early 2012 'Null Handling with Extension Methods':
public static class XElementExtension
{
public static string GetValueOrDefault(this XAttribute attribute,
string defaultValue = null)
{
return attribute == null ? defaultValue : attribute.Value;
}
public static string GetAttributeValueOrDefault(this XElement element,
string attributeName,
string defaultValue = null)
{
return element == null ? defaultValue : element.Attribut(attributeName)
.GetValueOrDefault(defaultValue);
}
}
If you want to return 'null' if the element or attribute doesn't exist:
var name = xelement.Element("Employee")
.GetAttributeValueOrDefault("name" );
If you want to return a default value if the element or attribute doesn't exist:
var name = xelement.Element("Employee")
.GetAttributeValueOrDefault("name","this is the default value");
To use in your for loop:
XElement xelement = XElement.Load("..\\..\\Employees.xml");
IEnumerable<XElement> employees = xelement.Elements();
Console.WriteLine("List of all Employee Names :");
foreach (var employee in employees)
{
Console.WriteLine(employee.GetAttributeValueOrDefault("Name"));
}
You could always use XPath:
string name = xelement.XPathEvaluate("string(Employee/#name)") as string;
This will be either the value of the attribute, or null if either Employee or #name do not exist.
And for the iterative example:
foreach (XNode item in (IEnumerable)xelement.XPathEvaluate("Employee/Name"))
{
Console.WriteLine(item.Value);
}
XPathEvaluate() will only select valid nodes here, so you can be assured that item will always be non-null.
It all depends on what you want to do with the data once you've extracted it from the XML.
You would do well to look at languages that are designed for XML processing, such as XSLT and XQuery, rather than using languages like C#, which aren't (though Linq gives you something of a hybrid). Using C# or Java you're always going to have to do a lot of work to cope with the fact that XML is so flexible.
Use the native XmlReader. If your problem is reading large XML files instead of allowing the XElement to build an object representation, you can build something like Java SAX parser that only stream the XML.
Ex:
http://www.codeguru.com/csharp/csharp/cs_data/xml/article.php/c4221/Writing-XML-SAX-Parsers-in-C.htm

Filtering XmlNodeList with additional XPath query?

I have a large XML document and I am using C# to query the content. I have something similar to the following:
var bookA = xmlDoc.SelectSingleNode("/books/book[yearPublished=2012 and id=123]");
var bookB = xmlDoc.SelectSingleNode("/books/book[yearPublished=2012 and id=456]");
var bookC = xmlDoc.SelectSingleNode("/books/book[yearPublished=2012 and id=789]");
As you can see, the filter on "yearPublished" is repeated in each query and, please correct me if I'm wrong, the entire list of books is parsed repeatedly. Would it be more efficient to have something like the following:
var newBooks = xmlDoc.SelectNodes("/books/book[yearPublished=2012]");
var bookA = newBooks.SelectSingleNode("book[id=123]");
var bookB = newBooks.SelectSingleNode("book[id=456]");
var bookC = newBooks.SelectSingleNode("book[id=789]");
Assuming my document is large (let's say it contains the data about several thousand books), am I correct that it would be more efficient to filter the data based on the first criteria and then select the desired XmlNode from the filtered list?
Secondly, I was trying to validate my assumption but I am having trouble. I get an error message about SelectSingleNode:
'System.Xml.XmlNodeList' does not contain a definition for
'SelectSingleNode' and no extension method 'SelectSingleNode'
accepting a first argument of type 'System.Xml.XmlNodeList' could be
found (are you missing a using directive or an assembly reference?)
I have a reference to System.Xml and also "using System.Xml". Am I missing something else?
The SelectNodes method returns an XmlNodeList type. The method SelectSingleNode belongs to the XmlNode class.
You can get your books like this:
var bookA = newBooks.Where(x => x.Attributes["id"].Value == 123);
var bookB = newBooks.Where(x => x.Attributes["id"].Value == 456);
var bookC = newBooks.Where(x => x.Attributes["id"].Value == 789);
var newBooks = xmlDoc.SelectNodes("/books/book[yearPublished=2012]");
This will return a XmlNodeList object. This object does not contain a method called SelectSingleNode, which is why the compiler is telling you that.
You can iterate over nodes in the XmlNodeList returned from the SelectNodes method like so:
foreach (XmlNode node in newBooks)
{
//...
}
As for your performance issue, or perceived performance issue, I suggest you load a file of your desired size and benchmark it. Only then can you tell if you have a performance problem, as you will have measured it.
You can evaluate an XPath expression that has variable references -- see XPathExpression.SetContext() and this example how to implement an IXsltContextVariable.

LINQ to XML - Where clause

I have an XML file with 2 types of information - Locations and job types which is determined by the SLvl value. I wish to bind the SearchTxt value for these to 2 drop downs (one for locations, one for job types) to be used as filters on my page.
The problem is I cant quite get my where clause to filter on the SLvl value. With the where clause in no results are returned. If I remove it the query does return all the text values.
C#
using System.Xml.Linq;
using System.Linq;
.....
// Loading from file
XDocument loaded = XDocument.Load(#"http://[LINKREMOVED]/vacancies.aspx");
// Query the data
var q = (from c in loaded.Descendants("items")
where c.Element("SLvl").ToString() == "0"
select c.Element("SearchTxt").ToString()).Distinct();
// Populate drop down
foreach(string name in q)
{
ddlLocation.Items.Add(new ListItem(name, name));
}
XML:
<VacancyMatch>
<items>
<SearchID>60</SearchID>
<SearchTxt>Scotland</SearchTxt>
<ParentID>0</ParentID>
<SearchCatID>1</SearchCatID>
<SLvl>1</SLvl>
<SubCat>1</SubCat>
</items>
<items>
<SearchID>92</SearchID>
<SearchTxt>Accounting</SearchTxt>
<ParentID>60</ParentID>
<SearchCatID>2</SearchCatID>
<SLvl>2</SLvl>
<SubCat>2</SubCat>
</items>
... More items here
</VacancyMatch>
I guess the problem is that the data is at the same level? Its my first time using LINQ to XML so any help is greatly appriciated.
Note:
XML is provided by a third party so formatting is up to them.
Drop the .ToString() and use .Value property instead:
var values = loaded.Descendants("items")
.Where(i => i.Element("SLvl").Value == "0")
.Select(i => i.Element("SearchTxt").Value)
.Distinct();
Calling ToString() on XElement will return entire node as text. For example, if we change i.Element("SearchTxt").Value in the query above to i.Element("SearchTxt").ToString() it will produce strings such as:
<SearchTxt>Accounting</SearchTxt>
Accessing Value property will extract node's inner text - "Accounting" in this case.
This:
where c.Element("SLvl").ToString() == "0"
should be:
where c.Element("SLvl").Value == "0"
You can't get a value of an element with "ToString()" method, you need to read it's "Value" property instead.
Same goes for any other lines where you are trying to get a value of an element.
Hope it helps.
I notice one problem, you use ToString() on the XElements which is not exactly what you want, I think :), to get the text content of a XElement use the Value property.
http://msdn.microsoft.com/en-us/library/system.xml.linq.xelement.value.aspx

Linq to XML Remove multiple returns

Hi have a Linq Query to extract some information. Following is the part of it.
node = "DocumentClass";
AVariable="Something"
na="NA";
var documentClassesScript = (from documentClass in configparentXML.Descendants(node)
where documentClass.Attribute("Name").Value.Contains(AVariable)
select new ReadingXmlWithLinq
{
CustomStorageString = documentClass.Element("ValidationPluginAssociations") != null ? documentClass.Descendants("ValidationPluginAssociation").Attributes("CustomStorageString").Single().Value : na,
}
).Distinct();
In Some case i got following error
Sequence contains more than one
element
The reason is ValidationPluginAssociations contain more than one ValidationPluginAssociation. I need to distinct and get only one of them. Is there any way to get it.
if there is no need to have single object you can use First:
documentClass.Descendants("ValidationPluginAssociation")
.Attributes("CustomStorageString").First().Value

Categories

Resources