I have to confess that every time I have to work with an external API and I'm faced with Xml serialization in C#, my reaction is: no biggie. And then, it hits me. I'll probably have to use custom namespaces. Meh.
The Google Shopping Feed specification uses its fair share of custom namespaces, so let's see how we can deal with that in order to serve the serialized feed through a Web API action method.
As you can see from the link above, we'll be using the Atom 1.0
specification, but using custom namespaces should work as well if you'd rather use the Rss
specification.
I recommend you download the Atom 1.0 example file from the specification page before we continue.
Defining the serializable classes
As you can see on the example file, the fun with namespaces starts right off the bat with a custom g
namespace that is defined on the root element:
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:g="http://base.google.com/ns/1.0">
...and then used on every attribute of the entry
element.
So, let us define the class for the feed
element first:
[XmlRoot("feed"), Serializable]
public class Feed
{
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("link")]
public string Link { get; set; }
[XmlElement("updated")]
public DateTime Updated { get; set; }
[XmlElement("entry")]
public List<Entry> Entries { get; set; }
}
As you can see, nothing too exciting here. A Feed
has a couple of simple properties and a collection of Entry
objects, each of which represents a product in your shopping feed.
"Hah!" I hear you say. "You forgot to define the g
namespace, you big doofus, no wonder you're having trouble with the namespaces!".
Well, no. If you thought you could get away with this:
[XmlRoot("feed", Namespace = "http://base.google.com/ns/1.0")]
public class Feed
{
// ...
}
you're out of luck. For one, there's no way (that I know of) to define the g
prefix for that namespace, and, to top it up, the feed
element requires two namespaces, the other one being the Atom namespace.
Moving on; time for the Entry
class:
[XmlType(TypeName = "entry"), Serializable]
public class Entry
{
[XmlElement("id", Namespace = "http://base.google.com/ns/1.0")]
public string Id { get; set; }
[XmlElement("title", Namespace = "http://base.google.com/ns/1.0")]
public string Title { get; set; }
[XmlElement("description",
Namespace = "http://base.google.com/ns/1.0")]
public string Description { get; set; }
[XmlElement("link", Namespace = "http://base.google.com/ns/1.0")]
public string Link { get; set; }
[XmlElement("image_link", Namespace = "http://base.google.com/ns/1.0")]
public string ImageLink { get; set; }
[XmlElement("condition", Namespace = "http://base.google.com/ns/1.0")]
public string Condition { get; set; }
[XmlElement("availability",
Namespace = "http://base.google.com/ns/1.0")]
public string Availablity { get; set; }
[XmlElement("price", Namespace = "http://base.google.com/ns/1.0")]
public string Price { get; set; }
[XmlElement("brand", Namespace = "http://base.google.com/ns/1.0")]
public string Brand { get; set; }
[XmlElement("mpn", Namespace = "http://base.google.com/ns/1.0")]
public string MPN { get; set; }
[XmlElement("shipping", Namespace = "http://base.google.com/ns/1.0")]
public List<Shipping> Shipping { get; set; }
[XmlElement("google_product_category",
Namespace = "http://base.google.com/ns/1.0")]
public string GoogleProductCategory { get; set; }
[XmlElement("product_type",
Namespace = "http://base.google.com/ns/1.0")]
public string ProductType { get; set; }
}
and, finally, the Shipping
class
[XmlType(TypeName = "shipping"), Serializable]
public class Shipping
{
[XmlElement("country", Namespace = "http://base.google.com/ns/1.0")]
public string Country { get; set; }
[XmlElement("service", Namespace = "http://base.google.com/ns/1.0")]
public string Service { get; set; }
[XmlElement("price", Namespace = "http://base.google.com/ns/1.0")]
public string Price { get; set; }
}
I'm not the kind of person to say "I told you so", but I told you so. Namespace galore.
But this is at least straightforward: the Namespace
property on the XmlElement
attribute assumes that the given namespace is already defined somewhere so we can just assign the right namespace. But how do we make sure the g
prefix is applied on serialization? And that the feed
element gets both it's namespaces?
Where the magic happens
We want to end up returning a stream of XML through a Web API action method, and for that we can use the ApiController.Content<T>
method since one of the overloads of the method allows us to use a custom formatter.
The Web API has two built in formatters, JSON and XML. Were it not for the custom namespaces we could just tell the Web API to return our Feed
class as XML and Robert's your mother's brother, but alas. Time for a custom formatter, then.
In order to create a custom formatter we have to create a class inheriting from XmlMediaTypeFormatter
, then, in the constructor of our custom formatter we'll be able to specify any custom XML namespaces we may need. We will also have to overwrite the WriteToStreamAsync
method, which is the method that'll be doing the actual serializing of our classes to XML.
So, there you go. Have fun.
What? Oh all right, all right. Here's the code.
Multiple namespaces? No problem!
Let's take a look at the constructor of our custom formatter.
public class NamespacedXmlMediaTypeFormatter : XmlMediaTypeFormatter
{
public XmlSerializerNamespaces Namespaces { get; private set; }
Dictionary<Type, XmlSerializer> Serializers { get; set; }
public NamespacedXmlMediaTypeFormatter()
{
Namespaces = new XmlSerializerNamespaces();
Namespaces.Add("g", "http://base.google.com/ns/1.0");
Serializers = new Dictionary<Type, XmlSerializer>();
}
// We'll take a look at this method in a minute
public override Task WriteToStreamAsync(...) {}
}
As you can see, the Namespaces
property, of type XmlSerializerNamespaces
will hold a collection of XML namespaces. We have to give a prefix to every namespace we add to the collection; in our case this is the g
of the Google namespace.
We have them, now where do we use them?
Next up, the method we need to overwrite. According to the documentation, WriteToStreamAsync
is:
Called during serialization to write an object of the specified type to the specified writeStream.
So it is not a method we'll have to worry about calling ourselves; the Web API will take care of that once we tell it to use our custom formatter. Here's what it looks like.
public override Task WriteToStreamAsync(Type type, object value,
Stream writeStream, HttpContent content,
TransportContext transportContext)
{
lock (Serializers)
{
if (!Serializers.ContainsKey(type))
{
// we instantiate the new serializer by passing in
// the main XML namespace,
// in this case the Atom namespace
var serializer = new XmlSerializer(type,
"http://www.w3.org/2005/Atom");
//we add a new serializer for this type
Serializers.Add(type, serializer);
}
}
return Task.Factory.StartNew(() =>
{
XmlSerializer serializer;
lock (Serializers)
{
serializer = Serializers[type];
}
var writerSettings = new XmlWriterSettings
{
OmitXmlDeclaration = false
};
var xmlWriter = XmlWriter.Create(writeStream, writerSettings);
serializer.Serialize(xmlWriter, value, Namespaces);
});
}
One thing to notice is that, as the name implies, it's an asynchronous method and so it has to return a Task
. Other that that, it mostly takes care of initializing a Serializer
for the type passed in the parameters, and then, already within the asynchronous Task
definition it gets the right serializer for the given type and calls the Serialize
method on it.
Putting it all together
Now that we have everything nice and ready, how do we use it? Time to create a Web API action method that will return our shiny XML feed ready to be fed to Google.
public IHttpActionResult Get()
{
// here's where you'd fill the properties of the Feed object,
// including the Entries collection
var feed = BuildFeed();
// and here, finally, we tell the Web API to format the
// 'feed' object using our custom formatter.
return Content(HttpStatusCode.OK, feed,
new NamespacedXmlMediaTypeFormatter());
}
And that's it, really. This action goes in the ApiController
that you're gonna use to generate the shopping Feed.
If you have any questions, remarks, or know of a better way to do this, feel free to let me know in the comments!