Skip to content

Commit

Permalink
Scaffolding: Create default templates
Browse files Browse the repository at this point in the history
This refactors our existing scaffolding code into T4 templates that we can both precompile to use as our default code generator and also ship somehow (probably `dotnet new`) as a starting point for users to start customizing.

Part of dotnet#4038, part of dotnet#14545, fixes dotnet#25473, resolves dotnet#25546, resolves dotnet#25547, fixes dotnet#27087, part of dotnet#27588, fixes dotnet#28187
  • Loading branch information
bricelam committed Aug 9, 2022
1 parent 2ea3df7 commit 6bf944d
Show file tree
Hide file tree
Showing 40 changed files with 4,196 additions and 2,865 deletions.
163 changes: 163 additions & 0 deletions src/EFCore.Design/Design/FluentApiCodeFragment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.EntityFrameworkCore.Design;

/// <summary>
/// Represents a fluent API method call.
/// </summary>
public class FluentApiCodeFragment : IMethodCallCodeFragment
{
/// <summary>
/// Initializes a new instance of the <see cref="FluentApiCodeFragment"/> class.
/// </summary>
/// <param name="method">The method's name.</param>
public FluentApiCodeFragment(string method)
=> Method = method;

/// <summary>
/// Gets the namespace of the method's declaring type.
/// </summary>
/// <value> The declaring type's name. </value>
public virtual string? Namespace { get; set; }

/// <summary>
/// Gets the name of the method's declaring type.
/// </summary>
/// <value> The declaring type's name. </value>
public virtual string? DeclaringType { get; set; }

/// <summary>
/// Gets the method's name.
/// </summary>
/// <value> The method's name. </value>
public virtual string Method { get; set; }

/// <summary>
/// Gets the method call's generic type arguments.
/// </summary>
/// <value>The type arguments.</value>
public virtual IList<string> TypeArguments { get; set; } = new List<string>();

IEnumerable<string> IMethodCallCodeFragment.TypeArguments
=> TypeArguments;

/// <summary>
/// Gets the method call's arguments.
/// </summary>
/// <value>The method call's arguments.</value>
public virtual IList<object?> Arguments { get; set; } = new List<object?>();

IEnumerable<object?> IMethodCallCodeFragment.Arguments
=> Arguments;

/// <summary>
/// Gets or sets a value indicating whether this method call has an equivalent data annotation.
/// </summary>
/// <value>A value indicating whether this method call has an equivalent data annotation.</value>
public virtual bool HasDataAnnotation { get; set; }

/// <summary>
/// Gets the next method call to chain after this.
/// </summary>
/// <value>The next method call.</value>
public virtual FluentApiCodeFragment? ChainedCall { get; set; }

IMethodCallCodeFragment? IMethodCallCodeFragment.ChainedCall
=> ChainedCall;

/// <summary>
/// Creates a new fluent API method call from an existing method call.
/// </summary>
/// <param name="call">The existing method call.</param>
/// <returns>The new fluent API method call.</returns>
[return: NotNullIfNotNull("call")]
public static FluentApiCodeFragment? From(MethodCallCodeFragment? call)
=> call is null
? null
: new(call.Method)
{
Namespace = call.Namespace,
DeclaringType = call.DeclaringType,
Arguments = call.Arguments.ToList(),
ChainedCall = From(call.ChainedCall)
};

/// <summary>
/// Creates a method chain from this method to another.
/// </summary>
/// <param name="call">The next method.</param>
/// <returns>A new fragment representing the method chain.</returns>
public virtual FluentApiCodeFragment Chain(FluentApiCodeFragment call)
{
var tail = this;
while (tail.ChainedCall is not null)
{
tail = tail.ChainedCall;
}

tail.ChainedCall = call;

return this;
}

/// <summary>
/// Gets the using statements required for this method chain.
/// </summary>
/// <returns>The usings.</returns>
public virtual IEnumerable<string> GetRequiredUsings()
{
var current = this;
do
{
if (current.Namespace is not null)
{
yield return current.Namespace;
}

foreach (var argumentNamespace in current.Arguments
.Where(a => a is not null and not NestedClosureCodeFragment and not PropertyAccessorCodeFragment)
.SelectMany(a => a!.GetType().GetNamespaces()))
{
yield return argumentNamespace;
}

current = current.ChainedCall;
}
while (current is not null);
}

/// <summary>
/// Creates a new method chain with calls filtered based on a predicate.
/// </summary>
/// <param name="predicate">A function to test each method call for a condition.</param>
/// <returns>A new method chain that only contains calls from the original one that satisfy the condition.</returns>
public virtual FluentApiCodeFragment? FilterChain(Func<FluentApiCodeFragment, bool> predicate)
{
FluentApiCodeFragment? newRoot = null;

var currentLink = this;
do
{
if (predicate(currentLink))
{
var unchained = new FluentApiCodeFragment(currentLink.Method)
{
Namespace = currentLink.Namespace,
DeclaringType = currentLink.DeclaringType,
TypeArguments = currentLink.TypeArguments,
Arguments = currentLink.Arguments,
HasDataAnnotation = currentLink.HasDataAnnotation
};
newRoot = newRoot?.Chain(unchained) ?? unchained;
}

currentLink = currentLink.ChainedCall;
}
while (currentLink is not null);

return newRoot;
}
}
69 changes: 56 additions & 13 deletions src/EFCore.Design/Design/Internal/CSharpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public CSharpHelper(ITypeMappingSource typeMappingSource)
{ typeof(int), (c, v) => c.Literal((int)v) },
{ typeof(long), (c, v) => c.Literal((long)v) },
{ typeof(NestedClosureCodeFragment), (c, v) => c.Fragment((NestedClosureCodeFragment)v) },
{ typeof(PropertyAccessorCodeFragment), (c, v) => c.Fragment((PropertyAccessorCodeFragment)v) },
{ typeof(object[]), (c, v) => c.Literal((object[])v) },
{ typeof(object[,]), (c, v) => c.Literal((object[,])v) },
{ typeof(sbyte), (c, v) => c.Literal((sbyte)v) },
Expand Down Expand Up @@ -1087,35 +1088,35 @@ private bool HandleList(IEnumerable<Expression> argumentExpressions, StringBuild
/// 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 Fragment(MethodCallCodeFragment fragment, string? instanceIdentifier, bool typeQualified)
public virtual string Fragment(IMethodCallCodeFragment fragment, string? instanceIdentifier, bool typeQualified)
{
var builder = new StringBuilder();

if (typeQualified)
{
if (instanceIdentifier is null || fragment.MethodInfo is null || fragment.ChainedCall is not null)
if (instanceIdentifier is null || fragment.DeclaringType is null || fragment.ChainedCall is not null)
{
throw new ArgumentException(DesignStrings.CannotGenerateTypeQualifiedMethodCall);
}

builder
.Append(fragment.DeclaringType!)
.Append(fragment.DeclaringType)
.Append('.')
.Append(fragment.Method)
.Append('(')
.Append(instanceIdentifier);

for (var i = 0; i < fragment.Arguments.Count; i++)
foreach (var argument in fragment.Arguments)
{
builder.Append(", ");

if (fragment.Arguments[i] is NestedClosureCodeFragment nestedFragment)
if (argument is NestedClosureCodeFragment nestedFragment)
{
builder.Append(Fragment(nestedFragment, 1));
}
else
{
builder.Append(UnknownLiteral(fragment.Arguments[i]));
builder.Append(UnknownLiteral(argument));
}
}

Expand All @@ -1140,7 +1141,7 @@ public virtual string Fragment(MethodCallCodeFragment fragment, string? instance
/// 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 Fragment(MethodCallCodeFragment? fragment, int indent = 0)
public virtual string Fragment(IMethodCallCodeFragment? fragment, int indent = 0)
{
if (fragment is null)
{
Expand Down Expand Up @@ -1173,27 +1174,42 @@ public virtual string Fragment(MethodCallCodeFragment? fragment, int indent = 0)

return builder.ToString();

void AppendMethodCall(MethodCallCodeFragment current)
void AppendMethodCall(IMethodCallCodeFragment current)
{
builder
.Append('.')
.Append(current.Method)
.Append(current.Method);

if (current.TypeArguments.Any())
{
builder
.Append("<")
.Append(string.Join(", ", current.TypeArguments))
.Append(">");
}

builder
.Append('(');

for (var i = 0; i < current.Arguments.Count; i++)
var first = true;
foreach (var argument in current.Arguments)
{
if (i != 0)
if (first)
{
first = false;
}
else
{
builder.Append(", ");
}

if (current.Arguments[i] is NestedClosureCodeFragment nestedFragment)
if (argument is NestedClosureCodeFragment nestedFragment)
{
builder.Append(Fragment(nestedFragment, indent + 1));
}
else
{
builder.Append(UnknownLiteral(current.Arguments[i]));
builder.Append(UnknownLiteral(argument));
}
}

Expand Down Expand Up @@ -1239,6 +1255,15 @@ public virtual string Fragment(NestedClosureCodeFragment fragment, int indent =
return builder.ToString();
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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 Fragment(PropertyAccessorCodeFragment fragment)
=> Lambda(fragment.Properties, fragment.Parameter);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -1335,6 +1360,24 @@ public virtual string XmlComment(string comment, int indent = 0)
return builder.ToString();
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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 Arguments(IEnumerable<object> values)
=> string.Join(", ", values.Select(UnknownLiteral));

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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 IEnumerable<string> GetRequiredUsings(Type type)
=> type.GetNamespaces();

private static bool IsIdentifierStartCharacter(char ch)
{
if (ch < 'a')
Expand Down
19 changes: 19 additions & 0 deletions src/EFCore.Design/EFCore.Design.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="$(MicrosoftExtensionsDependencyModelVersion)" />
<PackageReference Include="Microsoft.Extensions.HostFactoryResolver.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsHostFactoryResolverSourcesVersion)" />
<PackageReference Include="Mono.TextTemplating" Version="2.2.1" />
</ItemGroup>
Expand All @@ -69,6 +70,14 @@
<LastGenOutput>DesignStrings.Designer.cs</LastGenOutput>
<CustomToolNamespace>Microsoft.EntityFrameworkCore.Internal</CustomToolNamespace>
</None>
<None Update="Scaffolding\Internal\CSharpDbContextGenerator.tt">
<Generator>TextTemplatingFilePreprocessor</Generator>
<LastGenOutput>CSharpDbContextGenerator.cs</LastGenOutput>
</None>
<None Update="Scaffolding\Internal\CSharpEntityTypeGenerator.tt">
<Generator>TextTemplatingFilePreprocessor</Generator>
<LastGenOutput>CSharpEntityTypeGenerator.cs</LastGenOutput>
</None>
</ItemGroup>

<ItemGroup>
Expand All @@ -81,6 +90,16 @@
<AutoGen>True</AutoGen>
<DependentUpon>DesignStrings.Designer.tt</DependentUpon>
</Compile>
<Compile Update="Scaffolding\Internal\CSharpDbContextGenerator.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>CSharpDbContextGenerator.tt</DependentUpon>
</Compile>
<Compile Update="Scaffolding\Internal\CSharpEntityTypeGenerator.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>CSharpEntityTypeGenerator.tt</DependentUpon>
</Compile>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
/// </summary>
public static class InternalScaffoldingModelExtensions
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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 static ICollection<string> GetOrCreateReverseEngineeringErrors(this IMutableModel model)
{
var errors = (ICollection<string>?)model[ScaffoldingAnnotationNames.ReverseEngineeringErrors];
if (errors == null)
{
errors = new List<string>();
model.SetReverseEngineeringErrors(errors);
}

return errors;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// 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 static void SetReverseEngineeringErrors(this IMutableModel model, ICollection<string> value)
=> model.SetAnnotation(
ScaffoldingAnnotationNames.ReverseEngineeringErrors,
value);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Loading

0 comments on commit 6bf944d

Please sign in to comment.