Skip to content

Commit e18bcc2

Browse files
author
Anthony Sneed
committed
Merge branch 'kevin-a-naude-nullable-navigation-properties' into master
2 parents 3263550 + a389df4 commit e18bcc2

File tree

6 files changed

+132
-18
lines changed

6 files changed

+132
-18
lines changed

src/EntityFrameworkCore.Scaffolding.Handlebars/CodeTemplates/CSharpEntityType/Partials/Properties.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
{{#if nav-property-collection}}
2020
public virtual ICollection<{{nav-property-type}}> {{nav-property-name}} { get; set; }{{#if nullable-reference-types}} = default!;{{/if}}
2121
{{else}}
22-
public virtual {{nav-property-type}} {{nav-property-name}} { get; set; }{{#if nullable-reference-types}} = default!;{{/if}}
22+
public virtual {{nav-property-type}} {{nav-property-name}} { get; set; }{{#if nullable-reference-types}}{{#unless nav-property-isnullable}} = default!;{{/unless}}{{/if}}
2323
{{/if}}
2424
{{/each}}
2525
{{/if}}

src/EntityFrameworkCore.Scaffolding.Handlebars/HbsCSharpEntityTypeGenerator.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,25 @@ protected override void GenerateNavigationProperties(IEntityType entityType)
307307
GenerateNavigationDataAnnotations(navigation);
308308
}
309309

310+
var propertyIsNullable = !navigation.IsCollection && (
311+
navigation.IsOnDependent
312+
? !navigation.ForeignKey.IsRequired
313+
: !navigation.ForeignKey.IsRequiredDependent
314+
);
315+
var navPropertyType = navigation.TargetEntityType.Name;
316+
if (_options?.Value?.EnableNullableReferenceTypes == true &&
317+
!navPropertyType.EndsWith("?") &&
318+
propertyIsNullable) {
319+
navPropertyType += "?";
320+
}
321+
310322
navProperties.Add(new Dictionary<string, object>
311323
{
312324
{ "nav-property-collection", navigation.IsCollection },
313-
{ "nav-property-type", navigation.TargetEntityType.Name },
325+
{ "nav-property-type", navPropertyType },
314326
{ "nav-property-name", navigation.Name },
315327
{ "nav-property-annotations", NavPropertyAnnotations },
328+
{ "nav-property-isnullable", propertyIsNullable },
316329
{ "nullable-reference-types", _options?.Value?.EnableNullableReferenceTypes == true }
317330
});
318331
}

src/EntityFrameworkCore.Scaffolding.Handlebars/HbsEntityTypeTransformationService.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,11 @@ public List<Dictionary<string, object>> TransformNavigationProperties(List<Dicti
164164
navProperty["nav-property-name"] as string);
165165
var transformedProp = NavPropertyTransformer?.Invoke(propTypeInfo) ?? propTypeInfo;
166166

167-
transformedNavProperties.Add(new Dictionary<string, object>
168-
{
169-
{ "nav-property-collection", navProperty["nav-property-collection"] },
170-
{ "nav-property-type", transformedProp.PropertyType },
171-
{ "nav-property-name", transformedProp.PropertyName },
172-
{ "nav-property-annotations", navProperty["nav-property-annotations"] },
173-
{ "nullable-reference-types", navProperty["nullable-reference-types"] }
174-
});
167+
var newNavProperty = new Dictionary<string, object>(navProperty);
168+
newNavProperty["nav-property-type"] = transformedProp.PropertyType;
169+
newNavProperty["nav-property-name"] = transformedProp.PropertyName;
170+
171+
transformedNavProperties.Add(newNavProperty);
175172
}
176173

177174
return transformedNavProperties;

test/Scaffolding.Handlebars.Tests/CodeTemplates/CSharpEntityType/Partials/Properties.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{{#if nav-property-collection}}
1919
public virtual ICollection<{{nav-property-type}}> {{nav-property-name}} { get; set; }{{#if nullable-reference-types}} = default!;{{/if}}
2020
{{else}}
21-
public virtual {{nav-property-type}} {{nav-property-name}} { get; set; }{{#if nullable-reference-types}} = default!;{{/if}}
21+
public virtual {{nav-property-type}} {{nav-property-name}} { get; set; }{{#if nullable-reference-types}}{{#unless nav-property-isnullable}} = default!;{{/unless}}{{/if}}
2222
{{/if}}
2323
{{/each}}
2424
{{/if}}

test/Scaffolding.Handlebars.Tests/ExpectedEntities.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,57 @@ public partial class Product
120120
public virtual Category Category { get; set; }
121121
}
122122
}
123+
";
124+
}
125+
126+
private static class ExpectedEntitiesWithNullableNavigation
127+
{
128+
public const string CategoryClass =
129+
@"using System;
130+
using System.Collections.Generic;
131+
132+
namespace FakeNamespace
133+
{
134+
/// <summary>
135+
/// A category of products
136+
/// </summary>
137+
public partial class Category
138+
{
139+
public Category()
140+
{
141+
Products = new HashSet<Product>();
142+
}
143+
144+
public int CategoryId { get; set; } = default!;
145+
146+
/// <summary>
147+
/// The name of a category
148+
/// </summary>
149+
public string CategoryName { get; set; } = default!;
150+
151+
public virtual ICollection<Product> Products { get; set; } = default!;
152+
}
153+
}
154+
";
155+
156+
public const string ProductClass =
157+
@"using System;
158+
using System.Collections.Generic;
159+
160+
namespace FakeNamespace
161+
{
162+
public partial class Product
163+
{
164+
public int ProductId { get; set; } = default!;
165+
public string ProductName { get; set; } = default!;
166+
public decimal? UnitPrice { get; set; }
167+
public bool Discontinued { get; set; } = default!;
168+
public byte[]? RowVersion { get; set; }
169+
public int? CategoryId { get; set; }
170+
171+
public virtual Category? Category { get; set; }
172+
}
173+
}
123174
";
124175
}
125176
}

test/Scaffolding.Handlebars.Tests/HbsCSharpScaffoldingGeneratorTests.cs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,46 @@ public void WriteCode_Should_Generate_Entity_Files(bool useDataAnnotations, stri
199199
Assert.Equal(expectedProduct, product);
200200
}
201201

202+
[Theory]
203+
[InlineData("en-US")]
204+
[InlineData("tr-TR")]
205+
public void WriteCode_Should_Generate_Entities_With_Nullable_Navigation_When_Configured(string culture)
206+
{
207+
// Arrange
208+
Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
209+
var revEngOptions = ReverseEngineerOptions.EntitiesOnly;
210+
var scaffolder = CreateScaffolder(revEngOptions, options => {
211+
options.EnableNullableReferenceTypes = true;
212+
});
213+
214+
// Act
215+
var model = scaffolder.ScaffoldModel(
216+
connectionString: Constants.Connections.SqlServerConnection,
217+
databaseOptions: new DatabaseModelFactoryOptions(),
218+
modelOptions: new ModelReverseEngineerOptions(),
219+
codeOptions: new ModelCodeGenerationOptions
220+
{
221+
ContextNamespace = Constants.Parameters.RootNamespace,
222+
ModelNamespace = Constants.Parameters.RootNamespace,
223+
ContextName = Constants.Parameters.ContextName,
224+
ContextDir = Constants.Parameters.ProjectPath,
225+
UseDataAnnotations = false,
226+
Language = "C#",
227+
});
228+
229+
// Assert
230+
var files = GetGeneratedFiles(model, revEngOptions);
231+
var category = files[Constants.Files.CSharpFiles.CategoryFile];
232+
var product = files[Constants.Files.CSharpFiles.ProductFile];
233+
234+
object expectedCategory;
235+
object expectedProduct;
236+
expectedCategory = ExpectedEntitiesWithNullableNavigation.CategoryClass;
237+
expectedProduct = ExpectedEntitiesWithNullableNavigation.ProductClass;
238+
Assert.Equal(expectedCategory, category);
239+
Assert.Equal(expectedProduct, product);
240+
}
241+
202242
[Theory]
203243
[InlineData(false, "en-US")]
204244
[InlineData(true, "en-US")]
@@ -394,7 +434,12 @@ public void Save_Should_Write_Context_and_Entity_Files()
394434
}
395435
}
396436

397-
private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions options, string filenamePrefix = null)
437+
private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions revEngOptions, string filenamePrefix = null)
438+
{
439+
return CreateScaffolder(revEngOptions, _ => { }, filenamePrefix);
440+
}
441+
442+
private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions revEngOptions, Action<HandlebarsScaffoldingOptions> configureOptions, string filenamePrefix = null)
398443
{
399444
var fileService = new InMemoryTemplateFileService();
400445
fileService.InputFiles(ContextClassTemplate, ContextImportsTemplate,
@@ -407,7 +452,10 @@ private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions optio
407452
.AddSingleton<IEntityTypeTemplateService, FakeHbsEntityTypeTemplateService>()
408453
.AddSingleton<ITemplateFileService>(fileService)
409454
.AddSingleton<ITemplateLanguageService, FakeCSharpTemplateLanguageService>()
410-
.AddSingleton<IModelCodeGenerator, HbsCSharpModelGenerator>()
455+
.AddSingleton<IModelCodeGenerator, HbsCSharpModelGenerator>();
456+
457+
#pragma warning disable EF1001 // Internal EF Core API usage.
458+
services
411459
.AddSingleton(provider =>
412460
{
413461
ICSharpDbContextGenerator contextGenerator = new HbsCSharpDbContextGenerator(
@@ -417,8 +465,8 @@ private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions optio
417465
provider.GetRequiredService<IEntityTypeTransformationService>(),
418466
provider.GetRequiredService<ICSharpHelper>(),
419467
provider.GetRequiredService<IOptions<HandlebarsScaffoldingOptions>>());
420-
return options == ReverseEngineerOptions.DbContextOnly ||
421-
options == ReverseEngineerOptions.DbContextAndEntities
468+
return revEngOptions == ReverseEngineerOptions.DbContextOnly ||
469+
revEngOptions == ReverseEngineerOptions.DbContextAndEntities
422470
? contextGenerator
423471
: new NullCSharpDbContextGenerator();
424472
})
@@ -430,11 +478,14 @@ private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions optio
430478
provider.GetRequiredService<IEntityTypeTemplateService>(),
431479
provider.GetRequiredService<IEntityTypeTransformationService>(),
432480
provider.GetRequiredService<IOptions<HandlebarsScaffoldingOptions>>());
433-
return options == ReverseEngineerOptions.EntitiesOnly ||
434-
options == ReverseEngineerOptions.DbContextAndEntities
481+
return revEngOptions == ReverseEngineerOptions.EntitiesOnly ||
482+
revEngOptions == ReverseEngineerOptions.DbContextAndEntities
435483
? entityGenerator
436484
: new NullCSharpEntityTypeGenerator();
437-
})
485+
});
486+
#pragma warning restore EF1001 // Internal EF Core API usage.
487+
488+
services
438489
.AddSingleton<IHbsHelperService>(provider =>
439490
new HbsHelperService(new Dictionary<string, Action<TextWriter, Dictionary<string, object>, object[]>>
440491
{
@@ -458,6 +509,8 @@ private IReverseEngineerScaffolder CreateScaffolder(ReverseEngineerOptions optio
458509
.AddSingleton<IEntityTypeTransformationService>(y => new HbsEntityTypeTransformationService(entityFileNameTransformer: entityName => $"{filenamePrefix}{entityName}"));
459510
}
460511

512+
services.Configure(configureOptions);
513+
461514
#pragma warning disable EF1001 // Internal EF Core API usage.
462515
new SqlServerDesignTimeServices().ConfigureDesignTimeServices(services);
463516
#pragma warning restore EF1001 // Internal EF Core API usage.

0 commit comments

Comments
 (0)