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

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

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

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.


My model worked as expected in EF Core 3.1. I upgraded to .Net 5 and EF Core 5, and EF adds shadow alternate key attribute(s) named TempId to several entities. EF can't load those entities unless I add those attributes to the database. The shadow alternate key properties are NOT used in any relationships that I can find in the model. Virtually all discussion of shadow properties is either for foreign keys or hidden attributes. I can't find any explanation for why EF would add a shadow alternate key, especially if it doesn't use the attribute. Any suggestions?

One of the entities that gets a shadow alternate key is Conference, which is the child in one relationship and the parent in another. I have many similar entities which do NOT get a shadow alternate key, and I cannot see any difference between them.

I loop through the model entities identifying all shadow properties and all relationships using an alternate key for the principal key. None of the shadow alternate keys are used in a relationship. I do see the two defined relationships where I specifically use an alternate key, so I believe my code is correct.

Here is a complete simplified EF context and its two entities which demonstrates the problem.

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");
            });
        }
    }
}

Here is the code I use to identify the shadow key.

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;
        }
    }
}

Here is the context initialization and processing code.

    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

The output I get is: EFShadow.Conference.TempId: Property: Conference.TempId (no field, int) Shadow Required AlternateKey AfterSave:Throw.

No relationship uses this alternate key.

If I eliminate the Meeting entity's inheritance from BaseEntity and include the RowVersion timestamp property directly in Meeting, no shadow key is generated. That's the only change required to make the difference.

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

Tricky confusing issue, worth reporting it to EF Core GitHub issue tracker.

Using trial and error approach, looks like the strange behavior is caused by the [NotMapped] data annotation applied to the base class.

Remove it from there (and all other similar places) and the problem is solved. In general don't apply that attribute on model classes. Normally you don't need to explicitly mark a class as "non entity" if its is not referenced by navigation property, DbSet or Entity<>() fluent call. And if you really want to make sure explicitly it isn't used as entity, use Ignore fluent API instead, because the attribute breaks the default conventions which are applied before OnModelCreating.

e.g.

//[NotMapped] <-- remove 
public abstract partial class BaseEntity
{
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

and optionally

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

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

    // the rest...
}