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!

Posted by: Matt Hamilton
Last revised: 15 Oct, 2024 10:16 PM History

Comments

30 Jan, 2018 02:24 AM

The big security advantage of this approach is you also avoid over-posting vulnerabilities because your UpdateProductRequest model can leave out fields, such as CreatedBy, which shouldn't be updated by the client.

No new comments are allowed on this post.