45  
aspnet
Search  
Always will be ready notify the world about expectations as easy as possible: job change page
Nov 17, 2020

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

Source:
Views:
6434

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.

  • IPostService - Responsible for encapsulating the data logic, which returns a set of Post objects for a given query.
  • IFeedService - Responsible for encapsulating the feed logic, which accepts a set of Post objects and returns an XML document string representing the RSS feed.
  • 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.

Similar
Jun 27, 2022
Author: Jeffery Cheng
This example shows you the gRpc global exception handler in the unary server handle in gRpc. In microservice, we have two ways to integrate with other internal services. The first way is the Request-Response pattern, which is the most famous....
May 20
Author: Semih Tekin
Clean Architecture is a software design philosophy that promotes the separation of concerns and the creation of a modular and maintainable codebase. It was introduced by Robert C. Martin, also known as Uncle Bob, and has gained popularity in the...
Jul 18
Author: Ankit Sahu
Introduction Creating a CRUD (Create, Read, Update, Delete) API in .NET 8 with an In-memory collection is a common scenario in web development. In this article, we’ll walk through building a complete .NET 8 Web API with a real-world use...
Nov 25, 2022
Author: Amit Naik
In this article, you will see a Web API solution template which is built on Hexagonal Architecture with all essential features using .NET Core. Download source code from GitHub Download project template from Microsoft marketplace Introduction This is kick-off project...
Send message
Type
Email
Your name
*Message