6

I am upgrading a .NET Core Web API from 2.2 to 3.1. When testing the ChangePasswordAsync function, I receive the following error message:

Cannot update identity column 'UserId'.

I ran a SQL Profile and I can see that the Identity column is not included in the 2.2 UPDATE statement but it is in 3.1.

The line of code in question returns NULL, as opposed to success or errors, and is as follows:

objResult = await this.UserManager.ChangePasswordAsync(objUser, objChangePassword.OldPassword, objChangePassword.NewPassword);

The implementation of the ChangePasswordAsnyc is as follows (code truncated for brevity).

Note: AspNetUsers extends IdentityUser.

[HttpPost("/[controller]/change-password")]
public async Task<IActionResult> ChangePasswordAsync([FromBody] ChangePassword objChangePassword)
{
    AspNetUsers objUser = null; 
    IdentityResult objResult = null;    

    // retrieve strUserId from the token.

    objUser = await this.UserManager.FindByIdAsync(strUserId);
    objResult = await this.UserManager.ChangePasswordAsync(objUser, objChangePassword.OldPassword, objChangePassword.NewPassword);
    if (!objResult.Succeeded)
    {
        // Handle error.
    }

    return this.Ok(new User(objUser));
}

The UserId is included in the objResult, along with a lot of other fields, which is then returned at the end of the method. From what I can tell, without being able to step through the ChangePasswordAsync method, the function updates all fields that are contained in the objUser.

Question:

How do I suppress the identity column from being populated in the UPDATE statement that ChangePasswordAsnyc is generating? Do I need to add an attribute in the model? Do I need to remove the UserId from the objUser before passing it into the ChangePasswordAsync? Or, something else?

Bounty Question I have created a custom user class that extends the IdentityUser class. In this custom class, there is an additional IDENTITY column. In upgrading the .NET Core 2.2 to 3.1, the ChangePasswordAsync function no longer works because the 3.1 method is trying to UPDATE this IDENTITY column whereas this does not happen in 2.1.

There was no code change other than upgrading an installing the relevant packages. The accepted answer needs to fix the problem.

UPDATE

Migrations are not used as this results in a forced marriage between the database and the Web API with it's associated models. In my opinion, this violated the separation of duties between database, API, and UI. But, that's Microsoft up to its old ways again.

I have my own defined ADD, UPDATE, and DELETE methods that use EF and set the EntityState. But, when stepping through the code for ChangePasswordAsync, I do not see any of these functions called. It is as if ChangePasswordAsync uses the base methods in EF. So, I do not know how to modify this behavior. from Ivan's answer.

Note: I did post a question to try and understand how the ChangePasswordAsync method calls EF here [Can someone please explain how the ChangePasswordAsnyc method works?][1].

namespace ABC.Model.AspNetCore

using ABC.Common.Interfaces;
using ABC.Model.Clients;
using Microsoft.AspNetCore.Identity;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ABC.Model.AspNetCore
{
    // Class for AspNetUsers model
    public class AspNetUsers : IdentityUser
    {
        public AspNetUsers()
        {
            // Construct the AspNetUsers object to have some default values here.
        }

        public AspNetUsers(User objUser) : this()
        {
            // Populate the values of the AspNetUsers object with the values found in the objUser passed if it is not null.
            if (objUser != null)
            {
                this.UserId = objUser.UserId; // This is the problem field.
                this.Email = objUser.Email;
                this.Id = objUser.AspNetUsersId;
                // Other fields.
            }
        }

        // All of the properties added to the IdentityUser base class that are extra fields in the AspNetUsers table.
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        [Key]
        public int UserId { get; set; } 
        // Other fields.
    }
}

namespace ABC.Model.Clients

using ABC.Model.AspNetCore;
using JsonApiDotNetCore.Models;
using System;
using System.ComponentModel.DataAnnotations;

namespace ABC.Model.Clients
{
    public class User : Identifiable
    {
        public User()
        {
            // Construct the User object to have some default values show when creating a new object.
        }

        public User(AspNetUsers objUser) : this()
        {
            // Populate the values of the User object with the values found in the objUser passed if it is not null.
            if (objUser != null)
            {
                this.AspNetUsersId = objUser.Id;
                this.Id = objUser.UserId;    // Since the Identifiable is of type Identifiable<int> we use the UserIdas the Id value.
                this.Email = objUser.Email;
                // Other fields.
            }
        }

        // Properties
        [Attr("asp-net-users-id")]
        public string AspNetUsersId { get; set; }

        [Attr("user-id")]
        public int UserId { get; set; }

        [Attr("email")]
        public string Email { get; set; }

        [Attr("user-name")]
        public string UserName { get; set; }

        // Other fields.
    }
}

EntitiesRepo

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace ABC.Data.Infrastructure
{  
    public abstract class EntitiesRepositoryBase<T> where T : class
    {
        #region Member Variables
        protected Entities m_DbContext = null;
        protected DbSet<T> m_DbSet = null;
        #endregion


        public virtual void Update(T objEntity)
        {
            this.m_DbSet.Attach(objEntity);
            this.DbContext.Entry(objEntity).State = EntityState.Modified;
        }   
    }
}
J Weezy
  • 3,153
  • 2
  • 27
  • 67
  • did you upgrade your entityframework too? – Mehrdad Feb 26 '20 at 15:06
  • @Mehrdad Yes, I updated all of the packages in NuGet to their most recent version. – J Weezy Feb 26 '20 at 21:03
  • You could extend IdentityUser to include a unique field that contains the old UserId, and have your other tables link to that instead – Pedro Coelho Feb 26 '20 at 22:46
  • @PedroCoelho I'm not sure how adding another field solves the problem of trying to update an already existing field that is an identity column. – J Weezy Feb 26 '20 at 23:47
  • 2
    For me, this is yet another proof that you better leave the ASP.Net identity model unaltered. If you want to extend it, create your own tables/classes and link them to the ASP.Net objects. Also, IMO this is a bug. An identity column should never be marked as modified. Not sure if EF core 3 is the culprit or ASP.Net-core Identity. – Gert Arnold Feb 28 '20 at 10:10
  • @Gert Variable name `strUserId` sounds like `string` data type. Wondering how it could be identity then. – Ivan Stoev Feb 28 '20 at 10:16
  • @IvanStoev it is UserId, not strUserId, of type Int. – J Weezy Feb 28 '20 at 10:18
  • @GertArnold Any chance you can confirm where the bug lies? I will be happy to file a bug on Git. I am strongly leaning towards EF. – J Weezy Feb 28 '20 at 10:28
  • @JWeezy So you are using identity column which is not the PK? Please confirm that, or better add the relevant part of your custom user class and fluent configuration. – Ivan Stoev Feb 28 '20 at 12:27

3 Answers3

5

Not sure exactly when this happened, but the same behavior exists in the latest EF Core 2 (2.2.6), and also is exactly the same issue as in the following SO question Auto increment non key value entity framework core 2.0. Hence there is not much that I can add to my answer there:

The problem here is that for identity columns (i.e. ValueGeneratedOnAdd) which are not part of a key, the AfterSaveBehavior is Save, which in turn makes Update method to mark them as modified, which in turn generates wrong UPDATE command.

To fix that, you have to set the AfterSaveBehavior like this (inside OnModelCreating override):

...

The only difference in 3.x is that now instead of AfterSaveBehavior property you have GetAfterSaveBehavior and SetAfterSaveBehavior methods. To let EF Core always exclude the property from updates, use SetAfterSaveBehavior with PropertySaveBehavior.Ignore, e.g.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

...

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    
    modelBuilder.Entity<AspNetUsers>(builder =>
    {
        builder.Property(e => e.UserId).ValueGeneratedOnAdd()
            .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore); // <--
    });

}
Community
  • 1
  • 1
Ivan Stoev
  • 179,274
  • 12
  • 254
  • 292
  • Thank you for your research on isolating the fact that this is an EF bug. With respect to the solution, please see my update. – J Weezy Feb 28 '20 at 21:01
  • 1
    Other than the actual entity and property name, what exactly is added in your update? The above solution should have fixed the issue in question, isn't it? At some point `UserManager` asks `IUserStore` to "update the user", ane EF Core implementation of `IUserStore` is calling `Update` method of the dbset or dbcontext. Then EF Core `SaveChanges()` generates wrong `UPDATE` command. Configuring EF Core update behavior as suggested should generically fix the issue. Note that it has nothing to do with migrations, because the setting controls only EF Core runtime behavior. – Ivan Stoev Feb 29 '20 at 01:56
  • I have posted an update. The challenge here is how to exclude the UserId property from the Entity when the incoming collection is generic? In other words, the property is not available in intellisense. Do I need to setup a foreach loop and search for the property in the collection? Or, am I going to have to work at a higher level where the actual IdentityUser sends commands to the `EntityRepositoryBase`. Note: none of this code its executed when stepping into `ChangePasswordAsync` – J Weezy Feb 29 '20 at 08:26
  • 1
    Not sure what challenge are you talking about. The suggested solution is at EF Core entity **configuration** level, so it handles **all** usages - `UserManager`, your generic code etc. Whatever the code does, `UserId` value will not be included in `UPDATE` command. Which was your question. *"The accepted answer needs to fix the problem."*, I believe the above does fix the problem, have you tried it and what's the issue with it? – Ivan Stoev Feb 29 '20 at 10:05
  • If by "challenge here" you mean the *"What is best practice to exclude property from incoming generic collection?"* question in your last "update", I'm afraid it's a brand new question which falls outside the scope of the original post, hence should be asked in a separate post with different tags. – Ivan Stoev Feb 29 '20 at 10:19
  • Thank you! Also, you basically explained how `CreatePasswordAsync` saves through EF, which I have a separate question on. If you want to answer that then I will check it. https://stackoverflow.com/questions/60444513/can-someone-please-explain-how-the-changepasswordasnyc-method-works – J Weezy Feb 29 '20 at 10:46
1

You could try to use the 'EntityState' property to mark the proptery as unmodified?

db.Entry(model).State = EntityState.Modified;
db.Entry(model).Property(x => x.UserId).IsModified = false;
db.SaveChanges();

see Exclude Property on Update in Entity Framework and https://docs.microsoft.com/en-us/ef/ef6/saving/change-tracking/entity-state

Enrico
  • 1,942
  • 1
  • 18
  • 33
0

Create and Run migrations:

dotnet ef migrations add IdentityColumn
dotnet ef database update
McKabue
  • 1,896
  • 1
  • 16
  • 32
  • 1
    Migrations are not used as this results in a forced marriage between the database and the WebApi with it's associated models. In our opinion, this violated the separation of duties between database, API, and UI. But, that's Microsoft up to its old ways again. – J Weezy Feb 28 '20 at 10:26
  • You would then have to manually make sure all the needed columns are available in the DB... – McKabue Feb 28 '20 at 10:29
  • Correct. However, that is not what is at issue here. There appears to be a bug introduced in EF 3.0. See `Gert Arnold's` comment to my question. – J Weezy Feb 28 '20 at 10:31