October 1, 2012

Publishing content with AtomPub and ASP.NET Web API - Part 3

This is part 3 in a series of posts that covers adding AtomPub support to ASP.NET Web API.

Over the weekend I added support for AtomPub Categories. You’ll find the latest source code on GitHub.

There are two ways in which you can expose categories from an AtomPub service document. The first method is to include them inline. Since I don’t plan to abstract service document creation, you can easily accomplish this yourself:

var posts = new ResourceCollectionInfo("Blog",
    new Uri(Url.Link("DefaultApi", new { controller = "posts" })));

posts.Accepts.Add("application/atom+xml;type=entry");

var categories = new InlineCategoriesDocument(
    new[] { 
        new SyndicationCategory("AtomPub"),
        new SyndicationCategory("Web API") 
    }
);

posts.Categories.Add(categories);

As you can see from the response, the categories are included within the service document:

<?xml version="1.0" encoding="utf-8"?>
<app:service xmlns:a10="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app">
	<app:workspace xml:base="http://localhost:56592/">
		<a10:title type="text">My Site</a10:title>
		<app:collection href="http://localhost:56592/api/posts">
			<a10:title type="text">Blog</a10:title>
			<app:accept>application/atom+xml;type=entry</app:accept>
			<app:categories>
				<a10:category term="AtomPub" />
				<a10:category term="Web API" />
			</app:categories>
		</app:collection>
	</app:workspace>
</app:service>

The second option is to include a link to a separate categories document, a better solution if you’re already exposing category resources in your API (Tags in the sample project):

var categoriesUri = new Uri(Url.Link("DefaultApi", 
    new { controller = "tags" }));
var categories = new ReferencedCategoriesDocument(categoriesUri);
posts.Categories.Add(categories);

The code below is from the TagsController in the sample project. Here we’re returning an aggregated list of all Blog Post Tags:

public class TagsController : BlogControllerBase
{
    // GET api/tags
    public PublicationCategoriesDocument Get()
    {
        var tags = posts.SelectMany(p => p.Tags)
            .Distinct(StringComparer.InvariantCultureIgnoreCase)
            .Select(t => new TagModel { Name = t, Slug = t.ToSlug() });

        var doc = new PublicationCategoriesDocument(
            Url.Link("DefaultApi", new { controller = "tags" }),
            tags,
            isFixed: false
        );

        return doc;
    }
}

Within the Fabrik.Common.WebAPI.AtomPub namespace you’ll find the following interfaces:

/// <summary>
/// An interface for items that can be returned as Atom categories.
/// </summary>
public interface IPublicationCategory
{
    /// <summary>
    /// Required. The name (term) of the category.
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Optional.
    /// </summary>
    string Label { get; }
}

/// <summary>
/// An interface for generating AtomPub Category Documents.
/// </summary>
public interface IPublicationCategoriesDocument
{
    /// <summary>
    /// An IRI categorization scheme for all the categories contained within this document.
    /// </summary>
    string Scheme { get; }

    /// <summary>
    /// Indicates whether the list of categories is a fixed or an open set.
    /// </summary>
    bool IsFixed { get; }

    /// <summary>
    /// The categories within this document.
    /// </summary>
    IEnumerable<IPublicationCategory> Categories { get; }
}

Whilst it is possible to set the scheme of individual categories, in most cases it is sufficient to set the scheme of the overall document.

The library includes implementations for both of these interfaces such as the PublicationCategoriesDocument class that we are returning in the TagsController above.

Like we did for AtomPub Entries, we can use a custom MediaTypeFormatter to generate the categories document:

public class AtomPubCategoryMediaTypeFormatter : BufferedMediaTypeFormatter
{
    private const string AtomCategoryMediaType = "application/atomcat+xml";

    public AtomPubCategoryMediaTypeFormatter()
    {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue(AtomCategoryMediaType));
        this.AddQueryStringMapping("format", "atomcat", AtomCategoryMediaType);
    }

    public override bool CanReadType(Type type)
    {
        return false;
    }
    
    public override bool CanWriteType(Type type)
    {
        return type.Implements<IPublicationCategoriesDocument>();
    }

    public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
    {
        var document = value as IPublicationCategoriesDocument;

        var atomDoc = new InlineCategoriesDocument(
            document.Categories.Select(c => new SyndicationCategory(c.Name) { Label = c.Label }),
            document.IsFixed,
            document.Scheme
        );

        var formatter = new AtomPub10CategoriesDocumentFormatter(atomDoc);

        using (writeStream)
        {
            using (var writer = XmlWriter.Create(writeStream))
            {
                formatter.WriteTo(writer);
            }
        }
    }
}

This formatter handles requests for the AtomPub Category media type, application/atomcat+xml.

Navigating to /api/tags?format=atomcat in the sample project produces the following response:

<app:categories xmlns:a10=“http://www.w3.org/2005/Atom" scheme=“http://localhost:56592/api/tags” xmlns:app=“http://www.w3.org/2007/app"> <a10:category term=“ASP.NET” /> <a10:category term=“Web API” /> <a10:category term=“AtomPub” /> </app:categories>

In an AtomPub client (in this case Fude), the categories are now available:

In order for the AtomPub client to recognise when a category is linked to a AtomPub entry, we need to ensure that they share the same scheme.

Previously the IPublication interface exposed categories as a simple string collection. We would syndicate them like so:

publication.Categories.ForEach(category =>
    item.Categories.Add(new SyndicationCategory(category));

The IPublication interface has since been updated to expose categories as a collection of IPublicationCategory and requires that the category scheme be set:

/// <summary>
/// A collection of categories associated with the publication.
/// </summary>
IEnumerable<IPublicationCategory> Categories { get; }

/// <summary>
/// An IRI categorization scheme for all the categories associated with the publication.
/// </summary>
string CategoriesScheme { get; }

Again I’m assuming that you won’t link categories from different schemes to a single AtomPub entry. We now syndicate the categories like so:

publication.Categories.ForEach(category =>
    item.Categories.Add(new SyndicationCategory(
		category.Name, publication.CategoriesScheme, category.Label)
	));

Windows Live Writer

Yet again, Windows Live Writer neglects to send the correct Accept headers when requesting categories. My previous attempts to circumvent via the WLWMessageHandler won’t work this time since other than the URI, the requests from WLW to both Feeds, Entries and Categories are exactly the same. I should be able to put together a better solution for this in the coming weeks.

For now I’m just including a format querystring in the category link within the service document:

// For WLW to work we need to include format in the categories URI.
// Hoping to provide a better solution than this.
var categoriesUri = new Uri(Url.Link("DefaultApi", 
    new { controller = "tags", format = "atomcat" }));
var categories = new ReferencedCategoriesDocument(categoriesUri);
posts.Categories.Add(categories);

Unfortunately this will still not work. From Joe Cheng’s blog:

  1. If wlwmanifest.xml has a <categoryScheme> option, that’s the scheme that is used.
  2. If <categories fixed="no" /> is in the service document, the empty scheme is used.
  3. If <categories scheme="" fixed="yes|no" /> is in the service document, the empty scheme is used.
  4. Otherwise, categories are not supported.

It seems that when it came to adding support for AtomPub to WLW, there was an assumption that categories would be passed inline. For this reason options 2 and 3 are irrelevant since a referenced category document should not contain the fixed and scheme attributes.

That leaves option 1, adding a wlwmanifest.xml file to our site with the following contents:

<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns="http://schemas.microsoft.com/wlw/manifest/weblog">
  <options>
    <categoryScheme>http://localhost:56592/api/tags</categoryScheme>
    <supportsNewCategories>Yes</supportsNewCategories>
  </options>
</manifest>

Note that the scheme should match that of your categories document, so in the sample project this is http://localhost:56592/api/tags.

Now we can successfully categorize posts within Windows Live Writer and more importantly, have full AtomPub Category support within our ASP.NET Web API application.

© 2022 Ben Foster