.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 Framework
和
https://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
属性 有 GetAfterSaveBehavior
和 SetAfterSaveBehavior
方法。要让 EF Core 始终从更新中排除 属性,请将 SetAfterSaveBehavior
与 PropertySaveBehavior.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); // <--
});
}
我正在将 .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 Framework 和 https://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, theAfterSaveBehavior
isSave
, which in turn makesUpdate
method to mark them as modified, which in turn generates wrongUPDATE
command.To fix that, you have to set the
AfterSaveBehavior
like this (insideOnModelCreating
override):...
3.x 中的唯一区别是现在 AfterSaveBehavior
属性 有 GetAfterSaveBehavior
和 SetAfterSaveBehavior
方法。要让 EF Core 始终从更新中排除 属性,请将 SetAfterSaveBehavior
与 PropertySaveBehavior.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); // <--
});
}