For starters, any custom media formatter we create will need to inherit from a MediaTypeFormatter base class. I chose to inherit from BufferedMediaTypeFormatter because that's what I found first when I searched for it:
1: public class CustomXmlFormatter: BufferedMediaTypeFormatter
Once you do that, you'll need to add the media type to the SupportedMediaTypes list in the constructor:
1: public CustomXmlFormatter()
2: {
3: SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.class-name-assignment+xml"));
4: }
OK, great. But all that's really done is set up the media type. It doesn't tell the server when to use that media type. This next part is going to go in the WebApiConfig file, in the Register method:
1: config.Formatters.Add(new CustomXmlFormatter());
At this point we're all set up to send and receive content from a client using the aforementioned "application/vnd.class-name-assignment+xml" content type. As long as the request comes from the client with that content type, our new custom media formatter will be used to process it. Unfortunately, nothing will happen with it at this point (actually, the service won't even compile yet because we haven't overridden a couple of important methods). We have to override CanReadType and CanWriteType in order to read and write (respectively) the data as it comes in and goes out:
1: public override bool CanReadType(Type type)
2: {
3: return true;
4: }
5:
6: public override bool CanWriteType(Type type)
7: {
8: return true;
9: }
An important note about that code: it just passes everything right on through. You may want to set those methods up so that it can only read or write based on certain types. I didn't want to do that (and the assignment was coming due and I was short on time) so I just put in return true.
This is all great, but we still aren't actually doing anything here. We have to override a couple more methods in order to actually process the data that comes and goes through this formatter. Since it's shorter, I'll show you ReadFromStream first:
1: public override object ReadFromStream(Type type, Stream readStream, System.Net.Http.HttpContent content, IFormatterLogger formatterLogger)
2: {
3: var serializer = new XmlSerializer(type);
4: var val = serializer.Deserialize(readStream);
5: return val;
6: }
What we've said there is that anything that comes in should be deserialized using the XmlSerializer. We're trusting that the input is properly formatted XML. If it isn't, we'll throw an error. As for the data on the way out, well, hopefully I commented it well enough to make sense:
1: public override void WriteToStream(Type type, object value, Stream writeStream, System.Net.Http.HttpContent content)
2: {
3: // create a stream to work with
4: using (var writer = new StreamWriter(writeStream))
5: {
6: // check whether the object being written out is null
7: if (value == null)
8: {
9: throw new Exception("Cannot serialize type");
10: }
11:
12: // if the object isn't null, build the output as an XML string
13: var output = BuildSingleItemOutputAsXml(type, value);
14:
15: // write the XML string into the stream
16: writer.Write(output);
17: }
18: }
19:
20: private string BuildSingleItemOutputAsXml<T>(Type type, T viewModel)
21: {
22: // get the basic XML rendering of the object
23: var output = AsXml(type, viewModel);
24:
25: // strip off and store the closing tag
26: var closingNodeTag = output.Substring(output.LastIndexOf("</", StringComparison.InvariantCulture));
27: output = output.Substring(0, output.LastIndexOf("</", StringComparison.InvariantCulture));
28:
29: // use reflection to get the properties of the object
30: var properties = type.GetProperties();
31:
32: // iterate the properties of the object until the Links are found (this is related to the HATEOAS requirement)
33: // create a custom node for each link found in the Links property
34: output = (from property in properties
35: where property.PropertyType == typeof(List<LinkViewModel>)
36: select (List<LinkViewModel>)property.GetValue(viewModel, null)
37: into links
38: where links != null
39: from link in links
40: select link).Aggregate(output,
41: (current, link) =>
42: current +
43: string.Format("<link rel=\"{0}\" href=\"{1}\" />", link.Rel, link.Href));
44:
45: // append the closing tag back on the output
46: output += closingNodeTag;
47:
48: return output;
49: }
50:
51: private string AsXml<T>(Type type, T viewModel)
52: {
53: // Build an XML string representation of our object
54: string xmlResult;
55:
56: var settings = new XmlWriterSettings
57: {
58: Encoding = new UnicodeEncoding(false, false),
59: Indent = true,
60: OmitXmlDeclaration = true
61: };
62:
63: var xmlSerializer = new XmlSerializer(type);
64: using (var stringWriter = new StringWriter())
65: {
66: using (var xmlWriter = XmlWriter.Create(stringWriter, settings))
67: {
68: xmlSerializer.Serialize(xmlWriter, viewModel);
69: }
70:
71: //Strip out namespace info
72: xmlResult =
73: stringWriter.ToString()
74: .Replace("xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"", "")
75: .Replace("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"", "");
76: //This is the output as a string
77: }
78:
79: //Load the XML doc
80: var xdoc = new XmlDocument();
81: xdoc.LoadXml(xmlResult.Replace("xsi:nil", "nullable"));
82: //Remove all NULL values
83: var xmlNodeList = xdoc.SelectNodes("//*[@nullable]");
84: if (xmlNodeList != null)
85: foreach (XmlNode node in xmlNodeList)
86: {
87: if (node.ParentNode != null)
88: {
89: node.ParentNode.RemoveChild(node);
90: }
91: }
92:
93: return xdoc.OuterXml;
94: }
Everything up there ends up with one thing: a single XML string written into the stream that is being sent back to the client. There's only one part left now, which is to specify in our responses when to use this new custom media formatter. I use the CreateResponse extension of the HttpRequestMessage object to build my responses so this was a simple matter of specifying the content of the response like this:
1: response.Content = new ObjectContent(typeof(T), content, new CustomXmlFormatter(), "application/vnd.class-name-assignment+xml");
And that's all there is to it!