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

pjs*_*pjs 5 entity-framework entity-framework-core ef-core-5.0

更新:下面列出的示例代码现已完整,足以在会议中生成影子备用密钥。当会议实体继承自包含 RowVersion 属性的基本实体时,会在会议实体中生成影子备用键。如果该属性直接包含在会议实体中而不是继承,则不会生成影子备用键。


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

获得影子备用密钥的实体之一是 Conference,它是一种关系中的子实体和另一种关系中的父实体。我有许多类似的实体,它们没有获得影子备用密钥,而且我看不出它们之间有任何区别。

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

这是一个完整简化的 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");
            });
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我用来识别影子密钥的代码。

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是上下文初始化和处理代码。

    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
Run Code Online (Sandbox Code Playgroud)

我得到的输出是:EFShadow.Conference.TempId:属性:Conference.TempId(无字段,int)Shadow required AlternateKey AfterSave:Throw。

没有关系使用此备用键。

如果我消除会议实体从 BaseEntity 的继承并直接在会议中包含 RowVersion 时间戳属性,则不会生成影子键。这是产生影响所需的唯一改变。

Iva*_*oev 5

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

使用试错方法,看起来奇怪的行为是由NotMapped应用于基类的 [ ] 数据注释引起的。

从那里(以及所有其他类似的地方)删除它,问题就解决了。一般来说,不要将该属性应用于模型类。DbSet通常,如果导航属性或流畅调用未引用某个类,则无需将其显式标记为“非实体” Entity<>()。如果您确实想明确地确保它不用作实体,请改用IgnoreFluent API,因为该属性打破了之前应用的默认约定OnModelCreating

例如

//[NotMapped] <-- remove 
public abstract partial class BaseEntity
{
    [Timestamp]
    public byte[] RowVersion { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

以及可选的

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Ignore<BaseEntity>(); // <-- add this

    // the rest...
}
Run Code Online (Sandbox Code Playgroud)