Identity is Metadata
Identity is Metadata
As part of a little passion project I've been writing at home, I have been taking a different approach to the way I structure the models returned by my web API. I'm rather happy with the approach so I thought I'd write about it here.
(My project uses ASP.NET Core Web API on the server and Aurelia on the client. I want to write about Aurelia soon because it's been really fun to learn. Anyway, on with the show.)
Let's consider a product object:
public class Product
{
public int Id { get; internal set; }
public string Code { get; set; }
public string Description { get; set; }
// and a bunch of other properties
}
When the client is spinning up a new product, it doesn't know what its Id property will be yet. It needs to be able to pass the product's details to the server and be told what the Id is once the product is created. In the past I've handled this by using a nullable int property on the model, but this time around I'm using a couple of wrapper classes instead.
First, ProductResponse
is the actual JSON object returned by the server:
public class ProductResponse
{
public int Id { get; }
public DateTime Created { get; }
public string CreatedBy { get; }
public DateTime Updated { get; }
public string UpdatedBy { get; }
public bool IsEnabled { get; }
public Dictionary<string, Array> Options { get; }
public ProductModel Item { get; }
}
And secondly, ProductModel
is the core part of the product that would be posted back by the client:
public class ProductModel
{
public string Code { get; set; }
public string Description { get; set; }
// and a bunch of other properties
}
So the Id
property is part of the ProductResponse
class, because I consider the identity of the product to be metadata. It can't be changed by the client - it makes no sense for it to be posted back with the mutable properties of the ProductModel
class. How does the server know which product it is that you're changing? It's in the URI:
[HttpPatch("{id}")]
public async Task<IActionResult> Patch(int id, [FromBody]UpdateProductRequest model)
So here we're using a PATCH request to (for example) /products/3 and passing in an UpdateProductRequest
object, which for now looks like this:
public class UpdateProductRequest
{
public ProductModel Item { get; set; }
}
I've wrapped ProductModel
in a wrapper class like this just in case there might be metadata I want to pass along with the request that I don't want to be part of the URI. You could just as easily accept a ProductModel
as part of the controller action instead.
What does creating a new product look like, then? I'm glad you asked!
[HttpPost]
public async Task<IActionResult> Post([FromBody]CreateProductRequest model)
So all you need to do is POST to /products and pass in your CreateProductRequest
instance, which is predictably simple:
public class CreateProductRequest
{
public ProductModel Item { get; set; }
}
I'm pretty chuffed with this approach. Wrapping the actual model in classes that contain the metadata has served me very well so far. Keen to hear your thoughts!
No new comments are allowed on this post.
Comments
Adrian Clark
The big security advantage of this approach is you also avoid over-posting vulnerabilities because your
UpdateProductRequest
model can leave out fields, such asCreatedBy
, which shouldn't be updated by the client.