Search code examples
sql-serverasp.net-mvcentity-frameworkentity-framework-coreodata

ASP.NET Core / Entity Framework - HTTP Post not creating entities in join table when using many-to-many relationship


  1. I'm working on an ASP.NET Core 7 MVC app with a SQL Server database.
  2. I'm using EF Core 7 - my DB tables were generated by an EF migration with my current models.
  3. I'm using OData 8 controllers
  4. I'm following this document on setting up many-to-many relationships between my models. My understanding is that my model is 'conventional' and thus can take advantage of skip navigations within EF.

My models:

namespace testapp.Models;

using System.ComponentModel.DataAnnotations;

public class Home
{
    [Key]
    public Guid Id { get; set; }

    public bool Active { get; set; }

    public string? Name { get; set; }

    public string? Description { get; set; }

    public double BaseCost { get; set; }

    public double BasePrice { get; set; }

    public List<Option> Option { get; } = new();

}

public class Option
{
    [Key]
    public Guid Id { get; set; }

    public bool Active { get; set; }

    public string? Name { get; set; }

    public string? Description { get; set; }

    public double Cost { get; set; }

    public double Price { get; set; }

    public List<Home> Home { get; } = new();

}

My controller:

namespace testapp.Controllers;


using System.Collections.Generic;
using System.Linq;
using testapp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

[EnableQuery]
public class HomeController : ODataController
{
    private readonly TestDbContext _context;

    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger, TestDbContext dbContext)
    {
        _logger = logger;
        _context = dbContext;
    }

    [HttpPost]
    public IActionResult Post([FromBody] Home HttpBody)
    {
        if (ModelState.IsValid)
        {
            _context.Home.Add(new Home()
            {
                Name = HttpBody.Name,
                BaseCost = HttpBody.BaseCost,
                BasePrice = HttpBody.BasePrice
            });
     

            _context.SaveChanges();
            return Ok();
        }
        else
        { 
            return BadRequest(); 
        }
    }
}

My DbContext:

namespace testapp.Models;

using Microsoft.EntityFrameworkCore;


public class TestDbContext : DbContext
{

    public DbSet<Home> Home { get; set; }

    public DbSet<Option> Option { get; set; }

    public TestDbContext(DbContextOptions<TestDbContext> options) : base(options) { }

}

My relevant WebApplicationBuilder:

var modelBuilder = new ODataConventionModelBuilder();
//modelBuilder.EntityType<TestModel>();
modelBuilder.EntitySet<Home>("Home");
modelBuilder.EntitySet<Option>("Option");



builder.Services.AddControllers().AddOData(
    options => options.Select().Filter().OrderBy().Expand().Count().SetMaxTop(50).AddRouteComponents(
        routePrefix: "api",
        model: modelBuilder.GetEdmModel()).EnableAttributeRouting=false);

When the EF Migration completed, it created 3 tables in my SQL database - 'Home', 'Option', and 'HomeOption'. My understanding is that 'HomeOption' is a join table used to persist the many-to-many relationship 'Home' and 'Option'. Its only columns are 'HomeId' and 'OptionId'

When I send an HTTP Post request with the contents of a new 'Home' and an extant 'Option', I would expect an entry to appear in the join table ('HomeOption') to indicate a relationship between the two entities.

I've tried various combinations of DbContext and manually specifying a join type but have not had any success. I'm beginning to wonder if my controller action is wrong - is it up to me or EF to create the 'HomeOption' entity? MS recommendation in their KB is to let EF handle this by convention, so I'm looking to do that if applicable.

For reference, here's the body of my POST request:

{
    "Active": true,
    "Name": "5:58",
    "Description": null,
    "BaseCost": 9.99,
    "BasePrice": 29.99,
    "Option": [
        {
            "Id": "379ef6b2-b533-4e5e-8e67-08db77c67ccd",
            "Active": false,
            "Name": "chimney",
            "Description": null,
            "Cost": 0.0,
            "Price": 0.0
        }
    ]
}

Solution

  • You newed up a Home that got added to the DbContext, not the block of data you passed in via the POST:

           _context.Home.Add(new Home()
            {
                Name = HttpBody.Name,
                BaseCost = HttpBody.BaseCost,
                BasePrice = HttpBody.BasePrice
            });
    

    If you absolutely trust that the Home and Option(s) are trust-worthy and only contain data that can be inserted (not references to existing entities) then you could use:

    _context.Home.Add(HttpBody);
    

    Which would add the provided Home and it's Options.

    This won't work in the case where either Home or Option holds a reference to another entity that is expected to already exist in the database though. You will get an exception relating to inserting a duplicate PK. In those cases you need to identify anything that is already existing, check the DbContext local cache for any tracked references, substitute the reference with the tracked instance, or if nothing is tracked, Attach the referenced entity to the DbContext before saving the Home. I just answered a question around that scenario here: (Insert a List of complex objects without inserting navigational properties in Entity Framework)