Friday, September 17, 2010

Note on Using LINQ to Query an Atom Feed

More generally, this note is about using LINQ to query XML with one or more namespaces. I ran into the problem of querying XML with one or more namespaces when querying data from Windows Live using the Messenger Connect APIs (this post describes what I was doing). I scratched my head for a while wondering why my LINQ queries didn’t return anything. The XML in question in this example was an Atom feed from a Windows Live endpoint that returns contact information.

In a nutshell, you have to pay attention to what namespaces are defined. My problem was I forgot to consider the default namespace. These two topics on MSDN explain it in more detail: Working with XML Namespaces and Scope of Default Namespaces in C#.

To illustrate the problem precisely, suppose you have the following XML:

<contacts>
<contact>
<name>name</name>
</contact>
</contacts>
XML Listing 1

Then with this C# code you can get the name of the contact.

XDocument xdoc = XDocument.Load(MapPath("~/App_Data/XMLFile.xml"));
var nameQuery1 = xdoc.Descendants("contact");
foreach (XElement nameElement in nameQuery1)
{
Response.Write(nameElement.Element("name").Value);
}
Code Listing 1

Now suppose the XML is defined with a default namespace such as this:

<contacts xmlns="http://travelmarx.blogspot.com">
<contact>
<name>name</name>
</contact>
</contacts>
XML Listing 2

In this case Code Listing 1 would not return the name element because it doesn’t take in account the default namespace. The following code will work:

XNamespace ns = "http://travelmarx.blogspot.com";
var nameQuery2 = xdoc.Descendants(ns + "contact");
foreach (XElement nameElement in nameQuery2)
{
Response.Write(nameElement.Element(ns + "name").Value);
}
Code Listing 2

Now, suppose there is an additional namespace declared with a prefix defined such as this:

<contacts xmlns="http://travelmarx.blogspot.com" xmlns:an="http://anothernamespace">
<contact>
<name>name</name>
<an:note>note</an:note>
</contact>
</contacts>
XML Listing 3

Possible C# code to get the note element is:

XNamespace ns2 = "http://anothernamespace";
var nameQuery3 = xdoc.Descendants(ns + "contact");
foreach (XElement nameElement in nameQuery3)
{
Response.Write(nameElement.Element(ns2 + "note").Value);
}
Code Listing 3

The code examples above are just for illustration purposes. You should make the code more robust, e.g. using try/catch clauses, for your particular application.

The following XML is an excerpt from the Messenger Connect AllContacts ATOM feed. It is followed by code to read it. It runs in the context of a web page, but the code can be easily taken out and used in other scenarios.


<?xml version="1.0" encoding="utf-8"?>
<feed xml:base="http://bay.apis.live.net/V4.1/cid-3333333333333333/" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns="http://www.w3.org/2005/Atom">
<title type="text">AllContacts</title>
<id>uuid:1623d873-0cc4-45ac-bea0-8c09fad60370;id=9850</id>
<updated>2010-09-13T20:57:55Z</updated>
<link rel="self" type="application/atom+xml;type=feed" title="self" href="Contacts/AllContacts"/>
<link rel="edit" type="application/atom+xml;type=feed" title="edit" href="Contacts/AllContacts"/>
<entry p3:reserved=" " p4:etag="0001-01-01T00:00:00.0000000" xmlns:p4="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns:p3="http://schemas.microsoft.com/ado/2007/08/dataservices">
<id>urn:uuid:F2E3AWHYXZYUVHICHHNFXIXKHQ</id>
<title type="text">Cosentino Luparello</title>
<updated>2010-08-05T19:11:17Z</updated>
<link rel="self" type="application/atom+xml;type=entry" title="self" href="Contacts/AllContacts/F2E3AWHYXZYUVHICHHNFXIXKHQ"/>
<link rel="edit" type="application/atom+xml;type=entry" title="edit" href="Contacts/AllContacts/F2E3AWHYXZYUVHICHHNFXIXKHQ"/>
<category term="Contact" label="Contact" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>
<content type="application/xml">
<p4:properties xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<p3:Emails>
<p3:element>
<p3:Type>None</p3:Type>
</p3:element>
</p3:Emails>
<p3:FirstName>Cosentino</p3:FirstName>
<p3:FormattedName>Cosentino Luparello</p3:FormattedName>
<p3:LastName>Luparello</p3:LastName>
<p3:Locations>
<p3:element>
<p3:City>Palermo</p3:City>
<p3:Type>None</p3:Type>
</p3:element>
</p3:Locations>
<p3:PhoneNumbers/>
<p3:Urls/>
</p4:properties>
</content>
</entry>
</feed>
XML Listing 4


<%@ Page Language="C#" AutoEventWireup="true" %>

<%@ Import Namespace="System" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Linq" %>
<%@ Import Namespace="System.Web" %>
<%@ Import Namespace="System.Web.UI" %>
<%@ Import Namespace="System.Xml.Linq" %>
<script runat="server">
public class ContactName
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
protected void Page_Load(object sender, EventArgs e)
{
// Atom namespace.
XNamespace ns = "http://www.w3.org/2005/Atom";
// Dataservices namespace, p3.
XNamespace nsp3 = "http://schemas.microsoft.com/ado/2007/08/dataservices";
// Dataservices metadata namespace, p4.
XNamespace nsp4 = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";

XDocument xdoc = XDocument.Load(MapPath("~/App_Data/data.xml"));

Output("Query 1: Selecting content node and then iterating on it to find FirstName and LastName.");
// Select the content nodes directly.
var contentQuery = xdoc.Descendants(ns + "content");
// Iterate over the collection of content nodes.
foreach (XElement contentElement in contentQuery)
{
string firstName = contentElement.Descendants(nsp3 + "FirstName").FirstOrDefault().Value;
string lastName = contentElement.Descendants(nsp3 + "LastName").FirstOrDefault().Value;
Output(firstName + " " + lastName);
}

Output();
Output("Query 2: Selecting FirstName and LastName as string.");
var firstNameQuery = xdoc.Descendants(nsp3 + "FirstName").Select(entry => entry.Value + " " + entry.ElementsAfterSelf(nsp3 + "LastName").First().Value);
foreach (string firstNameEntry in firstNameQuery)
{
Output(firstNameEntry);
}

Output();
Output("Query 3: Selecting FirstName and LastName as ContactName object.");
var contactNameQuery = xdoc.Descendants(nsp4 + "properties").Select(entry => new ContactName
{
FirstName = entry.Element(nsp3 + "FirstName").Value,
LastName = entry.Element(nsp3 + "LastName").Value
});
foreach (var contactNameEntry in contactNameQuery)
{
Output(contactNameEntry.FirstName + " " + contactNameEntry.LastName);
}

Output();
Output("Query 4: Query 3 + filtering results.");
var contactNameFilteredQuery = xdoc.Descendants(nsp4 + "properties")
.Where(entry => entry.Element(nsp3 + "FirstName").Value.ToLower().StartsWith("c"))
.Select(entry => new ContactName
{
FirstName = entry.Element(nsp3 + "FirstName").Value,
LastName = entry.Element(nsp3 + "LastName").Value
});
foreach (var contactNameFilteredEntry in contactNameFilteredQuery)
{
Output(contactNameFilteredEntry.FirstName + " " + contactNameFilteredEntry.LastName);
}
}
protected void Output()
{
Output("");
}
protected void Output(string txt)
{
Label1.Text += txt + "<br/>";
}
</script>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>Reading Windows Live Contact Atom Feed</title>
</head>
<body>
<form id="form1" runat="server">
<div>
Using LINQ extension methods and lambda expressions.<br />
<asp:Label ID="Label1" runat='server'></asp:Label>
</div>
</form>
</body>
</html>
Code Listing 4

1 comment:

  1. I've been Googling and trying to understand how parsing with a namespace works for over 2 hours but failed, thanks for this great article, I've totally understood how it works now.

    ReplyDelete

All comments go through a moderation process. Even though it may not look like the comment was accepted, it probably was. Check back in a day if you asked a question. Thanks!