Creating a Simple RSS Feed with ASP.NET Core Web API

While the terminology might seem alien to us, but at its core its just a web service which returns data from its data source in a predefined XML schema called as RSS. In this article, let's build a simple endpoint in our ASP.NET Core WebAPI which can return RSS feed from a Posts database.

RSS stands for "Really Simple Syndication". It is a web feed mechanism, which helps applications or clients get updated content from websites in a timely fashion and empower applications to get content from more than one website regularly without having to manually check for new content.

The best example for this is a news aggregator application, which brings in news articles from more than one news websites and regularly checks for new content and updates whenever new content is available. This way users don’t need to go check each and every website for new content.

RSS feed works on XML, and so any website that exposes RSS feed returns content in XML making it a common medium for the RSS clients to work on.

While the terminology might seem alien to us, but at its core its just a web service which returns data from its data source in a predefined XML schema called as RSS. In this article, let’s build a simple endpoint in our ASP.NET Core WebAPI which can return RSS feed from a Posts database.

Hands-On: Getting Things Started:

Let’s start by creating a new WebAPI project which runs our endpoint for RSS feed. The goal is simple: the API pulls out data from the database for a set of Post objects, which the API would process and construct an RSS feed for consumption by the clients.

> dotnet new webapi --name RssFeedApi

We shall break our total functionality into three components which will be responsible for its own sub problems.

  1. IPostService – Responsible for encapsulating the data logic, which returns a set of Post objects for a given query.
  2. IFeedService – Responsible for encapsulating the feed logic, which accepts a set of Post objects and returns an XML document string representing the RSS feed.
  3. FeedController – Responsible for exposing the RSS feed endpoint, a simple GET call to /api/feed/rss would return an XML responsible which can be digested by any RSS client.

By the way, the Post class which represents a single Post object looks like below:

namespace RssFeedApi.Models
{
    public class Post
    {
        public Post(
            string title, 
            string description, 
            string content, 
            string category, 
            string author)
        {
            this.Title = title;
            this.Description = description;
            this.Content = content;
            this.Category = category;
            this.Author = author;
            this.PostId = Guid.NewGuid();
            this.Slug = Regex.Replace(
                    this.Title.ToLower(), @"s", "-", RegexOptions.Compiled);
            this.PublishedOn = DateTime.Now;
            this.AuthorEmail = $"{this.Author}@mail.com";
        }

        public Guid PostId { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string Content { get; set; }
        public string Slug { get; set; }
        public DateTime PublishedOn { get; set; }
        public string Author { get; set; }
        public string Category { get; set; }
        public string AuthorEmail { get; set; }
    }
}

Let’s start by constructing the IPostService and its implementation: a mock which returns a set of Post objects. The IPostService looks like below:

namespace RssFeedApi.Providers.Abstractions
{
    public interface IPostService
    {
        Task<IEnumerable<Post>> GetPosts();
    }

    public class MockPostService : IPostService
    {
        public Task<IEnumerable<Post>> GetPosts()
        {
            return Task.FromResult<IEnumerable<Post>>(new List<Post>()
            {
                new Post(
                    "How Endpoint Routing works in ASP.NET Core",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.",
                    "ASP.NET Core",
                    "Ram"),
                    new Post(
                    "Understanding Merge Sort - Comparison and Analysis",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.",
                    "DSA",
                    "Ram"),
                    new Post(
                    "Understanding Binary Search - Comparison and Analysis",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.",
                    "DSA",
                    "Ram"),
                    new Post(
                    "Understanding Quick Sort - Comparison and Analysis",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.",
                    "DSA",
                    "Ram"),
                    new Post(
                    "Understanding Modules Directives and Components in Angular",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum",
                    "Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.",
                    "Angular",
                    "Ram")
            });
        }
    }
}

Next, let’s focus on the IFeedService which is the crux of our application.

Constructing the Feed Document:

namespace RssFeedApi.Providers.Abstractions
{
    public interface IFeedService
    {
        Task<string> GetFeedDocument(string host);
    }
}

The host here refers to the domain where the application is running at. We’ll look to why we’re passing it as we look at the implementation of this GetFeedDocument() method.

The FeedService class, which is the implementation of the IFeedService receives an instance of IPostService via constructor injection.

To construct an RSS feed document, we’ll follow four simple steps:

  1. Fetch the Posts which we are exposing via our feed
  2. Construct an XML document with the RSS schema and the necessary metadata
  3. Convert the Post object into Feed Objects following the RSS schema and add them to the XML
  4. Convert the now created XML document into a String – the expected return
namespace RssFeedApi.Providers
{
    public class FeedService : IFeedService
    {
        private readonly IPostService _postsService;

        public FeedService(IPostService posts)
        {
            _postsService = posts;
        }

        public async Task<string> GetFeedDocument(string host)
        {
            // STEP 1: Fetch Posts from the DataSource
            IEnumerable<Post> posts = await _postsService.GetPosts();
            
            ... /* STEP 2 */ ...
        }
    }
}

Before Step 2, we’ll use the below package which helps us in creating the RSS document in simple steps.

> dotnet add package Microsoft.SyndicationFeed.ReaderWriter

Once we’ve installed the SyndicationFeed.ReaderWriter package, we’ll take up Step 2 as below:

// STEP 2: PREPARE THE FEED METADATA
StringWriter sw = new StringWriter();
using (XmlWriter xmlWriter = XmlWriter.Create(sw, 
    new XmlWriterSettings() { Async = true, Indent = true }))
{
    var rss = new RssFeedWriter(xmlWriter);
    await rss.WriteTitle("MyBlog");
    await rss.WriteDescription("My Blog is the Best one out there!");
    await rss.WriteGenerator("MyBlog");
    await rss.WriteValue("link", host);

    ... /* STEP 3 */ ...
}

The Step 3 is to add the Post objects to this XML. Like I said before, we need to first convert our Post object into equivalent RSS Feed object which is as per the schema. We’ll create an AtomEntry object for every Post which comes with similar fields such as Title, Description and so on. This is done as below:

private AtomEntry ToRssItem(Post post, string host)
{
    var item = new AtomEntry
    {
        Title = post.Title,
        Description = post.Content,
        Id = $"{host}/posts/{post.Slug}",
        Published = post.PublishedOn,
        LastUpdated = post.PublishedOn,
        ContentType = "html",
    };

    if (!string.IsNullOrEmpty(post.Category))
    {
        item.AddCategory(
            new SyndicationCategory(post.Category));
    }

    item.AddContributor(
        new SyndicationPerson(post.Author, post.AuthorEmail));
    
    item.AddLink(
        new SyndicationLink(new Uri(item.Id)));

    return item;
}

We’ll use this Method on all the Post objects we’ve obtained and add them to the RssFeedWriter.

if (posts != null && posts.Count() > 0)
{
    // STEP 3: PREPARE ITEMS FOR THE FEED
    var feedItems = new List<AtomEntry>();
    foreach (var post in posts)
    {
        var item = ToRssItem(post, host);
        feedItems.Add(item);
    }

    foreach (var feedItem in feedItems)
    {
        await rss.Write(feedItem);
    }
}

Finally we’ll extract the completed XML document out of the StringWriter instance, on which the RssFeedWriter has been writing onto.

public async Task<string> GetFeedDocument(string host)
{
    // STEP 1: Fetch Posts from the DataSource
    var posts = await _postsService.GetPosts();

    // STEP 2: PREPARE THE FEED METADATA
    StringWriter sw = new StringWriter();

    using (XmlWriter xmlWriter = XmlWriter.Create(
            sw, new XmlWriterSettings() { 
                Async = true, Indent = true }))
    {
        var rss = new RssFeedWriter(xmlWriter);
        await rss.WriteTitle("MyBlog");
        await rss.WriteDescription("My Blog is the Best one out there!");
        await rss.WriteGenerator("MyBlog");
        await rss.WriteValue("link", host);

        if (posts != null && posts.Count() > 0)
        {
            // STEP 3: ADD ITEMS TO THE FEED
            var feedItems = new List<AtomEntry>();
            foreach (var post in posts)
            {
                var item = ToRssItem(post, host);
                feedItems.Add(item);
            }

            foreach (var feedItem in feedItems)
            {
                await rss.Write(feedItem);
            }
        }
    }

    // STEP 4: EXTRACT THE XML DOCUMENT
    return sw.ToString();
}

Finally – Exposing the API:

The WebAPI has a very simple job, since both the IPostService and the IFeedService have done the major part. The API calls on the IFeedService.GetFeedDocument() method which returns an XML document string. The API writes the String to the Response with the ContentType set to "application/xml". We can do this by using the Content() method available from the ControllerBase class.

namespace RssFeedApi.Controllers
{

    [Route("api/[controller]")]
    [ApiController]
    public class FeedController : ControllerBase
    {
        private readonly IFeedService _feed;

        public FeedController(IFeedService feed)
        {
            _feed = feed;
        }

        [HttpGet, Route("rss")]
        public async Task<IActionResult> Rss()
        {
            string host = Request.Scheme + "://" + Request.Host;
            string contentType = "application/xml";

            var content = await _feed.GetFeedDocument(host);
            return Content(content, contentType);
        }
    }
}

Not to forget that we need to register both the IPostService and IFeedService in the DI during the Startup.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IPostService, MockPostService>();
    services.AddSingleton<IFeedService, FeedService>();
    services.AddControllers();
}

Testing for Results:

When we run this API and navigate to /api/feed/rss we notice the below XML response:

<?xml version="1.0" encoding="utf-16"?>
<rss version="2.0">
  <channel><title>MyBlog</title>
<description>My Blog is the Best one out there!</description>
<generator>MyBlog</generator>
<link>https://localhost:5001</link>
<item>
  <title>How Endpoint Routing works in ASP.NET Core</title>
  <link>https://localhost:5001/posts/how-endpoint-routing-works-in-asp.net-core</link>
  <description>Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.</description>
  <author>Ram@mail.com</author>
  <category>ASP.NET Core</category>
  <guid isPermaLink="false">https://localhost:5001/posts/how-endpoint-routing-works-in-asp.net-core</guid>
  <pubDate>Fri, 13 Nov 2020 14:52:42 GMT</pubDate>
</item>
<item>
  <title>Understanding Merge Sort - Comparison and Analysis</title>
  <link>https://localhost:5001/posts/understanding-merge-sort---comparison-and-analysis</link>
  <description>Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum. Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum Loreum Ipseum.</description>
  <author>Ram@mail.com</author>
  <category>DSA</category>
  <guid isPermaLink="false">https://localhost:5001/posts/understanding-merge-sort---comparison-and-analysis</guid>
  <pubDate>Fri, 13 Nov 2020 14:52:42 GMT</pubDate>
</item>
---- other items ----
</channel>
</rss>

To validate this result, we can use a standard RSS validator such as https://validator.w3.org/feed/ which can help us in further fine-tuning.

Final Thoughts:

Although RSS was an invention of the 1990s, its interoperability and open nature keeps it relavant for websites to expose content to clients such as aggregators even today. Most of the WordPress and Blogger sites even provide RSS feeds for websites built on their platforms. By using the SyndicationFeed library available for .NET Core, we can easily construct and expose RSS feeds via our APIs.


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *