Efcore: Explicitly defined many-to-many relationship causes EF to generate (incorrect) shadow properties

Created on 27 Oct 2020  路  1Comment  路  Source: dotnet/efcore

Problem Description

Apologies if this is not actually a bug, but the behavior seems odd to me.

Given the following classes (simplified for demonstration), as you can see we have products which can have multiple tags:
```C#
public class Product
{

    public long Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public bool IsDeleted { get; set; }
    public ICollection<ProductProductCategoryTag> CategoryTags { get; set; }        
}

public class ProductCategoryTag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string? Description { get; set; }
    public ICollection<ProductProductCategoryTag> TaggedProducts { get; set; }
}

public class ProductProductCategoryTag
{
    public long ProductId { get; set; }
    public Product Product { get; set; }
    public int ProductCategoryTagId { get; set; }
    public ProductCategoryTag ProductCategoryTag { get; set; }
}
I am not sure if this is helpful, but here is how the key and an index are generated:
```C#
private void SetupIndexes(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ProductProductCategoryTag>()
                .HasKey(p => new { p.ProductId, p.ProductCategoryTagId });

            modelBuilder.Entity<ProductProductCategoryTag>()
                .HasIndex(p => new { p.ProductCategoryTagId, p.ProductId })
                .IsUnique()
                .HasName("IX_ProductProductCategoryTag_ProductCategoryTagId_ProductId");
        }

In the DbContext class, if we have code that looks like the following it results in shadow properties (and duplicate columns) getting generated:

```C#
private void SetupRelationships(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.HasOne()
.WithMany(p => p.CategoryTags)
.HasForeignKey(p => p.ProductId);

        modelBuilder.Entity<ProductProductCategoryTag>()
            .HasOne<ProductCategoryTag>()
            .WithMany(t => t.TaggedProducts)
            .HasForeignKey(p => p.ProductCategoryTagId);

    }
If we comment out the above code, it seems to work correctly.  I don't see why explicitly declaring what EF is inferring should result in the shadow properties being generated (of course assuming I am correct that this is what the code above is actually doing).

Here is the relevant output from `ToDebugString()`:

EntityType: Product
Properties:
Id (long) Required PK AfterSave:Throw ValueGenerated.OnAdd
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.LongTypeMapping
Description (string) Required MaxLength4000
Annotations:
MaxLength: 4000
TypeMapping: Microsoft.EntityFrameworkCore.Storage.StringTypeMapping
IsDeleted (bool) Required
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.BoolTypeMapping
Name (string) Required MaxLength100
Annotations:
MaxLength: 100
TypeMapping: Microsoft.EntityFrameworkCore.Storage.StringTypeMapping
Navigations:
CategoryTags (ICollection) Collection ToDependent ProductProductCategoryTag
Keys:
Id PK
Foreign keys:
Annotations:
ConstructorBinding: Microsoft.EntityFrameworkCore.Metadata.ConstructorBinding
Relational:TableName: Products
EntityType: ProductCategoryTag
Properties:
Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.IntTypeMapping
Description (string) MaxLength4000
Annotations:
MaxLength: 4000
TypeMapping: Microsoft.EntityFrameworkCore.Storage.StringTypeMapping
Name (string) Required Index MaxLength50
Annotations:
MaxLength: 50
TypeMapping: Microsoft.EntityFrameworkCore.Storage.StringTypeMapping
Navigations:
TaggedProducts (ICollection) Collection ToDependent ProductProductCategoryTag
Keys:
Id PK
Annotations:
ConstructorBinding: Microsoft.EntityFrameworkCore.Metadata.ConstructorBinding
Relational:TableName: ProductCategoryTags
EntityType: ProductProductCategoryTag
Properties:
ProductId (long) Required PK FK Index AfterSave:Throw
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.LongTypeMapping
ProductCategoryTagId (int) Required PK FK Index AfterSave:Throw
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.IntTypeMapping
ProductCategoryTagId1 (no field, int) Shadow Required FK Index
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.IntTypeMapping
ProductId1 (no field, long) Shadow Required FK Index
Annotations:
TypeMapping: Microsoft.EntityFrameworkCore.Storage.LongTypeMapping
Navigations:
Product (Product) ToPrincipal Product
ProductCategoryTag (ProductCategoryTag) ToPrincipal ProductCategoryTag
Keys:
ProductId, ProductCategoryTagId PK
Foreign keys:
ProductProductCategoryTag {'ProductCategoryTagId'} -> ProductCategoryTag {'Id'} ToDependent: TaggedProducts
ProductProductCategoryTag {'ProductCategoryTagId1'} -> ProductCategoryTag {'Id'} ToPrincipal: ProductCategoryTag
ProductProductCategoryTag {'ProductId'} -> Product {'Id'} ToDependent: CategoryTags
ProductProductCategoryTag {'ProductId1'} -> Product {'Id'} ToPrincipal: Product
Annotations:
ConstructorBinding: Microsoft.EntityFrameworkCore.Metadata.ConstructorBinding
```

Include provider and version information

EF Core version: 3.1.9
Database provider: Sqlite
Target framework: .NET Core 3.1
Operating system: Windows 10 Pro
IDE: Visual Studio 16.7.6

closed-question customer-reported

Most helpful comment

The explicit configuration of FK is specifying only one navigation in WithMany but no inverse navigation in HasOne.
TaggedProducts & ProductCategoryTag are pair of navigations which participate in one relationship. When EF Core discovers it by convention it is configured that way. But in your explicit configuration you only specify one so the other navigation causes a different relationship to be created. Now there are 2 different navigations, you get 2 different FK properties. The explicitly configured relationship uses what you configured. The other one will create a property in shadow state. Specifying proper navigations in HasOne will make both the same.

>All comments

The explicit configuration of FK is specifying only one navigation in WithMany but no inverse navigation in HasOne.
TaggedProducts & ProductCategoryTag are pair of navigations which participate in one relationship. When EF Core discovers it by convention it is configured that way. But in your explicit configuration you only specify one so the other navigation causes a different relationship to be created. Now there are 2 different navigations, you get 2 different FK properties. The explicitly configured relationship uses what you configured. The other one will create a property in shadow state. Specifying proper navigations in HasOne will make both the same.

Was this page helpful?
0 / 5 - 0 ratings