Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scaffolding: Avoid recompiling T4 templates #28872

Merged
merged 1 commit into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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