CQRS with OData and Actions?
I love the Actions feature in OData – which is hardly surprising given I was one of its designers.
Here’s the main reason why I love it: Actions allow you move from a CRUD architecture style, where you query and modify data using the same model, to an architectural style where you have a clear separation between the model used to query and the model used to update. To me this feels a lot like Greg Young’s baby CQRS, or Command Query Responsibility Segregation.
I’ll admit I’m taking some liberties here because these two models are actually ‘merged’ into a single metadata document ($metadata) that describes them both, and you can share types between these two models… however this feels insignificant because the key benefits remain.
Why would you want to move from a CRUD style application to a CQRS style one?
Let’s look at a simple scenario, imagine you have Products that look like this:
public class Product
{
public int ID {get;set;}
public string Name {get;set;}
public Decimal Cost {get;set;}
public Decimal Price {get;set;}
}
And imagine you want to Discount Marmite (a product in your catalog) by 15%. Today using the CRUD style, the default in OData before Actions, there is only one option: you PUT a new version of the Marmite resource with the new Price to the URL that represents Marmite, i.e. something like this:
POST ~/Products(15) HTTP/1.1
Content-Type: application/json
{
// abbreviated for readability
“ID”: 15,
“Name”: “Marmite”,
“Cost”: 3.50,
“Price”: 4.25 // ($5 – 15%)
}
Notice to support this you have to allow PUT for Products. And this has some real issues:
- People can now make changes that we don’t necessarily want to allow, i.e. modifying the Name & Cost or changing the Price too much.
- Basically “Update Product” is NOT the same as “Discount Product”.
- When a change comes through we don’t actually know it is a Discount. It just looks like an attempt to update a Product.
- If you need information that is not part of Product to perform a Discount (perhaps a justification) there is no where to put that information.
More generally the CRUD model is painful because:
- If you want to update lots of resources simultaneously, imagine for example that you want to discount every product in a particular category, you first have to retrieve every product in that category, and then you have to do a PUT for each of them. This of course introduces a lot of unnecessary latency and introduces consistency challenges (it is hard to maintain a transaction boundary across requests & it the longer the ‘transaction’ lasts the more likely a concurrency check will fail).
- If you want to update something you have to allow it to be read somewhere.
Back to our scenario, it would be much better to disable PUT completely and create a Discount action, and advertise it’s availability in the Marmite resource (to keep your system as Hypermedia driven as possible):
{
“__metadata”: {
// abbreviated for simplicity
“Actions”: {
“#MyActions.Discount”: [{ “title”: “Discount Marmite”, “target”: “Products(15)/Discount”}]
}
},
“ID”: 15,
“Name”: “Marmite”,
“Cost”: 3.50,
“Price”: 5.00
}
The name of the Action (i.e. #MyActions.Discount) is an ‘anchor’ into the metadata document that can be found at ~/$metadata that says you need to provide a percentage.
POST ~/Products(15)/Discount HTTP/1.1
Content-Type: application/json
{
“percentage”: 15
}
This is much better. Notice this doesn’t allow me to modify the Cost or the Name, and indeed can easily be validated to make sure the percentage is within an acceptable range, and it is semantically much clearer what is happening.
In fact by moving from a CRUD style architecture to one inspired by CQRS but based on actions you can:
- Give targeted update capabilities that:
- Allow only certain parts of the ‘Read’ model to be modified.
- Allow things that are not even in the ‘Read’ model to be modified or provided if needed.
- Selectively give users permissions to only the ‘Actions’ or Commands they need.
- Log every Action and replay them at a later date to rebuild your data (i.e. Event Sourcing).
- Capture what is requested (i.e. Discount the product) and respond immediately before the change has actually been made, safe in the knowledge you will eventually process the request and achieve “Eventual Consistency”.
- Capture more information about what is happening (i.e. User X discounted Marmite by 15% is much better than User X updated Marmite).
- Create Actions that manipulate a lot of entities simultaneously (i.e. POST ~/Categories(‘Yeast Spreads’)/Products/Discount…)
Of course simply separating the read/write models in OData doesn’t give you all of these advantages immediately, but at least it creates a foundation that can scale to support things like Event Sourcing or Eventual Consistency later if required.
Some of you may be thinking you can achieve many of these goals by having a more granular model that makes things like “Discount” a resource, and that would be true. However for most people using OData that way of thinking is foreign and more importantly the EDM foundations of OData get in the way a little too. So for me Actions seems like the right approach in OData.
I love this.
But what do you think?
-Alex
Comments
- Anonymous
February 05, 2012
If you go back and DELETE a Discount that you previously POSTed, will the price revert back to the base price? If you POST a Discount to a category, will your action update the price of every item in the database?The base price and discount are independent. The user can change them directly. The price is dependent. It is calculated based on other properties. The user cannot directly modify a dependent property.I am a strong believer that dependents should not be stored in the database. You should only store independents, and calculate dependents when necessary. Is this how actions work with OData? - Anonymous
February 05, 2012
Actions are very flexible - it is basically your decision what they do.The real challenge for the concept of independent/dependent properties is the read model. if you want calculated properties (i.e. Price) you probably need to write your own custom provider (or configure your EntityFramework model to work through a view etc.)I know this because I worked on the EF team for a while and we were very close to implementing Calculated Properties but didn't... and the work around is using DB views etc. - Anonymous
February 23, 2012
Hey Alex,I have to ask is this actually CQS as opposed to CQRS? Now granted nothing in this prevents full fledged CQRS behind all this, I am just stating at the level you are discussing this (the protocol) there's no seperation of the multiple read systems from the update store here. As Udi often points many of the places we talk about CQRS we're meaning CQS actually. (www.udidahan.com/.../clarified-cqrs)cheers - Anonymous
February 23, 2012
@Michael,I get your point on this but actually I'd state that you cannot "delete" a discount. A discount by definition has a temporal concept (from and to where to might be forever). However, much like double entry book keeping, you can post a newer discount that is the functional inverse of the prior one. - Anonymous
February 23, 2012
The comment has been removed