How to create Apple iTunes and Google Play compatible RSS feed using ASP.NET

In order to create a podcast on Google Play or Apple iTunes, you need to create a valid RSS feed that conforms to the RSS 2.0 specifications and also to include their required tags. Luckily, both companies accept each other’s standards. In other words, if you decide to implement Apple’s tags, you won’t need to change or modify your feed to support Google’s standards.

In this article I’m going to explain how to create a feed that is compatible with both companies. Let’s assume you are building a feed for weekly church sermons, here is what you need to do:

1- Define a RSS element that includes iTunes & Atom namespace:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
</rss>

2- Inside the rss element, create a channel element which contains the following sub-elements:

  • <title> Title of the Podcast’s
  • <link> The feed’s URL
  • <atom:link> Same as <link>
  • <language> The ISO language used in the podcast. See http://www.loc.gov/standards/iso639-2/php/code_list.php for the languages list.
  • <copyright> The organization copyrights
  • <itunes:author> Author of the podcast (up to 255 characters)
  • <itunes:summary> Description of the podcast (up to 4000 characters)
  • <description> Same as summary
  • <itunes:owner> After the feed is submitted to either companies, they will send a verification email to the email provided in the sub element. This element contains two sub elements
    • <itunes:name> The organization’s name
      (up to 255 characters)
    • <itunes:email> Email address to authenticate podcast ownership (up to 255 characters). Make sure you have access to that email
  • <itunes:image> Image shown on Google Play or iTunes to identify your podcast (URL can include up to 2048 characters). The image has to be at least 1400 x 1400 pixels and at maximum 3000 x 3000 pixels in JPEG or PNG format
  • <itunes:category> The podcast’s category. See https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12 for a complete supported category and subcategory list.
  • <itunes:explicit> If your podcast has explicit content, you need to add the appropriate tag on your podcast’s RSS feed. See https://support.google.com/googleplay/podcasts/answer/6260345 to learn more about identifying explicit content.

The following is an example of a valid channel element

<channel>
  <title>Church Sermon</title>
  <link>http://www.church.com/Sermon</link>
  <atom:link>http://www.church.com/Sermon</atom:link>
  <language>en-us</language>
  <copyright>Church Copyright</copyright>
  <itunes:author>Author</itunes:author>
  <itunes:summary>Weekly Sermons</itunes:summary>
  <description>Church Weekly Sermons</description>
  <itunes:owner>
    <itunes:name>Church</itunes:name>
    <itunes:email>church@church.com</itunes:email>
  </itunes:owner>
  <itunes:image href="http://www.church.com/img/church.jpg" />
  <itunes:category text="Religion & Spirituality">
    <itunes:category text="Christianity" />
  </itunes:category>
  <itunes:explicit>no</itunes:explicit>
</channel>

3- Create a list of item elements (one for each episode) inside the channel element. Each item with the following sub-elements:

  • <title> The episode name
  • <itunes:author> The episode speaker (up to 255 characters)
  • <itunes:summary> The episode description (up to 4000 characters)
  • <enclosure> This element contains three attributes:
    • URL: The URL attribute points to your podcast content
    • length: The length attribute is the file size in bytes
    • type: The type attribute provides the correct category for the type of file you are using. The type values for the supported file formats are: audio/x-m4a, audio/mpeg, video/quicktime, video/mp4, video/x-m4v, and application/pdf.
  • <guid> Permanent, case-sensitive Globally Unique Identifier for a podcast episode. Each episode needs a GUID. GUIDs are compared to indicate which episodes are new. If the guid is not a link, set the isPermaLink attribute to false (As shown in the example)
  • <pubDate> The episode publication date shown to users
  • <itunes:explicit> Described above.

The following is an example of a valid item

<item>
  <title>episode 1</title>
  <itunes:author>Fr. James</itunes:author>
  <itunes:summary>This is the first episode</itunes:summary>
  <enclosure url="http://church.com/sermons/episode1.mp4" type="audio/x-mp3" length="6957974" />
  <guid isPermaLink="false">episode1.mp4</guid>
  <pubDate>Sat, 19 Jan 2019 00:00:00 +00:00</pubDate>
  <itunes:explicit>no</itunes:explicit>
</item>

If we put together what we have discussed we’ll end up with the following implementation

[HttpGet]
[HttpHead]
[AllowAnonymous]
public void RssFeed()
{
    XNamespace itunesNs = "http://www.itunes.com/dtds/podcast-1.0.dtd";
    XNamespace atomNs = "http://www.w3.org/2005/Atom";
    var feed = new XDocument(
        new XDeclaration("1.0", "utf-8", "yes"),
        new XElement("rss",
        new XAttribute("version", "2.0"),
        new XAttribute(XNamespace.Xmlns + "itunes", itunesNs.NamespaceName),
        new XAttribute(XNamespace.Xmlns + "content", "http://purl.org/rss/1.0/modules/content/"),
        new XAttribute(XNamespace.Xmlns + "atom", atomNs.NamespaceName),
            new XElement("channel",
                new XElement("title", "Church Sermon"),
                new XElement("link", new Uri(Url.Action(nameof(Index), "Sermon", null, Request.Scheme))),
                new XElement(atomNs + "link", new Uri(Url.Action(nameof(Index), "Sermon", null, Request.Scheme))),
                new XElement("language", "en-us"),
                new XElement("copyright", "Church Copyright"),
                new XElement(itunesNs + "author", "Author"),
                new XElement(itunesNs + "summary", "Weekly Sermons"),
                new XElement("description", "Church Weekly Sermons"),
                new XElement(itunesNs + "owner",
                    new XElement(itunesNs + "name", "Church"),
                    new XElement(itunesNs + "email", "church@church.com")),
                new XElement(itunesNs + "image", new XAttribute("href", new Uri(Request.Scheme + "://" + Request.Host.Value + "/img/church.jpg"))),
                new XElement(itunesNs + "category", new XAttribute("text", "Religion & Spirituality"),
                    new XElement(itunesNs + "category", new XAttribute("text", "Christianity"))),
                new XElement(itunesNs + "explicit", "no"),
                _Context.Sermons.Include(s => s.Author).AsNoTracking().ToList()
                .Select(s =>
                {
                    return new XElement("item",
                        new XElement("title", s.Title),
                        new XElement(itunesNs + "author", "Fr. " + s.Author.FirstName + " " + s.Author.LastName),
                        new XElement(itunesNs + "summary", s.Title),
                        new XElement("enclosure", new XAttribute("url", s.AudioUrl), new XAttribute("type", "audio/x-mp3"), new XAttribute("length", s.Size)),
                        new XElement("guid", s.AudioBlobId, new XAttribute("isPermaLink", "false")),
                        new XElement("pubDate", s.Date.ToString("ddd, dd MMM yyyy HH:mm:ss zzzz")),
                        new XElement(itunesNs + "explicit", "no"));
                }))));

    Response.ContentType = "application/rss+xml";
    using (XmlWriter writer = XmlWriter.Create(Response.Body))
    {
        feed.WriteTo(writer);
        writer.Close();
    }

    Response.Body.Flush();
}

The above controller action method implementation supports Head requests, which is a way of allowing Google Play or iTunes to get information about the episodes without downloading them.

Also, keep in your mind that this method will be called frequently according to your podcast popularity. Consequently, I’d recommend for an actual implementation to cache the request, or at least the database calls (querying the sermons in this case).

Log4net for .Net Core 2.0

Log4net is a an excellent library that allows developers to output log statements to a variety of output targets through what is called Appenders. However, it’s not compatible with .Net Core 2.0 yet, and all the online log4net extension libraries available today don’t provide a thorough solution to rectify all log4net appenders specifically the ADOAppender; which logs to a database.  In this article I am going to introduce a solution to this problem.

I have uploaded the solution to GitHub at this URL:  https://github.com/rizksobhi/Log4net.NetCore

1- AdoNetAppender

I have created an appender that is compatible with .Net Core that can be used to log to a database. The code is straight forward and I have imported most of it from the log4net library. https://github.com/rizksobhi/Log4net.NetCore/blob/master/Log4net.NetCore.Lib/Appenders/AdoNetAppender.cs

2- Configuration file replacements

I have substituted the configuration file with a static configuration class which provides several ways of creating a variety of appenders. This configuration class uses the AdoNetAppender which is described above.

public static IAppender CreateAdoNetAppender(string connectionString)
{
    AdoNetAppender appender = new AdoNetAppender()
    {
        Name = "AdoNetAppender",
        BufferSize = 1,
        ConnectionType = "System.Data.SqlClient.SqlConnection, System.Data, Version = 1.0.3300.0, Culture = neutral, PublicKeyToken = b77a5c561934e089",
        ConnectionString = connectionString,
        CommandText = "INSERT INTO Log ([Date],[Thread],[Level],[Logger],[Message],[Exception]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception)"
    };

    AddDateTimeParameterToAppender(appender, "@log_date");
    AddStringParameterToAppender(appender, "@thread", 255, "%thread");
    AddStringParameterToAppender(appender, "@log_level", 50, "%level");
    AddStringParameterToAppender(appender, "@logger", 255, "%logger");
    AddStringParameterToAppender(appender, "@message", 4000, "%message");
    AddErrorParameterToAppender(appender, "@exception", 2000);

    appender.ActivateOptions();
    return appender;
}

3- ASP.Net Core integration

The following extension method helps to register log4net in the logging factory

public static ILoggerFactory AddLog4Net(this ILoggerFactory factory, string connectionString, string logFilePath)
{
    factory.AddProvider(new Log4NetProvider(connectionString, logFilePath));
    return factory;
}

Now we can register our log4net provider through using the extension method at the Configure function in Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, Log4netDBContext context, ILoggerFactory loggerFactory)
{
    loggerFactory.AddLog4Net(Configuration["Logging:ConnectionString"], Configuration["Logging:LogFilePath"]);

    DBInitializer.Initialize(context);

    if (env.IsDevelopment())
    {
        app.UseBrowserLink();
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }
            
    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Here is the implementation of Log4NetProvider https://github.com/rizksobhi/Log4net.NetCore/blob/master/Log4net.NetCore.Lib/Log4NetProvider.cs

And finally using in the HomeController

public HomeController(ILogger logger)
{
    _Logger = logger;
}
public IActionResult Index()
{
    _Logger.LogDebug("Index has been requested");
    return View();
}

References

  1. https://github.com/apache/logging-log4net
  2. https://dotnetthoughts.net/how-to-use-log4net-with-aspnetcore-for-logging/

How to integrate PayPal with .Net Core

In this article I am going to explain how you can integrate PayPal express checkout with Asp .Net Core using REST API to process payments.

1- Download the sample project

I have created an Asp.net core project using VS 2017 for you. You can download it from Github https://github.com/rizksobhi/PaypalExpressCheckout

2- Create a PayPal App

Go to PayPal developer , and under REST API apps click Create App

CreateApp

3- Update ClientID and Client Secret

Copy the ClientID and Secret from Sandbox and update appsettings.json in the PayPalExpressCheckout.Web application

AppSecret

appsettings

4- Update Payee and Payer information

Go to PayPalServices and update the Payee email and merchant_id

PayeeInfo

For testing purposes, PayPal provides testing accounts that you can create under your PayPal account. You can create test buyer and seller accounts from Sandbox environment for testing

To create test accounts, go to Sandbox -> Accounts and click on Create Account and follow the instructions

TestPaymentAccounts

6- Build and Run the solution. YOU ARE ALL SET.