Skip to content

Commit d8a41aa

Browse files
committed
Emit all properties on the join type, even when there's no extra configuration on them.
Fixes #35524
1 parent 4957f0e commit d8a41aa

4 files changed

Lines changed: 306 additions & 8 deletions

File tree

src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,11 @@ public virtual string TransformText()
353353
foreach (var property in joinEntityType.GetProperties())
354354
{
355355
var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator);
356-
if (propertyFluentApiCalls == null)
356+
if (propertyFluentApiCalls != null)
357357
{
358-
continue;
358+
usings.AddRange(propertyFluentApiCalls.GetRequiredUsings());
359359
}
360360

361-
usings.AddRange(propertyFluentApiCalls.GetRequiredUsings());
362-
363361
this.Write(" j.IndexerProperty<");
364362
this.Write(this.ToStringHelper.ToStringWithCulture(code.Reference(property.ClrType)));
365363
this.Write(">(");

src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,10 @@ public partial class <#= Options.ContextName #> : DbContext
283283
foreach (var property in joinEntityType.GetProperties())
284284
{
285285
var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator);
286-
if (propertyFluentApiCalls == null)
286+
if (propertyFluentApiCalls != null)
287287
{
288-
continue;
288+
usings.AddRange(propertyFluentApiCalls.GetRequiredUsings());
289289
}
290-
291-
usings.AddRange(propertyFluentApiCalls.GetRequiredUsings());
292290
#>
293291
j.IndexerProperty<<#= code.Reference(property.ClrType) #>>(<#= code.Literal(property.Name) #>)<#= code.Fragment(propertyFluentApiCalls, indent: 7) #>;
294292
<#

test/EFCore.Design.Tests/Scaffolding/Internal/CSharpEntityTypeGeneratorTest.cs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.EntityFrameworkCore.Internal;
55
using Microsoft.EntityFrameworkCore.Metadata.Internal;
6+
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
67
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
78
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
89
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -2304,6 +2305,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
23042305
{
23052306
j.HasKey("BlogsId", "PostsId");
23062307
j.HasIndex(new[] { "PostsId" }, "IX_BlogPost_PostsId");
2308+
j.IndexerProperty<int>("BlogsId");
2309+
j.IndexerProperty<int>("PostsId");
23072310
});
23082311
});
23092312
@@ -2429,6 +2432,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
24292432
{
24302433
j.HasKey("BlogsId", "PostsId");
24312434
j.HasIndex(new[] { "PostsId" }, "IX_BlogPost_PostsId");
2435+
j.IndexerProperty<int>("BlogsId");
2436+
j.IndexerProperty<string>("PostsId");
24322437
});
24332438
});
24342439
@@ -2554,6 +2559,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
25542559
{
25552560
j.HasKey("BlogsId", "PostsId");
25562561
j.HasIndex(new[] { "PostsId" }, "IX_BlogPost_PostsId");
2562+
j.IndexerProperty<int>("BlogsId");
2563+
j.IndexerProperty<int>("PostsId");
25572564
});
25582565
});
25592566
@@ -2697,6 +2704,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
26972704
{
26982705
j.HasKey("BlogsKey", "PostsId");
26992706
j.HasIndex(new[] { "PostsId" }, "IX_BlogPost_PostsId");
2707+
j.IndexerProperty<int>("BlogsKey");
2708+
j.IndexerProperty<int>("PostsId");
27002709
});
27012710
});
27022711
@@ -2781,6 +2790,186 @@ public partial class Post
27812790
Assert.False(fk.PrincipalKey.IsPrimaryKey());
27822791
});
27832792

2793+
[ConditionalFact]
2794+
public Task Scaffold_skip_navigations_composite_fk()
2795+
{
2796+
var database = new DatabaseModel
2797+
{
2798+
Tables =
2799+
{
2800+
new DatabaseTable
2801+
{
2802+
Name = "AnnualValue",
2803+
Columns =
2804+
{
2805+
new DatabaseColumn { Name = "LearnAimRef", StoreType = "varchar(8)" },
2806+
new DatabaseColumn { Name = "EffectiveFrom", StoreType = "date" }
2807+
},
2808+
PrimaryKey = new DatabasePrimaryKey
2809+
{
2810+
Columns = { new DatabaseColumnRef("LearnAimRef"), new DatabaseColumnRef("EffectiveFrom") }
2811+
}
2812+
},
2813+
new DatabaseTable
2814+
{
2815+
Name = "AcademicYear_Lookup",
2816+
Columns =
2817+
{
2818+
new DatabaseColumn { Name = "AcademicYear", StoreType = "varchar(4)" },
2819+
new DatabaseColumn { Name = "AcademicYearDesc", StoreType = "varchar(150)", IsNullable = true },
2820+
new DatabaseColumn { Name = "AcademicYearDesc2", StoreType = "varchar(100)", IsNullable = true }
2821+
},
2822+
PrimaryKey = new DatabasePrimaryKey { Columns = { new DatabaseColumnRef("AcademicYear") } }
2823+
},
2824+
new DatabaseTable
2825+
{
2826+
Name = "AnnualValue_AcademicYear_Mapping",
2827+
Columns =
2828+
{
2829+
new DatabaseColumn { Name = "AcademicYear", StoreType = "varchar(4)" },
2830+
new DatabaseColumn { Name = "LearnAimRef", StoreType = "varchar(8)" },
2831+
new DatabaseColumn { Name = "EffectiveFrom", StoreType = "date" }
2832+
},
2833+
PrimaryKey = new DatabasePrimaryKey
2834+
{
2835+
Columns =
2836+
{
2837+
new DatabaseColumnRef("AcademicYear"),
2838+
new DatabaseColumnRef("LearnAimRef"),
2839+
new DatabaseColumnRef("EffectiveFrom")
2840+
}
2841+
},
2842+
ForeignKeys =
2843+
{
2844+
new DatabaseForeignKey
2845+
{
2846+
Columns = { new DatabaseColumnRef("LearnAimRef"), new DatabaseColumnRef("EffectiveFrom") },
2847+
PrincipalColumns = { new DatabaseColumnRef("LearnAimRef"), new DatabaseColumnRef("EffectiveFrom") },
2848+
PrincipalTable = new DatabaseTableRef("AnnualValue"),
2849+
OnDelete = ReferentialAction.Cascade
2850+
},
2851+
new DatabaseForeignKey
2852+
{
2853+
Columns = { new DatabaseColumnRef("AcademicYear") },
2854+
PrincipalColumns = { new DatabaseColumnRef("AcademicYear") },
2855+
PrincipalTable = new DatabaseTableRef("AcademicYear_Lookup"),
2856+
OnDelete = ReferentialAction.Cascade
2857+
}
2858+
}
2859+
}
2860+
}
2861+
};
2862+
2863+
return TestAsync(
2864+
serviceProvider =>
2865+
{
2866+
foreach (var table in database.Tables)
2867+
{
2868+
table.Database = database;
2869+
foreach (var column in table.Columns)
2870+
{
2871+
column.Table = table;
2872+
}
2873+
2874+
if (table.PrimaryKey != null)
2875+
{
2876+
table.PrimaryKey.Table = table;
2877+
FixupColumns(table, table.PrimaryKey.Columns);
2878+
}
2879+
2880+
foreach (var fk in table.ForeignKeys)
2881+
{
2882+
fk.Table = table;
2883+
FixupColumns(table, fk.Columns);
2884+
2885+
if (fk.PrincipalTable is DatabaseTableRef tableRef)
2886+
{
2887+
fk.PrincipalTable = database.Tables
2888+
.First(t => t.Name == tableRef.Name && t.Schema == tableRef.Schema);
2889+
}
2890+
2891+
FixupColumns(fk.PrincipalTable, fk.PrincipalColumns);
2892+
}
2893+
}
2894+
2895+
return serviceProvider.GetRequiredService<IScaffoldingModelFactory>().Create(
2896+
database, new ModelReverseEngineerOptions());
2897+
2898+
static void FixupColumns(DatabaseTable table, IList<DatabaseColumn> columns)
2899+
{
2900+
for (var i = 0; i < columns.Count; i++)
2901+
{
2902+
if (columns[i] is DatabaseColumnRef columnRef)
2903+
{
2904+
columns[i] = table.Columns.First(c => c.Name == columnRef.Name);
2905+
}
2906+
}
2907+
}
2908+
},
2909+
new ModelCodeGenerationOptions { UseDataAnnotations = false, UseNullableReferenceTypes = true },
2910+
code =>
2911+
{
2912+
// Context has DbSets for the two principal entities (join table is hidden)
2913+
Assert.Contains("DbSet<AcademicYearLookup>", code.ContextFile.Code);
2914+
Assert.Contains("DbSet<AnnualValue>", code.ContextFile.Code);
2915+
2916+
// Context wires up the many-to-many via UsingEntity
2917+
Assert.Contains("UsingEntity", code.ContextFile.Code);
2918+
Assert.Contains("AnnualValueAcademicYearMapping", code.ContextFile.Code);
2919+
Assert.Contains("IndexerProperty<DateOnly>(\"EffectiveFrom\")", code.ContextFile.Code);
2920+
2921+
// Two entity files generated (join table is Dictionary<string,object>, no class file)
2922+
Assert.Equal(2, code.AdditionalFiles.Count);
2923+
2924+
var lookupFile = code.AdditionalFiles.Single(f => f.Path == "AcademicYearLookup.cs");
2925+
Assert.Contains("string AcademicYear", lookupFile.Code);
2926+
Assert.Contains("string? AcademicYearDesc", lookupFile.Code);
2927+
Assert.Contains("string? AcademicYearDesc2", lookupFile.Code);
2928+
Assert.Contains("ICollection<AnnualValue> AnnualValues", lookupFile.Code);
2929+
2930+
var annualValueFile = code.AdditionalFiles.Single(f => f.Path == "AnnualValue.cs");
2931+
Assert.Contains("string LearnAimRef", annualValueFile.Code);
2932+
Assert.Contains("DateOnly EffectiveFrom", annualValueFile.Code);
2933+
Assert.Contains("ICollection<AcademicYearLookup> AcademicYears", annualValueFile.Code);
2934+
},
2935+
model =>
2936+
{
2937+
var lookupType = model.FindEntityType("TestNamespace.AcademicYearLookup");
2938+
Assert.NotNull(lookupType);
2939+
Assert.Collection(
2940+
lookupType.GetProperties().Select(p => p.Name).OrderBy(n => n),
2941+
p => Assert.Equal("AcademicYear", p),
2942+
p => Assert.Equal("AcademicYearDesc", p),
2943+
p => Assert.Equal("AcademicYearDesc2", p));
2944+
Assert.Empty(lookupType.GetNavigations());
2945+
var lookupSkipNav = Assert.Single(lookupType.GetSkipNavigations());
2946+
Assert.Equal("AnnualValues", lookupSkipNav.Name);
2947+
2948+
var annualValueType = model.FindEntityType("TestNamespace.AnnualValue");
2949+
Assert.NotNull(annualValueType);
2950+
Assert.Collection(
2951+
annualValueType.GetProperties().Select(p => p.Name).OrderBy(n => n),
2952+
p => Assert.Equal("EffectiveFrom", p),
2953+
p => Assert.Equal("LearnAimRef", p));
2954+
Assert.Empty(annualValueType.GetNavigations());
2955+
var annualValueSkipNav = Assert.Single(annualValueType.GetSkipNavigations());
2956+
Assert.Equal("AcademicYears", annualValueSkipNav.Name);
2957+
2958+
Assert.Equal(lookupSkipNav, annualValueSkipNav.Inverse);
2959+
Assert.Equal(annualValueSkipNav, lookupSkipNav.Inverse);
2960+
2961+
var joinEntityType = lookupSkipNav.ForeignKey.DeclaringEntityType;
2962+
Assert.Equal("AnnualValueAcademicYearMapping", joinEntityType.Name);
2963+
Assert.Equal(typeof(Dictionary<string, object>), joinEntityType.ClrType);
2964+
Assert.Equal(2, joinEntityType.GetForeignKeys().Count());
2965+
Assert.Collection(
2966+
joinEntityType.GetProperties().Select(p => p.Name).OrderBy(n => n),
2967+
p => Assert.Equal("AcademicYear", p),
2968+
p => Assert.Equal("EffectiveFrom", p),
2969+
p => Assert.Equal("LearnAimRef", p));
2970+
});
2971+
}
2972+
27842973
[ConditionalFact]
27852974
public Task Many_to_many_ef6()
27862975
=> TestAsync(

test/EFCore.Design.Tests/Scaffolding/Internal/RelationalScaffoldingModelFactoryTest.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,6 +2681,119 @@ public void Scaffold_skip_navigation_for_many_to_many_join_table_self_ref()
26812681
});
26822682
}
26832683

2684+
[ConditionalFact]
2685+
public void Scaffold_skip_navigation_for_many_to_many_join_table_composite_fk()
2686+
{
2687+
var database = new DatabaseModel
2688+
{
2689+
Tables =
2690+
{
2691+
new DatabaseTable
2692+
{
2693+
Name = "AnnualValue",
2694+
Columns =
2695+
{
2696+
new DatabaseColumn { Name = "LearnAimRef", StoreType = "varchar(8)" },
2697+
new DatabaseColumn { Name = "EffectiveFrom", StoreType = "date" }
2698+
},
2699+
PrimaryKey = new DatabasePrimaryKey
2700+
{
2701+
Columns = { new DatabaseColumnRef("LearnAimRef"), new DatabaseColumnRef("EffectiveFrom") }
2702+
}
2703+
},
2704+
new DatabaseTable
2705+
{
2706+
Name = "AcademicYear_Lookup",
2707+
Columns =
2708+
{
2709+
new DatabaseColumn { Name = "AcademicYear", StoreType = "varchar(4)" },
2710+
new DatabaseColumn { Name = "AcademicYearDesc", StoreType = "varchar(150)", IsNullable = true },
2711+
new DatabaseColumn { Name = "AcademicYearDesc2", StoreType = "varchar(100)", IsNullable = true }
2712+
},
2713+
PrimaryKey = new DatabasePrimaryKey { Columns = { new DatabaseColumnRef("AcademicYear") } }
2714+
},
2715+
new DatabaseTable
2716+
{
2717+
Name = "AnnualValue_AcademicYear_Mapping",
2718+
Columns =
2719+
{
2720+
new DatabaseColumn { Name = "AcademicYear", StoreType = "varchar(4)" },
2721+
new DatabaseColumn { Name = "LearnAimRef", StoreType = "varchar(8)" },
2722+
new DatabaseColumn { Name = "EffectiveFrom", StoreType = "date" }
2723+
},
2724+
PrimaryKey = new DatabasePrimaryKey
2725+
{
2726+
Columns =
2727+
{
2728+
new DatabaseColumnRef("AcademicYear"),
2729+
new DatabaseColumnRef("LearnAimRef"),
2730+
new DatabaseColumnRef("EffectiveFrom")
2731+
}
2732+
},
2733+
ForeignKeys =
2734+
{
2735+
new DatabaseForeignKey
2736+
{
2737+
Columns = { new DatabaseColumnRef("LearnAimRef"), new DatabaseColumnRef("EffectiveFrom") },
2738+
PrincipalColumns = { new DatabaseColumnRef("LearnAimRef"), new DatabaseColumnRef("EffectiveFrom") },
2739+
PrincipalTable = new DatabaseTableRef("AnnualValue"),
2740+
OnDelete = ReferentialAction.Cascade
2741+
},
2742+
new DatabaseForeignKey
2743+
{
2744+
Columns = { new DatabaseColumnRef("AcademicYear") },
2745+
PrincipalColumns = { new DatabaseColumnRef("AcademicYear") },
2746+
PrincipalTable = new DatabaseTableRef("AcademicYear_Lookup"),
2747+
OnDelete = ReferentialAction.Cascade
2748+
}
2749+
}
2750+
}
2751+
}
2752+
};
2753+
2754+
var model = _factory.Create(database, new ModelReverseEngineerOptions());
2755+
2756+
Assert.Collection(
2757+
model.GetEntityTypes().OrderBy(e => e.Name),
2758+
t1 =>
2759+
{
2760+
// AcademicYearLookup
2761+
Assert.Equal("AcademicYearLookup", t1.Name);
2762+
Assert.Collection(
2763+
t1.GetProperties().Select(p => p.Name).OrderBy(n => n),
2764+
p => Assert.Equal("AcademicYear", p),
2765+
p => Assert.Equal("AcademicYearDesc", p),
2766+
p => Assert.Equal("AcademicYearDesc2", p));
2767+
Assert.Empty(t1.GetNavigations());
2768+
var skipNavigation = Assert.Single(t1.GetSkipNavigations());
2769+
Assert.Equal("AnnualValues", skipNavigation.Name);
2770+
},
2771+
t2 =>
2772+
{
2773+
// AnnualValue
2774+
Assert.Equal("AnnualValue", t2.Name);
2775+
Assert.Collection(
2776+
t2.GetProperties().Select(p => p.Name).OrderBy(n => n),
2777+
p => Assert.Equal("EffectiveFrom", p),
2778+
p => Assert.Equal("LearnAimRef", p));
2779+
Assert.Empty(t2.GetNavigations());
2780+
var skipNavigation = Assert.Single(t2.GetSkipNavigations());
2781+
Assert.Equal("AcademicYears", skipNavigation.Name);
2782+
},
2783+
t3 =>
2784+
{
2785+
// AnnualValueAcademicYearMapping (join table)
2786+
Assert.Equal("AnnualValueAcademicYearMapping", t3.Name);
2787+
Assert.Collection(
2788+
t3.GetProperties().Select(p => p.Name).OrderBy(n => n),
2789+
p => Assert.Equal("AcademicYear", p),
2790+
p => Assert.Equal("EffectiveFrom", p),
2791+
p => Assert.Equal("LearnAimRef", p));
2792+
Assert.Empty(t3.GetNavigations());
2793+
Assert.Equal(2, t3.GetForeignKeys().Count());
2794+
});
2795+
}
2796+
26842797
[ConditionalFact]
26852798
public void Fk_property_ending_in_guid_navigation_name()
26862799
{

0 commit comments

Comments
 (0)