Tuesday, August 18, 2009

Turn your actions into feeds on ASP.net MVC

With this implementation you can re-use parts of your content Controller (the database query for example) and render it on a RSS and/or ATOM feed.

Let's say you have an action like this (extracted from DotNetBurner’s source):

       
       [AcceptVerbs (HttpVerbs.Get | HttpVerbs.Head), CompressFilter]
       public ActionResult Upcoming (int? page, string sort)
       {
           var viewData = GetStoryListViewData<StoryListByCategoryData> (page);
           int sortDays = GetSortDays (sort);

           viewData.Stories = Repository.GetUpcomingStories (CurrentUserId, sortDays, SiteSettings.Settings.UpcomingMaxDays,
                                                             CalculateStartIndex (page), StorySettings.Settings.StoryPerPage);

           viewData.Category = Strings.Upcoming;
           viewData.CategoryDisplayName = Strings.Upcoming;
           viewData.SortDays = sortDays;

           return View ("Upcoming", viewData);
       }

This action returns the upcoming stories. To turn it on a feed, i have just implement this new action:

   [AcceptVerbs (HttpVerbs.Get | HttpVerbs.Head), CompressFilter, OutputCache (CacheProfile = "Feeds")]
       public ActionResult UpcomingFeed(string sort, string feedType, int? feedCount)
       {
           return Content(Feed("Upcoming", null, sort, feedType, feedCount), "application/xml", Encoding.UTF8);
       }


    string Feed(string feed, int? id, string sort, string feedType, int? feedCount)
       {
           int sortDays = GetSortDays(sort);

           if (!id.HasValue)
               id = 0;

           if (!feedCount.HasValue || feedCount == 0)
               feedCount = StorySettings.Settings.StoryPerFeed;

           if (feedCount > 200)
               feedCount = 200;

           IEnumerable<Story> stories = null;

           var categories = Repository.GetCategories();

           stories = Repository.GetUpcomingStories (CurrentUserId, sortDays, SiteSettings.Settings.UpcomingMaxDays, 1, (int)feedCount);

           FeedType type =  ("rss".Equals(feedType, StringComparison.InvariantCultureIgnoreCase)) ? FeedType.RSS20 : FeedType.ATOM10;

           using (var memoryStream = new MemoryStream ())
           {
               FeedHelper.GenerateFeed (stories, categories, memoryStream, type, Url);

               return Encoding.UTF8.GetString (memoryStream.ToArray ());
           }
       }

The Feed method is simplified here for the sake of the example, but it can take a “feed” parameter to return different feeds (for categories, tags, etc.). To generate the feed, i have implemented a FeedHelper class (you probably will have to tune it for your needs):

   public enum FeedType
   {
       RSS20,
       ATOM10
   }

   public static class FeedHelper
   {
       public static void GenerateFeed(IEnumerable<Story> stories,
                                       IEnumerable<Category> categories,
                                       Stream output,
                                       FeedType type,
                                       UrlHelper urlHelper)
       {

           // Create an XmlWriter to write the feed into it
           using (XmlWriter writer = XmlWriter.Create(output))
           {
               // Set the feed properties
               SyndicationFeed feed = new SyndicationFeed
                   (SiteSettings.Settings.Name,
                    SiteSettings.Settings.Title,
                    new Uri(SiteSettings.Settings.Address));

               feed.LastUpdatedTime = DateTime.UtcNow;
               feed.Language = SiteSettings.Settings.Language;

               feed.Authors.Add(new SyndicationPerson(SiteSettings.Settings.AdminMail,
                                                      SiteSettings.Settings.Name,
                                                      SiteSettings.Settings.Address));

               // Add categories
               foreach (var category in categories)
                   foreach(var subcat in category.SubCategories)
                       feed.Categories.Add(new SyndicationCategory(subcat.Name));

               // Set generator
               feed.Generator = SiteSettings.Settings.Title;

               // Set language
               feed.Language = SiteSettings.Settings.Language;
               string siteUrl = SiteSettings.Settings.Address;


               // Add post items
               List<SyndicationItem> items = new List<syndicationitem>();

               foreach (var story in stories)
               {
                   string url = String.Concat(siteUrl,
                                              urlHelper.RouteUrl("Detail",
                                                                 new RouteValueDictionary
                                                                     {
                                                                         {"id", story.StoryId},
                                                                         {"title", story.Title.ConvertToUrlPath()}
                                                                     }));

                   string voteButton = "{0}<br>".FormatWith(SwissKnife.GetVoteButtonFor(story.Url));

                   TextSyndicationContent content =
                       SyndicationContent.CreateHtmlContent(String.Concat("<div><p>", story.Description, "</p>",
                                                                          voteButton, "</div>"));

                   SyndicationItem item = new SyndicationItem(story.Title,
                                                              content,
                                                              new Uri(url),
                                                              url,
                                                              new DateTimeOffset(story.PostedOn));
                   item.PublishDate = story.PostedOn;
                   item.Categories.Add(new SyndicationCategory(story.CategoryName));

                   items.Add(item);
               }

               feed.Items = items;

               // Write the feed to output
               if (type == FeedType.RSS20)
               {
                   Rss20FeedFormatter rssFormatter = new Rss20FeedFormatter(feed);
                   rssFormatter.WriteTo(writer);
               }
               else
               {
                   Atom10FeedFormatter atomFormatter = new Atom10FeedFormatter(feed);
                   atomFormatter.WriteTo(writer);
               }

               writer.Flush();
           }
       }
   }

This uses System.ServiceModel.Syndication to create a feed from the same database query that is used to render the site content. With this, i have enabled DotNetBurner to have feeds in less than 4 hours.

Drop me a comment if you implement this on a similar time :)

2 comments:

  1. ToUpper is an anti-pattern. You should be using "rss".Equals(feedType, StringComparison.InvariantCultureIgnoreCase)

    ReplyDelete