.NET Core 3.1 ChangePasswordAsync 内部异常 "Cannot update Identity column"

.NET Core 3.1 ChangePasswordAsync Inner Exception "Cannot update Identity column"

我正在将 .NET Core Web API 从 2.2 升级到 3.1。在测试 ChangePasswordAsync 函数时,我收到以下错误消息:

Cannot update identity column 'UserId'.

I 运行 a SQL Profile 并且我可以看到 Identity 列未包含在 2.2 UPDATE 语句中,但它包含在 3.1 中。

有问题的代码行returns NULL,与成功或错误相对,如下:

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

ChangePasswordAsnyc 的实现如下(为简洁起见截断了代码)。

注意:AspNetUsers 扩展了 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));
}

UserId 与许多其他字段一起包含在 objResult 中,然后在方法结束时返回。据我所知,在无法单步执行 ChangePasswordAsync 方法的情况下,该函数会更新 objUser 中包含的所有字段。

问题:

如何禁止在 ChangePasswordAsnyc 生成的 UPDATE 语句中填充标识列?我需要在模型中添加属性吗?在将 ChangePasswordAsync 传递到 ChangePasswordAsync 之前,我是否需要从 objUser 中删除 UserId?或者,别的什么?

赏金问题 我创建了一个扩展 IdentityUser class 的自定义用户 class。在这个自定义 class 中,有一个额外的 IDENTITY 列。在将 .NET Core 2.2 升级到 3.1 时,ChangePasswordAsync 函数不再有效,因为 3.1 方法正在尝试更新此 IDENTITY 列,而这在 2.1 中不会发生。

除了升级和安装相关包外,没有任何代码更改。接受的答案需要解决问题。

更新

不使用迁移,因为这会导致数据库与 Web API 及其关联模型之间的强制联姻。在我看来,这违反了数据库、API 和 UI 之间的职责分离。但是,这又是微软老一套了。

我有自己定义的 ADD、UPDATE 和 DELETE 方法,它们使用 EF 并设置 EntityState。但是,当单步执行 ChangePasswordAsync 的代码时,我没有看到任何这些函数被调用。就好像 ChangePasswordAsync 使用了 EF 中的基本方法。所以,我不知道如何修改此行为。来自 Ivan 的回答。

注意:我做了 post 一个问题来尝试理解 ChangePasswordAsync 方法如何在此处调用 EF [有人可以解释 ChangePasswordAsnyc 方法的工作原理吗?][1].

命名空间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.
    }
}

命名空间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;
        }   
    }
}

您可以尝试使用 'EntityState' 属性 将属性标记为未修改?

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

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

创建和运行 迁移:

dotnet ef migrations add IdentityColumn
dotnet ef database update

不确定发生这种情况的确切时间,但最新的 EF Core 2 (2.2.6) 中存在相同的行为,并且与以下 SO 问题 中的问题完全相同。因此,我可以在我的答案中添加的内容不多:

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):

...

3.x 中的唯一区别是现在 AfterSaveBehavior 属性 有 GetAfterSaveBehaviorSetAfterSaveBehavior 方法。要让 EF Core 始终从更新中排除 属性,请将 SetAfterSaveBehaviorPropertySaveBehavior.Ignore 一起使用,例如

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

}