Search code examples
xmlvb.netasp.net-mvc-2itunesatom-feed

Return View encoded in UTF-8 without BOM


In my MVC Web Application, I developed a function to return a Newsstand Atom Feed (for Apple's Newsstand). One of their requirements for this feed is that it is effectively encoded in UTF-8 and must not include a BOM. This is how I coded my view (class names are fictional to preserve my company's privacy):

<%@ Page Language="VB" Inherits="System.Web.Mvc.ViewPage(Of IEnumerable (Of AtomFeed))" ContentType="application/atom+xml" ResponseEncoding="UTF-8" %><?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:news="http://itunes.apple.com/2011/Newsstand"><%="" %><%  If Not Model Is Nothing Then%><%  Dim updateDate As String = ViewData("feedUpdate")%><% If (Not String.IsNullOrEmpty(updateDate)) Then%>
<updated><%= updateDate %></updated><%
End If%><% For Each f In Model%>
<entry>
    <id><%= f.id%></id>
    <updated><%= f.updated%></updated>
    <published><%= f.published%></published>
    <news:end_date><%= f.endDate%></news:end_date>
    <summary><%= f.summaryText%></summary>
    <news:cover_art_icons>
        <news:cover_art_icon size="SOURCE" src="<%= f.newspaperCover %>"/>
    </news:cover_art_icons>
</entry><%
        Next%><%
End If%>
</feed>

Today we received a mail from itunes complaining that they couldn't import our XML, without a clue as to why it failed. The rendered XML is compliant to their requirements so my only guess is that there is a problem with the encoding of my view.

How do I correctly return this view in UTF-8 without BOM, so that when they pull the XML from my given url, it will be processed correctly?

EDIT:

After using Darin's implementation, I ended up with the following feed

    <?xml version="1.0" encoding="utf-8"?>
<feed xmlns:news="http://itunes.apple.com/2011/Newsstand"
xmlns="http://www.w3.org/2005/Atom">
  <title type="text"></title>
  <id>uuid:5fc48c36-a1d3-4280-a856-a1a0528e2552;id=1</id>
  <updated>2012-07-23T00:40:00Z</updated>
  <entry>
    <id>23.07.2012</id>
    <title type="text"></title>
    <summary type="text">...</summary>
    <updated>2012-07-23T00:40:00Z</updated>
    <published xmlns="">2012-07-23T00:40:00Z</published>
    <news:end_date>2012-07-24T00:40:00Z</news:end_date>
    <news:cover_art_icons>
      <news:cover_art_icon size="SOURCE"
      src="https://www.someurl.com" />
    </news:cover_art_icons>
  </entry>
  <entry>
    <id>22.07.2012</id>
    <title type="text"></title>
    <summary type="text">...</summary>
    <updated>2012-07-22T00:40:00Z</updated>
    <published xmlns="">2012-07-22T00:40:00Z</published>
    <news:end_date>2012-07-23T00:40:00Z</news:end_date>
    <news:cover_art_icons>
      <news:cover_art_icon size="SOURCE"
      src="https://www.someurl.com" />
    </news:cover_art_icons>
  </entry>
</feed>

Now Apple's Newsstand cannot import the following feed because they say they can't find element in this feed's entry element.


Solution

  • Instead of generating the an XML feed manually in a view I would recommend you using the SyndicationFeed class which is designed for that purpose.

    So let's assume that you have some domain model representing your data:

    public class NewsstandFeed
    {
        public DateTime? Updated { get; set; }
        public IEnumerable<AtomFeed> Items { get; set; }
    }
    
    public class AtomFeed
    {
        public int Id { get; set; }
        public DateTime Updated { get; set; }
        public DateTime Published { get; set; }
        public DateTime EndDate { get; set; }
        public string SummaryText { get; set; }
        public string NewspaperCover { get; set; }
    }
    

    and then a controller that will query some DAL to retrieve the domain model:

    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // Normally this will come from a database or something,
            // but I am hardcoding it for demonstration purposes here
            var model = new NewsstandFeed
            {
                Updated = DateTime.Now,
                Items = new[]
                {
                    new AtomFeed 
                    {
                        Id = 1,
                        Updated = DateTime.Now,
                        Published = DateTime.Now,
                        EndDate = DateTime.Now,
                        SummaryText = "some summary",
                        NewspaperCover = "http://www.google.com"
                    }
                }
            };
    
            return new NewsstandFeedResult(model);
        }
    }
    

    Notice the NewsstandFeedResult that the controller action returns? Let's implement it:

    public class NewsstandFeedResult : ActionResult
    {
        public const string NewsstandNS = "http://itunes.apple.com/2011/Newsstand";
        public NewsstandFeed Model { get; private set; }
    
        public NewsstandFeedResult(NewsstandFeed model)
        {
            Model = model;
            if (model.Items == null)
            {
                model.Items = Enumerable.Empty<AtomFeed>();
            }
        }
    
        public override void ExecuteResult(ControllerContext context)
        {
            var response = context.HttpContext.Response;
            response.ContentType = "application/atom+xml";
    
            var feed = new SyndicationFeed();
            var n = new XmlQualifiedName("news", "http://www.w3.org/2000/xmlns/");
            XNamespace newsstandNs = NewsstandNS;
            feed.AttributeExtensions.Add(n, newsstandNs.ToString());
            if (Model.Updated.HasValue)
            {
                feed.LastUpdatedTime = new DateTimeOffset(Model.Updated.Value.ToUniversalTime());
            }
    
            var items = new List<SyndicationItem>();
            foreach (var item in Model.Items)
            {
                var si = new SyndicationItem();
                si.Id = item.Id.ToString();
                si.LastUpdatedTime = new DateTimeOffset(item.Updated.ToUniversalTime());
                si.Summary = new TextSyndicationContent(item.SummaryText);
    
                si.ElementExtensions.Add(new XElement(newsstandNs + "end_date", item.EndDate.ToUniversalTime()));
                si.ElementExtensions.Add(
                    new XElement(
                        newsstandNs + "cover_art_icons",
                        new XElement(
                            newsstandNs + "cover_art_icon", 
                            new XAttribute("size", "SOURCE"), 
                            new XAttribute("src", item.NewspaperCover)
                        )
                    )
                );
                items.Add(si);
            }
            feed.Items = items;
    
            using (var writer = XmlWriter.Create(response.OutputStream))
            {
                var formatter = new Atom10FeedFormatter(feed);
                formatter.WriteTo(writer);
            }
        }
    }
    

    That's it. Now simply navigate to /home/index and you will get a valid Atom feed respecting all industry standards so that you don't have to worry about BOMs and stuff.