【问题标题】:EF Core 5 adds shadow alternate key to some entities but does not use the propertyEF Core 5 为某些实体添加了影子备用键但不使用该属性
【发布时间】:2021-03-10 07:38:10
【问题描述】:

更新:下面列出的示例代码现已完整且足够 在会议中生成影子备用键。当会议 实体继承自包含 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 (no field, int) Shadow 需要 AlternateKey AfterSave:Throw。

没有关系使用这个备用键。

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

【问题讨论】:

  • 无法使用提供的代码重现。我在干净的环境中,与您发布的唯一区别是评论以 entity.HasOne(d =&gt; d.CurrentPhase) 开头的行(没有这样的属性/实体)和行 base.Configure(entity); (不知道里面有什么)跨度>
  • 谢谢伊万。完整模型太大而无法包含。我会看看一个缩减模型是否会产生同样的问题,然后发布完整的模型。
  • 我不知道这是不是好消息,但是只有 3 个实体的缩减模型没有添加阴影备用键。我从主模型中删除了尽可能多的实体,并且仍然生成了所有阴影备用键。 Model.DebugView 列出了影子键属性,并且没有列出这些键的任何用法。进一步的模型简化是困难的,因为其余的实体都参与了关系。对于确定原因还有其他建议吗?
  • 原帖现在已更新为可演示问题的工作代码。
  • 不幸的是,更新后的示例仍然没有重现该问题。所以我相信这里仍然有一些没有显示的东西导致它。让我们从名称“TempId”开始。是否有一个名为“Temp”的类具有属性“Id”? OnModelCreating 中是否有包含字符串“TempId”的代码?代码中是否有字符串“TempId”?等等。这个名称应该来自某个地方,EF Core 不使用硬编码的名称。

标签: entity-framework entity-framework-core ef-core-5.0


【解决方案1】:

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

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

从那里(以及所有其他类似的地方)删除它,问题就解决了。通常不要将该属性应用于模型类。通常,如果导航属性、DbSetEntity&lt;&gt;() 流畅调用未引用某个类,则通常不需要将其显式标记为“非实体”。如果你真的想明确地确保它不被用作实体,请改用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...
}

【讨论】:

  • 谢谢伊万!!!您的解决方案消除了完整模型和测试模型中的问题。非常感谢您为此付出的时间和精力。
猜你喜欢
  • 1970-01-01
  • 2019-11-26
  • 2023-02-04
  • 1970-01-01
  • 2021-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多