【问题标题】:.NET Core 3.1 ChangePasswordAsync Inner Exception "Cannot update Identity column".NET Core 3.1 ChangePasswordAsync 内部异常“无法更新标识列”
【发布时间】:2020-02-29 18:58:12
【问题描述】:

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

无法更新身份列“UserId”。

我运行了一个 SQL 配置文件,我可以看到 Identity 列未包含在 2.2 UPDATE 语句中,但它包含在 3.1 中。

有问题的代码行返回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 语句中填充标识列?我需要在模型中添加属性吗?在将objUser 传递到ChangePasswordAsync 之前,我是否需要从objUser 中删除UserId?或者,别的什么?

赏金问题 我创建了一个扩展 IdentityUser 类的自定义用户类。在这个自定义类中,有一个额外的 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 中的基本方法。所以,我不知道如何修改这种行为。来自伊万的回答。

注意:我确实发布了一个问题,试图了解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.
    }
}

实体回购

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

【问题讨论】:

  • 您是否也升级了实体框架?
  • @Mehrdad 是的,我将 NuGet 中的所有包都更新到了最新版本。
  • 您可以扩展 IdentityUser 以包含一个包含旧 UserId 的唯一字段,并让您的其他表链接到该字段
  • @PedroCoelho 我不确定添加另一个字段如何解决尝试更新作为标识列的现有字段的问题。
  • 对我来说,这是另一个证明您最好不要改变 ASP.Net 身份模型。如果您想扩展它,请创建您自己的表/类并将它们链接到 ASP.Net 对象。此外,IMO 这是一个错误。永远不应将标识列标记为已修改。不确定 EF core 3 是罪魁祸首还是 ASP.Net-core Identity。

标签: c# entity-framework-core asp.net-core-identity .net-core-3.1


【解决方案1】:

不确定这是什么时候发生的,但在最新的 EF Core 2 (2.2.6) 中存在相同的行为,并且与以下 SO 问题 Auto increment non key value entity framework core 2.0 中的问题完全相同。因此,我可以在我的答案中添加的内容不多:

这里的问题是,对于不是键的一部分的标识列(即ValueGeneratedOnAdd),AfterSaveBehaviorSave,这反过来又使Update 方法将它们标记为已修改,从而生成错误的UPDATE 命令。

要解决这个问题,您必须像这样设置AfterSaveBehavior(在OnModelCreating 覆盖内):

...

3.x 中的唯一区别是,现在您拥有 GetAfterSaveBehaviorSetAfterSaveBehavior 方法,而不是 AfterSaveBehavior 属性。要让 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); // <--
    });

}

【讨论】:

  • 感谢您对隔离这一事实的研究,即这是一个 EF 错误。关于解决方案,请看我的更新。
  • 除了实际的实体和属性名称之外,您的更新中究竟添加了什么?上述解决方案应该已经解决了相关问题,不是吗?在某些时候UserManager 要求IUserStore“更新用户”,IUserStore 的一个 EF Core 实现正在调用 dbset 或 dbcontext 的 Update 方法。然后 EF Core SaveChanges() 生成错误的 UPDATE 命令。按照建议配置 EF Core 更新行为通常应该可以解决该问题。请注意,它与迁移无关,因为该设置仅控制 EF Core 运行时行为。
  • 我已经发布了更新。这里的挑战是当传入集合是通用的时如何从实体中排除 UserId 属性?换句话说,该属性在智能感知中不可用。我是否需要设置一个 foreach 循环并搜索集合中的属性?或者,我是否必须在更高级别工作,实际 IdentityUser 将命令发送到EntityRepositoryBase。注意:进入ChangePasswordAsync 时不会执行此代码
  • 不确定你在说什么挑战。建议的解决方案是在 EF Core 实体 配置 级别,因此它处理 所有 用法 - UserManager、您的通用代码等。无论代码做什么,UserId 值都会不包含在UPDATE 命令中。这是你的问题。 “接受的答案需要解决问题。”,我相信上面确实解决了问题,你试过了吗?有什么问题?
  • 如果“这里的挑战”是指“从传入的通用集合中排除属性的最佳做法是什么?” 恐怕是您上次“更新”中的问题这是一个全新的问题,超出了原始帖子的范围,因此应该在具有不同标签的单独帖子中提出。
【解决方案2】:

您可以尝试使用“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

【讨论】:

    【解决方案3】:

    创建并运行迁移:

    dotnet ef migrations add IdentityColumn
    dotnet ef database update
    

    【讨论】:

    • 不使用迁移,因为这会导致数据库和 WebApi 及其关联模型之间的强制结合。在我们看来,这违反了数据库、API 和 UI 之间的职责分离。但是,这又是微软重蹈覆辙。
    • 您必须手动确保数据库中所有需要的列都可用...
    • 正确。然而,这不是这里的问题。 EF 3.0 中似乎引入了一个错误。请参阅Gert Arnold's 对我的问题发表评论。
    猜你喜欢
    • 1970-01-01
    • 2021-12-09
    • 2021-07-31
    • 1970-01-01
    • 2020-07-26
    • 1970-01-01
    • 2020-04-08
    • 2020-09-27
    • 2020-12-05
    相关资源
    最近更新 更多