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);
}