Create RSS and Atom feeds using custom ASP.Net MVC Action Results and the Microsoft Syndication classes
There are many ways to create RSS and Atom feeds, in this post I’m going to show one way of creating a re-usable solution that utilises the Syndication classes from the System.ServiceModel.Web assembly.
To make good use of the ASP.Net MVC framework I am going to show a nice way of creating custom action result methods to return the feeds without the need for creating any views.
As this is quite a detailed overview with a fair amount of code – you can download a working solution here.
The first thing to do is create the custom ActionResult methods to return our feeds.
You will need to add a project reference to the System.ServiceModel.Web assembly.
RssActionResult
- using System.ServiceModel.Syndication;
- using System.Web.Mvc;
- using System.Xml;
- namespace SyndicationTest
- {
- /// <summary>
- /// Returns an RSS feed to the resonse stream.
- /// </summary>
- public class RssActionResult : ActionResult
- {
- SyndicationFeed feed;
- /// <summary>
- /// Default constructor.
- /// </summary>
- public RssActionResult() { }
- /// <summary>
- /// Constructor to set up the action result feed.
- /// </summary>
- /// <param name="feed">Accepts a <see cref="SyndicationFeed"/>.</param>
- public RssActionResult(SyndicationFeed feed)
- {
- this.feed = feed;
- }
- /// <summary>
- /// Executes the call to the ActionResult method and returns the created feed to the output response.
- /// </summary>
- /// <param name="context">Accepts the current <see cref="ControllerContext"/>.</param>
- public override void ExecuteResult(ControllerContext context)
- {
- context.HttpContext.Response.ContentType = "application/rss+xml";
- Rss20FeedFormatter formatter = new Rss20FeedFormatter(this.feed);
- using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.Output))
- {
- formatter.WriteTo(writer);
- }
- }
- }
- }
AtomActionResult
- using System.ServiceModel.Syndication;
- using System.Web.Mvc;
- using System.Xml;
- namespace SyndicationTest
- {
- /// <summary>
- /// Returns an Atom feed to the resonse stream.
- /// </summary>
- public class AtomActionResult : ActionResult
- {
- SyndicationFeed feed;
- /// <summary>
- /// Default constructor.
- /// </summary>
- public AtomActionResult() { }
- /// <summary>
- /// Constructor to set up the action result feed.
- /// </summary>
- /// <param name="feed">Accepts a <see cref="SyndicationFeed"/>.</param>
- public AtomActionResult(SyndicationFeed feed)
- {
- this.feed = feed;
- }
- /// <summary>
- /// Executes the call to the ActionResult method and returns the created feed to the output response.
- /// </summary>
- /// <param name="context">Accepts the current <see cref="ControllerContext"/>.</param>
- public override void ExecuteResult(ControllerContext context)
- {
- context.HttpContext.Response.ContentType = "application/atom+xml";
- Atom10FeedFormatter formatter = new Atom10FeedFormatter(this.feed);
- using (XmlWriter writer = XmlWriter.Create(context.HttpContext.Response.Output))
- {
- formatter.WriteTo(writer);
- }
- }
- }
- }
Now that the action result methods have been created, they can be called from your views using the same syntax as any other action result method.
Now assuming that you will be returning a list of items from your repository that you will want mapped into a syndication feed, we need to create a class that will hold property mappings from your list of custom types to the internal properties of the syndication helper class that we will be creating shortly.
This mapping class will be used simply for storing delegate properties used for mappings, this will allow for easy, strongly typed mapping.
SyndicationFeedItemMapper
- using System;
- namespace SyndicationTest
- {
- public class SyndicationFeedItemMapper<TFeedItem> where TFeedItem : class
- {
- Func<TFeedItem, string> title;
- Func<TFeedItem, string> content;
- Func<TFeedItem, string> controller;
- Func<TFeedItem, string> action;
- Func<TFeedItem, string> id;
- Func<TFeedItem, DateTimeOffset> datePublished;
- string controllerString;
- string actionString;
- public SyndicationFeedItemMapper
- (
- Func<TFeedItem, string> title,
- Func<TFeedItem, string> content,
- string controller,
- string action,
- Func<TFeedItem, string> id,
- Func<TFeedItem, DateTimeOffset> datePublished
- ) : this(title, content, id, datePublished)
- {
- this.controllerString = controller;
- this.actionString = action;
- }
- public SyndicationFeedItemMapper
- (
- Func<TFeedItem, string> title,
- Func<TFeedItem, string> content,
- Func<TFeedItem, string> controller,
- Func<TFeedItem, string> action,
- Func<TFeedItem, string> id,
- Func<TFeedItem, DateTimeOffset> datePublished
- )
- : this(title, content, id, datePublished)
- {
- this.controller = controller;
- this.action = action;
- }
- protected SyndicationFeedItemMapper
- (
- Func<TFeedItem, string> title,
- Func<TFeedItem, string> content,
- Func<TFeedItem, string> id,
- Func<TFeedItem, DateTimeOffset> datePublished
- )
- {
- this.title = title;
- this.content = content;
- this.id = id;
- this.datePublished = datePublished;
- }
- public Func<TFeedItem, string> Title
- {
- get
- {
- return this.title;
- }
- }
- public Func<TFeedItem, string> Content
- {
- get
- {
- return this.content;
- }
- }
- public Func<TFeedItem, string> Controller
- {
- get
- {
- return this.controller;
- }
- }
- public Func<TFeedItem, string> Action
- {
- get
- {
- return this.action;
- }
- }
- public Func<TFeedItem, string> Id
- {
- get
- {
- return this.id;
- }
- }
- public Func<TFeedItem, DateTimeOffset> DatePublished
- {
- get
- {
- return this.datePublished;
- }
- }
- public string ControllerString
- {
- get
- {
- return this.controllerString;
- }
- }
- public string ActionString
- {
- get
- {
- return this.actionString;
- }
- }
- public Func<TFeedItem, string> AuthorName { get; set; }
- public Func<TFeedItem, string> AuthorEmail { get; set; }
- public Func<TFeedItem, string> AuthorUrl { get; set; }
- }
- }
You will notice that there is also support for allowing the controller and action method to be stored as a string rather than a delegate, this is because you may not want to hold this information in your repository.
To show you an example of how this mapping class is used.
Lets say the items come back from your repository into a list of MyFeedItem:
MyFeedItem
- using System;
- namespace SyndicationTest.Models
- {
- public class MyFeedItem
- {
- public int Id { get; set; }
- public string Title { get; set; }
- public string Description { get; set; }
- public DateTime DateAdded { get; set; }
- public string CreatedBy { get; set; }
- }
- }
These properties can be used to set up the feed mapper for example:
- SyndicationFeedItemMapper<MyFeedItem> mapper = new SyndicationFeedItemMapper<MyFeedItem>
- (
- f => f.Title,
- f => f.Description,
- "Home",
- "Articles",
- f => f.Id.ToString(),
- f => f.DateAdded
- );
Now that the items have been covered, you can create an options class to store information such as the feed title, description and url, there are also numerous optional properties you can set such as feed id, copyright statement, last updated date and language.
SyndicationFeedOptions
- using System;
- namespace SyndicationTest
- {
- public class SyndicationFeedOptions
- {
- string title;
- string description;
- string url;
- public SyndicationFeedOptions(string title, string description, string url)
- {
- this.title = title;
- this.description = description;
- this.url = url;
- }
- public string Title
- {
- get
- {
- return this.title;
- }
- }
- public string Description
- {
- get
- {
- return this.description;
- }
- }
- public string Url
- {
- get
- {
- return this.url;
- }
- }
- public string FeedId { get; set; }
- public DateTimeOffset LastUpdated { get; set; }
- public string Copyright { get; set; }
- public string Language { get; set; }
- }
- }
You can populate the SyndicationFeedOptions class like the following:
- SyndicationFeedOptions options = new SyndicationFeedOptions
- (
- "My Feed Title",
- "My Feed Description",
- "http://mytesturl.com"
- );
Now that all the support classes have been built, it is time to build the main SyndicationFeedHelper class that will take in all the information and return a SyndicationFeed for converting into RSS or Atom.
The are only two public methods in this class, the constructor which sets up the feed options and stores the current controller context, and the GetFeed() method which calls the necessary private methods to create and then return the feed.
The most interesting private methods are probably the CreateSyndicationItem and the GetUrl.
The CreateSyndicationItem accepts an instance of the MyFeedItem class from the passed in list and invokes the delegates stored in the mapper against it. This then extracts the instance values and creates a SyndicationItem.
The GetUrl method accepts an instance of the MyFeedItem class from the passed in list and invokes the necessary delegates in the mapper against it to create valid MVC style urls for each item.
SyndicationFeedHelper
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.ServiceModel.Syndication;
- using System.Web.Mvc;
- using System.Web.Routing;
- namespace SyndicationTest
- {
- /// <summary>
- /// Class to create an syndication feed.
- /// </summary>
- /// <typeparam name="TFeedItem">Accepts the custom feed item type.</typeparam>
- public class SyndicationFeedHelper<TFeedItem> where TFeedItem : class
- {
- const string Id = "id";
- ControllerContext context;
- IList<TFeedItem> feedItems;
- SyndicationFeedItemMapper<TFeedItem> feedItemMapper;
- SyndicationFeedOptions feedOptions;
- SyndicationFeed syndicationFeed;
- IList<SyndicationItem> syndicationItems;
- ControllerContext Context
- {
- get
- {
- return this.context;
- }
- }
- IList<TFeedItem> FeedItems
- {
- get
- {
- return this.feedItems;
- }
- }
- SyndicationFeedItemMapper<TFeedItem> FeedItemMapper
- {
- get
- {
- return this.feedItemMapper;
- }
- }
- SyndicationFeedOptions SyndicationFeedOptions
- {
- get
- {
- return this.feedOptions;
- }
- }
- /// <summary>
- /// Constructs the SyndicationFeedHelper.
- /// </summary>
- /// <param name="context">Accepts the current controller context.</param>
- /// <param name="feedItems">Accepts a list of feed items.</param>
- /// <param name="syndicationFeedItemMapper">Accepts a <see cref="SyndicationFeedItemMapper"/>.</param>
- /// <param name="syndicationFeedOptions">Accepts a <see cref="SyndicationFeedOptions"/>.</param>
- public SyndicationFeedHelper
- (
- ControllerContext context,
- IList<TFeedItem> feedItems,
- SyndicationFeedItemMapper<TFeedItem> syndicationFeedItemMapper,
- SyndicationFeedOptions syndicationFeedOptions
- )
- {
- this.context = context;
- this.feedItems = feedItems;
- this.feedItemMapper = syndicationFeedItemMapper;
- this.feedOptions = syndicationFeedOptions;
- syndicationItems = new List<SyndicationItem>();
- SetUpFeedOptions();
- }
- /// <summary>
- /// Creates and returns a <see cref="SyndicationFeed"/>.
- /// </summary>
- /// <returns><see cref="SyndicationFeed"/></returns>
- public SyndicationFeed GetFeed()
- {
- feedItems.ToList().ForEach(feedItem =>
- {
- SyndicationItem syndicationItem = CreateSyndicationItem(feedItem);
- AddItemAuthor(feedItem, syndicationItem);
- AddPublishDate(feedItem, syndicationItem);
- syndicationItems.Add(syndicationItem);
- });
- syndicationFeed.Items = syndicationItems;
- return syndicationFeed;
- }
- private void SetUpFeedOptions()
- {
- syndicationFeed = new SyndicationFeed
- (
- this.SyndicationFeedOptions.Title,
- this.SyndicationFeedOptions.Description,
- new Uri(this.SyndicationFeedOptions.Url)
- );
- AddFeedId();
- AddCopyrightStatement();
- AddLanguageIsoCode();
- AddLastUpdateDateTime();
- }
- private void AddLastUpdateDateTime()
- {
- if (this.SyndicationFeedOptions.LastUpdated != default(DateTimeOffset))
- {
- syndicationFeed.LastUpdatedTime = this.SyndicationFeedOptions.LastUpdated;
- }
- }
- private void AddLanguageIsoCode()
- {
- if (!string.IsNullOrEmpty(this.SyndicationFeedOptions.Language))
- {
- syndicationFeed.Language = this.SyndicationFeedOptions.Language;
- }
- }
- private void AddCopyrightStatement()
- {
- if (!string.IsNullOrEmpty(this.SyndicationFeedOptions.Copyright))
- {
- syndicationFeed.Copyright = new TextSyndicationContent(this.SyndicationFeedOptions.Copyright);
- }
- }
- private void AddFeedId()
- {
- if (!string.IsNullOrEmpty(this.SyndicationFeedOptions.FeedId))
- {
- syndicationFeed.Id = this.SyndicationFeedOptions.FeedId;
- }
- }
- private void AddPublishDate(TFeedItem feedItem, SyndicationItem syndicationItem)
- {
- var publishDate = this.FeedItemMapper.DatePublished.Invoke(feedItem);
- syndicationItem.PublishDate = publishDate;
- }
- private void AddItemAuthor(TFeedItem feedItem, SyndicationItem syndicationItem)
- {
- var authorEmail = this.FeedItemMapper.AuthorEmail == null ? string.Empty : this.FeedItemMapper.AuthorEmail.Invoke(feedItem);
- var authorName = this.FeedItemMapper.AuthorName == null ? string.Empty : this.FeedItemMapper.AuthorName.Invoke(feedItem);
- var authorUrl = this.FeedItemMapper.AuthorUrl == null ? string.Empty : this.FeedItemMapper.AuthorUrl.Invoke(feedItem);
- SyndicationPerson syndicationPerson = new SyndicationPerson();
- if (string.IsNullOrEmpty(authorName))
- {
- syndicationPerson.Name = authorName;
- }
- if (string.IsNullOrEmpty(authorEmail))
- {
- syndicationPerson.Email = authorEmail;
- }
- if (string.IsNullOrEmpty(authorUrl))
- {
- syndicationPerson.Uri = authorUrl;
- }
- if (!string.IsNullOrEmpty(syndicationPerson.Name) || !string.IsNullOrEmpty(syndicationPerson.Email)
- || !string.IsNullOrEmpty(syndicationPerson.Uri))
- {
- syndicationItem.Authors.Add(syndicationPerson);
- }
- }
- private SyndicationItem CreateSyndicationItem(TFeedItem feedItem)
- {
- SyndicationItem syndicationItem = new SyndicationItem
- (
- this.FeedItemMapper.Title.Invoke(feedItem),
- this.FeedItemMapper.Content.Invoke(feedItem),
- this.GetUrl(feedItem)
- );
- return syndicationItem;
- }
- private Uri GetUrl(TFeedItem feedItem)
- {
- UrlHelper urlHelper = new UrlHelper(this.Context.RequestContext);
- var routeValues = new RouteValueDictionary();
- routeValues.Add(Id, this.FeedItemMapper.Id.Invoke(feedItem));
- var action = this.FeedItemMapper.Action == null ? this.FeedItemMapper.ActionString
- : this.FeedItemMapper.Action.Invoke(feedItem);
- var controller = this.FeedItemMapper.Controller == null ? this.FeedItemMapper.ControllerString
- : this.FeedItemMapper.Controller.Invoke(feedItem);
- var url = urlHelper.Action
- (
- action,
- controller,
- routeValues
- );
- Uri uriPath = this.Context.RequestContext.HttpContext.Request.Url;
- var urlString = string.Format("{0}://{1}{2}",uriPath.Scheme, uriPath.Authority, url);
- return new Uri(urlString);
- }
- }
- }
At this point you have a fully functional, re-usable solution for displaying RSS and Atom feeds on your site.
You can now create some action results on your controller to return your feeds.
Here is an example of the Home controller on the downloadable example:
- using System.Collections.Generic;
- using System.Web.Mvc;
- using SyndicationTest.Models;
- using SyndicationTest.Repositories;
- namespace SyndicationTest.Controllers
- {
- [HandleError]
- public class HomeController : Controller
- {
- private FakeRepository fakeRepository = new FakeRepository();
- /// <summary>
- /// Index Action Result.
- /// </summary>
- /// <returns></returns>
- public ActionResult Index()
- {
- return View();
- }
- /// <summary>
- /// Returns an RSS Feed
- /// </summary>
- /// <param name="id">Accepts an int id.</param>
- /// <returns></returns>
- public RssActionResult GetRssFeed(int id)
- {
- SyndicationFeedItemMapper<MyFeedItem> mapper = SetUpFeedMapper();
- SyndicationFeedOptions options = SetUpFeedOptions();
- IList<MyFeedItem> feedItems = this.fakeRepository.GetLatestFeedItems(id);
- SyndicationFeedHelper<MyFeedItem> feedHelper = SetUpFeedHelper(mapper, options, feedItems);
- return new RssActionResult(feedHelper.GetFeed());
- }
- /// <summary>
- /// Returns an Atom feed.
- /// </summary>
- /// <param name="id">Accepts an int id.</param>
- /// <returns></returns>
- public AtomActionResult GetAtomFeed(int id)
- {
- SyndicationFeedItemMapper<MyFeedItem> mapper = SetUpFeedMapper();
- SyndicationFeedOptions options = SetUpFeedOptions();
- IList<MyFeedItem> feedItems = this.fakeRepository.GetLatestFeedItems(id);
- SyndicationFeedHelper<MyFeedItem> feedHelper = SetUpFeedHelper(mapper, options, feedItems);
- return new AtomActionResult(feedHelper.GetFeed());
- }
- private SyndicationFeedHelper<MyFeedItem> SetUpFeedHelper(SyndicationFeedItemMapper<MyFeedItem> mapper,
- SyndicationFeedOptions options, IList<MyFeedItem> feedItems)
- {
- SyndicationFeedHelper<MyFeedItem> feedHelper = new SyndicationFeedHelper<MyFeedItem>
- (
- this.ControllerContext,
- feedItems,
- mapper,
- options
- );
- return feedHelper;
- }
- private static SyndicationFeedOptions SetUpFeedOptions()
- {
- SyndicationFeedOptions options = new SyndicationFeedOptions
- (
- "My Feed Title",
- "My Feed Description",
- "http://mytesturl.com"
- );
- return options;
- }
- private static SyndicationFeedItemMapper<MyFeedItem> SetUpFeedMapper()
- {
- SyndicationFeedItemMapper<MyFeedItem> mapper = new SyndicationFeedItemMapper<MyFeedItem>
- (
- f => f.Title,
- f => f.Description,
- "Home",
- "Articles",
- f => f.Id.ToString(),
- f => f.DateAdded
- );
- return mapper;
- }
- }
- }
You can call these action results in the same way as another type of action result for example:
- <%= this.Html.ActionLink("RSS Feed", "GetRssFeed", new { id = 123 })%>
- <br />
- <%= this.Html.ActionLink("Atom Feed", "GetAtomFeed", new { id = 456 })%>
I hope this overview is helpful.
Kind Regards,
Sean McAlinden.