Need help with MediaTypeMapping and formatters

Topics: Web Api
Jan 3, 2012 at 2:43 PM

Hi everyone,

I'm working on implementing a solution to how my team wants to implement and expose REST resource types using custom MIME types with built-in versioning support (see http://barelyenough.org/blog/2008/05/versioning-rest-web-services/).
The incoming request has an accept header that specifies the resource type and version being requested, as well as the format the client wants it in.
Internally I have to choose a formatter (json/xml), the API method to run (based on attributes) and put the correct Content-Type on the response as well.

My requirements are roughly:
---------------------------------

1) Select the correct API method based on request Accept header + attribute data on the API method.
 - 1a) First try to find an exact match complete with version specifier (we don't support non-versioned resources)
 - 1b) Return latest version if Accept header is not provided or is: (empty), */*, application/json, application/xml.
2) If Accept header on request is none of the above, return HTTP 415 (Media Type Not Supported)
3) Support quality parameter in Accept header to select the best version based on client preferences.
4) Select correct serializer/formatter based on last part of Content-Type string (+json or +xml to start with).
 - Formatter chosen based on Accept header (no header="+json" by default).
 - Last part of content type set based on formatter chosen (+json, +xml etc)
5) Set the correct Content-Type header on response based on an attribute on the API method returning the data.
 - The last part of the type name is based on formatter used (+json or +xml).

I have #5 working as a response HttpOperationHandler (except choosing +json or +xml), for example: [ResourceType("application/vnd.mycompany.myapp-v2")].  However I'm having some issues with mapping the incoming requests to a formatter.
My first idea was to implement a custom MediaTypeMapping class to match on the last part of the Accept header, but I quickly found out that the media type is set in the constructor and is read only.. :(  Also, at that point the value set by my HttpOperationHandler gets overwritten by the MediaTypeMapping.. I suspect its because it runs after the handler when returning the response?
Does that mean that MediaTypeMapping classes cannot set different content types? If so, what would be the best approach to solving this?

As for the other steps, I suspect I have to somehow implement my own operation selector that inspects (and caches) attribute values on the API classes?

All help is appreciated! I know a few other people here are thinking and trying to implement similar things.

Jan 3, 2012 at 3:21 PM
Edited Jan 3, 2012 at 3:23 PM

I suppose I could programmatically inspect attributes on all API classes and all media types found (with +json, +xml etc) to the SupportedMediaTypes collection on the respective formatters.. that just seems a bit.. inelegant?

Jan 6, 2012 at 11:14 AM

Hi Siggi,

I'm trying a similar approach. Incoming requests have an Accept and/or Content-Type value of:

application/x.ourcompany.namespace.someobject-version+encoding

...where version is an integer, and encoding is xml or json.

First, I decorate our .Net classes with an attribute containing the media type stem, and an integer version, e.g.

 

[OurMediaType("x.ourcompany.namespace.someobject", 1)]
public class SomeObject_V1
{
}

 

Next, we have an HttpOperationHandler that returns a media types structure which can be taken as a parameter to a web method. This has a static constructor which builds a dictionary mapping media type to our .Net objects. When a request comes in, the Accept and Content-Type are parsed, and the corresponding .Net object types are passed out.

It goes something like this (bits snipped, variable names changed etc)

 

/// <summary>
/// Extract the object type that is being requested, by processing the "Accept" and/or "Content-Type" header entries,
/// and stripping out the media type definition. Also removes the custom media type definition and replaces with plain "application/xml"
/// or "application/json", in order not to confuse the .NET serialiser.
/// </summary>
public class ObjectTypeRequestHandler : HttpOperationHandler<HttpRequestMessage, IMediaTypes>
{
	private static IDictionary<Tuple<String, int>, Type> _types = new Dictionary<Tuple<String, int>, Type>();

	static ObjectTypeRequestHandler()
	{
		// Create dictionary of types.

		// Look at each type which has a OurMediaTypeAttribute.
		foreach (Type type in [our assembly].GetTypes().Where(f => f.GetCustomAttributes(typeof(OurMediaTypeAttribute), false).Any()))
		{
			OurMediaTypeAttribute attribute = (OurMediaTypeAttribute)type.GetCustomAttributes(typeof(OurMediaTypeAttribute), false).First();
			_types.Add(new Tuple<String, int>(attribute.TypeString, attribute.Version), type);
		}
	}

	public ObjectTypeRequestHandler()
		: base("ourMediaTypes")
	{
	}

	protected override IMediaTypes OnHandle(HttpRequestMessage input)
	{
		// Get the Accept header, i.e. what format the content will be returned in.
		Type objectResponseType = null;

		foreach (MediaTypeWithQualityHeaderValue acceptHeaderValue in input.Headers.Accept)
		{
			if (objectResponseType == null)
			{
				Match match = Regex.Match(acceptHeaderValue.MediaType, "application/x.(?<type>[\\w\\.]+)-v(?<version>\\d+)\\+(?<format>\\w+)");
				if (match.Success)
				{
					String type = match.Groups["type"].Value;
					String version = match.Groups["version"].Value;
					String format = match.Groups["format"].Value;

					_types.TryGetValue(new Tuple<string, int>(type.ToLower(), int.Parse(version)), out objectResponseType);

					// Spoof the format to be application/xml or application/json, so that the .NET serialiser
					// outputs in the appropriate format.
					acceptHeaderValue.MediaType = "application/" + format;
				}
				else
				{
					// Might be a standard type such as application/pdf.
					_types.TryGetValue(new Tuple<String, int>(acceptHeaderValue.MediaType, 0), out objectResponseType);
				}
			}
		}


		// Get the Content-Type header, i.e. what format the request content was supplied in (if any)
		Type objectRequestType = null;

		IEnumerable<String> contentTypesHeader = null;
		HttpContentHeaders requestHeaders = input.Content.Headers;
		if (requestHeaders.Select(f => f.Key).Contains("Content-Type"))
		{
			if (requestHeaders.TryGetValues("Content-Type", out contentTypesHeader))
			{
				foreach (String contentHeaderValue in contentTypesHeader)
				{
					if (objectRequestType == null)
					{
						Match match = Regex.Match(contentHeaderValue, "application/x.(?<type>[\\w\\.]+)-v(?<version>\\d+)\\+(?<format>\\w+)");
						if (match.Success)
						{
							String type = match.Groups["type"].Value;
							String version = match.Groups["version"].Value;
							String format = match.Groups["format"].Value;

							_types.TryGetValue(new Tuple<string, int>(type.ToLower(), int.Parse(version)), out objectRequestType);

							// Spoof the format to be application/xml or application/json, so that the .NET serialiser
							// deserialises from the appropriate format.
							requestHeaders.Remove("Content-Type");
							requestHeaders.Add("Content-Type", "application/" + format);
						}
					}
				}
			}
		}

		return new OurObjectTypes(objectRequestType, objectResponseType);
	}
}

 

Note that we intentionally modify the incoming Accept and Content-Type formats back to plain application/xml or application/json. This is so that we can rely upon the default .Net serialisation to format as xml or json accordingly. This feels a bit like a hack...

Anyway. Continuing on, the HttpOperationHandler returns a class called OurObjectTypes, which contains the .Net types of our custom objects. This follows an interface like:

 

/// <summary>
/// A representation of the media types which the client has requested in the "Accept" and/or "Content-Type" headers.
/// </summary>
public interface IOurObjectTypes
{
	/// <summary>
	/// The "Accept" object format which the client requires as a response.
	/// </summary>
	Type Response { get; }

	/// <summary>
	/// True if a Response type has been specified.
	/// </summary>
	bool HasResponseType { get; }

	/// <summary>
	/// The "Content-Type" object format. May be null, e.g. if this is a GET operation and therefore has no content.
	/// </summary>
	Type Request { get; }

	/// <summary>
	/// True if a Request type has been specified.
	/// </summary>
	bool HasRequestType { get; }

	/// <summary>
	/// True if both the Response and Request types are equal to this.
	/// </summary>
	/// <param name="type">An object type</param>
	bool AreBothTypes(Type type);
}

Finally, we make use of the IOurObjectTypes within our implementation of the API method, e.g.

public HttpResponseMessage Get(IOurObjectTypes mediaTypes)
{
	OurDomainObject domainObject = [fetch from domain layer];

	if (mediaTypes.Response == typeof(OurWcfObject_V1))
	{
		OurWcfObject_V1 result = new OurWcfObject_V1(domainObject);
		return new HttpResponseMessage<OurWcfObject_V1>(result);
	}
	else if (mediaTypes.Response == typeof(OurWcfObject_V2))
	{
		OurWcfObject_V2 result = new OurWcfObject_V2(domainObject);

		return new HttpResponseMessage<OurWcfObject_V2>(result);	
	}

	return ResponseMessages.InvalidMediaType();
}


I know there are a few shortcomings and inelegancies to this approach:

  • It doesn't take advantage of the media type formatter system. This is partly deliberate: we make use of OData filters, and if we always returned our domain object in the HttpResponseMessage, then the inbuilt OData handler would only know about the property names on our internal domain object. It would not see the property names on the _V1, _V2 etc WCF wrapper objects. I might launch a separate thread about this, as I don't know the best approach.
  • In order to keep the client happy, we should really adjust the Content-Type on the response message back to application/x.ourcompany...v1+xml etc, after the .Net serialiser has outputted the appropriate xml/json data. I guess this would be done using an HttpResponseHandler. Again, using a proper media type formatter would presumably also fix this.
  • It doesn't take into account q= etc for the client indicating a media type preference.
  • Probably more failures I've not thought about...

 

Hopefully that helps the discussion a bit. Like I say, I'm still wrangling over how to handle OData across different versions. Currently we're parsing the OData filter ourselves via another HttpOperationHandler.

Thanks,

Andrew

Jan 9, 2012 at 2:23 PM
Edited Jan 9, 2012 at 2:23 PM

Siggi,

>> The incoming request has an accept header that specifies the resource type and version being requested, as well as the format the client wants it in.

I think you're making the Accept header do an awful lot of work here, and this is leading to the complexity.  I find this ironic because I've gone the other way and pushed all of these decisions onto the URI.  I don't use the Accept header.

For selecting "resource type" / "API method" (if I understand you correctly) I would definitely use the URI.  That is: if by these terms you mean a resource, then definitely use a different URI for each resource; that's what the URI is for.

For version, I also recommend the URI.  See recipe 13.7 in Subbu's RESTful Cookbook.

Now for format, and this is where it gets interesting.  I have a service with a single, entry-point URI; all the other URIs are service generated.  Each resource has an XML- and a JSON- based representation, identified by media types that are private to the service.  The requested format is identified by "format=xml | json" in the query string; I don't use the Accept header at all.  Why?  I figure that if a client enters the service requesting a particular format, then they want to stick with that format for the duration.  I doubt a client will want to switch from XML to JSON half-way through.  So to support this, the client has to specify format=X on the initial request (default is XML, but I might switch it to JSON in order to better support code-on-demand via JavaScript) and from then on all outgoing service-generated URIs explicitly include this format selection query string.  In other words, the client doesn't have to set an Accept header on each and every request; why should it?

So: in WCF Web API terms my XML-based media type formatters always emit URIs with format=xml, and my JSON-based media type formatters always emit URIs with format=json.  Processing on the client and the service is greatly reduced.  If a client does want to flip from XML to JSON (or vice versa) then they can take a service-gen'd URI and modify the format=X part (that part of the identifier isn't opaque, by design).

Another thing that bothers me about the Accept header: if a resource returns a JSON representation in "application/vnd.something.very.specific+json" then should the Accept header be set to that media type in order to get it?  If so, then that's effort.  Or is "application/json" good enough?  If so then it doesn't seem quite right: bit of a mismatch there.  I find that using format=xml | json makes this sort of problem go away.

Jan 9, 2012 at 2:48 PM
Edited Jan 9, 2012 at 3:16 PM

Thanks for your reply Andrew (Riscy that is), it's good to know there are more people out there considering similar approaches :)
OData is not a concern in my case though, so I guess I can simplify some things.

I'm not very fond of having one method with branches for the different resource versions, I would prefer to have different methods for each.  I know this should be possible by writing an operation selector, but I haven't looked into it yet.

I did however find a way to leverage the built-in formatters to correctly serialize and mark a response based on the value in the Accept header, and at the same time discovered something about how the formatters work.
I used similar reflection logic as you have to discover methods with the attributes, but I made a complete list of all content type strings supported in the system, and then added them to the json and xml formatters (with a +json or +xml suffix).

This will do two things:
 * Incoming requests with Accept headers containing custom types are correctly routed to the json or xml formatters.
 * Responses will be serialized correctly and keep their custom Content-Type when overwritten in a handler.  Earlier I discovered that if you set the Content-Type of a response, the formatter would always overwrite it with something like application/json, but if the value is in the SupportedMediaTypes collection it leaves it alone (and automatically knows which formatter to use on the response).

So in my config class I did this:

// Formatters
var xmlFormatter = new XmlMediaTypeFormatter();
xmlFormatter.SupportedMediaTypes.Clear();
RestConfigSetup.AddMediaTypes("xml", xmlFormatter.SupportedMediaTypes);
xmlFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/xml") { CharSet = "utf-8" });
xmlFormatter.MediaTypeMappings.Clear();
 
var jsonFormatter = new JsonMediaTypeFormatter();
jsonFormatter.SupportedMediaTypes.Clear();
RestConfigSetup.AddMediaTypes("json", jsonFormatter.SupportedMediaTypes);
jsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" });
jsonFormatter.MediaTypeMappings.Clear();
jsonFormatter.AddMediaRangeMapping("*/*""application/json");
 
config.Formatters.Clear();
config.Formatters.AddRange(jsonFormatter); // JSON is default
config.Formatters.AddRange(xmlFormatter);


As you can see I want the JSON formatter to be used when the Accept header is empty or */*.
I then created a helper method to reflect the classes for the attribute and collect (and cache in a static member) all the different media types used.  These are then put into the SupportedMediaTypes collections on each formatter with the +json and +xml suffixes.

And finally I have a response handler that sets the Content-Type of the response to the type marked with the attribute.
I have to clean up this code a bit and it only supports json or xml atm, but it works ;)


protected override HttpResponseMessage OnHandle(HttpResponseMessage input)
{
    if (input == null)
    {
        throw new ArgumentNullException("input");
    }
 
    // Find the data from the target method's attribute
    ResourceTypeAttribute attr = this.operationDescription.Attributes.Where(t => t.GetType() == typeof(ResourceTypeAttribute)).FirstOrDefault() as ResourceTypeAttribute;
 
    if (attr != null)
    {
        // Add the last part based on formatter used on content (+json, +xml etc)
        string formatSuffix = "+json";  // The default
        foreach (MediaTypeWithQualityHeaderValue acceptHeader in input.RequestMessage.Headers.Accept)
        {
            if (acceptHeader.MediaType == "application/xml")
            {
                formatSuffix = "+xml";
                break;
            }
        }
 
        input.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(attr.ContentType + formatSuffix);
    }
 
    return input;
}


I then hook up the response handler like this to get the operation description in there:

config.ResponseHandlers = (handlers, endpoint, operation) =>
{
    // Hook up the ContentType handler (it needs the operation description)
    var attr = operation.Attributes.Where(t => t.GetType() == typeof(ResourceTypeAttribute)).FirstOrDefault();
 
    if (attr != null)
    {
        handlers.Add(new ContentTypeHandler(operation));
    }



So I'm not yet supporting:
* Casting a HTTP 415 when clients asks for an unsupported media.
* Selecting methods based on version
* Respecting the quality parameter in the Accept headers when client sends multiple formats.

I'm also a bit hesitant to writing the rest of the code.. based on what I'm reading here and on Twitter, there are some huge changes coming to the Web API framework (merge with MVC and all that).. so I kind of expect some major changes to the architecture of the framework. So for now I just wrote what I need right now, and plan to add proper versioning support later on.

Jan 9, 2012 at 3:16 PM
Edited Jan 9, 2012 at 3:17 PM

>> I'm not very fond of having one method with branches for the different resource versions, I would prefer to have different methods for each.  I know this should be possible by writing an operation selector, but I haven't looked into it yet.

I would say: have different methods for each, and let different URIs select them.

 

>> I did however find a way to leverage the built-in formatters to correctly serialize and mark a response based on the value in the Accept header

I avoid the built-in formatters, and have my own media type formatters, one for each of my service's resources.  Each formatter can output the resource as XML and/or JSON, using LINQ to XML and LINQ to JSON (via JSON.NET).  No reflection required.  My service is read-only, so I only have to worry about output.

Jan 9, 2012 at 3:41 PM

Andrew (Webb), I do agree that using the URL would simplify things a LOT :) However based on the REST texts I have been reading this breaks a few "rules".. You are essentially exposing the same resource in multiple places, but from what I've learned a resource should have one unique URL.  It can be argued that a different version of a resource is a different resource altogether :)
One of the reasons this is bad can be HTTP/proxy caching, if only one representation is invalidated but the other is not, you'll have (http) cache desync.  This can happen with the versioning approach too, but is more of a problem if you have different URLs per format, no?

I see both pros and cons to using the Accept header to control the version (and format).  You stick with a single URL, and when you stop supporting a media type a URL suddently doesn't start to return a 404, but something like a 415.
On the other end this can complicate clients quite a bit.. if a client is not adding proper Accept headers they will suddenly start receiving the latest version of a resource once compatibility has to be broken, and that WILL break that client.  But when using URLs the client is more "hard coded" to use a single version, and that same URI won't suddently start sending different data (but it might at some point give a 404).
I had planned to return the latest version of a resource if the Accept header was empty, */* or application/json (or xml).. but that will also break the client when the API evolves.
Educating people on how to write clients against this API is something I have been worrying about, the "lazy programmer" will just skip the Accept header and not worry until his app suddenly starts breaking.  So yes, like you mention, it is an effort.

Interesting approach you take there with using hypermedia links to stick to one format, that kind of bypasses the need for a server side session for this method :)

You have given me some food for thought, I've had a feeling for some time that this added complexity isn't worth it. I've been spending quite a lot of time on this that could have been used to implement features.


Just curious, how are you handling versioning when generating hypermedia links between resources? If you need to break compatibility in a resource that's being linked in other resources, do you also change their version/URL?  That is one thing the Accept header does better, the client could have a map of which versions of which resources it supports.. so when it sees a link to resource X it knows it supports v2.  But again, even more effort for the person implementing the client.

Jan 9, 2012 at 4:14 PM

Siggi,

>> However based on the REST texts I have been reading this breaks a few "rules".. You are essentially exposing the same resource in multiple places, but from what I've learned a resource should have one unique URL.  It can be argued that a different version of a resource is a different resource altogether

Have you got the RESTful Web Services Cookbook?  This is a book that (literally) never leaves my side.  Once you 'get' REST, this is the practical book to have to guide you through the design and implementation of your service.  Re. resources and versioning and URIs, see recipe 13.7 "How to Version RESTful Web Services".  Basically the man says: use URIs.

 

>> when you stop supporting a media type

I think I would avoid doing such a thing.  But if I did so, I would simply have my updated service emit URIs to resources that were based on other media types.  But see Mike Amundsen's hypermedia book on keeping media types static, but nevertheless allowing their schemas to evolve.  He credits Jan Algermissen with this idea, and gives HTML as an example.

 

>> Educating people on how to write clients against this API is something I have been worrying about, the "lazy programmer" will just skip the Accept header and not worry until his app suddenly starts breaking.  So yes, like you mention, it is an effort.

This is a big concern IMO.  Hence my recent post to leverage REST code-on-demand to make it easier for clients to interpret media types in the service-approved way.

 

>> Interesting approach you take there with using hypermedia links to stick to one format

I honestly don't see any one client being interested in more than one format.  For me it comes down to a binary choice: do you want the XML flavour or the JSON flavour of my media types?  And I'm half tempted to follow Sun Cloud API and just support JSON and be done with it.

 

>> I've had a feeling for some time that this added complexity isn't worth it.

That's my main point.  When complexity mounts up, re-evaluate.  I think you should push more on to your URIs.

 

>> how are you handling versioning when generating hypermedia links between resources

I don't have different versions yet.  But I will follow Subbu's RESTful cookbook recipes for sure when the time comes.

 

>> But again, even more effort for the person implementing the client

The most important thing for me is to make client implementation a) as correct as possible, b) as simple as possible.  Given that any one REST service has N clients, and you want N to be as high as possible, this becomes a priority.  Hence my idea re. downloadable code-on-demand "media type processor" scripts that run on the client.

 

Jan 9, 2012 at 5:13 PM

I believe I have skimmed that book yes, but even though that guys says "use URLs" many other authors/bloggers say different things, for example check the link in my original post.  This is one of the problems with REST web services, it's still new-ish and there are a lot of different opinions on different methods.. and then there are purists etc :)
At this point I'm starting to question things a lot more, and want to know a good practical reason to do something.. not "just because" or adhere to a purist goal.
My knowledge is mostly from the "Restful Web Services" book (read cover to cover), blogs/online resources and talking to colleagues.  I planned to check out the hypermedia book though (its the html5 one right?), and will give the cookbook another go :)

I'm not that familiar with the code-on-demand method you describe, but I could see that as a problem in my environment where we use .NET, Python and more.. everyone would have to find a way to execute that javascript and somehow get the results? I'd also worry about this being a potential security hole, array exploits in JSON are bad enough :)
But isn't what you're describing just something that was previously solved with schemas or a description language (WSDL and such)?

Jan 9, 2012 at 5:32 PM

Hi there Andrew(webb)..!

There are certainly two camps on versioning: URI vs Media Type, looking around various blogs. We initially used URIs with version numbers. We desperately wanted it to work, because custom Media Types are more complicated for the clients.

Where it fell down was when we started conforming to HATEOAS, with discoverable links between resources, instead of using a published API with hard-coded endpoints. The benefits of this are discussed in many places, but primarily it gives flexibility for future changes without breaking the URI structure.

So if you're not bothered about following the HATEOS RESTful principles, then this is academic ;)

Consider a sales ordering system having APIs for "customers", "orders", and "products"

/customers/v1/...

/orders/v1/...

/products/v1/...

When a client retrieves a Customer from /customers/v1/example, this contains the links to the Orders for that customer, and also all the Product information.

<customer id="example">
 <orderlist>
  <link>http://.../orders/v1/123456</link>
  <link>http://.../orders/v1/789012</link>
 </orderlist>
 <productlist>
  <link>http://.../products/v1/ABC</link>
  <link>http://.../products/v1/XYZ</link>
 </productlist>
</customers>

Now let's say that time passes, and we introduce new features. Client A wanted some extra features in Order; Client B wanted some extra features in Product. So we now have endpoints like:

/customers/v1/...

/orders/v1/...

/orders/v2/...

/products/v1/...

/products/v2/...

Client A loves the new features in Order V2, but isn't bothered about Product V2 and refuses to budge from Product V1.

Client B (you guessed it) loves the features in Product V2, but isn't bothered about Order V2 and refuses to budge from Order V1.

If you think about it, there's no way to cater for this using URI versioning, when following a purely HATEOAS approach from the initial Customer V1. It's Hobson's choice between:

  • Satisfy Client A, by having Customer V1 contain links for Order V2, Product V1
  • Satisfy Client B, by having Customer V1 contain links for Order V1, Product V2

...or you need to have new versions of customer which have those combinations of version links..! Not being flippant, but you could end up with something like:

/customers/v1/... (for the legacy clients, which you can't break)

/customers/v2order-with-v1product/... (for Client A)

/customers/v1order-with-v2product/... (for Client B)

This problem only gets magnified as the scale of the REST API increases. We already have a REST API grouped into about 6 services, all accessed via an initial "cool API". Bringing in multiple versions, in every combination, would obviously not be plausible.

The solution, as many blogs and references suggest, is to remove the version from the URI and allow the client to specify which version they expect. The Xml for Customer ends up like:

<customer id="example">
 <orderlist>
  <link suggestedtype="order-v2">http://.../orders/123456</link>
  <link suggestedtype="order-v2">http://.../orders/789012</link>
 </orderlist>
 <productlist>
  <link suggestedtype="product-v2">http://.../products/ABC</link>
  <link suggestedtype="product-v1">http://.../products/XYZ</link>
 </productlist>
</customers>

I used "suggestedtype" as an example; convention seems to suggest using the Atom spec. But clients are free to ignore the suggested type, and request their own preferred ones.

Like I say, we initially went down the route of version number in the URI; but fortunately we mapped out all the services before deployment and realised that there were many pitfalls for future breaking changes. So we spend a lot of time re-working it to remove the versions from the URI, and use Media Types instead.

It's all interesting stuff, anyway :)

Andrew

Jan 9, 2012 at 5:47 PM

Thank you "Riscy", this was the reason I was asking him about versioning links between resources.. you just explained it much much better :)

What is your experience with added client complexity though? I worry about that and having to educate users of a public API on how to "properly" write clients for it, that can evolve with an API without breaking.

Jan 9, 2012 at 6:03 PM

>> many other authors/bloggers say different things

Ha!  Yes, indeed; this is a problem!  I find that Subbu is a reliable guide though, and says things that are in accordance with my thinking on these matters.

 

>> I'm starting to question things a lot more, and want to know a good practical reason to do something.. not "just because" or adhere to a purist goal

Absolutely right!  Good for you!  A lot of this stuff can be done right by just keeping things simple, and using a lot of good old common sense.

 

>> I planned to check out the hypermedia book though (its the html5 one right?)

Yes.  Hypermedia APIs with HTML 5 and Node.  The Node angle is very interesting.  All the code samples are in JavaScript!  Regarding HTML 5, that's a bit of a title error IMO.  Mike shows examples of media types (data + server code + client code) based on XML, JSON and HTML 5.  There's a chapter devoted to each.

 

>> everyone would have to find a way to execute that javascript and somehow get the results

Yup, clients would have to execute JavaScript.  Yes, security is always a concern.  But, anyway, it's worth getting to know about REST's code-on-demand "constraint".

 

>> But isn't what you're describing just something that was previously solved with schemas or a description language (WSDL and such)?

Schema deals with syntax; and there's still scope for misinterpreting the semantics.  I want my service to do as much as possible to help devs write clients simply and correctly.  I float the idea that c-o-d could help here.

 

Jan 9, 2012 at 6:08 PM
Edited Jan 9, 2012 at 6:10 PM

@Riscy... I am totally concerned about HATEOAS.  Like I say, my service has a single entry-point URI, and hypermedia controls in outgoing resource representations with opaque URIs take it (i.e. resource linking and application flow) from there.  I'm too mentally tired to follow your scenario today, but will have a look tomorrow.  It's worth getting to the bottom of this thing.

Jan 10, 2012 at 7:55 AM

Riscy,

I have yet to extend or version my service and its resources, and so can't offer any hard-won advice.  However, it's been useful and interesting for me to consider such matters in this discussion thread.

 

>> Now let's say that time passes, and we introduce new features. Client A wanted some extra features in Order; Client B wanted some extra features in Product.

Mike Amundsen's hypermedia book has a very useful section on Extending and Versioning in chapter 5, and he makes a very clear distinction between the two (page 152+).  You introduced new features: could that have been achieved by extending and not versioning?  I.e. the meaning and processing of existing design elements are not changed, and the new design elements are designated as being optional.  Mike's main point: extend whenever possible, and version as a last resort.

Jan 10, 2012 at 9:04 AM

Extending is fine, that doesn't break compatibility.  But sometimes there is a case where you have to remove or change existing data, and that is a breaking change.  That is the whole reason I am planning for versioning :)

Jan 10, 2012 at 10:55 AM

Thanks for the discussion Andrewwebb & SiggiG, I've found it really interesting :)  I think the conclusion is that both approaches are valid, but are focussed around different problems.

  • URI versioning favours simpler client implementation, but doesn't necessarily answer the question of breaking changes.
  • Custom media types favour future proofing, at the expense of making life more difficult for clients.

@Siggi - our services will be used by a small number of our partner companies, meaning that we can do a lot of "hand holding" around added complexities like media types. Also, we're requiring API keys and message HMACs from client applications anyway; so if the developers can get their heads around that, then the media types shouldn't pose them much trouble. So far it's been very well received.

@andrewwebb - I'm not sure what direction we'll go in with future changes, which is why I'm paranoid about versioning! One of our partner companies will be using our services via traditional desktop applications, rather than the web - so once that is rolled out to their end-users, we really can't afford to break anything, as those end-users won't necessarily be taking updates.