ASP.NET Core / Entity Framework – 使用多对多关系时,HTTP Post未在连接表中创建实体

huangapple go评论72阅读模式
英文:

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

问题

  1. 我正在开发一个带有 SQL Server 数据库的 ASP.NET Core 7 MVC 应用程序。
  2. 我正在使用 EF Core 7 - 我的数据库表是通过当前模型的 EF 迁移生成的。
  3. 我正在使用 OData 8 控制器。
  4. 我正在遵循此文档,该文档介绍了如何在我的模型之间设置多对多关系。我的理解是我的模型是“传统的”,因此可以利用 EF 中的跳过导航。

我的模型:

namespace testapp.Models;

using System.ComponentModel.DataAnnotations;
// ...

我的控制器:

namespace testapp.Controllers;
// ...

我的 DbContext:

namespace testapp.Models;
// ...

我的相关 WebApplicationBuilder:

var modelBuilder = new ODataConventionModelBuilder();
// ...

当 EF 迁移完成时,在我的 SQL 数据库中创建了 3 个表 - 'Home'、'Option' 和 'HomeOption'。我理解 'HomeOption' 是一个连接表,用于保持 'Home' 和 'Option' 之间的多对多关系。它的唯一列是 'HomeId' 和 'OptionId'。

当我发送一个包含新 'Home' 和现有 'Option' 内容的 HTTP Post 请求时,我期望在连接表 ('HomeOption') 中出现一个条目,以指示两个实体之间的关系。

我尝试过各种组合的 DbContext 和手动指定连接类型,但都没有成功。我开始怀疑我的控制器动作是否有问题 - 是我还是 EF 负责创建 'HomeOption' 实体?微软在他们的知识库中建议让 EF 按照约定处理此事,所以如果适用的话,我希望这样做。

供参考,这是我的 POST 请求的主体:

{
    "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
        }
    ]
}
英文:
  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
        }
    ]
}

答案1

得分: 2

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 its 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: (https://stackoverflow.com/questions/76576283/insert-a-list-of-complex-objects-without-inserting-navigational-properties-in-en/76576838#76576838)

英文:

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: (https://stackoverflow.com/questions/76576283/insert-a-list-of-complex-objects-without-inserting-navigational-properties-in-en/76576838#76576838)

huangapple
  • 本文由 发表于 2023年6月29日 07:46:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76577311.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定