diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs index 00d18b1c017..63e4aaa26b5 100644 --- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs +++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Text; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Internal; @@ -195,6 +196,9 @@ public virtual string Lambda(IReadOnlyList properties) /// directly from your code. This API may change or be removed in future releases. /// public virtual string Reference(Type type) + => Reference(type, useFullName: false); + + private string Reference(Type type, bool useFullName) { Check.NotNull(type, nameof(type)); @@ -236,7 +240,10 @@ public virtual string Reference(Type type) .Append("."); } - builder.Append(type.ShortDisplayName()); + builder.Append( + useFullName + ? type.DisplayName() + : type.ShortDisplayName()); return builder.ToString(); } @@ -691,15 +698,99 @@ public virtual string UnknownLiteral(object value) return Array(array); } - var literal = _relationalTypeMappingSource.FindMapping(literalType)?.FindCodeLiteral(value, "C#"); - if (literal != null) + var mapping = _relationalTypeMappingSource.FindMapping(literalType); + if (mapping != null) { - return literal; + var builder = new StringBuilder(); + var expression = mapping.GenerateLiteralExpression(value); + var handled = HandleExpression(expression, builder); + + if (!handled) + { + throw new NotSupportedException( + DesignStrings.LiteralExpressionNotSupported( + expression.ToString(), + literalType.ShortDisplayName())); + } + + return builder.ToString(); } throw new InvalidOperationException(DesignStrings.UnknownLiteral(literalType)); } + private bool HandleExpression(Expression expression, StringBuilder builder) + { + // Only handle trivially simple cases for `new` and factory methods + switch (expression.NodeType) + { + case ExpressionType.Convert: + builder + .Append('(') + .Append(Reference(expression.Type)) + .Append(')'); + + return HandleExpression(((UnaryExpression)expression).Operand, builder); + case ExpressionType.New: + builder + .Append("new ") + .Append(Reference(expression.Type, useFullName: true)); + + return HandleArguments(((NewExpression)expression).Arguments, builder); + case ExpressionType.Call: + { + var callExpression = (MethodCallExpression)expression; + if (callExpression.Method.IsStatic) + { + builder + .Append(Reference(expression.Type, useFullName: true)); + } + else + { + if (!HandleExpression(callExpression.Object, builder)) + { + return false; + } + } + + builder + .Append('.') + .Append(callExpression.Method.Name); + + return HandleArguments(callExpression.Arguments, builder); + } + case ExpressionType.Constant: + builder + .Append(UnknownLiteral(((ConstantExpression)expression).Value)); + + return true; + } + + return false; + } + + private bool HandleArguments(IEnumerable argumentExpressions, StringBuilder builder) + { + builder.Append('('); + + var separator = string.Empty; + foreach (var expression in argumentExpressions) + { + builder.Append(separator); + + if (!HandleExpression(expression, builder)) + { + return false; + } + + separator = ", "; + } + + builder.Append(')'); + + return true; + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index b84c21e72cd..4580836cbc2 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -168,6 +168,14 @@ public static string UnknownLiteral([CanBeNull] object literalType) GetString("UnknownLiteral", nameof(literalType)), literalType); + /// + /// The literal expression '{expression}' for '{type}' cannot be parsed. Only simple constructor calls and factory methods are supported. + /// + public static string LiteralExpressionNotSupported([CanBeNull] object expression, [CanBeNull] object type) + => string.Format( + GetString("LiteralExpressionNotSupported", nameof(expression), nameof(type)), + expression, type); + /// /// Unable to find provider assembly with name {assemblyName}. Ensure the specified name is correct and is referenced by the project. /// diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx index e14c5411f60..a1babe1383f 100644 --- a/src/EFCore.Design/Properties/DesignStrings.resx +++ b/src/EFCore.Design/Properties/DesignStrings.resx @@ -1,17 +1,17 @@  - @@ -177,6 +177,9 @@ The current CSharpHelper cannot scaffold literals of type '{literalType}'. Configure your services to use one that can. + + The literal expression '{expression}' for '{type}' cannot be parsed. Only simple constructor calls and factory methods are supported. + Unable to find provider assembly with name {assemblyName}. Ensure the specified name is correct and is referenced by the project. diff --git a/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs b/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs index 45df71b9d88..74863bb45b3 100644 --- a/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs +++ b/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs @@ -8,7 +8,6 @@ using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Remotion.Linq.Parsing.ExpressionVisitors; @@ -106,21 +105,24 @@ public override Expression AddCustomConversion(Expression expression) } /// - /// Attempts generation of a code (e.g. C#) literal for the given value. + /// Creates a an expression tree that can be used to generate code for the literal value. + /// Currently, only very basic expressions such as constructor calls and factory methods taking + /// simple constants are supported. /// /// The value for which a literal is needed. - /// The language, for example "C#". - /// The generated literal, or null if a literal could not be generated. - public override string FindCodeLiteral(object value, string language) - { - var geometryText = AsText(value); + /// An expression tree that can be used to generate code for the literal value. + public override Expression GenerateLiteralExpression(object value) + => Expression.Convert( + Expression.Call( + Expression.New(WKTReaderType), + WKTReaderType.GetMethod("Read", new[] { typeof(string) }), + Expression.Constant(AsText(value), typeof(string))), + value.GetType()); - // TODO: Allow additional namespaces needed to be put in using directives - return geometryText != null - && language.Equals("C#", StringComparison.OrdinalIgnoreCase) - ? $"({value.GetType().ShortDisplayName()})new NetTopologySuite.IO.WKTReader().Read(\"{geometryText}\")" - : null; - } + /// + /// The type of the NTS 'WKTReader'. + /// + protected abstract Type WKTReaderType { get; } /// /// Returns the Well-Known-Text (WKT) representation of the given object, or null @@ -128,6 +130,6 @@ public override string FindCodeLiteral(object value, string language) /// /// The value. /// The WKT. - protected abstract string AsText(object value); + protected abstract string AsText([NotNull] object value); } } diff --git a/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs b/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs index 7f1920421e7..0c48eb29d3e 100644 --- a/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs +++ b/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs @@ -90,10 +90,6 @@ public override MethodInfo GetDataReaderMethod() protected override string AsText(object value) { var geometry = (IGeometry)value; - if (geometry == null) - { - return null; - } var srid = geometry.SRID; @@ -106,6 +102,13 @@ protected override string AsText(object value) return text; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Type WKTReaderType => typeof(WKTReader); + private static SqlServerSpatialReader CreateReader(IGeometryServices services, bool isGeography) => new SqlServerSpatialReader(services) { IsGeography = isGeography }; diff --git a/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs b/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs index 978834d3974..4daf68e66e6 100644 --- a/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs +++ b/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs @@ -1,8 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Data.Common; -using System.Globalization; using System.Reflection; using GeoAPI; using GeoAPI.Geometries; @@ -81,10 +81,6 @@ public override MethodInfo GetDataReaderMethod() protected override string AsText(object value) { var geometry = (IGeometry)value; - if (geometry == null) - { - return null; - } var srid = geometry.SRID; @@ -97,6 +93,12 @@ protected override string AsText(object value) return text; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + protected override Type WKTReaderType => typeof(WKTReader); + private static GaiaGeoReader CreateReader(IGeometryServices geometryServices) => new GaiaGeoReader( geometryServices.DefaultCoordinateSequenceFactory, diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 7a0979e33eb..fe721dbdf75 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -48,6 +48,14 @@ public static string InvalidEnumValue([CanBeNull] object argumentName, [CanBeNul public static string StillUsingTypeMapper => GetString("StillUsingTypeMapper"); + /// + /// The type mapping for '{type}' has not implemented code literal generation. + /// + public static string LiteralGenerationNotSupported([CanBeNull] object type) + => string.Format( + GetString("LiteralGenerationNotSupported", nameof(type)), + type); + /// /// The properties expression '{expression}' is not valid. The expression should represent a simple property access: 't => t.MyProperty'. When specifying multiple properties use an anonymous type: 't => new {{ t.MyProperty1, t.MyProperty2 }}'. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 58e6f04a1fe..ebb47d29cdd 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -130,6 +130,9 @@ The application or database provider is using an Obsolete TypeMapper API even after the provider has implemented a TypeMappingSource. The code must be updated to use the non-obsolete replacement APIs, as indicated by the Obsolete compiler warnings. + + The type mapping for '{type}' has not implemented code literal generation. + The properties expression '{expression}' is not valid. The expression should represent a simple property access: 't => t.MyProperty'. When specifying multiple properties use an anonymous type: 't => new {{ t.MyProperty1, t.MyProperty2 }}'. diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs index f468726c0d5..b3d2b824cdd 100644 --- a/src/EFCore/Storage/CoreTypeMapping.cs +++ b/src/EFCore/Storage/CoreTypeMapping.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Internal; @@ -220,12 +221,13 @@ private static ValueComparer CreateComparer(Type clrType, bool favorStructuralCo public abstract CoreTypeMapping Clone([CanBeNull] ValueConverter converter); /// - /// Attempts generation of a code (e.g. C#) literal for the given value. + /// Creates a an expression tree that can be used to generate code for the literal value. + /// Currently, only very basic expressions such as constructor calls and factory methods taking + /// simple constants are supported. /// /// The value for which a literal is needed. - /// The language, for example "C#". - /// The generated literal, or null if a literal could not be generated. - public virtual string FindCodeLiteral([CanBeNull] object value, [NotNull] string language) - => null; + /// An expression tree that can be used to generate code for the literal value. + public virtual Expression GenerateLiteralExpression([NotNull] object value) + => throw new NotSupportedException(CoreStrings.LiteralGenerationNotSupported(ClrType.ShortDisplayName())); } } diff --git a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs index 6d04c236590..e8eb5eb06f2 100644 --- a/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs +++ b/test/EFCore.Design.Tests/Design/Internal/CSharpHelperTest.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq.Expressions; +using System.Reflection; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; using Microsoft.EntityFrameworkCore.Storage; @@ -333,10 +335,297 @@ public void Fragment_MethodCallCodeFragment_works_when_nested_closure() Assert.Equal(".Test(x => x.Test())", result); } - private IRelationalTypeMappingSource TypeMappingSource { get; } - = new SqlServerTypeMappingSource( + [Fact] + public void Really_unknown_literal_with_no_mapping_support() + { + var typeMapping = CreateTypeMappingSource(null); + + Assert.Equal( + CoreStrings.LiteralGenerationNotSupported(nameof(SimpleTestType)), + Assert.Throws( + () => new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())).Message); + } + + [Fact] + public void Literal_with_parameterless_constructor() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.New(typeof(SimpleTestType))); + + Assert.Equal( + "new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType()", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())); + } + + [Fact] + public void Literal_with_one_parameter_constructor() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.New( + typeof(SimpleTestType).GetConstructor(new[] { typeof(string) }), + Expression.Constant(v.Arg1, typeof(string)))); + + Assert.Equal( + "new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType(\"Jerry\")", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry"))); + } + + [Fact] + public void Literal_with_two_parameter_constructor() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.New( + typeof(SimpleTestType).GetConstructor(new[] { typeof(string), typeof(int?) }), + Expression.Constant(v.Arg1, typeof(string)), + Expression.Constant(v.Arg2, typeof(int?)))); + + Assert.Equal( + "new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType(\"Jerry\", 77)", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry", 77))); + } + + [Fact] + public void Literal_with_parameterless_static_factory() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Call( + typeof(SimpleTestType).GetMethod( + nameof(SimpleTestType.Create), + new Type[0]))); + + Assert.Equal( + "Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType.Create()", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())); + } + + [Fact] + public void Literal_with_one_parameter_static_factory() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Call( + typeof(SimpleTestType).GetMethod( + nameof(SimpleTestType.Create), + new[] { typeof(string) }), + Expression.Constant(v.Arg1, typeof(string)))); + + Assert.Equal( + "Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType.Create(\"Jerry\")", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry"))); + } + + [Fact] + public void Literal_with_two_parameter_static_factory() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Call( + typeof(SimpleTestType).GetMethod( + nameof(SimpleTestType.Create), + new[] { typeof(string), typeof(int?) }), + Expression.Constant(v.Arg1, typeof(string)), + Expression.Constant(v.Arg2, typeof(int?)))); + + Assert.Equal( + "Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestType.Create(\"Jerry\", 77)", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry", 77))); + } + + [Fact] + public void Literal_with_parameterless_instance_factory() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Call( + Expression.New(typeof(SimpleTestTypeFactory)), + typeof(SimpleTestTypeFactory).GetMethod( + nameof(SimpleTestType.Create), + new Type[0]))); + + Assert.Equal( + "new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestTypeFactory().Create()", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())); + } + + [Fact] + public void Literal_with_one_parameter_instance_factory() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Convert( + Expression.Call( + Expression.New(typeof(SimpleTestTypeFactory)), + typeof(SimpleTestTypeFactory).GetMethod( + nameof(SimpleTestType.Create), + new[] { typeof(string) }), + Expression.Constant(v.Arg1, typeof(string))), + typeof(SimpleTestType))); + + Assert.Equal( + "(SimpleTestType)new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestTypeFactory().Create(\"Jerry\")", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry", 77))); + } + + [Fact] + public void Literal_with_two_parameter_instance_factory() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Convert( + Expression.Call( + Expression.New( + typeof(SimpleTestTypeFactory).GetConstructor(new[] { typeof(string) }), + Expression.Constant("4096", typeof(string))), + typeof(SimpleTestTypeFactory).GetMethod( + nameof(SimpleTestType.Create), + new[] { typeof(string), typeof(int?) }), + Expression.Constant(v.Arg1, typeof(string)), + Expression.Constant(v.Arg2, typeof(int?))), + typeof(SimpleTestType))); + + Assert.Equal( + "(SimpleTestType)new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestTypeFactory(\"4096\").Create(\"Jerry\", 77)", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry", 77))); + } + + [Fact] + public void Literal_with_two_parameter_instance_factory_and_internal_cast() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Convert( + Expression.Call( + Expression.New( + typeof(SimpleTestTypeFactory).GetConstructor(new[] { typeof(string) }), + Expression.Constant("4096", typeof(string))), + typeof(SimpleTestTypeFactory).GetMethod( + nameof(SimpleTestType.Create), + new[] { typeof(string), typeof(int?) }), + Expression.Constant(v.Arg1, typeof(string)), + Expression.Convert( + Expression.Constant(v.Arg2, typeof(int)), + typeof(int?))), + typeof(SimpleTestType))); + + Assert.Equal( + "(SimpleTestType)new Microsoft.EntityFrameworkCore.Design.Internal.SimpleTestTypeFactory(\"4096\").Create(\"Jerry\", (int?)77)", + new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType("Jerry", 77))); + } + + [Fact] + public void Literal_with_unsupported_node_throws() + { + var typeMapping = CreateTypeMappingSource( + v => Expression.Add( + Expression.Constant(10), + Expression.Constant(10))); + + + Assert.Equal( + DesignStrings.LiteralExpressionNotSupported( + "(10 + 10)", + nameof(SimpleTestType)), + Assert.Throws( + () => new CSharpHelper(typeMapping).UnknownLiteral(new SimpleTestType())).Message); + } + + private IRelationalTypeMappingSource TypeMappingSource { get; } = CreateTypeMappingSource(); + + private static SqlServerTypeMappingSource CreateTypeMappingSource( + Func literalExpressionFunc) + => CreateTypeMappingSource(new TestTypeMappingPlugin(literalExpressionFunc)); + + private static SqlServerTypeMappingSource CreateTypeMappingSource( + params IRelationalTypeMappingSourcePlugin[] plugins) + => new SqlServerTypeMappingSource( TestServiceFactory.Instance.Create(), - TestServiceFactory.Instance.Create()); + new RelationalTypeMappingSourceDependencies( + plugins)); + + private class TestTypeMappingPlugin : IRelationalTypeMappingSourcePlugin + { + private readonly Func _literalExpressionFunc; + + public TestTypeMappingPlugin(Func literalExpressionFunc) + { + _literalExpressionFunc = literalExpressionFunc; + } + + public RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) + => _literalExpressionFunc == null + ? (RelationalTypeMapping)new SimpleTestNonImplementedTypeMapping() + : new SimpleTestTypeMapping(_literalExpressionFunc); + } + + private class SimpleTestTypeMapping : RelationalTypeMapping + { + private readonly Func _literalExpressionFunc; + + public SimpleTestTypeMapping( + Func literalExpressionFunc) + : base("storeType", typeof(SimpleTestType)) + { + _literalExpressionFunc = literalExpressionFunc; + } + + public override Expression GenerateLiteralExpression(object value) + => _literalExpressionFunc((T)value); + } + + private class SimpleTestNonImplementedTypeMapping : RelationalTypeMapping + { + public SimpleTestNonImplementedTypeMapping() + : base("storeType", typeof(SimpleTestType)) + { + } + } + } + + internal class SimpleTestType + { + public SimpleTestType() + { + } + + public SimpleTestType(string arg1) + : this(arg1, null) + { + } + + public SimpleTestType(string arg1, int? arg2) + { + Arg1 = arg1; + Arg2 = arg2; + } + + public string Arg1 { get; } + public int? Arg2 { get; } + + public static SimpleTestType Create() + => new SimpleTestType(); + + public static SimpleTestType Create(string arg1) + => new SimpleTestType(arg1); + + public static SimpleTestType Create(string arg1, int? arg2) + => new SimpleTestType(arg1, arg2); + } + + internal class SimpleTestTypeFactory + { + public SimpleTestTypeFactory() + { + } + + public SimpleTestTypeFactory(string factoryArg) + { + FactoryArg = factoryArg; + } + + public string FactoryArg { get; } + + public SimpleTestType Create() + => new SimpleTestType(); + + public object Create(string arg1) + => new SimpleTestType(arg1); + + public object Create(string arg1, int? arg2) + => new SimpleTestType(arg1, arg2); } internal class Generic