.NET Core 3.1 ChangePasswordAsync Inner Exception

2020-03-23 02:21发布

问题:

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

回答1:

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); // <--
    });

}


回答2:

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



回答3:

Create and Run migrations:

dotnet ef migrations add IdentityColumn
dotnet ef database update