Skip to content

Commit

Permalink
Fix Fluent APIs on keys in model snapshot (#26058)
Browse files Browse the repository at this point in the history
Also generate Fluent API for IsMemoryOptimized.

Fixes #26045
  • Loading branch information
roji authored Sep 16, 2021
1 parent a5fa6bb commit 22906cd
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 31 deletions.
63 changes: 36 additions & 27 deletions src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
Expand Down Expand Up @@ -614,36 +615,40 @@ protected virtual void GenerateKey(
Check.NotNull(key, nameof(key));
Check.NotNull(stringBuilder, nameof(stringBuilder));

stringBuilder
.AppendLine()
var keyBuilderName = new StringBuilder()
.Append(entityTypeBuilderName)
.Append(primary ? ".HasKey(" : ".HasAlternateKey(")
.Append(string.Join(", ", key.Properties.Select(p => Code.Literal(p.Name))))
.Append(")");
.Append(")")
.ToString();

stringBuilder
.AppendLine()
.Append(keyBuilderName);

// Note that GenerateAnnotations below does the corresponding decrement
stringBuilder.IncrementIndent();

GenerateKeyAnnotations(entityTypeBuilderName, key, stringBuilder);
GenerateKeyAnnotations(keyBuilderName, key, stringBuilder);
}

/// <summary>
/// Generates code for the annotations on a key.
/// </summary>
/// <param name="entityTypeBuilderName"> The name of the builder variable. </param>
/// <param name="keyBuilderName"> The name of the builder variable. </param>
/// <param name="key"> The key. </param>
/// <param name="stringBuilder"> The builder code is added to. </param>
protected virtual void GenerateKeyAnnotations(string entityTypeBuilderName, IKey key, IndentedStringBuilder stringBuilder)
protected virtual void GenerateKeyAnnotations(string keyBuilderName, IKey key, IndentedStringBuilder stringBuilder)
{
Check.NotNull(entityTypeBuilderName, nameof(entityTypeBuilderName));
Check.NotNull(keyBuilderName, nameof(keyBuilderName));
Check.NotNull(key, nameof(key));
Check.NotNull(stringBuilder, nameof(stringBuilder));

var annotations = Dependencies.AnnotationCodeGenerator
.FilterIgnoredAnnotations(key.GetAnnotations())
.ToDictionary(a => a.Name, a => a);

GenerateAnnotations(entityTypeBuilderName, key, stringBuilder, annotations, inChainedCall: true);
GenerateAnnotations(keyBuilderName, key, stringBuilder, annotations, inChainedCall: true);
}

/// <summary>
Expand Down Expand Up @@ -1056,9 +1061,11 @@ protected virtual void GenerateForeignKey(
Check.NotNull(foreignKey, nameof(foreignKey));
Check.NotNull(stringBuilder, nameof(stringBuilder));

var foreignKeyBuilderNameStringBuilder = new StringBuilder();

if (!foreignKey.IsOwnership)
{
stringBuilder
foreignKeyBuilderNameStringBuilder
.Append(entityTypeBuilderName)
.Append(".HasOne(")
.Append(Code.Literal(foreignKey.PrincipalEntityType.Name))
Expand All @@ -1070,19 +1077,23 @@ protected virtual void GenerateForeignKey(
}
else
{
stringBuilder
foreignKeyBuilderNameStringBuilder
.Append(entityTypeBuilderName)
.Append(".WithOwner(");

if (foreignKey.DependentToPrincipal != null)
{
stringBuilder
foreignKeyBuilderNameStringBuilder
.Append(Code.Literal(foreignKey.DependentToPrincipal.Name));
}
}

foreignKeyBuilderNameStringBuilder.Append(")");

var foreignKeyBuilderName = foreignKeyBuilderNameStringBuilder.ToString();

stringBuilder
.Append(")")
.Append(foreignKeyBuilderName)
.AppendLine();

// Note that GenerateAnnotations below does the corresponding decrement
Expand Down Expand Up @@ -1170,29 +1181,29 @@ protected virtual void GenerateForeignKey(
}
}

GenerateForeignKeyAnnotations(entityTypeBuilderName, foreignKey, stringBuilder);
GenerateForeignKeyAnnotations(foreignKeyBuilderName, foreignKey, stringBuilder);
}

/// <summary>
/// Generates code for the annotations on a foreign key.
/// </summary>
/// <param name="entityTypeBuilderName"> The name of the builder variable. </param>
/// <param name="foreignKeyBuilderName"> The name of the builder variable. </param>
/// <param name="foreignKey"> The foreign key. </param>
/// <param name="stringBuilder"> The builder code is added to. </param>
protected virtual void GenerateForeignKeyAnnotations(
string entityTypeBuilderName,
string foreignKeyBuilderName,
IForeignKey foreignKey,
IndentedStringBuilder stringBuilder)
{
Check.NotNull(entityTypeBuilderName, nameof(entityTypeBuilderName));
Check.NotNull(foreignKeyBuilderName, nameof(foreignKeyBuilderName));
Check.NotNull(foreignKey, nameof(foreignKey));
Check.NotNull(stringBuilder, nameof(stringBuilder));

var annotations = Dependencies.AnnotationCodeGenerator
.FilterIgnoredAnnotations(foreignKey.GetAnnotations())
.ToDictionary(a => a.Name, a => a);

GenerateAnnotations(entityTypeBuilderName, foreignKey, stringBuilder, annotations, inChainedCall: true);
GenerateAnnotations(foreignKeyBuilderName, foreignKey, stringBuilder, annotations, inChainedCall: true);
}

/// <summary>
Expand Down Expand Up @@ -1269,11 +1280,9 @@ protected virtual void GenerateNavigation(
Check.NotNull(navigation, nameof(navigation));
Check.NotNull(stringBuilder, nameof(stringBuilder));

stringBuilder
.Append(entityTypeBuilderName)
.Append(".Navigation(")
.Append(Code.Literal(navigation.Name))
.Append(")");
var navigationBuilderName = $"{entityTypeBuilderName}.Navigation({Code.Literal(navigation.Name)})";

stringBuilder.Append(navigationBuilderName);

// Note that GenerateAnnotations below does the corresponding decrement
stringBuilder.IncrementIndent();
Expand All @@ -1287,29 +1296,29 @@ protected virtual void GenerateNavigation(
.Append(".IsRequired()");
}

GenerateNavigationAnnotations(entityTypeBuilderName, navigation, stringBuilder);
GenerateNavigationAnnotations(navigationBuilderName, navigation, stringBuilder);
}

/// <summary>
/// Generates code for the annotations on a navigation.
/// </summary>
/// <param name="entityTypeBuilderName"> The name of the builder variable. </param>
/// <param name="navigationBuilderName"> The name of the builder variable. </param>
/// <param name="navigation"> The navigation. </param>
/// <param name="stringBuilder"> The builder code is added to. </param>
protected virtual void GenerateNavigationAnnotations(
string entityTypeBuilderName,
string navigationBuilderName,
INavigation navigation,
IndentedStringBuilder stringBuilder)
{
Check.NotNull(entityTypeBuilderName, nameof(entityTypeBuilderName));
Check.NotNull(navigationBuilderName, nameof(navigationBuilderName));
Check.NotNull(navigation, nameof(navigation));
Check.NotNull(stringBuilder, nameof(stringBuilder));

var annotations = Dependencies.AnnotationCodeGenerator
.FilterIgnoredAnnotations(navigation.GetAnnotations())
.ToDictionary(a => a.Name, a => a);

GenerateAnnotations(entityTypeBuilderName, navigation, stringBuilder, annotations, inChainedCall: true);
GenerateAnnotations(navigationBuilderName, navigation, stringBuilder, annotations, inChainedCall: true);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ private static readonly MethodInfo _entityTypeToTableMethodInfo
= typeof(RelationalEntityTypeBuilderExtensions).GetRequiredRuntimeMethod(
nameof(RelationalEntityTypeBuilderExtensions.ToTable), typeof(EntityTypeBuilder), typeof(string));

private static readonly MethodInfo _entityTypeIsMemoryOptimizedMethodInfo
= typeof(SqlServerEntityTypeBuilderExtensions).GetRequiredRuntimeMethod(
nameof(SqlServerEntityTypeBuilderExtensions.IsMemoryOptimized), typeof(EntityTypeBuilder), typeof(bool));

private static readonly MethodInfo _propertyIsSparseMethodInfo
= typeof(SqlServerPropertyBuilderExtensions).GetRequiredRuntimeMethod(
nameof(SqlServerPropertyBuilderExtensions.IsSparse), typeof(PropertyBuilder), typeof(bool));
Expand Down Expand Up @@ -184,7 +188,14 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
IEntityType entityType,
IDictionary<string, IAnnotation> annotations)
{
var result = base.GenerateFluentApiCalls(entityType, annotations);
var fragments = new List<MethodCallCodeFragment>(base.GenerateFluentApiCalls(entityType, annotations));

if (GetAndRemove<bool?>(annotations, SqlServerAnnotationNames.MemoryOptimized) is bool isMemoryOptimized)
{
fragments.Add(isMemoryOptimized
? new(_entityTypeIsMemoryOptimizedMethodInfo)
: new(_entityTypeIsMemoryOptimizedMethodInfo, false));
}

if (annotations.TryGetValue(SqlServerAnnotationNames.IsTemporal, out var isTemporalAnnotation)
&& isTemporalAnnotation.Value as bool? == true)
Expand Down Expand Up @@ -250,16 +261,16 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
"ttb",
temporalTableBuilderCalls))));

fragments.Add(toTemporalTableCall);

annotations.Remove(SqlServerAnnotationNames.IsTemporal);
annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableName);
annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema);
annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartPropertyName);
annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndPropertyName);

return result.Concat(new[] { toTemporalTableCall }).ToList();
}

return result;
return fragments;
}

/// <summary>
Expand Down
94 changes: 94 additions & 0 deletions test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,37 @@ public virtual void EntityType_annotations_are_stored_in_snapshot()
});
}

[ConditionalFact]
public virtual void EntityType_Fluent_APIs_are_properly_generated()
{
Test(
builder =>
{
builder.Entity<EntityWithOneProperty>().IsMemoryOptimized();
builder.Ignore<EntityWithTwoProperties>();
},
AddBoilerPlate(
GetHeading()
+ @"
modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b =>
{
b.Property<int>(""Id"")
.ValueGeneratedOnAdd()
.HasColumnType(""int"");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>(""Id""), 1L, 1);
b.HasKey(""Id"");
SqlServerKeyBuilderExtensions.IsClustered(b.HasKey(""Id""), false);
b.ToTable(""EntityWithOneProperty"");
SqlServerEntityTypeBuilderExtensions.IsMemoryOptimized(b);
});"),
o => Assert.True(o.GetEntityTypes().Single().IsMemoryOptimized()));
}

[ConditionalFact]
public virtual void BaseType_is_stored_in_snapshot()
{
Expand Down Expand Up @@ -3833,6 +3864,35 @@ public virtual void Key_annotations_are_stored_in_snapshot()
"AnnotationValue", o.GetEntityTypes().First().GetKeys().Where(k => !k.IsPrimaryKey()).First()["AnnotationName"]));
}

[ConditionalFact]
public virtual void Key_Fluent_APIs_are_properly_generated()
{
Test(
builder =>
{
builder.Entity<EntityWithOneProperty>().HasKey(t => t.Id).IsClustered();
builder.Ignore<EntityWithTwoProperties>();
},
AddBoilerPlate(
GetHeading()
+ @"
modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b =>
{
b.Property<int>(""Id"")
.ValueGeneratedOnAdd()
.HasColumnType(""int"");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>(""Id""), 1L, 1);
b.HasKey(""Id"");
SqlServerKeyBuilderExtensions.IsClustered(b.HasKey(""Id""));
b.ToTable(""EntityWithOneProperty"");
});"),
o => Assert.True(o.GetEntityTypes().First().GetKeys().Single(k => k.IsPrimaryKey()).IsClustered()));
}

[ConditionalFact]
public virtual void Key_name_annotation_is_stored_in_snapshot_as_fluent_api()
{
Expand Down Expand Up @@ -3946,6 +4006,40 @@ public virtual void Index_annotations_are_stored_in_snapshot()
o => Assert.Equal("AnnotationValue", o.GetEntityTypes().First().GetIndexes().First()["AnnotationName"]));
}

[ConditionalFact]
public virtual void Index_Fluent_APIs_are_properly_generated()
{
Test(
builder =>
{
builder.Entity<EntityWithTwoProperties>().HasIndex(t => t.AlternateId).IsClustered();
builder.Ignore<EntityWithOneProperty>();
},
AddBoilerPlate(
GetHeading()
+ @"
modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b =>
{
b.Property<int>(""Id"")
.ValueGeneratedOnAdd()
.HasColumnType(""int"");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>(""Id""), 1L, 1);
b.Property<int>(""AlternateId"")
.HasColumnType(""int"");
b.HasKey(""Id"");
b.HasIndex(""AlternateId"");
SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex(""AlternateId""));
b.ToTable(""EntityWithTwoProperties"");
});"),
o => Assert.True(o.GetEntityTypes().Single().GetIndexes().Single().IsClustered()));
}

[ConditionalFact]
public virtual void Index_isUnique_is_stored_in_snapshot()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,29 @@ public void GenerateFluentApi_IModel_works_with_PerformanceLevel()
Assert.Equal("'foo'", Assert.Single(result.Arguments));
}

[ConditionalFact]
public void GenerateFluentApi_IEntityType_works_when_IsMemoryOptimized()
{
var generator = CreateGenerator();

var modelBuilder = SqlServerConventionSetBuilder.CreateModelBuilder();
modelBuilder.Entity(
"Post",
x =>
{
x.Property<int>("Id");
x.IsMemoryOptimized();
});
var entityType = (IEntityType)modelBuilder.Model.FindEntityType("Post");

var result = generator.GenerateFluentApiCalls(entityType, entityType.GetAnnotations().ToDictionary(a => a.Name, a => a))
.Single();

Assert.Equal(nameof(SqlServerEntityTypeBuilderExtensions.IsMemoryOptimized), result.Method);

Assert.Equal(0, result.Arguments.Count);
}

private SqlServerAnnotationCodeGenerator CreateGenerator()
=> new(
new AnnotationCodeGeneratorDependencies(
Expand Down

0 comments on commit 22906cd

Please sign in to comment.