Search code examples
c#linqasp.net-coreentity-framework-corerazor-pages

Why am i getting a "temporary value while attempting to change the entity's state to 'Modified'" When trying to update a Model that contains FK?


I'm trying to take a model from a DB table, make changes to it, and then update said changes when posting the form back. I've been digging through multiple resources trying to find the solution to this problem and cannot seem to understand exactly what is going on or find a good solution for it.

From previous lectures and experience, I have gotten this to work using

_context.Attach(Model).state = EntityState.Modified;

And it would work just fine. Now on another page, trying to implement the same technique that worked, Im running into the following error.

Error Message

InvalidOperationException: The property 'SpeakerForm.SpeakerFormId' has a temporary value while attempting to change the entity's state to 'Modified'. Either set a permanent value explicitly, or ensure that the database is configured to generate values for this property.
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, bool acceptChanges, bool modifyProperties)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, bool acceptChanges, bool modifyProperties, Nullable<EntityState> forceStateWhenUnknownKey, Nullable<EntityState> fallbackState)
Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry.set_State(EntityState value)
LosBarriosEvents.Pages.SpeakerFormsEditModel.OnPostAsync() in Edit.cshtml.cs
+
        _context.SpeakerForms.Attach(Form).State = EntityState.Modified;

Models

Here is my "SpeakerForm" model which is the main model I am trying to do the edit's on:

using System.ComponentModel.DataAnnotations;

namespace LosBarriosEvents.Models;

public class SpeakerForm
{
    [Key]
    public int SpeakerFormId { get; set; }

    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string Duration { get; set; } = string.Empty;

    //Navigation Properties
    public string SpeakerId { get; set; }
    public Speaker? Speaker { get; set; }
}

Which is tied to "Speaker" as the FK:

using System.ComponentModel.DataAnnotations;

namespace LosBarriosEvents.Models
{
    public class Speaker
    {
        [Key]
        public string SpeakerId { get; set; } = default!;//PK

        public string firstName { get; set; } = string.Empty;
        public string lastName { get; set; } = string.Empty;
        public string email { get; set; } = string.Empty;
        public string phoneNumber { get; set; } = string.Empty;

        //Navigation Properties
        public List<SpeakerForm>? SpeakerForms { get; set; }
    }
}

Edit.cshtml.cs

I then use this code on the code behind pages to try and bind the properties, make changes on the front end, and then update

using System.Security.Claims;
using LosBarriosEvents.Areas.Identity.Data;
using LosBarriosEvents.Authorization;
using LosBarriosEvents.Data;
using LosBarriosEvents.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

namespace LosBarriosEvents.Pages;

[Authorize(Roles = "Administrators,Speakers")]

public class SpeakerFormsEditModel : PageModel
{

    private readonly ILogger<SpeakerFormsEditModel> _logger;
    private readonly ApplicationDbContext _context;
    private readonly UserManager<LosBarriosUser> _userManager;


    public SpeakerFormsEditModel(ILogger<SpeakerFormsEditModel> logger, ApplicationDbContext context, UserManager<LosBarriosUser> userManager)
    {
        _logger = logger;
        _context = context;
        _userManager = userManager;
    }

    [BindProperty]
    public SpeakerForm Form { get; set; }

    public IActionResult OnGet(int? id)
    {
        var userId = _userManager.GetUserId(User);

        SpeakerForm frm = _context.SpeakerForms.FirstOrDefault(f => f.SpeakerFormId == id);

        if (frm == null)
        {
            return NotFound();
        }

        Form = frm;

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var isAuthorized = User.IsInRole(UserRoles.LosBarriosEventsSpeakerRole) || User.IsInRole(UserRoles.LosBarriosEventsAdministratorRole);

        if (!isAuthorized)
        {
            return Forbid();
        }

        _context.SpeakerForms.Attach(Form).State = EntityState.Modified;

        await _context.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}

edit.cshtml

Which is binded to this Razorpage and called back when posted

@page
@model SpeakerFormsEditModel
@{
    ViewData["Title"] = "Home page";
}

<div class="text-center">
    <h1 class="display-4">Edit Speaker Form</h1>
</div>

<form id="SpeakerEditForm" method="post">
    <h2>Edit Form</h2>
    <hr />
    <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>

    <div class="form-floating mb-3">
        <input asp-for="Form.Title" class="form-control" aria-required="true" placeholder="Lecture Title" />
        <label asp-for="Form.Title">Lecture Title</label>
        <span asp-validation-for="Form.Title" class="text-danger"></span>
    </div>
    <div class="form-floating mb-3">
        <input asp-for="Form.Description" class="form-control" aria-required="true" placeholder="Description" />
        <label asp-for="Form.Description">Description of Lecture</label>
        <span asp-validation-for="Form.Description" class="text-danger"></span>
    </div>
    <div class="form-floating mb-3">
        <input asp-for="Form.Duration" class="form-control" aria-required="true" placeholder="Lecture Duration" />
        <label asp-for="Form.Duration">Duration</label>
        <span asp-validation-for="Form.Duration" class="text-danger"></span>
    </div>
    <div class="form-floating mb-3">
        <input asp-for="Form.SpeakerId" type="hidden"/>
    </div>
    <div class="form-floating mb-3">
        <input asp-for="Form.Speaker" type="hidden"/>
    </div>


    <button id="SpeakerFormSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Submit</button>
</form>
<a asp-page="./Index">Back to Forms</a>

I've tried a variation of pretty much every single stackoverflow, microsoft, and github post I could find and have found myself trying to throw something at the wall until it sticks for hours. With no luck I decided to finally try and post here. Whats strange to me is I did almost an exact copy of this code on another edit page for another model and was able to get that to work just fine. I feel like its something so trivial but I have been going crazy trying to figure this out. I have also dug through migrations to verify it was build correctly and found no errors to my knowledge. Anything helps and Thank you in advance


Solution

  • Just to have an official answer, all thanks to Ivan Stoev above, he mentions:

    The error message indicates that SpeakerFormId was 0 (zero, default for int) when calling Attach, so EFC considers it new and generates temporary value. What about the reason, model binding is not my area, but I guess you need a hidden input inside razor form to hold the received Form.SpeakerFormId and post it back along with the other properties

    It was my incorrect assumption that when crafting a cshtml page, I would only need to add any properties that I wanted to change/edit. This is untrue and the reason I kept receiving my error was because I was assuming the Primary key was being automatically carried over from the original queried object. This was not the case as I need to add the hidden property to the cshtml page so that it would get passed back as well like this:

    <input asp-for="Form.SpeakerFormId" type="hidden">
    

    To summarize, Make sure the complete object is listed and bound to the front end (cshtml) so that the entire object gets passed back. Including primary keys, foreign keys, object navigation properties (EFCore), and anything else.

    While I'm sure there is a way to only use certain properties on the front end and fill the rest on the back end, For the way I was programming it, this is the way that worked.