Skip to content

Commit d8ce5d5

Browse files
committed
WIP on type-qualified Fluent APIs in model snapshot
1 parent 700e828 commit d8ce5d5

File tree

6 files changed

+229
-57
lines changed

6 files changed

+229
-57
lines changed

src/EFCore.Design/Design/Internal/CSharpHelper.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,44 @@ public virtual string Fragment(MethodCallCodeFragment fragment)
10001000
return builder.ToString();
10011001
}
10021002

1003+
/// <summary>
1004+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1005+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
1006+
/// any release. You should only use it directly in your code with extreme caution and knowing that
1007+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
1008+
/// </summary>
1009+
public virtual string TypeQualifiedMethodCall(MethodCallCodeFragment fragment, string instanceIdentifier)
1010+
{
1011+
// TODO: Strings
1012+
if (fragment.MethodInfo is null)
1013+
{
1014+
throw new ArgumentException("Cannot generate fully-qualified method calls for fragments without a MethodInfo", nameof(fragment));
1015+
}
1016+
if (fragment.ChainedCall is not null)
1017+
{
1018+
throw new ArgumentException("Cannot generate fully-qualified method calls for chained fragments", nameof(fragment));
1019+
}
1020+
1021+
var builder = new StringBuilder();
1022+
1023+
builder
1024+
.Append(fragment.DeclaringType)
1025+
.Append('.')
1026+
.Append(fragment.Method)
1027+
.Append('(')
1028+
.Append(instanceIdentifier);
1029+
1030+
for (var i = 0; i < fragment.Arguments.Count; i++)
1031+
{
1032+
builder.Append(", ");
1033+
builder.Append(UnknownLiteral(fragment.Arguments[i]));
1034+
}
1035+
1036+
builder.Append(')');
1037+
1038+
return builder.ToString();
1039+
}
1040+
10031041
private string Fragment(NestedClosureCodeFragment fragment)
10041042
{
10051043
if (fragment.MethodCalls.Count == 1)

src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,27 @@ public CSharpSnapshotGenerator(CSharpSnapshotGeneratorDependencies dependencies)
4040
private ICSharpHelper Code
4141
=> Dependencies.CSharpHelper;
4242

43+
static (IReadOnlyList<T>, IReadOnlyList<T>) Split<T>(IReadOnlyList<T> source, Func<T, bool> predicate)
44+
{
45+
List<T>? result1 = null, result2 = null;
46+
47+
foreach (var item in source)
48+
{
49+
if (predicate(item))
50+
{
51+
result1 ??= new();
52+
result1.Add(item);
53+
}
54+
else
55+
{
56+
result2 ??= new();
57+
result2.Add(item);
58+
}
59+
}
60+
61+
return (result1 ?? (IReadOnlyList<T>)Array.Empty<T>(), result2 ?? (IReadOnlyList<T>)Array.Empty<T>());
62+
}
63+
4364
/// <summary>
4465
/// Generates code for creating an <see cref="IModel" />.
4566
/// </summary>
@@ -58,41 +79,55 @@ public virtual void Generate(string builderName, IModel model, IndentedStringBui
5879

5980
var productVersion = model.GetProductVersion();
6081

61-
if (annotations.Count > 0 || productVersion != null)
82+
var fluentApiCalls = Dependencies.AnnotationCodeGenerator.GenerateFluentApiCalls(model, annotations);
83+
84+
var (fullyQualifiedFluentApiCalls, nonFullyQualifiedFluentApiCalls)
85+
= Split(fluentApiCalls, c => c.MethodInfo is not null && c.MethodInfo.IsStatic);
86+
87+
// First generate all chained (non-fully-qualified) calls
88+
if (nonFullyQualifiedFluentApiCalls.Count > 0 || annotations.Count > 0 || productVersion is not null)
6289
{
6390
stringBuilder.Append(builderName);
6491

6592
using (stringBuilder.Indent())
6693
{
67-
// Temporary patch: specifically exclude some annotations which are known to produce identical Fluent API calls across different
68-
// providers, generating them as raw annotations instead.
69-
var ambiguousAnnotations = RemoveAmbiguousFluentApiAnnotations(
70-
annotations,
71-
name => name.EndsWith(":ValueGenerationStrategy", StringComparison.Ordinal)
72-
|| name.EndsWith(":IdentityIncrement", StringComparison.Ordinal)
73-
|| name.EndsWith(":IdentitySeed", StringComparison.Ordinal)
74-
|| name.EndsWith(":HiLoSequenceName", StringComparison.Ordinal)
75-
|| name.EndsWith(":HiLoSequenceSchema", StringComparison.Ordinal));
76-
77-
foreach (var methodCallCodeFragment in
78-
Dependencies.AnnotationCodeGenerator.GenerateFluentApiCalls(model, annotations))
94+
if (nonFullyQualifiedFluentApiCalls.Count > 0)
7995
{
80-
stringBuilder
81-
.AppendLine()
82-
.Append(Code.Fragment(methodCallCodeFragment));
96+
foreach (var call in nonFullyQualifiedFluentApiCalls)
97+
{
98+
stringBuilder
99+
.AppendLine()
100+
.Append(Code.Fragment(call));
101+
}
83102
}
84103

85-
IEnumerable<IAnnotation> remainingAnnotations = annotations.Values;
86-
if (productVersion != null)
104+
if (annotations.Count > 0 || productVersion is not null)
87105
{
88-
remainingAnnotations = remainingAnnotations.Append(
89-
new Annotation(CoreAnnotationNames.ProductVersion, productVersion));
90-
}
106+
IEnumerable<IAnnotation> remainingAnnotations = annotations.Values;
107+
if (productVersion != null)
108+
{
109+
remainingAnnotations = remainingAnnotations.Append(
110+
new Annotation(CoreAnnotationNames.ProductVersion, productVersion));
111+
}
91112

92-
GenerateAnnotations(remainingAnnotations.Concat(ambiguousAnnotations), stringBuilder);
113+
GenerateAnnotations(remainingAnnotations, stringBuilder);
114+
}
93115
}
94116

95117
stringBuilder.AppendLine(";");
118+
119+
if (fullyQualifiedFluentApiCalls.Count > 0)
120+
{
121+
stringBuilder.AppendLine();
122+
}
123+
}
124+
125+
// Then generate fully-qualified calls
126+
foreach (var call in fullyQualifiedFluentApiCalls)
127+
{
128+
stringBuilder
129+
.Append(Code.TypeQualifiedMethodCall(call, builderName))
130+
.AppendLine(";");
96131
}
97132

98133
foreach (var sequence in model.GetSequences())
@@ -1632,6 +1667,7 @@ private void GenerateFluentApiForDefaultValue(
16321667
.Append(")");
16331668
}
16341669

1670+
// TODO: Remove
16351671
private static IReadOnlyList<IAnnotation> RemoveAmbiguousFluentApiAnnotations(
16361672
Dictionary<string, IAnnotation> annotations,
16371673
Func<string, bool> annotationNameMatcher)

src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ private static readonly MethodInfo _modelUseHiLoMethodInfo
3232
= typeof(SqlServerModelBuilderExtensions).GetRequiredRuntimeMethod(
3333
nameof(SqlServerModelBuilderExtensions.UseHiLo), typeof(ModelBuilder), typeof(string), typeof(string));
3434

35+
private static readonly MethodInfo _modelHasDatabaseMaxSizeMethodInfo
36+
= typeof(SqlServerModelBuilderExtensions).GetRequiredRuntimeMethod(
37+
nameof(SqlServerModelBuilderExtensions.HasDatabaseMaxSize), typeof(ModelBuilder), typeof(string));
38+
39+
private static readonly MethodInfo _modelHasServiceTierSqlMethodInfo
40+
= typeof(SqlServerModelBuilderExtensions).GetRequiredRuntimeMethod(
41+
nameof(SqlServerModelBuilderExtensions.HasServiceTierSql), typeof(ModelBuilder), typeof(string));
42+
43+
private static readonly MethodInfo _modelHasPerformanceLevelSqlMethodInfo
44+
= typeof(SqlServerModelBuilderExtensions).GetRequiredRuntimeMethod(
45+
nameof(SqlServerModelBuilderExtensions.HasPerformanceLevelSql), typeof(ModelBuilder), typeof(string));
46+
3547
private static readonly MethodInfo _modelHasAnnotationMethodInfo
3648
= typeof(ModelBuilder).GetRequiredRuntimeMethod(
3749
nameof(ModelBuilder.HasAnnotation), typeof(string), typeof(object));
@@ -118,6 +130,21 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
118130
fragments.Add(valueGenerationStrategy);
119131
}
120132

133+
GenerateSimpleFluentApiCall(
134+
annotations,
135+
SqlServerAnnotationNames.MaxDatabaseSize, _modelHasDatabaseMaxSizeMethodInfo,
136+
fragments);
137+
138+
GenerateSimpleFluentApiCall(
139+
annotations,
140+
SqlServerAnnotationNames.ServiceTierSql, _modelHasServiceTierSqlMethodInfo,
141+
fragments);
142+
143+
GenerateSimpleFluentApiCall(
144+
annotations,
145+
SqlServerAnnotationNames.PerformanceLevelSql, _modelHasPerformanceLevelSqlMethodInfo,
146+
fragments);
147+
121148
return fragments;
122149
}
123150

@@ -338,5 +365,22 @@ protected override bool IsHandledByConvention(IModel model, IAnnotation annotati
338365

339366
return default;
340367
}
368+
369+
private static void GenerateSimpleFluentApiCall(
370+
IDictionary<string, IAnnotation> annotations,
371+
string annotationName,
372+
MethodInfo methodInfo,
373+
List<MethodCallCodeFragment> methodCallCodeFragments)
374+
{
375+
if (annotations.TryGetValue(annotationName, out var annotation))
376+
{
377+
annotations.Remove(annotationName);
378+
if (annotation.Value is object annotationValue)
379+
{
380+
methodCallCodeFragments.Add(
381+
new MethodCallCodeFragment(methodInfo, annotationValue));
382+
}
383+
}
384+
}
341385
}
342386
}

src/EFCore/Design/ICSharpHelper.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ public interface ICSharpHelper
2020
/// <returns> The fragment. </returns>
2121
string Fragment(MethodCallCodeFragment fragment);
2222

23+
/// <summary>
24+
/// Generates a type-qualified method call code fragment.
25+
/// </summary>
26+
/// <param name="fragment"> The method call. </param>
27+
/// <param name="instanceIdentifier">
28+
/// An identifier to be prepended to the argument list of the <paramref name="fragment"/>, used for generating
29+
/// fully-qualified extension method calls.
30+
/// </param>
31+
/// <returns> The fragment. </returns>
32+
string TypeQualifiedMethodCall(MethodCallCodeFragment fragment, string instanceIdentifier);
33+
2334
/// <summary>
2435
/// Generates a valid C# identifier from the specified string unique to the scope.
2536
/// </summary>

0 commit comments

Comments
 (0)