Archive for the ‘ASP.NET’ Category.

Aggregating RSS Feeds in C# and ASP.NET MVC 3

I’m working on a Windows Phone project that requires me to surface up multiple RSS feeds as a single source. I needed a way to do this quickly and easily, and with a little help from friends on Twitter (particularly a suggestion from @bertcraven) I found a nice way to accomplish this using the SyndicationFeed in System.ServiceModel.Syndication.

I’ve detailed the steps below, but if you want to get to the heart of it then here’s the code to get this working:

SyndicationFeed mainFeed = new SyndicationFeed();
List<string> feeds = new List<string>();

feeds.Add("http://feeds2.feedburner.com/WadeWegner");
feeds.Add("http://www.nickharris.net/feed/");
feeds.Add("http://feeds.feedburner.com/ntotten");
feeds.Add("http://michaelwasham.com/feed/");
feeds.Add("http://blogs.msdn.com/b/hpctrekker/rss.aspx");

foreach (var feed in GetRssFeeds())
{
    Uri feedUri = new Uri(feed);
    SyndicationFeed syndicationFeed;
    using (XmlReader reader = XmlReader.Create(feedUri.AbsoluteUri))
    {
        syndicationFeed = SyndicationFeed.Load(reader);
    }

    syndicationFeed.Id = feed;

    SyndicationFeed tempFeed = new SyndicationFeed(
        mainFeed.Items.Union(syndicationFeed.Items).OrderByDescending(u => u.PublishDate));
    mainFeed = tempFeed;
}

It’s really quite simple – once you know how to do it!

As you iterate through the list of feeds we use LINQ to union the feeds together – in the end this produces a main feed that has all the contents. Along the way we sort the elements in a descending order based on the PublishDate – otherwise you’ll just get blocks from each of the feeds and nothing is sorted according to the date publish. Once this is done you end up with a main feed that you can use.

For me I wanted to create a service that published the aggregated feed – I chose to use ASP.NET MVC 3 for this new feed. Here are steps you can follow in order to get this working in ASP.NET MVC 3.

  1. Create a new ASP.NET MVC 3 Web Application. I’ve called mine RssFeed
    NewProject
  2. Choose an Internet Application using the Razor view engine and HTML5 semantic markup.
  3. Add System.ServiceModel as a reference in the application. We’ll use this with SyndicationFeed.
  4. Create an empty controller. I’ve called mine the RssController
    RssFeed
  5. We’re going to define our own ActionResult implementation that can emit RSS by deriving from ActionResult. Inspiration and original source comes from this post on Developer Zen.
    public class RssActionResult : ActionResult
    {
        public SyndicationFeed Feed { get; set; }
    
        public override void ExecuteResult(ControllerContext context)
        {
            context.HttpContext.Response.ContentType = "application/rss+xml";
    
            Rss20FeedFormatter rssFormatter = new Rss20FeedFormatter(Feed);
            using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.Output))
            {
                rssFormatter.WriteTo(writer);
            }
        }
    }
  6. We can now update the Index method to use the RssActionResult instead of the default ActionResult implementation.
    public RssActionResult Index()
    {
        return new RssActionResult();
    }
  7. Define a method that returns all the feeds with which you want to aggregate. You can pull from many different places – I recommend SQL Azure – but for the purposes of this demo you can just use a generic list of strings.
    private static List<string> GetRssFeeds()
    {
        List<string> feeds = new List<string>();
    
        feeds.Add("http://feeds2.feedburner.com/WadeWegner");
        feeds.Add("http://www.nickharris.net/feed/");
        feeds.Add("http://feeds.feedburner.com/ntotten");
    
        return feeds;
    }
  8. Now we can update our Index method to iterate through the feeds and aggregate them into a single SyndicationFeed that is sorted (descending) by the publish date.
    public RssActionResult Index()
    {
        SyndicationFeed mainFeed = new SyndicationFeed();
    
        foreach (var feed in GetRssFeeds())
        {
            Uri feedUri = new Uri(feed);
            SyndicationFeed syndicationFeed;
            using (XmlReader reader = XmlReader.Create(feedUri.AbsoluteUri))
            {
                syndicationFeed = SyndicationFeed.Load(reader);
            }
    
            syndicationFeed.Id = feed;
    
            SyndicationFeed tempFeed = new SyndicationFeed(
                mainFeed.Items.Union(syndicationFeed.Items).OrderByDescending(u => u.PublishDate));
            mainFeed = tempFeed;
        }
    
        return new RssActionResult() { Feed = mainFeed };
    }
  9. Now, hit F5 and run. Browse to http://localhost:<port>/rss to see the aggregated RSS feed. 
    RssFeed

And that’s it!

There’s certainly more you can do with this – in fact, given the cost it takes to aggregate a large number of feeds, I’ve started to take the aggregated feed and store it in Windows Azure blob storage attached to the Content Delivery Network (CDN). The code to do this is similar to the following:

StringBuilder builder = new StringBuilder();
using (XmlWriter writer = XmlWriter.Create(builder))
{
    mainFeed.SaveAsRss20(writer);
    string rssFeed = builder.ToString();

}
// write to Windows Azure blob storage
You might consider doing something similar.
I hope this helps!

Using ELMAH in Windows Azure with Table Storage

In this week’s episode of Cloud Cover, Steve and I covered Logging, Tracing, and ELMAH in Windows Azure. Steve explored the first two topics while I looked into ELMAH in Windows Azure. You should make sure and take a look at his posts – they’re useful:

ELMAH (Error Logging Modules and Handlers) itself is extremely useful, and with a few simple modifications can provide a very effective way to handle application-wide error logging for your ASP.NET web applications. If you aren’t already familiar with ELMAH, take a look at the ELMAH project page.

NuGet

Before going any further, I thought I’d let you know that I’ve created a NuGet package that makes this extremely easy to try. You can take a look at ELMAH with Windows Azure Table Storage on the NuGet gallery or immediately try this out with the following command:

       Install-Package WindowsAzure.ELMAH.Tables

By default this NuGet package is configured to use the local storage emulator. If you want to use your actual Windows Azure storage account you can uncomment the following line in the Web.Config file:

<!--
<errorLog
    type="WebRole1.TableErrorLog, WebRole1"
    connectionString="DefaultEndpointsProtocol=https;AccountName=YOURSTORAGEACCOUNT;
      AccountKey=YOURSTORAGEKEY" />
-->

Incidentally, if you like NuGet, then you should check out Cory Fowler’s post on must have NuGet packages for Windows Azure development.

Demo

For those of you unfamiliar with ELMAH, I put together a simple demo. You can try it out on http://elmahdemo.cloudapp.net/. Just enter a message (keep it clean, please!) and throw an exception.

ELMAHDemo

Click the ELMAH button to then load the handler. You’ll see all the errors logged with a lot of great detail.

ELMAHHandler

The great part is that these files are getting serialized into Windows Azure table storage. The benefit of this is you can read them from anywhere – in fact, you don’t have to even deploy the elmah.axd handler with your web application! You could run it locally.

Here’s what the files look like in table storage:

ELMAHInTables

How Does it Work?

The nice part is you can easily grab the NuGet package to view all the source code. There are two items of interest: ErrorEntity.cs and Web.Config.

In ErrorEntity.cs we first create our ErrorEntity:

public class ErrorEntity : TableServiceEntity
{
    public string SerializedError { get; set; }

    public ErrorEntity() { }
    public ErrorEntity(Error error)
        : base(string.Empty, (DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks).ToString("d19"))
    {
        this.SerializedError = ErrorXml.EncodeString(error);
    }
}

Then we implement the ErrorLog abstract class from ELMAH to create a TableErrorLog class with all the implementation details.

public class TableErrorLog : ErrorLog
{
    private string connectionString;

    public override ErrorLogEntry GetError(string id)
    {
        return new ErrorLogEntry(this, id, ErrorXml.DecodeString(CloudStorageAccount.Parse(
            connectionString).CreateCloudTableClient().GetDataServiceContext()
            .CreateQuery<ErrorEntity>("elmaherrors").Where(e => e.PartitionKey == string.Empty
                && e.RowKey == id).Single().SerializedError));
    }

    public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
    {
        var count = 0;
        foreach (var error in CloudStorageAccount.Parse(connectionString).
            CreateCloudTableClient().GetDataServiceContext()
            .CreateQuery<ErrorEntity>("elmaherrors")
            .Where(e => e.PartitionKey == string.Empty).AsTableServiceQuery()
            .Take((pageIndex + 1) * pageSize).ToList().Skip(pageIndex * pageSize))
        {
            errorEntryList.Add(new ErrorLogEntry(this, error.RowKey,
                ErrorXml.DecodeString(error.SerializedError)));
            count += 1;
        }
        return count;
    }

    public override string Log(Error error)
    {
        var entity = new ErrorEntity(error);
        var context = CloudStorageAccount.Parse(connectionString)
            .CreateCloudTableClient().GetDataServiceContext();
        context.AddObject("elmaherrors", entity);
        context.SaveChangesWithRetries();
        return entity.RowKey;
    }

    public TableErrorLog(IDictionary config)
    {
        connectionString = (string)config["connectionString"] ?? RoleEnvironment
            .GetConfigurationSettingValue((string)config["connectionStringName"]);
        Initialize();
    }

    public TableErrorLog(string connectionString)
    {
        this.connectionString = connectionString;
        Initialize();
    }

    void Initialize()
    {
        CloudStorageAccount.Parse(connectionString).CreateCloudTableClient()
            .CreateTableIfNotExist("elmaherrors");
    }
}

Now, to leverage these assets, we update the Web.Config file to include an <elmah> … </elmah> section that specifies our custom error log (and also allows remote access to the handler:

<elmah>
  <security allowRemoteAccess="yes" />
  <errorLog type="WebRole1.TableErrorLog, WebRole1"
            connectionString="UseDevelopmentStorage=true" />
</elmah>

That’s all there’s to it!

Of course, there are many other ways you could define your ErrorEntity and implement the TableErrorLog (i.e. you could extract more details into additional entities within your table), but this way is pretty effective.

I hope this helps!

Leveraging WMI in an Azure Web Role