Search code examples
c#entity-framework.net-coreodata

How to POST multiple related entities with Entity Framework + OData?


What is the proper way to POST multiple related entities and check required properties?

I have two entities Product and Package. A Product can have many Packages. A Package cannot exist without a related Product.

When I POST a new Product with a collection of multiple Packages, the ModelState says the model is invalid because the Packages do not have their required productId foreign key set. This causes my API to return a BAD REQUEST status.

It's my understanding that Entity Framework supports this. I would expect the ModelState to detect that the Package productId FK will be set automatically and thus wouldn't be invalid.

If I remove the block of code that checks the ModelState validity Entity Framework behaves as expected and the Product and Packages are created.

POST /api/v1/Products
BODY
{
   "name": "Coca-Cola",
   "packages": [
        {
            "count": 6,
            "quantity": 12,
            "quantityUnit": "oz"
        }
    ]
}

Here is my controller function:

[HttpPost]
[ODataRoute("")]
[EnableQuery]
public virtual async Task<IActionResult> PostEntity([FromBody] Product entityToCreate)
{
    //Removing this block will cause the request to succeed
    // but will also allow invalid requests to get to the database
    if (!ModelState.IsValid) 
    {
        return BadRequest(ModelState);
    }

    _context.Packages.Add(entityToCreate);
    await _context.SaveChangesAsync();

    return Created("DefaultApi", entityToCreate);
}

And here are my models:

public class Product: ModelBase
{
    [Required]
    public string name { get; set; }

    public List<Package> packages { get; set; }
}

public class Package: ModelBase
{
    //some required props...

    [Required]
    [ForeignKey("product")]
    public Guid? productId { get; set; }
    public Product product { get; set; }
}

Some things I've investigated:

  1. Dropping the [Required] attribute on packageId and manually editing the db migration to include the constraint. This causes an error to be thrown later in the stack by the database. I'd rather fail before getting to the database

  2. Writing my own validation attributes. This seems unnecessary given that the EF documentation claims to support this

  3. Possibly using OData Batch Requests


Solution

  • I typically create a very specific "model" class (not an entity) to represent the payload of an HTTP Post (Put, Patch, etc.). In this case you'd want a set of models. Then I have validation rules specific this set of models. Some call these models, DTOs, ViewModels, etc. The important thing is that they are not EF entities.

    After verifying their validity, you will have to map these models into the appropriate entities so they can be persisted with EF. This mapping is the unfortunate tax of decoupling your entities from your exposed models. You can do this manually or use a library like automapper.

    There are many advantages to this strategy:

    • You can support several variations of manipulating your persisted Product/Package data without having to accommodate each of the variations with one set of entities. This means you can sensibly create and expose different sets of properties (models) with different validation rules for the same persisted data types (entities).
    • You protect yourself from overposting. You don't want the user to be able to set the state of all properties without restriction.
    • Your exposed endpoints (controller actions) have parameters that are decoupled from your entities. This means that you can freely change the entities without necessarily having to change the exposed models that represent them and vice versa. You could hypothetically abandon EF altogether and move to some neat NoSQL persistence solution without having to change your expose APIs.