Warm tip: This article is reproduced from serverfault.com, please click

entity framework-EF Core 5将阴影备用键添加到某些实体,但不使用该属性

(entity framework - EF Core 5 adds shadow alternate key to some entities but does not use the property)

发布于 2020-11-27 14:07:08

更新:下面列出的示例代码现已完成,足以在会议中生成影子备用键。当会议实体从包含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时间戳属性,则不会生成任何影子密钥。这是唯一需要改变的改变。

Questioner
pjs
Viewed
0
Ivan Stoev 2020-11-28 22:43:15

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

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

从那里(和所有其他类似的地方)将其删除,即可解决问题。通常,不要将该属性应用于模型类。通常,如果导航属性DbSetEntity<>()流利调用未引用该类,则无需将其显式标记为“非实体” 而且,如果你确实要明确确定它不会用作实体,请改用Ignorefluent 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...
}