I'm trying to implement Json PATCH in a REST Api to update an Event Sourced Aggregate, but I'm struggling to do it right.
I don't want to create an Anemic Model, so my Aggregate Root properties are private. For that reason, I can't patch my Object using the ASP.NET Json PATCH library.
There's any technique to accomplish that or am I doing something wrong trying to apply a patch over an Event Source Aggregate?
Example: how would you Patch the InventoryItem in the Greg Young SimpleCQRS example?
Example: how would you Patch the InventoryItem in the Greg Young SimpleCQRS example?
You probably wouldn't. PATCH, like PUT, supports the semantics of an anemic data store. "Make your representation look like mine" means that you are taking control away from the remote domain model. You aren't supposed to be telling the domain model what state to be in next, you are supposed to tell it what to do.
That said, let's take a careful look at PATCH
The PATCH method requests that a set of changes described in the request entity be applied to the resource identified by the Request-URI. The set of changes is represented in a format called a "patch document" identified by a media type.
So the media type in this case would define the processing rules for a list of zero or more commands to be applied to the model. Conceptually, it's similar to JSON-Patch, in that the document describes a sequence of operations to be applied by the resource.
To be clear, JSON-Patch isn't the right media type to use; the semantics are wrong. So if somebody tried to JSON-Patch your InventoryItem then you should probably send back a 415 Unsupported Media Type and a stern note.
For instance, if you look at the HTTP documentation of Event Store, you'll see that writing to a stream uses a bespoke media type to describe the events: application/vnd.eventstore.events+json
.
If you look very carefully, you'll see that the method used is POST, rather than PATCH.
It's probably a good idea to keep in mind that there's a bit of indirection between your the resources in your API and the aggregates in your model. Here's how Jim Webber described it
The web is not your domain, it's a document management system. All the HTTP verbs apply to the document management domain. URIs do NOT map onto domain objects - that violates encapsulation. Work (ex: issuing commands to the domain model) is a side effect of managing resources. In other words, the resources are part of the anti-corruption layer. You should expect to have many many more resources in your integration domain than you do business objects in your business domain.
Resources adapt your domain model for the web
So here's the point: if you are going to use PATCH (or PUT, for that matter), then the idiom that you are proposing is that the client fetch a representation of the resource, modify that representation, and then return it to the API, at which point the API needs to figure out what domain commands those changes actually mean.
Leaning on the inventory item as the example; if we are outside the model, looking at a representation of an inventory item, then we are normally looking at a read model, so a representation might look like
GET /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
{
"Id": "ef4454ae-4a82-4bf2-82e1-3da9229ecc94",
"Version": 7,
"Name" : "REST in Practice",
"CurrentCount": 20
}
In an anemic domain, where the resources are just documents, we could update the current count simply by...
PUT /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
{
"Id": "ef4454ae-4a82-4bf2-82e1-3da9229ecc94",
"Version": 8,
"Name" : "REST in Practice",
"CurrentCount": 30
}
But if the domain isn't anemic, then we need to recast the changes to the document state as commands to send to the model. In this specific case, where the change is simple, we'd need to observe the different in the CurrentCount, compute the difference, use the sign of the change to identify the correct command, and the magnitude of the change to initialize that command.
Now, as we noted the representation being patched is that of the resource, not that of the inventory item; we've got a bit more leeway. So we could do something like...
GET /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
{
"Id": "ef4454ae-4a82-4bf2-82e1-3da9229ecc94",
"Version": 7,
"Name" : "REST in Practice",
"CurrentCount": 20,
"pendingCommands" : []
}
and a clever client could then try...
PUT /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
{
"Id": "ef4454ae-4a82-4bf2-82e1-3da9229ecc94",
"Version": 7,
"Name" : "REST in Practice",
"CurrentCount": 20,
"pendingCommands" : [
{
"command":"CheckIn",
"count": 10
}
]
}
And now the job of the API layer is simple again; it just needs to know how to parse the objects in the pendingCommand list.
If you are happy with that sort of approach, then you can of course replace PUT with PATCH
PATCH /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
Content-Type: application/json-patch+json
[
{
"op":"add",
"path":"/pendingCommands/0",
"value":
{
"command":"CheckIn",
"count": 10
}
}
]
Note that PUT doesn't actually promise that the received representation will be observable
A successful PUT of a given representation would suggest that a subsequent GET on that same target resource will result in an equivalent representation being sent in a 200 (OK) response. However, there is no guarantee that such a state change will be observable, since the target resource might be acted upon by other user agents in parallel, or might be subject to dynamic processing by the origin server, before any subsequent GET is received. A successful response only implies that the user agent's intent was achieved at the time of its processing by the origin server.
The "dynamic processing" in this case being the running of the pendingCommands.
Now, if you look very carefully at JSON-Patch, you might realize that it is just a list of well defined commands that are targeting a "JSON Document" aggregate, and that's right.
So by analogy, you could define an InventoryItem-Patch format, which describes a list of operations that are valid for InventoryItems (so instead of the operations add/move/replace/... you would be defining operations ChangeName/Checkin/Deactivate...)
GET /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
{
"Id": "ef4454ae-4a82-4bf2-82e1-3da9229ecc94",
"Version": 7,
"Name" : "REST in Practice",
"CurrentCount": 20
}
PATCH /ef4454ae-4a82-4bf2-82e1-3da9229ecc94
Content-Type: application/vnd.inventory-item-patch+json
[
{
"changeName" :
{
"newName" : "REST in Patch"
}
}
]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With