为啥我的 ASP.NET Core 3.1 控制器会自动为从视图返回的 EF Core 模型分配 ID?

Posted

技术标签:

【中文标题】为啥我的 ASP.NET Core 3.1 控制器会自动为从视图返回的 EF Core 模型分配 ID?【英文标题】:Why is my ASP.NET Core 3.1 controller automatically assigning an ID to an EF Core model that was returned from a view?为什么我的 ASP.NET Core 3.1 控制器会自动为从视图返回的 EF Core 模型分配 ID? 【发布时间】:2021-10-02 17:06:37 【问题描述】:

这有点奇怪,我 90% 确定这是一个错误,但你永远不知道。

我正在为学生提供辅导课程,并试图让他们了解如何使用 EF Core 和 ASP.NET Core 3.1。由于他们是初学者,因此我不想为每个视图编写模型,以去除视图不需要的所有 EF Core 实体数据。相反,我只是让他们将实体模型类直接传递给视图,并在他们从视图中POST 时将其直接传递回控制器。

我试图向他们展示如何将 Comment 添加到 Review 实体,但我遇到了一个奇怪的问题。

鉴于以下GET 操作:

[HttpGet]
public IActionResult Write([FromRoute] int id)

    return View(new Comment
    
        ReviewId = id
    );

还有以下视图:

@model Comment
@
    ViewBag.Title = "Add a comment";

<nav>
    <a asp-controller="Home" asp-action="Index">Back</a>
</nav>
<h1>ID: @Model.Id</h1>
<form method="post" asp-controller="Comments" asp-action="Write">
    @html.HiddenFor(c => c.ReviewId)
    @Html.LabelFor(c => c.Content)
    @Html.TextAreaFor(c => c.Content)
    <button type="submit">Submit</button>
</form>

Comment 的 ID 会自动设置为我数据库中的最后一个 ID,即使它没有被 View 修改或由 GET 方法设置,只要它到达我的控制器的 POST行动。

[HttpPost]
public IActionResult Write(Comment model)

    // Using a debugger, "model" has an ID equal to that of the 
    // last Comment in my database as soon as we enter this method...

    var review = _context.Reviews
                    .Include(r => r.Comments)
                    .FirstOrDefault(r => r.Id == model.ReviewId);

    if (review != null)
    
        review.Comments.Add(model);

        _context.Update(review);
        _context.SaveChanges(); // <-- Causes a primary key conflict error because of "model.Id"'s value
    

我对这种行为感到有点困惑,但我并没有永远将 EF Core 模型传递给控制器​​(我们通常编写视图特定的模型来去除我工作的不需要的数据),所以也许这个是正常行为吗?但它感觉对我来说就像一个错误。

我已经找到了一个“修复”,只需明确地绑定我想要的属性:

[HttpPost]
public IActionResult Write([Bind("Content, ReviewId")]Comment model)

但我想知道为什么modelId 属性一碰到我的控制器就会自动设置为最后一个Comment 实体的Id

【问题讨论】:

@GertArnold,当你第一次回复我的帖子时,我正在写我的答案。我发现了这个问题。这是因为我的签名中没有 [FromForm],并且框架从 URL 中推断出模型的 ID(我猜是因为默认端点路由映射中的 id? 位与 model.Id 属性名称匹配?),它具有父评论的 ID(不会因为我们添加了更多评论而改变)。这意味着向同一个Review 对象添加更多Comments 最终会与Review.Id 的值发生冲突。 我怀疑会发生这样的事情,但不知道如何发生。所以就是这样 我从来没有在网络核心中使用过 [FromForm]。但它通常仅在需要指向源的特殊情况下使用(在同一操作中,一些参数来自路由,一些来自查询字符串,一些来自表单)。 【参考方案1】:

在进一步摆弄之后,我找到了问题的根源。


TL;DR:

这解决了我的问题。

[HttpPost]
public IActionResult Write([FromForm]Comment model)

解释:

我完全忘记将[FromForm] 添加到我的控制器的POST 操作中,因此框架没有将正确的数据放在正确的字段中。我猜它正在使用默认控制器路由端点映射的id? 部分并将此值分配给model.Id,这会在将多个Comments 添加到Review 时导致问题,因为URL 的路由@987654329 @ 值是父 ReviewId,而不是 Comment,当您将多个 Comments 添加到单个 Review 时,此值保持不变。

这意味着向第一个Review 对象添加第二个Comment 会导致主键冲突,因为ReviewId 是“1”,第一个 CommentId也为“1”,URL为/write/1,将第二个CommentId赋值为“1”,导致key冲突。

这也是为什么model.Id 的值一接触到我的控制器就似乎发生了变化。因为它是。控制器只是使用它认为最好的方式从请求中获取数据,但它做错了。

当我添加一个隐藏的 &lt;input&gt; 标记以确保将 Id 强制设置为 0 不会解决问题时,我意识到绑定有问题。

我之前在视图中添加了以下行,以确保我的 Id 在视图中设置为“0”:

<h1>ID: @Model.Id</h1>

但隐藏的&lt;input&gt; 标记的值与ReviewId 匹配,而不是“0”。所以我再次查看数据绑定,并在 MSDN 上找到了这个页面,其中涵盖了 Binding source parameter inference 的主题,其中指出框架将尝试使用它认为最好的方法,而不是给出明确的绑定源属性。

【讨论】:

以上是关于为啥我的 ASP.NET Core 3.1 控制器会自动为从视图返回的 EF Core 模型分配 ID?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 ASP.Net Core Web API 控制器不返回 XML?

Asp.net core 3.1 中的路由

ASP.Net Core 3.1 脚手架的身份页面并未全部使用(即登录页面正常,注册页面不正常)......为啥?

如何使用 C# 在 ASP.NET Core 3.1 MVC 中使用会话变量

Asp.Net Core 3.1 控制器方法未调用

为啥 Asp.net Core 无法读取 Ajax 参数?