更新:下面列出的示例代码现已完成,足以在会议中生成影子备用键。当会议实体从包含RowVersion属性的基础实体继承时,会在会议实体中生成影子备用键。如果该属性直接包括在会议实体中,没有继承,则不会生成影子备用密钥。
我的模型在EF Core 3.1中按预期工作。我升级到.Net 5和EF Core 5,并且EF向多个实体添加了名为TempId的影子备用键属性。除非我将这些属性添加到数据库中,否则EF无法加载这些实体。我可以在模型中找到的任何关系中都没有使用阴影备用键属性。几乎所有有关阴影属性的讨论都针对外键或隐藏属性。我找不到关于EF为什么要添加阴影备用键的任何解释,特别是如果它不使用该属性的话。有什么建议么?
获得影子备用键的实体之一是会议,这是一种关系中的子级,另一种关系中的父级。我有许多相似的实体,它们没有阴影替代键,因此看不到它们之间的任何区别。
我使用主键的备用键遍历模型实体,以识别所有阴影属性和所有关系。关系中没有使用任何阴影备用键。我确实看到了两个定义的关系,在这些关系中我专门使用了备用键,因此我相信我的代码是正确的。
这是一个完整的简化EF上下文及其两个实体,充分说明了该问题。
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EFShadow
{
public partial class Conference
{
public Conference()
{
Meetings = new HashSet<Meeting>();
}
[Key]
public string ConferenceCode { get; set; }
[Required]
public string ConferenceName { get; set; }
public ICollection<Meeting> Meetings { get; }
}
public partial class Meeting : BaseEntity
{
public Meeting() { }
[Key]
public int MeetingId { get; set; }
[Required]
public string ConferenceCode { get; set; }
[Required]
public string Title { get; set; }
public Conference Conference { get; set; }
}
[NotMapped]
public abstract partial class BaseEntity
{
[Timestamp]
public byte[] RowVersion { get; set; }
}
public class EFShadowContext : DbContext
{
public EFShadowContext(DbContextOptions<EFShadowContext> options)
: base(options)
{
ChangeTracker.LazyLoadingEnabled = false;
}
public DbSet<Conference> Conferences { get; set; }
public DbSet<Meeting> Meetings { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Conference>(entity =>
{
entity.HasKey(e => e.ConferenceCode);
entity.ToTable("Conferences", "Settings");
entity.Property(e => e.ConferenceCode)
.IsRequired()
.HasMaxLength(25)
.IsUnicode(false)
.ValueGeneratedNever();
entity.Property(e => e.ConferenceName)
.IsRequired()
.HasMaxLength(100);
});
builder.Entity<Meeting>(entity =>
{
entity.HasKey(e => e.MeetingId);
entity.ToTable("Meetings", "Offerings");
entity.Property(e => e.ConferenceCode).HasMaxLength(25).IsUnicode(false).IsRequired();
entity.Property(e => e.Title).HasMaxLength(255).IsRequired();
//Inherited properties from BaseEntityWithUpdatedAndRowVersion
entity.Property(e => e.RowVersion)
.IsRequired()
.IsRowVersion();
entity.HasOne(p => p.Conference)
.WithMany(d => d.Meetings)
.HasForeignKey(d => d.ConferenceCode)
.HasPrincipalKey(p => p.ConferenceCode)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("Meetings_FK_IsAnOccurrenceOf_Conference");
});
}
}
}
这是我用来识别影子钥匙的代码。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace ConferenceEF.Code
{
public class EFModelAnalysis
{
readonly DbContext _context;
public EFModelAnalysis(DbContext context)
{
Contract.Requires(context != null);
_context = context;
}
public List<string> ShadowProperties()
{
List<string> results = new List<string>();
var entityTypes = _context.Model.GetEntityTypes();
foreach (var entityType in entityTypes)
{
var entityProperties = entityType.GetProperties();
foreach (var entityProperty in entityProperties)
{
if (entityProperty.IsShadowProperty())
{
string output = $"{entityType.Name}.{entityProperty.Name}: {entityProperty}.";
results.Add(output);
}
}
}
return results;
}
public List<string> AlternateKeyRelationships()
{
List<string> results = new List<string>();
var entityTypes = _context.Model.GetEntityTypes();
foreach (var entityType in entityTypes)
{
foreach (var fk in entityType.GetForeignKeys())
{
if (!fk.PrincipalKey.IsPrimaryKey())
{
string output = $"{entityType.DisplayName()} Foreign Key {fk.GetConstraintName()} " +
$"references principal ALTERNATE key {fk.PrincipalKey} " +
$"in table {fk.PrincipalEntityType}.";
results.Add(output);
}
}
}
return results;
}
}
}
这是上下文初始化和处理代码。
var connectionSettings = ((LoadDataConferencesSqlServer)this).SqlConnectionSettings;
DbContextOptionsBuilder builderShadow = new DbContextOptionsBuilder<EFShadowContext>()
.UseSqlServer(connectionSettings.ConnectionString);
var optionsShadow = (DbContextOptions<EFShadowContext>)builderShadow.Options;
using EFShadowContext contextShadow = new EFShadowContext(optionsShadow);
EFModelAnalysis efModelShadow = new EFModelAnalysis(contextShadow);
var shadowPropertiesShadow = efModelShadow.ShadowProperties();
foreach (var shadow in shadowPropertiesShadow)
progressReport?.Report(shadow); //List the shadow properties
var alternateKeysShadow = efModelShadow.AlternateKeyRelationships();
foreach (var ak in alternateKeysShadow)
progressReport?.Report(ak); //List relationships using alternate key
我得到的输出是:EFShadow.Conference.TempId:属性:Conference.TempId(无字段,整数)阴影必需AlternateKey AfterSave:抛出。
没有关系使用此备用键。
如果我消除了会议实体对BaseEntity的继承,并直接在会议中包含RowVersion时间戳属性,则不会生成任何影子密钥。这是唯一需要改变的改变。
棘手的令人困惑的问题,值得将其报告给EF Core GitHub问题跟踪器。
使用反复试验的方法,看起来奇怪的行为似乎是由NotMapped
应用于基类的[ ]数据注释引起的。
从那里(和所有其他类似的地方)将其删除,即可解决问题。通常,不要将该属性应用于模型类。通常,如果导航属性DbSet
或Entity<>()
流利调用未引用该类,则无需将其显式标记为“非实体” 。而且,如果你确实要明确确定它不会用作实体,请改用Ignore
fluent API,因为该属性违反了之前应用的默认约定OnModelCreating
。
例如
//[NotMapped] <-- remove
public abstract partial class BaseEntity
{
[Timestamp]
public byte[] RowVersion { get; set; }
}
以及可选
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Ignore<BaseEntity>(); // <-- add this
// the rest...
}
谢谢你,伊万!您的解决方案消除了完整模型和测试模型中的问题。非常感谢您为此付出的时间和精力。