Skip to content

Commit

Permalink
Scaffolding: Avoid recompiling T4 templates
Browse files Browse the repository at this point in the history
This dramatically improves the performance of the commands.

Part of #27598
  • Loading branch information
bricelam committed Aug 25, 2022
1 parent 87f851a commit a2f6746
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public virtual Encoding OutputEncoding
/// </summary>
public virtual void Initialize()
{
_session = null;
_session?.Clear();
_errors = null;
_extension = null;
_outputEncoding = null;
Expand Down
133 changes: 85 additions & 48 deletions src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Text;
using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using Engine = Mono.TextTemplating.TemplatingEngine;
using Mono.TextTemplating;

namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal;

Expand All @@ -23,7 +23,7 @@ public class TextTemplatingModelGenerator : TemplatedModelGenerator

private readonly IOperationReporter _reporter;
private readonly IServiceProvider _serviceProvider;
private Engine? _engine;
private TemplatingEngine? _engine;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -47,8 +47,8 @@ public TextTemplatingModelGenerator(
/// 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 Engine Engine
=> _engine ??= new Engine();
protected virtual TemplatingEngine Engine
=> _engine ??= new TemplatingEngine();

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -107,7 +107,9 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO
{
host.TemplateFile = contextTemplate;

generatedCode = ProcessTemplate(contextTemplate, host);
generatedCode = Engine.ProcessTemplate(File.ReadAllText(contextTemplate), host);
CheckEncoding(host.OutputEncoding);
HandleErrors(host);
}
else
{
Expand Down Expand Up @@ -153,23 +155,41 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO
{
host.TemplateFile = entityTypeTemplate;

foreach (var entityType in model.GetEntityTypes())
CompiledTemplate? compiledEntityTypeTemplate = null;
string? entityTypeExtension = null;
try
{
host.Initialize();
host.Session.Add("EntityType", entityType);
host.Session.Add("Options", options);
host.Session.Add("NamespaceHint", options.ModelNamespace);
host.Session.Add("ProjectDefaultNamespace", options.RootNamespace);

generatedCode = ProcessTemplate(entityTypeTemplate, host);
if (string.IsNullOrWhiteSpace(generatedCode))
foreach (var entityType in model.GetEntityTypes())
{
continue;
}
host.Initialize();
host.Session.Add("EntityType", entityType);
host.Session.Add("Options", options);
host.Session.Add("NamespaceHint", options.ModelNamespace);
host.Session.Add("ProjectDefaultNamespace", options.RootNamespace);

if (compiledEntityTypeTemplate is null)
{
compiledEntityTypeTemplate = Engine.CompileTemplate(File.ReadAllText(entityTypeTemplate), host);
entityTypeExtension = host.Extension;
CheckEncoding(host.OutputEncoding);
}

generatedCode = compiledEntityTypeTemplate.Process();
HandleErrors(host);

if (string.IsNullOrWhiteSpace(generatedCode))
{
continue;
}

var entityTypeFileName = entityType.Name + host.Extension;
resultingFiles.AdditionalFiles.Add(
new ScaffoldedFile { Path = entityTypeFileName, Code = generatedCode });
var entityTypeFileName = entityType.Name + entityTypeExtension;
resultingFiles.AdditionalFiles.Add(
new ScaffoldedFile { Path = entityTypeFileName, Code = generatedCode });
}
}
finally
{
compiledEntityTypeTemplate?.Dispose();
}
}

Expand All @@ -178,54 +198,71 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO
{
host.TemplateFile = configurationTemplate;

foreach (var entityType in model.GetEntityTypes())
CompiledTemplate? compiledConfigurationTemplate = null;
string? configurationExtension = null;
try
{
host.Initialize();
host.Session.Add("EntityType", entityType);
host.Session.Add("Options", options);
host.Session.Add("NamespaceHint", options.ContextNamespace ?? options.ModelNamespace);
host.Session.Add("ProjectDefaultNamespace", options.RootNamespace);

generatedCode = ProcessTemplate(configurationTemplate, host);
if (string.IsNullOrWhiteSpace(generatedCode))
foreach (var entityType in model.GetEntityTypes())
{
continue;
}
host.Initialize();
host.Session.Add("EntityType", entityType);
host.Session.Add("Options", options);
host.Session.Add("NamespaceHint", options.ContextNamespace ?? options.ModelNamespace);
host.Session.Add("ProjectDefaultNamespace", options.RootNamespace);

if (compiledConfigurationTemplate is null)
{
compiledConfigurationTemplate = Engine.CompileTemplate(File.ReadAllText(configurationTemplate), host);
configurationExtension = host.Extension;
CheckEncoding(host.OutputEncoding);
}

var configurationFileName = entityType.Name + "Configuration" + host.Extension;
resultingFiles.AdditionalFiles.Add(
new ScaffoldedFile
generatedCode = compiledConfigurationTemplate.Process();
HandleErrors(host);

if (string.IsNullOrWhiteSpace(generatedCode))
{
Path = options.ContextDir != null
? Path.Combine(options.ContextDir, configurationFileName)
: configurationFileName,
Code = generatedCode
});
continue;
}

var configurationFileName = entityType.Name + "Configuration" + configurationExtension;
resultingFiles.AdditionalFiles.Add(
new ScaffoldedFile
{
Path = options.ContextDir != null
? Path.Combine(options.ContextDir, configurationFileName)
: configurationFileName,
Code = generatedCode
});
}
}
finally
{
compiledConfigurationTemplate?.Dispose();
}
}

return resultingFiles;
}

private string ProcessTemplate(string inputFile, TextTemplatingEngineHost host)
private void CheckEncoding(Encoding outputEncoding)
{
var output = Engine.ProcessTemplate(File.ReadAllText(inputFile), host);

foreach (CompilerError error in host.Errors)
if (outputEncoding != Encoding.UTF8)
{
_reporter.Write(error);
_reporter.WriteWarning(DesignStrings.EncodingIgnored(outputEncoding.WebName));
}
}

if (host.OutputEncoding != Encoding.UTF8)
private void HandleErrors(TextTemplatingEngineHost host)
{
foreach (CompilerError error in host.Errors)
{
_reporter.WriteWarning(DesignStrings.EncodingIgnored(host.OutputEncoding.WebName));
_reporter.Write(error);
}

if (host.Errors.HasErrors)
{
throw new OperationException(DesignStrings.ErrorGeneratingOutput(inputFile));
throw new OperationException(DesignStrings.ErrorGeneratingOutput(host.TemplateFile));
}

return output;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.Design.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal;
using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;

Expand Down Expand Up @@ -326,6 +327,7 @@ public void GenerateModel_uses_output_extension()
var generator = CreateGenerator();
var model = new ModelBuilder()
.Entity("Entity1", b => { })
.Entity("Entity2", b => { })
.FinalizeModel();

var result = generator.GenerateModel(
Expand All @@ -339,9 +341,11 @@ public void GenerateModel_uses_output_extension()

Assert.Equal("Context.vb", result.ContextFile.Path);

Assert.Equal(2, result.AdditionalFiles.Count);
Assert.Equal(4, result.AdditionalFiles.Count);
Assert.Single(result.AdditionalFiles, f => f.Path == "Entity1.fs");
Assert.Single(result.AdditionalFiles, f => f.Path == "Entity2.fs");
Assert.Single(result.AdditionalFiles, f => f.Path == "Entity1Configuration.py");
Assert.Single(result.AdditionalFiles, f => f.Path == "Entity2Configuration.py");
}

[ConditionalFact]
Expand Down Expand Up @@ -387,8 +391,7 @@ public void GenerateModel_reports_errors()
Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
File.WriteAllText(
contextTemplate,
@"<# Warning(""This is a warning"");
Error(""This is an error""); #>");
@"<# Error(""This is an error""); #>");

var reporter = new TestOperationReporter();
var generator = CreateGenerator(reporter);
Expand All @@ -407,17 +410,64 @@ public void GenerateModel_reports_errors()

Assert.Equal(DesignStrings.ErrorGeneratingOutput(contextTemplate), ex.Message);

Assert.Collection(
reporter.Messages,
x =>
{
Assert.Equal(LogLevel.Error, x.Level);
Assert.Contains("This is an error", x.Message);
});
}

[ConditionalFact]
public void GenerateModel_reports_warnings()
{
using var projectDir = new TempDirectory();

var contextTemplate = Path.Combine(projectDir, "CodeTemplates", "EFCore", "DbContext.t4");
Directory.CreateDirectory(Path.GetDirectoryName(contextTemplate));
File.WriteAllText(
contextTemplate,
@"<# Warning(""Warning about DbContext""); #>");
var entityTypeTemplate = Path.Combine(projectDir, "CodeTemplates", "EFCore", "EntityType.t4");
File.WriteAllText(
entityTypeTemplate,
@"<#@ assembly name=""Microsoft.EntityFrameworkCore"" #>
<#@ parameter name=""EntityType"" type=""Microsoft.EntityFrameworkCore.Metadata.IEntityType"" #>
<# Warning(""Warning about "" + EntityType.Name); #>");

var reporter = new TestOperationReporter();
var generator = CreateGenerator(reporter);
var model = new ModelBuilder()
.Entity("Entity1", b => { })
.Entity("Entity2", b => { })
.FinalizeModel();

var result = generator.GenerateModel(
model,
new()
{
ContextName = "Context",
ConnectionString = @"Name=DefaultConnection",
ProjectDir = projectDir
});

Assert.Collection(
reporter.Messages,
x =>
{
Assert.Equal(LogLevel.Warning, x.Level);
Assert.Contains("This is a warning", x.Message);
Assert.Contains("Warning about DbContext", x.Message);
},
x =>
{
Assert.Equal(LogLevel.Error, x.Level);
Assert.Contains("This is an error", x.Message);
Assert.Equal(LogLevel.Warning, x.Level);
Assert.Contains("Warning about Entity1", x.Message);
},
x =>
{
Assert.Equal(LogLevel.Warning, x.Level);
Assert.Contains("Warning about Entity2", x.Message);
});
}

Expand Down

0 comments on commit a2f6746

Please sign in to comment.