diff --git a/src/EFCore.InMemory/Query/Pipeline/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Pipeline/InMemoryQueryExpression.cs index e8ca711a80b..5bb9de441eb 100644 --- a/src/EFCore.InMemory/Query/Pipeline/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Pipeline/InMemoryQueryExpression.cs @@ -142,7 +142,16 @@ public Expression GetProjectionExpression(ProjectionMember member) var readValueExpression = (MethodCallExpression)projection; var index = (int)((ConstantExpression)readValueExpression.Arguments[1]).Value; - return _valueBufferSlots[index]; + var valueBufferSlotExpression = _valueBufferSlots[index]; + + var memberType = member.MemberType; + if (memberType != null + && memberType != valueBufferSlotExpression.Type) + { + valueBufferSlotExpression = Convert(valueBufferSlotExpression, memberType); + } + + return valueBufferSlotExpression; } public LambdaExpression GetScalarProjectionLambda() diff --git a/src/EFCore.InMemory/Query/Pipeline/Translator.cs b/src/EFCore.InMemory/Query/Pipeline/Translator.cs index 7427fee4607..136683f19d0 100644 --- a/src/EFCore.InMemory/Query/Pipeline/Translator.cs +++ b/src/EFCore.InMemory/Query/Pipeline/Translator.cs @@ -54,7 +54,15 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var entityType = entityShaper.EntityType; var property = entityType.FindProperty((string)((ConstantExpression)methodCallExpression.Arguments[1]).Value); - return _inMemoryQueryExpression.BindProperty(entityShaper.ValueBufferExpression, property); + var boundPropertyExpression = _inMemoryQueryExpression.BindProperty(entityShaper.ValueBufferExpression, property); + + if (boundPropertyExpression.Type + != methodCallExpression.Type) + { + boundPropertyExpression = Expression.Convert(boundPropertyExpression, methodCallExpression.Type); + } + + return boundPropertyExpression; } } diff --git a/src/EFCore.Relational/Query/Pipeline/ExpressionExtensions.cs b/src/EFCore.Relational/Query/Pipeline/ExpressionExtensions.cs index 74d55c652b2..644eeba0b0d 100644 --- a/src/EFCore.Relational/Query/Pipeline/ExpressionExtensions.cs +++ b/src/EFCore.Relational/Query/Pipeline/ExpressionExtensions.cs @@ -1,22 +1,57 @@ // 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.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline { public static class ExpressionExtensions { - public static RelationalTypeMapping InferTypeMapping(params Expression[] expressions) + public static RelationalTypeMapping InferTypeMapping( + IValueConverterSelector valueConverterSelector, + params Expression[] expressions) { for (var i = 0; i < expressions.Length; i++) { - if (expressions[i] is SqlExpression sql - && sql.TypeMapping != null) + if (expressions[i] is SqlExpression sqlExpression) { - return sql.TypeMapping; + var mapping = sqlExpression.TypeMapping; + if (mapping != null) + { + return mapping; + } + + // This is to cover the cases of enums and char values being erased when using + // literals in the expression tree. It adds a new converter onto the type mapper + // that takes the literal and converts it back to the type that the type mapping + // is expecting. + // This code is temporary until more complete type inference is completed. + if (sqlExpression is SqlUnaryExpression sqlUnaryExpression + && sqlUnaryExpression.OperatorType == ExpressionType.Convert) + { + var operandType = sqlUnaryExpression.Operand.Type.UnwrapNullableType(); + mapping = InferTypeMapping(valueConverterSelector, sqlUnaryExpression.Operand); + + if (mapping != null + && (operandType.IsEnum + || operandType == typeof(char) + || mapping.Converter?.ProviderClrType == typeof(byte[]))) + { + if (mapping.ClrType != sqlUnaryExpression.Type) + { + var converter = valueConverterSelector.Select(sqlUnaryExpression.Type, mapping.ClrType).ToList(); + + mapping = (RelationalTypeMapping)mapping.Clone(converter.First().Create()); + } + + return mapping; + } + } } } diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs index 5bd4f2a5378..b2c19c045f7 100644 --- a/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs @@ -7,17 +7,22 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline { public class SqlExpressionFactory : ISqlExpressionFactory { private readonly IRelationalTypeMappingSource _typeMappingSource; + private readonly IValueConverterSelector _valueConverterSelector; private readonly RelationalTypeMapping _boolTypeMapping; - public SqlExpressionFactory(IRelationalTypeMappingSource typeMappingSource) + public SqlExpressionFactory( + IRelationalTypeMappingSource typeMappingSource, + IValueConverterSelector valueConverterSelector) { _typeMappingSource = typeMappingSource; + _valueConverterSelector = valueConverterSelector; _boolTypeMapping = typeMappingSource.FindMapping(typeof(bool)); } @@ -75,8 +80,9 @@ public SqlExpression ApplyTypeMapping(SqlExpression sqlExpression, RelationalTyp private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression) { var inferredTypeMapping = ExpressionExtensions.InferTypeMapping( - likeExpression.Match, likeExpression.Pattern, likeExpression.EscapeChar) - ?? _typeMappingSource.FindMapping(likeExpression.Match.Type); + _valueConverterSelector, + likeExpression.Match, likeExpression.Pattern, likeExpression.EscapeChar) + ?? _typeMappingSource.FindMapping(likeExpression.Match.Type); return new LikeExpression( ApplyTypeMapping(likeExpression.Match, inferredTypeMapping), @@ -155,7 +161,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.LessThanOrEqual: case ExpressionType.NotEqual: { - inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right) + inferredTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, left, right) ?? _typeMappingSource.FindMapping(left.Type); resultType = typeof(bool); resultTypeMapping = _boolTypeMapping; @@ -180,7 +186,7 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( case ExpressionType.And: case ExpressionType.Or: { - inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right); + inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(_valueConverterSelector, left, right); resultType = left.Type; resultTypeMapping = inferredTypeMapping; } diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs index 3d8a852c681..498847baef3 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs @@ -214,10 +214,18 @@ public RelationalTypeMappingInfo( public bool? IsRowVersion => _coreTypeMappingInfo.IsRowVersion; /// - /// The CLR type in the model. + /// The CLR type used to store the property in the model. This may be the backing field type. + /// May be null if type information is conveyed via other means (e.g. the store name in a relational type mapping info) /// public Type ClrType => _coreTypeMappingInfo.ClrType; + /// + /// The CLR type of the property, which may be different from if a backing field of + /// a different type is used. + /// May be null if type information is conveyed via other means (e.g. the store name in a relational type mapping info) + /// + public Type DeclaredClrType => _coreTypeMappingInfo.DeclaredClrType; + /// /// Returns a new with the given converter applied. /// diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs index bc41ae5630f..3c23f3c0b71 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reflection; using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -126,54 +127,79 @@ private RelationalTypeMapping FindMappingWithConversion( k => { var (info, providerType, converter) = k; + + var sourceType = info.ClrType; + var sourceDeclaredType = info.DeclaredClrType; + + var needsConvertFromObject + = sourceType == typeof(object) + && sourceDeclaredType != typeof(object); + + var effectiveSourceType = needsConvertFromObject ? sourceDeclaredType : sourceType; + + ValueConverterInfo fromObjectConverter = default; + var fromObjectMappingInfo = info; + + if (needsConvertFromObject) + { + fromObjectConverter = Dependencies.ValueConverterSelector + .Select(typeof(object), sourceDeclaredType) + .Single(); + + fromObjectMappingInfo = info.WithConverter(fromObjectConverter); + } + var mapping = providerType == null - || providerType == info.ClrType - ? FindMapping(info) + || providerType == effectiveSourceType + ? FindMapping(fromObjectMappingInfo) : null; - if (mapping == null) + if (mapping == null + && effectiveSourceType != null) { - var sourceType = info.ClrType; - - if (sourceType != null) + foreach (var converterInfo in Dependencies + .ValueConverterSelector + .Select(effectiveSourceType, providerType)) { - foreach (var converterInfo in Dependencies - .ValueConverterSelector - .Select(sourceType, providerType)) - { - var mappingInfoUsed = info.WithConverter(converterInfo); - mapping = FindMapping(mappingInfoUsed); + var mappingInfoUsed = info.WithConverter(converterInfo); + mapping = FindMapping(mappingInfoUsed); - if (mapping == null - && providerType != null) + if (mapping == null + && providerType != null) + { + foreach (var secondConverterInfo in Dependencies + .ValueConverterSelector + .Select(providerType)) { - foreach (var secondConverterInfo in Dependencies - .ValueConverterSelector - .Select(providerType)) - { - mapping = FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); + mapping = FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); - if (mapping != null) - { - mapping = (RelationalTypeMapping)mapping.Clone(secondConverterInfo.Create()); - break; - } + if (mapping != null) + { + mapping = (RelationalTypeMapping)mapping.Clone(secondConverterInfo.Create()); + break; } } + } - if (mapping != null) - { - mapping = (RelationalTypeMapping)mapping.Clone(converterInfo.Create()); - break; - } + if (mapping != null) + { + mapping = (RelationalTypeMapping)mapping.Clone(converterInfo.Create()); + break; } } } - if (mapping != null - && converter != null) + if (mapping != null) { - mapping = (RelationalTypeMapping)mapping.Clone(converter); + if (needsConvertFromObject) + { + mapping = (RelationalTypeMapping)mapping.Clone(fromObjectConverter.Create()); + } + + if (converter != null) + { + mapping = (RelationalTypeMapping)mapping.Clone(converter); + } } return mapping; diff --git a/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerGeometryMethodTranslator.cs b/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerGeometryMethodTranslator.cs index 5b9c5ecad6a..5a98f31048d 100644 --- a/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerGeometryMethodTranslator.cs +++ b/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerGeometryMethodTranslator.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using NetTopologySuite.Geometries; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline @@ -49,13 +50,16 @@ public class SqlServerGeometryMethodTranslator : IMethodCallTranslator private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IValueConverterSelector _valueConverterSelector; public SqlServerGeometryMethodTranslator( IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { _typeMappingSource = typeMappingSource; _sqlExpressionFactory = sqlExpressionFactory; + _valueConverterSelector = valueConverterSelector; } public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList arguments) @@ -64,7 +68,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< { var geometryExpressions = new[] { instance }.Concat( arguments.Where(e => typeof(IGeometry).IsAssignableFrom(e.Type))); - var typeMapping = ExpressionExtensions.InferTypeMapping(geometryExpressions.ToArray()); + var typeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, geometryExpressions.ToArray()); Debug.Assert(typeMapping != null, "At least one argument must have typeMapping."); var storeType = typeMapping.StoreType; diff --git a/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerNetTopologySuiteMethodCallTranslatorPlugin.cs b/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerNetTopologySuiteMethodCallTranslatorPlugin.cs index 88ac0181a07..9a651426f23 100644 --- a/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerNetTopologySuiteMethodCallTranslatorPlugin.cs +++ b/src/EFCore.SqlServer.NTS/Query/Pipeline/SqlServerNetTopologySuiteMethodCallTranslatorPlugin.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline @@ -30,12 +31,14 @@ public class SqlServerNetTopologySuiteMethodCallTranslatorPlugin : IMethodCallTr /// 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 SqlServerNetTopologySuiteMethodCallTranslatorPlugin(IRelationalTypeMappingSource typeMappingSource, - ISqlExpressionFactory sqlExpressionFactory) + public SqlServerNetTopologySuiteMethodCallTranslatorPlugin( + IRelationalTypeMappingSource typeMappingSource, + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { Translators = new IMethodCallTranslator[] { - new SqlServerGeometryMethodTranslator(typeMappingSource, sqlExpressionFactory), + new SqlServerGeometryMethodTranslator(typeMappingSource, sqlExpressionFactory, valueConverterSelector), new SqlServerGeometryCollectionMethodTranslator(typeMappingSource, sqlExpressionFactory), new SqlServerLineStringMethodTranslator(typeMappingSource, sqlExpressionFactory), new SqlServerPolygonMethodTranslator(typeMappingSource, sqlExpressionFactory) diff --git a/src/EFCore.SqlServer/Query/Pipeline/SqlServerDateDiffFunctionsTranslator.cs b/src/EFCore.SqlServer/Query/Pipeline/SqlServerDateDiffFunctionsTranslator.cs index a4d93281142..664365c455b 100644 --- a/src/EFCore.SqlServer/Query/Pipeline/SqlServerDateDiffFunctionsTranslator.cs +++ b/src/EFCore.SqlServer/Query/Pipeline/SqlServerDateDiffFunctionsTranslator.cs @@ -6,6 +6,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline { @@ -232,11 +233,14 @@ private readonly Dictionary _methodInfoDateDiffMapping } }; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IValueConverterSelector _valueConverterSelector; public SqlServerDateDiffFunctionsTranslator( - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { _sqlExpressionFactory = sqlExpressionFactory; + _valueConverterSelector = valueConverterSelector; } public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList arguments) @@ -245,7 +249,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< { var startDate = arguments[1]; var endDate = arguments[2]; - var typeMapping = ExpressionExtensions.InferTypeMapping(startDate, endDate); + var typeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, startDate, endDate); startDate = _sqlExpressionFactory.ApplyTypeMapping(startDate, typeMapping); endDate = _sqlExpressionFactory.ApplyTypeMapping(endDate, typeMapping); diff --git a/src/EFCore.SqlServer/Query/Pipeline/SqlServerMathTranslator.cs b/src/EFCore.SqlServer/Query/Pipeline/SqlServerMathTranslator.cs index 655c3124987..31bde55cc7b 100644 --- a/src/EFCore.SqlServer/Query/Pipeline/SqlServerMathTranslator.cs +++ b/src/EFCore.SqlServer/Query/Pipeline/SqlServerMathTranslator.cs @@ -7,6 +7,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline { @@ -61,10 +62,14 @@ public class SqlServerMathTranslator : IMethodCallTranslator typeof(Math).GetRuntimeMethod(nameof(Math.Round), new[] { typeof(double), typeof(int) }) }; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IValueConverterSelector _valueConverterSelector; - public SqlServerMathTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqlServerMathTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { _sqlExpressionFactory = sqlExpressionFactory; + _valueConverterSelector = valueConverterSelector; } public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList arguments) @@ -72,8 +77,8 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< if (_supportedMethodTranslations.TryGetValue(method, out var sqlFunctionName)) { var typeMapping = arguments.Count == 1 - ? ExpressionExtensions.InferTypeMapping(arguments[0]) - : ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]); + ? ExpressionExtensions.InferTypeMapping(_valueConverterSelector, arguments[0]) + : ExpressionExtensions.InferTypeMapping(_valueConverterSelector, arguments[0], arguments[1]); var newArguments = new SqlExpression[arguments.Count]; newArguments[0] = _sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping); diff --git a/src/EFCore.SqlServer/Query/Pipeline/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Pipeline/SqlServerMethodCallTranslatorProvider.cs index 6badbe7858c..293268e17bd 100644 --- a/src/EFCore.SqlServer/Query/Pipeline/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Pipeline/SqlServerMethodCallTranslatorProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline { @@ -10,16 +11,17 @@ public class SqlServerMethodCallTranslatorProvider : RelationalMethodCallTransla { public SqlServerMethodCallTranslatorProvider( ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector, IEnumerable plugins) : base(sqlExpressionFactory, plugins) { AddTranslators(new IMethodCallTranslator[] { - new SqlServerMathTranslator(sqlExpressionFactory), + new SqlServerMathTranslator(sqlExpressionFactory, valueConverterSelector), new SqlServerNewGuidTranslator(sqlExpressionFactory), - new SqlServerStringMethodTranslator(sqlExpressionFactory), + new SqlServerStringMethodTranslator(sqlExpressionFactory, valueConverterSelector), new SqlServerDateTimeMethodTranslator(sqlExpressionFactory), - new SqlServerDateDiffFunctionsTranslator(sqlExpressionFactory), + new SqlServerDateDiffFunctionsTranslator(sqlExpressionFactory, valueConverterSelector), new SqlServerConvertTranslator(sqlExpressionFactory), new SqlServerObjectToStringTranslator(sqlExpressionFactory), new SqlServerFullTextSearchFunctionsTranslator(sqlExpressionFactory), diff --git a/src/EFCore.SqlServer/Query/Pipeline/SqlServerStringMethodTranslator.cs b/src/EFCore.SqlServer/Query/Pipeline/SqlServerStringMethodTranslator.cs index f5e7c0a066c..e1ee7e82725 100644 --- a/src/EFCore.SqlServer/Query/Pipeline/SqlServerStringMethodTranslator.cs +++ b/src/EFCore.SqlServer/Query/Pipeline/SqlServerStringMethodTranslator.cs @@ -8,6 +8,7 @@ using System.Text; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Pipeline { @@ -49,12 +50,16 @@ private static readonly MethodInfo _containsMethodInfo private static readonly MethodInfo _endsWithMethodInfo = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) }); private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IValueConverterSelector _valueConverterSelector; private const char LikeEscapeChar = '\\'; - public SqlServerStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqlServerStringMethodTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { _sqlExpressionFactory = sqlExpressionFactory; + _valueConverterSelector = valueConverterSelector; } public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList arguments) @@ -62,7 +67,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< if (_indexOfMethodInfo.Equals(method)) { var argument = arguments[0]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, argument); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, argument); argument = _sqlExpressionFactory.ApplyTypeMapping(argument, stringTypeMapping); var charIndexExpression = _sqlExpressionFactory.Subtract( @@ -92,7 +97,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< { var firstArgument = arguments[0]; var secondArgument = arguments[1]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, firstArgument, secondArgument); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, firstArgument, secondArgument); instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping); firstArgument = _sqlExpressionFactory.ApplyTypeMapping(firstArgument, stringTypeMapping); @@ -215,7 +220,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< if (_containsMethodInfo.Equals(method)) { var pattern = arguments[0]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, pattern); instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping); pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping); @@ -251,7 +256,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< private SqlExpression TranslateStartsEndsWith(SqlExpression instance, SqlExpression pattern, bool startsWith) { - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, pattern); instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping); pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping); diff --git a/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMathTranslator.cs b/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMathTranslator.cs index 86f545b9be7..d0832316bd9 100644 --- a/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMathTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMathTranslator.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Pipeline { @@ -43,10 +44,14 @@ public class SqliteMathTranslator : IMethodCallTranslator }; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IValueConverterSelector _valueConverterSelector; - public SqliteMathTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqliteMathTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { _sqlExpressionFactory = sqlExpressionFactory; + _valueConverterSelector = valueConverterSelector; } public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList arguments) @@ -58,7 +63,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< if (string.Equals(sqlFunctionName, "max") || string.Equals(sqlFunctionName, "max")) { - typeMapping = ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]); + typeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, arguments[0], arguments[1]); newArguments = new List { _sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping), diff --git a/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMethodCallTranslatorProvider.cs b/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMethodCallTranslatorProvider.cs index 3ad2e6a17b5..20e4e3badf5 100644 --- a/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMethodCallTranslatorProvider.cs +++ b/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteMethodCallTranslatorProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Pipeline { @@ -10,15 +11,16 @@ public class SqliteMethodCallTranslatorProvider : RelationalMethodCallTranslator { public SqliteMethodCallTranslatorProvider( ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector, IEnumerable plugins) : base(sqlExpressionFactory, plugins) { AddTranslators( new IMethodCallTranslator[] { - new SqliteMathTranslator(sqlExpressionFactory), + new SqliteMathTranslator(sqlExpressionFactory, valueConverterSelector), new SqliteDateTimeAddTranslator(sqlExpressionFactory), - new SqliteStringMethodTranslator(sqlExpressionFactory), + new SqliteStringMethodTranslator(sqlExpressionFactory, valueConverterSelector), }); } } diff --git a/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteStringMethodTranslator.cs b/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteStringMethodTranslator.cs index b181f887447..e85a79845cd 100644 --- a/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteStringMethodTranslator.cs +++ b/src/EFCore.Sqlite.Core/Query/Pipeline/SqliteStringMethodTranslator.cs @@ -8,6 +8,7 @@ using System.Text; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Pipeline { @@ -57,11 +58,15 @@ private static readonly MethodInfo _endsWithMethodInfo = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), new[] { typeof(string) }); private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IValueConverterSelector _valueConverterSelector; private const char LikeEscapeChar = '\\'; - public SqliteStringMethodTranslator(ISqlExpressionFactory sqlExpressionFactory) + public SqliteStringMethodTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IValueConverterSelector valueConverterSelector) { _sqlExpressionFactory = sqlExpressionFactory; + _valueConverterSelector = valueConverterSelector; } public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList arguments) @@ -69,7 +74,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< if (_indexOfMethodInfo.Equals(method)) { var argument = arguments[0]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, argument); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, argument); return _sqlExpressionFactory.Subtract( _sqlExpressionFactory.Function( @@ -87,7 +92,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< { var firstArgument = arguments[0]; var secondArgument = arguments[1]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, firstArgument, secondArgument); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, firstArgument, secondArgument); return _sqlExpressionFactory.Function( "replace", @@ -161,7 +166,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< if (_containsMethodInfo.Equals(method)) { var pattern = arguments[0]; - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, pattern); instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping); pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping); @@ -193,7 +198,7 @@ public SqlExpression Translate(SqlExpression instance, MethodInfo method, IList< private SqlExpression TranslateStartsEndsWith(SqlExpression instance, SqlExpression pattern, bool startsWith) { - var stringTypeMapping = ExpressionExtensions.InferTypeMapping(instance, pattern); + var stringTypeMapping = ExpressionExtensions.InferTypeMapping(_valueConverterSelector, instance, pattern); instance = _sqlExpressionFactory.ApplyTypeMapping(instance, stringTypeMapping); pattern = _sqlExpressionFactory.ApplyTypeMapping(pattern, stringTypeMapping); diff --git a/src/EFCore/ChangeTracking/CollectionEntry`.cs b/src/EFCore/ChangeTracking/CollectionEntry`.cs index b5354249b73..129ebcc11ae 100644 --- a/src/EFCore/ChangeTracking/CollectionEntry`.cs +++ b/src/EFCore/ChangeTracking/CollectionEntry`.cs @@ -64,7 +64,7 @@ public CollectionEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] IN /// public new virtual IEnumerable CurrentValue { - get => this.GetInfrastructure().GetCurrentValue>(Metadata); + get => this.GetInfrastructure().GetDeclaredCurrentValue>(Metadata); [param: CanBeNull] set => base.CurrentValue = value; } diff --git a/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs index 0f4c897b1ca..c24d08a0ae6 100644 --- a/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs +++ b/src/EFCore/ChangeTracking/Internal/CurrentPropertyValues.cs @@ -33,7 +33,7 @@ public CurrentPropertyValues([NotNull] InternalEntityEntry internalEntry) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override TValue GetValue(string propertyName) - => InternalEntry.GetCurrentValue(InternalEntry.EntityType.GetProperty(propertyName)); + => InternalEntry.GetDeclaredCurrentValue(InternalEntry.EntityType.GetProperty(propertyName)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -42,7 +42,7 @@ public override TValue GetValue(string propertyName) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override TValue GetValue(IProperty property) - => InternalEntry.GetCurrentValue(InternalEntry.EntityType.CheckPropertyBelongsToType(property)); + => InternalEntry.GetDeclaredCurrentValue(InternalEntry.EntityType.CheckPropertyBelongsToType(property)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index be72a7a63a5..0073df9c5f8 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -688,6 +688,15 @@ internal static readonly MethodInfo GetCurrentValueMethod public virtual TProperty GetCurrentValue(IPropertyBase propertyBase) => ((Func)propertyBase.GetPropertyAccessors().CurrentValueGetter)(this); + /// + /// 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 TProperty GetDeclaredCurrentValue(IPropertyBase propertyBase) + => ((Func)propertyBase.GetPropertyAccessors().DeclaredCurrentValueGetter)(this); + /// /// 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 @@ -703,9 +712,8 @@ public virtual TProperty GetOriginalValue(IProperty property) /// 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 TProperty GetRelationshipSnapshotValue([NotNull] IPropertyBase propertyBase) - => ((Func)propertyBase.GetPropertyAccessors().RelationshipSnapshotGetter)( - this); + public virtual TProperty GetDeclaredOriginalValue(IProperty property) + => ((Func)property.GetPropertyAccessors().DeclaredOriginalValueGetter)(this); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs b/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs index b26786cf087..828ebd001dd 100644 --- a/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs +++ b/src/EFCore/ChangeTracking/Internal/OriginalPropertyValues.cs @@ -33,7 +33,7 @@ public OriginalPropertyValues([NotNull] InternalEntityEntry internalEntry) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override TValue GetValue(string propertyName) - => InternalEntry.GetOriginalValue(InternalEntry.EntityType.GetProperty(propertyName)); + => InternalEntry.GetDeclaredOriginalValue(InternalEntry.EntityType.GetProperty(propertyName)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -42,7 +42,7 @@ public override TValue GetValue(string propertyName) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override TValue GetValue(IProperty property) - => InternalEntry.GetOriginalValue(InternalEntry.EntityType.CheckPropertyBelongsToType(property)); + => InternalEntry.GetDeclaredOriginalValue(InternalEntry.EntityType.CheckPropertyBelongsToType(property)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index f6509683e43..02f9d6692c4 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -163,12 +163,20 @@ private Expression CreateSnapshotExpression( memberAccess = Expression.Convert(memberAccess, propertyBase.ClrType); } - arguments[i] = (propertyBase as INavigation)?.IsCollection() ?? false - ? Expression.Call( - null, - _snapshotCollectionMethod, - memberAccess) - : CreateSnapshotValueExpression(memberAccess, propertyBase); + if (propertyBase is INavigation navigation + && navigation.IsCollection()) + { + if (!typeof(IEnumerable).IsAssignableFrom(memberAccess.Type)) + { + memberAccess = Expression.Convert(memberAccess, typeof(IEnumerable)); + } + + arguments[i] = Expression.Call(null, _snapshotCollectionMethod, memberAccess); + } + else + { + arguments[i] = CreateSnapshotValueExpression(memberAccess, propertyBase); + } } var constructorExpression = Expression.Convert( diff --git a/src/EFCore/ChangeTracking/PropertyEntry`.cs b/src/EFCore/ChangeTracking/PropertyEntry`.cs index fc2ef3b404f..3998a85865f 100644 --- a/src/EFCore/ChangeTracking/PropertyEntry`.cs +++ b/src/EFCore/ChangeTracking/PropertyEntry`.cs @@ -59,7 +59,7 @@ public PropertyEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] IPro /// public new virtual TProperty CurrentValue { - get => InternalEntry.GetCurrentValue(Metadata); + get => InternalEntry.GetDeclaredCurrentValue(Metadata); [param: CanBeNull] set => base.CurrentValue = value; } @@ -71,7 +71,7 @@ public PropertyEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] IPro /// public new virtual TProperty OriginalValue { - get => InternalEntry.GetOriginalValue(Metadata); + get => InternalEntry.GetDeclaredOriginalValue(Metadata); [param: CanBeNull] set => base.OriginalValue = value; } } diff --git a/src/EFCore/ChangeTracking/ReferenceEntry`.cs b/src/EFCore/ChangeTracking/ReferenceEntry`.cs index 955df0d0ea9..ccce578fcdc 100644 --- a/src/EFCore/ChangeTracking/ReferenceEntry`.cs +++ b/src/EFCore/ChangeTracking/ReferenceEntry`.cs @@ -75,7 +75,7 @@ public ReferenceEntry([NotNull] InternalEntityEntry internalEntry, [NotNull] INa /// public new virtual TProperty CurrentValue { - get => this.GetInfrastructure().GetCurrentValue(Metadata); + get => this.GetInfrastructure().GetDeclaredCurrentValue(Metadata); [param: CanBeNull] set => base.CurrentValue = value; } diff --git a/src/EFCore/ChangeTracking/ValueComparer`.cs b/src/EFCore/ChangeTracking/ValueComparer`.cs index 9d4ebfac2a9..b0c8e8aed08 100644 --- a/src/EFCore/ChangeTracking/ValueComparer`.cs +++ b/src/EFCore/ChangeTracking/ValueComparer`.cs @@ -113,9 +113,7 @@ protected static Expression> CreateDefaultEqualsExpression() || unwrappedType == typeof(bool) || unwrappedType == typeof(decimal) || unwrappedType == typeof(double) - || unwrappedType == typeof(float) - || unwrappedType == typeof(object) - ) + || unwrappedType == typeof(float)) { return Expression.Lambda>( Expression.Equal(param1, param2), diff --git a/src/EFCore/Metadata/IPropertyBase.cs b/src/EFCore/Metadata/IPropertyBase.cs index 9d2208fed88..f5277c06864 100644 --- a/src/EFCore/Metadata/IPropertyBase.cs +++ b/src/EFCore/Metadata/IPropertyBase.cs @@ -23,10 +23,16 @@ public interface IPropertyBase : IAnnotatable ITypeBase DeclaringType { get; } /// - /// Gets the type of value that this property holds. + /// Gets the type of value that this property holds, which will be the type of the + /// field for properties that are known to be backed by fields. /// Type ClrType { get; } + /// + /// Gets the type that this property is declared as. + /// + Type DeclaredClrType { get; } + /// /// Gets the for the underlying CLR property that this /// object represents. This may be null for shadow properties or properties mapped directly to fields. diff --git a/src/EFCore/Metadata/Internal/Navigation.cs b/src/EFCore/Metadata/Internal/Navigation.cs index 28a4f2d6a49..48bf7ada3f2 100644 --- a/src/EFCore/Metadata/Internal/Navigation.cs +++ b/src/EFCore/Metadata/Internal/Navigation.cs @@ -49,7 +49,15 @@ public Navigation( /// 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 override Type ClrType => this.GetIdentifyingMemberInfo()?.GetMemberType() ?? typeof(object); + public override Type ClrType => (FieldInfo ?? (MemberInfo)PropertyInfo)?.GetMemberType() ?? typeof(object); + + /// + /// 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 override Type DeclaredClrType => (PropertyInfo ?? (MemberInfo)FieldInfo)?.GetMemberType() ?? typeof(object); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index e8f2f8b3a77..814e1a27596 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -55,7 +55,7 @@ public Property( Check.NotNull(declaringEntityType, nameof(declaringEntityType)); DeclaringEntityType = declaringEntityType; - ClrType = clrType; + DeclaredClrType = clrType; _configurationSource = configurationSource; _typeConfigurationSource = typeConfigurationSource; @@ -95,7 +95,21 @@ public override TypeBase DeclaringType /// 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 override Type ClrType { [DebuggerStepThrough] get; } + public override Type ClrType + { + [DebuggerStepThrough] get => FieldInfo?.FieldType ?? DeclaredClrType; + } + + /// + /// 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 override Type DeclaredClrType + { + [DebuggerStepThrough] get; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/PropertyAccessors.cs b/src/EFCore/Metadata/Internal/PropertyAccessors.cs index 60b0700b5df..668c8445652 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessors.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessors.cs @@ -23,14 +23,18 @@ public sealed class PropertyAccessors /// public PropertyAccessors( [NotNull] Delegate currentValueGetter, + [NotNull] Delegate declaredCurrentValueGetter, [NotNull] Delegate preStoreGeneratedCurrentValueGetter, [CanBeNull] Delegate originalValueGetter, + [CanBeNull] Delegate declaredOriginalValueGetter, [NotNull] Delegate relationshipSnapshotGetter, [CanBeNull] Func valueBufferGetter) { CurrentValueGetter = currentValueGetter; + DeclaredCurrentValueGetter = declaredCurrentValueGetter; PreStoreGeneratedCurrentValueGetter = preStoreGeneratedCurrentValueGetter; OriginalValueGetter = originalValueGetter; + DeclaredOriginalValueGetter = declaredOriginalValueGetter; RelationshipSnapshotGetter = relationshipSnapshotGetter; ValueBufferGetter = valueBufferGetter; } @@ -43,6 +47,14 @@ public PropertyAccessors( /// public Delegate CurrentValueGetter { get; } + /// + /// 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 Delegate DeclaredCurrentValueGetter { get; } + /// /// 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 @@ -59,6 +71,14 @@ public PropertyAccessors( /// public Delegate OriginalValueGetter { get; } + /// + /// 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 Delegate DeclaredOriginalValueGetter { get; } + /// /// 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 diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 113fa9084d9..fafd7f18d62 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -27,7 +27,7 @@ public class PropertyAccessorsFactory /// public virtual PropertyAccessors Create([NotNull] IPropertyBase propertyBase) => (PropertyAccessors)_genericCreate - .MakeGenericMethod(propertyBase.ClrType) + .MakeGenericMethod(propertyBase.ClrType, propertyBase.DeclaredClrType) .Invoke( null, new object[] { @@ -38,13 +38,34 @@ private static readonly MethodInfo _genericCreate = typeof(PropertyAccessorsFactory).GetTypeInfo().GetDeclaredMethod(nameof(CreateGeneric)); [UsedImplicitly] - private static PropertyAccessors CreateGeneric(IPropertyBase propertyBase) + private static PropertyAccessors CreateGeneric(IPropertyBase propertyBase) { var property = propertyBase as IProperty; + + var currentValueGetter + = CreateCurrentValueGetter(propertyBase, useStoreGeneratedValues: true); + + var declaredCurrentValueGetter + = typeof(TProperty) == typeof(TDeclaredProperty) + ? (Func)(object)currentValueGetter + : CreateDeclaredCurrentValueGetter(propertyBase, useStoreGeneratedValues: true); + + var originalValueGetter + = property == null + ? null + : CreateOriginalValueGetter(property); + + var declaredOriginalValueGetter + = property == null || typeof(TProperty) == typeof(TDeclaredProperty) + ? (Func)(object)originalValueGetter + : CreateDeclaredOriginalValueGetter(property); + return new PropertyAccessors( - CreateCurrentValueGetter(propertyBase, useStoreGeneratedValues: true), + currentValueGetter, + declaredCurrentValueGetter, CreateCurrentValueGetter(propertyBase, useStoreGeneratedValues: false), - property == null ? null : CreateOriginalValueGetter(property), + originalValueGetter, + declaredOriginalValueGetter, CreateRelationshipSnapshotGetter(propertyBase), property == null ? null : CreateValueBufferGetter(property)); } @@ -52,9 +73,34 @@ private static PropertyAccessors CreateGeneric(IPropertyBase property private static Func CreateCurrentValueGetter( IPropertyBase propertyBase, bool useStoreGeneratedValues) { - var entityClrType = propertyBase.DeclaringType.ClrType; var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); + return Expression.Lambda>( + CreateCurrentValueExpression( + entryParameter, propertyBase, useStoreGeneratedValues), + entryParameter) + .Compile(); + } + + private static Func CreateDeclaredCurrentValueGetter( + IPropertyBase propertyBase, bool useStoreGeneratedValues) + { + var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); + + return Expression.Lambda>( + Expression.Convert( + CreateCurrentValueExpression( + entryParameter, propertyBase, useStoreGeneratedValues), + typeof(TDeclaredProperty)), + entryParameter) + .Compile(); + } + + private static Expression CreateCurrentValueExpression( + ParameterExpression entryParameter, IPropertyBase propertyBase, bool useStoreGeneratedValues) + { + var entityClrType = propertyBase.DeclaringType.ClrType; + var shadowIndex = propertyBase.GetShadowIndex(); Expression currentValueExpression; if (shadowIndex >= 0) @@ -120,36 +166,49 @@ private static Func CreateCurrentValueGetter CreateOriginalValueGetter(IProperty property) + { + var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); + return Expression.Lambda>( - currentValueExpression, + CreateOriginalValueGetterExpression(entryParameter, property), entryParameter) .Compile(); } - private static Func CreateOriginalValueGetter(IProperty property) + private static Func CreateDeclaredOriginalValueGetter(IProperty property) { var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); - var originalValuesIndex = property.GetOriginalValueIndex(); - return Expression.Lambda>( - originalValuesIndex >= 0 - ? (Expression)Expression.Call( - entryParameter, - InternalEntityEntry.ReadOriginalValueMethod.MakeGenericMethod(typeof(TProperty)), - Expression.Constant(property), - Expression.Constant(originalValuesIndex)) - : Expression.Block( - Expression.Throw( - Expression.Constant( - new InvalidOperationException( - CoreStrings.OriginalValueNotTracked(property.Name, property.DeclaringEntityType.DisplayName())))), -#pragma warning disable IDE0034 // Simplify 'default' expression - default infer to default(object) instead of default(TProperty) - Expression.Constant(default(TProperty), typeof(TProperty))), -#pragma warning restore IDE0034 // Simplify 'default' expression + return Expression.Lambda>( + Expression.Convert( + CreateOriginalValueGetterExpression(entryParameter, property), + typeof(TDeclaredProperty)), entryParameter) .Compile(); } + private static Expression CreateOriginalValueGetterExpression(ParameterExpression entryParameter, IProperty property) + { + var originalValuesIndex = property.GetOriginalValueIndex(); + + return originalValuesIndex >= 0 + ? (Expression)Expression.Call( + entryParameter, + InternalEntityEntry.ReadOriginalValueMethod.MakeGenericMethod(typeof(TProperty)), + Expression.Constant(property), + Expression.Constant(originalValuesIndex)) + : Expression.Block( + Expression.Throw( + Expression.Constant( + new InvalidOperationException( + CoreStrings.OriginalValueNotTracked(property.Name, property.DeclaringEntityType.DisplayName())))), + Expression.Constant(default(TProperty), typeof(TProperty))); + } + private static Func CreateRelationshipSnapshotGetter(IPropertyBase propertyBase) { var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry"); diff --git a/src/EFCore/Metadata/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs index 28e9675f946..18c27cf87fe 100644 --- a/src/EFCore/Metadata/Internal/PropertyBase.cs +++ b/src/EFCore/Metadata/Internal/PropertyBase.cs @@ -292,6 +292,14 @@ private void UpdateFieldInfoConfigurationSource(ConfigurationSource configuratio /// public abstract Type ClrType { get; } + /// + /// 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 abstract Type DeclaredClrType { get; } + /// /// 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 diff --git a/src/EFCore/Metadata/Internal/ServiceProperty.cs b/src/EFCore/Metadata/Internal/ServiceProperty.cs index 33a2f8ea906..18dae2ef524 100644 --- a/src/EFCore/Metadata/Internal/ServiceProperty.cs +++ b/src/EFCore/Metadata/Internal/ServiceProperty.cs @@ -40,7 +40,8 @@ public ServiceProperty( Check.NotNull(declaringEntityType, nameof(declaringEntityType)); DeclaringEntityType = declaringEntityType; - ClrType = propertyInfo?.PropertyType ?? fieldInfo?.FieldType; + DeclaredClrType = (propertyInfo ?? (MemberInfo)fieldInfo).GetMemberType(); + ClrType = (fieldInfo ?? (MemberInfo)propertyInfo).GetMemberType(); _configurationSource = configurationSource; Builder = new InternalServicePropertyBuilder(this, declaringEntityType.Model.Builder); @@ -81,6 +82,14 @@ public override TypeBase DeclaringType /// public override Type ClrType { get; } + /// + /// 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 override Type DeclaredClrType { get; } + /// /// 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 diff --git a/src/EFCore/Query/Pipeline/ProjectionMember.cs b/src/EFCore/Query/Pipeline/ProjectionMember.cs index dab0fa2d3e9..20b5e4023f6 100644 --- a/src/EFCore/Query/Pipeline/ProjectionMember.cs +++ b/src/EFCore/Query/Pipeline/ProjectionMember.cs @@ -1,6 +1,7 @@ // 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.Linq; using System.Collections.Generic; using System.Reflection; @@ -39,6 +40,8 @@ public ProjectionMember ShiftMember(MemberInfo member) return new ProjectionMember(existingChain); } + public Type MemberType => _memberChain.LastOrDefault()?.GetMemberType(); + public override int GetHashCode() { unchecked diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs index 16d9f26ea62..fd14ffae494 100644 --- a/src/EFCore/Storage/CoreTypeMapping.cs +++ b/src/EFCore/Storage/CoreTypeMapping.cs @@ -38,33 +38,15 @@ protected readonly struct CoreTypeMappingParameters /// Converts types to and from the store whenever this mapping is used. /// Supports custom value snapshotting and comparisons. /// Supports custom comparisons between keys--e.g. PK to FK comparison. + /// Supports structural snapshotting needed for mutable reference types. /// An optional factory for creating a specific . public CoreTypeMappingParameters( [NotNull] Type clrType, [CanBeNull] ValueConverter converter = null, [CanBeNull] ValueComparer comparer = null, [CanBeNull] ValueComparer keyComparer = null, + [CanBeNull] ValueComparer structuralComparer = null, [CanBeNull] Func valueGeneratorFactory = null) - : this(clrType, converter, comparer, keyComparer, null, valueGeneratorFactory) - { - } - - /// - /// Creates a new parameter object. - /// - /// The .NET type used in the EF model. - /// Converts types to and from the store whenever this mapping is used. - /// Supports custom value snapshotting and comparisons. - /// Supports custom comparisons between keys--e.g. PK to FK comparison. - /// Supports structural snapshotting needed for mutable reference types. - /// An optional factory for creating a specific . - public CoreTypeMappingParameters( - [NotNull] Type clrType, - [CanBeNull] ValueConverter converter, - [CanBeNull] ValueComparer comparer, - [CanBeNull] ValueComparer keyComparer, - [CanBeNull] ValueComparer structuralComparer, - [CanBeNull] Func valueGeneratorFactory) { Check.NotNull(clrType, nameof(clrType)); diff --git a/src/EFCore/Storage/TypeMappingInfo.cs b/src/EFCore/Storage/TypeMappingInfo.cs index 7f2d1f56b05..7ce50464172 100644 --- a/src/EFCore/Storage/TypeMappingInfo.cs +++ b/src/EFCore/Storage/TypeMappingInfo.cs @@ -85,6 +85,7 @@ public TypeMappingInfo( IsUnicode = isUnicode ?? mappingHints?.IsUnicode ?? fallbackUnicode; IsRowVersion = property.IsConcurrencyToken && property.ValueGenerated == ValueGenerated.OnAddOrUpdate; ClrType = (customConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); + DeclaredClrType = (customConverter?.ProviderClrType ?? property.DeclaredClrType).UnwrapNullableType(); Scale = mappingHints?.Scale ?? fallbackScale; Precision = mappingHints?.Precision ?? fallbackPrecision; } @@ -131,6 +132,7 @@ public TypeMappingInfo( int? scale = null) { ClrType = type?.UnwrapNullableType(); + DeclaredClrType = ClrType; IsKeyOrIndex = keyOrIndex; Size = size; @@ -170,6 +172,7 @@ public TypeMappingInfo( Precision = precision ?? source.Precision ?? mappingHints?.Precision; ClrType = converter.ProviderClrType.UnwrapNullableType(); + DeclaredClrType = source.DeclaredClrType; } /// @@ -211,11 +214,18 @@ public TypeMappingInfo WithConverter(in ValueConverterInfo converterInfo) public int? Scale { get; } /// - /// The CLR type in the model. May be null if type information is conveyed via other means - /// (e.g. the store name in a relational type mapping info) + /// The CLR type used to store the property in the model. This may be the backing field type. + /// May be null if type information is conveyed via other means (e.g. the store name in a relational type mapping info) /// public Type ClrType { get; } + /// + /// The CLR type of the property, which may be different from if a backing field of + /// a different type is used. + /// May be null if type information is conveyed via other means (e.g. the store name in a relational type mapping info) + /// + public Type DeclaredClrType { get; } + /// /// Compares this to another to check if they represent the same mapping. /// @@ -223,6 +233,7 @@ public TypeMappingInfo WithConverter(in ValueConverterInfo converterInfo) /// True if they represent the same mapping; false otherwise. public bool Equals(TypeMappingInfo other) => ClrType == other.ClrType + && DeclaredClrType == other.DeclaredClrType && IsKeyOrIndex == other.IsKeyOrIndex && Size == other.Size && IsUnicode == other.IsUnicode @@ -247,6 +258,7 @@ public override bool Equals(object obj) public override int GetHashCode() { var hashCode = ClrType?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (DeclaredClrType?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ IsKeyOrIndex.GetHashCode(); hashCode = (hashCode * 397) ^ (Size?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ (IsUnicode?.GetHashCode() ?? 0); diff --git a/src/EFCore/Storage/TypeMappingSource.cs b/src/EFCore/Storage/TypeMappingSource.cs index 394b45330e7..6d4b5979cd7 100644 --- a/src/EFCore/Storage/TypeMappingSource.cs +++ b/src/EFCore/Storage/TypeMappingSource.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Reflection; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; @@ -79,54 +80,79 @@ private CoreTypeMapping FindMappingWithConversion( k => { var (info, providerType, converter) = k; + + var sourceType = info.ClrType; + var sourceDeclaredType = info.DeclaredClrType; + + var needsConvertFromObject + = sourceType == typeof(object) + && sourceDeclaredType != typeof(object); + + var effectiveSourceType = needsConvertFromObject ? sourceDeclaredType : sourceType; + + ValueConverterInfo fromObjectConverter = default; + var fromObjectMappingInfo = info; + + if (needsConvertFromObject) + { + fromObjectConverter = Dependencies.ValueConverterSelector + .Select(typeof(object), sourceDeclaredType) + .Single(); + + fromObjectMappingInfo = info.WithConverter(fromObjectConverter); + } + var mapping = providerType == null - || providerType == info.ClrType - ? FindMapping(info) + || providerType == effectiveSourceType + ? FindMapping(fromObjectMappingInfo) : null; - if (mapping == null) + if (mapping == null + && effectiveSourceType != null) { - var sourceType = info.ClrType; - - if (sourceType != null) + foreach (var converterInfo in Dependencies + .ValueConverterSelector + .Select(effectiveSourceType, providerType)) { - foreach (var converterInfo in Dependencies - .ValueConverterSelector - .Select(sourceType, providerType)) - { - var mappingInfoUsed = info.WithConverter(converterInfo); - mapping = FindMapping(mappingInfoUsed); + var mappingInfoUsed = info.WithConverter(converterInfo); + mapping = FindMapping(mappingInfoUsed); - if (mapping == null - && providerType != null) + if (mapping == null + && providerType != null) + { + foreach (var secondConverterInfo in Dependencies + .ValueConverterSelector + .Select(providerType)) { - foreach (var secondConverterInfo in Dependencies - .ValueConverterSelector - .Select(providerType)) - { - mapping = FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); + mapping = FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); - if (mapping != null) - { - mapping = mapping.Clone(secondConverterInfo.Create()); - break; - } + if (mapping != null) + { + mapping = mapping.Clone(secondConverterInfo.Create()); + break; } } + } - if (mapping != null) - { - mapping = mapping.Clone(converterInfo.Create()); - break; - } + if (mapping != null) + { + mapping = mapping.Clone(converterInfo.Create()); + break; } } } - if (mapping != null - && converter != null) + if (mapping != null) { - mapping = mapping.Clone(converter); + if (needsConvertFromObject) + { + mapping = mapping.Clone(fromObjectConverter.Create()); + } + + if (converter != null) + { + mapping = mapping.Clone(converter); + } } return mapping; diff --git a/src/EFCore/Storage/ValueConversion/BoolToZeroOneConverter.cs b/src/EFCore/Storage/ValueConversion/BoolToZeroOneConverter.cs index d7ab53d9571..324bd0a7d3c 100644 --- a/src/EFCore/Storage/ValueConversion/BoolToZeroOneConverter.cs +++ b/src/EFCore/Storage/ValueConversion/BoolToZeroOneConverter.cs @@ -36,7 +36,7 @@ private static TProvider Zero() typeof(BoolToZeroOneConverter), typeof(int), typeof(short), typeof(long), typeof(sbyte), typeof(uint), typeof(ushort), typeof(ulong), typeof(byte), - typeof(decimal), typeof(double), typeof(float)); + typeof(decimal), typeof(double), typeof(float), typeof(char)); return Activator.CreateInstance(); } @@ -67,7 +67,9 @@ private static TProvider One() ? (double)1 : type == typeof(float) ? (float)1 - : (object)1); + : type == typeof(char) + ? (char)1 + : (object)1); } } } diff --git a/src/EFCore/Storage/ValueConversion/EnumToNumberConverter.cs b/src/EFCore/Storage/ValueConversion/EnumToNumberConverter.cs index 312bccb9491..37f8ca66aab 100644 --- a/src/EFCore/Storage/ValueConversion/EnumToNumberConverter.cs +++ b/src/EFCore/Storage/ValueConversion/EnumToNumberConverter.cs @@ -66,7 +66,7 @@ private static Expression> ToNumber() typeof(EnumToNumberConverter), typeof(int), typeof(long), typeof(short), typeof(byte), typeof(uint), typeof(ulong), typeof(ushort), typeof(sbyte), - typeof(double), typeof(float), typeof(decimal)); + typeof(double), typeof(float), typeof(decimal), typeof(char)); var param = Expression.Parameter(typeof(TEnum), "value"); diff --git a/src/EFCore/Storage/ValueConversion/ValueConverterSelector.cs b/src/EFCore/Storage/ValueConversion/ValueConverterSelector.cs index bb2e18c9224..59bf5a08d4c 100644 --- a/src/EFCore/Storage/ValueConversion/ValueConverterSelector.cs +++ b/src/EFCore/Storage/ValueConversion/ValueConverterSelector.cs @@ -37,7 +37,7 @@ public class ValueConverterSelector : IValueConverterSelector private static readonly Type[] _charPreferred = { typeof(char), typeof(int), typeof(ushort), typeof(uint), typeof(long), typeof(ulong), typeof(decimal) }; - private static readonly Type[] _numerics = { typeof(int), typeof(long), typeof(short), typeof(byte), typeof(ulong), typeof(uint), typeof(ushort), typeof(sbyte), typeof(decimal), typeof(double), typeof(float) }; + private static readonly Type[] _numerics = { typeof(int), typeof(long), typeof(short), typeof(byte), typeof(ulong), typeof(uint), typeof(ushort), typeof(sbyte), typeof(decimal), typeof(double), typeof(float), typeof(char) }; /// /// Initializes a new instance of the class. @@ -72,6 +72,18 @@ public virtual IEnumerable Select( var underlyingModelType = modelClrType.UnwrapNullableType(); var underlyingProviderType = providerClrType?.UnwrapNullableType(); + if (underlyingModelType == typeof(object) + && underlyingProviderType != null + && underlyingProviderType != typeof(object)) + { + yield return _converters.GetOrAdd( + (typeof(object), underlyingProviderType), + k => (ValueConverterInfo)typeof(CastingConverter<,>) + .MakeGenericType(typeof(object), k.ProviderClrType) + .GetAnyProperty("DefaultInfo") + .GetValue(null)); + } + if (underlyingModelType.IsEnum) { foreach (var converterInfo in FindNumericConventions( @@ -166,6 +178,12 @@ public virtual IEnumerable Select( .GetAnyProperty("DefaultInfo") .GetValue(null)); } + else if (underlyingProviderType == typeof(char)) + { + yield return _converters.GetOrAdd( + (underlyingModelType, typeof(char)), + k => StringToCharConverter.DefaultInfo); + } else if (_numerics.Contains(underlyingProviderType)) { foreach (var converterInfo in FindNumericConventions( @@ -207,12 +225,6 @@ public virtual IEnumerable Select( (underlyingModelType, typeof(bool)), k => StringToBoolConverter.DefaultInfo); } - else if (underlyingProviderType == typeof(char)) - { - yield return _converters.GetOrAdd( - (underlyingModelType, typeof(char)), - k => StringToCharConverter.DefaultInfo); - } } else if (underlyingModelType == typeof(DateTime) || underlyingModelType == typeof(DateTimeOffset) @@ -266,7 +278,8 @@ public virtual IEnumerable Select( && (underlyingProviderType == null || underlyingProviderType == typeof(byte[]) || underlyingProviderType == typeof(string) - || _numerics.Contains(underlyingProviderType))) + || _numerics.Contains(underlyingProviderType) + || underlyingProviderType.IsEnum)) { foreach (var converterInfo in FindNumericConventions( underlyingModelType, @@ -477,6 +490,18 @@ private IEnumerable FindNumericConventions( .GetValue(null)); } } + + if (_numerics.Contains(modelType) + && providerType != null + && providerType.IsEnum) + { + yield return _converters.GetOrAdd( + (modelType, providerType), + k => (ValueConverterInfo)typeof(CastingConverter<,>) + .MakeGenericType(k.ModelClrType, k.ProviderClrType) + .GetAnyProperty("DefaultInfo") + .GetValue(null)); + } } private IEnumerable FindPreferredConversions( diff --git a/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs b/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs index b81c6a00923..5b7ed10104f 100644 --- a/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs +++ b/test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -53,7 +54,7 @@ public virtual async Task Can_filter_projection_with_captured_enum_variable(bool } } - [Theory(Skip = "QueryIssue")] + [Theory] [InlineData(false)] [InlineData(true)] public virtual async Task Can_filter_projection_with_inline_enum_variable(bool async) @@ -203,7 +204,7 @@ public virtual void Can_perform_query_with_ansi_strings_test() } } - [Fact(Skip = "QueryIssue")] + [Fact] public virtual void Can_query_using_any_data_type() { using (var context = CreateContext()) @@ -216,7 +217,7 @@ public virtual void Can_query_using_any_data_type() } } - [Fact(Skip = "QueryIssue")] + [Fact] public virtual void Can_query_using_any_data_type_shadow() { using (var context = CreateContext()) @@ -549,7 +550,7 @@ protected EntityEntry AddTestBuiltInDataTypes(DbSet s return entityEntry; } - [Fact(Skip = "QueryIssue")] + [Fact] public virtual void Can_query_using_any_nullable_data_type() { using (var context = CreateContext()) @@ -562,7 +563,7 @@ public virtual void Can_query_using_any_nullable_data_type() } } - [Fact(Skip = "QueryIssue")] + [Fact] public virtual void Can_query_using_any_data_type_nullable_shadow() { using (var context = CreateContext()) @@ -925,7 +926,7 @@ protected virtual EntityEntry AddTestBuiltInNullableDataTypes( return entityEntry; } - [Fact(Skip = "QueryIssue")] + [Fact] public virtual void Can_query_using_any_nullable_data_type_as_literal() { using (var context = CreateContext()) @@ -1135,7 +1136,7 @@ public virtual void Can_query_using_any_nullable_data_type_as_literal() } } - [Fact(Skip = "Tasklist#8")] + [Fact] public virtual void Can_query_with_null_parameters_using_any_nullable_data_type() { using (var context = CreateContext()) @@ -1670,6 +1671,220 @@ public virtual void Can_insert_and_read_back_all_nullable_data_types_with_values } } + [Fact] + public virtual void Can_insert_and_read_back_object_backed_data_types() + { + using (var context = CreateContext()) + { + context.Set().Add( + new ObjectBackedDataTypes + { + Id = 101, + PartitionId = 101, + String = "TestString", + Bytes = new byte[] { 10, 9, 8, 7, 6 }, + Int16 = -1234, + Int32 = -123456789, + Int64 = -1234567890123456789L, + Double = -1.23456789, + Decimal = -1234567890.01M, + DateTime = DateTime.Parse("01/01/2000 12:34:56"), + DateTimeOffset = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)), + TimeSpan = new TimeSpan(0, 10, 9, 8, 7), + Single = -1.234F, + Boolean = false, + Byte = 255, + UnsignedInt16 = 1234, + UnsignedInt32 = 1234565789U, + UnsignedInt64 = 1234567890123456789UL, + Character = 'a', + SignedByte = -128, + Enum64 = Enum64.SomeValue, + Enum32 = Enum32.SomeValue, + Enum16 = Enum16.SomeValue, + Enum8 = Enum8.SomeValue, + EnumU64 = EnumU64.SomeValue, + EnumU32 = EnumU32.SomeValue, + EnumU16 = EnumU16.SomeValue, + EnumS8 = EnumS8.SomeValue + }); + + Assert.Equal(1, context.SaveChanges()); + } + + using (var context = CreateContext()) + { + var dt = context.Set().Where(ndt => ndt.Id == 101).ToList().Single(); + + Assert.Equal("TestString", dt.String); + Assert.Equal(new byte[] { 10, 9, 8, 7, 6 }, dt.Bytes); + Assert.Equal((short)-1234, dt.Int16); + Assert.Equal(-123456789, dt.Int32); + Assert.Equal(-1234567890123456789L, dt.Int64); + Assert.Equal(-1.23456789, dt.Double); + Assert.Equal(-1234567890.01M, dt.Decimal); + Assert.Equal(DateTime.Parse("01/01/2000 12:34:56"), dt.DateTime); + Assert.Equal(new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)), dt.DateTimeOffset); + Assert.Equal(new TimeSpan(0, 10, 9, 8, 7), dt.TimeSpan); + Assert.Equal(-1.234F, dt.Single); + Assert.Equal(false, dt.Boolean); + Assert.Equal((byte)255, dt.Byte); + Assert.Equal(Enum64.SomeValue, dt.Enum64); + Assert.Equal(Enum32.SomeValue, dt.Enum32); + Assert.Equal(Enum16.SomeValue, dt.Enum16); + Assert.Equal(Enum8.SomeValue, dt.Enum8); + Assert.Equal((ushort)1234, dt.UnsignedInt16); + Assert.Equal(1234565789U, dt.UnsignedInt32); + Assert.Equal(1234567890123456789UL, dt.UnsignedInt64); + Assert.Equal('a', dt.Character); + Assert.Equal((sbyte)-128, dt.SignedByte); + Assert.Equal(EnumU64.SomeValue, dt.EnumU64); + Assert.Equal(EnumU32.SomeValue, dt.EnumU32); + Assert.Equal(EnumU16.SomeValue, dt.EnumU16); + Assert.Equal(EnumS8.SomeValue, dt.EnumS8); + } + } + + [Fact] + public virtual void Can_insert_and_read_back_nullable_backed_data_types() + { + using (var context = CreateContext()) + { + context.Set().Add( + new NullableBackedDataTypes + { + Id = 101, + PartitionId = 101, + Int16 = -1234, + Int32 = -123456789, + Int64 = -1234567890123456789L, + Double = -1.23456789, + Decimal = -1234567890.01M, + DateTime = DateTime.Parse("01/01/2000 12:34:56"), + DateTimeOffset = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)), + TimeSpan = new TimeSpan(0, 10, 9, 8, 7), + Single = -1.234F, + Boolean = false, + Byte = 255, + UnsignedInt16 = 1234, + UnsignedInt32 = 1234565789U, + UnsignedInt64 = 1234567890123456789UL, + Character = 'a', + SignedByte = -128, + Enum64 = Enum64.SomeValue, + Enum32 = Enum32.SomeValue, + Enum16 = Enum16.SomeValue, + Enum8 = Enum8.SomeValue, + EnumU64 = EnumU64.SomeValue, + EnumU32 = EnumU32.SomeValue, + EnumU16 = EnumU16.SomeValue, + EnumS8 = EnumS8.SomeValue + }); + + Assert.Equal(1, context.SaveChanges()); + } + + using (var context = CreateContext()) + { + var dt = context.Set().Where(ndt => ndt.Id == 101).ToList().Single(); + + Assert.Equal((short)-1234, dt.Int16); + Assert.Equal(-123456789, dt.Int32); + Assert.Equal(-1234567890123456789L, dt.Int64); + Assert.Equal(-1.23456789, dt.Double); + Assert.Equal(-1234567890.01M, dt.Decimal); + Assert.Equal(DateTime.Parse("01/01/2000 12:34:56"), dt.DateTime); + Assert.Equal(new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)), dt.DateTimeOffset); + Assert.Equal(new TimeSpan(0, 10, 9, 8, 7), dt.TimeSpan); + Assert.Equal(-1.234F, dt.Single); + Assert.Equal(false, dt.Boolean); + Assert.Equal((byte)255, dt.Byte); + Assert.Equal(Enum64.SomeValue, dt.Enum64); + Assert.Equal(Enum32.SomeValue, dt.Enum32); + Assert.Equal(Enum16.SomeValue, dt.Enum16); + Assert.Equal(Enum8.SomeValue, dt.Enum8); + Assert.Equal((ushort)1234, dt.UnsignedInt16); + Assert.Equal(1234565789U, dt.UnsignedInt32); + Assert.Equal(1234567890123456789UL, dt.UnsignedInt64); + Assert.Equal('a', dt.Character); + Assert.Equal((sbyte)-128, dt.SignedByte); + Assert.Equal(EnumU64.SomeValue, dt.EnumU64); + Assert.Equal(EnumU32.SomeValue, dt.EnumU32); + Assert.Equal(EnumU16.SomeValue, dt.EnumU16); + Assert.Equal(EnumS8.SomeValue, dt.EnumS8); + } + } + + [Fact] + public virtual void Can_insert_and_read_back_non_nullable_backed_data_types() + { + using (var context = CreateContext()) + { + context.Set().Add( + new NonNullableBackedDataTypes + { + Id = 101, + PartitionId = 101, + Int16 = -1234, + Int32 = -123456789, + Int64 = -1234567890123456789L, + Double = -1.23456789, + Decimal = -1234567890.01M, + DateTime = DateTime.Parse("01/01/2000 12:34:56"), + DateTimeOffset = new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)), + TimeSpan = new TimeSpan(0, 10, 9, 8, 7), + Single = -1.234F, + Boolean = false, + Byte = 255, + UnsignedInt16 = 1234, + UnsignedInt32 = 1234565789U, + UnsignedInt64 = 1234567890123456789UL, + Character = 'a', + SignedByte = -128, + Enum64 = Enum64.SomeValue, + Enum32 = Enum32.SomeValue, + Enum16 = Enum16.SomeValue, + Enum8 = Enum8.SomeValue, + EnumU64 = EnumU64.SomeValue, + EnumU32 = EnumU32.SomeValue, + EnumU16 = EnumU16.SomeValue, + EnumS8 = EnumS8.SomeValue + }); + + Assert.Equal(1, context.SaveChanges()); + } + + using (var context = CreateContext()) + { + var dt = context.Set().Where(ndt => ndt.Id == 101).ToList().Single(); + + Assert.Equal((short)-1234, dt.Int16); + Assert.Equal(-123456789, dt.Int32); + Assert.Equal(-1234567890123456789L, dt.Int64); + Assert.Equal(-1.23456789, dt.Double); + Assert.Equal(-1234567890.01M, dt.Decimal); + Assert.Equal(DateTime.Parse("01/01/2000 12:34:56"), dt.DateTime); + Assert.Equal(new DateTimeOffset(DateTime.Parse("01/01/2000 12:34:56"), TimeSpan.FromHours(-8.0)), dt.DateTimeOffset); + Assert.Equal(new TimeSpan(0, 10, 9, 8, 7), dt.TimeSpan); + Assert.Equal(-1.234F, dt.Single); + Assert.Equal(false, dt.Boolean); + Assert.Equal((byte)255, dt.Byte); + Assert.Equal(Enum64.SomeValue, dt.Enum64); + Assert.Equal(Enum32.SomeValue, dt.Enum32); + Assert.Equal(Enum16.SomeValue, dt.Enum16); + Assert.Equal(Enum8.SomeValue, dt.Enum8); + Assert.Equal((ushort)1234, dt.UnsignedInt16); + Assert.Equal(1234565789U, dt.UnsignedInt32); + Assert.Equal(1234567890123456789UL, dt.UnsignedInt64); + Assert.Equal('a', dt.Character); + Assert.Equal((sbyte)-128, dt.SignedByte); + Assert.Equal(EnumU64.SomeValue, dt.EnumU64); + Assert.Equal(EnumU32.SomeValue, dt.EnumU32); + Assert.Equal(EnumU16.SomeValue, dt.EnumU16); + Assert.Equal(EnumS8.SomeValue, dt.EnumS8); + } + } + public abstract class BuiltInDataTypesFixtureBase : SharedStoreFixtureBase { protected override string StoreName { get; } = "BuiltInDataTypes"; @@ -1806,6 +2021,101 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con TemplateType = EmailTemplateType.PasswordResetRequest }); }); + + modelBuilder.Entity() + .HasData(new ObjectBackedDataTypes + { + Id = 13, + PartitionId = 1, + String = "string", + Bytes = new byte[] { 4, 20 }, + Int16 = -1234, + Int32 = -123456789, + Int64 = -1234567890123456789L, + Double = -1.23456789, + Decimal = -1234567890.01M, + DateTime = new DateTime(1973, 9,3), + DateTimeOffset = new DateTimeOffset(new DateTime(), TimeSpan.FromHours(-8.0)), + TimeSpan = new TimeSpan(0, 10, 9, 8, 7), + Single = -1.234F, + Boolean = true, + Byte = 255, + UnsignedInt16 = 1234, + UnsignedInt32 = 1234565789U, + UnsignedInt64 = 1234567890123456789UL, + Character = 'a', + SignedByte = -128, + Enum64 = Enum64.SomeValue, + Enum32 = Enum32.SomeValue, + Enum16 = Enum16.SomeValue, + Enum8 = Enum8.SomeValue, + EnumU64 = EnumU64.SomeValue, + EnumU32 = EnumU32.SomeValue, + EnumU16 = EnumU16.SomeValue, + EnumS8 = EnumS8.SomeValue + }); + + modelBuilder.Entity() + .HasData(new NullableBackedDataTypes + { + Id = 13, + PartitionId = 1, + Int16 = -1234, + Int32 = -123456789, + Int64 = -1234567890123456789L, + Double = -1.23456789, + Decimal = -1234567890.01M, + DateTime = new DateTime(1973, 9,3), + DateTimeOffset = new DateTimeOffset(new DateTime(), TimeSpan.FromHours(-8.0)), + TimeSpan = new TimeSpan(0, 10, 9, 8, 7), + Single = -1.234F, + Boolean = true, + Byte = 255, + UnsignedInt16 = 1234, + UnsignedInt32 = 1234565789U, + UnsignedInt64 = 1234567890123456789UL, + Character = 'a', + SignedByte = -128, + Enum64 = Enum64.SomeValue, + Enum32 = Enum32.SomeValue, + Enum16 = Enum16.SomeValue, + Enum8 = Enum8.SomeValue, + EnumU64 = EnumU64.SomeValue, + EnumU32 = EnumU32.SomeValue, + EnumU16 = EnumU16.SomeValue, + EnumS8 = EnumS8.SomeValue + }); + + modelBuilder.Entity() + .HasData(new NonNullableBackedDataTypes + { + Id = 13, + PartitionId = 1, + Int16 = -1234, + Int32 = -123456789, + Int64 = -1234567890123456789L, + Double = -1.23456789, + Decimal = -1234567890.01M, + DateTime = new DateTime(1973, 9,3), + DateTimeOffset = new DateTimeOffset(new DateTime(), TimeSpan.FromHours(-8.0)), + TimeSpan = new TimeSpan(0, 10, 9, 8, 7), + Single = -1.234F, + Boolean = true, + Byte = 255, + UnsignedInt16 = 1234, + UnsignedInt32 = 1234565789U, + UnsignedInt64 = 1234567890123456789UL, + Character = 'a', + SignedByte = -128, + Enum64 = Enum64.SomeValue, + Enum32 = Enum32.SomeValue, + Enum16 = Enum16.SomeValue, + Enum8 = Enum8.SomeValue, + EnumU64 = EnumU64.SomeValue, + EnumU32 = EnumU32.SomeValue, + EnumU16 = EnumU16.SomeValue, + EnumS8 = EnumS8.SomeValue + }); } protected static void MakeRequired(ModelBuilder modelBuilder) @@ -2024,5 +2334,547 @@ protected enum EmailTemplateTypeDto PasswordResetRequest = 0, EmailConfirmation = 1 } + + protected class ObjectBackedDataTypes + { + private object _string; + private object _bytes; + private object _int16; + private object _int32; + private object _int64; + private object _double; + private object _decimal; + private object _dateTime; + private object _dateTimeOffset; + private object _timeSpan; + private object _single; + private object _boolean; + private object _byte; + private object _unsignedInt16; + private object _unsignedInt32; + private object _unsignedInt64; + private object _character; + private object _signedByte; + private object _enum64; + private object _enum32; + private object _enum16; + private object _enum8; + private object _enumU64; + private object _enumU32; + private object _enumU16; + private object _enumS8; + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public int PartitionId { get; set; } + + public string String + { + get => (string)_string; + set => _string = value; + } + + public byte[] Bytes + { + get => (byte[])_bytes; + set => _bytes = value; + } + + public short Int16 + { + get => (short)_int16; + set => _int16 = value; + } + + public int Int32 + { + get => (int)_int32; + set => _int32 = value; + } + + public long Int64 + { + get => (long)_int64; + set => _int64 = value; + } + + public double Double + { + get => (double)_double; + set => _double = value; + } + + public decimal Decimal + { + get => (decimal)_decimal; + set => _decimal = value; + } + + public DateTime DateTime + { + get => (DateTime)_dateTime; + set => _dateTime = value; + } + + public DateTimeOffset DateTimeOffset + { + get => (DateTimeOffset)_dateTimeOffset; + set => _dateTimeOffset = value; + } + + public TimeSpan TimeSpan + { + get => (TimeSpan)_timeSpan; + set => _timeSpan = value; + } + + public float Single + { + get => (float)_single; + set => _single = value; + } + + public bool Boolean + { + get => (bool)_boolean; + set => _boolean = value; + } + + public byte Byte + { + get => (byte)_byte; + set => _byte = value; + } + + public ushort UnsignedInt16 + { + get => (ushort)_unsignedInt16; + set => _unsignedInt16 = value; + } + + public uint UnsignedInt32 + { + get => (uint)_unsignedInt32; + set => _unsignedInt32 = value; + } + + public ulong UnsignedInt64 + { + get => (ulong)_unsignedInt64; + set => _unsignedInt64 = value; + } + + public char Character + { + get => (char)_character; + set => _character = value; + } + + public sbyte SignedByte + { + get => (sbyte)_signedByte; + set => _signedByte = value; + } + + public Enum64 Enum64 + { + get => (Enum64)_enum64; + set => _enum64 = value; + } + + public Enum32 Enum32 + { + get => (Enum32)_enum32; + set => _enum32 = value; + } + + public Enum16 Enum16 + { + get => (Enum16)_enum16; + set => _enum16 = value; + } + + public Enum8 Enum8 + { + get => (Enum8)_enum8; + set => _enum8 = value; + } + + public EnumU64 EnumU64 + { + get => (EnumU64)_enumU64; + set => _enumU64 = value; + } + + public EnumU32 EnumU32 + { + get => (EnumU32)_enumU32; + set => _enumU32 = value; + } + + public EnumU16 EnumU16 + { + get => (EnumU16)_enumU16; + set => _enumU16 = value; + } + + public EnumS8 EnumS8 + { + get => (EnumS8)_enumS8; + set => _enumS8 = value; + } + } + + protected class NullableBackedDataTypes + { + private short? _int16; + private int? _int32; + private long? _int64; + private double? _double; + private decimal? _decimal; + private DateTime? _dateTime; + private DateTimeOffset? _dateTimeOffset; + private TimeSpan? _timeSpan; + private float? _single; + private bool? _boolean; + private byte? _byte; + private ushort? _unsignedInt16; + private uint? _unsignedInt32; + private ulong? _unsignedInt64; + private char? _character; + private sbyte? _signedByte; + private Enum64? _enum64; + private Enum32? _enum32; + private Enum16? _enum16; + private Enum8? _enum8; + private EnumU64? _enumU64; + private EnumU32? _enumU32; + private EnumU16? _enumU16; + private EnumS8? _enumS8; + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public int PartitionId { get; set; } + + public short Int16 + { + get => (short)_int16; + set => _int16 = value; + } + + public int Int32 + { + get => (int)_int32; + set => _int32 = value; + } + + public long Int64 + { + get => (long)_int64; + set => _int64 = value; + } + + public double Double + { + get => (double)_double; + set => _double = value; + } + + public decimal Decimal + { + get => (decimal)_decimal; + set => _decimal = value; + } + + public DateTime DateTime + { + get => (DateTime)_dateTime; + set => _dateTime = value; + } + + public DateTimeOffset DateTimeOffset + { + get => (DateTimeOffset)_dateTimeOffset; + set => _dateTimeOffset = value; + } + + public TimeSpan TimeSpan + { + get => (TimeSpan)_timeSpan; + set => _timeSpan = value; + } + + public float Single + { + get => (float)_single; + set => _single = value; + } + + public bool Boolean + { + get => (bool)_boolean; + set => _boolean = value; + } + + public byte Byte + { + get => (byte)_byte; + set => _byte = value; + } + + public ushort UnsignedInt16 + { + get => (ushort)_unsignedInt16; + set => _unsignedInt16 = value; + } + + public uint UnsignedInt32 + { + get => (uint)_unsignedInt32; + set => _unsignedInt32 = value; + } + + public ulong UnsignedInt64 + { + get => (ulong)_unsignedInt64; + set => _unsignedInt64 = value; + } + + public char Character + { + get => (char)_character; + set => _character = value; + } + + public sbyte SignedByte + { + get => (sbyte)_signedByte; + set => _signedByte = value; + } + + public Enum64 Enum64 + { + get => (Enum64)_enum64; + set => _enum64 = value; + } + + public Enum32 Enum32 + { + get => (Enum32)_enum32; + set => _enum32 = value; + } + + public Enum16 Enum16 + { + get => (Enum16)_enum16; + set => _enum16 = value; + } + + public Enum8 Enum8 + { + get => (Enum8)_enum8; + set => _enum8 = value; + } + + public EnumU64 EnumU64 + { + get => (EnumU64)_enumU64; + set => _enumU64 = value; + } + + public EnumU32 EnumU32 + { + get => (EnumU32)_enumU32; + set => _enumU32 = value; + } + + public EnumU16 EnumU16 + { + get => (EnumU16)_enumU16; + set => _enumU16 = value; + } + + public EnumS8 EnumS8 + { + get => (EnumS8)_enumS8; + set => _enumS8 = value; + } + } + + protected class NonNullableBackedDataTypes + { + private short _int16; + private int _int32; + private long _int64; + private double _double; + private decimal _decimal; + private DateTime _dateTime; + private DateTimeOffset _dateTimeOffset; + private TimeSpan _timeSpan; + private float _single; + private bool _boolean; + private byte _byte; + private ushort _unsignedInt16; + private uint _unsignedInt32; + private ulong _unsignedInt64; + private char _character; + private sbyte _signedByte; + private Enum64 _enum64; + private Enum32 _enum32; + private Enum16 _enum16; + private Enum8 _enum8; + private EnumU64 _enumU64; + private EnumU32 _enumU32; + private EnumU16 _enumU16; + private EnumS8 _enumS8; + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public int PartitionId { get; set; } + + public short? Int16 + { + get => _int16; + set => _int16 = (short)value; + } + + public int? Int32 + { + get => _int32; + set => _int32 = (int)value; + } + + public long? Int64 + { + get => _int64; + set => _int64 = (long)value; + } + + public double? Double + { + get => _double; + set => _double = (double)value; + } + + public decimal? Decimal + { + get => _decimal; + set => _decimal = (decimal)value; + } + + public DateTime? DateTime + { + get => _dateTime; + set => _dateTime = (DateTime)value; + } + + public DateTimeOffset? DateTimeOffset + { + get => _dateTimeOffset; + set => _dateTimeOffset = (DateTimeOffset)value; + } + + public TimeSpan? TimeSpan + { + get => _timeSpan; + set => _timeSpan = (TimeSpan)value; + } + + public float? Single + { + get => _single; + set => _single = (float)value; + } + + public bool? Boolean + { + get => _boolean; + set => _boolean = (bool)value; + } + + public byte? Byte + { + get => _byte; + set => _byte = (byte)value; + } + + public ushort? UnsignedInt16 + { + get => _unsignedInt16; + set => _unsignedInt16 = (ushort)value; + } + + public uint? UnsignedInt32 + { + get => _unsignedInt32; + set => _unsignedInt32 = (uint)value; + } + + public ulong? UnsignedInt64 + { + get => _unsignedInt64; + set => _unsignedInt64 = (ulong)value; + } + + public char? Character + { + get => _character; + set => _character = (char)value; + } + + public sbyte? SignedByte + { + get => _signedByte; + set => _signedByte = (sbyte)value; + } + + public Enum64? Enum64 + { + get => _enum64; + set => _enum64 = (Enum64)value; + } + + public Enum32? Enum32 + { + get => _enum32; + set => _enum32 = (Enum32)value; + } + + public Enum16? Enum16 + { + get => _enum16; + set => _enum16 = (Enum16)value; + } + + public Enum8? Enum8 + { + get => _enum8; + set => _enum8 = (Enum8)value; + } + + public EnumU64? EnumU64 + { + get => _enumU64; + set => _enumU64 = (EnumU64)value; + } + + public EnumU32? EnumU32 + { + get => _enumU32; + set => _enumU32 = (EnumU32)value; + } + + public EnumU16? EnumU16 + { + get => _enumU16; + set => _enumU16 = (EnumU16)value; + } + + public EnumS8? EnumS8 + { + get => _enumS8; + set => _enumS8 = (EnumS8)value; + } + } } } diff --git a/test/EFCore.Specification.Tests/ConvertToProviderTypesTestBase.cs b/test/EFCore.Specification.Tests/ConvertToProviderTypesTestBase.cs index 50d360cad74..982f76cf023 100644 --- a/test/EFCore.Specification.Tests/ConvertToProviderTypesTestBase.cs +++ b/test/EFCore.Specification.Tests/ConvertToProviderTypesTestBase.cs @@ -1,6 +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 Xunit; + namespace Microsoft.EntityFrameworkCore { public abstract class ConvertToProviderTypesTestBase : BuiltInDataTypesTestBase @@ -11,6 +13,36 @@ protected ConvertToProviderTypesTestBase(TFixture fixture) { } + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_data_type() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_data_type_shadow() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_data_type_nullable_shadow() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_nullable_data_type() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_nullable_data_type_as_literal() + { + base.Can_query_using_any_data_type(); + } + public abstract class ConvertToProviderTypesFixtureBase : BuiltInDataTypesFixtureBase { protected override string StoreName { get; } = "ConvertToProviderTypes"; diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index 1da9ce20444..54dd57c4d52 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -19,6 +19,42 @@ protected CustomConvertersTestBase(TFixture fixture) { } + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_data_type() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_data_type_shadow() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_data_type_nullable_shadow() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_nullable_data_type() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_using_any_nullable_data_type_as_literal() + { + base.Can_query_using_any_data_type(); + } + + [Fact(Skip = "QueryIssue")] + public override void Can_query_with_null_parameters_using_any_nullable_data_type() + { + base.Can_query_using_any_data_type(); + } + [Fact] public virtual void Can_query_and_update_with_nullable_converter_on_unique_index() { diff --git a/test/EFCore.Specification.Tests/FieldMappingTestBase.cs b/test/EFCore.Specification.Tests/FieldMappingTestBase.cs index abf2b7e09c2..ba8d90a4322 100644 --- a/test/EFCore.Specification.Tests/FieldMappingTestBase.cs +++ b/test/EFCore.Specification.Tests/FieldMappingTestBase.cs @@ -27,7 +27,22 @@ protected interface IUser2 protected class User2 : IUser2 { + private object _loginSessionId; + private object _loginSessionId2; + public int Id { get; set; } + + public int? LoginSessionId + { + get => (int?)_loginSessionId; + set => _loginSessionId = value; + } + + public int? LoginSessionId2 + { + get => (int?)_loginSessionId2; + set => _loginSessionId2 = value; + } } protected class LoginSession @@ -1883,7 +1898,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().Metadata.FindNavigation("Blog").SetField("_myblog"); modelBuilder.Entity().Metadata.FindNavigation("Posts").SetField("_myposts"); - modelBuilder.Entity().UsePropertyAccessMode(PropertyAccessMode.Field); + modelBuilder.Entity( + b => + { + b.UsePropertyAccessMode(PropertyAccessMode.Field); + + b.HasOne(e => e.User).WithOne().HasForeignKey(e => e.LoginSessionId); + b.HasMany(e => e.Users).WithOne().HasForeignKey(e => e.LoginSessionId2); + }); if (modelBuilder.Model.GetPropertyAccessMode() != PropertyAccessMode.Property) { diff --git a/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs b/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs index fbba62259d3..776b2462df7 100644 --- a/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs +++ b/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs @@ -1068,6 +1068,80 @@ public virtual void Fields_used_correctly_for_store_generated_values() }); } + [Fact] + public virtual void Nullable_fields_get_defaults_when_not_set() + { + ExecuteWithStrategyInTransaction( + context => + { + var entity = context.Add(new WithNullableBackingFields()).Entity; + + context.SaveChanges(); + + Assert.NotEqual(0, entity.Id); + Assert.True(entity.NullableBackedBool); + Assert.Equal(-1, entity.NullableBackedInt); + }, + context => + { + var entity = context.Set().Single(); + Assert.True(entity.NullableBackedBool); + Assert.Equal(-1, entity.NullableBackedInt); + }); + } + + [Fact] + public virtual void Nullable_fields_store_non_defaults_when_set() + { + ExecuteWithStrategyInTransaction( + context => + { + var entity = context.Add(new WithNullableBackingFields + { + NullableBackedBool = false, + NullableBackedInt = 0 + }).Entity; + + context.SaveChanges(); + + Assert.NotEqual(0, entity.Id); + Assert.False(entity.NullableBackedBool); + Assert.Equal(0, entity.NullableBackedInt); + }, + context => + { + var entity = context.Set().Single(); + Assert.False(entity.NullableBackedBool); + Assert.Equal(0, entity.NullableBackedInt); + }); + } + + [Fact] + public virtual void Nullable_fields_store_any_value_when_set() + { + ExecuteWithStrategyInTransaction( + context => + { + var entity = context.Add(new WithNullableBackingFields + { + NullableBackedBool = true, + NullableBackedInt = 3 + }).Entity; + + context.SaveChanges(); + + Assert.NotEqual(0, entity.Id); + Assert.True(entity.NullableBackedBool); + Assert.Equal(3, entity.NullableBackedInt); + }, + context => + { + var entity = context.Set().Single(); + Assert.True(entity.NullableBackedBool); + Assert.Equal(3, entity.NullableBackedInt); + }); + } + protected class Darwin { public int Id { get; set; } @@ -1148,13 +1222,12 @@ protected class WithBackingFields { private int _id; -#pragma warning disable RCS1085 // Use auto-implemented property. + // ReSharper disable once ConvertToAutoProperty public int Id { get => _id; set => _id = value; } -#pragma warning restore RCS1085 // Use auto-implemented property. private int? _nullableAsNonNullable = 0; @@ -1173,6 +1246,31 @@ public int? NonNullableAsNullable } } + protected class WithNullableBackingFields + { + private int? _id; + + public int Id + { + get => _id ?? 0; + set => _id = value; + } + + private bool? _nullableBackedBool; + public bool NullableBackedBool + { + get => _nullableBackedBool ?? false; + set => _nullableBackedBool = value; + } + + private int? _nullableBackedInt; + public int NullableBackedInt + { + get => _nullableBackedInt ?? 0; + set => _nullableBackedInt = value; + } + } + protected class WithConverter { public TKey Id { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs index 6e7efca1bd1..613ed19eedd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BuiltInDataTypesSqlServerTest.cs @@ -2368,7 +2368,9 @@ public virtual void Can_insert_and_read_back_all_mapped_data_types_with_precisio [ConditionalFact] public virtual void Columns_have_expected_data_types() { - var actual = QueryForColumnTypes(CreateContext()); + var actual = QueryForColumnTypes( + CreateContext(), + nameof(ObjectBackedDataTypes), nameof(NullableBackedDataTypes), nameof(NonNullableBackedDataTypes)); const string expected = @"BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable varbinary] [MaxLength = 900] BinaryForeignKeyDataType.Id ---> [int] [Precision = 10 Scale = 0] @@ -2805,7 +2807,7 @@ public void Can_get_column_types_from_built_model() } } - public static string QueryForColumnTypes(DbContext context) + public static string QueryForColumnTypes(DbContext context, params string[] tablesToIgnore) { const string query = @"SELECT @@ -2844,7 +2846,10 @@ const string query DateTimePrecision = reader.IsDBNull(7) ? null : (int?)reader.GetInt16(7) }; - columns.Add(columnInfo); + if (!tablesToIgnore.Contains(columnInfo.TableName)) + { + columns.Add(columnInfo); + } } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs index 5d5e8696cdf..a50177f30cf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ConvertToProviderTypesSqlServerTest.cs @@ -42,7 +42,9 @@ public virtual void Warning_when_suspicious_conversion_in_sql() [ConditionalFact] public virtual void Columns_have_expected_data_types() { - var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes(CreateContext()); + var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes( + CreateContext(), + nameof(ObjectBackedDataTypes), nameof(NullableBackedDataTypes), nameof(NonNullableBackedDataTypes)); const string expected = @"BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable nvarchar] [MaxLength = 450] BinaryForeignKeyDataType.Id ---> [int] [Precision = 10 Scale = 0] diff --git a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs index 6aeec85c6f6..235631e358c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CustomConvertersSqlServerTest.cs @@ -21,7 +21,9 @@ public CustomConvertersSqlServerTest(CustomConvertersSqlServerFixture fixture) [ConditionalFact] public virtual void Columns_have_expected_data_types() { - var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes(CreateContext()); + var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes( + CreateContext(), + nameof(ObjectBackedDataTypes), nameof(NullableBackedDataTypes), nameof(NonNullableBackedDataTypes)); const string expected = @"BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable varbinary] [MaxLength = 900] BinaryForeignKeyDataType.Id ---> [int] [Precision = 10 Scale = 0] diff --git a/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs index fd529d2bf05..499a87b9a9c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EverythingIsBytesSqlServerTest.cs @@ -25,7 +25,9 @@ public EverythingIsBytesSqlServerTest(EverythingIsBytesSqlServerFixture fixture) [ConditionalFact] public virtual void Columns_have_expected_data_types() { - var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes(CreateContext()); + var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes( + CreateContext(), + nameof(ObjectBackedDataTypes), nameof(NullableBackedDataTypes), nameof(NonNullableBackedDataTypes)); const string expected = @"BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable varbinary] [MaxLength = 900] BinaryForeignKeyDataType.Id ---> [varbinary] [MaxLength = 4] @@ -243,29 +245,26 @@ private RelationalTypeMapping FindRawMapping(RelationalTypeMappingInfo mappingIn } } - if (clrType != null) + if (clrType == typeof(byte[])) { - if (clrType == typeof(byte[])) + if (mappingInfo.IsRowVersion == true) { - if (mappingInfo.IsRowVersion == true) - { - return _rowversion; - } - - var isFixedLength = mappingInfo.IsFixedLength == true; + return _rowversion; + } - var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? (int?)900 : null); - if (size > 8000) - { - size = isFixedLength ? 8000 : (int?)null; - } + var isFixedLength = mappingInfo.IsFixedLength == true; - return new SqlServerByteArrayTypeMapping( - "varbinary(" + (size == null ? "max" : size.ToString()) + ")", - size, - isFixedLength, - storeTypePostfix: size == null ? StoreTypePostfix.None : (StoreTypePostfix?)null); + var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? (int?)900 : null); + if (size > 8000) + { + size = isFixedLength ? 8000 : (int?)null; } + + return new SqlServerByteArrayTypeMapping( + "varbinary(" + (size == null ? "max" : size.ToString()) + ")", + size, + isFixedLength, + storeTypePostfix: size == null ? StoreTypePostfix.None : (StoreTypePostfix?)null); } return null; diff --git a/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs index 222b5a2e548..9a50a0fa610 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EverythingIsStringsSqlServerTest.cs @@ -26,7 +26,9 @@ public EverythingIsStringsSqlServerTest(EverythingIsStringsSqlServerFixture fixt [ConditionalFact] public virtual void Columns_have_expected_data_types() { - var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes(CreateContext()); + var actual = BuiltInDataTypesSqlServerTest.QueryForColumnTypes( + CreateContext(), + nameof(ObjectBackedDataTypes), nameof(NullableBackedDataTypes), nameof(NonNullableBackedDataTypes)); const string expected = @"BinaryForeignKeyDataType.BinaryKeyDataTypeId ---> [nullable nvarchar] [MaxLength = 450] BinaryForeignKeyDataType.Id ---> [nvarchar] [MaxLength = 64] diff --git a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs index 655e81cf3c3..8824422e767 100644 --- a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs @@ -163,6 +163,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.NonNullableAsNullable).HasComputedColumnSql("1"); }); + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableBackedBool).HasDefaultValue(true); + b.Property(e => e.NullableBackedInt).HasDefaultValue(-1); + }); + base.OnModelCreating(modelBuilder, context); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs index 75df70f4842..abf1c2b8bbd 100644 --- a/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/StoreGeneratedSqliteTest.cs @@ -82,6 +82,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property(e => e.OnUpdateThrowBeforeThrowAfter).HasDefaultValue("Rabbit"); }); + modelBuilder.Entity( + b => + { + b.Property(e => e.NullableBackedBool).HasDefaultValue(true); + b.Property(e => e.NullableBackedInt).HasDefaultValue(-1); + }); + base.OnModelCreating(modelBuilder, context); } } diff --git a/test/EFCore.Tests/ChangeTracking/PropertyEntryTest.cs b/test/EFCore.Tests/ChangeTracking/PropertyEntryTest.cs index 0a7aa0dea92..e728e1ccced 100644 --- a/test/EFCore.Tests/ChangeTracking/PropertyEntryTest.cs +++ b/test/EFCore.Tests/ChangeTracking/PropertyEntryTest.cs @@ -6,7 +6,6 @@ using System.Runtime.CompilerServices; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -195,12 +194,19 @@ public void Setting_IsModified_is_not_reset_by_OriginalValues() [Fact] public void Can_get_name() + => Can_get_name(); + + [Fact] + public void Can_get_name_with_object_field() + => Can_get_name(); + + private void Can_get_name() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { var entry = context .Entry( - new Wotty + new TWotty { Id = 1, Primate = "Monkey", @@ -216,12 +222,19 @@ public void Can_get_name() [Fact] public void Can_get_current_value() + => Can_get_current_value(); + + [Fact] + public void Can_get_current_value_with_object_field() + => Can_get_current_value(); + + private void Can_get_current_value() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { var entry = context .Entry( - new Wotty + new TWotty { Id = 1, Primate = "Monkey", @@ -238,10 +251,17 @@ public void Can_get_current_value() [Fact] public void Can_set_current_value() + => Can_set_current_value(); + + [Fact] + public void Can_set_current_value_with_object_field() + => Can_set_current_value(); + + private void Can_set_current_value() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey", @@ -265,10 +285,17 @@ public void Can_set_current_value() [Fact] public void Can_set_current_value_to_null() + => Can_set_current_value_to_null(); + + [Fact] + public void Can_set_current_value_to_null_with_object_field() + => Can_set_current_value_to_null(); + + private void Can_set_current_value_to_null() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey", @@ -292,10 +319,17 @@ public void Can_set_current_value_to_null() [Fact] public void Can_set_and_get_original_value() + => Can_set_and_get_original_value(); + + [Fact] + public void Can_set_and_get_original_value_with_object_field() + => Can_set_and_get_original_value(); + + private void Can_set_and_get_original_value() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey", @@ -328,10 +362,17 @@ public void Can_set_and_get_original_value() [Fact] public void Can_set_and_get_original_value_starting_null() + => Can_set_and_get_original_value_starting_null(); + + [Fact] + public void Can_set_and_get_original_value_starting_null_with_object_field() + => Can_set_and_get_original_value_starting_null(); + + private void Can_set_and_get_original_value_starting_null() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1 }; @@ -362,10 +403,17 @@ public void Can_set_and_get_original_value_starting_null() [Fact] public void Can_set_original_value_to_null() + => Can_set_original_value_to_null(); + + [Fact] + public void Can_set_original_value_to_null_with_object_field() + => Can_set_original_value_to_null(); + + private void Can_set_original_value_to_null() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey", @@ -389,10 +437,17 @@ public void Can_set_original_value_to_null() [Fact] public void Can_set_and_clear_modified_on_Modified_entity() + => Can_set_and_clear_modified_on_Modified_entity(); + + [Fact] + public void Can_set_and_clear_modified_on_Modified_entity_with_object_field() + => Can_set_and_clear_modified_on_Modified_entity(); + + private void Can_set_and_clear_modified_on_Modified_entity() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1 }; @@ -435,10 +490,19 @@ public void Can_set_and_clear_modified_on_Modified_entity() [InlineData(EntityState.Added)] [InlineData(EntityState.Deleted)] public void Can_set_and_clear_modified_on_Added_or_Deleted_entity(EntityState initialState) + => Can_set_and_clear_modified_on_Added_or_Deleted_entity(initialState); + + [Theory] + [InlineData(EntityState.Added)] + [InlineData(EntityState.Deleted)] + public void Can_set_and_clear_modified_on_Added_or_Deleted_entity_with_object_field(EntityState initialState) + => Can_set_and_clear_modified_on_Added_or_Deleted_entity(initialState); + + private void Can_set_and_clear_modified_on_Added_or_Deleted_entity(EntityState initialState) where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1 }; @@ -472,10 +536,19 @@ public void Can_set_and_clear_modified_on_Added_or_Deleted_entity(EntityState in [InlineData(EntityState.Detached)] [InlineData(EntityState.Unchanged)] public void Can_set_and_clear_modified_on_Unchanged_or_Detached_entity(EntityState initialState) + => Can_set_and_clear_modified_on_Unchanged_or_Detached_entity(initialState); + + [Theory] + [InlineData(EntityState.Detached)] + [InlineData(EntityState.Unchanged)] + public void Can_set_and_clear_modified_on_Unchanged_or_Detached_entity_with_object_field(EntityState initialState) + => Can_set_and_clear_modified_on_Unchanged_or_Detached_entity(initialState); + + private void Can_set_and_clear_modified_on_Unchanged_or_Detached_entity(EntityState initialState) where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1 }; @@ -507,10 +580,17 @@ public void Can_set_and_clear_modified_on_Unchanged_or_Detached_entity(EntitySta [Fact] public void Can_reject_changes_when_clearing_modified_flag() + => Can_reject_changes_when_clearing_modified_flag(); + + [Fact] + public void Can_reject_changes_when_clearing_modified_flag_with_object_field() + => Can_reject_changes_when_clearing_modified_flag(); + + private void Can_reject_changes_when_clearing_modified_flag() where TWotty : IWotty, new() { using (var context = new PrimateContext()) { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey", @@ -592,11 +672,18 @@ public void Can_reject_changes_when_clearing_modified_flag() [Fact] public void Can_get_name_generic() + => Can_get_name_generic(); + + [Fact] + public void Can_get_name_generic_with_object_field() + => Can_get_name_generic(); + + private void Can_get_name_generic() where TWotty : class, IWotty, new() { var entry = InMemoryTestHelpers.Instance.CreateInternalEntry( BuildModel(), EntityState.Unchanged, - new Wotty + new TWotty { Id = 1, Primate = "Monkey" @@ -607,11 +694,18 @@ public void Can_get_name_generic() [Fact] public void Can_get_current_value_generic() + => Can_get_current_value_generic(); + + [Fact] + public void Can_get_current_value_generic_with_object_field() + => Can_get_current_value_generic(); + + private void Can_get_current_value_generic() where TWotty : class, IWotty, new() { var entry = InMemoryTestHelpers.Instance.CreateInternalEntry( BuildModel(), EntityState.Unchanged, - new Wotty + new TWotty { Id = 1, Primate = "Monkey" @@ -622,8 +716,15 @@ public void Can_get_current_value_generic() [Fact] public void Can_set_current_value_generic() + => Can_set_current_value_generic(); + + [Fact] + public void Can_set_current_value_generic_with_object_field() + => Can_set_current_value_generic(); + + private void Can_set_current_value_generic() where TWotty : class, IWotty, new() { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey" @@ -641,8 +742,15 @@ public void Can_set_current_value_generic() [Fact] public void Can_set_current_value_to_null_generic() + => Can_set_current_value_to_null_generic(); + + [Fact] + public void Can_set_current_value_to_null_generic_with_object_field() + => Can_set_current_value_to_null_generic(); + + private void Can_set_current_value_to_null_generic() where TWotty : class, IWotty, new() { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey" @@ -660,8 +768,15 @@ public void Can_set_current_value_to_null_generic() [Fact] public void Can_set_and_get_original_value_generic() + => Can_set_and_get_original_value_generic(); + + [Fact] + public void Can_set_and_get_original_value_generic_with_object_field() + => Can_set_and_get_original_value_generic(); + + private void Can_set_and_get_original_value_generic() where TWotty : class, IWotty, new() { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey" @@ -682,11 +797,18 @@ public void Can_set_and_get_original_value_generic() [Fact] public void Can_set_original_value_to_null_generic() + => Can_set_original_value_to_null_generic(); + + [Fact] + public void Can_set_original_value_to_null_generic_with_object_field() + => Can_set_original_value_to_null_generic(); + + private void Can_set_original_value_to_null_generic() where TWotty : class, IWotty, new() { var entry = InMemoryTestHelpers.Instance.CreateInternalEntry( BuildModel(), EntityState.Unchanged, - new Wotty + new TWotty { Id = 1, Primate = "Monkey" @@ -699,8 +821,15 @@ public void Can_set_original_value_to_null_generic() [Fact] public void Can_set_and_clear_modified_generic() + => Can_set_and_clear_modified_generic(); + + [Fact] + public void Can_set_and_clear_modified_generic_with_object_field() + => Can_set_and_clear_modified_generic(); + + private void Can_set_and_clear_modified_generic() where TWotty : class, IWotty, new() { - var entity = new Wotty + var entity = new TWotty { Id = 1, Primate = "Monkey" @@ -972,7 +1101,47 @@ public void Can_set_or_get_original_value_when_property_explicitly_marked_to_be_ Assert.Equal("Monkey", entity.Primate); } - private class Wotty + private interface IWotty + { + int Id { get; set; } + string Primate { get; set; } + string RequiredPrimate { get; set; } + string Marmate { get; set; } + } + + private class ObjectWotty : IWotty + { + private object _id; + private object _primate; + private object _requiredPrimate; + private object _marmate; + + public int Id + { + get => (int)_id; + set => _id = value; + } + + public string Primate + { + get => (string)_primate; + set => _primate = value; + } + + public string RequiredPrimate + { + get => (string)_requiredPrimate; + set => _requiredPrimate = value; + } + + public string Marmate + { + get => (string)_marmate; + set => _marmate = value; + } + } + + private class Wotty : IWotty { public int Id { get; set; } public string Primate { get; set; } @@ -1096,6 +1265,13 @@ public static IMutableModel BuildModel( b.HasChangeTrackingStrategy(ChangeTrackingStrategy.Snapshot); }); + builder.Entity( + b => + { + b.Property(e => e.RequiredPrimate).IsRequired(); + b.HasChangeTrackingStrategy(ChangeTrackingStrategy.Snapshot); + }); + builder.Entity( b => b.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications)); diff --git a/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs index b3d54a058c3..181e8d8b762 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrCollectionAccessorFactoryTest.cs @@ -43,6 +43,7 @@ private class FakeNavigation : INavigation, IClrCollectionAccessor public string Name { get; } public ITypeBase DeclaringType { get; } public Type ClrType { get; } + public Type DeclaredClrType { get; } public PropertyInfo PropertyInfo { get; } public FieldInfo FieldInfo { get; } public IEntityType DeclaringEntityType { get; } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index 97826693fbb..e07b3ce81bf 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -30,9 +30,9 @@ private class FakeProperty : IProperty, IClrPropertyGetter public string Name { get; } public ITypeBase DeclaringType { get; } public Type ClrType { get; } + public Type DeclaredClrType { get; } public IEntityType DeclaringEntityType { get; } public bool IsNullable { get; } - public bool IsStoreGeneratedAlways { get; } public ValueGenerated ValueGenerated { get; } public bool IsConcurrencyToken { get; } public PropertyInfo PropertyInfo { get; } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs index 77bb5a54342..97a318d1afc 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs @@ -31,11 +31,9 @@ private class FakeProperty : IProperty, IClrPropertySetter public string Name { get; } public ITypeBase DeclaringType { get; } public Type ClrType { get; } + public Type DeclaredClrType { get; } public IEntityType DeclaringEntityType { get; } public bool IsNullable { get; } - public bool IsReadOnlyBeforeSave { get; } - public bool IsReadOnlyAfterSave { get; } - public bool IsStoreGeneratedAlways { get; } public ValueGenerated ValueGenerated { get; } public bool IsConcurrencyToken { get; } public PropertyInfo PropertyInfo { get; } diff --git a/test/EFCore.Tests/Metadata/Internal/NavigationTest.cs b/test/EFCore.Tests/Metadata/Internal/NavigationTest.cs index 73952c35c3f..26d4c98dac2 100644 --- a/test/EFCore.Tests/Metadata/Internal/NavigationTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/NavigationTest.cs @@ -30,6 +30,7 @@ private class FakeNavigation : INavigation public string Name { get; } public ITypeBase DeclaringType { get; } public Type ClrType { get; } + public Type DeclaredClrType { get; } public PropertyInfo PropertyInfo { get; } public FieldInfo FieldInfo { get; } public IEntityType DeclaringEntityType { get; } diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs index fe26618a084..f336bd32cc5 100644 --- a/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs @@ -46,9 +46,9 @@ private class FakeProperty : IProperty public string Name { get; } public ITypeBase DeclaringType { get; } public Type ClrType { get; } + public Type DeclaredClrType { get; } public IEntityType DeclaringEntityType { get; } public bool IsNullable { get; } - public bool IsStoreGeneratedAlways { get; } public ValueGenerated ValueGenerated { get; } public bool IsConcurrencyToken { get; } public PropertyInfo PropertyInfo { get; } diff --git a/test/EFCore.Tests/Storage/EnumToNumberConverterTest.cs b/test/EFCore.Tests/Storage/EnumToNumberConverterTest.cs index 404aef6afff..654f507381b 100644 --- a/test/EFCore.Tests/Storage/EnumToNumberConverterTest.cs +++ b/test/EFCore.Tests/Storage/EnumToNumberConverterTest.cs @@ -289,7 +289,7 @@ public void Enum_to_integer_converter_throws_for_bad_types() CoreStrings.ConverterBadType( typeof(EnumToNumberConverter).ShortDisplayName(), "Guid", - "int, long, short, byte, uint, ulong, ushort, sbyte, double, float, decimal"), + "int, long, short, byte, uint, ulong, ushort, sbyte, double, float, decimal, char"), Assert.Throws( () => new EnumToNumberConverter()).Message); } diff --git a/test/EFCore.Tests/Storage/ValueConverterSelectorTest.cs b/test/EFCore.Tests/Storage/ValueConverterSelectorTest.cs index 9d491307e3e..40f137c0fcd 100644 --- a/test/EFCore.Tests/Storage/ValueConverterSelectorTest.cs +++ b/test/EFCore.Tests/Storage/ValueConverterSelectorTest.cs @@ -32,7 +32,8 @@ public void Can_get_converters_for_int_enums() (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), - (typeof(EnumToNumberConverter), default)); + (typeof(EnumToNumberConverter), default), + (typeof(EnumToNumberConverter), default)); } [Fact] @@ -52,7 +53,8 @@ public void Can_get_converters_for_ulong_enums() (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), - (typeof(EnumToNumberConverter), default)); + (typeof(EnumToNumberConverter), default), + (typeof(EnumToNumberConverter), default)); } [Fact] @@ -72,7 +74,8 @@ public void Can_get_converters_for_long_enums() (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), - (typeof(EnumToNumberConverter), default)); + (typeof(EnumToNumberConverter), default), + (typeof(EnumToNumberConverter), default)); } [Fact] @@ -92,7 +95,8 @@ public void Can_get_converters_for_byte_enums() (typeof(CompositeValueConverter), new ConverterMappingHints(size: 1)), (typeof(EnumToNumberConverter), default), (typeof(EnumToNumberConverter), default), - (typeof(EnumToNumberConverter), default)); + (typeof(EnumToNumberConverter), default), + (typeof(EnumToNumberConverter), default)); } [Fact] @@ -143,7 +147,8 @@ public void Can_get_converters_for_int() (typeof(CastingConverter), default), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -162,7 +167,8 @@ public void Can_get_converters_for_uint() (typeof(CastingConverter), default), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -181,7 +187,8 @@ public void Can_get_converters_for_sbyte() (typeof(CastingConverter), default), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -200,7 +207,8 @@ public void Can_get_converters_for_byte() (typeof(NumberToBytesConverter), new ConverterMappingHints(size: 1)), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -219,7 +227,8 @@ public void Can_get_converters_for_double() (typeof(CastingConverter), default), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -238,7 +247,8 @@ public void Can_get_converters_for_float() (typeof(CastingConverter), default), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -257,7 +267,8 @@ public void Can_get_converters_for_decimal() (typeof(CastingConverter), default), (typeof(CastingConverter), default), (typeof(CastingConverter), default), - (typeof(CastingConverter), default)); + (typeof(CastingConverter), default), + (typeof(CastingConverter), default)); } [Fact] @@ -366,6 +377,7 @@ public void Can_get_converters_for_bool() (typeof(BoolToZeroOneConverter), default), (typeof(BoolToZeroOneConverter), default), (typeof(BoolToZeroOneConverter), default), + (typeof(BoolToZeroOneConverter), default), (typeof(BoolToStringConverter), new ConverterMappingHints(size: 1)), (typeof(CompositeValueConverter), new ConverterMappingHints(size: 1))); }