locked
Why Can't I Get the Scoped Identity From EF After a Record is Saved? RRS feed

  • Question

  • User-939035612 posted

    I have this code that successfully creates a new entry in my Posts table but for some reason the Postid is null even though I don't try to access it until after the new record is created. 

    public async Task<IActionResult> OnPostAsync()
    		{
    			if (!ModelState.IsValid)
    			{
    				return Page();
    			}
    			_context.Posts.Add(Posts);
    			await _context.SaveChangesAsync();
    			int postid = Posts.Postid;
    			string targeturl = linkgenerator.postlink(Posts.Postid, Posts.Title);
    			return RedirectToPage(targeturl);
    		}

    According to this tutorial all I need to do to access a newly created primary key is to type the name of the table followed by a "." and the name of the column https://www.entityframeworktutorial.net/faq/how-to-get-id-of-saved-entity-in-entity-framework.aspx but the above code returns a null value instead of the newly created Postid.

    Most of the new record is created using a form on a Razor Page. The above code is from code behind on that page. After checking the database I can confirm that the new record is in fact created. Please word any response in a way that someone familiar with WebForms and not familiar at all with MVC can understand. I chose to convert my WebForms site to a .Net Core Razor Pages site because I was given the impression that it is closesr to WebForms than any other modern option that uses the .Net Framework. So far it seems nothing like WebForms. Am I at least correct in believing that the OnPostAsync event in a Razor Page is the equivalent of the FormView_ItemInserting event in a WebForm?

    UPDATE: Looking back it appears that I was never able to use the new primary key value in my WebForm until the SqlDataSource1_Inserted event. Is it possible to get the primary key during the same event on a Razor Page or do I need to add a second task that runs after the first one?

    Tuesday, October 27, 2020 12:18 AM

Answers

  • User-939035612 posted

    I eventually changed all the code to create a new Posts object and assign all the values from Posts to my new object I called newPost like so:

    var newPost = new Posts();
    			newPost.Title = Posts.Title;
    await _context.Posts.AddAsync(newPost);
    			await _context.SaveChangesAsync();
    			int postid = newPost.Postid;
    

    That works, but there is still no explanation as to why I had to go to all that trouble. Nobody at Microsoft has yet to explain why their default method does not work with scope identity.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Tuesday, October 27, 2020 11:32 PM

All replies

  • User-939035612 posted

    This tutorial appears close to what I need but it is not asynchronous https://www.aspsnippets.com/Articles/Using-SCOPEIDENTITY-with-Entity-Framework-in-ASPNet-MVC.aspx Because I use async methods to get some of the data to insert with the record I have to use an async task. My goal with the above code was basically to convert the non-async wording to async wording. Why does this approach work with non-async tasks and not async tasks?

    Tuesday, October 27, 2020 1:26 AM
  • User-939035612 posted

    Based on what I've been reading about async, I am thinking that the problem might be int postid = Posts.Postid trying to get the Postid before

    await _context.SaveChangesAsync();

    is finished. So, how do I require int postid to wait for SaveChangesAsync() to complete before trying to get the Postid?

    Tuesday, October 27, 2020 1:43 AM
  • User-939035612 posted

    Looking for a work around that would at least allow me to work on other parts of the form that depend on the Postid I thought I'd just take the last Postid from the Posts table, but for some reason that is also null even though there are thousands of posts in the table

    int postid = _context.Posts.Last().Postid;

    That is null, why is it null?

    Tuesday, October 27, 2020 3:31 AM
  • User1120430333 posted

    Based on what I've been reading about async, I am thinking that the problem might be int postid = Posts.Postid trying to get the Postid before

    await _context.SaveChangesAsync();

    is finished. So, how do I require int postid to wait for SaveChangesAsync() to complete before trying to get the Postid?

    Well use a sync processing on the save or put some kind of delaying code to make the program wait after the async save is executed.

    https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay?view=netcore-3.1

    Tuesday, October 27, 2020 3:37 AM
  • User-939035612 posted

    I tried adding Task.WaitAll(); but that made no difference.

    Tuesday, October 27, 2020 3:58 AM
  • User1120430333 posted

    I tried adding Task.WaitAll(); but that made no difference.

    Well that's not waiting on a timer to elapse. Make it wait on a specified amount of time after the save is executed. The time is adjustable until you make it wait long enough to get the id out of the saved object or don't use async use a sync save.

    Tuesday, October 27, 2020 4:18 AM
  • User-939035612 posted

    I tired using _context.SaveChanges(); instead but the Postid is still null.

    Tuesday, October 27, 2020 4:23 AM
  • User1120430333 posted

    I tired using _context.SaveChanges(); instead but the Postid is still null.

    I would say that you may not be following the code in the link you have provided. When I persist an EF entity to the database, I new-up a new entity, populate its properties and then do the save in the insert method. The ID on an inserted record to the database table is saved to the ID property of the entity that is sitting there in memory that I newed-up in the insert method's code and populated the properties of the entity I am going to do an insert on.

    Of course, an ID = 0 tells EF that it is going to insert the entity to the table, and ID > 0 tells EF to update an existing record by its ID.

    If you have newed-up a new Posts entity, then EF is automatically going to set the ID to 0 in the entity. So I don't see how a Post.ID in a Posts entity is ever going to be null if the sequence of events to insert an EF entity is being done correctly.

    Tuesday, October 27, 2020 5:06 AM
  • User-939035612 posted

    Are you using a form in a Razor Page for that? I built mine starting with a standard CRUD page and then just modified it. That standard CRUD page never needed to create a new Posts object using c# the way that the tutorial does. The form on the page is basically like this:

    <form id="newpost" method="post">
            <div class="text-danger" asp-validation-summary="All">
            </div>
            <div class="form-group row">
                <label class="col-sm-2 col-form-label" asp-for="Posts.Title"></label>
                <div class="col-sm-7">
                    <input class="form-control" asp-for="Posts.Title" />
                </div>
                <span class="text-danger col-sm-3 col-form-label" asp-validation-for="Posts.Title"></span>
            </div>
            <button type="submit" value="Create" class="btn btn-primary">Submit Post</button>
        </form>

    Then the code behind is basically like this:

    private readonly ApplicationDbContext _context;
    
    		private UserManager<IdentityUser> _userManager;
    		public IndexModel(ApplicationDbContext context, UserManager<IdentityUser> userManager)
    		{
    			_context = context;
    			_userManager = userManager;
    		}
    		[BindProperty]
    		public Posts Posts
    		{
    			get;
    			set;
    		}
           public async Task<IActionResult> OnPostAsync()
    		{
    			if (!ModelState.IsValid)
    			{
    				return Page();
    			}
    			// Set Parameters That Don't Require User Input
    			if (User.Identity.IsAuthenticated)
    			{
    				IdentityUser user = await _userManager.GetUserAsync(User);
    				Posts.Userid = await _userManager.GetUserIdAsync(user);
                }
                else
                {
    				return Page();
                }
    			_context.Posts.Add(Posts);
    			_context.SaveChanges();
    			int postid = Posts.Postid;
    			string targeturl = linkgenerator.postlink(postid, Posts.Title);
    			return RedirectToPage(targeturl);
    		}

    If the method I linked to does not work then what works with a CRUD Page?

    The original WebForms site could get the Postid with this line of code in the SqlDataSource_Inserted event handler

    int postid = (int)e.Command.Parameters["@postid"].Value;

    What is the equivalent of that for a basic CRUD page such as the one in this tutoria? https://docs.microsoft.com/en-us/aspnet/core/data/ef-rp/crud?view=aspnetcore-3.1

    Here is an example of what you were referring to when you mentioned creating a new object manually (http://technotipstutorial.blogspot.com/2016/11/part-13-insert-data-into-multiple-table.html). That works fine if you are building a MVC controller from scratch, but not with an auto-generated CRUD Razor Page. The relevant part of its code is as follows:

    Employee emp = new Employee();
                    emp.Address = model.Address;
                    emp.Name = model.Name;
                    emp.DepartmentId = model.DepartmentId;
    
                    db.Employees.Add(emp);
                    db.SaveChanges();
    
                    int latestEmpId = emp.EmployeeId;

    That method does not work for me because creating the new object is handled by the CRUD page automatically without me having to add any code. You can replicate this by creating an ASP.Net Core 3.1 web application, scaffolding your db, right clicking on the Pages folder in Visual Studio, selecting Add new Razor Page, selecting Razor Pages using Entity Framework (CRUD), and selecting the primary table you want to work with. Then Visual Studio will generate the CRUD page for you automatically. That works great if you are only creating a record for one table, but to add data to a second table you need the primary key from that first table if a relationship exists between the two. Since I have no clue how exactly Microsoft does everything with a CRUD page since apparently there are things going on in addition to just what the code on the pages show, I need to know how to get the primary key from a CRUD page like that and that method cannot involve manually creating the new object for the main table because that is already done by Microsoft automatically.

    Tuesday, October 27, 2020 6:23 AM
  • User-821857111 posted

    If you are getting a null reference exception, it is because of something else in your code. The PostId will never be null because it is an int. Use the debugger to step through your code in the OnPostAsync method to see what's actually going on.

    Tuesday, October 27, 2020 7:13 AM
  • User-939035612 posted

    I tried the debugger and there were no build errors. Just some warnings. The only error comes up when I try to use the postid derived from the scoped identity for the first time. It says:

    System.NullReferenceException
      HResult=0x80004003
      Message=Object reference not set to an instance of an object.
      Source=PostAlmostAnything
      StackTrace:
       at PostAlmostAnything.Pages.post.IndexModel.<OnPostAsync>d__48.MoveNext() in C:\...\Pages\post\Index.cshtml.cs:line 178

    That really is nothing to go on since it just confirms that postid is null but without saying why. Then I checked the database and a new record was created and that record has a Postid. None of this explains why I can't get that post id by typing Post.Postid after adding data to Posts.

    The details include:

    Name Value Type
    $exception {"Object reference not set to an instance of an object."} System.NullReferenceException
    ▶ Data {System.Collections.ListDictionaryInternal} System.Collections.IDictionary {System.Collections.ListDictionaryInternal}
    HResult -2147467261 int
    HelpLink null string
    ▶ InnerException null System.Exception
    Message "Object reference not set to an instance of an object." string
    Source "PostAlmostAnything" string
    StackTrace " at PostAlmostAnything.Pages.post.IndexModel.<OnPostAsync>d__48.MoveNext() in C:\\Users\\sulli\\source\\repos\\PostAlmostAnything\\PostAlmostAnything\\Pages\\post\\Index.cshtml.cs:line 178" string
    ▶ TargetSite {Void MoveNext()} System.Reflection.MethodBase {System.Reflection.RuntimeMethodInfo}
    ▶ Static members
    ▶ Non-Public members
    Tuesday, October 27, 2020 7:39 AM
  • User-821857111 posted

    What is on line 178 of Index.cshtml.cs?

    Tuesday, October 27, 2020 7:56 AM
  • User-939035612 posted

    line 178 is just

    Community.Post = postid;

    It comes right before 

    _context.Community.Add(Community);

    The Community model is used when Posts.Category = 1 and gets its data from a partial view that is loaded dynamically when the category dropdown list is selected. It has different views for different categories. _CreateCommunity.cshtml contains the following:

    @model PostAlmostAnything.Pages.post.IndexModel
    @{
    }
    <div class="form-group row">
        <label class="col-sm-2 col-form-label" asp-for="Community.Date"></label>
        <div class="col-sm-5">
            <input class="form-control" asp-for="Community.Date" />
        </div>
        <span class="text-danger col-sm-3 col-form-label" asp-validation-for="Community.Date"></span>
    </div>
    

    The Community Model is as follows:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace PostAlmostAnything.Models
    {
        public partial class Community
        {
            [Key]
            public int Communityid { get; set; }
            [Required]
            public int? Post { get; set; }
            [Required(ErrorMessage="Date Required!")]
            [Display(Name = "Story or Event Date: ")]
            [DataType(DataType.DateTime)]
            public DateTime? Date { get; set; }
    
            public virtual Posts PostNavigation { get; set; }
        }
    }

    Tuesday, October 27, 2020 8:04 AM
  • User-821857111 posted

    CopBlaster

    line 178 is just

    Community.Post = postid;

    Seems to me that your code is working fine in terms of getting the ID for the newly created post. The error message you are getting has nothing to do with that operation. You get the error when you try to assign it to something else. But the error doesn't relate to the postid. It seems to relate to the Community object.

    Tuesday, October 27, 2020 10:02 AM
  • User1120430333 posted

    That works fine if you are building a MVC controller from scratch, but not with an auto-generated CRUD Razor Page. The relevant part of its code is as follows:

    The thing is that I create the Razor page manually and any code in a codebehind is created manually so that I have complete control of everything.

    Using the EF code for inserting an Employee and getting the returned Emoloyee.ID works the same in an ASP.NET MVC controller action method as opposed to an ASP.NET Razor page codebehind action method. EF is EF no matter what type of project code is using EF.

    Tuesday, October 27, 2020 3:57 PM
  • User-939035612 posted

    I eventually changed all the code to create a new Posts object and assign all the values from Posts to my new object I called newPost like so:

    var newPost = new Posts();
    			newPost.Title = Posts.Title;
    await _context.Posts.AddAsync(newPost);
    			await _context.SaveChangesAsync();
    			int postid = newPost.Postid;
    

    That works, but there is still no explanation as to why I had to go to all that trouble. Nobody at Microsoft has yet to explain why their default method does not work with scope identity.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Tuesday, October 27, 2020 11:32 PM
  • User1120430333 posted

    That works, but there is still no explanation as to why I had to go to all that trouble.

    It works because that's the way it's done where the individual EF entity/object is created in memory and populated as a standalone object,  and then it is persisted to the database table in order for EF to point back to the individual/standalone object and return the identity scoped ID back to the object's ID property.  It has always worked that way from day one when EF was released for usage starting with EF 3.5 back in year 2008. 

    This Razor CRUD page nonsense,  some wizard code I guess,  is really only for simplistic CRUD using EF. Getting the Identity ID back from EF for an object that was just inserted is not simplistic usage that causes the Razor CRUD code to not work in obtaining the ID of the inserted object via the object that was just inserted controlled by the Razor CRUD code.

    Nobody at Microsoft has yet to explain why their default method does not work with scope identity.

    Wizard generated code always falls short when you break the normal usage of wizard code. It's that simple.

    Wednesday, October 28, 2020 3:07 AM