From 995ac0731e1fb3b29b154e235696b6407143f006 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 2 Dec 2023 16:08:52 +0000 Subject: [PATCH] Proof of concept for #32385 --- src/EFCore.Design/EFCore.Design.csproj | 2 +- .../Internal/TextTemplatingModelGenerator.cs | 85 ++++- .../Internal/CSharpDbContextGeneratorTest.cs | 361 ++++++++++++------ .../Internal/ModelCodeGeneratorTestBase.cs | 18 +- .../Internal/TextTemplatingEngineHostTest.cs | 36 +- 5 files changed, 345 insertions(+), 157 deletions(-) diff --git a/src/EFCore.Design/EFCore.Design.csproj b/src/EFCore.Design/EFCore.Design.csproj index f1fd0f511d3..7f724b52bde 100644 --- a/src/EFCore.Design/EFCore.Design.csproj +++ b/src/EFCore.Design/EFCore.Design.csproj @@ -62,7 +62,7 @@ - + diff --git a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs index fba4c63fe0f..9acf85f2f16 100644 --- a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CodeDom.Compiler; +using System.Globalization; using System.Text; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.VisualStudio.TextTemplating; using Mono.TextTemplating; namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -102,12 +104,18 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO }; var contextTemplate = Path.Combine(options.ProjectDir!, TemplatesDirectory, DbContextTemplate); - string generatedCode; + string? generatedCode = null; if (File.Exists(contextTemplate)) { host.TemplateFile = contextTemplate; - generatedCode = Engine.ProcessTemplate(File.ReadAllText(contextTemplate), host); + var compiledTemplate = Engine.CompileTemplateAsync(File.ReadAllText(contextTemplate), host, new()).GetAwaiter().GetResult(); + + if (compiledTemplate != null) + { + generatedCode = ProcessTemplate(compiledTemplate, host); + } + CheckEncoding(host.OutputEncoding); HandleErrors(host); } @@ -142,7 +150,7 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO Path = options.ContextDir != null ? Path.Combine(options.ContextDir, dbContextFileName) : dbContextFileName, - Code = generatedCode + Code = generatedCode! } }; @@ -165,12 +173,16 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO if (compiledEntityTypeTemplate is null) { - compiledEntityTypeTemplate = Engine.CompileTemplate(File.ReadAllText(entityTypeTemplate), host); + compiledEntityTypeTemplate = Engine.CompileTemplateAsync(File.ReadAllText(entityTypeTemplate), host, new()).GetAwaiter().GetResult();; entityTypeExtension = host.Extension; CheckEncoding(host.OutputEncoding); } - generatedCode = compiledEntityTypeTemplate.Process(); + if (compiledEntityTypeTemplate != null) + { + generatedCode = ProcessTemplate(compiledEntityTypeTemplate, host); + } + HandleErrors(host); if (string.IsNullOrWhiteSpace(generatedCode)) @@ -208,12 +220,16 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO if (compiledConfigurationTemplate is null) { - compiledConfigurationTemplate = Engine.CompileTemplate(File.ReadAllText(configurationTemplate), host); + compiledConfigurationTemplate = Engine.CompileTemplateAsync(File.ReadAllText(configurationTemplate), host, new()).GetAwaiter().GetResult();; configurationExtension = host.Extension; CheckEncoding(host.OutputEncoding); } - generatedCode = compiledConfigurationTemplate.Process(); + if (compiledConfigurationTemplate != null) + { + generatedCode = ProcessTemplate(compiledConfigurationTemplate, host); + } + HandleErrors(host); if (string.IsNullOrWhiteSpace(generatedCode)) @@ -241,6 +257,61 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO return resultingFiles; } + private static string ProcessTemplate(CompiledTemplate compiledTemplate, TextTemplatingEngineHost host) + { + var templateAssemblyData = GetField(compiledTemplate, "templateAssemblyData")!; + var templateClassFullName = (string)GetField(compiledTemplate, "templateClassFullName")!; + var culture = GetField(compiledTemplate, "culture"); + var assemblyBytes = (byte[])templateAssemblyData.GetType().GetProperty("Assembly")!.GetValue(templateAssemblyData)!; + + var assembly = Assembly.Load(assemblyBytes); + var transformType = assembly.GetType(templateClassFullName)!; + var textTransformation = Activator.CreateInstance(transformType); + + var hostProp = transformType.GetProperty("Host", typeof(ITextTemplatingEngineHost)); + if (hostProp != null) + { + hostProp.SetValue(textTransformation, host, null); + } + + var sessionProp = transformType.GetProperty("Session", typeof(IDictionary)); + if (sessionProp != null) + { + sessionProp.SetValue(textTransformation, host.Session, null); + } + + var errorProp = transformType.GetProperty("Errors", BindingFlags.Instance | BindingFlags.NonPublic)!; + var errorMethod = transformType.GetMethod("Error", new[] { typeof(string) })!; + + var errors = (CompilerErrorCollection)errorProp.GetValue(textTransformation, null)!; + errors.Clear(); + + ToStringHelper.FormatProvider = culture != null ? (IFormatProvider)culture : CultureInfo.InvariantCulture; + + string? output = null; + + var initMethod = transformType.GetMethod("Initialize")!; + var transformMethod = transformType.GetMethod("TransformText")!; + + try + { + initMethod.Invoke(textTransformation, null); + output = (string?)transformMethod.Invoke(textTransformation, null); + } + catch (Exception ex) + { + errorMethod.Invoke(textTransformation, new object[] { "Error running transform: " + ex }); + } + + host.LogErrors(errors); + return output!; + + static object? GetField(CompiledTemplate compiledTemplate, string fieldName) + => compiledTemplate.GetType() + .GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(compiledTemplate); + } + private void CheckEncoding(Encoding outputEncoding) { if (outputEncoding != Encoding.UTF8) diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index 73e294a56ce..10c67399449 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -18,8 +18,10 @@ public CSharpDbContextGeneratorTest(ModelCodeGeneratorTestFixture fixture, ITest { } - [ConditionalFact] - public Task Empty_model() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Empty_model(bool useTemplateGenerator) => TestAsync( modelBuilder => { }, new ModelCodeGenerationOptions(), @@ -60,10 +62,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Assert.Empty(code.AdditionalFiles); }, - model => Assert.Empty(model.GetEntityTypes())); + model => Assert.Empty(model.GetEntityTypes()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task SuppressConnectionStringWarning_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task SuppressConnectionStringWarning_works(bool useTemplateGenerator) => TestAsync( modelBuilder => { }, new ModelCodeGenerationOptions { SuppressConnectionStringWarning = true }, @@ -103,10 +108,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Assert.Empty(code.AdditionalFiles); }, - model => Assert.Empty(model.GetEntityTypes())); + model => Assert.Empty(model.GetEntityTypes()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task SuppressOnConfiguring_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task SuppressOnConfiguring_works(bool useTemplateGenerator) => TestAsync( modelBuilder => { }, new ModelCodeGenerationOptions { SuppressOnConfiguring = true }, @@ -141,8 +149,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) }, null); - [ConditionalFact] - public Task DbSets_without_nrt() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task DbSets_without_nrt(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity"), new ModelCodeGenerationOptions @@ -156,10 +166,13 @@ public Task DbSets_without_nrt() Assert.Contains("DbSet Entity { get; set; }", code.ContextFile.Code); Assert.DoesNotContain("DbSet Entity { get; set; } = null!;", code.ContextFile.Code); }, - null); + null, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task DbSets_with_nrt() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task DbSets_with_nrt(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity"), new ModelCodeGenerationOptions @@ -173,7 +186,8 @@ public Task DbSets_with_nrt() Assert.Contains("DbSet Entity { get; set; }", code.ContextFile.Code); Assert.DoesNotContain("DbSet Entity { get; set; } = null!;", code.ContextFile.Code); }, - null); + null, + useTemplateGenerator: useTemplateGenerator); [ConditionalFact] public void Required_options_to_GenerateModel_are_not_null() @@ -230,8 +244,10 @@ public void Plugins_work() scaffoldedModel.ContextFile.Code); } - [ConditionalFact] - public Task IsRequired_is_generated_for_ref_property_without_nrt() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task IsRequired_is_generated_for_ref_property_without_nrt(bool useTemplateGenerator) => TestAsync( modelBuilder => { @@ -259,10 +275,13 @@ public Task IsRequired_is_generated_for_ref_property_without_nrt() Assert.True(entityType.GetProperty("NonRequiredString").IsNullable); Assert.False(entityType.GetProperty("RequiredInt").IsNullable); Assert.True(entityType.GetProperty("NonRequiredInt").IsNullable); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task IsRequired_is_not_generated_for_ref_property_with_nrt() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task IsRequired_is_not_generated_for_ref_property_with_nrt(bool useTemplateGenerator) => TestAsync( modelBuilder => { @@ -290,10 +309,13 @@ public Task IsRequired_is_not_generated_for_ref_property_with_nrt() Assert.True(entityType.GetProperty("NonRequiredString").IsNullable); Assert.False(entityType.GetProperty("RequiredInt").IsNullable); Assert.True(entityType.GetProperty("NonRequiredInt").IsNullable); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Comments_use_fluent_api() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Comments_use_fluent_api(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity( "Entity", @@ -309,10 +331,13 @@ public Task Comments_use_fluent_api() code.ContextFile.Code), model => Assert.Equal( "An int property", - model.FindEntityType("TestNamespace.Entity").GetProperty("Property").GetComment())); + model.FindEntityType("TestNamespace.Entity").GetProperty("Property").GetComment()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Entity_comments_use_fluent_api() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Entity_comments_use_fluent_api(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity( "Entity", @@ -326,10 +351,13 @@ public Task Entity_comments_use_fluent_api() code.ContextFile.Code), model => Assert.Equal( "An entity comment", - model.FindEntityType("TestNamespace.Entity").GetComment())); + model.FindEntityType("TestNamespace.Entity").GetComment()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Views_work() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Views_work(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Vista").ToView("Vista"), new ModelCodeGenerationOptions { UseDataAnnotations = true }, @@ -343,10 +371,13 @@ public Task Views_work() Assert.Null(entityType.GetViewSchema()); Assert.Null(entityType.GetTableName()); Assert.Null(entityType.GetSchema()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task ModelInDifferentNamespaceDbContext_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ModelInDifferentNamespaceDbContext_works(bool useTemplateGenerator) { var modelGenerationOptions = new ModelCodeGenerationOptions { @@ -356,30 +387,34 @@ public Task ModelInDifferentNamespaceDbContext_works() const string entityInAnotherNamespaceTypeName = "EntityInAnotherNamespace"; return TestAsync( - modelBuilder => modelBuilder.Entity(entityInAnotherNamespaceTypeName) - , modelGenerationOptions - , code => Assert.Contains(string.Concat("using ", modelGenerationOptions.ModelNamespace, ";"), code.ContextFile.Code) - , model => Assert.NotNull(model.FindEntityType(string.Concat(modelGenerationOptions.ModelNamespace, ".", entityInAnotherNamespaceTypeName))) - ); + modelBuilder => modelBuilder.Entity(entityInAnotherNamespaceTypeName), + modelGenerationOptions, + code => Assert.Contains(string.Concat("using ", modelGenerationOptions.ModelNamespace, ";"), code.ContextFile.Code), + model => Assert.NotNull(model.FindEntityType(string.Concat(modelGenerationOptions.ModelNamespace, ".", entityInAnotherNamespaceTypeName))), + useTemplateGenerator: useTemplateGenerator); } - [ConditionalFact] - public Task ModelSameNamespaceDbContext_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ModelSameNamespaceDbContext_works(bool useTemplateGenerator) { var modelGenerationOptions = new ModelCodeGenerationOptions { ContextNamespace = "TestNamespace" }; const string entityInAnotherNamespaceTypeName = "EntityInAnotherNamespace"; return TestAsync( - modelBuilder => modelBuilder.Entity(entityInAnotherNamespaceTypeName) - , modelGenerationOptions - , code => Assert.DoesNotContain(string.Concat("using ", modelGenerationOptions.ModelNamespace, ";"), code.ContextFile.Code) - , model => Assert.NotNull(model.FindEntityType(string.Concat(modelGenerationOptions.ModelNamespace, ".", entityInAnotherNamespaceTypeName))) - ); + modelBuilder => modelBuilder.Entity(entityInAnotherNamespaceTypeName), + modelGenerationOptions, + code => Assert.DoesNotContain(string.Concat("using ", modelGenerationOptions.ModelNamespace, ";"), code.ContextFile.Code), + model => Assert.NotNull(model.FindEntityType(string.Concat(modelGenerationOptions.ModelNamespace, ".", entityInAnotherNamespaceTypeName))), + useTemplateGenerator: useTemplateGenerator); } - [ConditionalFact] - public Task ValueGenerated_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ValueGenerated_works(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity( "Entity", @@ -408,10 +443,13 @@ public Task ValueGenerated_works() Assert.True(entity.GetProperty("ConcurrencyToken").IsConcurrencyToken); Assert.Equal(ValueGenerated.OnUpdate, entity.GetProperty("ValueGeneratedOnUpdate").ValueGenerated); Assert.Equal(ValueGenerated.Never, entity.GetProperty("ValueGeneratedNever").ValueGenerated); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task HasPrecision_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task HasPrecision_works(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity( "Entity", @@ -433,10 +471,13 @@ public Task HasPrecision_works() Assert.Null(entity.GetProperty("HasPrecision").GetScale()); Assert.Equal(14, entity.GetProperty("HasPrecisionAndScale").GetPrecision()); Assert.Equal(7, entity.GetProperty("HasPrecisionAndScale").GetScale()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Collation_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Collation_works(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("UseCollation").UseCollation("Some Collation"), new ModelCodeGenerationOptions(), @@ -445,10 +486,13 @@ public Task Collation_works() { var entity = model.FindEntityType("TestNamespace.Entity"); Assert.Equal("Some Collation", entity.GetProperty("UseCollation").GetCollation()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task ComputedColumnSql_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ComputedColumnSql_works(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("ComputedColumn").HasComputedColumnSql("1 + 2"), new ModelCodeGenerationOptions(), @@ -457,10 +501,13 @@ public Task ComputedColumnSql_works() { var entity = model.FindEntityType("TestNamespace.Entity"); Assert.Equal("1 + 2", entity.GetProperty("ComputedColumn").GetComputedColumnSql()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Column_with_default_value_only_uses_default_value() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Column_with_default_value_only_uses_default_value(bool useTemplateGenerator) => TestAsync( serviceProvider => serviceProvider.GetService().Create( BuildModelWithColumn("nvarchar(max)", null, "Hot"), new ModelReverseEngineerOptions()), @@ -471,10 +518,13 @@ public Task Column_with_default_value_only_uses_default_value() var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); Assert.Equal("Hot", property.GetDefaultValue()); Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql)); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Column_with_default_value_sql_only_uses_default_value_sql() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Column_with_default_value_sql_only_uses_default_value_sql(bool useTemplateGenerator) => TestAsync( serviceProvider => serviceProvider.GetService().Create( BuildModelWithColumn("nvarchar(max)", "('Hot')", null), new ModelReverseEngineerOptions()), @@ -485,10 +535,13 @@ public Task Column_with_default_value_sql_only_uses_default_value_sql() var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); Assert.Equal("('Hot')", property.GetDefaultValueSql()); Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValue)); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Column_with_default_value_sql_and_default_value_uses_default_value() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Column_with_default_value_sql_and_default_value_uses_default_value(bool useTemplateGenerator) => TestAsync( serviceProvider => serviceProvider.GetService().Create( BuildModelWithColumn("nvarchar(max)", "('Hot')", "Hot"), new ModelReverseEngineerOptions()), @@ -499,10 +552,13 @@ public Task Column_with_default_value_sql_and_default_value_uses_default_value() var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); Assert.Equal("Hot", property.GetDefaultValue()); Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql)); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Column_with_default_value_sql_and_default_value_where_value_is_CLR_default_uses_neither() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Column_with_default_value_sql_and_default_value_where_value_is_CLR_default_uses_neither(bool useTemplateGenerator) => TestAsync( serviceProvider => serviceProvider.GetService().Create( BuildModelWithColumn("int", "((0))", 0), new ModelReverseEngineerOptions()), @@ -513,10 +569,13 @@ public Task Column_with_default_value_sql_and_default_value_where_value_is_CLR_d var property = model.FindEntityType("TestNamespace.Table")!.GetProperty("Column"); Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValue)); Assert.Null(property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql)); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task IsUnicode_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task IsUnicode_works(bool useTemplateGenerator) => TestAsync( modelBuilder => { @@ -534,10 +593,13 @@ public Task IsUnicode_works() var entity = model.FindEntityType("TestNamespace.Entity"); Assert.True(entity.GetProperty("UnicodeColumn").IsUnicode()); Assert.False(entity.GetProperty("NonUnicodeColumn").IsUnicode()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task ComputedColumnSql_works_stored() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ComputedColumnSql_works_stored(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("ComputedColumn") .HasComputedColumnSql("1 + 2", stored: true), @@ -547,10 +609,13 @@ public Task ComputedColumnSql_works_stored() { var entity = model.FindEntityType("TestNamespace.Entity"); Assert.True(entity.GetProperty("ComputedColumn").GetIsStored()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task ComputedColumnSql_works_unspecified() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ComputedColumnSql_works_unspecified(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("ComputedColumn").HasComputedColumnSql(), new ModelCodeGenerationOptions(), @@ -559,10 +624,13 @@ public Task ComputedColumnSql_works_unspecified() { var entity = model.FindEntityType("TestNamespace.Entity"); Assert.Empty(entity.GetProperty("ComputedColumn").GetComputedColumnSql()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task DefaultValue_works_unspecified() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task DefaultValue_works_unspecified(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("DefaultedColumn").HasDefaultValue(), new ModelCodeGenerationOptions(), @@ -571,10 +639,13 @@ public Task DefaultValue_works_unspecified() { var entity = model.FindEntityType("TestNamespace.Entity"); Assert.Equal(DBNull.Value, entity.GetProperty("DefaultedColumn").GetDefaultValue()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task DefaultValueSql_works_unspecified() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task DefaultValueSql_works_unspecified(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("DefaultedColumn").HasDefaultValueSql(), new ModelCodeGenerationOptions(), @@ -583,10 +654,13 @@ public Task DefaultValueSql_works_unspecified() { var entity = model.FindEntityType("TestNamespace.Entity"); Assert.Empty(entity.GetProperty("DefaultedColumn").GetDefaultValueSql()); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Entity_with_indexes_and_use_data_annotations_false_always_generates_fluent_API() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Entity_with_indexes_and_use_data_annotations_false_always_generates_fluent_API(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder .Entity( @@ -655,10 +729,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) code.ContextFile); }, model => - Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Entity_with_indexes_and_use_data_annotations_true_generates_fluent_API_only_for_indexes_with_annotations() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Entity_with_indexes_and_use_data_annotations_true_generates_fluent_API_only_for_indexes_with_annotations(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder .Entity( @@ -723,10 +800,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) code.ContextFile); }, model => - Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count())); + Assert.Equal(2, model.FindEntityType("TestNamespace.EntityWithIndexes").GetIndexes().Count()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Indexes_with_descending() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Indexes_with_descending(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder .Entity( @@ -818,10 +898,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) var mixedIndex = Assert.Single(entityType.GetIndexes(), i => i.Name == "IX_mixed"); Assert.Equal(new[] { false, true, false }, mixedIndex.IsDescending); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Entity_lambda_uses_correct_identifiers() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Entity_lambda_uses_correct_identifiers(bool useTemplateGenerator) => TestAsync( modelBuilder => { @@ -898,10 +981,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) """, code.ContextFile); }, - model => { }); + model => { }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Column_type_is_not_scaffolded_as_annotation() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Column_type_is_not_scaffolded_as_annotation(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder .Entity( @@ -957,10 +1043,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) code.ContextFile); }, model => - Assert.Equal("date", model.FindEntityType("TestNamespace.Employee").GetProperty("HireDate").GetConfiguredColumnType())); + Assert.Equal("date", model.FindEntityType("TestNamespace.Employee").GetProperty("HireDate").GetConfiguredColumnType()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Is_fixed_length_annotation_should_be_scaffolded_without_optional_parameter() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Is_fixed_length_annotation_should_be_scaffolded_without_optional_parameter(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder .Entity( @@ -973,10 +1062,13 @@ public Task Is_fixed_length_annotation_should_be_scaffolded_without_optional_par new ModelCodeGenerationOptions { UseDataAnnotations = false }, code => Assert.Contains(".IsFixedLength()", code.ContextFile.Code), model => - Assert.Equal(true, model.FindEntityType("TestNamespace.Employee").GetProperty("Name").IsFixedLength())); + Assert.Equal(true, model.FindEntityType("TestNamespace.Employee").GetProperty("Name").IsFixedLength()), + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Global_namespace_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Global_namespace_works(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("MyEntity"), new ModelCodeGenerationOptions { ModelNamespace = string.Empty }, @@ -1025,10 +1117,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) model => { Assert.NotNull(model.FindEntityType("MyEntity")); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Global_namespace_works_just_context() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Global_namespace_works_just_context(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("MyEntity"), new ModelCodeGenerationOptions { ModelNamespace = "TestNamespace", ContextNamespace = string.Empty }, @@ -1041,10 +1136,13 @@ public Task Global_namespace_works_just_context() model => { Assert.NotNull(model.FindEntityType("TestNamespace.MyEntity")); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Global_namespace_works_just_model() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Global_namespace_works_just_model(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("MyEntity"), new ModelCodeGenerationOptions { ModelNamespace = string.Empty, ContextNamespace = "TestNamespace" }, @@ -1056,10 +1154,13 @@ public Task Global_namespace_works_just_model() model => { Assert.NotNull(model.FindEntityType("MyEntity")); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Fluent_calls_in_custom_namespaces_work() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Fluent_calls_in_custom_namespaces_work(bool useTemplateGenerator) => TestAsync( modelBuilder => TestModelBuilderExtensions.TestFluentApiCall(modelBuilder), new ModelCodeGenerationOptions { SuppressOnConfiguring = true }, @@ -1096,7 +1197,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) Assert.Empty(code.AdditionalFiles); }, model => Assert.Empty(model.GetEntityTypes()), - skipBuild: true); + skipBuild: true, + useTemplateGenerator: useTemplateGenerator); [ConditionalFact] public async Task Temporal_table_works() @@ -1171,8 +1273,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // TODO }))).Message); - [ConditionalFact] - public Task Sequences_work() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Sequences_work(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.HasSequence("EvenNumbers", "dbo") .StartsAt(2) @@ -1202,10 +1306,13 @@ public Task Sequences_work() Assert.Equal(2, sequence.MinValue); Assert.Equal(100, sequence.MaxValue); Assert.True(sequence.IsCyclic); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task Trigger_works() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task Trigger_works(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder .Entity( @@ -1278,10 +1385,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) triggers, t => Assert.Equal("Trigger1", t.GetDatabaseName()), t => Assert.Equal("Trigger2", t.GetDatabaseName())); - }); + }, + useTemplateGenerator: useTemplateGenerator); - [ConditionalFact] - public Task ValueGenerationStrategy_works_when_none() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public Task ValueGenerationStrategy_works_when_none(bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity( "Channel", @@ -1339,12 +1449,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) var entityType = Assert.Single(model.GetEntityTypes()); var property = Assert.Single(entityType.GetProperties()); Assert.Equal(SqlServerValueGenerationStrategy.None, property.GetValueGenerationStrategy()); - }); + }, + useTemplateGenerator: useTemplateGenerator); [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] - public Task ColumnOrder_is_ignored(bool useDataAnnotations) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public Task ColumnOrder_is_ignored(bool useDataAnnotations, bool useTemplateGenerator) => TestAsync( modelBuilder => modelBuilder.Entity("Entity").Property("Property").HasColumnOrder(1), new ModelCodeGenerationOptions { UseDataAnnotations = useDataAnnotations }, diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs index ae1cc54c2d0..62b739eac48 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs @@ -24,7 +24,8 @@ protected Task TestAsync( ModelCodeGenerationOptions options, Action assertScaffold, Action assertModel, - bool skipBuild = false) + bool skipBuild = false, + bool useTemplateGenerator = true) { var designServices = new ServiceCollection(); AddModelServices(designServices); @@ -39,7 +40,7 @@ protected Task TestAsync( var serviceProvider = services.BuildServiceProvider(validateScopes: true); - return TestAsync(serviceProvider, model, options, assertScaffold, assertModel, skipBuild); + return TestAsync(serviceProvider, model, options, assertScaffold, assertModel, skipBuild, useTemplateGenerator); } protected Task TestAsync( @@ -47,7 +48,8 @@ protected Task TestAsync( ModelCodeGenerationOptions options, Action assertScaffold, Action assertModel, - bool skipBuild = false) + bool skipBuild = false, + bool useTemplateGenerator = true) { var designServices = new ServiceCollection(); AddModelServices(designServices); @@ -56,7 +58,7 @@ protected Task TestAsync( var serviceProvider = services.BuildServiceProvider(validateScopes: true); var model = buildModel(serviceProvider); - return TestAsync(serviceProvider, model, options, assertScaffold, assertModel, skipBuild); + return TestAsync(serviceProvider, model, options, assertScaffold, assertModel, skipBuild, useTemplateGenerator); } protected async Task TestAsync( @@ -65,12 +67,14 @@ protected async Task TestAsync( ModelCodeGenerationOptions options, Action assertScaffold, Action assertModel, - bool skipBuild = false) + bool skipBuild = false, + bool useTemplateGenerator = true) { var generators = serviceProvider.GetServices(); - var generator = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + var generator = + RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - || Random.Shared.Next() % 12 != 0 + || !useTemplateGenerator ? generators.Last(g => g is CSharpModelGenerator) : generators.Last(g => g is TextTemplatingModelGenerator); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingEngineHostTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingEngineHostTest.cs index 1fd71e38ffc..5b58c44d932 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingEngineHostTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/TextTemplatingEngineHostTest.cs @@ -22,9 +22,9 @@ public void Service_works() .AddSingleton("Hello, Services!") .BuildServiceProvider()); - var result = _engine.ProcessTemplate( + var result = _engine.ProcessTemplateAsync( @"<#@ template hostSpecific=""true"" #><#= ((IServiceProvider)Host).GetService(typeof(string)) #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal("Hello, Services!", result); @@ -35,9 +35,9 @@ public void Session_works() { var host = new TextTemplatingEngineHost { Session = new TextTemplatingSession { ["Value"] = "Hello, Session!" } }; - var result = _engine.ProcessTemplate( + var result = _engine.ProcessTemplateAsync( @"<#= Session[""Value""] #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal("Hello, Session!", result); @@ -48,9 +48,9 @@ public void Session_works_with_parameter() { var host = new TextTemplatingEngineHost { Session = new TextTemplatingSession { ["Value"] = "Hello, Session!" } }; - var result = _engine.ProcessTemplate( + var result = _engine.ProcessTemplateAsync( @"<#@ parameter name=""Value"" type=""System.String"" #><#= Value #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal("Hello, Session!", result); @@ -66,9 +66,9 @@ public void Include_works() var host = new TextTemplatingEngineHost { TemplateFile = Path.Combine(dir, "test.tt") }; - var result = _engine.ProcessTemplate( + var result = _engine.ProcessTemplateAsync( @"<#@ include file=""test.ttinclude"" #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal("Hello, Include!", result); @@ -79,9 +79,9 @@ public void Error_works() { var host = new TextTemplatingEngineHost(); - _engine.ProcessTemplate( + _engine.ProcessTemplateAsync( @"<# Error(""Hello, Error!""); #>", - host); + host).GetAwaiter().GetResult();; var error = Assert.Single(host.Errors.Cast()); Assert.Equal("Hello, Error!", error.ErrorText); @@ -93,9 +93,9 @@ public void Directive_throws_when_processor_unknown() var host = new TextTemplatingEngineHost(); var ex = Assert.Throws( - () => _engine.ProcessTemplate( + () => _engine.ProcessTemplateAsync( @"<#@ test processor=""TestDirectiveProcessor"" #>", - host)); + host).GetAwaiter().GetResult()); Assert.Equal(DesignStrings.UnknownDirectiveProcessor("TestDirectiveProcessor"), ex.Message); } @@ -107,9 +107,9 @@ public void ResolvePath_work() var host = new TextTemplatingEngineHost { TemplateFile = Path.Combine(dir, "test.tt") }; - var result = _engine.ProcessTemplate( + var result = _engine.ProcessTemplateAsync( @"<#@ template hostSpecific=""true"" #><#= Host.ResolvePath(""data.json"") #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal(Path.Combine(dir, "data.json"), result); @@ -120,9 +120,9 @@ public void Output_works() { var host = new TextTemplatingEngineHost(); - _engine.ProcessTemplate( + _engine.ProcessTemplateAsync( @"<#@ output extension="".txt"" encoding=""us-ascii"" #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal(".txt", host.Extension); @@ -134,9 +134,9 @@ public void Assembly_works() { var host = new TextTemplatingEngineHost(); - var result = _engine.ProcessTemplate( + var result = _engine.ProcessTemplateAsync( @"<#@ assembly name=""Microsoft.EntityFrameworkCore"" #><#= nameof(Microsoft.EntityFrameworkCore.DbContext) #>", - host); + host).GetAwaiter().GetResult();; Assert.Empty(host.Errors); Assert.Equal("DbContext", result);