Skip to content

Commit

Permalink
Scaffold with nullable reference types (#24874)
Browse files Browse the repository at this point in the history
Closes #15520
  • Loading branch information
roji committed May 13, 2021
1 parent 7249f62 commit 14da5d9
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 126 deletions.
2 changes: 1 addition & 1 deletion src/EFCore.Design/Design/Internal/DatabaseOperations.cs
Expand Up @@ -111,7 +111,7 @@ public class DatabaseOperations
ModelNamespace = finalModelNamespace,
ContextNamespace = finalContextNamespace,
Language = _language,
Nullable = _nullable,
UseNullableReferenceTypes = _nullable,
ContextDir = MakeDirRelative(outputDir, outputContextDir),
ContextName = dbContextClassName,
SuppressOnConfiguring = suppressOnConfiguring
Expand Down
60 changes: 33 additions & 27 deletions src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs
Expand Up @@ -30,6 +30,8 @@ public class CSharpDbContextGenerator : ICSharpDbContextGenerator
private readonly IAnnotationCodeGenerator _annotationCodeGenerator;
private IndentedStringBuilder _sb = null!;
private bool _entityTypeBuilderInitialized;
private bool _useDataAnnotations;
private bool _useNullableReferenceTypes;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -64,11 +66,15 @@ public class CSharpDbContextGenerator : ICSharpDbContextGenerator
string? contextNamespace,
string? modelNamespace,
bool useDataAnnotations,
bool useNullableReferenceTypes,
bool suppressConnectionStringWarning,
bool suppressOnConfiguring)
{
Check.NotNull(model, nameof(model));

_useDataAnnotations = useDataAnnotations;
_useNullableReferenceTypes = useNullableReferenceTypes;

_sb = new IndentedStringBuilder();

_sb.AppendLine("using System;"); // Guid default values require new Guid() which requires this using
Expand All @@ -84,9 +90,6 @@ public class CSharpDbContextGenerator : ICSharpDbContextGenerator

_sb.AppendLine();

_sb.AppendLine("#nullable disable");
_sb.AppendLine();

if (!string.IsNullOrEmpty(finalContextNamespace))
{
_sb.AppendLine($"namespace {finalContextNamespace}");
Expand All @@ -98,7 +101,6 @@ public class CSharpDbContextGenerator : ICSharpDbContextGenerator
model,
contextName,
connectionString,
useDataAnnotations,
suppressConnectionStringWarning,
suppressOnConfiguring);

Expand All @@ -121,7 +123,6 @@ public class CSharpDbContextGenerator : ICSharpDbContextGenerator
IModel model,
string contextName,
string connectionString,
bool useDataAnnotations,
bool suppressConnectionStringWarning,
bool suppressOnConfiguring)
{
Expand All @@ -142,7 +143,7 @@ public class CSharpDbContextGenerator : ICSharpDbContextGenerator
GenerateOnConfiguring(connectionString, suppressConnectionStringWarning);
}

GenerateOnModelCreating(model, useDataAnnotations);
GenerateOnModelCreating(model);
}

_sb.AppendLine();
Expand Down Expand Up @@ -175,8 +176,14 @@ private void GenerateDbSets(IModel model)
{
foreach (var entityType in model.GetEntityTypes())
{
_sb.AppendLine(
$"public virtual DbSet<{entityType.Name}> {entityType.GetDbSetName()} {{ get; set; }}");
_sb.Append($"public virtual DbSet<{entityType.Name}> {entityType.GetDbSetName()} {{ get; set; }}");

if (_useNullableReferenceTypes)
{
_sb.Append(" = null!;");
}

_sb.AppendLine();
}

if (model.GetEntityTypes().Any())
Expand Down Expand Up @@ -258,9 +265,7 @@ private void GenerateEntityTypeErrors(IModel model)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected virtual void GenerateOnModelCreating(
IModel model,
bool useDataAnnotations)
protected virtual void GenerateOnModelCreating(IModel model)
{
Check.NotNull(model, nameof(model));

Expand Down Expand Up @@ -310,7 +315,7 @@ private void GenerateEntityTypeErrors(IModel model)
{
_entityTypeBuilderInitialized = false;

GenerateEntityType(entityType, useDataAnnotations);
GenerateEntityType(entityType);

if (_entityTypeBuilderInitialized)
{
Expand Down Expand Up @@ -346,9 +351,9 @@ private void InitializeEntityTypeBuilder(IEntityType entityType)
_entityTypeBuilderInitialized = true;
}

private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations)
private void GenerateEntityType(IEntityType entityType)
{
GenerateKey(entityType.FindPrimaryKey(), entityType, useDataAnnotations);
GenerateKey(entityType.FindPrimaryKey(), entityType);

var annotations = _annotationCodeGenerator
.FilterIgnoredAnnotations(entityType.GetAnnotations())
Expand All @@ -362,14 +367,14 @@ private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations)
annotations.Remove(ScaffoldingAnnotationNames.DbSetName);
annotations.Remove(RelationalAnnotationNames.ViewDefinitionSql);

if (useDataAnnotations)
if (_useDataAnnotations)
{
// Strip out any annotations handled as attributes - these are already handled when generating
// the entity's properties
_ = _annotationCodeGenerator.GenerateDataAnnotationAttributes(entityType, annotations);
}

if (!useDataAnnotations || entityType.GetViewName() != null)
if (!_useDataAnnotations || entityType.GetViewName() != null)
{
GenerateTableName(entityType);
}
Expand All @@ -389,20 +394,20 @@ private void GenerateEntityType(IEntityType entityType, bool useDataAnnotations)
.ToDictionary(a => a.Name, a => a);
_annotationCodeGenerator.RemoveAnnotationsHandledByConventions(index, indexAnnotations);

if (!useDataAnnotations || indexAnnotations.Count > 0)
if (!_useDataAnnotations || indexAnnotations.Count > 0)
{
GenerateIndex(index);
}
}

foreach (var property in entityType.GetProperties())
{
GenerateProperty(property, useDataAnnotations);
GenerateProperty(property);
}

foreach (var foreignKey in entityType.GetForeignKeys())
{
GenerateRelationship(foreignKey, useDataAnnotations);
GenerateRelationship(foreignKey);
}
}

Expand Down Expand Up @@ -434,11 +439,11 @@ private void AppendMultiLineFluentApi(IEntityType entityType, IList<string> line
}
}

private void GenerateKey(IKey? key, IEntityType entityType, bool useDataAnnotations)
private void GenerateKey(IKey? key, IEntityType entityType)
{
if (key == null)
{
if (!useDataAnnotations)
if (!_useDataAnnotations)
{
var line = new List<string> { $".{nameof(EntityTypeBuilder.HasNoKey)}()" };

Expand Down Expand Up @@ -469,7 +474,7 @@ private void GenerateKey(IKey? key, IEntityType entityType, bool useDataAnnotati
}

if (!explicitName
&& useDataAnnotations)
&& _useDataAnnotations)
{
return;
}
Expand Down Expand Up @@ -557,7 +562,7 @@ private void GenerateIndex(IIndex index)
AppendMultiLineFluentApi(index.DeclaringEntityType, lines);
}

private void GenerateProperty(IProperty property, bool useDataAnnotations)
private void GenerateProperty(IProperty property)
{
var lines = new List<string> { $".{nameof(EntityTypeBuilder.Property)}({_code.Lambda(new[] { property.Name }, "e")})" };

Expand All @@ -567,7 +572,7 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations)
_annotationCodeGenerator.RemoveAnnotationsHandledByConventions(property, annotations);
annotations.Remove(ScaffoldingAnnotationNames.ColumnOrdinal);

if (useDataAnnotations)
if (_useDataAnnotations)
{
// Strip out any annotations handled as attributes - these are already handled when generating
// the entity's properties
Expand All @@ -579,7 +584,8 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations)
}
else
{
if (!property.IsNullable
if ((!_useNullableReferenceTypes || property.ClrType.IsValueType)
&& !property.IsNullable
&& property.ClrType.IsNullableType()
&& !property.IsPrimaryKey())
{
Expand Down Expand Up @@ -679,7 +685,7 @@ private void GenerateProperty(IProperty property, bool useDataAnnotations)
AppendMultiLineFluentApi(property.DeclaringEntityType, lines);
}

private void GenerateRelationship(IForeignKey foreignKey, bool useDataAnnotations)
private void GenerateRelationship(IForeignKey foreignKey)
{
var canUseDataAnnotations = true;
var annotations = _annotationCodeGenerator
Expand Down Expand Up @@ -732,7 +738,7 @@ private void GenerateRelationship(IForeignKey foreignKey, bool useDataAnnotation
_annotationCodeGenerator.GenerateFluentApiCalls(foreignKey, annotations).Select(m => _code.Fragment(m))
.Concat(GenerateAnnotations(annotations.Values)));

if (!useDataAnnotations
if (!_useDataAnnotations
|| !canUseDataAnnotations)
{
AppendMultiLineFluentApi(foreignKey.DeclaringEntityType, lines);
Expand Down
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Design.Internal;
Expand All @@ -28,6 +29,7 @@ public class CSharpEntityTypeGenerator : ICSharpEntityTypeGenerator

private IndentedStringBuilder _sb = null!;
private bool _useDataAnnotations;
private bool _useNullableReferenceTypes;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -51,12 +53,14 @@ public class CSharpEntityTypeGenerator : ICSharpEntityTypeGenerator
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual string WriteCode(IEntityType entityType, string? @namespace, bool useDataAnnotations)
public virtual string WriteCode(IEntityType entityType, string? @namespace, bool useDataAnnotations, bool useNullableReferenceTypes)
{
Check.NotNull(entityType, nameof(entityType));

_sb = new IndentedStringBuilder();
_useDataAnnotations = useDataAnnotations;
_useNullableReferenceTypes = useNullableReferenceTypes;

_sb = new IndentedStringBuilder();

_sb.AppendLine("using System;");
_sb.AppendLine("using System.Collections.Generic;");
Expand All @@ -77,9 +81,6 @@ public virtual string WriteCode(IEntityType entityType, string? @namespace, bool
_sb.AppendLine($"using {ns};");
}

_sb.AppendLine();
_sb.AppendLine("#nullable disable");

_sb.AppendLine();

if (!string.IsNullOrEmpty(@namespace))
Expand Down Expand Up @@ -277,7 +278,12 @@ protected virtual void GenerateProperties(IEntityType entityType)
GeneratePropertyDataAnnotations(property);
}

_sb.AppendLine($"public {_code.Reference(property.ClrType)} {property.Name} {{ get; set; }}");
_sb.AppendLine(
!_useNullableReferenceTypes || property.ClrType.IsValueType
? $"public {_code.Reference(property.ClrType)} {property.Name} {{ get; set; }}"
: property.IsNullable
? $"public {_code.Reference(property.ClrType)}? {property.Name} {{ get; set; }}"
: $"public {_code.Reference(property.ClrType)} {property.Name} {{ get; set; }} = null!;");
}
}

Expand Down Expand Up @@ -350,7 +356,8 @@ private void GenerateColumnAttribute(IProperty property)

private void GenerateRequiredAttribute(IProperty property)
{
if (!property.IsNullable
if ((!_useNullableReferenceTypes || property.ClrType.IsValueType)
&& !property.IsNullable
&& property.ClrType.IsNullableType()
&& !property.IsPrimaryKey())
{
Expand Down Expand Up @@ -440,7 +447,13 @@ protected virtual void GenerateNavigationProperties(IEntityType entityType)

var referencedTypeName = navigation.TargetEntityType.Name;
var navigationType = navigation.IsCollection ? $"ICollection<{referencedTypeName}>" : referencedTypeName;
_sb.AppendLine($"public virtual {navigationType} {navigation.Name} {{ get; set; }}");

_sb.AppendLine(
!_useNullableReferenceTypes || navigation.IsCollection
? $"public virtual {navigationType} {navigation.Name} {{ get; set; }}"
: navigation.ForeignKey.IsRequired
? $"public virtual {navigationType} {navigation.Name} {{ get; set; }} = null!;"
: $"public virtual {navigationType}? {navigation.Name} {{ get; set; }}");
}
}
}
Expand Down
Expand Up @@ -88,14 +88,14 @@ public override string Language
CoreStrings.ArgumentPropertyNull(nameof(options.ConnectionString), nameof(options)), nameof(options));
}

// TODO: Honor options.Nullable (issue #15520)
var generatedCode = CSharpDbContextGenerator.WriteCode(
model,
options.ContextName,
options.ConnectionString,
options.ContextNamespace,
options.ModelNamespace,
options.UseDataAnnotations,
options.UseNullableReferenceTypes,
options.SuppressConnectionStringWarning,
options.SuppressOnConfiguring);

Expand All @@ -114,8 +114,11 @@ public override string Language

foreach (var entityType in model.GetEntityTypes())
{
// TODO: Honor options.Nullable (issue #15520)
generatedCode = CSharpEntityTypeGenerator.WriteCode(entityType, options.ModelNamespace, options.UseDataAnnotations);
generatedCode = CSharpEntityTypeGenerator.WriteCode(
entityType,
options.ModelNamespace,
options.UseDataAnnotations,
options.UseNullableReferenceTypes);

// output EntityType poco .cs file
var entityTypeFileName = entityType.Name + FileExtension;
Expand Down
Expand Up @@ -26,6 +26,7 @@ public interface ICSharpDbContextGenerator
string? contextNamespace,
string? modelNamespace,
bool useDataAnnotations,
bool useNullableReferenceTypes,
bool suppressConnectionStringWarning,
bool suppressOnConfiguring);
}
Expand Down
Expand Up @@ -19,6 +19,6 @@ public interface ICSharpEntityTypeGenerator
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
string WriteCode(IEntityType entityType, string? @namespace, bool useDataAnnotations);
string WriteCode(IEntityType entityType, string? @namespace, bool useDataAnnotations, bool useNullableReferenceTypes);
}
}
Expand Up @@ -54,7 +54,7 @@ public class ModelCodeGenerationOptions
/// Gets or sets a value indicating whether nullable reference types are enabled.
/// </summary>
/// <value> A value indicating whether nullable reference types are enabled. </value>
public virtual bool Nullable { get; set; }
public virtual bool UseNullableReferenceTypes { get; set; }

/// <summary>
/// Gets or sets the DbContext output directory.
Expand Down

0 comments on commit 14da5d9

Please sign in to comment.