Merge vs. Replace Semantics for Update Operations
So far Astoria has used “merge” semantics for update. That is, an “update” operation (an HTTP PUT request) replaces the values of the properties for the target entity that are specified in the input payload; this applies both to properties and links. If a property or link is not present in a PUT operation, it means “leave it with its current value”. To null-out a property or link it has to be present in the PUT body and has to have a null marker (null attribute for properties, null href for links).
While supporting merge is important and will remain part of Astoria, there are scenarios where we need “replace” semantics. That is, an operation where the entity properties are entirely replaced by those in the request body, even if the request body is partial. In particular, the AtomPub protocol requires PUT to have replace semantics.
In a “replace” operation, each property in the target entity either takes the value specified in the payload (if any) or its default value. The meaning of “default value” for a “replace” operation is server implementation-specific. The Astoria server will likely use the CLR default values for each value type and null for references.
For operations on primitive values (e.g. PUT /Customers(123)/CompanyName), there is no practical difference between “merge” and “replace”.
A “replace” operation, just like a “merge” operation, cannot specify different values in the key properties. That is, keys remain non-updatable in “replace”. Similarly, a “replace” operation is subject to the same requirements from the ETags perspective as a “merge” operation.
About links inside entity payloads: “replace” operations replace the properties in entities themselves, not its links. If you include a link in a “merge” or “replace” operation we’ll wire it up, but if you don’t it’ll maintain the existing links.
About directly-addressed links: links are currently atomic values (just the link itself), so there is no difference between replacing it and merging it when operating on link resources (e.g. /$links/…). Astoria servers should support both operations but keep identical semantics. The rest of this discussion won’t touch on links as stand-alone resources anymore.
Note that these operational semantics are the same regardless of the actual format (Atom, JSON, etc.) used to represent the resources being exchanged.
Finally, this in general does not apply to service operations, as the meaning of service operations is service-defined. It’s interesting to think about whether in the Astoria server library we introduce some mechanism to allow it to expose a MERGE-enabled URL, but that would still require the user-implementation to make sense of it.
AtomPub specifically requires HTTP PUT to mean replace. So we adjusted the way Astoria interprets PUT to mean “replace”.
In order to request a “merge” operation we have two options:
1. Introduce a new HTTP method, “MERGE”, and rely on verb tunneling (POST + a header) for the cases where custom methods are not allowed. There has been talk about a PATCH verb in multiple circles including the AtomPub community, but it seems to be going in a somewhat different direction.
2. Introduce a new custom header, “DataServices-Merge” or something, that when set to “1” in a PUT request indicates that the server should merge the body with the server entity instead of replacing it.
While we’re not thrilled with the idea of introducing a new HTTP method, overloading PUT with an extra header seems to be very problematic. If anything else, a server that does not support “merge” through headers would see PUT as a regular “replace” request and perform an operation that’s not what the client expected. Also other things break. For example, if a server sees an actual MERGE request and cannot handle it then it can respond with 405 – method not supported.
So we’re leaning toward MERGE and tunneling (we already support tunneling for PUT/DELETE in Astoria servers and clients).
Responses to “merge” and “replace” requests are identical.
Using “merge” from the client has several advantages:
1. The server does not know what a client considers a “whole” entity. The client may be using entity types that contain a subset of the properties of the server-side version, either due to versioning mismatches or because the client is not interested in all of the properties.
2. The client is pretty much required to use “merge” to make links work. Since an entity might have been brought down to the client without its related entities expanded, one or more links won’t be present, and if we used replace we’d lose information on the server.
Based on the above, it would seem that the client should do “merge”, which will effectively result in “replacing the subset the client knows about” because the client always sends all the fields.
The problem now is servers that don’t implement “merge” operation. One option is to require “merge” for the client to work, but that leaves too many interesting scenarios out. Since we already had a SaveChangesOptions enum that’s used as an argument to SaveChanges/BeginSaveChanges, we introduced SaveChangeOptions.UpdateAsReplace to indicate that you want the client to use PUT.
The AJAX client’s DataService.update method can be extended with a new argument “UpdateAsReplace” that enables use of replace when set to true. By default we would continue to do “merge” (this requires tweaking the library because currently DataService.update generates a PUT request).
If the new boolean adds one too many arguments for update(), alternatively we could add a knob to the DataService class, we still made this change.
Astoria runtime-data source interaction
The only change in the interaction between Astoria and the data source should be that for the resource being replaced the system will call IUpdatable.ReplaceResource instead of IUpdatable.GetResource.
This post is part of the transparent design exercise in the Astoria Team. To understand how it works and how your feedback will be used please look at this post.