Creating an RSS feed with XML literals

As I promised, I will now give an example of how to use XML literals in Visual Basic 9 to create an RSS feed.

RSS feeds are an example where XML literals are ideally suited for the task. RSS feeds are commonly automatically generated, and instead of having to deal with XmlWriter or XSLT or something similar we can create it directly in VB with minimal effort. This is not a made-up example; the RSS feed for ookii.org is currently generated using the XmlWriter approach similar to the first example in the previous post. When .Net 3.5 is released and my host installs it on the server, I will replace that code with what you see in this post.

We will use a generic RssFeed class to generate the XML from. This also has the advantage that if you have multiple different data sources you want to generate an RSS feed for you can reuse this code. All you need to do is fill an RssFeed class with the appropriate data (for which LINQ is also ideally suited).

For brevity, I will not list the full source of the RssFeed class and its associated RssItem and RssCategory classes here. Suffice it to say they are classes that contain properties for things such as the title of a feed or the text of an item. RssFeed has a collection of RssItems and RssItem has a collection of RssCategories. If you want to see the definitions check out the full source of the example.

The first thing we need to take care of is XML namespaces. RSS itself doesn’t use a namespace, but we’ll be using some extensions that do. We’ll be using these namespaces in multiple places in the VB source and it’d be nice if we don’t have to repeat the namespace URI every time. Fortunately, VB allows us to import XML namespaces in much the same way as regular .Net namespace so we can use them in any XML literal in the file:

Imports <xmlns:dc="http://purl.org/dc/elements/1.1/">
Imports <xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
Imports <xmlns:wfw="http://wellformedweb.org/CommentAPI/">

Before we get started on the heavy work, we have one more thing to do. If we want this to be applicable generically we must realize that some items do not apply to all feeds. For instance I will be using the <slash:comments /> element which gives the number of comments for an item. Not all types of items can have comments so that element doesn’t always apply. Although we could put the code to omit these elements directly in the XML embedded expressions this doesn’t aid readability, so I’ve opted to create functions for them. Here we make use of the fact that if an embedded expression returns Nothing, it’s ignored.

Private Function CreateCommentCountElement(ByVal commentCount As Integer?) As XElement
    If commentCount Is Nothing Then
        Return Nothing
    Else
        Return <slash:comments><%= commentCount %></slash:comments>
    End If
End Function

Private Function CreateCommentsElement(ByVal commentLink As String) As XElement
    If commentLink Is Nothing Then
        Return Nothing
    Else
        Return <comments><%= commentLink %></comments>
    End If
End Function

Private Shared Function CreateCommentRssElement(ByVal commentRssUrl As String) As XElement
    If commentRssUrl Is Nothing Then
        Return Nothing
    Else
        Return <wfw:commentRss><%= commentRssUrl %></wfw:commentRss>
    End If
End Function

Private Function CreateCategories(ByVal categories As IEnumerable(Of RssCategory)) As IEnumerable(Of XElement)
    If categories Is Nothing Then
        Return Nothing
    Else
        Return From category In categories _
               Select <category domain=<%= category.Domain %>><%= category.Name %></category>
    End If
End Function

Now we can finally get to the meat of this sample, generating the RSS feed, which is exceedingly simple:

Public Function CreateXml() As XDocument
    Dim itemElements = From item In Items _
                       Select <item>
                                  <title><%= item.Title %></title>
                                  <link><%= item.Link %></link>
                                  <guid isPermaLink=<%= item.GuidIsPermalink.ToString().ToLowerInvariant() %>><%= item.Guid %></guid>
                                  <pubDate><%= item.PubDate.ToString("r") %></pubDate>
                                  <dc:creator><%= item.Creator %></dc:creator>
                                  <%= CreateCommentCountElement(item.CommentCount) %>
                                  <%= CreateCommentsElement(item.CommentsLink) %>
                                  <%= CreateCommentRssElement(item.CommentRssUrl) %>
                                  <description><%= New XCData(item.Description) %></description>
                                  <%= CreateCategories(item.Categories) %>
                              </item>

    Return <?xml version="1.0" encoding="utf-8"?>
           <rss version="2.0">
               <channel>
                   <title><%= Title %></title>
                   <link><%= Link %></link>
                   <dc:language><%= Language %></dc:language>
                   <%= itemElements %>
               </channel>
           </rss>
End Function

I do it in two steps, first the items and then the main feed, but it could easily be done in one, I just find this more readable. One thing to note is the way I create a CDATA section. I do it this way because you can't put embedded expresssions inside a CDATA section, as the embedded expression syntax is valid content for a CDATA section. Is that really all there is to it? Yes! It’s that simple.

But wait, there’s more. Remember last time I mentioned how you can also query existing XML documents. This means we can also easily load any existing RSS feed into the RssFeed class:

Public Shared Function FromXml(ByVal feed As XDocument) As RssFeed
    If feed Is Nothing Then
        Throw New ArgumentNullException("feed")
    End If

    Dim result = From channel In feed.<rss>.<channel> _
                 Select New RssFeed() With _
                     { _
                         .Title = channel.<title>.Value, _
                         .Link = channel.<link>.Value, _
                         .Language = channel.<dc:language>.Value, _
                         .Items = From item In channel.<item> _
                                  Select New RssItem() With _
                                     { _
                                          .Title = item.<title>.Value, _
                                          .Link = item.<link>.Value, _
                                          .Guid = item.<guid>.Value, _
                                          .GuidIsPermalink = (item.<guid>.@isPermaLink = "true"), _
                                          .PubDate = Date.Parse(item.<pubDate>.Value), _
                                          .CommentCount = CType(item.<slash:comments>.Value, Integer?), _
                                          .CommentsLink = item.<comments>.Value, _
                                          .CommentRssUrl = item.<wfw:commentRss>.Value, _
                                          .Description = item.<description>.Value, _
                                          .Categories = From category In item.<category> _
                                                        Select New RssCategory() With _
                                                           { _
                                                                .Name = category.Value, _
                                                                .Domain = category.@domain _
                                                           } _
                                      } _
                     }

    Return result.First()
End Function

Here we can also see the nice new object initializers at work. Imagine if you will how much work this would’ve been with XmlReader, and how much harder to read that code would’ve been. And in case you’re wondering if this won’t crash if a feed omits one of the optional elements, it won’t: if a feed omits e.g. <slash:comments>, in that case the item.<slash:comments> query will return an empty list, and the Value property will return Nothing, no exceptions will be thrown.

The full source of the example is available here.

This article was written for Visual Studio 2008 Beta 2. Some of it may not apply to other versions.

Categories: Programming
Posted on: 2007-10-21 10:15 UTC.

Comments

Frederic Norro

2008-12-28 07:44 UTC

Hoi Sven,
Schitterend, dit is exact wat ik zocht!
Om m'n asp.net 3.5 sites met RSS uit te breiden dacht ik meteen in de richting van XML literals, en toen ik "create rss feed xml literals" ingaf in Google was jouw site dan ook de allereerste link! :-)

Ik zie echter dat je op jouw site ook een Atom feed aanbiedt. Biedt dat voordelen, en heb je daar dan ook een class voor geschreven?

Thanks,
Frédéric

Add comment

Latest posts

Categories

Archive

Syndication

RSS Subscribe