From e2506493b80f03555be697824b4a9907b1d73c88 Mon Sep 17 00:00:00 2001 From: AndriySvyryd Date: Sat, 14 Aug 2021 11:48:37 -0700 Subject: [PATCH] Split out unmapped scalars configuration --- .../Query/Internal/ISqlExpressionFactory.cs | 11 +- .../Query/Internal/InExpression.cs | 3 +- .../Query/Internal/SqlExpressionFactory.cs | 58 +++--- .../Design/Internal/DatabaseOperations.cs | 5 +- ...ropertiesConfigurationBuilderExtensions.cs | 2 +- .../Extensions/RelationalModelExtensions.cs | 40 ---- .../ScalarConfigurationBuilderExtensions.cs | 77 ++++++++ ...lExpressionSimplifyingExpressionVisitor.cs | 2 + ...lationalSqlTranslatingExpressionVisitor.cs | 2 +- .../Query/SqlExpressionFactory.cs | 26 ++- .../Query/SqlExpressionFactoryDependencies.cs | 3 - .../Storage/RelationalTypeMappingSource.cs | 123 ++++-------- .../RelationalTypeMappingSourceExtensions.cs | 42 ++++- .../Builders/ScalarConfigurationBuilder.cs | 177 ++++++++++++++++++ .../Builders/ScalarConfigurationBuilder`.cs | 98 ++++++++++ .../Conventions/RuntimeModelConvention.cs | 17 +- src/EFCore/Metadata/IModel.cs | 14 +- ...uration.cs => IScalarTypeConfiguration.cs} | 4 +- src/EFCore/Metadata/Internal/Model.cs | 17 +- .../Metadata/Internal/ModelConfiguration.cs | 114 ++++++++--- .../Internal/PropertyConfiguration.cs | 2 +- src/EFCore/Metadata/RuntimeModel.cs | 22 +-- ...n.cs => RuntimeScalarTypeConfiguration.cs} | 16 +- src/EFCore/ModelConfigurationBuilder.cs | 68 ++++++- src/EFCore/Storage/TypeMappingSource.cs | 78 ++------ src/Shared/SharedTypeExtensions.cs | 10 +- .../CSharpRuntimeModelCodeGeneratorTest.cs | 13 +- .../Storage/RelationalTypeMapperTestBase.cs | 18 +- .../ValueConvertersEndToEndTestBase.cs | 8 - .../ValueConvertersEndToEndSqlServerTest.cs | 4 +- .../ModelValidatorTest.PropertyMapping.cs | 2 +- .../ModelBuilding/NonRelationshipTestBase.cs | 110 ++++++++--- 32 files changed, 782 insertions(+), 404 deletions(-) create mode 100644 src/EFCore.Relational/Extensions/ScalarConfigurationBuilderExtensions.cs create mode 100644 src/EFCore/Metadata/Builders/ScalarConfigurationBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/ScalarConfigurationBuilder`.cs rename src/EFCore/Metadata/{IPropertyTypeConfiguration.cs => IScalarTypeConfiguration.cs} (94%) rename src/EFCore/Metadata/{RuntimePropertyTypeConfiguration.cs => RuntimeScalarTypeConfiguration.cs} (80%) diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs index 90ff147664f..9652f0e02dd 100644 --- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; -#nullable disable warnings - namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal { /// @@ -25,7 +22,8 @@ public interface ISqlExpressionFactory /// 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. /// - SqlExpression ApplyTypeMapping(SqlExpression? sqlExpression, CoreTypeMapping typeMapping); + [return: NotNullIfNotNull("sqlExpression")] + SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, CoreTypeMapping? typeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,7 +31,8 @@ public interface ISqlExpressionFactory /// 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. /// - SqlExpression ApplyDefaultTypeMapping(SqlExpression? sqlExpression); + [return: NotNullIfNotNull("sqlExpression")] + SqlExpression? ApplyDefaultTypeMapping(SqlExpression? sqlExpression); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Cosmos/Query/Internal/InExpression.cs b/src/EFCore.Cosmos/Query/Internal/InExpression.cs index 5cf2a6d1e7e..6912c28b612 100644 --- a/src/EFCore.Cosmos/Query/Internal/InExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/InExpression.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; @@ -27,7 +26,7 @@ public InExpression( SqlExpression item, bool negated, SqlExpression values, - CoreTypeMapping typeMapping) + CoreTypeMapping? typeMapping) : base(typeof(bool), typeMapping) { Item = item; diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index 31b934b7352..b7420e92756 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -12,8 +10,6 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Storage; -#nullable disable - namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal { /// @@ -38,7 +34,7 @@ public SqlExpressionFactory(ITypeMappingSource typeMappingSource, IModel model) { _typeMappingSource = typeMappingSource; _model = model; - _boolTypeMapping = typeMappingSource.FindMapping(typeof(bool)); + _boolTypeMapping = typeMappingSource.FindMapping(typeof(bool), model)!; } /// @@ -47,11 +43,12 @@ public SqlExpressionFactory(ITypeMappingSource typeMappingSource, IModel model) /// 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 SqlExpression ApplyDefaultTypeMapping(SqlExpression sqlExpression) + [return: NotNullIfNotNull("sqlExpression")] + public virtual SqlExpression? ApplyDefaultTypeMapping(SqlExpression? sqlExpression) => sqlExpression == null || sqlExpression.TypeMapping != null ? sqlExpression - : ApplyTypeMapping(sqlExpression, _model.FindMapping(sqlExpression.Type)); + : ApplyTypeMapping(sqlExpression, _typeMappingSource.FindMapping(sqlExpression.Type, _model)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -59,7 +56,8 @@ public virtual SqlExpression ApplyDefaultTypeMapping(SqlExpression sqlExpression /// 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 SqlExpression ApplyTypeMapping(SqlExpression sqlExpression, CoreTypeMapping typeMapping) + [return: NotNullIfNotNull("sqlExpression")] + public virtual SqlExpression? ApplyTypeMapping(SqlExpression? sqlExpression, CoreTypeMapping? typeMapping) { if (sqlExpression == null || sqlExpression.TypeMapping != null) @@ -96,7 +94,7 @@ public virtual SqlExpression ApplyTypeMapping(SqlExpression sqlExpression, CoreT private SqlExpression ApplyTypeMappingOnSqlConditional( SqlConditionalExpression sqlConditionalExpression, - CoreTypeMapping typeMapping) + CoreTypeMapping? typeMapping) { return sqlConditionalExpression.Update( sqlConditionalExpression.Test, @@ -106,11 +104,11 @@ private SqlExpression ApplyTypeMappingOnSqlConditional( private SqlExpression ApplyTypeMappingOnSqlUnary( SqlUnaryExpression sqlUnaryExpression, - CoreTypeMapping typeMapping) + CoreTypeMapping? typeMapping) { SqlExpression operand; Type resultType; - CoreTypeMapping resultTypeMapping; + CoreTypeMapping? resultTypeMapping; switch (sqlUnaryExpression.OperatorType) { case ExpressionType.Equal: @@ -150,14 +148,14 @@ when sqlUnaryExpression.IsLogicalNot(): private SqlExpression ApplyTypeMappingOnSqlBinary( SqlBinaryExpression sqlBinaryExpression, - CoreTypeMapping typeMapping) + CoreTypeMapping? typeMapping) { var left = sqlBinaryExpression.Left; var right = sqlBinaryExpression.Right; Type resultType; - CoreTypeMapping resultTypeMapping; - CoreTypeMapping inferredTypeMapping; + CoreTypeMapping? resultTypeMapping; + CoreTypeMapping? inferredTypeMapping; switch (sqlBinaryExpression.OperatorType) { case ExpressionType.Equal: @@ -170,8 +168,8 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right) // We avoid object here since the result does not get typeMapping from outside. ?? (left.Type != typeof(object) - ? _model.FindMapping(left.Type) - : _model.FindMapping(right.Type)); + ? _typeMappingSource.FindMapping(left.Type, _model) + : _typeMappingSource.FindMapping(right.Type, _model)); resultType = typeof(bool); resultTypeMapping = _boolTypeMapping; } @@ -226,7 +224,7 @@ public virtual SqlBinaryExpression MakeBinary( ExpressionType operatorType, SqlExpression left, SqlExpression right, - CoreTypeMapping typeMapping) + CoreTypeMapping? typeMapping) { var returnType = left.Type; switch (operatorType) @@ -325,7 +323,7 @@ public virtual SqlBinaryExpression OrElse(SqlExpression left, SqlExpression righ /// 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 SqlBinaryExpression Add(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression Add(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Add, left, right, typeMapping); /// @@ -334,7 +332,7 @@ public virtual SqlBinaryExpression Add(SqlExpression left, SqlExpression right, /// 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 SqlBinaryExpression Subtract(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression Subtract(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Subtract, left, right, typeMapping); /// @@ -343,7 +341,7 @@ public virtual SqlBinaryExpression Subtract(SqlExpression left, SqlExpression ri /// 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 SqlBinaryExpression Multiply(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression Multiply(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Multiply, left, right, typeMapping); /// @@ -352,7 +350,7 @@ public virtual SqlBinaryExpression Multiply(SqlExpression left, SqlExpression ri /// 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 SqlBinaryExpression Divide(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression Divide(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Divide, left, right, typeMapping); /// @@ -361,7 +359,7 @@ public virtual SqlBinaryExpression Divide(SqlExpression left, SqlExpression righ /// 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 SqlBinaryExpression Modulo(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression Modulo(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Modulo, left, right, typeMapping); /// @@ -370,7 +368,7 @@ public virtual SqlBinaryExpression Modulo(SqlExpression left, SqlExpression righ /// 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 SqlBinaryExpression And(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression And(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.And, left, right, typeMapping); /// @@ -379,14 +377,14 @@ public virtual SqlBinaryExpression And(SqlExpression left, SqlExpression right, /// 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 SqlBinaryExpression Or(SqlExpression left, SqlExpression right, CoreTypeMapping typeMapping = null) + public virtual SqlBinaryExpression Or(SqlExpression left, SqlExpression right, CoreTypeMapping? typeMapping = null) => MakeBinary(ExpressionType.Or, left, right, typeMapping); private SqlUnaryExpression MakeUnary( ExpressionType operatorType, SqlExpression operand, Type type, - CoreTypeMapping typeMapping = null) + CoreTypeMapping? typeMapping = null) { return (SqlUnaryExpression)ApplyTypeMapping(new SqlUnaryExpression(operatorType, operand, type, null), typeMapping); } @@ -415,7 +413,7 @@ public virtual SqlBinaryExpression IsNotNull(SqlExpression operand) /// 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 SqlUnaryExpression Convert(SqlExpression operand, Type type, CoreTypeMapping typeMapping = null) + public virtual SqlUnaryExpression Convert(SqlExpression operand, Type type, CoreTypeMapping? typeMapping = null) => MakeUnary(ExpressionType.Convert, operand, type, typeMapping); /// @@ -446,7 +444,7 @@ public virtual SqlFunctionExpression Function( string functionName, IEnumerable arguments, Type returnType, - CoreTypeMapping typeMapping = null) + CoreTypeMapping? typeMapping = null) { var typeMappedArguments = new List(); @@ -486,7 +484,7 @@ public virtual SqlConditionalExpression Condition(SqlExpression test, SqlExpress /// public virtual InExpression In(SqlExpression item, SqlExpression values, bool negated) { - var typeMapping = item.TypeMapping ?? _model.FindMapping(item.Type); + var typeMapping = item.TypeMapping ?? _typeMappingSource.FindMapping(item.Type, _model); item = ApplyTypeMapping(item, typeMapping); values = ApplyTypeMapping(values, typeMapping); @@ -500,7 +498,7 @@ public virtual InExpression In(SqlExpression item, SqlExpression values, bool ne /// 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 SqlConstantExpression Constant(object value, CoreTypeMapping typeMapping = null) + public virtual SqlConstantExpression Constant(object? value, CoreTypeMapping? typeMapping = null) => new(Expression.Constant(value), typeMapping); /// diff --git a/src/EFCore.Design/Design/Internal/DatabaseOperations.cs b/src/EFCore.Design/Design/Internal/DatabaseOperations.cs index 9d882296e59..f6237286f96 100644 --- a/src/EFCore.Design/Design/Internal/DatabaseOperations.cs +++ b/src/EFCore.Design/Design/Internal/DatabaseOperations.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.IO; using System.Reflection; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.EntityFrameworkCore.Utilities; @@ -93,7 +90,7 @@ public virtual SavedModelFiles ScaffoldContext( : outputDir; var services = _servicesBuilder.Build(provider); - using var scope = services.CreateScope(); + using var scope = services.CreateScope(); var scaffolder = scope.ServiceProvider.GetRequiredService(); diff --git a/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs b/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs index d2d8f5165af..9fb0d029fe1 100644 --- a/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/PropertiesConfigurationBuilderExtensions.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore { /// - /// Relational database specific extension methods for . + /// Relational database specific extension methods for . /// public static class PropertiesConfigurationBuilderExtensions { diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index c181470fa9c..7754880519e 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -1,15 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; // ReSharper disable once CheckNamespace @@ -521,40 +516,5 @@ public static void SetCollation(this IMutableModel model, string? value) /// The configuration source for the collation. public static ConfigurationSource? GetCollationConfigurationSource(this IConventionModel model) => model.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource(); - - /// - /// Gets the relational database type for a given object, throwing if no mapping is found. - /// - /// The model. - /// The object to get the mapping for. - /// The type mapping to be used. - public static RelationalTypeMapping GetMappingForValue( - this IModel model, - object? value) - => value == null - || value == DBNull.Value - ? RelationalTypeMapping.NullMapping - : model.GetMapping(value.GetType()); - - /// - /// Gets the relational database type for a given .NET type, throwing if no mapping is found. - /// - /// The model. - /// The type to get the mapping for. - /// The type mapping to be used. - public static RelationalTypeMapping GetMapping( - this IModel model, - Type clrType) - { - Check.NotNull(clrType, nameof(clrType)); - - var mapping = model.FindMapping(clrType); - if (mapping != null) - { - return (RelationalTypeMapping)mapping; - } - - throw new InvalidOperationException(RelationalStrings.UnsupportedType(clrType.ShortDisplayName())); - } } } diff --git a/src/EFCore.Relational/Extensions/ScalarConfigurationBuilderExtensions.cs b/src/EFCore.Relational/Extensions/ScalarConfigurationBuilderExtensions.cs new file mode 100644 index 00000000000..af0b4b87eaa --- /dev/null +++ b/src/EFCore.Relational/Extensions/ScalarConfigurationBuilderExtensions.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Utilities; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore +{ + /// + /// Relational database specific extension methods for . + /// + public static class ScalarConfigurationBuilderExtensions + { + /// + /// Configures the data type of the column that the scalar maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// The builder for the scalar being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static ScalarConfigurationBuilder HaveColumnType( + this ScalarConfigurationBuilder scalarBuilder, + string typeName) + { + Check.NotNull(scalarBuilder, nameof(scalarBuilder)); + Check.NotEmpty(typeName, nameof(typeName)); + + scalarBuilder.HaveAnnotation(RelationalAnnotationNames.ColumnType, typeName); + + return scalarBuilder; + } + + /// + /// Configures the data type of the column that the scalar maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// The type of the scalar being configured. + /// The builder for the scalar being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static ScalarConfigurationBuilder HaveColumnType( + this ScalarConfigurationBuilder scalarBuilder, + string typeName) + => (ScalarConfigurationBuilder)HaveColumnType((ScalarConfigurationBuilder)scalarBuilder, typeName); + + /// + /// Configures the scalar as capable of storing only fixed-length data, such as strings. + /// + /// The builder for the scalar being configured. + /// A value indicating whether the scalar is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static ScalarConfigurationBuilder AreFixedLength( + this ScalarConfigurationBuilder scalarBuilder, + bool fixedLength = true) + { + Check.NotNull(scalarBuilder, nameof(scalarBuilder)); + + scalarBuilder.HaveAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength); + + return scalarBuilder; + } + + /// + /// Configures the scalar as capable of storing only fixed-length data, such as strings. + /// + /// The type of the scalar being configured. + /// The builder for the scalar being configured. + /// A value indicating whether the scalar is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static ScalarConfigurationBuilder AreFixedLength( + this ScalarConfigurationBuilder scalarBuilder, + bool fixedLength = true) + => (ScalarConfigurationBuilder)AreFixedLength((ScalarConfigurationBuilder)scalarBuilder, fixedLength); + } +} diff --git a/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs index 3582153d42e..a7df7406f9e 100644 --- a/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 5b55164b4c2..e7a4ef1f0ab 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1050,7 +1050,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) // Introduce explicit cast only if the target type is mapped else we need to client eval if (unaryExpression.Type == typeof(object) - || Dependencies.Model.FindMapping(unaryExpression.Type) != null) + || Dependencies.TypeMappingSource.FindMapping(unaryExpression.Type, Dependencies.Model) != null) { sqlOperand = _sqlExpressionFactory.ApplyDefaultTypeMapping(sqlOperand); diff --git a/src/EFCore.Relational/Query/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/SqlExpressionFactory.cs index a34a891a865..154bf12382e 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactory.cs @@ -16,7 +16,6 @@ namespace Microsoft.EntityFrameworkCore.Query /// public class SqlExpressionFactory : ISqlExpressionFactory { - private readonly IModel _model; private readonly RelationalTypeMapping _boolTypeMapping; /// @@ -28,8 +27,7 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) Check.NotNull(dependencies, nameof(dependencies)); Dependencies = dependencies; - _model = dependencies.Model; - _boolTypeMapping = (RelationalTypeMapping)_model.FindMapping(typeof(bool))!; + _boolTypeMapping = dependencies.TypeMappingSource.FindMapping(typeof(bool), dependencies.Model)!; } /// @@ -40,16 +38,14 @@ public SqlExpressionFactory(SqlExpressionFactoryDependencies dependencies) /// [return: NotNullIfNotNull("sqlExpression")] public virtual SqlExpression? ApplyDefaultTypeMapping(SqlExpression? sqlExpression) - { - return sqlExpression == null + => sqlExpression == null || sqlExpression.TypeMapping != null ? sqlExpression : sqlExpression is SqlUnaryExpression sqlUnaryExpression && sqlUnaryExpression.OperatorType == ExpressionType.Convert && sqlUnaryExpression.Type == typeof(object) ? sqlUnaryExpression.Operand - : ApplyTypeMapping(sqlExpression, (RelationalTypeMapping?)_model.FindMapping(sqlExpression.Type)); - } + : ApplyTypeMapping(sqlExpression, Dependencies.TypeMappingSource.FindMapping(sqlExpression.Type, Dependencies.Model)); /// [return: NotNullIfNotNull("sqlExpression")] @@ -86,7 +82,7 @@ private SqlExpression ApplyTypeMappingOnLike(LikeExpression likeExpression) likeExpression.Match, likeExpression.Pattern) : ExpressionExtensions.InferTypeMapping( likeExpression.Match, likeExpression.Pattern, likeExpression.EscapeChar)) - ?? (RelationalTypeMapping?)_model.FindMapping(likeExpression.Match.Type); + ?? Dependencies.TypeMappingSource.FindMapping(likeExpression.Match.Type, Dependencies.Model); return new LikeExpression( ApplyTypeMapping(likeExpression.Match, inferredTypeMapping), @@ -189,8 +185,8 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( inferredTypeMapping = ExpressionExtensions.InferTypeMapping(left, right) // We avoid object here since the result does not get typeMapping from outside. ?? (left.Type != typeof(object) - ? (RelationalTypeMapping?)_model.FindMapping(left.Type) - : (RelationalTypeMapping?)_model.FindMapping(right.Type)); + ? Dependencies.TypeMappingSource.FindMapping(left.Type, Dependencies.Model) + : Dependencies.TypeMappingSource.FindMapping(right.Type, Dependencies.Model)); resultType = typeof(bool); resultTypeMapping = _boolTypeMapping; break; @@ -411,7 +407,7 @@ public virtual SqlFunctionExpression Coalesce(SqlExpression left, SqlExpression var resultType = right.Type; var inferredTypeMapping = typeMapping ?? ExpressionExtensions.InferTypeMapping(left, right) - ?? (RelationalTypeMapping?)_model.FindMapping(resultType); + ?? Dependencies.TypeMappingSource.FindMapping(resultType, Dependencies.Model); var typeMappedArguments = new List { @@ -506,7 +502,7 @@ public virtual CaseExpression Case(SqlExpression? operand, IReadOnlyList wc.Test.Type)) - .Where(t => t != typeof(object)).Select(t => (RelationalTypeMapping?)_model.FindMapping(t)).FirstOrDefault(); + .Where(t => t != typeof(object)).Select(t => Dependencies.TypeMappingSource.FindMapping(t, Dependencies.Model)).FirstOrDefault(); var resultTypeMapping = elseResult?.TypeMapping ?? whenClauses.Select(wc => wc.Result.TypeMapping).FirstOrDefault(t => t != null); @@ -748,7 +744,7 @@ public virtual InExpression In(SqlExpression item, SqlExpression values, bool ne Check.NotNull(item, nameof(item)); Check.NotNull(values, nameof(values)); - var typeMapping = item.TypeMapping ?? (RelationalTypeMapping?)_model.FindMapping(item.Type); + var typeMapping = item.TypeMapping ?? Dependencies.TypeMappingSource.FindMapping(item.Type, Dependencies.Model); item = ApplyTypeMapping(item, typeMapping); values = ApplyTypeMapping(values, typeMapping); @@ -978,11 +974,11 @@ private SqlExpression IsNotNull(IProperty property, EntityProjectionExpression e /// [Obsolete("Use IRelationalTypeMappingSource directly.")] public virtual RelationalTypeMapping GetTypeMappingForValue(object? value) - => _model.GetMappingForValue(value); + => Dependencies.TypeMappingSource.GetMappingForValue(value, Dependencies.Model); /// [Obsolete("Use IRelationalTypeMappingSource directly.")] public virtual RelationalTypeMapping? FindMapping(Type type) - => (RelationalTypeMapping?)_model.FindMapping(Check.NotNull(type, nameof(type))); + => Dependencies.TypeMappingSource.FindMapping(type, Dependencies.Model); } } diff --git a/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs b/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs index 10c7b381ede..adf6e1eacc8 100644 --- a/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs +++ b/src/EFCore.Relational/Query/SqlExpressionFactoryDependencies.cs @@ -60,15 +60,12 @@ public SqlExpressionFactoryDependencies(IModel model, IRelationalTypeMappingSour Check.NotNull(typeMappingSource, nameof(typeMappingSource)); Model = model; -#pragma warning disable CS0618 // Type or member is obsolete TypeMappingSource = typeMappingSource; -#pragma warning restore CS0618 // Type or member is obsolete } /// /// The type mapping source. /// - [Obsolete("Use Model instead")] public IRelationalTypeMappingSource TypeMappingSource { get; init; } /// diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs index cf1dde32d31..6a3f222ea23 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs @@ -260,108 +260,65 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) /// The type mapping, or if none was found. public override CoreTypeMapping? FindMapping(Type type, IModel model) { - var typeConfigurations = model.FindPropertyTypeConfigurations(type); - var mappingInfo = new RelationalTypeMappingInfo(type); + type = type.UnwrapNullableType(); + var typeConfiguration = model.FindScalarTypeConfiguration(type); + RelationalTypeMappingInfo mappingInfo; Type? providerClrType = null; ValueConverter? customConverter = null; - if (typeConfigurations != null) + if (typeConfiguration == null) { - foreach (var typeConfiguration in typeConfigurations) - { - if (providerClrType == null) - { - var providerType = typeConfiguration.GetProviderClrType(); - if (providerType != null) - { - providerClrType = providerType.UnwrapNullableType(); - } - } - - var isUnicode = typeConfiguration.IsUnicode(); - var scale = typeConfiguration.GetScale(); - var precision = typeConfiguration.GetPrecision(); - var size = typeConfiguration.GetMaxLength(); - - if (mappingInfo.StoreTypeName == null) - { - var storeTypeName = (string?)typeConfiguration[RelationalAnnotationNames.ColumnType]; - if (storeTypeName != null) - { - var storeTypeBaseName = ParseStoreTypeName( - storeTypeName, out var parsedUnicode, out var parsedSize, out var parsedPrecision, out var parsedScale); - - mappingInfo = mappingInfo with { StoreTypeName = storeTypeName }; - - if (mappingInfo.StoreTypeNameBase == null) - { - mappingInfo = mappingInfo with { StoreTypeNameBase = storeTypeBaseName }; - } - - if (size == null) - { - size = parsedSize; - } - - if (precision == null) - { - precision = parsedPrecision; - } - - if (scale == null) - { - scale = parsedScale; - } + mappingInfo = new RelationalTypeMappingInfo(type); + } + else + { + providerClrType = typeConfiguration.GetProviderClrType()?.UnwrapNullableType(); + customConverter = typeConfiguration.GetValueConverter(); - if (isUnicode == null) - { - isUnicode = parsedUnicode; - } - } - } + var isUnicode = typeConfiguration.IsUnicode(); + var scale = typeConfiguration.GetScale(); + var precision = typeConfiguration.GetPrecision(); + var size = typeConfiguration.GetMaxLength(); - if (mappingInfo.IsUnicode == null - && isUnicode != null) - { - mappingInfo = mappingInfo with { IsUnicode = isUnicode }; - } + var storeTypeName = (string?)typeConfiguration[RelationalAnnotationNames.ColumnType]; + string? storeTypeBaseName = null; + if (storeTypeName != null) + { + storeTypeBaseName = ParseStoreTypeName( + storeTypeName, out var parsedUnicode, out var parsedSize, out var parsedPrecision, out var parsedScale); - if (mappingInfo.Scale == null - && scale != null) + if (size == null) { - mappingInfo = mappingInfo with { Scale = scale }; + size = parsedSize; } - if (mappingInfo.Precision == null - && precision != null) + if (precision == null) { - mappingInfo = mappingInfo with { Precision = precision }; + precision = parsedPrecision; } - if (mappingInfo.Size == null - && size != null) + if (scale == null) { - mappingInfo = mappingInfo with { Size = size }; + scale = parsedScale; } - if (mappingInfo.IsFixedLength == null) + if (isUnicode == null) { - var isFixedLength = (bool?)typeConfiguration[RelationalAnnotationNames.IsFixedLength]; - if (isFixedLength != null) - { - mappingInfo = mappingInfo with { IsFixedLength = isFixedLength }; - } + isUnicode = parsedUnicode; } } - var firstConfiguration = typeConfigurations.FirstOrDefault(); - customConverter = firstConfiguration?.ClrType == type - ? firstConfiguration.GetValueConverter() - : null; - - if (customConverter != null) - { - mappingInfo = mappingInfo with { ClrType = customConverter.ProviderClrType }; - } + var isFixedLength = (bool?)typeConfiguration[RelationalAnnotationNames.IsFixedLength]; + mappingInfo = new RelationalTypeMappingInfo( + customConverter?.ProviderClrType ?? type, + storeTypeName, + storeTypeBaseName, + keyOrIndex: false, + unicode: isUnicode, + size: size, + rowVersion: false, + fixedLength: isFixedLength, + precision: precision, + scale: scale); } return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingSourceExtensions.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingSourceExtensions.cs index 5c103edc42b..47a79b05807 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingSourceExtensions.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingSourceExtensions.cs @@ -20,14 +20,29 @@ public static class RelationalTypeMappingSourceExtensions /// The object to get the mapping for. /// The type mapping to be used. public static RelationalTypeMapping GetMappingForValue( - this IRelationalTypeMappingSource? typeMappingSource, + this IRelationalTypeMappingSource typeMappingSource, object? value) => value == null || value == DBNull.Value - || typeMappingSource == null ? RelationalTypeMapping.NullMapping : typeMappingSource.GetMapping(value.GetType()); + /// + /// Gets the relational database type for a given object, throwing if no mapping is found. + /// + /// The type mapping source. + /// The object to get the mapping for. + /// The model. + /// The type mapping to be used. + public static RelationalTypeMapping GetMappingForValue( + this IRelationalTypeMappingSource typeMappingSource, + object? value, + IModel model) + => value == null + || value == DBNull.Value + ? RelationalTypeMapping.NullMapping + : typeMappingSource.GetMapping(value.GetType(), model); + /// /// Gets the relational database type for a given property, throwing if no mapping is found. /// @@ -77,6 +92,29 @@ public static RelationalTypeMapping GetMapping( throw new InvalidOperationException(RelationalStrings.UnsupportedType(clrType.ShortDisplayName())); } + /// + /// Gets the relational database type for a given .NET type, throwing if no mapping is found. + /// + /// The type mapping source. + /// The type to get the mapping for. + /// The model. + /// The type mapping to be used. + public static RelationalTypeMapping GetMapping( + this IRelationalTypeMappingSource typeMappingSource, + Type clrType, + IModel model) + { + Check.NotNull(clrType, nameof(clrType)); + + var mapping = typeMappingSource.FindMapping(clrType, model); + if (mapping != null) + { + return mapping; + } + + throw new InvalidOperationException(RelationalStrings.UnsupportedType(clrType.ShortDisplayName())); + } + /// /// /// Gets the mapping that represents the given database type, throwing if no mapping is found. diff --git a/src/EFCore/Metadata/Builders/ScalarConfigurationBuilder.cs b/src/EFCore/Metadata/Builders/ScalarConfigurationBuilder.cs new file mode 100644 index 00000000000..da0f8d9a537 --- /dev/null +++ b/src/EFCore/Metadata/Builders/ScalarConfigurationBuilder.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API surface for setting property defaults before conventions run. + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class ScalarConfigurationBuilder + { + /// + /// 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. + /// + [EntityFrameworkInternal] + public ScalarConfigurationBuilder(PropertyConfiguration scalar) + { + Check.NotNull(scalar, nameof(scalar)); + + Scalar = scalar; + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + protected virtual PropertyConfiguration Scalar { get; } + + /// + /// Adds or updates an annotation on the property. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder HaveAnnotation(string annotation, object value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Scalar[annotation] = value; + + return this; + } + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// The maximum length of data allowed in the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder HaveMaxLength(int maxLength) + { + Scalar.SetMaxLength(maxLength); + + return this; + } + + /// + /// Configures the precision and scale of the property. + /// + /// The precision of the property. + /// The scale of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder HavePrecision(int precision, int scale) + { + Scalar.SetPrecision(precision); + Scalar.SetScale(scale); + + return this; + } + + /// + /// + /// Configures the precision of the property. + /// + /// + /// The precision of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder HavePrecision(int precision) + { + Scalar.SetPrecision(precision); + + return this; + } + + /// + /// Configures whether the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder AreUnicode(bool unicode = true) + { + Scalar.SetIsUnicode(unicode); + + return this; + } + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder HaveConversion() + => HaveConversion(typeof(TConversion)); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ScalarConfigurationBuilder HaveConversion(Type conversionType) + { + Check.NotNull(conversionType, nameof(conversionType)); + + if (typeof(ValueConverter).IsAssignableFrom(conversionType)) + { + Scalar.SetValueConverter(conversionType); + } + else + { + Scalar.SetProviderClrType(conversionType); + } + + return this; + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore/Metadata/Builders/ScalarConfigurationBuilder`.cs b/src/EFCore/Metadata/Builders/ScalarConfigurationBuilder`.cs new file mode 100644 index 00000000000..5ae0a31e4f1 --- /dev/null +++ b/src/EFCore/Metadata/Builders/ScalarConfigurationBuilder`.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// + /// Provides a simple API surface for setting property defaults before conventions run. + /// + /// + /// Instances of this class are returned from methods when using the API + /// and it is not designed to be directly constructed in your application code. + /// + /// + public class ScalarConfigurationBuilder : ScalarConfigurationBuilder + { + /// + /// 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. + /// + [EntityFrameworkInternal] + public ScalarConfigurationBuilder(PropertyConfiguration scalar) + : base(scalar) + { + } + + /// + /// Adds or updates an annotation on the property. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder HaveAnnotation(string annotation, object value) + => (ScalarConfigurationBuilder)base.HaveAnnotation(annotation, value); + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// The maximum length of data allowed in the property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder HaveMaxLength(int maxLength) + => (ScalarConfigurationBuilder)base.HaveMaxLength(maxLength); + + /// + /// Configures the precision and scale of the property. + /// + /// The precision of the property. + /// The scale of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder HavePrecision(int precision, int scale) + => (ScalarConfigurationBuilder)base.HavePrecision(precision, scale); + + /// + /// + /// Configures the precision of the property. + /// + /// + /// The precision of the property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder HavePrecision(int precision) + => (ScalarConfigurationBuilder)base.HavePrecision(precision); + + /// + /// Configures the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder AreUnicode(bool unicode = true) + => (ScalarConfigurationBuilder)base.AreUnicode(unicode); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder HaveConversion() + => (ScalarConfigurationBuilder)base.HaveConversion(); + + /// + /// Configures the property so that the property value is converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that derives from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ScalarConfigurationBuilder HaveConversion(Type conversionType) + => (ScalarConfigurationBuilder)base.HaveConversion(conversionType); + } +} diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 6587a58790b..7588208f1b0 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; @@ -150,11 +147,11 @@ protected virtual RuntimeModel Create(IModel model) convention.ProcessEntityTypeAnnotations(annotations, source, target, runtime)); } - foreach (var typeConfiguration in model.GetPropertyTypeConfigurations()) + foreach (var typeConfiguration in model.GetScalarTypeConfigurations()) { var runtimeTypeConfiguration = Create(typeConfiguration, runtimeModel); CreateAnnotations(typeConfiguration, runtimeTypeConfiguration, static (convention, annotations, source, target, runtime) => - convention.ProcessPropertyTypeConfigurationAnnotations(annotations, source, target, runtime)); + convention.ProcessScalarTypeConfigurationAnnotations(annotations, source, target, runtime)); } CreateAnnotations(model, runtimeModel, static (convention, annotations, source, target, runtime) => @@ -258,8 +255,8 @@ protected virtual void ProcessEntityTypeAnnotations( } } - private RuntimePropertyTypeConfiguration Create(IPropertyTypeConfiguration typeConfiguration, RuntimeModel model) - => model.AddPropertyTypeConfiguration( + private RuntimeScalarTypeConfiguration Create(IScalarTypeConfiguration typeConfiguration, RuntimeModel model) + => model.AddScalarTypeConfiguration( typeConfiguration.ClrType, typeConfiguration.GetMaxLength(), typeConfiguration.IsUnicode(), @@ -275,10 +272,10 @@ private RuntimePropertyTypeConfiguration Create(IPropertyTypeConfiguration typeC /// The source property. /// The target property that will contain the annotations. /// Indicates whether the given annotations are runtime annotations. - protected virtual void ProcessPropertyTypeConfigurationAnnotations( + protected virtual void ProcessScalarTypeConfigurationAnnotations( Dictionary annotations, - IPropertyTypeConfiguration typeConfiguration, - RuntimePropertyTypeConfiguration runtimeTypeConfiguration, + IScalarTypeConfiguration typeConfiguration, + RuntimeScalarTypeConfiguration runtimeTypeConfiguration, bool runtime) { if (!runtime) diff --git a/src/EFCore/Metadata/IModel.cs b/src/EFCore/Metadata/IModel.cs index 6607ca7fed2..bd10d44c43d 100644 --- a/src/EFCore/Metadata/IModel.cs +++ b/src/EFCore/Metadata/IModel.cs @@ -148,25 +148,17 @@ RuntimeModelDependencies GetModelDependencies() /// The to check. bool IsIndexerMethod(MethodInfo methodInfo); - /// - /// - /// Finds the type mapping for a given , taking pre-convention configuration into the account. - /// - /// - /// The CLR type. - CoreTypeMapping? FindMapping(Type type); - /// /// Gets all the pre-convention configurations. /// /// The pre-convention configurations. - IEnumerable GetPropertyTypeConfigurations(); + IEnumerable GetScalarTypeConfigurations(); /// /// Finds the pre-convention configurations for a given scalar . /// /// The CLR type. - /// The pre-convention configurations in order of most to least specific, or if none is found. - IEnumerable? FindPropertyTypeConfigurations(Type propertyType); + /// The pre-convention configuration or if none is found. + IScalarTypeConfiguration? FindScalarTypeConfiguration(Type propertyType); } } diff --git a/src/EFCore/Metadata/IPropertyTypeConfiguration.cs b/src/EFCore/Metadata/IScalarTypeConfiguration.cs similarity index 94% rename from src/EFCore/Metadata/IPropertyTypeConfiguration.cs rename to src/EFCore/Metadata/IScalarTypeConfiguration.cs index 1bdb905489e..7e60278040c 100644 --- a/src/EFCore/Metadata/IPropertyTypeConfiguration.cs +++ b/src/EFCore/Metadata/IScalarTypeConfiguration.cs @@ -7,9 +7,9 @@ namespace Microsoft.EntityFrameworkCore.Metadata { /// - /// Represents the configuration for a scalar property type. + /// Represents the configuration for a scalar type. /// - public interface IPropertyTypeConfiguration : IAnnotatable + public interface IScalarTypeConfiguration : IAnnotatable { /// /// Gets the type configured by this object. diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 40ed614e20a..841359cd0e4 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -653,8 +653,8 @@ public virtual bool IsIgnoredType(Type type) /// 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 CoreTypeMapping? FindMapping(Type type) - => ((IModel)this).GetModelDependencies().TypeMappingSource.FindMapping(type, this); + public virtual IEnumerable GetScalarTypeConfigurations() + => Configuration?.GetScalarTypeConfigurations() ?? Enumerable.Empty(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -662,17 +662,8 @@ public virtual bool IsIgnoredType(Type type) /// 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 IEnumerable GetPropertyTypeConfigurations() - => Configuration?.GetPropertyTypeConfigurations() ?? Enumerable.Empty(); - - /// - /// 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 IEnumerable? FindPropertyTypeConfigurations(Type propertyType) - => Configuration?.FindPropertyTypeConfigurations(propertyType); + public virtual IScalarTypeConfiguration? FindScalarTypeConfiguration(Type propertyType) + => Configuration?.FindScalarTypeConfiguration(propertyType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/ModelConfiguration.cs b/src/EFCore/Metadata/Internal/ModelConfiguration.cs index 0e09821e030..4b73c725808 100644 --- a/src/EFCore/Metadata/Internal/ModelConfiguration.cs +++ b/src/EFCore/Metadata/Internal/ModelConfiguration.cs @@ -1,9 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections; +using System.Dynamic; using Microsoft.EntityFrameworkCore.Diagnostics; namespace Microsoft.EntityFrameworkCore.Metadata.Internal @@ -17,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal public partial class ModelConfiguration { private readonly Dictionary _properties = new(); + private readonly Dictionary _scalars = new(); private readonly HashSet _ignoredTypes = new(); private readonly Dictionary _configurationTypes = new(); @@ -37,7 +37,7 @@ public ModelConfiguration() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool IsEmpty() - => _properties.Count == 0 && _ignoredTypes.Count == 0; + => _properties.Count == 0 && _ignoredTypes.Count == 0 && _scalars.Count == 0; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -66,6 +66,13 @@ public virtual bool IsEmpty() } Type? configuredType = null; + + if (type.IsConstructedGenericType) + { + configurationType = GetConfigurationType( + type.GetGenericTypeDefinition(), configurationType, ref configuredType, getBaseTypes: false); + } + if (getBaseTypes) { if (type.BaseType != null) @@ -74,12 +81,6 @@ public virtual bool IsEmpty() type.BaseType, configurationType, ref configuredType); } - if (type.IsConstructedGenericType) - { - configurationType = GetConfigurationType( - type.GetGenericTypeDefinition(), configurationType, ref configuredType, getBaseTypes: false); - } - foreach (var @interface in type.GetDeclaredInterfaces()) { configurationType = GetConfigurationType( @@ -130,8 +131,8 @@ private static void EnsureCompatible( /// 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 IEnumerable GetPropertyTypeConfigurations() - => _properties.Values; + public virtual IEnumerable GetScalarTypeConfigurations() + => _scalars.Values; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -139,12 +140,10 @@ public virtual IEnumerable GetPropertyTypeConfigurat /// 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 IEnumerable? FindPropertyTypeConfigurations(Type propertyType) - => GetConfigurationType(propertyType) != TypeConfigurationType.Property + public virtual IScalarTypeConfiguration? FindScalarTypeConfiguration(Type scalarType) + => _scalars.Count == 0 ? null - : propertyType.GetBaseTypesAndInterfacesInclusive() - .Select(type => _properties.GetValueOrDefault(type)!) - .Where(type => type != null); + : _scalars.GetValueOrDefault(scalarType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -174,16 +173,26 @@ public virtual void ConfigureProperty(IMutableProperty property) /// public virtual PropertyConfiguration GetOrAddProperty(Type type) { - if (type.UnwrapNullableType() == typeof(object) - || type == Model.DefaultPropertyBagType) - { - throw new InvalidOperationException( - CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), TypeConfigurationType.Property)); - } - var property = FindProperty(type); if (property == null) { + if (type == typeof(object) + || type == typeof(ExpandoObject) + || type == typeof(SortedDictionary) + || type == typeof(Dictionary) + || type == typeof(IDictionary) + || type == typeof(IReadOnlyDictionary) + || type == typeof(IDictionary) + || type == typeof(ICollection>) + || type == typeof(IReadOnlyCollection>) + || type == typeof(ICollection) + || type == typeof(IEnumerable>) + || type == typeof(IEnumerable)) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), TypeConfigurationType.Property)); + } + RemoveIgnored(type); property = new PropertyConfiguration(type); @@ -213,6 +222,46 @@ public virtual PropertyConfiguration GetOrAddProperty(Type type) public virtual bool RemoveProperty(Type type) => _properties.Remove(type); + /// + /// 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 PropertyConfiguration GetOrAddScalar(Type type) + { + var scalar = FindScalar(type); + if (scalar == null) + { + if (type == typeof(object) + || type == typeof(ExpandoObject) + || type == typeof(SortedDictionary) + || type == typeof(Dictionary) + || type.IsNullableValueType() + || !type.IsInstantiable()) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), "Scalar")); + } + + scalar = new PropertyConfiguration(type); + _scalars.Add(type, scalar); + } + + return scalar; + } + + /// + /// 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 PropertyConfiguration? FindScalar(Type type) + => _scalars.TryGetValue(type, out var property) + ? property + : null; + /// /// 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 @@ -222,10 +271,19 @@ public virtual bool RemoveProperty(Type type) public virtual void AddIgnored(Type type) { if (type.UnwrapNullableType() == typeof(int) - || type.UnwrapNullableType() == typeof(int?) - || type.UnwrapNullableType() == typeof(string) - || type.UnwrapNullableType() == typeof(object) - || type == Model.DefaultPropertyBagType) + || type == typeof(string) + || type == typeof(object) + || type == typeof(ExpandoObject) + || type == typeof(SortedDictionary) + || type == typeof(Dictionary) + || type == typeof(IDictionary) + || type == typeof(IReadOnlyDictionary) + || type == typeof(IDictionary) + || type == typeof(ICollection>) + || type == typeof(IReadOnlyCollection>) + || type == typeof(ICollection) + || type == typeof(IEnumerable>) + || type == typeof(IEnumerable)) { throw new InvalidOperationException( CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), TypeConfigurationType.Ignored)); diff --git a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs index e7198b4af19..64ada8b89c3 100644 --- a/src/EFCore/Metadata/Internal/PropertyConfiguration.cs +++ b/src/EFCore/Metadata/Internal/PropertyConfiguration.cs @@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public class PropertyConfiguration : AnnotatableBase, IPropertyTypeConfiguration + public class PropertyConfiguration : AnnotatableBase, IScalarTypeConfiguration { private ValueConverter? _valueConverter; diff --git a/src/EFCore/Metadata/RuntimeModel.cs b/src/EFCore/Metadata/RuntimeModel.cs index 5af3cf6b042..2911c083223 100644 --- a/src/EFCore/Metadata/RuntimeModel.cs +++ b/src/EFCore/Metadata/RuntimeModel.cs @@ -9,7 +9,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Microsoft.EntityFrameworkCore.Metadata @@ -29,12 +28,11 @@ public class RuntimeModel : AnnotatableBase, IRuntimeModel { private readonly SortedDictionary _entityTypes = new(StringComparer.Ordinal); private readonly Dictionary> _sharedTypes = new(); - private readonly Dictionary _typeConfigurations = new(); + private readonly Dictionary _typeConfigurations = new(); private bool _skipDetectChanges; private readonly ConcurrentDictionary _indexerPropertyInfoMap = new(); private readonly ConcurrentDictionary _clrTypeNameMap = new(); - private readonly ConcurrentDictionary _typeMappings = new(); /// /// Creates a new instance of @@ -139,7 +137,7 @@ private IEnumerable FindEntityTypes(Type type) } /// - /// Adds configuration for a scalar property type. + /// Adds configuration for a scalar type. /// /// The type of value the property will hold. /// The maximum length of data that is allowed in this property type. @@ -151,7 +149,7 @@ private IEnumerable FindEntityTypes(Type type) /// /// The type of a custom set for this property type. /// The newly created property. - public virtual RuntimePropertyTypeConfiguration AddPropertyTypeConfiguration( + public virtual RuntimeScalarTypeConfiguration AddScalarTypeConfiguration( Type clrType, int? maxLength = null, bool? unicode = null, @@ -160,7 +158,7 @@ public virtual RuntimePropertyTypeConfiguration AddPropertyTypeConfiguration( Type? providerPropertyType = null, Type? valueConverterType = null) { - var typeConfiguration = new RuntimePropertyTypeConfiguration( + var typeConfiguration = new RuntimeScalarTypeConfiguration( clrType, maxLength, unicode, @@ -294,19 +292,13 @@ bool IReadOnlyModel.IsShared(Type type) => _sharedTypes.ContainsKey(type); /// - IEnumerable IModel.GetPropertyTypeConfigurations() + IEnumerable IModel.GetScalarTypeConfigurations() => _typeConfigurations.Values; /// - IEnumerable? IModel.FindPropertyTypeConfigurations(Type propertyType) + IScalarTypeConfiguration? IModel.FindScalarTypeConfiguration(Type propertyType) => _typeConfigurations.Count == 0 ? null - : propertyType.GetBaseTypesAndInterfacesInclusive() - .Select(type => _typeConfigurations.GetValueOrDefault(type)!) - .Where(type => type != null); - - /// - CoreTypeMapping? IModel.FindMapping(Type type) - => _typeMappings.GetOrAdd(type, (type, model) => ((IModel)model).GetModelDependencies().TypeMappingSource.FindMapping(type, model), this); + : _typeConfigurations.GetValueOrDefault(propertyType); } } diff --git a/src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs b/src/EFCore/Metadata/RuntimeScalarTypeConfiguration.cs similarity index 80% rename from src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs rename to src/EFCore/Metadata/RuntimeScalarTypeConfiguration.cs index 73b23b7f0fd..ecdb828cc12 100644 --- a/src/EFCore/Metadata/RuntimePropertyTypeConfiguration.cs +++ b/src/EFCore/Metadata/RuntimeScalarTypeConfiguration.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata /// /// Represents a scalar property type. /// - public sealed class RuntimePropertyTypeConfiguration : AnnotatableBase, IPropertyTypeConfiguration + public sealed class RuntimeScalarTypeConfiguration : AnnotatableBase, IScalarTypeConfiguration { private readonly ValueConverter? _valueConverter; @@ -22,7 +22,7 @@ public sealed class RuntimePropertyTypeConfiguration : AnnotatableBase, IPropert /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public RuntimePropertyTypeConfiguration( + public RuntimeScalarTypeConfiguration( Type clrType, int? maxLength, bool? unicode, @@ -71,28 +71,28 @@ public RuntimePropertyTypeConfiguration( /// [DebuggerStepThrough] - int? IPropertyTypeConfiguration.GetMaxLength() => (int?)this[CoreAnnotationNames.MaxLength]; + int? IScalarTypeConfiguration.GetMaxLength() => (int?)this[CoreAnnotationNames.MaxLength]; /// [DebuggerStepThrough] - bool? IPropertyTypeConfiguration.IsUnicode() => (bool?)this[CoreAnnotationNames.Unicode]; + bool? IScalarTypeConfiguration.IsUnicode() => (bool?)this[CoreAnnotationNames.Unicode]; /// [DebuggerStepThrough] - int? IPropertyTypeConfiguration.GetPrecision() => (int?)this[CoreAnnotationNames.Precision]; + int? IScalarTypeConfiguration.GetPrecision() => (int?)this[CoreAnnotationNames.Precision]; /// [DebuggerStepThrough] - int? IPropertyTypeConfiguration.GetScale() => (int?)this[CoreAnnotationNames.Scale]; + int? IScalarTypeConfiguration.GetScale() => (int?)this[CoreAnnotationNames.Scale]; /// [DebuggerStepThrough] - ValueConverter? IPropertyTypeConfiguration.GetValueConverter() + ValueConverter? IScalarTypeConfiguration.GetValueConverter() => _valueConverter; /// [DebuggerStepThrough] - Type? IPropertyTypeConfiguration.GetProviderClrType() + Type? IScalarTypeConfiguration.GetProviderClrType() => (Type?)this[CoreAnnotationNames.ProviderClrType]; } } diff --git a/src/EFCore/ModelConfigurationBuilder.cs b/src/EFCore/ModelConfigurationBuilder.cs index 3c1fc6e1ce8..80ea23bba34 100644 --- a/src/EFCore/ModelConfigurationBuilder.cs +++ b/src/EFCore/ModelConfigurationBuilder.cs @@ -77,7 +77,7 @@ public virtual ModelConfigurationBuilder IgnoreAny(Type type) /// Marks the given and derived types as corresponding to entity type properties. /// /// The property type to be configured. - /// An object that can be used to provide the configuration defaults for the properties. + /// An object that can be used to configure the properties. public virtual PropertiesConfigurationBuilder Properties() { var property = _modelConfiguration.GetOrAddProperty(typeof(TProperty)); @@ -139,6 +139,72 @@ public virtual ModelConfigurationBuilder Properties( return this; } + /// + /// Marks the given type as a scalar, even when used outside of entity types. + /// + /// The scalar type to be configured. + /// An object that can be used to configure the scalars. + public virtual ScalarConfigurationBuilder Scalars() + { + var scalar = _modelConfiguration.GetOrAddScalar(typeof(TScalar)); + + return new ScalarConfigurationBuilder(scalar); + } + + /// + /// Marks the given type as a scalar, even when used outside of entity types. + /// + /// The scalar type to be configured. + /// An action that performs configuration for the scalars. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelConfigurationBuilder Scalars( + Action> buildAction) + { + Check.NotNull(buildAction, nameof(buildAction)); + + var scalarBuilder = Scalars(); + buildAction(scalarBuilder); + + return this; + } + + /// + /// Marks the given type as a scalar, even when used outside of entity types. + /// + /// The scalar type to be configured. + /// An object that can be used to configure the scalars. + public virtual ScalarConfigurationBuilder Scalars(Type scalarType) + { + Check.NotNull(scalarType, nameof(scalarType)); + + var scalar = _modelConfiguration.GetOrAddScalar(scalarType); + + return new ScalarConfigurationBuilder(scalar); + } + + /// + /// Marks the given type as a scalar, even when used outside of entity types. + /// + /// The scalar type to be configured. + /// An action that performs configuration for the scalars. + /// + /// The same instance so that additional configuration calls can be chained. + /// + public virtual ModelConfigurationBuilder Scalars( + Type scalarType, + Action buildAction) + { + Check.NotNull(scalarType, nameof(scalarType)); + Check.NotNull(buildAction, nameof(buildAction)); + + var scalarBuilder = Scalars(scalarType); + buildAction(scalarBuilder); + + return this; + } + /// /// Creates the configured used to create the model. This is done automatically when using /// ; this method allows it to be run diff --git a/src/EFCore/Storage/TypeMappingSource.cs b/src/EFCore/Storage/TypeMappingSource.cs index d368d5bb805..dc8695ad974 100644 --- a/src/EFCore/Storage/TypeMappingSource.cs +++ b/src/EFCore/Storage/TypeMappingSource.cs @@ -1,10 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Concurrent; using System.Reflection; -using System.Security.Principal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.DependencyInjection; @@ -184,69 +182,25 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) /// The type mapping, or if none was found. public override CoreTypeMapping? FindMapping(Type type, IModel model) { - var typeConfigurations = model.FindPropertyTypeConfigurations(type); - var mappingInfo = new TypeMappingInfo(type); + type = type.UnwrapNullableType(); + var typeConfiguration = model.FindScalarTypeConfiguration(type); + TypeMappingInfo mappingInfo; Type? providerClrType = null; ValueConverter? customConverter = null; - if (typeConfigurations != null) + if (typeConfiguration == null) { - foreach (var typeConfiguration in typeConfigurations) - { - if (providerClrType == null) - { - var providerType = typeConfiguration.GetProviderClrType(); - if (providerType != null) - { - providerClrType = providerType.UnwrapNullableType(); - } - } - - if (mappingInfo.IsUnicode == null) - { - var isUnicode = typeConfiguration.IsUnicode(); - if (isUnicode != null) - { - mappingInfo = mappingInfo with { IsUnicode = isUnicode }; - } - } - - if (mappingInfo.Scale == null) - { - var scale = typeConfiguration.GetScale(); - if (scale != null) - { - mappingInfo = mappingInfo with { Scale = scale }; - } - } - - if (mappingInfo.Precision == null) - { - var precision = typeConfiguration.GetPrecision(); - if (precision != null) - { - mappingInfo = mappingInfo with { Precision = precision }; - } - } - - if (mappingInfo.Size == null) - { - var size = typeConfiguration.GetMaxLength(); - if (size != null) - { - mappingInfo = mappingInfo with { Size = size }; - } - } - } - - var firstConfiguration = typeConfigurations.FirstOrDefault(); - customConverter = firstConfiguration?.ClrType == type - ? firstConfiguration.GetValueConverter() - : null; - - if (customConverter != null) - { - mappingInfo = mappingInfo with { ClrType = customConverter.ProviderClrType }; - } + mappingInfo = new TypeMappingInfo(type); + } + else + { + providerClrType = typeConfiguration.GetProviderClrType()?.UnwrapNullableType(); + customConverter = typeConfiguration.GetValueConverter(); + mappingInfo = new TypeMappingInfo( + customConverter?.ProviderClrType ?? type, + unicode: typeConfiguration.IsUnicode(), + size: typeConfiguration.GetMaxLength(), + precision: typeConfiguration.GetPrecision(), + scale: typeConfiguration.GetScale()); } return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index 4a59a0ee9b7..76cb5f45161 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -312,6 +312,11 @@ public static List GetBaseTypesAndInterfacesInclusive(this Type type) type = typesToProcess.Dequeue(); baseTypes.Add(type); + if (type.IsConstructedGenericType) + { + typesToProcess.Enqueue(type.GetGenericTypeDefinition()); + } + if (!type.IsGenericTypeDefinition && !type.IsInterface) { @@ -320,11 +325,6 @@ public static List GetBaseTypesAndInterfacesInclusive(this Type type) typesToProcess.Enqueue(type.BaseType); } - if (type.IsConstructedGenericType) - { - typesToProcess.Enqueue(type.GetGenericTypeDefinition()); - } - foreach (var @interface in GetDeclaredInterfaces(type)) { typesToProcess.Enqueue(@interface); diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 4b399ad101c..cb7e8292486 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -2289,18 +2286,14 @@ private int GetCount(Guid? id, string condition) => throw new NotImplementedException(); public IQueryable GetData(int id) - { - return FromExpression(() => GetData(id)); - } + => FromExpression(() => GetData(id)); public IQueryable GetData() - { - return FromExpression(() => GetData()); - } + => FromExpression(() => GetData()); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { - configurationBuilder.Properties().HaveMaxLength(256); + configurationBuilder.Scalars().HaveMaxLength(256); } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs index 6bc720c65a3..7e41a4620fc 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTypeMapperTestBase.cs @@ -44,47 +44,47 @@ protected RelationalTypeMapping GetTypeMapping( { var model = CreateModelBuilder(c => { - var properties = c.Properties(propertyType); + var scalarBuilder = c.Scalars(propertyType); if (maxLength.HasValue) { - properties.HaveMaxLength(maxLength.Value); + scalarBuilder.HaveMaxLength(maxLength.Value); } if (precision.HasValue) { if (scale.HasValue) { - properties.HavePrecision(precision.Value, scale.Value); + scalarBuilder.HavePrecision(precision.Value, scale.Value); } else { - properties.HavePrecision(precision.Value); + scalarBuilder.HavePrecision(precision.Value); } } if (providerType != null) { - properties.HaveConversion(providerType); + scalarBuilder.HaveConversion(providerType); } if (unicode.HasValue) { - properties.AreUnicode(unicode.Value); + scalarBuilder.AreUnicode(unicode.Value); } if (fixedLength.HasValue) { - properties.AreFixedLength(fixedLength.Value); + scalarBuilder.AreFixedLength(fixedLength.Value); } if (storeTypeName != null) { - properties.HaveColumnType(storeTypeName); + scalarBuilder.HaveColumnType(storeTypeName); } }).FinalizeModel(); - return (RelationalTypeMapping)model.FindMapping(propertyType); + return CreateRelationalTypeMappingSource().GetMapping(propertyType, model); } else { diff --git a/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs b/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs index b43b86fe0a1..7d1c6a96725 100644 --- a/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs +++ b/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs @@ -840,14 +840,6 @@ public NonNullIntToNullStringConverter() } } - protected class CustomValueComparer : ValueComparer - { - public CustomValueComparer() - : base(false) - { - } - } - protected enum TheExperience : ushort { Jimi, diff --git a/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerTest.cs index d412cb09e48..0a94c3c6921 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerTest.cs @@ -173,7 +173,7 @@ public virtual void Can_use_custom_converters_without_property() Assert.Equal(@"SELECT [c].[Id], [c].[BoolAsChar], [c].[BoolAsInt], [c].[BoolAsNullableChar], [c].[BoolAsNullableInt], [c].[BoolAsNullableString], [c].[BoolAsString], [c].[BytesAsNullableString], [c].[BytesAsString], [c].[CharAsNullableString], [c].[CharAsString], [c].[DateTimeOffsetToBinary], [c].[DateTimeOffsetToNullableBinary], [c].[DateTimeOffsetToNullableString], [c].[DateTimeOffsetToString], [c].[DateTimeToBinary], [c].[DateTimeToNullableBinary], [c].[DateTimeToNullableString], [c].[DateTimeToString], [c].[EnumToNullableNumber], [c].[EnumToNullableString], [c].[EnumToNumber], [c].[EnumToString], [c].[GuidToBytes], [c].[GuidToNullableBytes], [c].[GuidToNullableString], [c].[GuidToString], [c].[IPAddressToBytes], [c].[IPAddressToNullableBytes], [c].[IPAddressToNullableString], [c].[IPAddressToString], [c].[IntAsLong], [c].[IntAsNullableLong], [c].[NonNullIntToNonNullString], [c].[NonNullIntToNullString], [c].[NonNullStringToNullString], [c].[NullIntToNonNullString], [c].[NullIntToNullString], [c].[NullStringToNonNullString], [c].[NullableBoolAsChar], [c].[NullableBoolAsInt], [c].[NullableBoolAsNullableChar], [c].[NullableBoolAsNullableInt], [c].[NullableBoolAsNullableString], [c].[NullableBoolAsString], [c].[NullableBytesAsNullableString], [c].[NullableBytesAsString], [c].[NullableCharAsNullableString], [c].[NullableCharAsString], [c].[NullableDateTimeOffsetToBinary], [c].[NullableDateTimeOffsetToNullableBinary], [c].[NullableDateTimeOffsetToNullableString], [c].[NullableDateTimeOffsetToString], [c].[NullableDateTimeToBinary], [c].[NullableDateTimeToNullableBinary], [c].[NullableDateTimeToNullableString], [c].[NullableDateTimeToString], [c].[NullableEnumToNullableNumber], [c].[NullableEnumToNullableString], [c].[NullableEnumToNumber], [c].[NullableEnumToString], [c].[NullableGuidToBytes], [c].[NullableGuidToNullableBytes], [c].[NullableGuidToNullableString], [c].[NullableGuidToString], [c].[NullableIPAddressToBytes], [c].[NullableIPAddressToNullableBytes], [c].[NullableIPAddressToNullableString], [c].[NullableIPAddressToString], [c].[NullableIntAsLong], [c].[NullableIntAsNullableLong], [c].[NullableNumberToBytes], [c].[NullableNumberToNullableBytes], [c].[NullableNumberToNullableString], [c].[NullableNumberToString], [c].[NullablePhysicalAddressToBytes], [c].[NullablePhysicalAddressToNullableBytes], [c].[NullablePhysicalAddressToNullableString], [c].[NullablePhysicalAddressToString], [c].[NullableStringToBool], [c].[NullableStringToBytes], [c].[NullableStringToChar], [c].[NullableStringToDateTime], [c].[NullableStringToDateTimeOffset], [c].[NullableStringToEnum], [c].[NullableStringToGuid], [c].[NullableStringToNullableBool], [c].[NullableStringToNullableBytes], [c].[NullableStringToNullableChar], [c].[NullableStringToNullableDateTime], [c].[NullableStringToNullableDateTimeOffset], [c].[NullableStringToNullableEnum], [c].[NullableStringToNullableGuid], [c].[NullableStringToNullableNumber], [c].[NullableStringToNullableTimeSpan], [c].[NullableStringToNumber], [c].[NullableStringToTimeSpan], [c].[NullableTimeSpanToNullableString], [c].[NullableTimeSpanToNullableTicks], [c].[NullableTimeSpanToString], [c].[NullableTimeSpanToTicks], [c].[NullableUriToNullableString], [c].[NullableUriToString], [c].[NumberToBytes], [c].[NumberToNullableBytes], [c].[NumberToNullableString], [c].[NumberToString], [c].[PhysicalAddressToBytes], [c].[PhysicalAddressToNullableBytes], [c].[PhysicalAddressToNullableString], [c].[PhysicalAddressToString], [c].[StringToBool], [c].[StringToBytes], [c].[StringToChar], [c].[StringToDateTime], [c].[StringToDateTimeOffset], [c].[StringToEnum], [c].[StringToGuid], [c].[StringToNullableBool], [c].[StringToNullableBytes], [c].[StringToNullableChar], [c].[StringToNullableDateTime], [c].[StringToNullableDateTimeOffset], [c].[StringToNullableEnum], [c].[StringToNullableGuid], [c].[StringToNullableNumber], [c].[StringToNullableTimeSpan], [c].[StringToNumber], [c].[StringToTimeSpan], [c].[TimeSpanToNullableString], [c].[TimeSpanToNullableTicks], [c].[TimeSpanToString], [c].[TimeSpanToTicks], [c].[UriToNullableString], [c].[UriToString] FROM [ConvertingEntity] AS [c] -WHERE CAST(DATALENGTH(CAST(N'' AS nvarchar(max))) AS int) = 0", Fixture.TestSqlLoggerFactory.SqlStatements[0]); +WHERE CAST(DATALENGTH(CAST(N'' AS nvarchar(max))) AS int) = 1", Fixture.TestSqlLoggerFactory.SqlStatements[0]); } private struct WrappedString @@ -195,7 +195,7 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura { base.ConfigureConventions(configurationBuilder); - configurationBuilder.Properties().HaveConversion>(); + configurationBuilder.Scalars().HaveConversion(); } protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs index a9e7876a427..9d2fc6f1cbe 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.PropertyMapping.cs @@ -241,7 +241,7 @@ public virtual void Does_not_throw_when_interface_generic_type_property_type_is_ public virtual void Does_not_throw_when_interface_base_type_property_type_is_ignored() { var modelBuilder = CreateConventionlessModelBuilder( - configurationBuilder => configurationBuilder.IgnoreAny()); + configurationBuilder => configurationBuilder.IgnoreAny>()); modelBuilder.Entity(typeof(InterfaceNavigationEntity)).HasNoKey(); Validate(modelBuilder); diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index 8fb537fefd2..925c0042596 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -420,6 +420,13 @@ public virtual void Int32_cannot_be_ignored() Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); } + [ConditionalFact] + public virtual void Object_cannot_be_ignored() + { + Assert.Equal(CoreStrings.UnconfigurableType("object", "Ignored"), + Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); + } + [ConditionalFact] public virtual void Can_ignore_a_property_that_is_part_of_explicit_entity_key() { @@ -527,13 +534,6 @@ public virtual void Ignoring_a_base_type_removes_relationships() Assert.Empty(model.GetEntityTypes().Single().GetForeignKeys()); } - [ConditionalFact] - public virtual void Object_cannot_be_ignored() - { - Assert.Equal(CoreStrings.UnconfigurableType("object", "Ignored"), - Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); - } - [ConditionalFact] public virtual void Properties_can_be_made_required() { @@ -938,24 +938,42 @@ private static ExpandoObject DeserializeExpandoObject(string value) return obj; } + private class ExpandoObjectConverter : ValueConverter + { + public ExpandoObjectConverter() + : base(v => (string)((IDictionary)v)["Value"], v => DeserializeExpandoObject(v)) + { + } + } + + private class ExpandoObjectComparer : ValueComparer + { + public ExpandoObjectComparer() + : base((v1, v2) => v1.SequenceEqual(v2), v => v.GetHashCode()) + { + } + } + [ConditionalFact] - public virtual void IEnumerable_properties_can_have_value_converter_configured_by_type() + public virtual void Properties_can_have_value_converter_configured_by_type() { var modelBuilder = CreateModelBuilder(c => { - c.Properties>().HaveMaxLength(20); - c.Properties().HaveConversion(typeof(ExpandoObjectConverter)); + c.Properties(typeof(IWrapped<>)).AreUnicode(false); + c.Properties().HaveMaxLength(20); + c.Properties().HaveConversion(typeof(WrappedStringToStringConverter)); }); - modelBuilder.Entity(); + modelBuilder.Entity(); var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.GetEntityTypes().Single(); - var expandoProperty = entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)); - Assert.Equal(20, expandoProperty.GetMaxLength()); - Assert.IsType(expandoProperty.GetValueConverter()); - Assert.IsType>(expandoProperty.GetValueComparer()); + var wrappedProperty = entityType.FindProperty(nameof(WrappedStringEntity.WrappedString)); + Assert.False(wrappedProperty.IsUnicode()); + Assert.Equal(20, wrappedProperty.GetMaxLength()); + Assert.IsType(wrappedProperty.GetValueConverter()); + Assert.IsType>(wrappedProperty.GetValueComparer()); } [ConditionalFact] @@ -963,28 +981,41 @@ public virtual void Value_converter_configured_on_base_type_is_not_applied() { var modelBuilder = CreateModelBuilder(c => { - c.Properties>().HaveConversion(typeof(ExpandoObjectConverter), typeof(ExpandoObjectComparer)); + c.Properties().HaveConversion(typeof(WrappedStringToStringConverter)); }); - modelBuilder.Entity(); + modelBuilder.Entity(); Assert.Equal(CoreStrings.PropertyNotMapped( - nameof(DynamicProperty), nameof(DynamicProperty.ExpandoObject), nameof(ExpandoObject)), + nameof(WrappedStringEntity), nameof(WrappedStringEntity.WrappedString), nameof(WrappedString)), Assert.Throws(() => modelBuilder.FinalizeModel()).Message); } - private class ExpandoObjectConverter : ValueConverter + private interface IWrapped { - public ExpandoObjectConverter() - : base(v => (string)((IDictionary)v)["Value"], v => DeserializeExpandoObject(v)) - { - } + T Value { get; init; } } - private class ExpandoObjectComparer : ValueComparer + private abstract class WrappedStringBase : IWrapped { - public ExpandoObjectComparer() - : base((v1, v2) => v1.SequenceEqual(v2), v => v.GetHashCode()) + public abstract string Value { get; init; } + } + + private class WrappedString : WrappedStringBase + { + public override string Value { get; init; } + } + + private class WrappedStringEntity + { + public int Id { get; set; } + public WrappedString WrappedString { get; set; } + } + + private class WrappedStringToStringConverter : ValueConverter + { + public WrappedStringToStringConverter() + : base(v => v.Value, v => new WrappedString { Value = v }) { } } @@ -994,14 +1025,14 @@ public virtual void Throws_for_conflicting_base_configurations_by_type() { var modelBuilder = CreateModelBuilder(c => { - c.Properties(); - c.IgnoreAny(); + c.Properties(); + c.IgnoreAny>(); }); Assert.Equal(CoreStrings.TypeConfigurationConflict( - nameof(INotifyPropertyChanged), "Ignored", - nameof(IEnumerable), "Property"), - Assert.Throws(() => modelBuilder.Entity()).Message); + nameof(WrappedString), "Property", + "IWrapped", "Ignored"), + Assert.Throws(() => modelBuilder.Entity()).Message); } [ConditionalFact] @@ -1373,6 +1404,23 @@ protected class StringCollectionEntity public ICollection Property { get; set; } } + [ConditionalFact] + public virtual void Object_cannot_be_configured_as_property() + { + Assert.Equal(CoreStrings.UnconfigurableType("object", "Property"), + Assert.Throws(() => CreateModelBuilder(c => c.Properties())).Message); + } + + [ConditionalFact] + public virtual void Property_bag_cannot_be_configured_as_property() + { + Assert.Equal(CoreStrings.UnconfigurableType("Dictionary", "Property"), + Assert.Throws(() => CreateModelBuilder(c => c.Properties>())).Message); + + Assert.Equal(CoreStrings.UnconfigurableType("IDictionary", "Property"), + Assert.Throws(() => CreateModelBuilder(c => c.Properties>())).Message); + } + [ConditionalFact] protected virtual void Mapping_throws_for_non_ignored_array() {