简体   繁体   中英

Why is my ASP.NET Core 3.1 controller automatically assigning an ID to an EF Core model that was returned from a view?

This is a bit of a weird one, and I'm 90% sure this is a bug, but you never know.

I'm giving tutoring sessions to students, and I'm trying to get to wrap their heads around using EF Core with ASP.NET Core 3.1. Since they're beginners, I didn't want to jump into writing models for each view that strip out all the EF Core entity data that won't be needed by the view. Instead, I just had them pass the entity model class directly to the view and pass it right back to the controller when they POST from the view.

I'm trying to show them how to add a Comment to a Review entity, but I've run into a weird problem.

Given the following GET action:

[HttpGet]
public IActionResult Write([FromRoute] int id)
{
    return View(new Comment
    {
        ReviewId = id
    });
}

And the following View:

@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>

The Comment 's ID is automatically set to the last ID in my database, even though it isn't modified by the View or set by the GET method, as soon as it reaches my controller's POST action.

[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
    }
}

I'm kind of confused by this behavior, but I haven't passed an EF Core model to a controller in forever (we usually write View-specific models that strip out the unneeded data where I work), so maybe this is normal behavior? But it feels like a bug to me.

I've found a "fix" for it, by simply explicitly binding what properties I want:

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

But I'd like to know why the model 's Id property is automatically set to the last Comment entity's Id as soon as it hits my controller.

After fiddling around with it further, I've found the source of the problem.


The TL;DR:

This fixed my issue.

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

Explanation:

I completely forgot to add [FromForm] to my controller's POST action, so the framework wasn't putting the right data in the right field. I'm guessing it was using the {id?} portion of the default controller route endpoint mapping and assigning this value to model.Id , which caused issues when adding multiple Comment s to a Review , because the URL's Route id value was the parent Review 's Id , not the Comment 's, and this value stays the same when you're adding multiple Comment s to a single Review .

This means that adding a second Comment to the first Review object would result in a primary key conflict, because that Review 's Id was "1", the first Comment 's Id was also "1", and the URL would be /write/1 , which assigned the second Comment 's Id to "1", causing the key conflict.

It's also why model.Id 's value was seemingly changing as soon as it hit my controller. Because it was. The controller was just using whatever it thought best to get the data from the request, and it was doing it wrong.

I realized something was messing with the bindings when I added a hidden <input> tag to make sure that forcefully setting the Id to 0 wouldn't solve the issue.

I had added the following line to my view before to make sure my Id was set to "0" on the view:

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

But the hidden <input> tag had a value matching the Review 's Id , instead of "0". So I went looking into data bindings again, and found this page on MSDN which covered the topic of Binding source parameter inference , which states that the framework will try to use what it thinks is best when not given an explicit binding source attribute.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM