Accessing optimistic concurrency features in Azure Mobile Services client SDKs


In my last post I talked about the new features in the tables inside Azure Mobile Services. One of those features is the ability to do conditional retrieval and updates of items, which can be used to implement optimistic concurrency in such tables. The scenario is that many clients can perform updates to the items in the server, but those updates will only work if the version the clients are updating actually match the version of the item in the server, to avoid a case where an update done by one client is lost because of an update done by a different client.

Another feature added to the tables were some new system properties, which provide useful information about the items (creation and update time, in addition to the version, used for optimistic concurrency). As those properties cannot be sent by the clients, and are only returned if explicitly asked for, they also require a special handling in the client.

At this moment, the managed (Windows Store / Windows Phone / Full .NET 4.5) version of the SDK has built-in support for those features, with the support for other platforms planned to arrive soon. But if you want to start using those features in the other platforms now, you can do so by using special filters, which is what I’ll show in this post (once the support in those platforms is there I’ll likely make another post with more information).

Quick recap

In order to request that the server send the system properties in its CRUD responses, the clients need to send the ‘__systemProperties’ query parameter, informing which of those special columns it wants to receive. So If a client is interested in the creation time and the version of the items it receives in a query, it can send ‘__systemProperties=createdAt,version’ in its request, like the following example (some headers omitted for brevity):

GET /tables/Person?__systemproperties=__createdAt%2C__version HTTP/1.1
X-ZUMO-APPLICATION: your-app-key
Accept: application/json
Host: your-service.azure-mobile.net

And the server would respond with them (some headers omitted, JSON response pretty-printed):

HTTP/1.1 200 OK
Content-Length: 565
Content-Type: application/json
Server: Microsoft-IIS/8.0
x-zumo-version: Zumo.Main.0.1.6.4247.Runtime
Date: Thu, 28 Nov 2013 20:54:40 GMT

[{"id":"A3EDC179-462E-4266-8691-1A1610CAE121","__createdAt":"2013-11-28T20:54:13.604Z","__version":"AAAAAAAAB+c=","name":"John Doe","age":33},{"id":"AF4E2B0B-625B-44F3-876B-BB65BB40BD78","__createdAt":"2013-11-28T20:54:13.901Z","__version":"AAAAAAAAB+k=","name":"Jane Roe","age":32}]

That’s it for system properties – if the client requests them (like in the example above, ‘__createdAt’ and ‘__version’), the server will include them in the response; if the client doesn’t (in the example above, ‘__updatedAt’), the response will not have it.

Conditional retrieval is done by using the value of the ‘__version’ property and using it as the item ETag (it’s also returned as the ETag HTTP response header in insert and update operations, and when querying for single items), and using it in the ‘If-None-Match’ request header. For the items above, if the client wants to check whether there have been any changes:

GET /tables/Person/A3EDC179-462E-4266-8691-1A1610CAE121?__systemProperties=version HTTP/1.1
X-ZUMO-APPLICATION: your-app-key
Accept: application/json
If-None-Match: "AAAAAAAAB+c="
Host: your-service.azure-mobile.net

If the item had not been updated, the server will just return with a 304 response. Otherwise, it will return the item as it normally would.

Conditional updates is also done via the item version mapped to the ETag of the server resource. For all PATCH requests, if the client wants the server only to update the item if the version on the server is the same version on the client, then it needs to send the ‘If-Match’ request header with the version as the ETag:

PATCH /tables/Person/A3EDC179-462E-4266-8691-1A1610CAE121?__systemProperties=version HTTP/1.1
X-ZUMO-APPLICATION: your-app-key
Content-Type: application/json
If-Match: "AAAAAAAAB+c="
Host: your-service.azure-mobile.net
Content-Length: 54

{"age":34,"id":"A3EDC179-462E-4266-8691-1A1610CAE121"}

If the server version matched that version, the update will happen as expected. However, if the item in the server had been updated (and its version changed), the server would respond with a 412 (Precondition Failed) response, including the current version of the item in the server in its response body. With the item in the response of the failed update, the client can look at both versions (its own and the server one) and decide what to do – a few examples are ignoring the client changes and take the server version; override the server version (by resubmitting the PATCH request, but with the version from the server item) or applying some custom merge policy and retrying the update.

Now let’s see how to access those features from the client side. Since each platform has a different behavior, let’s look at each one individually.

Managed clients

The managed client currently has full support for the table system properties, and it will do the “right” thing when sending / receiving data to / from the service. As described in the previous post, the service will only return those properties if explicitly asked for. The support is different depending on whether the calls are done for typed tables (i.e., IMobileServiceTable<T>) or untyped tables (i.e., IMobileServiceTable), so let’s look at them separately.

Untyped tables

Untyped tables are those in which all operations are done with JSON objects (represented by the classes in the Newtonsoft.Json.Linq namespace, usually with the classes JToken, JObject, JArray and JValue. Those tables are represented by the interface IMobileServiceTable, and returned by calls to MobileServiceClient.GetTable(string)). With the latest update (NuGet 1.1.0) there’s a new property, ‘SystemProperties’, in which you can set to determine which values will be requested by all CRUD operations made in that object. For example, in the code below the data returned by the service will have, in addition to the “regular” columns stored in the ‘Person’ table, the values of the columns ‘__createdAt’ and ‘__version’ from the first 5 items in that table.

  1. var table = MobileService.GetTable("Person");
  2. table.SystemProperties = MobileServiceSystemProperties.CreatedAt | MobileServiceSystemProperties.Version;
  3. var data = await table.ReadAsync("$top=5");

Notice that by setting the ‘SystemProperties’ in the table object, it doesn’t only add the ‘__systemProperties’ parameter in read operations – it will add them in all of the table operations. This is important so that you can get those properties when creating or updating an item, for example. Take the code below:

  1. var table = MobileService.GetTable("Person");
  2. table.SystemProperties = MobileServiceSystemProperties.CreatedAt |
  3.     MobileServiceSystemProperties.Version;
  4. var item = new JObject(new JProperty("name", "John Doe"), new JProperty("age", 33));
  5. var inserted = (JObject)(await table.InsertAsync(item));
  6. inserted["age"] = 34;
  7. var updated = await table.UpdateAsync(inserted);
  8. AddToDebug("Updated: {0}", updated);

When executed, the insert (POST) call will add the query parameter, so that the response will have the requested values:

POST /tables/Person?__systemproperties=__createdAt%2C__version HTTP/1.1
X-ZUMO-APPLICATION: your-app-key
Accept: application/json
Content-Type: application/json; charset=utf-8
Host: your-service.azure-mobile.net
Content-Length: 28

{"name":"John Doe","age":33}

HTTP/1.1 201 Created
Content-Length: 140
Content-Type: application/json
ETag: "AAAAAAAAB/U="
Location:
https://your-service.azure-mobile.net/tables/Person/6DD0F0A1-EF4E-4F40-A03C-91C7F528F57E
Server: Microsoft-IIS/8.0
x-zumo-version: Zumo.Main.0.1.6.4247.Runtime
Date: Fri, 29 Nov 2013 15:38:35 GMT

{"name":"John Doe","age":33,"id":"6DD0F0A1-EF4E-4F40-A03C-91C7F528F57E","__createdAt":"2013-11-29T15:38:35.618Z","__version":"AAAAAAAAB/U="}

Notice that the inserted object version is also mapped to the ETag header – we’re being good HTTP citizens here, using the appropriate header to “tag” the resource which has just been created in the server.

The UpdateAsync call is more interesting. The object had both a ‘__createdAt’ and a ‘__version’ property, but those system properties cannot be send by the client – so the client SDK will actually remove those prior to sending the request over the wire. Also, since the object being updated has a ‘__version’ property, it will be “lifted” into the request’s If-Match header, thus making it a conditional update. The simple presence of that property enables the optimistic concurrency scenario, as can be seen below (the request / response for that call).

PATCH /tables/Person/6DD0F0A1-EF4E-4F40-A03C-91C7F528F57E?__systemproperties=__createdAt%2C__version HTTP/1.1
If-Match: "AAAAAAAAB/U="
X-ZUMO-APPLICATION: your-app-key
Accept: application/json
Content-Type: application/json; charset=utf-8
Host: your-service.azure-mobile.net
Content-Length: 72

{"name":"John Doe","age":34,"id":"6DD0F0A1-EF4E-4F40-A03C-91C7F528F57E"}

HTTP/1.1 200 OK
Content-Length: 140
Content-Type: application/json
ETag: "AAAAAAAAB/c="
Server: Microsoft-IIS/8.0
x-zumo-version: Zumo.Main.0.1.6.4247.Runtime
Date: Fri, 29 Nov 2013 15:38:35 GMT

{"name":"John Doe","age":34,"id":"6DD0F0A1-EF4E-4F40-A03C-91C7F528F57E","__version":"AAAAAAAAB/c=","__createdAt":"2013-11-29T15:38:35.618Z"}

So in the success case it works as expected. But what if another client had updated the same item? In this case, the version we have in the client would be incorrect, and the server would return a 412 (Precondition Failed) response. The client SDK actually catches that response, and throws a special exception (MobileServicePreconditionFailedException, a subclass of MobileServiceInvalidOperationException), which contains the server item returned by the server. The client can then retrieve that item, and decide what to do (i.e., its merge policy).

  1. var item = new JObject(new JProperty("name", "John Doe"), new JProperty("age", 33));
  2. var inserted = (JObject)(await table.InsertAsync(item));
  3. inserted["age"] = 34;
  4. try
  5. {
  6.     // Invalid version, simulate another client making an update prior to this one
  7.     inserted["__version"] = "incorrect";
  8.     var updated = await table.UpdateAsync(inserted);
  9.     AddToDebug("Updated: {0}", updated);
  10. }
  11. catch (MobileServicePreconditionFailedException ex)
  12. {
  13.     var serverVersion = ex.Value;
  14.     AddToDebug("The value of the item in the server: {0}", serverVersion);
  15. }

Typed tables

Those are the types of tables which are bound to a specific user type. For example, the code below defines a table which can only be used to work (insert / query / update / delete) objects of type Person1:

  1. var table = MobileService.GetTable<Person>();
  2. var person = new Person { Name = "John Doe", Age = 33 };
  3. await table.InsertAsync(person);

To access the system properties in those tables, you can also use the ‘SystemProperties’ property in the table object. However, since in this case the SDK knows which objects the requests will be bound to, it can infer the properties you want based on the type itself. As long as the type has properties which map to the system columns, the ‘SystemProperties’ property will be set with the appropriate value. The can be accomplished in two ways. The first is by naming the property the name of the system column (‘__createdAt’, ‘__updatedAt’, ‘__version’) either directly or using the [JsonProperty] annotation, as shown below (created / updated using the attribute, version defined directly).

  1. public class Person
  2. {
  3.     public string Id { get; set; }
  4.     [JsonProperty("name")]
  5.     public string Name { get; set; }
  6.     [JsonProperty("age")]
  7.     public int Age { get; set; }
  8.     [JsonProperty("__createdAt")]
  9.     public DateTime CreatedAt { get; set; }
  10.     [JsonProperty("__updatedAt")]
  11.     public DateTime UpdatedAt { get; set; }
  12.     public string __version { get; set; }
  13. }

The second option is to use the special attributes (introduced in the version 1.1.0 of the NuGet package for the Mobile Services SDK) that declare that those fields will be mapped to the corresponding special property. Those attributes are  [CreatedAt], [UpdatedAt] and [Version], all from the Microsoft.WindowsAzure.MobileServices namespace (as of the time of this post, the documentation has yet to be updated to account for those attributes). This is the same class, defined with those attributes:

  1. public class Person
  2. {
  3.     public string Id { get; set; }
  4.     [JsonProperty("name")]
  5.     public string Name { get; set; }
  6.     [JsonProperty("age")]
  7.     public int Age { get; set; }
  8.     [CreatedAt]
  9.     public DateTime CreatedAt { get; set; }
  10.     [UpdatedAt]
  11.     public DateTime UpdatedAt { get; set; }
  12.     [Version]
  13.     public string Version { get; set; }
  14. }

When a table has the items shown above, you don’t need to set the value of the ‘SystemProperties’ member in the table, as it will be done when the table object is being created by the MobileServiceClient.GetTable<T> call. Assuming that the type Person is defined as above, the code below will print the id of the newly inserted object and the creation time (based on the server clock), and the updated time will show (roughly) 5 seconds after the creation time after the call to UpdateAsync.

  1. var table = MobileService.GetTable<Person>();
  2. var item = new Person { Name = "John Doe", Age = 33 };
  3. await table.InsertAsync(item);
  4. AddToDebug("Person {0} was created at {1}", item.Id, item.CreatedAt);
  5. await Task.Delay(5000);
  6. item.Age = 34;
  7. await table.UpdateAsync(item);
  8. AddToDebug("Person was last updated at {0}", item.UpdatedAt);

Like in the case with untyped tables, the SDK will map the ETag header in the server responses to the property corresponding to the ‘__version’ column in the client – and on update requests, that property will be mapped to the If-Match HTTP header. The update call shown above will only succeed if no changes had been made to the item in the 5 seconds which elapsed between its creation time and the update call.

If the update call fail because of a version mismatch, the UpdateAsync operation will throw a MobileServicePreconditionFailedException<T> (subclass of the non-generic MobileServicePreconditionFailedException class), where T is the type bound to the table. In this case, the exception will have a property containing the (typed) item which reflects the value of the resource in the server, as can be seen below.

  1. var item = new Person { Name = "John Doe", Age = 33 };
  2. await table.InsertAsync(item);
  3. AddToDebug("Person {0} was created at {1}", item.Id, item.CreatedAt);
  4. try
  5. {
  6.     // Invalid version, simulate another client making an update prior to this one
  7.     item.Version = "invalid";
  8.     item.Age = 34;
  9.     await table.UpdateAsync(item);
  10. }
  11. catch (MobileServicePreconditionFailedException<Person> ex)
  12. {
  13.     Person serverItem = ex.Item;
  14.     AddToDebug("Update failed, server item: {0}-{1}", serverItem.Name, serverItem.Age);
  15. }

One last thing about typed tables and the new features: if you call the method IMobileServiceTable<T>.RefreshAsync in an object which has a ‘__version’ property, that call will be made as a condition GET: the __version property will be “lifted” into the If-None-Match request header, and the server will only return a response (instead of a 304 Not Modified) if the version in the table is different than the one passed in that header.

1 to be more precise, it can also use the untyped notation, by inserting / reading / etc. data of type JToken and its derived classes; but I mean that it cannot work with objects of type ‘Order’ or ‘Product’, for example.

JavaScript

If you want to access system properties and use optimistic concurrency in the managed client, it does almost all of it for you, by requesting the appropriate fields and mapping the version property to the proper HTTP ETag in request and responses. But for other platforms, this support isn’t there (at least not as of the writing of this post; it should come in the near future). So let’s see how we can use the filters to enable that support. First: JavaScript (both for WinStore applications and for HTML/JS apps)

If you take an existing code like the one below, it will run and the response will not have any of the system properties.

  1. var table = client.getTable('person');
  2. var item = { name: 'John Doe', age: 33 };
  3. table.insert(item).then(function (inserted) {
  4.     document.getElementById('result').innerText = 'Inserted: ' + JSON.stringify(inserted);
  5. }, function (err) {
  6.     document.getElementById('result').innerText = 'Error: ' + JSON.stringify(err);
  7. });

We can use the additional parameter to the ‘insert’ method, and that would actually work. In the code below the inserted item (passed to the success callback) will have not only the name, age and id properties, but also the ‘__createdAt’ and ‘__version’ ones as requested.

  1. var table = client.getTable('person');
  2. var item = { name: 'John Doe', age: 33 };
  3. table.insert(item, { __systemProperties: 'createdAt,version' }).then(function (inserted) {
  4.     document.getElementById('result').innerText = 'Inserted: ' + JSON.stringify(inserted);
  5. }, function (err) {
  6.     document.getElementById('result').innerText = 'Error: ' + JSON.stringify(err);
  7. });

That works for requesting the properties, but then you’ll need to pass those parameters in all requests for that table. A better alternative is to use a filter which will do it for all operations, like in the example below:

  1. function createSystemPropertiesFilter(properties) {
  2.     properties = properties || '*'; // all properties if none specified
  3.     return function (req, next, callback) {
  4.         var url = req.url;
  5.         url = addSystemProperties(url);
  6.         req.url = url;
  7.         next(req, callback);
  8.  
  9.         function addSystemProperties(url) {
  10.             var queryIndex = url.indexOf('?');
  11.             var query, urlNoQuery;
  12.             if (queryIndex >= 0) {
  13.                 urlNoQuery = url.substring(0, queryIndex);
  14.                 query = url.substring(queryIndex + 1) + '&';
  15.             } else {
  16.                 urlNoQuery = url;
  17.                 query = '';
  18.             }
  19.             query = query + '__systemProperties=' + properties;
  20.             return urlNoQuery + '?' + query;
  21.         }
  22.     };
  23. }
  24. client = client.withFilter(createSystemPropertiesFilter('createdAt,version'));
  25. var table = client.getTable('person');
  26. var item = { name: 'John Doe', age: 33 };
  27. table.insert(item).then(function (inserted) {
  28.     document.getElementById('result').innerText = 'Inserted: ' + JSON.stringify(inserted);
  29. }, function (err) {
  30.     document.getElementById('result').innerText = 'Error: ' + JSON.stringify(err);
  31. });

Now let’s try an update call. After we succeed inserting an item, we’ll try to update that item using the code below.

  1. client = client.withFilter(createSystemPropertiesFilter('createdAt,version'));
  2. var table = client.getTable('person');
  3. var item = { name: 'John Doe', age: 33 };
  4. table.insert(item).then(function (inserted) {
  5.     document.getElementById('result').innerText = 'Inserted: ' + JSON.stringify(inserted);
  6.     // Let's now update the item...
  7.     inserted.age = 34;
  8.     table.update(item).done(function (updated) {
  9.         document.getElementById('result').innerText += '\nUpdated: ' + JSON.stringify(updated);
  10.     }, function (err) {
  11.         document.getElementById('result').innerText += '\nError updating: ' + JSON.stringify(err);
  12.     });
  13. });

Unfortunately the update call fails with the message “Error: The property '__createdAt' can not be set. Properties that begin with a '__' are considered system properties.” We need then to update our filter to remove such properties on update calls (since they cannot be set from the client, as I mentioned before).

  1. function createSystemPropertiesFilter(properties) {
  2.     properties = properties || '*'; // all properties if none specified
  3.     return function (req, next, callback) {
  4.         var url = req.url;
  5.         url = addSystemProperties(url);
  6.         req.url = url;
  7.         removeSystemPropertiesFromBody(req);
  8.         next(req, callback);
  9.  
  10.         function removeSystemPropertiesFromBody(request) {
  11.             var method = request.type;
  12.             var data = request.data;
  13.             if (method === 'PATCH' || method === 'PUT') {
  14.                 var body = JSON.parse(data);
  15.                 if (typeof body === 'object') {
  16.                     var toRemove = [];
  17.                     for (var k in body) {
  18.                         if (k.indexOf('__') === 0) {
  19.                             toRemove.push(k);
  20.                         }
  21.                     }
  22.  
  23.                     if (toRemove.length) {
  24.                         for (var i = 0; i < toRemove.length; i++) {
  25.                             delete body[toRemove[i]];
  26.                         }
  27.                         req.data = JSON.stringify(body);
  28.                     }
  29.                 }
  30.             }
  31.         }
  32.  
  33.         function addSystemProperties(url) {
  34.             // Same as previous...
  35.         }
  36.     };
  37. }

Now the insert-then-update code which failed before works fine, and if all you want is to access system properties then we’re done. But let’s complete the picture – lifting the __version property into the If-Match request header for update calls. This can be done by changing the ‘removeSystemPropertiesFromBody’ function. Below is the full code for the filter-generating function, which can be used for optimistic concurrency scenarios.

  1. function createSystemPropertiesFilter(properties) {
  2.     properties = properties || '*'; // all properties if none specified
  3.     return function (req, next, callback) {
  4.         var url = req.url;
  5.         url = addSystemProperties(url);
  6.         req.url = url;
  7.         removeSystemPropertiesFromBody(req);
  8.         next(req, callback);
  9.  
  10.         function removeSystemPropertiesFromBody(request) {
  11.             var method = request.type;
  12.             var data = request.data;
  13.             if (method === 'PATCH' || method === 'PUT') {
  14.                 var body = JSON.parse(data);
  15.                 if (typeof body === 'object') {
  16.                     var toRemove = [];
  17.                     for (var k in body) {
  18.                         if (k.indexOf('__') === 0) {
  19.                             toRemove.push(k);
  20.                             if (k === '__version') {
  21.                                 var etag = '\"' + body[k] + '\"';
  22.                                 request.headers['If-Match'] = etag;
  23.                             }
  24.                         }
  25.                     }
  26.  
  27.                     if (toRemove.length) {
  28.                         for (var i = 0; i < toRemove.length; i++) {
  29.                             delete body[toRemove[i]];
  30.                         }
  31.                         req.data = JSON.stringify(body);
  32.                     }
  33.                 }
  34.             }
  35.         }
  36.  
  37.         function addSystemProperties(url) {
  38.             var queryIndex = url.indexOf('?');
  39.             var query, urlNoQuery;
  40.             if (queryIndex >= 0) {
  41.                 urlNoQuery = url.substring(0, queryIndex);
  42.                 query = url.substring(queryIndex + 1) + '&';
  43.             } else {
  44.                 urlNoQuery = url;
  45.                 query = '';
  46.             }
  47.             query = query + '__systemProperties=' + properties;
  48.             return urlNoQuery + '?' + query;
  49.         }
  50.     };
  51. }

With this filter you should be able to get your feet wet with the new features in JavaScript while the official support doesn’t arrive.

iOS

iOS is fairly similar to JavaScript in a way that both languages only deal with “untyped” data (objects, arrays and primitives on JavaScript; NSDictionary, NSArray, NSNumber, NSString, NSDate and NSNull in iOS), so the filter implementation is pretty much the same, translated into Objective-C. Without further ado, this is a filter which can be used in iOS to consume system properties and perform conditional updates.

@interface SystemPropertiesFilter : NSObject<MSFilter>
{
    NSString *_properties;
}
@end

@implementation SystemPropertiesFilter

- (id)init {
    self = [super init];
    if (self) {
         // Default to all properties
        self->_properties = @"*";
    }
    return self;
}

- (id)initWithProperties:(NSString *)properties {
    self = [super init];
    if (self) {
        self->_properties = properties;
    }
    return self;
}

- (void)handleRequest:(NSURLRequest *)request next:(MSFilterNextBlock)onNext response:(MSFilterResponseBlock)onResponse {
    // Append __systemProperties to the query string
    NSString *url = [[request URL] absoluteString];
    NSString *query, *urlWithoutQuery;
    NSRange indexOfQuery = [url rangeOfString:@"?"];
    if (indexOfQuery.location == NSNotFound) {
        query = @"";
         urlWithoutQuery = url;
    } else {
        query = [url substringFromIndex:(indexOfQuery.location + 1)];
        query = [query stringByAppendingString:@"&"];
        urlWithoutQuery = [url substringToIndex:indexOfQuery.location];
    }
    urlWithoutQuery = [urlWithoutQuery stringByAppendingString:@"?"];
    query = [query stringByAppendingString:@"__systemProperties="];
    query = [query stringByAppendingString:self->_properties];
    url = [urlWithoutQuery stringByAppendingString:query];

    // Clone the request and headers
    NSMutableURLRequest *newRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
    NSString *HTTPMethod = [request HTTPMethod];
    [newRequest setHTTPMethod:HTTPMethod];
    NSDictionary *requestHeaders = [request allHTTPHeaderFields];
    for (NSString *headerName in [requestHeaders keyEnumerator]) {
        NSString *headerValue = [requestHeaders objectForKey:headerName];
        [newRequest setValue:headerValue forHTTPHeaderField:headerName];
    }
    [newRequest setHTTPBody:[request HTTPBody]];

    // Remove the system properties from the request
    if ([HTTPMethod isEqualToString:@"PUT"] || [HTTPMethod isEqualToString:@"PATCH"]) {
         NSError *error;
        NSData *requestBody = [request HTTPBody];
        id body = [NSJSONSerialization JSONObjectWithData:requestBody options:0 error:&error];
        if (error) {
             onResponse(nil, nil, error);
            return;
         }
        if ([body isKindOfClass:[NSDictionary class]]) {
             NSDictionary *item = body;
            NSString *version = nil;
            NSMutableDictionary *newItem = [[NSMutableDictionary alloc] init];
            BOOL propertyRemoved = NO;
             for (NSString *key in [item allKeys]) {
                 if ([key hasPrefix:@"__"]) {
                    propertyRemoved = YES;
                    if ([key isEqualToString:@"__version"]) {
                        version = [item objectForKey:key];
                    }
                } else {
                     [newItem setValue:[item objectForKey:key] forKey:key];
                 }
            }

            if (version) {
                 NSString *etag = [NSString stringWithFormat:@"\"%@\"", version];
                [newRequest setValue:etag forHTTPHeaderField:@"If-Match"];
            }

            if (propertyRemoved) {
                NSData *newBody = [NSJSONSerialization dataWithJSONObject:newItem options:0 error:&error];
                 if (error) {
                    onResponse(nil, nil, error);
                    return;
                }
                [newRequest setHTTPBody:newBody];
            }
        }
    }

    // Now send the updated request
    onNext(newRequest, onResponse);
}

@end

The filter can be used by calling the clientWithFilter: selector in the MSClient class to create a new client object which uses that filter.

MSClient *client = [MSClient clientWithApplicationURLString:@"https://my-service.azure-mobile.net/"
    applicationKey:@"my-application-key"];
SystemPropertiesFilter *filter = [[SystemPropertiesFilter alloc]
    initWithPropreties:@"createdAt,version"];

client = [client clientWithFilter:filter];
MSTable *table = [client tableWithName:@"Person"];
NSDictionary *item = @{@"name":@"John Doe",@"age":@33};
[table insert:item completion:^(NSDictionary *inserted, NSError *error) {       
    if (error) {
        NSLog(@"Error: %@", error);
        return;
    }
    NSLog(@"Inserted: %@", inserted);
    NSMutableDictionary *toUpdate = [[NSMutableDictionary alloc] initWithDictionary:inserted];
    [toUpdate setValue:@34 forKey:@"age"];
    [table update:toUpdate completion:^(NSDictionary *updated, NSError *error) {
        if (error) {
            NSLog(@"Error: %@", error);
            return;
        }
        NSLog(@"Updated: %@", updated);
    }];
}];

And that’s it for iOS.

Android 

Android is more similar to the managed SDK, since it has both a typed (using user-defined types) and untyped (using the com.google.gson types). For the untyped case, we’re at the same boat as both JavaScript and iOS, so we can define a filter with the same behavior (this time, in Java):

  1. class SystemPropertiesFilter implements ServiceFilter {
  2.  
  3.     private String systemProperties;
  4.     public SystemPropertiesFilter() {
  5.         this.systemProperties = "*";
  6.     }
  7.     public SystemPropertiesFilter(String properties) {
  8.         this.systemProperties = properties;
  9.     }
  10.  
  11.     @Override
  12.     public void handleRequest(ServiceFilterRequest req,
  13.             NextServiceFilterCallback next,
  14.             ServiceFilterResponseCallback callback) {
  15.         String url = req.getUrl();
  16.         url = addSystemPropertiesToQuery(url);
  17.         try {
  18.             req.setUrl(url);
  19.         } catch (URISyntaxException e) {
  20.             e.printStackTrace();
  21.         }
  22.             
  23.         removeSystemPropertiesFromBody(req);
  24.         next.onNext(req, callback);
  25.     }
  26.         
  27.     private void removeSystemPropertiesFromBody(ServiceFilterRequest req) {
  28.         String method = req.getMethod();
  29.         if (method.equalsIgnoreCase("PUT") || method.equalsIgnoreCase("PATCH")) {
  30.             // Remove system properties from update calls
  31.             String body = req.getContent();
  32.             JsonElement json = new JsonParser().parse(body);
  33.             if (json.isJsonObject()) {
  34.                 JsonObject obj = json.getAsJsonObject();
  35.                 List<String> toRemove = new ArrayList<String>();
  36.                 for (Entry<String, JsonElement> entry : obj.entrySet()) {
  37.                     String key = entry.getKey();
  38.                     if (key.startsWith("__")) {
  39.                         toRemove.add(key);
  40.                         if (key.equals("__version")) {
  41.                             String version = entry.getValue().getAsString();
  42.                             req.addHeader("If-Match", "\"" + version + "\"");
  43.                         }
  44.                     }
  45.                 }
  46.                 if (!toRemove.isEmpty()) {
  47.                     for (String prop : toRemove) {
  48.                         obj.remove(prop);
  49.                     }
  50.                     try {
  51.                         req.setContent(obj.toString());
  52.                     } catch (Exception e) {
  53.                         e.printStackTrace();
  54.                     }
  55.                 }
  56.             }
  57.         }
  58.     }
  59.  
  60.     private String addSystemPropertiesToQuery(String url) {
  61.         int queryIndex = url.indexOf('?');
  62.         String query;
  63.         String urlNoQuery;
  64.         if (queryIndex >= 0) {
  65.             urlNoQuery = url.substring(0, queryIndex);
  66.             query = url.substring(queryIndex + 1) + "&";
  67.         } else {
  68.             query = "";
  69.             urlNoQuery = url;
  70.         }
  71.         query = query + "__systemProperties=" + this.systemProperties;
  72.         return urlNoQuery + "?" + query;
  73.     }
  74. }

And we can then use it if we’re talking to an untyped table (i.e., one represented by the MobileServiceJsonTable class), we can use this filter:

  1. final MobileServiceClient spClient = mClient.withFilter(new SystemPropertiesFilter("createdAt,version"));
  2. final MobileServiceJsonTable table = spClient.getTable("person");
  3. JsonObject jo = new JsonObject();
  4. jo.addProperty("name", "John Doe");
  5. jo.addProperty("age", 33);
  6. table.insert(jo, new TableJsonOperationCallback() {
  7.  
  8.     @Override
  9.     public void onCompleted(JsonObject inserted, Exception error,
  10.             ServiceFilterResponse response) {
  11.         if (error != null) {
  12.             tv.setText("Error: " + error.toString());
  13.         } else {
  14.             tv.setText("Inserted: " + inserted.toString());
  15.             inserted.remove("age");
  16.             inserted.addProperty("age", 34);
  17.             table.update(inserted, new TableJsonOperationCallback() {
  18.                 @Override
  19.                 public void onCompleted(JsonObject updated,
  20.                         Exception error,
  21.                         ServiceFilterResponse response) {
  22.                     String text = (String) tv.getText();
  23.                     if (error != null) {
  24.                         text = text + "\nError: " + error.toString();
  25.                     } else {
  26.                         text = text + "\nUpdated: " + updated.toString();
  27.                     }
  28.                     tv.setText(text);
  29.                 }
  30.             });
  31.         }
  32.     }
  33. });

But what if we’re using typed tables (i.e., MobileServiceTable<E>)? Let’s try it. So let’s define our type with the properties we want:

  1. public class Person {
  2.     @SerializedName("id")
  3.     private String mId;
  4.     @SerializedName("name")
  5.     private String mName;
  6.     @SerializedName("age")
  7.     private int mAge;
  8.     @SerializedName("__createdAt")
  9.     private Date mCreatedAt;
  10.     @SerializedName("__version")
  11.     private String mVersion;
  12.     
  13.     public String getId() { return mId; }
  14.     public void setId(String id) { mId = id; }
  15.     public String getName() { return mName; }
  16.     public void setName(String name) { mName = name; }
  17.     public int getAge() { return mAge; }
  18.     public void setAge(int age) { mAge = age; }
  19.     public Date getCreatedAt() { return mCreatedAt; }
  20.     public String getVersion() { return mVersion; }
  21.     
  22.     @Override
  23.     public String toString() {
  24.         StringBuilder sb = new StringBuilder();
  25.         sb.append("Person[Name=" + this.getName());
  26.         sb.append(",Age=" + this.getAge());
  27.         sb.append(",CreatedAt=" + this.getCreatedAt());
  28.         sb.append(",Id=" + this.getId());
  29.         sb.append(",Version=" + this.getVersion() + "]");
  30.         return sb.toString();
  31.     }
  32. }

And now try to use it:

  1. final MobileServiceTable<Person> table = spClient.getTable(Person.class);
  2. Person p = new Person();
  3. p.setName("John Doe");
  4. p.setAge(33);
  5. table.insert(p, new TableOperationCallback<Person>() {
  6.  
  7.     @Override
  8.     public void onCompleted(Person inserted, Exception error,
  9.             ServiceFilterResponse response) {
  10.         if (error != null) {
  11.             tv.setText("Error: " + error.toString());
  12.             Throwable t = error.getCause();
  13.             while (t != null) {
  14.                 tv.setText((String)tv.getText() + "\n  Cause: " + t.toString());
  15.                 t = t.getCause();
  16.             }
  17.         } else {
  18.             tv.setText("Inserted: " + inserted.toString());
  19.             inserted.setAge(34);
  20.             table.update(inserted, new TableOperationCallback<Person>() {
  21.  
  22.                 @Override
  23.                 public void onCompleted(Person updated,
  24.                         Exception error,
  25.                         ServiceFilterResponse response) {
  26.                     String text = (String) tv.getText();
  27.                     if (error != null) {
  28.                         text = text + "\nError: " + error.toString();
  29.                     } else {
  30.                         text = text + "\nUpdated: " + updated.toString();
  31.                     }                                    
  32.                     tv.setText(text);
  33.                 }                                
  34.             });
  35.         }
  36.     }
  37. });

When we run it, we get an error, though. The server responds saying that the client cannot send an item with properties starting with ‘__’. The problem here is that in the insert operation, the value of the mCreatedAt and mVersion properties are being serialized (as ‘null’), and the server rejects it. Maybe there’s a way to change the Gson serializer to avoid that, but for now I’ll take the simple route and change the filter itself. It now needs to remove system properties from the request body on botu updates and insert operations. It also needs to account for possible null values in the ‘__version’ field (which is the case for inserts). Here’s the updated definition of the ‘removeSystemPropertiesFromBody’ method:

  1. private void removeSystemPropertiesFromBody(ServiceFilterRequest req) {
  2.     String method = req.getMethod();
  3.     if (method.equalsIgnoreCase("PUT") || method.equalsIgnoreCase("PATCH") || method.equalsIgnoreCase("POST")) {
  4.         // Remove system properties from insert / update calls
  5.         String body = req.getContent();
  6.         JsonElement json = new JsonParser().parse(body);
  7.         if (json.isJsonObject()) {
  8.             JsonObject obj = json.getAsJsonObject();
  9.             List<String> toRemove = new ArrayList<String>();
  10.             for (Entry<String, JsonElement> entry : obj.entrySet()) {
  11.                 String key = entry.getKey();
  12.                 if (key.startsWith("__")) {
  13.                     toRemove.add(key);
  14.                     if (key.equals("__version")) {
  15.                         JsonElement versionJson = entry.getValue();
  16.                         if (versionJson != null && versionJson.isJsonPrimitive()) {
  17.                             String version = versionJson.getAsString();
  18.                             if (version != null) {
  19.                                 req.addHeader("If-Match", "\"" + version + "\"");
  20.                             }
  21.                         }
  22.                     }
  23.                 }
  24.             }
  25.             if (!toRemove.isEmpty()) {
  26.                 for (String prop : toRemove) {
  27.                     obj.remove(prop);
  28.                 }
  29.                 try {
  30.                     req.setContent(obj.toString());
  31.                 } catch (Exception e) {
  32.                     e.printStackTrace();
  33.                 }
  34.             }
  35.         }
  36.     }
  37. }

After those changes, the code should run fine.

Wrapping up

Hopefully this post explained how you can use the new system properties / optimistic concurrency features for all supported client SDKs. For those where the official support isn’t there yet what I showed here is mostly a workaround until the “real” support arrives.

Comments (0)

Skip to main content