EF Core 5 向某些实体添加了影子备用键,但不使用 属性

EF Core 5 adds shadow alternate key to some entities but does not use the property

UPDATED: The sample code listed below is now complete and sufficient to generate the shadow alternate key in Conference. When the Meeting entity inherits from a base entity containing a RowVersion attribute the shadow alternate key is generated in the Conference entity. If that attribute is included directly in the Meeting entity, without inheritance, the shadow alternate key is not generated.


我的模型在 EF Core 3.1 中按预期运行。我升级到 .Net 5 和 EF Core 5,并且 EF 将名为 TempId 的影子备用键属性添加到多个实体。除非我将这些属性添加到数据库中,否则 EF 无法加载这些实体。阴影备用键属性未用于我在模型中可以找到的任何关系。实际上所有关于影子属性的讨论都是针对外键或隐藏属性的。我找不到任何解释为什么 EF 会添加影子备用键,特别是如果它不使用该属性。有什么建议吗?

Conference 是获得影子备用键的实体之一,它是一种关系中的子项和另一种关系中的父项。我有许多类似的实体没有得到影子备用键,我看不出它们之间有什么区别。

我循环遍历模型实体,使用主键的备用键识别所有影子属性和所有关系。 None 个影子备用键用于关系中。我确实看到了我专门使用备用键的两个定义关系,所以我相信我的代码是正确的。

这是一个完整的简化 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 (无字段, int) Shadow Required AlternateKey AfterSave:Throw.

没有任何关系使用此备用键。

如果我消除 Meeting 实体对 BaseEntity 的继承并直接在 Meeting 中包含 RowVersion 时间戳 属性,则不会生成影子密钥。这是产生差异所需的唯一更改。

棘手的令人困惑的问题,值得向 EF Core GitHub 问题跟踪器报告。

使用试错法,看起来奇怪的行为是由应用于基础 class.

的 [NotMapped] 数据注释引起的

从那里(以及所有其他类似的地方)删除它,问题就解决了。通常不要在模型 classes 上应用该属性。如果 class 未被导航 属性、DbSetEntity<>() 流畅调用引用,通常您无需将其显式标记为“非实体”。如果你真的想明确地确保它不被用作实体,请改用 Ignore fluent API,因为该属性打破了 OnModelCreating.[= 之前​​应用的默认约定17=]

例如

//[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...
}