Complex types and Azure Mobile Services

About a year ago I posted a series of entries in this blog about supporting arbitrary types in Azure Mobile Services. Back then, the managed client SDK was using a custom serializer which only supported a very limited subset of simple types. To be use other types in the CRUD operations, one would need to decorate the types / properties with a special attribute, and implement an interface which was used to convert between those types to / from a JSON representation. That was cumbersome for two reasons – the first was that even for simple types, one would need to define a converter class; the other was that the JSON representation was different for the supported platforms (JSON.NET for Windows Phone; Windows.Data.Json classes for Windows Store) – and the OM for the WinStore JSON representation was, frankly, quite poor.

With the changes made on the client SDK prior to the general release, the SDK for all managed platforms started using a unified serializer – JSON.NET for all platforms (in the context of this post, I mean all platforms using managed code). It also started taking more advantage of the extensibility features of that serializer, so that whatever JSON.NET could do on its own the mobile services SDK itself wouldn’t need to do anything else. That by itself gave the mobile services SDK the ability to serialize, in all supported platforms all primitive types which JSON.NET supported natively, so there was no need to implement custom serialization for things such as TimeSpan, enumerations, Uri, among others which weren’t supported in the initial version of the SDK.

What that means is that, for simple types, all the code which I wrote on the first post with the JSON converter isn’t required anymore. You can still change how a simple type is serialized, though, if you really want to, by using a JsonConverter (and decorating the member with the JsonConverterAttribute). For complex types, however, there’s still some work which needs to be done – the serializer on the client will happily serialize the object with its complex members, but the runtime won’t know what to do with those until we tell it what it needs to do.

Let’s look at an example – my app adds and display movies, and each movie has some reviews associated with it.

  1. public class Movie
  2. {
  3.     [JsonProperty("id")]
  4.     public int Id { get; set; }
  5.     [JsonProperty("title")]
  6.     public string Title { get; set; }
  7.     [JsonProperty("year")]
  8.     public int ReleaseYear { get; set; }
  9.     [JsonProperty("reviews")]
  10.     public MovieReview[] Reviews { get; set; }
  11. }
  12.  
  13. public class MovieReview
  14. {
  15.     [JsonProperty("stars")]
  16.     public int Stars { get; set; }
  17.     [JsonProperty("comment")]
  18.     public string Comment { get; set; }
  19. }

Now let’s try to insert one movie into the server:

  1. try
  2. {
  3.     var movieToInsert = new Movie
  4.     {
  5.         Title = "Pulp Fiction",
  6.         ReleaseYear = 1994,
  7.         Reviews = new MovieReview[] { new MovieReview { Stars = 5, Comment = "Best Movie Ever!" } }
  8.     };
  9.     var table = MobileService.GetTable<Movie>();
  10.     await table.InsertAsync(movieToInsert);
  11.     this.AddToDebug("Inserted movie {0} with id = {1}", movieToInsert.Title, movieToInsert.Id);
  12. }
  13. catch (Exception ex)
  14. {
  15.     this.AddToDebug("Error: {0}", ex);
  16. }

The movie object is serialized without problems to the server, as can be seen in Fiddler (many headers omitted):

POST https://MY-SERVICE-NAME.azure-mobile.net/tables/Movie HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: MY-SERVICE-NAME.azure-mobile.net
Content-Length: 89
Connection: Keep-Alive

{"title":"Pulp Fiction","year":1994,"reviews":[{"stars":5,"comment":"Best Movie Ever!"}]}

The complex type was properly serialized as expected. But the server responds saying that it was a bad request:

HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Content-Length: 112
Content-Type: application/json
Server: Microsoft-IIS/8.0
Date: Thu, 22 Aug 2013 22:10:11 GMT

{"code":400,"error":"Error: The value of property 'reviews' is of type 'object' which is not a supported type."}

Since the runtime doesn’t know which column type to insert non-primitive types. So, as I mentioned on the original posts, there are two ways to solve this issue – make the data, on the client side, of a type which the runtime understands, or “teach” the runtime to understand non-primitive types, by defining scripts for the table operations. Let’s look at both alternatives.

Client-side data manipulation

For the client side, in the original post we converted the complex types into simple types by using a data member JSON converter. That interface doesn’t exist anymore, so we just use the converters from JSON.NET to do that. Below would be one possible implementation of such converter, which “flattens” the reviews array into a single string.

  1. public class Movie_ComplexClientSide
  2. {
  3.     [JsonProperty("id")]
  4.     public int Id { get; set; }
  5.     [JsonProperty("title")]
  6.     public string Title { get; set; }
  7.     [JsonProperty("year")]
  8.     public int ReleaseYear { get; set; }
  9.     [JsonProperty("reviews")]
  10.     [JsonConverter(typeof(ReviewArrayConverter))]
  11.     public MovieReview[] Reviews { get; set; }
  12. }
  13.  
  14. class ReviewArrayConverter : JsonConverter
  15. {
  16.     public override bool CanConvert(Type objectType)
  17.     {
  18.         return objectType == typeof(MovieReview[]);
  19.     }
  20.  
  21.     public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  22.     {
  23.         var reviewsAsString = serializer.Deserialize<string>(reader);
  24.         return reviewsAsString == null ?
  25.             null :
  26.             JsonConvert.DeserializeObject<MovieReview[]>(reviewsAsString);
  27.     }
  28.  
  29.     public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  30.     {
  31.         var reviewsAsString = JsonConvert.SerializeObject(value);
  32.         serializer.Serialize(writer, reviewsAsString);
  33.     }
  34. }

This approach has the advantage which it is fairly simple – the converter implementation, as seen above, is trivial, and there’s no need to change server scripts. However, this has the drawback that we’re essentially denormalizing the relationship between the movie and its comments. In this case, this actually shouldn’t be a big deal (since a review is inherently tied to a movie), but in other scenarios the loss of normalization may lead to other issue. For example, we can’t (easily) query the database for which movie has the most reviews, or which movie has more 5-star reviews. Also, if we want to use the same data in different platforms (such as JavaScript, Android, iOS, etc.) we’ll need to do this manipulation on those platforms as well.

Server-side data manipulation

Another alternative is to not do anything on the client, and deal with the complex data on the server-side itself. At the server-side, we have two more options – denormalize the data (using a similar technique as we did at the client side), or keep it normalized (in two different tables). The scripts at the second post of the original series can still be used for the denormalization technique, so I won’t repeat them here.

To keep the table normalized (i.e., to implement a 1:n relationship between the Movie and the new MovieReview tables) we need to add some scripts at the server side to deal with that data. The last post on the original series talked about that, but with a different scenario. For completeness sake, I’ll add the scripts for this scenario (movies / reviews) here as well.

First, inserting data. When the data arrives at the server, we first remove the complex type (which the runtime doesn’t know how to handle), and after inserting the movie, we iterate through the reviews to insert them with the associated movie id as the “foreign key”.

  1. function insert(item, user, request) {
  2.     var reviews = item.reviews;
  3.     if (reviews) {
  4.         delete item.reviews; // will add in the related table later
  5.     }
  6.  
  7.     request.execute({
  8.         success: function () {
  9.             var movieId = item.id;
  10.             var reviewsTable = tables.getTable('MovieReview');
  11.             if (reviews) {
  12.                 item.reviews = [];
  13.                 var insertNextReview = function (index) {
  14.                     if (index >= reviews.length) {
  15.                         // done inserting reviews, respond to client
  16.                         request.respond();
  17.                     } else {
  18.                         var review = reviews[index];
  19.                         review.movieId = movieId;
  20.                         reviewsTable.insert(review, {
  21.                             success: function () {
  22.                                 item.reviews.push(review);
  23.                                 insertNextReview(index + 1);
  24.                             }
  25.                         });
  26.                     }
  27.                 };
  28.  
  29.                 insertNextReview(0);
  30.             } else {
  31.                 // no need to do anythin else
  32.                 request.respond();
  33.             }
  34.         }
  35.     });
  36. }

Reading is similar – first read the movies themselves, then iterate through them and read their reviews from the associated table.

  1. function read(query, user, request) {
  2.     request.execute({
  3.         success: function (movies) {
  4.             var reviewsTable = tables.getTable('MovieReview');
  5.             var readReviewsForMovie = function (movieIndex) {
  6.                 if (movieIndex >= movies.length) {
  7.                     request.respond();
  8.                 } else {
  9.                     reviewsTable.where({ movieId: movies[movieIndex].id }).read({
  10.                         success: function (reviews) {
  11.                             movies[movieIndex].reviews = reviews;
  12.                             readReviewsForMovie(movieIndex + 1);
  13.                         }
  14.                     });
  15.                 }
  16.             };
  17.  
  18.             readReviewsForMovie(0);
  19.         }
  20.     });
  21. }

That’s it. I wanted to have an updated post so I could add a warning in the original ones that some of their content was out-of-date. The code for this project can be found in GitHub at https://github.com/carlosfigueira/blogsamples.