diff --git a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingEngineHost.cs b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingEngineHost.cs index 696abeab02b..f54380ef245 100644 --- a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingEngineHost.cs +++ b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingEngineHost.cs @@ -119,7 +119,7 @@ public virtual Encoding OutputEncoding /// public virtual void Initialize() { - _session = null; + _session?.Clear(); _errors = null; _extension = null; _outputEncoding = null; diff --git a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs index cbfb1c59c1b..e6e255c6dd4 100644 --- a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs @@ -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; @@ -23,7 +23,7 @@ public class TextTemplatingModelGenerator : TemplatedModelGenerator private readonly IOperationReporter _reporter; private readonly IServiceProvider _serviceProvider; - private Engine? _engine; + private TemplatingEngine? _engine; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -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. /// - protected virtual Engine Engine - => _engine ??= new Engine(); + protected virtual TemplatingEngine Engine + => _engine ??= new TemplatingEngine(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -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 { @@ -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(); } } @@ -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; } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs index 4179112e455..6a0fc2e1124 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingModelGeneratorTest.cs @@ -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; @@ -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( @@ -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] @@ -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); @@ -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); }); }