diff --git a/.editorconfig b/.editorconfig index aa151b986e4..fd1da7162ae 100644 --- a/.editorconfig +++ b/.editorconfig @@ -44,6 +44,10 @@ indent_size = 2 end_of_line = crlf indent_size = 2 +# Scaffolding templates and their pre-processed output: line breaks here end up in scaffolded code, which is always LF +[src/EFCore.Design/Scaffolding/Internal/CSharp{DbContext,EntityType}Generator.{tt,cs}] +end_of_line = lf + ## Language conventions # Dotnet code style settings: [*.{cs,vb}] diff --git a/.gitattributes b/.gitattributes index 31d89692eef..09c14da47fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,4 +4,11 @@ *.sln eol=crlf *.sql diff +# The scaffolding templates and their pre-processed output must use LF so that scaffolded code always has LF line endings +# (line breaks inside the templates become string literals in the generated code). +src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt eol=lf +src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs eol=lf +src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt eol=lf +src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs eol=lf + .github/workflows/*.lock.yml linguist-generated=true \ No newline at end of file diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs index 08a74c78dee..a9d7ea0737f 100644 --- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs +++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs @@ -23,6 +23,8 @@ namespace Microsoft.EntityFrameworkCore.Design.Internal; /// public class CSharpHelper : ICSharpHelper { + private const int IndentSize = 4; + private readonly ITypeMappingSource _typeMappingSource; private readonly Project _project; private readonly RuntimeModelLinqToCSharpSyntaxTranslator _translator; @@ -46,6 +48,17 @@ public CSharpHelper(ITypeMappingSource typeMappingSource) _translator = new RuntimeModelLinqToCSharpSyntaxTranslator(syntaxGenerator); } + /// + /// The line terminator used by , + /// and . + /// Defaults to . + /// + /// + /// This does not affect the vertical collection literals, which are built with . + /// + protected virtual string NewLine + => Environment.NewLine; + private static readonly IReadOnlyCollection Keywords = [ "__arglist", @@ -1368,7 +1381,7 @@ public virtual string Fragment(IMethodCallCodeFragment? fragment, int indent = 0 return string.Empty; } - var builder = new IndentedStringBuilder(); + var builder = new StringBuilder(); if (fragment.ChainedCall is null) { @@ -1376,15 +1389,12 @@ public virtual string Fragment(IMethodCallCodeFragment? fragment, int indent = 0 } else { - for (var i = 0; i < indent; i++) - { - builder.IncrementIndent(); - } + var indentString = new string(' ', indent * IndentSize); var current = fragment; do { - builder.AppendLine(); + builder.Append(NewLine).Append(indentString); AppendMethodCall(current); current = current.ChainedCall; @@ -1450,26 +1460,23 @@ public virtual string Fragment(NestedClosureCodeFragment fragment, int indent = return fragment.Parameter + " => " + fragment.Parameter + Fragment(fragment.MethodCalls[0], indent); } - var builder = new IndentedStringBuilder(); - builder.AppendLine(fragment.Parameter + " =>"); - for (var i = 0; i < indent - 1; i++) - { - builder.IncrementIndent(); - } + var outerIndent = new string(' ', Math.Max(indent - 1, 0) * IndentSize); + var innerIndent = outerIndent + new string(' ', IndentSize); - builder.AppendLine("{"); - using (builder.Indent()) + var builder = new StringBuilder(); + builder.Append(fragment.Parameter).Append(" =>").Append(NewLine); + builder.Append(outerIndent).Append('{').Append(NewLine); + foreach (var methodCall in fragment.MethodCalls) { - foreach (var methodCall in fragment.MethodCalls) - { - builder - .Append(fragment.Parameter) - .Append(Fragment(methodCall, indent + 1)); - builder.AppendLine(";"); - } + builder + .Append(innerIndent) + .Append(fragment.Parameter) + .Append(Fragment(methodCall, indent + 1)) + .Append(';') + .Append(NewLine); } - builder.Append("}"); + builder.Append(outerIndent).Append('}'); return builder.ToString(); } @@ -1564,8 +1571,8 @@ public virtual string XmlComment(string comment, int indent = 0) if (!first) { builder - .AppendLine() - .Append(' ', indent * 4) + .Append(NewLine) + .Append(' ', indent * IndentSize) .Append("/// "); } else diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 841a6024a3d..c79c6729f35 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -53,20 +53,20 @@ public virtual string TransformText() this.Write("namespace "); this.Write(this.ToStringHelper.ToStringWithCulture(NamespaceHint)); - this.Write(";\r\n\r\n"); + this.Write(";\n\n"); } this.Write("public partial class "); this.Write(this.ToStringHelper.ToStringWithCulture(Options.ContextName)); - this.Write(" : DbContext\r\n{\r\n"); + this.Write(" : DbContext\n{\n"); if (!Options.SuppressOnConfiguring) { this.Write(" public "); this.Write(this.ToStringHelper.ToStringWithCulture(Options.ContextName)); - this.Write("()\r\n {\r\n }\r\n\r\n"); + this.Write("()\n {\n }\n\n"); } @@ -74,7 +74,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(Options.ContextName)); this.Write("(DbContextOptions<"); this.Write(this.ToStringHelper.ToStringWithCulture(Options.ContextName)); - this.Write("> options)\r\n : base(options)\r\n {\r\n }\r\n\r\n"); + this.Write("> options)\n : base(options)\n {\n }\n\n"); foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType())) { @@ -83,7 +83,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(entityType.Name)); this.Write("> "); this.Write(this.ToStringHelper.ToStringWithCulture(entityType.GetDbSetName())); - this.Write(" { get; set; }\r\n\r\n"); + this.Write(" { get; set; }\n\n"); } @@ -91,7 +91,7 @@ public virtual string TransformText() { this.Write(" protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)" + - "\r\n"); + "\n"); if (!Options.SuppressConnectionStringWarning) { @@ -106,12 +106,12 @@ public virtual string TransformText() this.Write(" => optionsBuilder"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(useProviderCall, indent: 3))); - this.Write(";\r\n\r\n"); + this.Write(";\n\n"); } - this.Write(" protected override void OnModelCreating(ModelBuilder modelBuilder)\r\n {\r\n"); + this.Write(" protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n"); var anyConfiguration = false; @@ -122,7 +122,7 @@ public virtual string TransformText() this.Write(" modelBuilder"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(modelFluentApiCalls, indent: 3))); - this.Write(";\r\n"); + this.Write(";\n"); anyConfiguration = true; } @@ -136,14 +136,14 @@ public virtual string TransformText() if (anyConfiguration) { - WriteLine(""); + Write("\n"); } var anyEntityTypeConfiguration = false; this.Write(" modelBuilder.Entity<"); this.Write(this.ToStringHelper.ToStringWithCulture(entityType.Name)); - this.Write(">(entity =>\r\n {\r\n"); + this.Write(">(entity =>\n {\n"); var key = entityType.FindPrimaryKey(); if (key != null) @@ -161,7 +161,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(code.Lambda(key.Properties, "e"))); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(keyFluentApiCalls, indent: 4))); - this.Write(";\r\n"); + this.Write(";\n"); anyEntityTypeConfiguration = true; } @@ -175,12 +175,12 @@ public virtual string TransformText() if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } this.Write(" entity"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(entityTypeFluentApiCalls, indent: 4))); - this.Write(";\r\n"); + this.Write(";\n"); anyEntityTypeConfiguration = true; } @@ -190,7 +190,7 @@ public virtual string TransformText() { if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator); @@ -205,7 +205,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(code.Literal(index.GetDatabaseName()))); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(indexFluentApiCalls, indent: 4))); - this.Write(";\r\n"); + this.Write(";\n"); anyEntityTypeConfiguration = true; } @@ -225,14 +225,14 @@ public virtual string TransformText() if (anyEntityTypeConfiguration && firstProperty) { - WriteLine(""); + Write("\n"); } this.Write(" entity.Property(e => e."); this.Write(this.ToStringHelper.ToStringWithCulture(property.Name)); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(propertyFluentApiCalls, indent: 4))); - this.Write(";\r\n"); + this.Write(";\n"); anyEntityTypeConfiguration = true; firstProperty = false; @@ -251,7 +251,7 @@ public virtual string TransformText() if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } this.Write(" entity.HasOne(d => d."); @@ -262,7 +262,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(foreignKey.PrincipalToDependent != null ? $"p => p.{foreignKey.PrincipalToDependent.Name}" : "")); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(foreignKeyFluentApiCalls, indent: 4))); - this.Write(";\r\n"); + this.Write(";\n"); anyEntityTypeConfiguration = true; } @@ -271,7 +271,7 @@ public virtual string TransformText() { if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } var left = skipNavigation.ForeignKey; @@ -294,18 +294,18 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(skipNavigation.Name)); this.Write(").WithMany(p => p."); this.Write(this.ToStringHelper.ToStringWithCulture(skipNavigation.Inverse.Name)); - this.Write(")\r\n .UsingEntity>(\r\n " + + this.Write(")\n .UsingEntity>(\n " + " "); this.Write(this.ToStringHelper.ToStringWithCulture(code.Literal(joinEntityType.Name))); - this.Write(",\r\n r => r.HasOne<"); + this.Write(",\n r => r.HasOne<"); this.Write(this.ToStringHelper.ToStringWithCulture(right.PrincipalEntityType.Name)); this.Write(">().WithMany()"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(rightFluentApiCalls, indent: 6))); - this.Write(",\r\n l => l.HasOne<"); + this.Write(",\n l => l.HasOne<"); this.Write(this.ToStringHelper.ToStringWithCulture(left.PrincipalEntityType.Name)); this.Write(">().WithMany()"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(leftFluentApiCalls, indent: 6))); - this.Write(",\r\n j =>\r\n {\r\n"); + this.Write(",\n j =>\n {\n"); var joinKey = joinEntityType.FindPrimaryKey(); var joinKeyFluentApiCalls = joinKey.GetFluentApiCalls(annotationCodeGenerator); @@ -319,7 +319,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(code.Arguments(joinKey.Properties.Select(e => e.Name)))); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(joinKeyFluentApiCalls, indent: 7))); - this.Write(";\r\n"); + this.Write(";\n"); var joinEntityTypeFluentApiCalls = joinEntityType.GetFluentApiCalls(annotationCodeGenerator); if (joinEntityTypeFluentApiCalls != null) @@ -328,7 +328,7 @@ public virtual string TransformText() this.Write(" j"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(joinEntityTypeFluentApiCalls, indent: 7))); - this.Write(";\r\n"); + this.Write(";\n"); } @@ -346,7 +346,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(code.Literal(index.GetDatabaseName()))); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(indexFluentApiCalls, indent: 7))); - this.Write(";\r\n"); + this.Write(";\n"); } @@ -364,16 +364,16 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(code.Literal(property.Name))); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(propertyFluentApiCalls, indent: 7))); - this.Write(";\r\n"); + this.Write(";\n"); } - this.Write(" });\r\n"); + this.Write(" });\n"); anyEntityTypeConfiguration = true; } - this.Write(" });\r\n"); + this.Write(" });\n"); // If any signicant code was generated, append it to the main environment if (anyEntityTypeConfiguration) @@ -399,17 +399,17 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(needsSchema ? $", {code.Literal(sequence.Schema)}" : "")); this.Write(")"); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(sequenceFluentApiCalls, indent: 3))); - this.Write(";\r\n"); + this.Write(";\n"); } if (anyConfiguration) { - WriteLine(""); + Write("\n"); } - this.Write(" OnModelCreatingPartial(modelBuilder);\r\n }\r\n\r\n partial void OnModelC" + - "reatingPartial(ModelBuilder modelBuilder);\r\n}\r\n"); + this.Write(" OnModelCreatingPartial(modelBuilder);\n }\n\n partial void OnModelC" + + "reatingPartial(ModelBuilder modelBuilder);\n}\n"); mainEnvironment = GenerationEnvironment; GenerationEnvironment = new StringBuilder(); @@ -419,11 +419,11 @@ public virtual string TransformText() this.Write("using "); this.Write(this.ToStringHelper.ToStringWithCulture(ns)); - this.Write(";\r\n"); + this.Write(";\n"); } - WriteLine(""); + Write("\n"); GenerationEnvironment.Append(mainEnvironment); @@ -705,7 +705,7 @@ public void Write(string textToAppend) this.endsWithNewline = false; } // Check if the current text ends with a newline - if (textToAppend.EndsWith(global::System.Environment.NewLine, global::System.StringComparison.CurrentCulture)) + if (textToAppend.EndsWith("\n", global::System.StringComparison.Ordinal)) { this.endsWithNewline = true; } @@ -717,7 +717,7 @@ public void Write(string textToAppend) return; } // Everywhere there is a newline in the text, add an indent after it - textToAppend = textToAppend.Replace(global::System.Environment.NewLine, (global::System.Environment.NewLine + this.currentIndentField)); + textToAppend = textToAppend.Replace("\n", ("\n" + this.currentIndentField)); // If the text ends with a newline, then we should strip off the indent added at the very end // because the appropriate indent will be added when the next time Write() is called if (this.endsWithNewline) @@ -735,7 +735,7 @@ public void Write(string textToAppend) public void WriteLine(string textToAppend) { this.Write(textToAppend); - this.GenerationEnvironment.AppendLine(); + this.GenerationEnvironment.Append('\n'); this.endsWithNewline = true; } /// diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt index 5dfd1a48a32..2db4e8c41a2 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt @@ -112,7 +112,7 @@ public partial class <#= Options.ContextName #> : DbContext if (anyConfiguration) { - WriteLine(""); + Write("\n"); } var anyEntityTypeConfiguration = false; @@ -146,7 +146,7 @@ public partial class <#= Options.ContextName #> : DbContext if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } #> entity<#= code.Fragment(entityTypeFluentApiCalls, indent: 4) #>; @@ -159,7 +159,7 @@ public partial class <#= Options.ContextName #> : DbContext { if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } var indexFluentApiCalls = index.GetFluentApiCalls(annotationCodeGenerator); @@ -188,7 +188,7 @@ public partial class <#= Options.ContextName #> : DbContext if (anyEntityTypeConfiguration && firstProperty) { - WriteLine(""); + Write("\n"); } #> entity.Property(e => e.<#= property.Name #>)<#= code.Fragment(propertyFluentApiCalls, indent: 4) #>; @@ -210,7 +210,7 @@ public partial class <#= Options.ContextName #> : DbContext if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } #> entity.HasOne(d => d.<#= foreignKey.DependentToPrincipal.Name #>).<#= foreignKey.IsUnique ? "WithOne" : "WithMany" #>(<#= foreignKey.PrincipalToDependent != null ? $"p => p.{foreignKey.PrincipalToDependent.Name}" : "" #>)<#= code.Fragment(foreignKeyFluentApiCalls, indent: 4) #>; @@ -222,7 +222,7 @@ public partial class <#= Options.ContextName #> : DbContext { if (anyEntityTypeConfiguration) { - WriteLine(""); + Write("\n"); } var left = skipNavigation.ForeignKey; @@ -322,7 +322,7 @@ public partial class <#= Options.ContextName #> : DbContext if (anyConfiguration) { - WriteLine(""); + Write("\n"); } #> OnModelCreatingPartial(modelBuilder); @@ -341,7 +341,7 @@ using <#= ns #>; <# } - WriteLine(""); + Write("\n"); GenerationEnvironment.Append(mainEnvironment); #> diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index f34bc4a92d7..e8e1c199c84 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -58,16 +58,16 @@ public virtual string TransformText() this.Write("namespace "); this.Write(this.ToStringHelper.ToStringWithCulture(NamespaceHint)); - this.Write(";\r\n\r\n"); + this.Write(";\n\n"); } if (!string.IsNullOrEmpty(EntityType.GetComment())) { - this.Write("/// \r\n/// "); + this.Write("/// \n/// "); this.Write(this.ToStringHelper.ToStringWithCulture(code.XmlComment(EntityType.GetComment()))); - this.Write("\r\n/// \r\n"); + this.Write("\n/// \n"); } @@ -77,29 +77,29 @@ public virtual string TransformText() { this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(dataAnnotation))); - this.Write("\r\n"); + this.Write("\n"); } } this.Write("public partial class "); this.Write(this.ToStringHelper.ToStringWithCulture(EntityType.Name)); - this.Write("\r\n{\r\n"); + this.Write("\n{\n"); var firstProperty = true; foreach (var property in EntityType.GetProperties().OrderBy(p => p.GetColumnOrder() ?? -1)) { if (!firstProperty) { - WriteLine(""); + Write("\n"); } if (!string.IsNullOrEmpty(property.GetComment())) { - this.Write(" /// \r\n /// "); + this.Write(" /// \n /// "); this.Write(this.ToStringHelper.ToStringWithCulture(code.XmlComment(property.GetComment(), indent: 1))); - this.Write("\r\n /// \r\n"); + this.Write("\n /// \n"); } @@ -112,7 +112,7 @@ public virtual string TransformText() this.Write(" "); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(dataAnnotation))); - this.Write("\r\n"); + this.Write("\n"); } } @@ -129,14 +129,14 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(property.Name)); this.Write(" { get; set; }"); this.Write(this.ToStringHelper.ToStringWithCulture(needsInitializer ? " = null!;" : "")); - this.Write("\r\n"); + this.Write("\n"); firstProperty = false; } foreach (var navigation in EntityType.GetNavigations()) { - WriteLine(""); + Write("\n"); if (Options.UseDataAnnotations) { @@ -145,7 +145,7 @@ public virtual string TransformText() this.Write(" "); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(dataAnnotation))); - this.Write("\r\n"); + this.Write("\n"); } } @@ -160,7 +160,7 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(navigation.Name)); this.Write(" { get; set; } = new List<"); this.Write(this.ToStringHelper.ToStringWithCulture(targetType)); - this.Write(">();\r\n"); + this.Write(">();\n"); } else @@ -175,14 +175,14 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(navigation.Name)); this.Write(" { get; set; }"); this.Write(this.ToStringHelper.ToStringWithCulture(needsInitializer ? " = null!;" : "")); - this.Write("\r\n"); + this.Write("\n"); } } foreach (var skipNavigation in EntityType.GetSkipNavigations()) { - WriteLine(""); + Write("\n"); if (Options.UseDataAnnotations) { @@ -191,7 +191,7 @@ public virtual string TransformText() this.Write(" "); this.Write(this.ToStringHelper.ToStringWithCulture(code.Fragment(dataAnnotation))); - this.Write("\r\n"); + this.Write("\n"); } } @@ -202,11 +202,11 @@ public virtual string TransformText() this.Write(this.ToStringHelper.ToStringWithCulture(skipNavigation.Name)); this.Write(" { get; set; } = new List<"); this.Write(this.ToStringHelper.ToStringWithCulture(skipNavigation.TargetEntityType.Name)); - this.Write(">();\r\n"); + this.Write(">();\n"); } - this.Write("}\r\n"); + this.Write("}\n"); var previousOutput = GenerationEnvironment; GenerationEnvironment = new StringBuilder(); @@ -216,11 +216,11 @@ public virtual string TransformText() this.Write("using "); this.Write(this.ToStringHelper.ToStringWithCulture(ns)); - this.Write(";\r\n"); + this.Write(";\n"); } - WriteLine(""); + Write("\n"); GenerationEnvironment.Append(previousOutput); @@ -502,7 +502,7 @@ public void Write(string textToAppend) this.endsWithNewline = false; } // Check if the current text ends with a newline - if (textToAppend.EndsWith(global::System.Environment.NewLine, global::System.StringComparison.CurrentCulture)) + if (textToAppend.EndsWith("\n", global::System.StringComparison.Ordinal)) { this.endsWithNewline = true; } @@ -514,7 +514,7 @@ public void Write(string textToAppend) return; } // Everywhere there is a newline in the text, add an indent after it - textToAppend = textToAppend.Replace(global::System.Environment.NewLine, (global::System.Environment.NewLine + this.currentIndentField)); + textToAppend = textToAppend.Replace("\n", ("\n" + this.currentIndentField)); // If the text ends with a newline, then we should strip off the indent added at the very end // because the appropriate indent will be added when the next time Write() is called if (this.endsWithNewline) @@ -532,7 +532,7 @@ public void Write(string textToAppend) public void WriteLine(string textToAppend) { this.Write(textToAppend); - this.GenerationEnvironment.AppendLine(); + this.GenerationEnvironment.Append('\n'); this.endsWithNewline = true; } /// diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt index fedb55bae0c..b66953f43f6 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt @@ -72,7 +72,7 @@ public partial class <#= EntityType.Name #> { if (!firstProperty) { - WriteLine(""); + Write("\n"); } if (!string.IsNullOrEmpty(property.GetComment())) @@ -108,7 +108,7 @@ public partial class <#= EntityType.Name #> foreach (var navigation in EntityType.GetNavigations()) { - WriteLine(""); + Write("\n"); if (Options.UseDataAnnotations) { @@ -139,7 +139,7 @@ public partial class <#= EntityType.Name #> foreach (var skipNavigation in EntityType.GetSkipNavigations()) { - WriteLine(""); + Write("\n"); if (Options.UseDataAnnotations) { @@ -167,7 +167,7 @@ using <#= ns #>; <# } - WriteLine(""); + Write("\n"); GenerationEnvironment.Append(previousOutput); #> diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs index 6778858a4e8..3a52f6501f2 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpModelGenerator.cs @@ -65,7 +65,8 @@ public override ScaffoldedModel GenerateModel( CoreStrings.ArgumentPropertyNull(nameof(options.ConnectionString), nameof(options)), nameof(options)); } - var host = new TextTemplatingEngineHost(_serviceProvider); + // Scaffolded code always uses LF line endings, regardless of platform. + var host = new TextTemplatingEngineHost(new LfCSharpHelperServiceProvider(_serviceProvider)); var contextTemplate = new CSharpDbContextGenerator { Host = host, Session = host.CreateSession() }; contextTemplate.Session.Add("Model", model); contextTemplate.Session.Add("Options", options); diff --git a/src/EFCore.Design/Scaffolding/Internal/LfCSharpHelperServiceProvider.cs b/src/EFCore.Design/Scaffolding/Internal/LfCSharpHelperServiceProvider.cs new file mode 100644 index 00000000000..d71a2263ba3 --- /dev/null +++ b/src/EFCore.Design/Scaffolding/Internal/LfCSharpHelperServiceProvider.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Design.Internal; + +namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +/// +/// Wraps a service provider so that scaffolding templates are handed an that emits LF +/// line endings, without changing the shared that migrations, snapshots and compiled +/// models use. A helper that has been replaced or subclassed by the application is left alone. +/// +internal sealed class LfCSharpHelperServiceProvider(IServiceProvider inner) : IServiceProvider +{ + private ICSharpHelper? _helper; + + public object? GetService(Type serviceType) + { + var service = inner.GetService(serviceType); + + if (serviceType != typeof(ICSharpHelper) + || service?.GetType() != typeof(CSharpHelper)) + { + return service; + } + + return _helper ??= new ScaffoldingCSharpHelper(inner.GetRequiredService()); + } +} diff --git a/src/EFCore.Design/Scaffolding/Internal/ScaffoldingCSharpHelper.cs b/src/EFCore.Design/Scaffolding/Internal/ScaffoldingCSharpHelper.cs new file mode 100644 index 00000000000..cab84774676 --- /dev/null +++ b/src/EFCore.Design/Scaffolding/Internal/ScaffoldingCSharpHelper.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Design.Internal; + +namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; + +/// +/// 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. +/// +public class ScaffoldingCSharpHelper : CSharpHelper +{ + /// + /// 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. + /// + public ScaffoldingCSharpHelper(ITypeMappingSource typeMappingSource) + : base(typeMappingSource) + { + } + + /// + /// 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. + /// + protected override string NewLine + => "\n"; +} diff --git a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs index cc1df14d264..ffe369bea1d 100644 --- a/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/TextTemplatingModelGenerator.cs @@ -87,7 +87,8 @@ public override ScaffoldedModel GenerateModel(IModel model, ModelCodeGenerationO CoreStrings.ArgumentPropertyNull(nameof(options.ConnectionString), nameof(options)), nameof(options)); } - var host = new TextTemplatingEngineHost(_serviceProvider) + // Scaffolded code always uses LF line endings, regardless of platform. + var host = new TextTemplatingEngineHost(new LfCSharpHelperServiceProvider(_serviceProvider)) { Session = { diff --git a/src/EFCore.Templates/EFCore.Templates.csproj b/src/EFCore.Templates/EFCore.Templates.csproj index f21d190ab80..645d77576e0 100644 --- a/src/EFCore.Templates/EFCore.Templates.csproj +++ b/src/EFCore.Templates/EFCore.Templates.csproj @@ -57,7 +57,7 @@ var versionCheckAdded = !IsDbContext; Directory.CreateDirectory(Path.GetDirectoryName(OutputPath)); using (var reader = new StreamReader(TemplateFile)) -using (var writer = new StreamWriter(OutputPath)) +using (var writer = new StreamWriter(OutputPath) { NewLine = "\n" }) // scaffolded code always uses LF, so the templates do too { string line; while ((line = reader.ReadLine()) != null) diff --git a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs index 63031f7087f..3130b071585 100644 --- a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs +++ b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs @@ -3,6 +3,7 @@ using System.Numerics; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Scaffolding.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; @@ -612,6 +613,62 @@ public void XmlComment_handles_all_line_terminators() result); } + [Fact] + public void Scaffolding_helper_uses_only_LF_for_fragments_and_comments() + { + var helper = new ScaffoldingCSharpHelper(TypeMappingSource); + + var chain = new MethodCallCodeFragment(_testFuncMethodInfo, "One") + .Chain( + new MethodCallCodeFragment( + _testFuncMethodInfo, + new NestedClosureCodeFragment( + "x", + [new MethodCallCodeFragment(_testFuncMethodInfo, "Two"), new MethodCallCodeFragment(_testFuncMethodInfo, "Three")]))); + + Assert.Equal( + "\n .TestFunc(\"One\")\n .TestFunc(x =>\n {\n x.TestFunc(\"Two\");\n x.TestFunc(\"Three\");\n })", + helper.Fragment(chain, indent: 1)); + + Assert.Equal( + "one\n /// two\n /// three", + helper.XmlComment("one\r\ntwo\nthree", indent: 1)); + } + + [Fact] + public void Fragment_and_XmlComment_use_the_NewLine_of_the_helper() + { + // Independent of the platform: the default is Environment.NewLine, and an override is honored everywhere. + var chain = new MethodCallCodeFragment(_testFuncMethodInfo, "One") + .Chain( + new MethodCallCodeFragment( + _testFuncMethodInfo, + new NestedClosureCodeFragment( + "x", + [new MethodCallCodeFragment(_testFuncMethodInfo, "Two"), new MethodCallCodeFragment(_testFuncMethodInfo, "Three")]))); + + var defaultHelper = new CSharpHelper(TypeMappingSource); + var crlfHelper = new CrLfCSharpHelper(TypeMappingSource); + + Assert.Equal( + defaultHelper.Fragment(chain, indent: 1), + crlfHelper.Fragment(chain, indent: 1).Replace("\r\n", EOL)); + Assert.Equal( + defaultHelper.XmlComment("one\ntwo", indent: 1), + crlfHelper.XmlComment("one\ntwo", indent: 1).Replace("\r\n", EOL)); + + var crlfFragment = crlfHelper.Fragment(chain, indent: 1); + Assert.Equal(6, crlfFragment.Split("\r\n").Length - 1); + Assert.DoesNotContain('\n', crlfFragment.Replace("\r\n", "")); + Assert.Equal("one\r\n /// two", crlfHelper.XmlComment("one\ntwo", indent: 1)); + } + + private sealed class CrLfCSharpHelper(ITypeMappingSource typeMappingSource) : CSharpHelper(typeMappingSource) + { + protected override string NewLine + => "\r\n"; + } + [Fact] public void Really_unknown_literal_with_no_mapping_support() { diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs index 38518da63c0..c4067e4b053 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpModelGeneratorTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore.Design.Internal; // ReSharper disable InconsistentNaming @@ -44,8 +45,63 @@ public void WriteCode_works() Assert.NotEmpty(result.AdditionalFiles[0].Code); } - private static IModelCodeGenerator CreateGenerator() + [Theory] + [InlineData(false, typeof(CSharpModelGenerator))] + [InlineData(true, typeof(CSharpModelGenerator))] + [InlineData(false, typeof(TextTemplatingModelGenerator))] + [InlineData(true, typeof(TextTemplatingModelGenerator))] + public void Generated_code_uses_only_LF_line_endings(bool useDataAnnotations, Type generatorType) { + var generator = CreateGenerator(generatorType); + var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); + modelBuilder.Entity( + "TestEntity", b => + { + b.ToTable(t => t.HasComment("Entity comment\r\nover several\nlines")); + b.Property("Id"); + b.Property("Name").HasMaxLength(10).IsRequired().HasComment("Property comment\r\nover several\nlines"); + b.Property("Other").HasColumnName("other_name").HasDefaultValueSql("1 +\r\n2"); + b.HasIndex("Name", "Other").IsUnique().HasFilter("[Name] IS NOT NULL"); + }); + modelBuilder.Entity("Related", b => b.Property("Id")); + modelBuilder.Entity("TestEntity").HasMany("Related", "Relateds").WithMany("TestEntities"); + modelBuilder.HasSequence("TestSequence").StartsAt(5).IsCyclic(); + + var result = generator.GenerateModel( + modelBuilder.FinalizeModel(designTime: true), + new ModelCodeGenerationOptions + { + ModelNamespace = "TestNamespace", + ContextName = "TestContext", + ConnectionString = "Data Source=Test", + Language = "C#", + ProjectDir = Path.Combine(Path.GetTempPath(), "NoCodeTemplates"), // no custom templates here + UseDataAnnotations = useDataAnnotations + }); + + var files = new[] { result.ContextFile }.Concat(result.AdditionalFiles).ToList(); + if (generatorType == typeof(CSharpModelGenerator)) + { + // TextTemplatingModelGenerator only scaffolds the DbContext when there are no custom entity type templates + Assert.True(files.Count > 2); + Assert.Contains(files, f => f.Code.Contains("/// Entity comment")); + Assert.Contains(files, f => f.Code.Contains("/// Property comment")); + } + + Assert.Contains(files, f => Regex.IsMatch(f.Code, @"\r?\n\s+\.\w+\(")); + Assert.Contains(files, f => f.Code.Contains("UsingEntity")); + Assert.Contains(files, f => f.Code.Contains("HasSequence")); + Assert.Contains(files, f => f.Code.Contains("#warning")); + + foreach (var file in files) + { + Assert.False(file.Code.Contains('\r'), $"{file.Path} contains a carriage return."); + } + } + + private static IModelCodeGenerator CreateGenerator(Type? generatorType = null) + { + generatorType ??= typeof(CSharpModelGenerator); var testAssembly = typeof(CSharpModelGeneratorTest).Assembly; var reporter = new TestOperationReporter(); return new DesignTimeServicesBuilder(testAssembly, testAssembly, reporter, []) @@ -54,6 +110,6 @@ private static IModelCodeGenerator CreateGenerator() .AddSingleton() .BuildServiceProvider(validateScopes: true) .GetServices() - .Last(g => g is CSharpModelGenerator); + .Last(g => g.GetType() == generatorType); } } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs index 401f7caab80..4332b995d4d 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/ModelCodeGeneratorTestBase.cs @@ -154,18 +154,18 @@ protected virtual IServiceCollection AddScaffoldingServices(IServiceCollection s protected static void AssertFileContents( string expectedCode, ScaffoldedFile file) - => Assert.Equal(expectedCode, file.Code.TrimEnd(), ignoreLineEndingDifferences: true); + // Scaffolded code always uses LF; the expected code in the tests may have been checked out with CRLF. + => Assert.Equal(expectedCode.Replace("\r\n", "\n"), file.Code.TrimEnd()); protected static void AssertContains( string expected, string actual) { - // Normalize line endings to Environment.Newline + // Scaffolded code always uses LF expected = expected .Replace("\r\n", "\n") .Replace("\n\r", "\n") - .Replace("\r", "\n") - .Replace("\n", Environment.NewLine); + .Replace("\r", "\n"); Assert.Contains(expected, actual); }