From 040ef66d2e1a9179f4f2a903c2b48fe6d44b3af1 Mon Sep 17 00:00:00 2001 From: Matt Morrissette Date: Sun, 12 Jun 2022 17:09:44 -0700 Subject: [PATCH] Add support for CSharpHelper for List literals Fixes #19274 Also relates to https://github.com/npgsql/efcore.pg/pull/2402 --- .../Design/Internal/CSharpHelper.cs | 99 +++++++++++++++++++ .../Design/Internal/CSharpHelperTest.cs | 45 +++++++++ 2 files changed, 144 insertions(+) diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs index 9ee6e914bc6..4a6947e01f2 100644 --- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs +++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs @@ -737,6 +737,71 @@ public virtual string Literal(object?[,] values) return builder.ToString(); } + /// + /// 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 virtual string Literal(IList values, bool vertical = false) + => List(typeof(T), values, vertical); + + private string List(Type type, IEnumerable values, bool vertical = false) + { + var builder = new IndentedStringBuilder(); + + builder.Append("new List<") + .Append(Reference(type)) + .Append("> {"); + + var first = true; + foreach (var value in values) + { + if (first) + { + if (vertical) + { + builder.AppendLine(); + builder.IncrementIndent(); + } + else + { + builder.Append(" "); + } + first = false; + } + else + { + builder.Append(","); + + if (vertical) + { + builder.AppendLine(); + } + else + { + builder.Append(" "); + } + } + + builder.Append(UnknownLiteral(value)); + } + + if (vertical) + { + builder.AppendLine(); + builder.DecrementIndent(); + } + else + { + builder.Append(" "); + } + + builder.Append("}"); + + return builder.ToString(); + } + /// /// 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 @@ -844,6 +909,11 @@ public virtual string UnknownLiteral(object? value) return Array(literalType.GetElementType()!, array); } + if (value is IList list && value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition() == typeof(List<>)) + { + return List(value.GetType().GetGenericArguments()[0], list); + } + var mapping = _typeMappingSource.FindMapping(literalType); if (mapping != null) { @@ -878,6 +948,35 @@ private bool HandleExpression(Expression expression, StringBuilder builder, bool HandleList(((NewArrayExpression)expression).Expressions, builder, simple: true); + builder + .Append(" }"); + + return true; + case ExpressionType.ListInit: + var listExpr = (ListInitExpression)expression; + if (listExpr.Initializers.Any(i => i.Arguments.Count != 1)) + { + // If there is an initializer with more than one argument we can't make a literal cleanly + return false; + } + + builder + .Append("new ") + .Append(Reference(expression.Type)); + + if (listExpr.NewExpression.Arguments.Count > 0 && !HandleArguments(listExpr.NewExpression.Arguments, builder)) + { + return false; + } + + builder + .Append(" { "); + + if (!HandleList(listExpr.Initializers.Select(_ => _.Arguments.First()), builder, simple: true)) + { + return false; + } + builder .Append(" }"); diff --git a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs index d03c74c1aa9..db50e063185 100644 --- a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs +++ b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs @@ -121,6 +121,24 @@ public void Literal_works_when_many_ByteArray() new byte[] { 1, 2 }, "new byte[] { 1, 2 }"); + [ConditionalFact] + public void Literal_works_when_empty_list() + => Literal_works( + new List(), + @"new List { }"); + + [ConditionalFact] + public void Literal_works_when_list_without_ctor_arguments() + => Literal_works( + new List { "one", "two" }, + @"new List { ""one"", ""two"" }"); + + [ConditionalFact] + public void Literal_works_when_list_with_ctor_arguments() + => Literal_works( + new List(new [] { "one" }) { "two", "three" }, + @"new List { ""one"", ""two"", ""three"" }"); + [ConditionalFact] public void Literal_works_when_multiline_string() => Literal_works( @@ -607,6 +625,33 @@ public void Literal_with_add() new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())); } + [ConditionalFact] + public void Literal_with_list_of_string_init_no_arguments() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.ListInit( + Expression.New(typeof(List<>).MakeGenericType(typeof(string))), + Expression.Constant("one"), Expression.Constant("two"))); + + Assert.Equal( + @"new List { ""one"", ""two"" }", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())); + } + + [ConditionalFact] + public void Literal_with_list_of_string_init_with_arguments() + { + var constructor = typeof(List<>).MakeGenericType((typeof(string))).GetConstructor(new[] { typeof(IEnumerable) })!; + var typeMapping = CreateTypeMappingSource( + v => Expression.ListInit( + Expression.New(constructor, Expression.NewArrayInit(typeof(string), Expression.Constant("one"))), + Expression.Constant("one"), Expression.Constant("two"))); + + Assert.Equal( + @"new List(new string[] { ""one"" }) { ""one"", ""two"" }", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())); + } + [ConditionalFact] public void Literal_with_unsupported_node_throws() {