diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs index 3a70b1cf5b0..47035873762 100644 --- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs +++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Globalization; using System.Numerics; +using System.Runtime.CompilerServices; using System.Security; using System.Text; using Microsoft.EntityFrameworkCore.Internal; @@ -693,6 +694,54 @@ private string Array(Type type, IEnumerable values, bool vertical = false) return builder.ToString(); } + private string ValueTuple(ITuple tuple) + { + var builder = new StringBuilder(); + + Type[]? typeArguments = null; + var i = 0; + + if (tuple.Length == 1) + { + builder.Append("ValueTuple.Create("); + AppendItem(tuple[i]); + builder.Append(')'); + + return builder.ToString(); + } + + builder.Append('('); + + for (; i < tuple.Length; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + AppendItem(tuple[i]); + } + + builder.Append(')'); + + return builder.ToString(); + + void AppendItem(object? item) + { + if (item is null) + { + typeArguments ??= tuple.GetType().GenericTypeArguments; + + builder + .Append('(') + .Append(Reference(typeArguments[i])) + .Append(')'); + } + + builder.Append(UnknownLiteral(item)); + } + } + /// /// 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 @@ -970,6 +1019,12 @@ public virtual string UnknownLiteral(object? value) return Array(literalType.GetElementType()!, array); } + if (value is ITuple tuple + && value.GetType().FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true) + { + return ValueTuple(tuple); + } + var valueType = value.GetType(); if (valueType.IsGenericType && !valueType.IsGenericTypeDefinition) { diff --git a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs index e2914637c71..3d82ee354f5 100644 --- a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs +++ b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs @@ -8,6 +8,8 @@ namespace Microsoft.EntityFrameworkCore.Design.Internal; +#nullable enable + public class CSharpHelperTest { private static readonly string EOL = Environment.NewLine; @@ -183,6 +185,26 @@ public void Literal_works_when_multiline_string() "multi-line\r\nstring\nwith\r\"", "\"multi-line\\r\\nstring\\nwith\\r\\\"\""); + [ConditionalFact] + public void Literal_works_when_value_tuple() + => Literal_works((1, "hello"), "(1, \"hello\")"); + + [ConditionalFact] + public void Literal_works_when_value_tuple_with_null_value_type() + => Literal_works((1, (int?)null, "hello"), "(1, (int?)null, \"hello\")"); + + [ConditionalFact] + public void Literal_works_when_value_tuple_with_null_reference_type() + => Literal_works((1, (string?)null, "hello"), "(1, (string)null, \"hello\")"); + + [ConditionalFact] + public void Literal_works_when_value_tuple_of_length_1() + => Literal_works(ValueTuple.Create(1), "ValueTuple.Create(1)"); + + [ConditionalFact] + public void Literal_works_when_value_tuple_of_length_9() + => Literal_works((1, 2, 3, 4, 5, 6, 7, 8, 9), "(1, 2, 3, 4, 5, 6, 7, 8, 9)"); + [ConditionalFact] [UseCulture("de-DE")] public void Literal_works_when_DateTime() @@ -331,7 +353,7 @@ public void Identifier_works(string input, string expected) [InlineData(new[] { "#", "$" }, "_._")] [InlineData(new[] { "" }, "_")] [InlineData(new string[] { }, "_")] - [InlineData(new string[] { null }, "_")] + [InlineData(new string?[] { null }, "_")] public void Namespace_works(string[] input, string excepted) => Assert.Equal(excepted, new CSharpHelper(TypeMappingSource).Namespace(input)); @@ -587,7 +609,7 @@ public void Literal_with_one_parameter_constructor() { var typeMapping = CreateTypeMappingSource( v => Expression.New( - typeof(SimpleTestType).GetConstructor(new[] { typeof(string) }), + typeof(SimpleTestType).GetConstructor(new[] { typeof(string) })!, Expression.Constant(v.Arg1, typeof(string)))); Assert.Equal( @@ -600,7 +622,7 @@ public void Literal_with_two_parameter_constructor() { var typeMapping = CreateTypeMappingSource( v => Expression.New( - typeof(SimpleTestType).GetConstructor(new[] { typeof(string), typeof(int?) }), + typeof(SimpleTestType).GetConstructor(new[] { typeof(string), typeof(int?) })!, Expression.Constant(v.Arg1, typeof(string)), Expression.Constant(v.Arg2, typeof(int?)))); @@ -616,7 +638,7 @@ public void Literal_with_parameterless_static_factory() v => Expression.Call( typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.StaticCreate), - new Type[0]))); + Type.EmptyTypes)!)); Assert.Equal( "Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestTypeFactory.StaticCreate()", @@ -630,7 +652,7 @@ public void Literal_with_one_parameter_static_factory() v => Expression.Call( typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.StaticCreate), - new[] { typeof(string) }), + new[] { typeof(string) })!, Expression.Constant(v.Arg1, typeof(string)))); Assert.Equal( @@ -645,7 +667,7 @@ public void Literal_with_two_parameter_static_factory() v => Expression.Call( typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.StaticCreate), - new[] { typeof(string), typeof(int?) }), + new[] { typeof(string), typeof(int?) })!, Expression.Constant(v.Arg1, typeof(string)), Expression.Constant(v.Arg2, typeof(int?)))); @@ -662,7 +684,7 @@ public void Literal_with_parameterless_instance_factory() Expression.New(typeof(SimpleTestTypeFactory)), typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.Create), - new Type[0]))); + new Type[0])!)); Assert.Equal( "new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestTypeFactory().Create()", @@ -678,7 +700,7 @@ public void Literal_with_one_parameter_instance_factory() Expression.New(typeof(SimpleTestTypeFactory)), typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.Create), - new[] { typeof(string) }), + new[] { typeof(string) })!, Expression.Constant(v.Arg1, typeof(string))), typeof(SimpleTestType))); @@ -694,11 +716,11 @@ public void Literal_with_two_parameter_instance_factory() v => Expression.Convert( Expression.Call( Expression.New( - typeof(SimpleTestTypeFactory).GetConstructor(new[] { typeof(string) }), + typeof(SimpleTestTypeFactory).GetConstructor(new[] { typeof(string) })!, Expression.Constant("4096", typeof(string))), typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.Create), - new[] { typeof(string), typeof(int?) }), + new[] { typeof(string), typeof(int?) })!, Expression.Constant(v.Arg1, typeof(string)), Expression.Constant(v.Arg2, typeof(int?))), typeof(SimpleTestType))); @@ -715,11 +737,11 @@ public void Literal_with_two_parameter_instance_factory_and_internal_cast() v => Expression.Convert( Expression.Call( Expression.New( - typeof(SimpleTestTypeFactory).GetConstructor(new[] { typeof(string) }), + typeof(SimpleTestTypeFactory).GetConstructor(new[] { typeof(string) })!, Expression.Constant("4096", typeof(string))), typeof(SimpleTestTypeFactory).GetMethod( nameof(SimpleTestTypeFactory.Create), - new[] { typeof(string), typeof(int?) }), + new[] { typeof(string), typeof(int?) })!, Expression.Constant(v.Arg1, typeof(string)), Expression.Convert( Expression.Constant(v.Arg2, typeof(int)), @@ -735,7 +757,7 @@ public void Literal_with_two_parameter_instance_factory_and_internal_cast() public void Literal_with_static_field() { var typeMapping = CreateTypeMappingSource( - v => Expression.Field(null, typeof(SimpleTestType).GetField(nameof(SimpleTestType.SomeStaticField)))); + v => Expression.Field(null, typeof(SimpleTestType).GetField(nameof(SimpleTestType.SomeStaticField))!)); Assert.Equal( "Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType.SomeStaticField", @@ -746,7 +768,7 @@ public void Literal_with_static_field() public void Literal_with_static_property() { var typeMapping = CreateTypeMappingSource( - v => Expression.Property(null, typeof(SimpleTestType).GetProperty(nameof(SimpleTestType.SomeStaticProperty)))); + v => Expression.Property(null, typeof(SimpleTestType).GetProperty(nameof(SimpleTestType.SomeStaticProperty))!)); Assert.Equal( "Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType.SomeStaticProperty", @@ -759,7 +781,7 @@ public void Literal_with_instance_property() var typeMapping = CreateTypeMappingSource( v => Expression.Property( Expression.New(typeof(SimpleTestType)), - typeof(SimpleTestType).GetProperty(nameof(SimpleTestType.SomeInstanceProperty)))); + typeof(SimpleTestType).GetProperty(nameof(SimpleTestType.SomeInstanceProperty))!)); Assert.Equal( "new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType().SomeInstanceProperty", @@ -797,8 +819,7 @@ public void Literal_with_unsupported_node_throws() private IRelationalTypeMappingSource TypeMappingSource { get; } = CreateTypeMappingSource(); - private static SqlServerTypeMappingSource CreateTypeMappingSource( - Func literalExpressionFunc) + private static SqlServerTypeMappingSource CreateTypeMappingSource(Func? literalExpressionFunc) => CreateTypeMappingSource(new TestTypeMappingPlugin(literalExpressionFunc)); private static SqlServerTypeMappingSource CreateTypeMappingSource( @@ -810,9 +831,9 @@ private static SqlServerTypeMappingSource CreateTypeMappingSource( private class TestTypeMappingPlugin : IRelationalTypeMappingSourcePlugin { - private readonly Func _literalExpressionFunc; + private readonly Func? _literalExpressionFunc; - public TestTypeMappingPlugin(Func literalExpressionFunc) + public TestTypeMappingPlugin(Func? literalExpressionFunc) { _literalExpressionFunc = literalExpressionFunc; } @@ -855,7 +876,7 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p private static readonly MethodInfo _testFuncMethodInfo = typeof(CSharpHelperTest).GetRuntimeMethod( nameof(TestFunc), - new[] { typeof(object), typeof(object), typeof(object), typeof(object) }); + new[] { typeof(object), typeof(object), typeof(object), typeof(object) })!; public static void TestFunc(object builder, object o1, object o2, object o3) => throw new NotSupportedException(); @@ -883,7 +904,7 @@ public SimpleTestType(string arg1, int? arg2) Arg2 = arg2; } - public string Arg1 { get; } + public string Arg1 { get; } = null!; public int? Arg2 { get; } } @@ -898,7 +919,7 @@ public SimpleTestTypeFactory(string factoryArg) FactoryArg = factoryArg; } - public string FactoryArg { get; } + public string FactoryArg { get; } = null!; public SimpleTestType Create() => new();