MateuszNaKodach / SelfImprovement

This project has some sample code for my personal learning purpose. Things which I've learnead are collected as issues here: https://github.com/nowakprojects/SelfImprovement/issues
107 stars 17 forks source link

JsonPatch standard and DDD/CQRS/ES approach compatibility #5438

Closed MateuszNaKodach closed 1 year ago

MateuszNaKodach commented 1 year ago

How to PATCH an Aggregate Root

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" } } ]