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

[release/7.0] Scaffolding: Fix missing HasForeignKey when principal key is an alternate key #29731

Merged
merged 1 commit into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/EFCore.Design/Extensions/ScaffoldingModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,11 @@ public static IEnumerable<AttributeCodeFragment> GetDataAnnotations(
var hasForeignKey =
new FluentApiCodeFragment(nameof(ReferenceReferenceBuilder.HasForeignKey)) { IsHandledByDataAnnotations = true };

if (!foreignKey.PrincipalKey.IsPrimaryKey())
{
hasForeignKey.IsHandledByDataAnnotations = false;
}

if (foreignKey.IsUnique)
{
hasForeignKey.TypeArguments.Add(foreignKey.DeclaringEntityType.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1636,7 +1636,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
{
entity.Property(e => e.Id).UseIdentityColumn();

entity.HasOne(d => d.BlogNavigation).WithMany(p => p.Posts).HasPrincipalKey(p => new { p.Id1, p.Id2 });
entity.HasOne(d => d.BlogNavigation).WithMany(p => p.Posts)
.HasPrincipalKey(p => new { p.Id1, p.Id2 })
.HasForeignKey(d => new { d.BlogId1, d.BlogId2 });
});

OnModelCreatingPartial(modelBuilder);
Expand All @@ -1656,6 +1658,137 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
Assert.Equal(new[] { "Id1", "Id2" }, blogNavigation.ForeignKey.PrincipalKey.Properties.Select(p => p.Name));
});

[ConditionalFact]
public Task ForeignKeyAttribute_is_generated_for_fk_referencing_ak()
=> TestAsync(
modelBuilder => modelBuilder
.Entity(
"Color",
x =>
{
x.Property<int>("Id");
x.Property<string>("ColorCode");
})
.Entity(
"Car",
x =>
{
x.Property<int>("Id");

x.HasOne("Color", "Color").WithMany("Cars")
.HasPrincipalKey("ColorCode")
.HasForeignKey("ColorCode");
}),
new ModelCodeGenerationOptions
{
UseDataAnnotations = true,
UseNullableReferenceTypes = true
},
code =>
{
AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

public partial class Color
{
[Key]
public int Id { get; set; }

public string ColorCode { get; set; } = null!;

public virtual ICollection<Car> Cars { get; } = new List<Car>();
}
",
code.AdditionalFiles.Single(f => f.Path == "Color.cs"));

AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

public partial class Car
{
[Key]
public int Id { get; set; }

public string? ColorCode { get; set; }

public virtual Color? Color { get; set; }
}
",
code.AdditionalFiles.Single(f => f.Path == "Car.cs"));

AssertFileContents(
@"using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

public partial class TestDbContext : DbContext
{
public TestDbContext()
{
}

public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}

public virtual DbSet<Car> Car { get; set; }

public virtual DbSet<Color> Color { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning "
+ DesignStrings.SensitiveInformationWarning
+ @"
=> optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();

entity.HasOne(d => d.Color).WithMany(p => p.Cars)
.HasPrincipalKey(p => p.ColorCode)
.HasForeignKey(d => d.ColorCode);
});

modelBuilder.Entity<Color>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();
});

OnModelCreatingPartial(modelBuilder);
}

partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
",
code.ContextFile);
},
model =>
{
var carType = model.FindEntityType("TestNamespace.Car");
var colorNavigation = carType.FindNavigation("Color");
Assert.Equal("TestNamespace.Color", colorNavigation.ForeignKey.PrincipalEntityType.Name);
Assert.Equal(new[] { "ColorCode" }, colorNavigation.ForeignKey.Properties.Select(p => p.Name));
Assert.Equal(new[] { "ColorCode" }, colorNavigation.ForeignKey.PrincipalKey.Properties.Select(p => p.Name));
});

[ConditionalFact]
public Task InverseProperty_when_navigation_property_with_same_type_and_navigation_name()
=> TestAsync(
Expand Down Expand Up @@ -1832,6 +1965,54 @@ public partial class Post
Assert.Equal("OriginalPosts", originalInverseNavigation.Name);
});

[ConditionalFact]
public Task InverseProperty_when_navigation_property_and_keyless()
=> TestAsync(
modelBuilder => modelBuilder
.Entity(
"Blog",
x => x.Property<int>("Id"))
.Entity(
"Post",
x =>
{
x.HasNoKey();
x.HasOne("Blog", "Blog").WithMany();
}),
new ModelCodeGenerationOptions { UseDataAnnotations = true },
code =>
{
AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

[Keyless]
public partial class Post
{
public int? BlogId { get; set; }

[ForeignKey(""BlogId"")]
public virtual Blog Blog { get; set; }
}
",
code.AdditionalFiles.Single(f => f.Path == "Post.cs"));
},
model =>
{
var postType = model.FindEntityType("TestNamespace.Post");
var blogNavigation = postType.FindNavigation("Blog");

var foreignKeyProperty = Assert.Single(blogNavigation.ForeignKey.Properties);
Assert.Equal("BlogId", foreignKeyProperty.Name);

Assert.Null(blogNavigation.Inverse);
});

[ConditionalFact]
public Task Entity_with_custom_annotation()
=> TestAsync(
Expand Down Expand Up @@ -2386,6 +2567,158 @@ public partial class Post
Assert.Equal(2, joinEntityType.GetForeignKeys().Count());
});

[ConditionalFact]
public Task Scaffold_skip_navigations_alternate_key_data_annotations()
=> TestAsync(
modelBuilder => modelBuilder
.Entity(
"Blog",
x =>
{
x.Property<int>("Id");
x.Property<int>("Key");
})
.Entity(
"Post",
x => x.Property<int>("Id"))
.Entity("Blog").HasMany("Post", "Posts").WithMany("Blogs")
.UsingEntity(
"BlogPost",
r => r.HasOne("Post").WithMany(),
l => l.HasOne("Blog").WithMany().HasPrincipalKey("Key")),
new ModelCodeGenerationOptions { UseDataAnnotations = true },
code =>
{
AssertFileContents(
@"using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

public partial class TestDbContext : DbContext
{
public TestDbContext()
{
}

public TestDbContext(DbContextOptions<TestDbContext> options)
: base(options)
{
}

public virtual DbSet<Blog> Blog { get; set; }

public virtual DbSet<Post> Post { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning "
+ DesignStrings.SensitiveInformationWarning
+ @"
=> optionsBuilder.UseSqlServer(""Initial Catalog=TestDatabase"");

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();

entity.HasMany(d => d.Posts).WithMany(p => p.Blogs)
.UsingEntity<Dictionary<string, object>>(
""BlogPost"",
r => r.HasOne<Post>().WithMany().HasForeignKey(""PostsId""),
l => l.HasOne<Blog>().WithMany()
.HasPrincipalKey(""Key"")
.HasForeignKey(""BlogsKey""),
j =>
{
j.HasKey(""BlogsKey"", ""PostsId"");
j.HasIndex(new[] { ""PostsId"" }, ""IX_BlogPost_PostsId"");
});
});

modelBuilder.Entity<Post>(entity =>
{
entity.Property(e => e.Id).UseIdentityColumn();
});

OnModelCreatingPartial(modelBuilder);
}

partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
",
code.ContextFile);

AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

public partial class Blog
{
[Key]
public int Id { get; set; }

public int Key { get; set; }

public virtual ICollection<Post> Posts { get; } = new List<Post>();
}
",
code.AdditionalFiles.Single(e => e.Path == "Blog.cs"));

AssertFileContents(
@"using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace TestNamespace;

public partial class Post
{
[Key]
public int Id { get; set; }

[ForeignKey(""PostsId"")]
[InverseProperty(""Posts"")]
public virtual ICollection<Blog> Blogs { get; } = new List<Blog>();
}
",
code.AdditionalFiles.Single(e => e.Path == "Post.cs"));

Assert.Equal(2, code.AdditionalFiles.Count);
},
model =>
{
var blogType = model.FindEntityType("TestNamespace.Blog");
Assert.Empty(blogType.GetNavigations());
var postsNavigation = Assert.Single(blogType.GetSkipNavigations());
Assert.Equal("Posts", postsNavigation.Name);

var postType = model.FindEntityType("TestNamespace.Post");
Assert.Empty(postType.GetNavigations());
var blogsNavigation = Assert.Single(postType.GetSkipNavigations());
Assert.Equal("Blogs", blogsNavigation.Name);

Assert.Equal(postsNavigation, blogsNavigation.Inverse);
Assert.Equal(blogsNavigation, postsNavigation.Inverse);

var joinEntityType = blogsNavigation.ForeignKey.DeclaringEntityType;
Assert.Equal("BlogPost", joinEntityType.Name);
Assert.Equal(typeof(Dictionary<string, object>), joinEntityType.ClrType);
Assert.Single(joinEntityType.GetIndexes());
Assert.Equal(2, joinEntityType.GetForeignKeys().Count());

var fk = Assert.Single(joinEntityType.FindDeclaredForeignKeys(new[] { joinEntityType.GetProperty("BlogsKey") }));
Assert.False(fk.PrincipalKey.IsPrimaryKey());
});

protected override void AddModelServices(IServiceCollection services)
=> services.Replace(ServiceDescriptor.Singleton<IRelationalAnnotationProvider, TestModelAnnotationProvider>());

Expand Down
Loading