EF Core修改IdentityProvider的Scheme备用键报错问题求助
问题描述
尝试更新IdentityProvider实体的Scheme属性(该属性是备用键,同时也是IdentityProviderConfiguration实体的外键),即便该实体没有对应的IdentityProviderConfiguration子实体,仍抛出如下异常:
System.InvalidOperationException: The property 'IdentityProvider.Scheme' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key, first delete the dependent and invoke 'SaveChanges', and then associate the dependent with the new principal.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectValueChange(InternalEntityEntry entry, IProperty property)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.LocalDetectChanges(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager)
at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.DetectChanges()
at Microsoft.EntityFrameworkCore.DbContext.TryDetectChanges()
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
相关实体代码
public class IdentityProvider { public int Id { get; set; } public string Scheme { get; set; } public string DisplayName { get; set; } public bool Enabled { get; set; } public string Type { get; set; } public string Properties { get; set; } public DateTime Created { get; set; } public DateTime? Updated { get; set; } public DateTime? LastAccessed { get; set; } public bool NonEditable { get; set; } public virtual IdentityProviderConfiguration IdentityProviderConfiguration { get; set; } } public class IdentityProviderConfiguration { public string Scheme { get; set; } public string Options { get; set; } public virtual IdentityProvider IdentityProvider { get; set; } }
EF Core配置代码
public class IdentityProviderConfiguration : IEntityTypeConfiguration<IdentityProvider> { public void Configure(EntityTypeBuilder<IdentityProvider> entity) { entity.HasIndex(e => e.Scheme, "AK_IdentityProviders_Scheme") .IsUnique(); entity.HasIndex(e => e.Scheme, "IX_IdentityProviders_Scheme") .IsUnique(); entity.Property(e => e.DisplayName).HasMaxLength(200); entity.Property(e => e.Scheme) .IsRequired() .HasMaxLength(200); entity.Property(e => e.Type) .IsRequired() .HasMaxLength(20); entity.HasOne(e => e.IdentityProviderConfiguration) .WithOne() .OnDelete(DeleteBehavior.Cascade); entity.ToTable("IdentityProviders"); } } public class IdentityProviderConfigurationConfiguration : IEntityTypeConfiguration<IAM.BackOffice.Core.Entities.AuthZ.IdentityProviderConfiguration> { public void Configure(EntityTypeBuilder<Core.Entities.AuthZ.IdentityProviderConfiguration> entity) { entity.HasKey(e => e.Scheme); entity.HasIndex(e => e.Scheme, "IX_IdentityProviderConfigurations_Scheme") .IsUnique(); entity.Property(e => e.Scheme).HasMaxLength(200); entity.HasOne(d => d.IdentityProvider) .WithOne(p => p.IdentityProviderConfiguration) .HasPrincipalKey<IdentityProvider>(p => p.Scheme) .HasForeignKey<Core.Entities.AuthZ.IdentityProviderConfiguration>(d => d.Scheme); entity.ToTable("IdentityProviderConfigurations"); } }
解决方案
原因分析
EF Core 不允许直接修改**备用键(Alternate Key)**属性,因为备用键被用作关联实体的标识外键时,会被视为实体标识的一部分——即便当前没有关联的从属实体,EF Core的模型验证逻辑仍会阻止修改这类属性。
具体解决步骤
方案1:重新设计模型,避免修改备用键
如果业务允许,建议将Scheme设为不可修改属性,改用Id作为主外键关联IdentityProviderConfiguration:
- 修改
IdentityProviderConfiguration实体,新增主键与关联外键:
public class IdentityProviderConfiguration { public int Id { get; set; } // 新增主键 public int IdentityProviderId { get; set; } // 关联IdentityProvider.Id的外键 public string Scheme { get; set; } public string Options { get; set; } public virtual IdentityProvider IdentityProvider { get; set; } }
- 更新EF Core配置,调整关联关系:
// IdentityProvider配置类中修改关联逻辑 entity.HasOne(e => e.IdentityProviderConfiguration) .WithOne(p => p.IdentityProvider) .HasForeignKey<IdentityProviderConfiguration>(d => d.IdentityProviderId) .OnDelete(DeleteBehavior.Cascade); // IdentityProviderConfiguration配置类中修改主键与关联 entity.HasKey(e => e.Id); entity.HasOne(d => d.IdentityProvider) .WithOne(p => p.IdentityProviderConfiguration) .HasForeignKey<IdentityProviderConfiguration>(d => d.IdentityProviderId);
修改后Scheme仅作为普通唯一索引字段,可自由编辑。
方案2:删除原实体再重建(适用于必须修改Scheme的场景)
若业务要求必须修改Scheme,可采用“删除旧实体+创建新实体”的方式实现:
// 查询原实体及关联配置 var oldProvider = await dbContext.IdentityProviders .Include(p => p.IdentityProviderConfiguration) .FirstAsync(p => p.Id == providerId); // 删除原实体及关联数据 dbContext.IdentityProviders.Remove(oldProvider); if (oldProvider.IdentityProviderConfiguration != null) { dbContext.IdentityProviderConfigurations.Remove(oldProvider.IdentityProviderConfiguration); } await dbContext.SaveChangesAsync(); // 创建新实体并复制原属性(Scheme设为新值) var newProvider = new IdentityProvider { Scheme = newScheme, DisplayName = oldProvider.DisplayName, Enabled = oldProvider.Enabled, Type = oldProvider.Type, Properties = oldProvider.Properties, Created = oldProvider.Created, Updated = DateTime.UtcNow, LastAccessed = oldProvider.LastAccessed, NonEditable = oldProvider.NonEditable }; // 同步创建新的关联配置(如果存在) if (oldProvider.IdentityProviderConfiguration != null) { newProvider.IdentityProviderConfiguration = new IdentityProviderConfiguration { Scheme = newScheme, Options = oldProvider.IdentityProviderConfiguration.Options }; } // 保存新实体 dbContext.IdentityProviders.Add(newProvider); await dbContext.SaveChangesAsync();
方案3:直接执行SQL绕过EF Core验证
若上述方案均不适用,可直接执行原始SQL修改Scheme(需手动保证数据一致性):
// 更新IdentityProvider的Scheme await dbContext.Database.ExecuteSqlInterpolatedAsync( $"UPDATE IdentityProviders SET Scheme = {newScheme} WHERE Id = {providerId}"); // 同步更新关联的IdentityProviderConfiguration(如果存在) await dbContext.Database.ExecuteSqlInterpolatedAsync( $"UPDATE IdentityProviderConfigurations SET Scheme = {newScheme} WHERE Scheme = {oldScheme}");
内容的提问来源于stack exchange,提问作者pampua84

