Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further relax value-generation restrictions on composite derived FKs #11939

Closed
paultobey opened this issue May 8, 2018 · 15 comments · Fixed by #22372
Closed

Further relax value-generation restrictions on composite derived FKs #11939

paultobey opened this issue May 8, 2018 · 15 comments · Fixed by #22372
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. punted-for-2.2 punted-for-3.0 type-bug
Milestone

Comments

@paultobey
Copy link

paultobey commented May 8, 2018

I thought I'd posted this some time ago, but can't find that issue.

I have an EF Core 2.1 preview application which uses three entities. Each entity has a multiple-component primary key, an auto-generated ID and a "CompanyId". One of these entities references another, optionally. The code is roughly like this (I've changed some names because we are very sensitive about sharing code):

Class declarations:

	public abstract partial class FE
	{
		public FE()
		{
		}

		public int Id { get; set; }

		public int CompanyId { get; set; }

		public string Name { get; set; }
	}

	public partial class FB : FE
	{
		public FB()
		{
		}

		public int? TSId { get; set; } = null;

		public virtual S TS { get; set; }
	}

	public partial class S
	{
		public S()
		{
		}

		public int Id { get; set; }

		public int CompanyId { get; set; }

		// Other properties of various types here.
	}

Entity descriptions:

	modelBuilder.Entity<FE>(entity =>
	{
		entity.HasKey(e => new
		{
			e.Id,
			e.CompanyId
		})
				 .HasName("PK_FE");

		entity.Property(e => e.Id)
				 .HasColumnName("ID")
				 .ValueGeneratedOnAdd();

		entity.Property(e => e.CompanyId)
				 .HasColumnName("CompanyID")
				 .HasDefaultValueSql("1");

		entity.Property(e => e.Name)
				 .IsRequired()
				 .HasMaxLength(256);

	});

	// FB is a subclass, in .NET, of FE, so inherits its primary key setup. We are using table-per-hierarchy, 
	// so a Discriminator column is used to identify the type.
	modelBuilder.Entity<FB>(entity =>
	{
		// Here is the reference of interest. Note that it's optional, int?
		entity.Property<int?>("TSId")
				 .HasColumnName("TSId");

		// And the foreign key description. Note that the foreign key uses the same CompanyId
		// value as the FB object. That is, we want this foreign key to refer to "S's" which have the
		// same CompanyId as the FB's which refer to them.
		entity.HasOne(mv => mv.TS)
				 .WithMany()
				 .HasForeignKey(mv => new
				 {
					 mv.TSId,
					 mv.CompanyId
				 })
				 .OnDelete(DeleteBehavior.ClientSetNull);
	});

	modelBuilder.Entity<S>(entity =>
	{
		entity.HasKey(e => new
		{
			e.Id,
			e.CompanyId
		})
				.HasName("PK_S");

		entity.Property(e => e.Id)
				.HasColumnName("ID")
				.ValueGeneratedOnAdd();

		entity.Property(e => e.CompanyId)
				.HasColumnName("CompanyID")
				.HasDefaultValueSql("1");

		//	Other properties of various types here.
	});

The issue occurs when a referenced S is deleted from the S table. We now have a dead reference from one or more FB's which we want to null. Before the delete occurs, I'm assuring that the FB list is loaded into memory for EF Core to use. It seems to be finding the dead reference and trying to null it, but:

System.InvalidOperationException : The property 'CompanyId' on entity type 'FB' is defined to be read-only after it has been saved, but its value has been modified or marked as modified.

So, it might be successfully clearing the SId reference, but, because CompanyId is a portion of the primary key of the FB object, it can't null it (and, of course, I don't want it to). What I hope is for EF Core to null the nullable part of the foreign key and ignore the non-nullable part.

Steps to reproduce

Further technical details

EF Core version: 2.1.0-preview2-final
Database Provider: SQL Server 2016
Operating system: Windows 7/64
IDE: Visual Studio 2017 15.7

@paultobey
Copy link
Author

Note also that I can write pre-S-delete code enumerating FB's referencing the to-be-deleted-S and setting each TS navigation property to null. I was expecting the same invalid operation exception for that case, but it seems that explicitly nulling it and nulling it as part of ClientSetNull don't work the same.

@ajcvickers
Copy link
Member

Notes for triage:

  • I was able to repro this; full code below.
  • In 2.0, the model does not build--gives error "The property 'CompanyId' cannot be part of a foreign key on 'FB' because it has value generation enabled and is contained in the key {'Id', 'CompanyId'} defined on a base entity type 'FE'.
public abstract class FE
{
    public int Id { get; set; }
    public int CompanyId { get; set; }
    public string Name { get; set; }
}

public class FB : FE
{
    public int? TSId { get; set; } = null;
    public virtual S TS { get; set; }
}

public class S
{
    public int Id { get; set; }
    public int CompanyId { get; set; }
}

public class Program
{
    public static void Main()
    {
        using (var context = new BloggingContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var fb = context.Add(new FB
            {
                Name = "Wubbsy, Inc",
                TS = new S()
            }).Entity;

            context.SaveChanges();
        }

        using (var context = new BloggingContext())
        {
            var fb = context.Set<FB>().Single();
            var s = context.Set<S>().Single();

            Console.WriteLine($"FB.Id = {fb.Id}; FB.CompanyId = {fb.CompanyId}");
            Console.WriteLine($"S.Id = {s.Id}; S.CompanyId = {s.CompanyId}");
            Console.WriteLine($"FB.TS.Id = {fb.TS.Id}; FB.TS.CompanyId = {fb.TS.CompanyId}");

            context.Remove(s);

            context.SaveChanges();
        }
    }
}

public class BloggingContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<FE>(entity =>
        {
            entity.HasKey(e => new
                {
                    e.Id,
                    e.CompanyId
                })
                .HasName("PK_FE");

            entity.Property(e => e.Id)
                .HasColumnName("ID")
                .ValueGeneratedOnAdd();

            entity.Property(e => e.CompanyId)
                .HasColumnName("CompanyID")
                .HasDefaultValueSql("1");

            entity.Property(e => e.Name)
                .IsRequired()
                .HasMaxLength(256);

        });

        // FB is a subclass, in .NET, of FE, so inherits its primary key setup. We are using table-per-hierarchy, 
        // so a Discriminator column is used to identify the type.
        modelBuilder.Entity<FB>(entity =>
        {
            // Here is the reference of interest. Note that it's optional, int?
            entity.Property<int?>("TSId")
                .HasColumnName("TSId");

            // And the foreign key description. Note that the foreign key uses the same CompanyId
            // value as the FB object. That is, we want this foreign key to refer to "S's" which have the
            // same CompanyId as the FB's which refer to them.
            entity.HasOne(mv => mv.TS)
                .WithMany()
                .HasForeignKey(mv => new
                {
                    mv.TSId,
                    mv.CompanyId
                })
                .OnDelete(DeleteBehavior.ClientSetNull);
        });

        modelBuilder.Entity<S>(entity =>
        {
            entity.HasKey(e => new
                {
                    e.Id,
                    e.CompanyId
                })
                .HasName("PK_S");

            entity.Property(e => e.Id)
                .HasColumnName("ID")
                .ValueGeneratedOnAdd();

            entity.Property(e => e.CompanyId)
                .HasColumnName("CompanyID")
                .HasDefaultValueSql("1");

            //	Other properties of various types here.
        });
    }
}

@wmkew
Copy link

wmkew commented May 31, 2018

I just downloaded the 2.1.0 bits, and I am now encountering the same error that @ajcvickers mentioned that was yielded by 2.0: "The property 'CompanyId' cannot be part of a foreign key on 'FB' because it has value generation enabled and is contained in the key {'Id', 'CompanyId'} defined on a base entity type 'FE'." Did I miss something?

@ajcvickers
Copy link
Member

@wmkew Two questions:

  • What version of EF Core were you using before upgrading to 2.1?
  • Do you feel that the exception message is incorrect? If so, in what way?

@wmkew
Copy link

wmkew commented May 31, 2018

@ajcvickers I am currently on EF Core version 2.1.0-preview2-final, and I don't get this error when adding migrations. As soon as I upgraded to 2.1.0, I started seeing this error, even though my models haven't changed. I have a class design similar to the one above; it seems that this should be a valid design, so I'm wondering why I'm even getting an error. If this is indeed a bug, is there a workaround?

@paultobey
Copy link
Author

This does seem like a back-step and locks me into EF Core 2.1 preview-2 final, also, I think.

I'm using table-per-hierarchy; I would expect the table structure to be derived from a flattened version of the class hierarchy.

There's only one CompanyId in the FB hierarchy. It's public. I don't see any reason why it can't be both a part of the primary key for FB and part of a foreign key from FB to S.

@ajcvickers
Copy link
Member

@paultobey @wmkew What the error is saying is that it doesn't make sense to have value generation for a foreign key property because valid values for that property are defined by the property at the principal end of the relationship. Therefore, any generated values will be wrong as they will not, other than accidentally, conform to the FK constraint.

@AndriySvyryd
Copy link
Member

AndriySvyryd commented May 31, 2018

@paultobey @wmkew Before 2.1.0 this wasn't detected consistently. You can workaround this by ignoring the base type or be removing the default value from CompanyId.

We could further relax the restriction by moving this check to key generation and only throwing if no value was provided, but then specifying a default value would be pointless anyway as it would never be used.

@paultobey
Copy link
Author

@AndriySvyryd Thanks. So it sounds like you're saying that the default value 'attribute' is causing the message and, if we simply did not specify any automatic value generation all would be well. Am I getting that right?

@AndriySvyryd
Copy link
Member

@paultobey Correct

@paultobey
Copy link
Author

@ajcvickers OK, I think I can live with that.

@wmkew
Copy link

wmkew commented Jun 1, 2018

I can confirm that once I remove HasDefaultValueSql from CompanyId in the base classes, I can add migrations now. @ajcvickers I actually was thrown by the exception message, specifically by the phrase "value generation enabled". When I saw that, it made me think of ValueGeneratedOnAdd, which is on Id, but not CompanyId, so initially I was thinking, "wait a minute, CompanyId doesn't have value generation." It didn't occur to me that HasDefaultValueSql also falls into the "value generation" category, so I didn't think to remove it.

@ajcvickers
Copy link
Member

@wmkew I was perhaps too hasty in my comments yesterday. I agree the message is confusing when HasDefaultValueSql is used--I don't think we were considering this case when it was written. Also, when the default is a single, fixed value, it may be that we should allow this. I'll chat with @AndriySvyryd and @divega about it.

@divega divega removed this from the 2.2.0 milestone Jun 3, 2018
@divega
Copy link
Contributor

divega commented Jun 3, 2018

Temporarily clearing milestone so that we remember talking about #11939 (comment) in triage.

@ajcvickers ajcvickers added this to the 2.2.0 milestone Jun 5, 2018
@ajcvickers ajcvickers modified the milestones: 2.2.0-preview2, 2.2.0 Sep 11, 2018
@AndriySvyryd AndriySvyryd changed the title Optional composite foreign key error on delete Further relax value-generation restrictions on composite derived FKs Sep 26, 2018
@ajcvickers ajcvickers modified the milestones: 2.2.0, 3.0.0 Oct 3, 2018
@ajcvickers ajcvickers modified the milestones: 3.0.0, Backlog Jun 27, 2019
AndriySvyryd added a commit that referenced this issue Sep 2, 2020
@AndriySvyryd AndriySvyryd modified the milestones: Backlog, 5.0.0-rc2 Sep 2, 2020
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 2, 2020
@AndriySvyryd AndriySvyryd removed their assignment Sep 2, 2020
AndriySvyryd added a commit that referenced this issue Sep 2, 2020
@paultobey
Copy link
Author

👍

@ajcvickers ajcvickers modified the milestones: 5.0.0-rc2, 5.0.0 Nov 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. punted-for-2.2 punted-for-3.0 type-bug
Projects
None yet
5 participants