diff --git a/src/EFCore/Metadata/Internal/ModelConfiguration.cs b/src/EFCore/Metadata/Internal/ModelConfiguration.cs index 879dd0eca3b..f587f35895b 100644 --- a/src/EFCore/Metadata/Internal/ModelConfiguration.cs +++ b/src/EFCore/Metadata/Internal/ModelConfiguration.cs @@ -41,6 +41,56 @@ public ModelConfiguration() public virtual bool IsEmpty() => _properties.Count == 0 && _ignoredTypes.Count == 0 && _typeMappings.Count == 0; + /// + /// 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 ModelConfiguration Validate() + { + Type? configuredType = null; + var stringType = GetConfigurationType(typeof(string), null, ref configuredType); + if (stringType != null + && stringType != TypeConfigurationType.Property) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType( + typeof(string).DisplayName(fullName: false), + stringType, + TypeConfigurationType.Property, + configuredType!.DisplayName(fullName: false))); + } + + configuredType = null; + var intType = GetConfigurationType(typeof(int?), null, ref configuredType); + if (intType != null + && intType != TypeConfigurationType.Property) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType( + typeof(int?).DisplayName(fullName: false), + intType, + TypeConfigurationType.Property, + configuredType!.DisplayName(fullName: false))); + } + + configuredType = null; + var propertyBagType = GetConfigurationType(Model.DefaultPropertyBagType, null, ref configuredType); + if (propertyBagType != null + && !propertyBagType.Value.IsEntityType()) + { + throw new InvalidOperationException( + CoreStrings.UnconfigurableType( + Model.DefaultPropertyBagType.DisplayName(fullName: false), + propertyBagType, + TypeConfigurationType.SharedTypeEntityType, + configuredType!.DisplayName(fullName: false))); + } + + return 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 @@ -69,6 +119,12 @@ public virtual bool IsEmpty() Type? configuredType = null; + if (type.IsNullableValueType()) + { + configurationType = GetConfigurationType( + Nullable.GetUnderlyingType(type)!, configurationType, ref configuredType, getBaseTypes: false); + } + if (type.IsConstructedGenericType) { configurationType = GetConfigurationType( @@ -178,23 +234,6 @@ public virtual PropertyConfiguration GetOrAddProperty(Type type) 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); @@ -232,8 +271,8 @@ public virtual bool RemoveProperty(Type type) /// public virtual PropertyConfiguration GetOrAddTypeMapping(Type type) { - var scalar = FindTypeMapping(type); - if (scalar == null) + var typeMappingConfiguration = FindTypeMapping(type); + if (typeMappingConfiguration == null) { if (type == typeof(object) || type == typeof(ExpandoObject) @@ -243,14 +282,14 @@ public virtual PropertyConfiguration GetOrAddTypeMapping(Type type) || !type.IsInstantiable()) { throw new InvalidOperationException( - CoreStrings.UnconfigurableType(type.DisplayName(fullName: false), "DefaultTypeMapping")); + CoreStrings.UnconfigurableTypeMapping(type.DisplayName(fullName: false))); } - scalar = new PropertyConfiguration(type); - _typeMappings.Add(type, scalar); + typeMappingConfiguration = new PropertyConfiguration(type); + _typeMappings.Add(type, typeMappingConfiguration); } - return scalar; + return typeMappingConfiguration; } /// @@ -272,25 +311,6 @@ public virtual PropertyConfiguration GetOrAddTypeMapping(Type type) /// public virtual void AddIgnored(Type type) { - if (type.UnwrapNullableType() == typeof(int) - || 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)); - } - RemoveProperty(type); _ignoredTypes.Add(type); } diff --git a/src/EFCore/ModelConfigurationBuilder.cs b/src/EFCore/ModelConfigurationBuilder.cs index 95572e5ce29..40c468d2099 100644 --- a/src/EFCore/ModelConfigurationBuilder.cs +++ b/src/EFCore/ModelConfigurationBuilder.cs @@ -318,7 +318,7 @@ public virtual ModelConfigurationBuilder DefaultTypeMapping( /// The dependencies object used during model building. /// The configured . public virtual ModelBuilder CreateModelBuilder(ModelDependencies? modelDependencies) - => new(_conventions, modelDependencies, _modelConfiguration.IsEmpty() ? null : _modelConfiguration); + => new(_conventions, modelDependencies, _modelConfiguration.IsEmpty() ? null : _modelConfiguration.Validate()); #region Hidden System.Object members diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index f74efdb1b76..24e63224664 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1,7 +1,9 @@ // using System; +using System.Reflection; using System.Resources; +using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; @@ -2770,12 +2772,20 @@ public static string UnableToSetIsUnique(object? isUnique, object? navigationNam isUnique, navigationName, entityType); /// - /// The type '{type}' cannot be configured as '{configuration}'. The current model building logic is unable to honor this configuration. + /// The type '{type}' cannot be configured as '{configuration}' since model building assumes that it is configured as '{expectedConfiguration}'. Remove the unsupported configuration for '{configurationType}'. /// - public static string UnconfigurableType(object? type, object? configuration) + public static string UnconfigurableType(object? type, object? configuration, object? expectedConfiguration, object? configurationType) => string.Format( - GetString("UnconfigurableType", nameof(type), nameof(configuration)), - type, configuration); + GetString("UnconfigurableType", nameof(type), nameof(configuration), nameof(expectedConfiguration), nameof(configurationType)), + type, configuration, expectedConfiguration, configurationType); + + /// + /// Default type mapping cannot be configured for the type '{type}' since it's not a valid scalar type. Remove the unsupported configuration. + /// + public static string UnconfigurableTypeMapping(object? type) + => string.Format( + GetString("UnconfigurableTypeMapping", nameof(type)), + type); /// /// Unhandled expression node type '{nodeType}'. diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 4511a3a4dc0..3bf20ef9fbd 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1501,7 +1501,10 @@ Unable to set 'IsUnique' to '{isUnique}' on the relationship associated with the navigation '{2_entityType}.{1_navigationName}' because the navigation has the opposite multiplicity. - The type '{type}' cannot be configured as '{configuration}'. The current model building logic is unable to honor this configuration. + The type '{type}' cannot be configured as '{configuration}' since model building assumes that it is configured as '{expectedConfiguration}'. Remove the unsupported configuration for '{configurationType}'. + + + Default type mapping cannot be configured for the type '{type}' since it's not a valid scalar type. Remove the unsupported configuration. Unhandled expression node type '{nodeType}'. diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index 5fbb9058378..2a35a50f7ee 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -41,7 +41,7 @@ public static Type UnwrapNullableType(this Type type) => Nullable.GetUnderlyingType(type) ?? type; public static bool IsNullableValueType(this Type type) - => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + => type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); public static bool IsNullableType(this Type type) => !type.IsValueType || type.IsNullableValueType(); @@ -312,6 +312,11 @@ public static List GetBaseTypesAndInterfacesInclusive(this Type type) type = typesToProcess.Dequeue(); baseTypes.Add(type); + if (type.IsNullableValueType()) + { + typesToProcess.Enqueue(Nullable.GetUnderlyingType(type)!); + } + if (type.IsConstructedGenericType) { typesToProcess.Enqueue(type.GetGenericTypeDefinition()); diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index 4401f2c0b1d..401f883c9d5 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -378,7 +378,7 @@ public TestModelBuilder CreateModelBuilder( IDiagnosticsLogger validationLogger) => new(Conventions, modelDependencies, - ModelConfiguration.IsEmpty() ? null : ModelConfiguration, + ModelConfiguration.IsEmpty() ? null : ModelConfiguration.Validate(), modelRuntimeInitializer, validationLogger); diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs index 80f717586d3..947dd045a2a 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs @@ -84,6 +84,7 @@ public virtual void Can_set_store_type_for_property_type() { c.Properties().HaveColumnType("smallint"); c.Properties().HaveColumnType("nchar(max)"); + c.Properties(typeof(Nullable<>)).HavePrecision(2); }); modelBuilder.Entity( @@ -91,7 +92,7 @@ public virtual void Can_set_store_type_for_property_type() { b.Property("Charm"); b.Property("Strange"); - b.Property("Top"); + b.Property("Top"); b.Property("Bottom"); }); @@ -101,9 +102,13 @@ public virtual void Can_set_store_type_for_property_type() Assert.Equal("smallint", entityType.FindProperty(Customer.IdProperty.Name).GetColumnType()); Assert.Equal("smallint", entityType.FindProperty("Up").GetColumnType()); Assert.Equal("nchar(max)", entityType.FindProperty("Down").GetColumnType()); - Assert.Equal("smallint", entityType.FindProperty("Charm").GetColumnType()); + var charm = entityType.FindProperty("Charm"); + Assert.Equal("smallint", charm.GetColumnType()); + Assert.Null(charm.GetPrecision()); Assert.Equal("nchar(max)", entityType.FindProperty("Strange").GetColumnType()); - Assert.Equal("smallint", entityType.FindProperty("Top").GetColumnType()); + var top = entityType.FindProperty("Top"); + Assert.Equal("smallint", top.GetColumnType()); + Assert.Equal(2, top.GetPrecision()); Assert.Equal("nchar(max)", entityType.FindProperty("Bottom").GetColumnType()); } diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index 6090006075e..57942f18502 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -419,14 +419,14 @@ public virtual void Properties_can_be_ignored_by_type() [ConditionalFact] public virtual void Int32_cannot_be_ignored() { - Assert.Equal(CoreStrings.UnconfigurableType("int", "Ignored"), + Assert.Equal(CoreStrings.UnconfigurableType("int?", "Ignored", "Property", "int"), Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); } [ConditionalFact] public virtual void Object_cannot_be_ignored() { - Assert.Equal(CoreStrings.UnconfigurableType("object", "Ignored"), + Assert.Equal(CoreStrings.UnconfigurableType("string", "Ignored", "Property", "object"), Assert.Throws(() => CreateModelBuilder(c => c.IgnoreAny())).Message); } @@ -1410,17 +1410,17 @@ protected class StringCollectionEntity [ConditionalFact] public virtual void Object_cannot_be_configured_as_property() { - Assert.Equal(CoreStrings.UnconfigurableType("object", "Property"), + Assert.Equal(CoreStrings.UnconfigurableType("Dictionary", "Property", "SharedTypeEntityType", "object"), 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.Equal(CoreStrings.UnconfigurableType("Dictionary", "Property", "SharedTypeEntityType", "Dictionary"), Assert.Throws(() => CreateModelBuilder(c => c.Properties>())).Message); - Assert.Equal(CoreStrings.UnconfigurableType("IDictionary", "Property"), + Assert.Equal(CoreStrings.UnconfigurableType("Dictionary", "Property", "SharedTypeEntityType", "IDictionary"), Assert.Throws(() => CreateModelBuilder(c => c.Properties>())).Message); } diff --git a/tools/Resources.tt b/tools/Resources.tt index 413f4fedce1..246cebf88a3 100644 --- a/tools/Resources.tt +++ b/tools/Resources.tt @@ -15,11 +15,14 @@ #> // +using System; +using System.Reflection; using System.Resources; <# if (!model.NoDiagnostics) { #> +using System.Threading; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; diff --git a/tools/SqliteResources.tt b/tools/SqliteResources.tt index 3069b8db653..c1448d7f601 100644 --- a/tools/SqliteResources.tt +++ b/tools/SqliteResources.tt @@ -14,6 +14,8 @@ #> // +using System; +using System.Reflection; using System.Resources; #nullable enable