How to implement HTTP's PUT that works with child collections when using DDD's rich domain models?
Let's say we've got an aggregate root with a nicely encapsulated collection of items:
(I omitted persistence specific properties like Id for brevity)
public class Foo : IAggregateRoot
{
private readonly List<Bar> _items = new List<Bar>();
public IReadOnlyCollection<Bar> Items => _items;
public void AddItem(Bar bar)
{
_items.Add(bar);
}
public void RemoveItem(Bar bar)
{
_items.Remove(bar);
}
}
Now we want to implement PUT /foos/{id}
where you pass DTO like:
public class FooDto
{
public IEnumerable<BarDto> Items { get; set }
}
Now the problem is we can't simply map FooDto
to Foo
. It becomes a complex problem to solve, especially when you want to remove some Bar
from Items
.
We're left with two options:
- Create 2 separate routes for adding and removing items, like
POST /foos/{id}/items
andDELETE /foos/{fooId}/items/{itemId}
- Write some twisted logic to compare changes in collection and based on that deduce what was added and removed and call
AddItem
orRemoveItem
respectively
Is there anything I am missing here or doing wrong? Would I be better off using anemic domain models to simplify implementation of Web API? That would mean Web API would dictate how my domain models should look like and I think it's a bit wrong.
A similar question I've found while looking for solutions: Do RESTful APIs tend to encourage anemic domain models?
4 Answers 4
I wouldn't consider your option #2 as "twisted logic".
You just need an equality function (something to determine if two Bar
s are the same) and then:
toAdd = elements in newItems that don't exist in currentItems
toDelete = elements currentItems that don't exist in newItems
I don't know C#, but in pseudo-code could be something like this:
for e in newItems:
if not e in currentItems:
currentItems.add(e)
for e in currentItems:
if not e in newItems:
currentItems.remove(e)
How to implement HTTP's PUT that works with child collections when using DDD's rich domain models?
With great difficulty.
PUT, like DELETE and PATCH, have remote authoring semantics. "Make the resource look like this".
The basic idea being that I can take any HTTP aware editor that understands the media type, and use it to load a new representation, make changes to it, then store the changes -- without needing to know anything about how the origin server stores that information.
Thus, the burden is on the server to figure out how to convert the new representation into commands to send to its own backing storage.
One possible answer is to put more of the burden on your media type. Resources are allowed to have more than one representation, and you aren't required to support PUT
for all of them. 415 Unsupported Media Type
is there for you in the case where you need to reject a PUT
that you can't convert into messages your domain model will understand.
That flow might look something like
GET /foo
Content-Type: application/json
And the consumer looking at this representation sees that they want to edit the resource. So it asks for the editable representation
GET /foo
Content-Type: application/vnd.hacky-workarounds.edit+json
PUT /foo
Content-Type: application/vnd.hacky-workarounds.edit+json
You might imagine a representation analogous to application/json-patch+json, with operations that match the command message that your domain model understands, and a query operations that describe the initial state (for instance, the hash of the current tree).
Depending on your schema, the client might be able to convert the original DTO to the editable representation on its own, without needing to perform a GET. HTTP is stateless, so the subsequent PUT
should behave the same way regardless.
Another approach to consider is that the domain model has two responsibilities: making sure that state is internally consistent, and making sure that the transitions are valid. In other words, the origin server doesn't necessarily need to apply the business rules itself, but instead just validate that the rules have been applied correctly.
Thus, you check that the DTO is internally consistent, and you check that the transition from old state to new state is legal, and if both of those hold you just store the result without worrying about repeating the work yourself.
But the real answer is the one you hate - diff the two representations, reverse engineer the commands required to transform one into the other, and then apply those commands via the domain model.
PUT can be used to create resource, in addition to editing them (provided that you are comfortable with the notion that a client gets some control of the URI). So you could PUT a representation of a command message to some new resource. Likely you would want to use the If-None-Match header to ensure that particular URI is not already occupied. So each command would get a unique URI, selected by the client. What you don't get in that case is cache invalidation of the DTO (which has it's own URI already)
Allowing the client to select a URI for the the command message resource is analogous to allowing the client to select its own correlation identifiers.
Like most design decisions the answer is "it depends".
Does updating a Foo
usually require updating it's collection of Bar
as well? Do they need to be updated in the same unit of work? Is Bar
a value object? If the answer to one or more of these questions is "yes" then option 2 is probably the only course of action. Under these conditions you can consider FooDTO
's Items
collection to be a representation of the final state of Foo
's Items
collection at the completion of the unit of work. And since Bar
is a value object, comparing them should be trivial.
If, on the other hand, Bar
is an entity and has an identity (and potentially lifetime) of its own apart from Foo
or it's expected that a Bar
can be updated without needing a corresponding change to Foo
then option 1 may be more suitable.
Another possibility is that a rich domain model is not the most appropriate architecture for this particular application. Evan's is quick to point out that DDD is a mindset and that there are plenty of applications that simply are not appropriate for a full implementation of of rich domain model, repositories, services, etc. That doesn't mean it won't benefit from domain modelling or establishing a ubiquitous language.
The problem is, you have Foo
and FooDto
. What is FooDto
? Is that part of the "Domain"? It is probably a technical thing just because of some framework that needs it.
So you basically already have anemic objects, that is why you are struggling. You have to get rid of all anemic stuff to be able to really make a "rich" domain model work. You could just receive the full Foo
on a PUT
. You are the one defining how to deserialize from JSON (or whatever), so you could easily build the "right" object immediately, without any intermediaries, then you could use the "real" business methods on it immediately to make any request work.
-
no
FooDto
is not part of the domain. The only part of the domain in my example isFoo
.FooDto
is part of the API.Foo
is not anemic.FooDto
is a POCO(in DDD terms anemic)Konrad– Konrad09/07/2018 19:53:53Commented Sep 7, 2018 at 19:53
Explore related questions
See similar questions with these tags.