Posts
203
Comments
1119
Trackbacks
51
June 2011 Entries
Documenting Link Relations with Web API

In a previous post, I talked about how to build your own custom Media Type Formatter with the WCF Web API. In this post, I used HAL as the media type to expose an API for Nerd Dinner. There are a core set of guiding principles to building RESTful services and one of those principles is that documentation efforts for a service should be focused on defining the media type(s) and link relations that the service uses. While it’s important to have a consistent pattern for URI design, it’s really not that important in comparison to defining the link relations because clients should just “follow the links” and not have tight coupling to URI’s (i.e., clients shouldn’t have knowledge of what the URI’s look like).

Let’s take the example HAL response that returned a dinner resource in my last post:

   1:  <resource rel="self" href="http://localhost:1700/dinners/1">
   2:    <link rel="http://localhost:1700/rels/rsvps" href="http://localhost:1700/dinners/1/rsvps" />
   3:    <link rel="http://localhost:1700/rels/update-dinner" href="http://localhost:1700/dinners/1" />
   4:    <link rel="http://localhost:1700/rels/delete-dinner" href="http://localhost:1700/dinners/1" />
   5:    <DinnerId>1</DinnerId>
   6:    <Title>MVC Dinner</Title>
   7:    <Description>This dinner will be for MVC developers.</Description>
   8:    <Address>123 Main Street</Address>
   9:    <Country>USA</Country>
  10:    <EventDate>7/1/2011 12:00:00 AM</EventDate>
  11:  </resource>

The hypermedia shown on lines 2-4 have link relations in URI format (ignore the ugly “localhost:1700” and imagine this was something like “http://nerddinner.com/rels/rsvps”). Since the rel attributes are already in URI format, a common convention is to actually provide the documentation for that link relation at that URI if someone hits that in a browser. This documentation page can contain information such as a human readable description of the purpose of the link relation, what media type should be used in the Accept header, which HTTP verb to use, the format of the resource messages, etc.

How can we produce this documentation when using the WCF Web API? The /help page that comes out of the box in WCF 4 won’t help us much here because that is much more in an RPC style with application/xml and application/json formats that are too generic to match our application’s media type. By leveraging the HAL media type formatter that I’ve previously built, I can use this to help document my link relations to produce a help page that looks like this (this page looks like the WCF /help page simply because I copied their CSS):

linkRelDocPage

 

There were several steps that went in to producing this page. First I defined a metadata object that I used for documentation purposes.

   1:  using System;
   2:  using System.Collections.Generic;
   3:   
   4:  namespace NerdDinnerWebApi.ResourceModel
   5:  {
   6:      public class LinkRelationMetadata
   7:      {
   8:          public LinkRelationMetadata()
   9:          {
  10:              this.HypermediaItems = new List<HypermediaMetadata>();
  11:          }
  12:   
  13:          public string Rel { get; set; }
  14:   
  15:          public string Description { get; set; }
  16:   
  17:          public string HttpMethod { get; set; }
  18:   
  19:          public string Schema { get; set; }
  20:   
  21:          public string Example { get; set; }
  22:   
  23:          public string MediaType { get; set; }
  24:   
  25:          public List<HypermediaMetadata> HypermediaItems { get; set; }
  26:      }
  27:  }

This will allow me to “write my documentation” like this:

   1:  linkRelations.AddItem(LinkRelationNames.Dinner, HttpMethod.Get, "Get a dinner resource for Nerd Dinner.", typeof(Dinner),
   2:      new HypermediaMetadata { Rel = LinkRelationNames.UpdateDinner, ConditionsDescription = "Always present as a resource." },
   3:      new HypermediaMetadata { Rel = LinkRelationNames.DeleteDinner, ConditionsDescription = "Always present as a resource." },
   4:      new HypermediaMetadata { Rel = LinkRelationNames.Rsvps, ConditionsDescription = "Always present as a resource." });

 

The AddItem() method above is the key. Specifically, its job is the instantiate an instance of the type and populate it with dummy values (e.g., “abc” for strings, 123 for integers, etc. This implementation is fairly rudimentary right now) – it does this on line #3 below. Then on line #4 it calls the ToHalRepresentation() method which internally leverages the HalMediaTypeFormatter (line #42) to create the example XML representation that will be shown on the screen.

 

   1:  private static void AddItem(this Dictionary<string, LinkRelationMetadata> dict, string rel, HttpMethod httpMethod, string description, Type responseMessageType, params HypermediaMetadata[] hypermedia)
   2:  {
   3:      var instance = PopulateInstance(responseMessageType);
   4:      var xml = ToHalRepresentation(instance);
   5:           
   6:      dict.Add(rel.Replace("http://localhost:1700/rels/", string.Empty), new LinkRelationMetadata { Rel = rel, HttpMethod = httpMethod.ToString(), MediaType = "application/vnd.nerddinner.hal+xml", Description = description, Example = xml, HypermediaItems = hypermedia.ToList() });
   7:  }
   8:   
   9:  private static object PopulateInstance(Type type)
  10:  {
  11:      var instance = Activator.CreateInstance(type) as HalResource;
  12:      if (instance == null)
  13:      {
  14:          return null;
  15:      }
  16:   
  17:      // First populate HAL-specific properties
  18:      instance.Rel = "self";
  19:      instance.HRef = "http://example.com/foo";
  20:   
  21:      instance.Links = new List<Link>
  22:      {
  23:          new Link { Rel = "abc", HRef = "http://example.com/abc" },
  24:          new Link { Rel = "xyz", HRef = "http://example.com/xyz" }
  25:      };
  26:   
  27:      foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty))
  28:      {
  29:          if (property.IsValueTypeOrString())
  30:          {
  31:              SetDummyValue(instance, property);
  32:          }
  33:      }
  34:   
  35:      return instance;
  36:  }
  37:   
  38:  private static string ToHalRepresentation(object instance)
  39:  {
  40:      var formatter = new HalMediaTypeFormatter();
  41:      var stream = new MemoryStream();
  42:      formatter.WriteToStream(instance.GetType(), instance, stream, null, null);
  43:      stream.Position = 0;
  44:      var xml = new StreamReader(stream).ReadToEnd();
  45:      return xml;
  46:  }
  47:   
  48:  private static void SetDummyValue(object instance, PropertyInfo property)
  49:  {
  50:      if (property.PropertyType == typeof(int))
  51:      {
  52:          instance.SetValue(property, 123);
  53:      }
  54:      else if (property.PropertyType == typeof(string))
  55:      {
  56:          instance.SetValue(property, "abc");
  57:      }
  58:      else if (property.PropertyType == typeof(DateTime))
  59:      {
  60:          instance.SetValue(property, DateTime.Now);
  61:      }
  62:      else
  63:      {
  64:          throw new InvalidOperationException("Unsupported type for setting a dummy value.");
  65:      }
  66:  }

 

At this point I’m just populating the dictionary that will be used for the service call which looks like this:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Net;
   4:  using System.Net.Http;
   5:  using System.ServiceModel;
   6:  using System.ServiceModel.Web;
   7:  using Microsoft.ApplicationServer.Http;
   8:  using Microsoft.ApplicationServer.Http.Dispatcher;
   9:  using NerdDinnerWebApi.ResourceModel;
  10:   
  11:  namespace NerdDinnerWebApi.Services.Services
  12:  {
  13:      [ServiceContract]
  14:      public class LinkRelationService
  15:      {
  16:          static readonly Dictionary<string, LinkRelationMetadata> linkRelations = LinkRelationInitializer.Create();
  17:   
  18:          [WebGet(UriTemplate = "{rel}")]
  19:          public HttpResponseMessage<LinkRelationMetadata> Get(string rel)
  20:          {
  21:              LinkRelationMetadata linkRelation = null;
  22:              if (!linkRelations.TryGetValue(rel, out linkRelation))
  23:              {
  24:                  var notFoundResponse = new HttpResponseMessage();
  25:                  notFoundResponse.StatusCode = HttpStatusCode.NotFound;
  26:                  notFoundResponse.Content = new StringContent("Link Relation '" + rel + "' not found");
  27:                  throw new HttpResponseException(notFoundResponse);
  28:              }
  29:              return new HttpResponseMessage<LinkRelationMetadata>(linkRelation);
  30:          }
  31:      }
  32:  }

 

I would like to provide this data in an HTML format when a user agent such as a browser is requesting with an Accept header of “text/html”. To accomplish this, I’ll use my RazorHtmlMediaTypeFormatter which I blogged about here which utilizes Razor *outside* of MVC and *within* the WCF Web API to render this HTML resource:

   1:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   2:  <html xmlns="http://www.w3.org/1999/xhtml">
   3:    <head>
   4:      <title>Link relation - @Model.Rel</title>
   5:      <style>{copied from WCF help page but omitted here for brevity}</style>
   6:    </head>
   7:    <body>
   8:      <div id="content">
   9:        <p class="heading1">Link relation - @Model.Rel</p> 
  10:        <p><b>Description: @Model.Description</b></p>
  11:        <table>
  12:          <tr>
  13:            <th>Item</th>
  14:            <th>Description</th>
  15:          </tr>
  16:          <tr>
  17:              <td>HTTP Method</td>
  18:              <td>@Model.HttpMethod</td>
  19:          </tr>
  20:          <tr>
  21:              <td>Media Type</td>
  22:              <td>@Model.MediaType</td>
  23:          </tr>
  24:          <tr>
  25:              <td>Potential Hypermedia</td>
  26:              <td>
  27:                  <table>
  28:                      @foreach (var item in @Model.HypermediaItems)
  29:                      {
  30:                          <tr>
  31:                              <td><a href="@item.Rel">@item.Rel</a></td>
  32:                              <td>@item.ConditionsDescription</td>
  33:                          </tr>
  34:                      }
  35:                  </table>
  36:              </td>
  37:          </tr>
  38:        </table>
  39:   
  40:        <p>
  41:          The following is an example response:
  42:          <pre class="response-xml">@FormatXmlForHtml(Model.Example)</pre>
  43:        </p>
  44:   
  45:        <p>
  46:          The following is the response Schema (XSD):
  47:          <pre class="response-xml">@FormatXmlForHtml(Model.Schema)</pre>
  48:        </p>
  49:      </div>
  50:    </body>
  51:  </html>

 

This gives me the HTML that is shown in the browser screen shot above. There might also be consumers who want to generate client side types based on the XML and XSD shown in the link relations doc. Screen scraping HTML is not the most user friendly thing for this. Therefore, if someone requests with an Accept header of “application/xml” we can also provide this same data in XML format just using built-in Web API functionality:

linkRelDocPageXml

 

This is just a prototype of early thinking. I am very curious to hear what other people think about this approach and how it could potentially be evolved.

The complete code sample for this can be downloaded here.

Posted On Monday, June 6, 2011 8:32 PM | Comments (2)
Understanding Custom WCF Web API Media Type Formatters on Both Server and Client

Media Type Formatters in the WCF Web API provide a way for the consumer of your service to not only specify the format they want for their response, but also specify the format they are using on their request. Typically the desired response format is specified in the “Accept” request header and the request body format is specified in the “Content-Type” request header. Media types are central to building any RESTful service. Two of the most common media types are “application/xml” and “application/json”. However, to build a truly RESTful service that is hypermedia driven, these media types should not be used. Instead, a media type that indicates the specifics of your application domain and has rich semantics for hypermedia is preferred.

REST is an architectural style that is governed by a guiding set of principles. While adhering to these principles, there is no one right way to do things. Recently, Darrel Miller turned me on the HAL (Hypermedia Application Language) which was created by Mike Kelly. I really like the well-structured style that HAL has – making it easy and flexible to embed hypermedia in your resources. Although HAL is an XML-based media type, it is not just “application/xml” because of the specific rules that govern its format. We *could* accomplish this format with some well placed attributes that control XML serialization but instead let’s create our own custom Media Type Formatter to create the format. Media Type Formatters expose OnWritetoStream() and OnReadFromStream() methods to allow serialization/deserialization. Quite often people think of media type formatters as only being used on the server. However, they can be used on the client side as well to control serialization/deserialization of the requests. The following diagram shows the process of an HTTP request/response and the role that the Media Type Formatter plays:

 

mediatypeformatterprocess

 

For my example, I’m going to use the Nerd Dinner domain since it’s something most people are familiar with. I could just use “application/hal” for my media type but instead I’m going to use “application/vnd.nerddinner.hal+xml” to more clearly indicate the specifics of my application’s domain (“vnd” indicates a vendor specific domain). One of the core tenets of REST services is loose coupling between the client and server which allows evolvability. Clients need to know about the bookmark URI (i.e., the “home page” so to speak) but should not have knowledge of any other URIs. Instead clients must understand the link relations (i.e., “rel” attributes of the <link> elements) and just “follow the links.”

If I request my well-known “bookmark URI”, to get a specific dinner, it would look like this:

 

nerddinnerxml

 

Notice I passed an Accept header of “application/vnd.nerddinner.hal+xml”. Also notice the three <link> elements that came back in my response – this is my hypermedia. The Link Relations are in URI format and the client must have knowledge of what these mean.

My Dinner class just looks like this:

   1:  public class Dinner : HalResource
   2:  {
   3:      public int DinnerId { get; set; }
   4:      public string Title { get; set; }
   5:      public string Description { get; set; }
   6:      public string Address { get; set; }
   7:      public string ContactPhone { get; set; }
   8:      public string Country { get; set; }
   9:      public DateTime EventDate { get; set; }
  10:      public string HostedBy { get; set; }
  11:  }

The HalResource base class just defines the standard HAL attributes:

   1:  public abstract class HalResource
   2:  {
   3:      public HalResource()
   4:      {
   5:          this.Links = new List<Link>();
   6:      }
   7:   
   8:      public string Rel { get; set; }
   9:      public string HRef { get; set; }
  10:      public string LinkName { get; set; }
  11:      public List<Link> Links { get; set; }
  12:  }

The code snippet below shows what my custom HalMediaTypeFormatter currently looks like. Notice line #18 in the constructor shows the media types that the formatter is intended to support.

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.IO;
   4:  using System.Linq;
   5:  using System.Net;
   6:  using System.Net.Http.Headers;
   7:  using System.Reflection;
   8:  using System.Xml;
   9:  using System.Xml.Linq;
  10:  using Microsoft.ApplicationServer.Http;
  11:   
  12:  namespace NerdDinnerWebApi.ResourceModel
  13:  {
  14:      public class HalMediaTypeFormatter : MediaTypeFormatter
  15:      {
  16:          public HalMediaTypeFormatter()
  17:          {
  18:              this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/vnd.nerddinner.hal+xml"));
  19:          }
  20:   
  21:          public override object OnReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders)
  22:          {
  23:              var instance = Activator.CreateInstance(type);
  24:              var xml = XElement.Load(stream);
  25:   
  26:              // First set the well-known HAL elements
  27:              type.GetProperty("Rel").SetValue(instance, xml.Attribute("rel").Value, null);
  28:              type.SetPropertyValueFromString("HRef", xml.Attribute("href").Value, instance);
  29:   
  30:              var links = xml.Elements("link");
  31:              Console.WriteLine("links count: " + links.Count());
  32:              var linksList = new List<Link>();
  33:              foreach (var link in links)
  34:              {
  35:                  linksList.Add(new Link { Rel = link.Attribute("rel").Value, HRef = link.Attribute("href").Value });
  36:              }
  37:              type.GetProperty("Links").SetValue(instance, linksList, null);
  38:   
  39:              // Now set the rest of the properties
  40:              foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty))
  41:              {
  42:                  Console.WriteLine("setting property: " + property.Name);
  43:                  type.SetPropertyValue(property.Name, xml.Element(property.Name), instance);
  44:              }
  45:   
  46:              return instance;
  47:          }
  48:   
  49:          public override void OnWriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext context)
  50:          {
  51:              XmlWriterSettings settings = new XmlWriterSettings();
  52:              settings.Indent = true;
  53:   
  54:              var writer = XmlWriter.Create(stream, settings);
  55:              var resource = value as HalResource;
  56:              if (resource != null)
  57:              {
  58:                  writer.WriteStartElement("resource");
  59:                  writer.WriteAttributeString("rel", "self");
  60:                  writer.WriteAttributeString("href", resource.HRef);
  61:   
  62:                  foreach (var link in resource.Links)
  63:                  {
  64:                      writer.WriteStartElement("link");
  65:                      writer.WriteAttributeString("rel", link.Rel);
  66:                      writer.WriteAttributeString("href", link.HRef);
  67:                      writer.WriteEndElement();
  68:                  }
  69:   
  70:                  foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty))
  71:                  {
  72:                      if (ShouldWriteProperty(property))
  73:                      {
  74:                          var propertyString = GetPropertyString(property, value);
  75:                          if (propertyString != null)
  76:                          {
  77:                              writer.WriteElementString(property.Name, propertyString);
  78:                          }
  79:                      }
  80:                  }
  81:   
  82:                  writer.WriteEndElement();
  83:                  writer.Flush();
  84:              }
  85:          }
  86:   
  87:          private static bool ShouldWriteProperty(PropertyInfo property)
  88:          {
  89:              var nonSerializedProperties = new[] { "Rel", "HRef", "LinkName" };
  90:              var typesToSerialize = new[] { typeof(int), typeof(string), typeof(DateTime) };
  91:   
  92:              if (nonSerializedProperties.Contains(property.Name))
  93:              {
  94:                  return false;
  95:              }
  96:   
  97:              return typesToSerialize.Contains(property.PropertyType);
  98:          }
  99:   
 100:          private static string GetPropertyString(PropertyInfo property, object instance)
 101:          {
 102:              var propertyValue = property.GetValue(instance, null);
 103:              if (property.PropertyType.IsValueType || propertyValue != null)
 104:              {
 105:                  return propertyValue.ToString();
 106:              }
 107:              return null;
 108:          }
 109:      }
 110:  }

 

The code is not yet Production ready because it only handles a few primitives types and it is still incomplete with respect to the way HAL handles child collections but it gives a good flavor of *both* serialization/deserialization directions for a custom media type formatter. To tell the Web API to use this formatter, you can just add this configuration to your start up code (e.g., Global.asax or a PreApplicationStartMethod). The key is on line #20 below:

   1:  using System;
   2:  using System.Web.Routing;
   3:  using Microsoft.ApplicationServer.Http.Activation;
   4:  using Microsoft.ApplicationServer.Http.Description;
   5:  using NerdDinnerWebApi.ResourceModel;
   6:  using NerdDinnerWebApi.Services.Infrastructure;
   7:  using NerdDinnerWebApi.Services.Services;
   8:   
   9:  [assembly: WebActivator.PreApplicationStartMethod(typeof(NerdDinnerWebApi.Services.App_Start.ServicesInitializer), "Start")]
  10:   
  11:  namespace NerdDinnerWebApi.Services.App_Start
  12:  {
  13:      public static class ServicesInitializer
  14:      {
  15:          public static void Start()
  16:          {
  17:              var iocContainer = StructureMapBootstrapper.Initialize();
  18:              var config = HttpHostConfiguration.Create()
  19:                  .SetResourceFactory(new StructureMapResourceFactory(iocContainer))
  20:                  .AddFormatters(new HalMediaTypeFormatter());
  21:              RouteTable.Routes.MapServiceRoute<DinnerService>("dinners", config);
  22:          }
  23:      }
  24:  }

 

Finally, let’s see what the code looks like on this client side:

   1:  using System;
   2:  using System.Net.Http;
   3:  using System.Net.Http.Headers;
   4:  using Microsoft.ApplicationServer.Http;
   5:  using Microsoft.VisualStudio.TestTools.UnitTesting;
   6:  using NerdDinnerWebApi.ResourceModel;
   7:  using FluentAssertions;
   8:   
   9:  namespace WebApiTests
  10:  {
  11:      [TestClass]
  12:      public class DinnerServiceTests
  13:      {
  14:          private const string baseUri = "http://localhost:1700";
  15:          private static readonly MediaTypeWithQualityHeaderValue halMediaTypeHeader = new MediaTypeWithQualityHeaderValue("application/vnd.nerddinner.hal+xml");
  16:          private static readonly MediaTypeFormatter[] formatters = new[] { new HalMediaTypeFormatter() };
  17:   
  18:          [TestMethod]
  19:          public void Hal_formatter_should_serialize_request_for_dinner_resource()
  20:          {
  21:              // arrange
  22:              var httpClient = new HttpClient(baseUri);
  23:              httpClient.DefaultRequestHeaders.Accept.Add(halMediaTypeHeader);
  24:   
  25:              // act
  26:              var response = httpClient.Get("/dinners/1");
  27:              var dinnerResource = response.Content.ReadAs<Dinner>(formatters);
  28:   
  29:              // assert
  30:              dinnerResource.Should().NotBeNull();
  31:              dinnerResource.Rel.Should().Be("self");
  32:              dinnerResource.HRef.Should().EndWith("/dinners/1");
  33:              dinnerResource.DinnerId.Should().Be(1);
  34:              dinnerResource.Title.Should().Be("MVC Dinner");
  35:              dinnerResource.Description.Should().Be("This dinner will be for MVC developers.");
  36:              dinnerResource.Address.Should().Be("123 Main Street");
  37:              dinnerResource.EventDate.Should().Be(new DateTime(2011, 7, 1));
  38:              dinnerResource.Country.Should().Be("USA");
  39:              dinnerResource.Links.Count.Should().Be(3);
  40:          }
  41:      }
  42:  }

 

There are a couple of very important things to note in the code above. First, on line #23 I am setting the Accept header of the request (the variable was defined on line #15). Second, when I deserialize the xml to a .NET object on line #27, I pass the formatters in (which is just my HalMediaTypeFormatter) so that it knows how to deal with this request. If I don’t do this correctly, I’ll get an InvalidOperationException: No 'MediaTypeFormatter' is available to read an object of type <Type Name> with the media type <Media Type of Response>.

As you can see from the example above, I’m using the *same* custom Media Type Formatter on both the server *and* the client.

As I mentioned earlier in this post, the coupling between the client and server should be focused on defining the link relations for your media types. Stay tuned for my next post where I’ll show a mechanism for leveraging a media type formatter (like the one shown in this post) to generate documentation for consumers of your service so that consumers can fully understand all hypermedia link relations.

The complete solution for the code sample above can be downloaded here.

Posted On Monday, June 6, 2011 12:19 AM | Comments (3)

View Steve Michelotti's profile on LinkedIn

profile for Steve Michelotti at Stack Overflow, Q&A for professional and enthusiast programmers




Google My Blog

Tag Cloud