From b001aabcc1d8ab956f41946570edac8f2e2b4c27 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 30 Jul 2025 13:54:49 -0700 Subject: [PATCH] Fix saving null complex properties with nested required complex properties Change the convention to automatically configure nested properties inside complex types as complex properties instead of navigations Throw for shadow properties on value complex types Throw for complex collections not mapped to JSON Warn when mapping a collection as non-collection complex property Fix typos in string resources Part of #31237 --- ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 4 +- .../RelationalModelValidator.cs | 13 +- .../Metadata/Internal/TableBase.cs | 10 +- .../Properties/RelationalStrings.Designer.cs | 52 ++--- .../Properties/RelationalStrings.resx | 23 ++- .../Update/ColumnModification.cs | 17 +- .../Update/ModificationCommand.cs | 2 - src/EFCore/Diagnostics/CoreEventId.cs | 14 ++ .../Diagnostics/CoreLoggerExtensions.cs | 38 ++++ src/EFCore/Diagnostics/LoggingDefinitions.cs | 9 + src/EFCore/Infrastructure/ModelValidator.cs | 36 +++- .../ComplexPropertyDiscoveryConvention.cs | 7 +- .../Metadata/Internal/InternalModelBuilder.cs | 2 +- .../Metadata/Internal/MemberClassifier.cs | 7 +- src/EFCore/Metadata/Internal/Model.cs | 20 +- src/EFCore/Properties/CoreStrings.Designer.cs | 77 +++++--- src/EFCore/Properties/CoreStrings.resx | 39 ++-- .../NavigationExpandingExpressionVisitor.cs | 2 +- .../ValueGeneration/ValueGeneratorSelector.cs | 2 +- src/Shared/SharedTypeExtensions.cs | 2 + .../Migrations/MigrationsTestBase.cs | 32 ++++ .../RelationalModelBuilderTest.cs | 11 +- .../PropertyValuesRelationalTestBase.cs | 22 +++ .../CompiledModelRelationalTestBase.cs | 9 +- ...ionalModelValidatorTest.PropertyMapping.cs | 53 ++++++ .../ModelBuilderTest.ComplexCollections.cs | 63 ++++-- .../ModelBuilderTest.ComplexType.cs | 9 +- .../PropertyValuesTestBase.cs | 38 +--- .../Migrations/MigrationsSqlServerTest.cs | 23 +++ .../PropertyValuesSqlServerTest.cs | 4 +- .../DbContextModelBuilder.cs | 8 +- .../ComplexTypes/DbContextModelBuilder.cs | 180 +----------------- .../PrincipalDerivedEntityType.cs | 1 + .../SqlServerValueGeneratorSelectorTest.cs | 15 +- .../Migrations/MigrationsSqliteTest.cs | 24 ++- .../PropertyValuesSqliteTest.cs | 4 +- .../DbContextModelBuilder.cs | 8 +- .../Infrastructure/ModelValidatorTest.cs | 39 +++- .../Infrastructure/ModelValidatorTestBase.cs | 8 + 39 files changed, 555 insertions(+), 372 deletions(-) create mode 100644 test/EFCore.Relational.Specification.Tests/PropertyValuesRelationalTestBase.cs diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index e772d17ff9f..a521fa01210 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -1547,7 +1547,9 @@ private void GenerateAddMapping( } var table = tableMapping.Table; - var isOptional = table.IsOptional(typeBase); + var isOptional = typeBase.IsMappedToJson() + ? (bool?)null + : table.IsOptional(typeBase); mainBuilder .AppendLine($"{tableVariable}.AddTypeMapping({tableMappingVariable}, {code.Literal(isOptional)});") .AppendLine($"{tableMappingsVariable}.Add({tableMappingVariable});"); diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 508fbffb37e..e0161316d14 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -98,9 +98,18 @@ static void ValidateType(ITypeBase typeBase) } /// - protected override void ValidatePropertyMapping(IConventionComplexProperty complexProperty) + protected override void ValidatePropertyMapping( + IConventionComplexProperty complexProperty, + IDiagnosticsLogger logger) { - base.ValidatePropertyMapping(complexProperty); + base.ValidatePropertyMapping(complexProperty, logger); + + if (complexProperty.IsCollection && !complexProperty.ComplexType.IsMappedToJson()) + { + throw new InvalidOperationException( + RelationalStrings.ComplexCollectionNotMappedToJson( + complexProperty.DeclaringType.DisplayName(), complexProperty.Name)); + } if (!complexProperty.ComplexType.IsMappedToJson() && complexProperty.IsNullable diff --git a/src/EFCore.Relational/Metadata/Internal/TableBase.cs b/src/EFCore.Relational/Metadata/Internal/TableBase.cs index d2accdc373d..7910aea9b33 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableBase.cs @@ -126,11 +126,13 @@ public override bool IsReadOnly /// 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 void AddTypeMapping(ITableMappingBase tableMapping, bool optional) + public virtual void AddTypeMapping(ITableMappingBase tableMapping, bool? optional) { - OptionalTypes ??= new Dictionary(); - - OptionalTypes.Add(tableMapping.TypeBase, optional); + if (optional.HasValue) + { + OptionalTypes ??= []; + OptionalTypes.Add(tableMapping.TypeBase, optional.Value); + } if (tableMapping.TypeBase is IEntityType) { diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 5485801c0e6..a82b0b7286e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -95,6 +95,14 @@ public static string CompiledModelFunctionTranslation(object? function) GetString("CompiledModelFunctionTranslation", nameof(function)), function); + /// + /// The complex collection property '{entityType}.{property}' must be mapped to a JSON column. Use 'ToJson()' to configure this complex collection as mapped to a JSON column. + /// + public static string ComplexCollectionNotMappedToJson(object? entityType, object? property) + => string.Format( + GetString("ComplexCollectionNotMappedToJson", nameof(entityType), nameof(property)), + entityType, property); + /// /// Complex property '{complexProperty}' cannot have both a JSON column name ('{columnName}') and a JSON property name ('{propertyName}') configured. Use ToJson() to map to a JSON column or HasJsonPropertyName() to map as a JSON property within a containing JSON column, but not both. /// @@ -119,14 +127,6 @@ public static string ComplexPropertyJsonPropertyNameWithoutJsonMapping(object? c GetString("ComplexPropertyJsonPropertyNameWithoutJsonMapping", nameof(complexProperty)), complexProperty); - /// - /// Property '{property}' cannot have both a column name ('{columnName}') and a JSON property name ('{jsonPropertyName}') configured. Properties in JSON-mapped types should use JSON property names, not column names. - /// - public static string PropertyBothColumnNameAndJsonPropertyName(object? property, object? columnName, object? jsonPropertyName) - => string.Format( - GetString("PropertyBothColumnNameAndJsonPropertyName", nameof(property), nameof(columnName), nameof(jsonPropertyName)), - property, columnName, jsonPropertyName); - /// /// The optional complex property '{type}.{property}' is mapped to columns by flattening the contained properties, but it only contains optional properties. Add a required property or discriminator or map this complex property to a JSON column. /// @@ -674,7 +674,7 @@ public static string DuplicateSeedDataSensitive(object? entityType, object? keyV entityType, keyValue, table); /// - /// The EF.MultipoleParameters<T> method may only be used within Entity Framework LINQ queries. + /// The EF.MultipleParameters<T> method may only be used within Entity Framework LINQ queries. /// public static string EFMultipleParametersInvoked => GetString("EFMultipleParametersInvoked"); @@ -857,14 +857,6 @@ public static string ExecuteUpdateDeleteOnEntityNotMappedToTable(object? entityT GetString("ExecuteUpdateDeleteOnEntityNotMappedToTable", nameof(entityType)), entityType); - /// - /// ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead. - /// - public static string ExecuteUpdateSubqueryNotSupportedOverComplexTypes(object? complexType) - => string.Format( - GetString("ExecuteUpdateSubqueryNotSupportedOverComplexTypes", nameof(complexType)), - complexType); - /// /// ExecuteUpdate is being used over type '{structuralType}' which is mapped to JSON; ExecuteUpdate on JSON is not supported. /// @@ -873,6 +865,14 @@ public static string ExecuteUpdateOverJsonIsNotSupported(object? structuralType) GetString("ExecuteUpdateOverJsonIsNotSupported", nameof(structuralType)), structuralType); + /// + /// ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead. + /// + public static string ExecuteUpdateSubqueryNotSupportedOverComplexTypes(object? complexType) + => string.Format( + GetString("ExecuteUpdateSubqueryNotSupportedOverComplexTypes", nameof(complexType)), + complexType); + /// /// Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'. /// @@ -1366,7 +1366,7 @@ public static string MethodOnNonTphRootNotSupported(object? methodName, object? methodName, entityType); /// - /// The 'Down' method for this migration has not been implemented. Both the 'Up' abd 'Down' methods must be implemented to support reverting migrations. + /// The 'Down' method for this migration has not been implemented. Both the 'Up' and 'Down' methods must be implemented to support reverting migrations. /// public static string MigrationDownMissing => GetString("MigrationDownMissing"); @@ -1410,7 +1410,7 @@ public static string MissingParameterValue(object? parameter) parameter); /// - /// A result set was was missing when reading the results of a SaveChanges operation; this may indicate that a stored procedure was configured to return results in the EF model, but did not. Check your stored procedure definitions. + /// A result set was missing when reading the results of a SaveChanges operation; this may indicate that a stored procedure was configured to return results in the EF model, but did not. Check your stored procedure definitions. /// public static string MissingResultSetWhenSaving => GetString("MissingResultSetWhenSaving"); @@ -1647,6 +1647,14 @@ public static string PendingAmbientTransaction public static string ProjectionMappingCountMismatch => GetString("ProjectionMappingCountMismatch"); + /// + /// Property '{property}' cannot have both a column name ('{columnName}') and a JSON property name ('{jsonPropertyName}') configured. Properties in JSON-mapped types should use JSON property names, not column names. + /// + public static string PropertyBothColumnNameAndJsonPropertyName(object? property, object? columnName, object? jsonPropertyName) + => string.Format( + GetString("PropertyBothColumnNameAndJsonPropertyName", nameof(property), nameof(columnName), nameof(jsonPropertyName)), + property, columnName, jsonPropertyName); + /// /// The '{propertyType}' property '{entityType}.{property}' could not be mapped to the database type '{storeType}' because the database provider does not support mapping '{propertyType}' properties to '{storeType}' columns. Consider mapping to a different database type or converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. /// @@ -2022,7 +2030,7 @@ public static string SubqueryOverComplexTypesNotSupported(object? complexType) complexType); /// - /// The entity type '{entityType}' is not mapped to the store object '{table}'. + /// The type '{entityType}' is not mapped to the store object '{table}'. /// public static string TableNotMappedEntityType(object? entityType, object? table) => string.Format( @@ -2200,7 +2208,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec nodeType, expressionType); /// - /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. + /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'. /// public static string UnsupportedPropertyType(object? entity, object? property, object? clrType) => string.Format( @@ -3445,7 +3453,7 @@ public static EventDefinition LogKeyHasDefaultValue(IDiagnostics } /// - /// The key {keyProperties} on the entity type '{entityType}' cannot be represented in the database. Either all or some of the properties aren't mapped to table '{table}'. All key properties must be mapped to a single table for the unique constraint to be created. + /// The key {keyProperties} on the entity type '{entityType}' cannot be represented in the database. Some or all of the properties aren't mapped to table '{table}'. All key properties must be mapped to a single table for the unique constraint to be created. /// public static EventDefinition LogKeyPropertiesNotMappedToTable(IDiagnosticsLogger logger) { diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 5ef534dfacf..75e42be0127 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -148,6 +148,9 @@ The function '{function}' has a custom translation. Compiled model can't be generated, because custom function translations are not supported. + + The complex collection property '{entityType}.{property}' must be mapped to a JSON column. Use 'ToJson()' to configure this complex collection as mapped to a JSON column. + Complex property '{complexProperty}' cannot have both a JSON column name ('{columnName}') and a JSON property name ('{propertyName}') configured. Use ToJson() to map to a JSON column or HasJsonPropertyName() to map as a JSON property within a containing JSON column, but not both. @@ -157,9 +160,6 @@ Complex property '{complexProperty}' cannot use 'HasJsonPropertyName()' because it is not contained within a JSON-mapped type. Use 'ToJson()' to map the complex property to a JSON column, or ensure it is contained within a type that is mapped to JSON. - - Property '{property}' cannot have both a column name ('{columnName}') and a JSON property name ('{jsonPropertyName}') configured. Properties in JSON-mapped types should use JSON property names, not column names. - The optional complex property '{type}.{property}' is mapped to columns by flattening the contained properties, but it only contains optional properties. Add a required property or discriminator or map this complex property to a JSON column. @@ -439,12 +439,12 @@ ExecuteUpdate or ExecuteDelete was called on entity type '{entityType}', but that entity type is not mapped to a table. - - ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead. - ExecuteUpdate is being used over type '{structuralType}' which is mapped to JSON; ExecuteUpdate on JSON is not supported. + + ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead. + Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'. @@ -806,7 +806,7 @@ Warning RelationalEventId.ModelValidationKeyDefaultValueWarning string string - The key {keyProperties} on the entity type '{entityType}' cannot be represented in the database. Either all or some of the properties aren't mapped to table '{table}'. All key properties must be mapped to a single table for the unique constraint to be created. + The key {keyProperties} on the entity type '{entityType}' cannot be represented in the database. Some or all of the properties aren't mapped to table '{table}'. All key properties must be mapped to a single table for the unique constraint to be created. Error RelationalEventId.KeyPropertiesNotMappedToTable string string string @@ -962,7 +962,7 @@ Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. - The 'Down' method for this migration has not been implemented. Both the 'Up' abd 'Down' methods must be implemented to support reverting migrations. + The 'Down' method for this migration has not been implemented. Both the 'Up' and 'Down' methods must be implemented to support reverting migrations. The migration '{migrationName}' was not found. @@ -980,7 +980,7 @@ No value was provided for the required parameter '{parameter}'. - A result set was was missing when reading the results of a SaveChanges operation; this may indicate that a stored procedure was configured to return results in the EF model, but did not. Check your stored procedure definitions. + A result set was missing when reading the results of a SaveChanges operation; this may indicate that a stored procedure was configured to return results in the EF model, but did not. Check your stored procedure definitions. Cannot add commands to a completed ModificationCommandBatch. @@ -1078,6 +1078,9 @@ Unable to translate set operations when both sides don't assign values to the same properties in the nominal type. Please make sure that the same properties are included on both sides, and consider assigning default values if a property doesn't require a specific value. + + Property '{property}' cannot have both a column name ('{columnName}') and a JSON property name ('{jsonPropertyName}') configured. Properties in JSON-mapped types should use JSON property names, not column names. + The '{propertyType}' property '{entityType}.{property}' could not be mapped to the database type '{storeType}' because the database provider does not support mapping '{propertyType}' properties to '{storeType}' columns. Consider mapping to a different database type or converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'. @@ -1223,7 +1226,7 @@ The query requires a subquery over complex type '{complexType}'. Subqueries over complex types are currently unsupported. - The entity type '{entityType}' is not mapped to the store object '{table}'. + The type '{entityType}' is not mapped to the store object '{table}'. The property '{propertySpecification}' has specific configuration for the table '{table}', but isn't mapped to a column on that table. Remove the specific configuration, or map an entity type that contains this property to '{table}'. diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index a77ee091db3..a38ee3e6f4f 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -205,13 +205,16 @@ private void SetOriginalValue(object? value) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public static object? GetCurrentValue(IUpdateEntry entry, IProperty property) - => property.DeclaringType switch - { - IComplexType { ComplexProperty: var complexProperty } - when complexProperty.IsNullable && !complexProperty.IsCollection && entry.GetCurrentValue(complexProperty) == null - => null, - _ => entry.GetCurrentValue(property) - }; + => property.DeclaringType is IComplexType { ComplexProperty: var complexProperty } + && IsNull(complexProperty, entry) + ? null + : entry.GetCurrentValue(property); + + private static bool IsNull(IComplexProperty complexProperty, IUpdateEntry entry) + => (complexProperty.DeclaringType is IComplexType { ComplexProperty: var parentComplexProperty } + && IsNull(parentComplexProperty, entry)) + || (complexProperty.IsNullable && !complexProperty.IsCollection + && entry.GetCurrentValue(complexProperty) == null); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index d4a1fd522fd..6b08744f549 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -5,10 +5,8 @@ using System.Data; using System.Text; using System.Text.Json; -using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using IColumnMapping = Microsoft.EntityFrameworkCore.Metadata.IColumnMapping; using ITableMapping = Microsoft.EntityFrameworkCore.Metadata.ITableMapping; diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index b702d9ce9b8..5467ba54048 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -126,6 +126,7 @@ private enum Id SkippedEntityTypeConfigurationWarning, NoEntityTypeConfigurationsWarning, AccidentalEntityType, + AccidentalComplexPropertyCollection, // ChangeTracking events DetectChangesStarting = CoreBaseId + 800, @@ -707,6 +708,19 @@ private static EventId MakeModelValidationId(Id id) /// public static readonly EventId AccidentalEntityType = MakeModelValidationId(Id.AccidentalEntityType); + /// + /// A complex property is configured with a collection type but is not marked as a collection. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId AccidentalComplexPropertyCollection = MakeModelValidationId(Id.AccidentalComplexPropertyCollection); + /// /// The on the collection navigation property was ignored. /// diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index 32a9ad3f945..d47c7cfc9ff 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -1570,6 +1570,44 @@ public static void AccidentalEntityType( private static string AccidentalEntityType(EventDefinitionBase definition, EventData payload) => ((EventDefinition)definition).GenerateMessage(((EntityTypeEventData)payload).EntityType.DisplayName()); + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The complex property. + public static void AccidentalComplexPropertyCollection( + this IDiagnosticsLogger diagnostics, + IComplexProperty complexProperty) + { + var definition = CoreResources.LogAccidentalComplexPropertyCollection(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log( + diagnostics, + complexProperty.DeclaringType.DisplayName(), + complexProperty.Name, + complexProperty.ClrType.ShortDisplayName()); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new ComplexPropertyEventData(definition, AccidentalComplexPropertyCollection, complexProperty); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + + private static string AccidentalComplexPropertyCollection(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (ComplexPropertyEventData)payload; + return d.GenerateMessage( + p.Property.DeclaringType.DisplayName(), + p.Property.Name, + p.Property.ClrType.ShortDisplayName()); + } + /// /// Logs for the event. /// diff --git a/src/EFCore/Diagnostics/LoggingDefinitions.cs b/src/EFCore/Diagnostics/LoggingDefinitions.cs index c5c558498ba..25095796486 100644 --- a/src/EFCore/Diagnostics/LoggingDefinitions.cs +++ b/src/EFCore/Diagnostics/LoggingDefinitions.cs @@ -610,6 +610,15 @@ public abstract class LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogAccidentalEntityType; + /// + /// 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 EventDefinitionBase? LogAccidentalComplexPropertyCollection; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 6e3632a81df..2c6d4506c69 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -137,7 +137,7 @@ protected virtual void ValidatePropertyMapping( foreach (var entityType in conventionModel.GetEntityTypes()) { - ValidatePropertyMapping(entityType, conventionModel); + ValidatePropertyMapping(entityType, conventionModel, logger); } } @@ -146,7 +146,8 @@ protected virtual void ValidatePropertyMapping( /// /// The type base to validate. /// The model to validate. - protected virtual void ValidatePropertyMapping(IConventionTypeBase structuralType, IConventionModel model) + /// The logger to use. + protected virtual void ValidatePropertyMapping(IConventionTypeBase structuralType, IConventionModel model, IDiagnosticsLogger logger) { var unmappedProperty = structuralType.GetDeclaredProperties().FirstOrDefault( p => (!ConfigurationSource.Convention.Overrides(p.GetConfigurationSource()) @@ -164,9 +165,9 @@ protected virtual void ValidatePropertyMapping(IConventionTypeBase structuralTyp foreach (var complexProperty in structuralType.GetDeclaredComplexProperties()) { - ValidatePropertyMapping(complexProperty); + ValidatePropertyMapping(complexProperty, logger); - ValidatePropertyMapping(complexProperty.ComplexType, model); + ValidatePropertyMapping(complexProperty.ComplexType, model, logger); } if (structuralType.ClrType == Model.DefaultPropertyBagType) @@ -300,10 +301,12 @@ protected virtual void ValidatePropertyMapping(IConventionTypeBase structuralTyp /// Validates property mappings for a given complex property. /// /// The complex property to validate. - protected virtual void ValidatePropertyMapping(IConventionComplexProperty complexProperty) + /// The logger to use. + protected virtual void ValidatePropertyMapping(IConventionComplexProperty complexProperty, IDiagnosticsLogger logger) { var structuralType = complexProperty.DeclaringType; + // Issue #31243: Shadow complex properties are not supported if (complexProperty.IsShadowProperty()) { throw new InvalidOperationException( @@ -322,12 +325,35 @@ protected virtual void ValidatePropertyMapping(IConventionComplexProperty comple CoreStrings.EmptyComplexType(complexProperty.ComplexType.DisplayName())); } + if (!complexProperty.IsCollection && complexProperty.ClrType.IsGenericType) + { + var genericTypeDefinition = complexProperty.ClrType.GetGenericTypeDefinition(); + if (genericTypeDefinition == typeof(List<>) + || genericTypeDefinition == typeof(HashSet<>) + || genericTypeDefinition == typeof(Collection<>) + || genericTypeDefinition == typeof(ObservableCollection<>)) + { + logger.AccidentalComplexPropertyCollection((IComplexProperty)complexProperty); + } + } + // Issue #31411: Complex value type collections are not supported if (complexProperty.IsCollection && complexProperty.ComplexType.ClrType.IsValueType) { throw new InvalidOperationException( CoreStrings.ComplexValueTypeCollection(structuralType.DisplayName(), complexProperty.Name)); } + + // Issue #35337: Shadow properties on value type complex types are not supported + if (complexProperty.ComplexType.ClrType.IsValueType) + { + var shadowProperty = complexProperty.ComplexType.GetDeclaredProperties().FirstOrDefault(p => p.IsShadowProperty()); + if (shadowProperty != null) + { + throw new InvalidOperationException( + CoreStrings.ComplexValueTypeShadowProperty(complexProperty.ComplexType.DisplayName(), shadowProperty.Name)); + } + } } /// diff --git a/src/EFCore/Metadata/Conventions/ComplexPropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/ComplexPropertyDiscoveryConvention.cs index cf5450f23bf..f10a4f278c9 100644 --- a/src/EFCore/Metadata/Conventions/ComplexPropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/ComplexPropertyDiscoveryConvention.cs @@ -8,8 +8,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// -/// A convention that configures relationships between entity types based on the navigation properties -/// as long as there is no ambiguity as to which is the corresponding inverse navigation. +/// A convention that configures complex properties on structural types as long as the type has been previously configured +/// as a complex type or the declaring type is a complex type. /// /// /// See Model building conventions for more information and examples. @@ -121,7 +121,8 @@ protected virtual bool IsCandidateComplexProperty( return false; } - if (!explicitlyConfigured + if (structuralType is not IReadOnlyComplexType + && !explicitlyConfigured && model.FindIsComplexConfigurationSource(targetClrType) == null) { AddComplexCandidate(memberInfo, structuralType.Builder); diff --git a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs index c27de56f8c8..9da4bd271ae 100644 --- a/src/EFCore/Metadata/Internal/InternalModelBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalModelBuilder.cs @@ -470,7 +470,7 @@ public virtual InternalModelBuilder Complex(Type type, ConfigurationSource confi Metadata.Builder.HasNoEntityType(existingEntityType, ConfigurationSource.Convention); } - var properties = Metadata.FindProperties(type); + var properties = Metadata.FindProperties(type.UnwrapNullableType()); if (properties != null) { foreach (var property in properties) diff --git a/src/EFCore/Metadata/Internal/MemberClassifier.cs b/src/EFCore/Metadata/Internal/MemberClassifier.cs index d7300b5d5e1..26eecb14713 100644 --- a/src/EFCore/Metadata/Internal/MemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/MemberClassifier.cs @@ -218,7 +218,7 @@ public virtual bool IsCandidateComplexProperty( } var targetType = memberInfo.GetMemberType(); - if (targetType.TryGetSequenceType() is Type sequenceType + if (targetType.TryGetElementType(typeof(IList<>)) is Type sequenceType && IsCandidateComplexType(sequenceType, model, out explicitlyConfigured)) { elementType = sequenceType; @@ -230,8 +230,9 @@ public virtual bool IsCandidateComplexProperty( private static bool IsCandidateComplexType(Type targetType, IConventionModel model, out bool explicitlyConfigured) { - if (targetType.IsGenericType - && targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + if (!targetType.IsValidComplexType() + || (targetType.IsGenericType + && targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>))) { explicitlyConfigured = false; return false; diff --git a/src/EFCore/Metadata/Internal/Model.cs b/src/EFCore/Metadata/Internal/Model.cs index 847e5748186..2dc3aecc280 100644 --- a/src/EFCore/Metadata/Internal/Model.cs +++ b/src/EFCore/Metadata/Internal/Model.cs @@ -838,20 +838,7 @@ public virtual void RemoveComplexType(ComplexType complexType) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual IReadOnlySet? FindProperties(Type type) - { - if (_propertiesByType == null) - { - return null; - } - - var unwrappedType = type.UnwrapNullableType(); - if (unwrappedType.IsScalarType()) - { - return null; - } - - return _propertiesByType.GetValueOrDefault(unwrappedType); - } + => _propertiesByType?.GetValueOrDefault(type); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -862,13 +849,14 @@ public virtual void RemoveComplexType(ComplexType complexType) public virtual void AddProperty(Property property) { var type = property.ClrType.UnwrapNullableType(); - if (type.IsScalarType()) + if (type.IsScalarType() + || type.IsEnum) { return; } EnsureMutable(); - _propertiesByType ??= new Dictionary>(); + _propertiesByType ??= []; if (_propertiesByType.TryGetValue(type, out var properties)) { diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index c76b5a13959..bcdfb378ad4 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -287,7 +287,7 @@ public static string CannotConvertQueryableToEnumerableMethod => GetString("CannotConvertQueryableToEnumerableMethod"); /// - /// Cannot create an instance of reade/writer type '{readerWriterType}'. Ensure that the type can be instantiated and has a public parameterless constructor, or has a public static 'Instance' field returning the singleton instance to use. + /// Cannot create an instance of reader/writer type '{readerWriterType}'. Ensure that the type can be instantiated and has a public parameterless constructor, or has a public static 'Instance' field returning the singleton instance to use. /// public static string CannotCreateJsonValueReaderWriter(object? readerWriterType) => string.Format( @@ -343,7 +343,7 @@ public static string CannotMarkShared(object? type) type); /// - /// Unable to create an instance of entity type '{entityType}' because it is abstract. Consider making make it non-abstract or mapping at least one derived type. + /// Unable to create an instance of entity type '{entityType}' because it is abstract. Consider making it non-abstract or mapping at least one derived type. /// public static string CannotMaterializeAbstractType(object? entityType) => string.Format( @@ -358,6 +358,14 @@ public static string CanOnlyConfigureExistingNavigations(object? navigationName, GetString("CanOnlyConfigureExistingNavigations", "0_navigationName", "1_entityType"), navigationName, entityType); + /// + /// The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. + /// + public static string ChangeTrackingInterfaceMissing(object? entityType, object? changeTrackingStrategy, object? notificationInterface) + => string.Format( + GetString("ChangeTrackingInterfaceMissing", nameof(entityType), nameof(changeTrackingStrategy), nameof(notificationInterface)), + entityType, changeTrackingStrategy, notificationInterface); + /// /// Unable to save changes because a circular dependency was detected in the data to be saved: '{cycle}'. /// @@ -423,7 +431,7 @@ public static string ClashingOwnedDerivedEntityType(object? entityType, object? entityType, derivedType); /// - /// The entity type '{entityType}' cannot be configured as non-owned because it has already been configured as a owned. Use the nested builder in 'OwnsOne' or 'OwnsMany' on the owner entity type builder to further configure this type. If the entity type shouldn't be owned and you are unable to remove the 'OwnsOne' or 'OwnsMany' call you can remove the entity type from the model by calling 'Ignore'. See https://aka.ms/efcore-docs-owned for more information and examples. + /// The entity type '{entityType}' cannot be configured as non-owned because it has already been configured as owned. Use the nested builder in 'OwnsOne' or 'OwnsMany' on the owner entity type builder to further configure this type. If the entity type shouldn't be owned and you are unable to remove the 'OwnsOne' or 'OwnsMany' call you can remove the entity type from the model by calling 'Ignore'. See https://aka.ms/efcore-docs-owned for more information and examples. /// public static string ClashingOwnedEntityType(object? entityType) => string.Format( @@ -718,6 +726,14 @@ public static string ComplexValueTypeCollection(object? type, object? property) GetString("ComplexValueTypeCollection", nameof(type), nameof(property)), type, property); + /// + /// The shadow property '{type}.{property}' cannot be configured on the value type complex type '{type}'. Shadow properties are not supported on value type complex types. See https://github.com/dotnet/efcore/issues/35337 for more information. + /// + public static string ComplexValueTypeShadowProperty(object? type, object? property) + => string.Format( + GetString("ComplexValueTypeShadowProperty", nameof(type), nameof(property)), + type, property); + /// /// There are multiple properties with the [ForeignKey] attribute pointing to navigation '{1_entityType}.{0_navigation}'. To define a composite foreign key using data annotations, use the [ForeignKey] attribute on the navigation. /// @@ -727,7 +743,7 @@ public static string CompositeFkOnProperty(object? navigation, object? entityTyp navigation, entityType); /// - /// The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys configured by placing the [PrimaryKey] attribute on the entity type class, or by using 'HasKey' in 'OnModelCreating'. + /// The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys can be configured by placing the [PrimaryKey] attribute on the entity type class, or by using 'HasKey' in 'OnModelCreating'. /// public static string CompositePKWithDataAnnotation(object? entityType) => string.Format( @@ -1163,7 +1179,7 @@ public static string EFParameterInvoked => GetString("EFParameterInvoked"); /// - /// Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model. + /// Complex type '{complexType}' has no properties defined. Configure at least one property or don't include this type in the model. /// public static string EmptyComplexType(object? complexType) => string.Format( @@ -1473,15 +1489,7 @@ public static string HiLoBadBlockSize => GetString("HiLoBadBlockSize"); /// - /// The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. - /// - public static string ChangeTrackingInterfaceMissing(object? entityType, object? changeTrackingStrategy, object? notificationInterface) - => string.Format( - GetString("ChangeTrackingInterfaceMissing", nameof(entityType), nameof(changeTrackingStrategy), nameof(notificationInterface)), - entityType, changeTrackingStrategy, notificationInterface); - - /// - /// A relationship cycle involving the primary keys of the following entity types was detected: '{entityType}'. This would prevent any entity to be inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them. + /// A relationship cycle involving the primary keys of the following entity types was detected: '{entityType}'. This would prevent any entity from being inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them. /// public static string IdentifyingRelationshipCycle(object? entityType) => string.Format( @@ -1756,7 +1764,7 @@ public static string InvalidSetSameTypeWithDifferentNamespace(object? typeName, typeName, entityTypeName); /// - /// Cannot create a DbSet for '{typeName}' because it is configured as an shared-type entity type. Access the entity type via the 'Set' method overload that accepts an entity type name. + /// Cannot create a DbSet for '{typeName}' because it is configured as a shared-type entity type. Access the entity type via the 'Set' method overload that accepts an entity type name. /// public static string InvalidSetSharedType(object? typeName) => string.Format( @@ -1796,11 +1804,11 @@ public static string InvalidType(object? property, object? entityType, object? v property, entityType, valueType, propertyType); /// - /// Unable to include navigation chain '{includeExpression}' specified by 'Include' operation as the converted type '{type}' is not part of model. + /// Unable to include navigation chain '{includeExpression}' specified by 'Include' operation as the converted type '{type}' is not part of the model. /// - public static string InvalidTypeConversationWithInclude(object? includeExpression, object? type) + public static string InvalidTypeConversionWithInclude(object? includeExpression, object? type) => string.Format( - GetString("InvalidTypeConversationWithInclude", nameof(includeExpression), nameof(type)), + GetString("InvalidTypeConversionWithInclude", nameof(includeExpression), nameof(type)), includeExpression, type); /// @@ -1966,7 +1974,7 @@ public static string MemberMemberBindingNotSupported => GetString("MemberMemberBindingNotSupported"); /// - /// An asynchronous store managment operation was performed and no asynchronous seed delegate has been provided, however a synchronous seed delegate was. Set 'UseAsyncSeeding' option with a delegate equivalent to the one supplied in 'UseSeeding'. + /// An asynchronous store management operation was performed and no asynchronous seed delegate has been provided, however a synchronous seed delegate was. Set 'UseAsyncSeeding' option with a delegate equivalent to the one supplied in 'UseSeeding'. /// public static string MissingAsyncSeeder => GetString("MissingAsyncSeeder"); @@ -1980,7 +1988,7 @@ public static string MissingBackingField(object? field, object? property, object field, property, entityType); /// - /// A synchronous store managment operation was performed and no synchronous seed delegate has been provided, however an asynchronous seed delegate was. Set 'UseSeeding' option with a delegate equivalent to the one supplied in 'UseAsyncSeeding'. + /// A synchronous store management operation was performed and no synchronous seed delegate has been provided, however an asynchronous seed delegate was. Set 'UseSeeding' option with a delegate equivalent to the one supplied in 'UseAsyncSeeding'. /// public static string MissingSeeder => GetString("MissingSeeder"); @@ -3467,6 +3475,31 @@ public static class CoreResources private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.EntityFrameworkCore.Properties.CoreStrings", typeof(CoreResources).Assembly); + /// + /// The complex property '{entityType}.{property}' is configured with a collection type '{collectionType}' but is not marked as a collection. Consider using 'ComplexCollection()' to configure this as a complex collection instead. + /// + public static EventDefinition LogAccidentalComplexPropertyCollection(IDiagnosticsLogger logger) + { + var definition = ((LoggingDefinitions)logger.Definitions).LogAccidentalComplexPropertyCollection; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((LoggingDefinitions)logger.Definitions).LogAccidentalComplexPropertyCollection, + logger, + static logger => new EventDefinition( + logger.Options, + CoreEventId.AccidentalComplexPropertyCollection, + LogLevel.Warning, + "CoreEventId.AccidentalComplexPropertyCollection", + level => LoggerMessage.Define( + level, + CoreEventId.AccidentalComplexPropertyCollection, + _resourceManager.GetString("LogAccidentalComplexPropertyCollection")!))); + } + + return (EventDefinition)definition; + } + /// /// The type '{entityType}' has been mapped as an entity type. If you are mapping this type intentionally, then please suppress this warning and report the issue on GitHub. /// @@ -4543,7 +4576,7 @@ public static EventDefinition LogNavigationLazyLoading(IDiagnost } /// - /// No instantiatable types implementing `IEntityTypeConfiguration` were found while while scanning assembly '{assemblyName}'. + /// No instantiatable types implementing `IEntityTypeConfiguration` were found while scanning assembly '{assemblyName}'. /// public static EventDefinition LogNoEntityTypeConfigurationsWarning(IDiagnosticsLogger logger) { @@ -5293,7 +5326,7 @@ public static EventDefinition LogSkipCollectio } /// - /// The type '{entityTypeConfig}' was found while scanning assemblies but could not instantiated because it does not have a parameterless constructor. + /// The type '{entityTypeConfig}' was found while scanning assemblies but could not be instantiated because it does not have a parameterless constructor. /// public static EventDefinition LogSkippedEntityTypeConfigurationWarning(IDiagnosticsLogger logger) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 2a7ed7a0739..6e46a4370af 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -217,7 +217,7 @@ Unable to convert a queryable method to an enumerable method. This is likely an issue in Entity Framework, please file an issue at https://go.microsoft.com/fwlink/?linkid=2142044. - Cannot create an instance of reade/writer type '{readerWriterType}'. Ensure that the type can be instantiated and has a public parameterless constructor, or has a public static 'Instance' field returning the singleton instance to use. + Cannot create an instance of reader/writer type '{readerWriterType}'. Ensure that the type can be instantiated and has a public parameterless constructor, or has a public static 'Instance' field returning the singleton instance to use. Cannot create an instance of value comparer type '{generatorType}'. Ensure that the type can be instantiated and has a parameterless constructor, or use the overload of '{method}' that accepts a delegate. @@ -238,11 +238,14 @@ The type '{type}' cannot be marked as a shared type since an entity type with the same CLR type already exists in the model. - Unable to create an instance of entity type '{entityType}' because it is abstract. Consider making make it non-abstract or mapping at least one derived type. + Unable to create an instance of entity type '{entityType}' because it is abstract. Consider making it non-abstract or mapping at least one derived type. Navigation '{1_entityType}.{0_navigationName}' was not found. Please add the navigation to the entity type before configuring it. + + The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. + Unable to save changes because a circular dependency was detected in the data to be saved: '{cycle}'. @@ -268,7 +271,7 @@ The entity type '{entityType}' cannot be marked as non-owned because the derived entity type '{derivedType}' has been configured as owned. Either don't configure '{derivedType}' as owned, or call 'HasBaseType(null)' for it in 'OnModelCreating'. See https://aka.ms/efcore-docs-owned for more information and examples. - The entity type '{entityType}' cannot be configured as non-owned because it has already been configured as a owned. Use the nested builder in 'OwnsOne' or 'OwnsMany' on the owner entity type builder to further configure this type. If the entity type shouldn't be owned and you are unable to remove the 'OwnsOne' or 'OwnsMany' call you can remove the entity type from the model by calling 'Ignore'. See https://aka.ms/efcore-docs-owned for more information and examples. + The entity type '{entityType}' cannot be configured as non-owned because it has already been configured as owned. Use the nested builder in 'OwnsOne' or 'OwnsMany' on the owner entity type builder to further configure this type. If the entity type shouldn't be owned and you are unable to remove the 'OwnsOne' or 'OwnsMany' call you can remove the entity type from the model by calling 'Ignore'. See https://aka.ms/efcore-docs-owned for more information and examples. The entity type '{entityType}' cannot be added to the model because its CLR type has been configured as a shared type. @@ -378,11 +381,14 @@ The complex type collection '{type}.{property}' cannot be configured because complex value type collections are not supported. See https://github.com/dotnet/efcore/issues/31411 for more information. + + The shadow property '{type}.{property}' cannot be configured on the value type complex type '{type}'. Shadow properties are not supported on value type complex types. See https://github.com/dotnet/efcore/issues/35337 for more information. + There are multiple properties with the [ForeignKey] attribute pointing to navigation '{1_entityType}.{0_navigation}'. To define a composite foreign key using data annotations, use the [ForeignKey] attribute on the navigation. - The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys configured by placing the [PrimaryKey] attribute on the entity type class, or by using 'HasKey' in 'OnModelCreating'. + The entity type '{entityType}' has multiple properties with the [Key] attribute. Composite primary keys can be configured by placing the [PrimaryKey] attribute on the entity type class, or by using 'HasKey' in 'OnModelCreating'. A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913. @@ -553,7 +559,7 @@ The EF.Parameter<T> method may only be used within Entity Framework LINQ queries. - Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model. + Complex type '{complexType}' has no properties defined. Configure at least one property or don't include this type in the model. The empty string is not valid JSON. @@ -672,11 +678,8 @@ The block size used for Hi-Lo value generation is not positive. The Hi-Lo generator is usually backed by a SQL sequence and this means that the sequence increment must be positive. - - The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. - - A relationship cycle involving the primary keys of the following entity types was detected: '{entityType}'. This would prevent any entity to be inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them. + A relationship cycle involving the primary keys of the following entity types was detected: '{entityType}'. This would prevent any entity from being inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them. The instance of entity type '{entityType}' cannot be tracked because another instance with the same key value for {keyProperties} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values. @@ -782,7 +785,7 @@ Cannot create a DbSet for '{typeName}' because this type is not included in the model for the context. However the model contains an entity type with the same name in a different namespace: '{entityTypeName}'. - Cannot create a DbSet for '{typeName}' because it is configured as an shared-type entity type. Access the entity type via the 'Set' method overload that accepts an entity type name. + Cannot create a DbSet for '{typeName}' because it is configured as a shared-type entity type. Access the entity type via the 'Set' method overload that accepts an entity type name. Cannot create a DbSet for '{typeName}' because this type is not included in the model for the context. @@ -796,8 +799,8 @@ The value for property '{1_entityType}.{0_property}' cannot be set to a value of type '{valueType}' because its type is '{propertyType}'. - - Unable to include navigation chain '{includeExpression}' specified by 'Include' operation as the converted type '{type}' is not part of model. + + Unable to include navigation chain '{includeExpression}' specified by 'Include' operation as the converted type '{type}' is not part of the model. A call was made to '{useService}', but Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or build the '{service}' services to use into the service provider before passing it to '{useInternalServiceProvider}'. @@ -853,6 +856,10 @@ The type mapping for '{type}' has not implemented code literal generation. + + The complex property '{entityType}.{property}' is configured with a collection type '{collectionType}' but is not marked as a collection. Consider using 'ComplexCollection()' to configure this as a complex collection instead. + Warning CoreEventId.AccidentalComplexPropertyCollection string string string + The type '{entityType}' has been mapped as an entity type. If you are mapping this type intentionally, then please suppress this warning and report the issue on GitHub. Warning CoreEventId.AccidentalEntityType string @@ -1026,7 +1033,7 @@ Debug CoreEventId.NavigationLazyLoading string string - No instantiatable types implementing `IEntityTypeConfiguration` were found while while scanning assembly '{assemblyName}'. + No instantiatable types implementing `IEntityTypeConfiguration` were found while scanning assembly '{assemblyName}'. Warning CoreEventId.NoEntityTypeConfigurationsWarning string @@ -1146,7 +1153,7 @@ Debug CoreEventId.SkipCollectionChangeDetected int int string string string - The type '{entityTypeConfig}' was found while scanning assemblies but could not instantiated because it does not have a parameterless constructor. + The type '{entityTypeConfig}' was found while scanning assemblies but could not be instantiated because it does not have a parameterless constructor. Warning CoreEventId.SkippedEntityTypeConfigurationWarning string @@ -1199,13 +1206,13 @@ EF Core does not support MemberMemberBinding: 'new Blog { Data = { Name = "hello world" } }'. - An asynchronous store managment operation was performed and no asynchronous seed delegate has been provided, however a synchronous seed delegate was. Set 'UseAsyncSeeding' option with a delegate equivalent to the one supplied in 'UseSeeding'. + An asynchronous store management operation was performed and no asynchronous seed delegate has been provided, however a synchronous seed delegate was. Set 'UseAsyncSeeding' option with a delegate equivalent to the one supplied in 'UseSeeding'. The specified field '{field}' could not be found for property '{2_entityType}.{1_property}'. - A synchronous store managment operation was performed and no synchronous seed delegate has been provided, however an asynchronous seed delegate was. Set 'UseSeeding' option with a delegate equivalent to the one supplied in 'UseAsyncSeeding'. + A synchronous store management operation was performed and no synchronous seed delegate has been provided, however an asynchronous seed delegate was. Set 'UseSeeding' option with a delegate equivalent to the one supplied in 'UseAsyncSeeding'. Runtime metadata changes are not allowed when the model hasn't been marked as read-only. diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index bdef62ec943..41bc9f06e5d 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -2243,7 +2243,7 @@ bool TryExtractIncludeTreeNode( if (entityType == null) { throw new InvalidOperationException( - CoreStrings.InvalidTypeConversationWithInclude(expression, convertedType.ShortDisplayName())); + CoreStrings.InvalidTypeConversionWithInclude(expression, convertedType.ShortDisplayName())); } } diff --git a/src/EFCore/ValueGeneration/ValueGeneratorSelector.cs b/src/EFCore/ValueGeneration/ValueGeneratorSelector.cs index c125b416539..4faf826bf27 100644 --- a/src/EFCore/ValueGeneration/ValueGeneratorSelector.cs +++ b/src/EFCore/ValueGeneration/ValueGeneratorSelector.cs @@ -55,7 +55,7 @@ public ValueGeneratorSelector(ValueGeneratorSelectorDependencies dependencies) /// public virtual bool TrySelect(IProperty property, ITypeBase typeBase, out ValueGenerator? valueGenerator) { - valueGenerator = Cache.GetOrAdd(property, typeBase, (p, t) => Find(p, t)); + valueGenerator = Cache.GetOrAdd(property, typeBase, Find); return valueGenerator != null; } diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index b07eb8d053d..164469a01b7 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -399,7 +399,9 @@ public static MethodInfo GetGenericMethod( { typeof(DateOnly), default(DateOnly) }, { typeof(DateTime), default(DateTime) }, { typeof(DateTimeOffset), default(DateTimeOffset) }, + { typeof(decimal), default(decimal) }, { typeof(TimeOnly), default(TimeOnly) }, + { typeof(TimeSpan), default(TimeSpan) }, { typeof(long), default(long) }, { typeof(bool), default(bool) }, { typeof(double), default(double) }, diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index d5138073d2b..c890d375498 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -2785,6 +2785,7 @@ public virtual Task Create_table_with_complex_type_with_required_properties_on_d "MyComplex", ct => { ct.ComplexProperty("MyNestedComplex").IsRequired(); + ct.ComplexCollection(c => c.NestedCollection).ToJson(); }); }); }, @@ -2811,6 +2812,21 @@ public virtual Task Create_table_with_complex_type_with_required_properties_on_d { Assert.Equal("MyComplex_MyNestedComplex_Foo", c.Name); Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("MyComplex_Nested_Bar", c.Name); + Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("MyComplex_Nested_Foo", c.Name); + Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("NestedCollection", c.Name); + Assert.True(c.IsNullable); }); }); @@ -2831,6 +2847,7 @@ public virtual Task Create_table_with_optional_complex_type_with_required_proper "MyComplex", ct => { ct.ComplexProperty("MyNestedComplex"); + ct.ComplexCollection(c => c.NestedCollection).ToJson(); }); }); }, @@ -2855,6 +2872,21 @@ public virtual Task Create_table_with_optional_complex_type_with_required_proper { Assert.Equal("MyComplex_MyNestedComplex_Foo", c.Name); Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("MyComplex_Nested_Bar", c.Name); + Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("MyComplex_Nested_Foo", c.Name); + Assert.True(c.IsNullable); + }, + c => + { + Assert.Equal("NestedCollection", c.Name); + Assert.True(c.IsNullable); }); }); diff --git a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs index 0f49a904390..88283a4ca89 100644 --- a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs @@ -703,10 +703,6 @@ public virtual void Complex_property_mapped_to_json_with_nested_complex_properti .ComplexProperty(e => e.Customer, b => { b.ToJson("customer_data"); - b.ComplexProperty(c => c.Details, db => - { - db.Property(d => d.Id); - }); b.Ignore(c => c.Orders); }); @@ -745,10 +741,17 @@ public virtual void Complex_property_mapped_to_json_uses_property_name_when_colu Assert.True(complexType.IsMappedToJson()); Assert.Equal(nameof(ComplexProperties.Customer), complexType.GetContainerColumnName()); } + + // Complex collections must be mapped to JSON + public override void Complex_properties_can_be_configured_by_type() + => Assert.Throws(base.Complex_properties_can_be_configured_by_type); } public abstract class RelationalComplexCollectionTestBase(RelationalModelBuilderFixture fixture) : ComplexCollectionTestBase(fixture) { + protected override TestComplexCollectionBuilder ConfigureComplexCollection(TestComplexCollectionBuilder builder) + => builder.ToJson(); + [ConditionalFact] public virtual void Complex_collection_mapped_to_json_uses_property_name_when_column_name_not_specified() { diff --git a/test/EFCore.Relational.Specification.Tests/PropertyValuesRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/PropertyValuesRelationalTestBase.cs new file mode 100644 index 00000000000..d4c6363d48f --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/PropertyValuesRelationalTestBase.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public abstract class PropertyValuesRelationalTestBase(TFixture fixture) + : PropertyValuesTestBase(fixture) + where TFixture : PropertyValuesRelationalTestBase.PropertyValuesRelationalFixture, new() +{ + public abstract class PropertyValuesRelationalFixture : PropertyValuesFixtureBase + { + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => b.ComplexCollection(e => e.Departments, b => b.ToJson())); + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs index 155531ba223..15a06fa9d05 100644 --- a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs @@ -343,7 +343,7 @@ protected override void AssertBigModel(IModel model, bool jsonColumns) principalKey.GetReferencingForeignKeys()); Assert.Equal( - new[] { dependentBaseForeignKey, tptForeignKey, referenceOwnership, derivedSkipNavigation.Inverse.ForeignKey }, + [dependentBaseForeignKey, tptForeignKey, referenceOwnership, derivedSkipNavigation.Inverse.ForeignKey], principalBase.GetReferencingForeignKeys()); } } @@ -416,6 +416,13 @@ protected override void BuildComplexTypesModel(ModelBuilder modelBuilder) eb.ToTable("PrincipalBase"); eb.ToFunction((string?)null); }); + + modelBuilder.Entity>>( + eb => + { + eb.ComplexCollection, OwnedType>( + "ManyOwned", "OwnedCollection", eb => eb.ToJson()); + }); } [ConditionalFact] diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.PropertyMapping.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.PropertyMapping.cs index 4d86a4d33c8..bda3cdd08a5 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.PropertyMapping.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.PropertyMapping.cs @@ -1,6 +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 Microsoft.EntityFrameworkCore.Diagnostics.Internal; + // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.Infrastructure; @@ -39,4 +41,55 @@ public void Throws_when_added_property_is_not_mapped_to_store_even_if_configured "some_int_mapping"), Assert.Throws(() => Validate(modelBuilder)).Message); } + + [ConditionalFact] + public override void Detects_non_list_complex_collection() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity( + eb => + { + eb.Property(e => e.Id); + eb.ComplexCollection(e => e.Tags, + cb => + { + cb.ToJson(); + cb.Property(p => p.Key).IsRequired(); + }); + }); + + VerifyError( + CoreStrings.NonListCollection(nameof(WithReadOnlyCollection), nameof(WithReadOnlyCollection.Tags), "IReadOnlyCollection", "IList"), + modelBuilder); + } + + [ConditionalFact] + public void Throws_when_complex_collection_is_not_mapped_to_json() + { + var modelBuilder = CreateConventionlessModelBuilder(); + var entityTypeBuilder = modelBuilder.Entity(); + entityTypeBuilder.Property(e => e.Id); + entityTypeBuilder.HasKey(e => e.Id); + + var complexCollectionBuilder = entityTypeBuilder.ComplexCollection(e => e.Tags); + + Assert.Equal( + RelationalStrings.ComplexCollectionNotMappedToJson( + typeof(ComplexCollectionEntity).Name, + nameof(ComplexCollectionEntity.Tags)), + Assert.Throws(() => Validate(modelBuilder)).Message); + } + + protected class ComplexCollectionEntity + { + public int Id { get; set; } + public List Tags { get; set; } = new(); + } + + protected class ComplexTag + { + public string Name { get; set; } = ""; + public int Value { get; set; } + } } diff --git a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexCollections.cs b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexCollections.cs index f5782e88439..6f7353be740 100644 --- a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexCollections.cs +++ b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexCollections.cs @@ -13,16 +13,21 @@ public abstract partial class ModelBuilderTest { public abstract class ComplexCollectionTestBase(ModelBuilderFixtureBase fixture) : ModelBuilderTestBase(fixture) { + // This is used for common configuration that would be applied for all complex collections + protected virtual TestComplexCollectionBuilder ConfigureComplexCollection(TestComplexCollectionBuilder builder) + where TElement : notnull + => builder; + [ConditionalFact] public virtual void Can_set_complex_property_annotation() { var modelBuilder = CreateModelBuilder(); - var complexCollectionBuilder = modelBuilder + var complexCollectionBuilder = ConfigureComplexCollection(modelBuilder .Ignore() .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers) + .ComplexCollection(e => e.Customers)) .HasTypeAnnotation("foo", "bar") .HasPropertyAnnotation("foo2", "bar2") .Ignore(c => c.Details) @@ -51,12 +56,12 @@ public virtual void Can_set_property_annotation() { var modelBuilder = CreateModelBuilder(); - modelBuilder + ConfigureComplexCollection(modelBuilder .Ignore() .Ignore() .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers) + .ComplexCollection(e => e.Customers)) .Ignore(c => c.Details) .Ignore(c => c.Orders) .Property(c => c.Name).HasAnnotation("foo", "bar"); @@ -73,12 +78,12 @@ public virtual void Can_set_property_annotation_when_no_clr_property() { var modelBuilder = CreateModelBuilder(); - modelBuilder + ConfigureComplexCollection(modelBuilder .Ignore() .Ignore() .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers) + .ComplexCollection(e => e.Customers)) .Ignore(c => c.Details) .Ignore(c => c.Orders) .Property(Customer.NameProperty.Name).HasAnnotation("foo", "bar"); @@ -95,12 +100,12 @@ public virtual void Can_set_property_annotation_by_type() { var modelBuilder = CreateModelBuilder(c => c.Properties().HaveAnnotation("foo", "bar")); - modelBuilder + ConfigureComplexCollection(modelBuilder .Ignore() .Ignore() .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers) + .ComplexCollection(e => e.Customers)) .Ignore(c => c.Details) .Ignore(c => c.Orders) .Property(c => c.Name); @@ -125,6 +130,7 @@ public virtual void Properties_are_required_by_default_only_if_CLR_type_is_nulla e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up); b.Property(e => e.Down); b.Property("Charm"); @@ -157,6 +163,7 @@ public virtual void Properties_can_be_ignored() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Ignore(e => e.Up); b.Ignore(e => e.Down); b.Ignore("Charm"); @@ -184,7 +191,7 @@ public virtual void Properties_can_be_ignored_by_type() .Ignore() .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers, b => b.Ignore(c => c.Details).Ignore(c => c.Orders)); + .ComplexCollection(e => e.Customers, b => ConfigureComplexCollection(b).Ignore(c => c.Details).Ignore(c => c.Orders)); var model = modelBuilder.FinalizeModel(); var complexType = model.FindEntityType(typeof(ComplexProperties)).GetComplexProperties().Single().ComplexType; @@ -200,7 +207,9 @@ public virtual void Can_ignore_shadow_properties_when_they_have_been_added_expli .Ignore() .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers, b => b.Ignore(c => c.Details).Ignore(c => c.Orders)); + .ComplexCollection(e => e.Customers) + .Ignore(c => c.Details).Ignore(c => c.Orders); + ConfigureComplexCollection(complexCollectionBuilder); complexCollectionBuilder.Property("Shadow"); complexCollectionBuilder.Ignore("Shadow"); @@ -224,6 +233,7 @@ public virtual void Can_add_shadow_properties_when_they_have_been_ignored() e => e.Customers, b => { + ConfigureComplexCollection(b); b.Ignore(c => c.Details); b.Ignore(c => c.Orders); b.Ignore("Shadow"); @@ -249,6 +259,7 @@ public virtual void Properties_can_be_made_required() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up).IsRequired(); b.Property(e => e.Down).IsRequired(); b.Property("Charm").IsRequired(); @@ -281,6 +292,7 @@ public virtual void Properties_can_be_made_optional() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.IsRequired(false); b.Property(e => e.Down).IsRequired(false); b.Property("Strange").IsRequired(false); @@ -310,6 +322,7 @@ public virtual void Non_nullable_properties_cannot_be_made_optional() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); Assert.Equal( CoreStrings.CannotBeNullable("Up", "ComplexProperties.QuarksCollection#Quarks", "int"), Assert.Throws(() => b.Property(e => e.Up).IsRequired(false)).Message); @@ -344,6 +357,7 @@ public virtual void Properties_specified_by_string_are_shadow_properties_unless_ e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Up"); b.Property("Gluon"); b.Property("Down"); @@ -380,6 +394,7 @@ public virtual void Properties_can_have_access_mode_set() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); b.UseDefaultPropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction); b.Property(e => e.Up); @@ -410,7 +425,7 @@ public virtual void Access_mode_can_be_overridden_at_entity_and_property_levels( modelBuilder .Entity() .Ignore(e => e.Customer) - .ComplexCollection(e => e.Customers, b => b.Ignore(c => c.Details).Ignore(c => c.Orders)); + .ComplexCollection(e => e.Customers, b => ConfigureComplexCollection(b).Ignore(c => c.Details).Ignore(c => c.Orders)); modelBuilder .Entity() @@ -420,6 +435,7 @@ public virtual void Access_mode_can_be_overridden_at_entity_and_property_levels( e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.UsePropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction); b.UseDefaultPropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); b.Property(e => e.Up).UsePropertyAccessMode(PropertyAccessMode.Property); @@ -456,6 +472,7 @@ protected virtual void Properties_can_have_provider_type_set() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up); b.Property(e => e.Down).HasConversion(); b.Property("Charm").HasConversion>(); @@ -506,6 +523,7 @@ public virtual void Properties_can_have_provider_type_set_for_type() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up); b.Property(e => e.Down); b.Property("Charm"); @@ -540,6 +558,7 @@ protected virtual void Properties_can_have_non_generic_value_converter_set e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up); b.Property(e => e.Down).HasConversion(stringConverter); b.Property("Charm").HasConversion(intConverter, null, new CustomValueComparer()); @@ -581,6 +600,7 @@ protected virtual void Properties_can_have_custom_type_value_converter_type_set< e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up).HasConversion>(); b.Property(e => e.Down) .HasConversion, CustomValueComparer>(); @@ -631,6 +651,7 @@ public virtual void Properties_can_have_value_converter_set_inline() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up); b.Property(e => e.Down).HasConversion(v => int.Parse(v), v => v.ToString()); b.Property("Charm").HasConversion(v => (long)v, v => (int)v, new CustomValueComparer()); @@ -676,6 +697,7 @@ public virtual void Properties_can_have_value_converter_set() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up); b.Property(e => e.Down).HasConversion( new ValueConverter(v => int.Parse(v), v => v.ToString())); @@ -728,6 +750,7 @@ public virtual void Value_converter_configured_on_non_nullable_type_is_applied() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Wierd"); }); @@ -762,6 +785,7 @@ public virtual void Value_converter_configured_on_nullable_type_overrides_non_nu e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Wierd"); }); @@ -793,6 +817,7 @@ public virtual void Value_converter_type_is_checked() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); Assert.Equal( CoreStrings.ConverterPropertyMismatch("string", "ComplexProperties.QuarksCollection#Quarks", "Up", "int"), Assert.Throws( @@ -818,6 +843,7 @@ public virtual void Properties_can_have_field_set() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Up").HasField("_forUp"); b.Property(e => e.Down).HasField("_forDown"); b.Property("_forWierd").HasField("_forWierd"); @@ -895,6 +921,7 @@ public virtual void Can_set_max_length_for_property_type() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Charm"); b.Property("Strange"); b.Property("Top"); @@ -926,6 +953,7 @@ public virtual void Can_set_sentinel_for_properties() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up).HasSentinel(1); b.Property(e => e.Down).HasSentinel("100"); b.Property("Charm").HasSentinel(-1); @@ -964,6 +992,7 @@ public virtual void Can_set_sentinel_for_property_type() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Charm"); b.Property("Strange"); b.Property("Top"); @@ -1000,6 +1029,7 @@ public virtual void Can_set_unbounded_max_length_for_property_type() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Charm"); b.Property("Strange"); b.Property("Top"); @@ -1036,6 +1066,7 @@ public virtual void Can_set_precision_and_scale_for_property_type() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Charm"); b.Property("Strange"); b.Property("Top"); @@ -1074,6 +1105,7 @@ public virtual void Can_set_custom_value_generator_for_properties() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up).HasValueGenerator(); b.Property(e => e.Down).HasValueGenerator(typeof(CustomValueGenerator)); b.Property("Strange").HasValueGenerator(); @@ -1181,6 +1213,7 @@ public virtual void Can_set_unicode_for_properties() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property(e => e.Up).IsUnicode(); b.Property(e => e.Down).IsUnicode(false); b.Property("Charm").IsUnicode(); @@ -1219,6 +1252,7 @@ public virtual void Can_set_unicode_for_property_type() e => e.QuarksCollection, b => { + ConfigureComplexCollection(b); b.Property("Charm"); b.Property("Strange"); b.Property("Top"); @@ -1285,11 +1319,11 @@ protected virtual void Mapping_throws_for_non_ignored_navigations_on_complex_typ modelBuilder .Entity() - .ComplexCollection(e => e.Customers); + .ComplexCollection(e => e.Customers, b => ConfigureComplexCollection(b)); Assert.Equal( CoreStrings.NavigationNotAddedComplexType( - "ComplexProperties.Customers#Customer", nameof(Customer.Details), typeof(CustomerDetails).ShortDisplayName()), + "ComplexProperties.Customers#Customer", nameof(Customer.Orders), "IEnumerable"), Assert.Throws(modelBuilder.FinalizeModel).Message); } @@ -1305,7 +1339,8 @@ protected virtual void Mapping_throws_for_empty_complex_types() .Ignore(c => c.Name) .Ignore(c => c.Title) .Ignore(c => c.Id) - .Ignore(c => c.AlternateKey); + .Ignore(c => c.AlternateKey) + .Ignore(c => c.Details); Assert.Equal( CoreStrings.EmptyComplexType( diff --git a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs index d167b29151b..2d9020023e5 100644 --- a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs +++ b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs @@ -1593,8 +1593,8 @@ public virtual void Complex_properties_not_discovered_by_convention() { b.Ignore(e => e.Tuple); b.Ignore(e => e.Tuples); - b.ComplexProperty(e => e.Label, b => b.ComplexProperty(e => e.Customer)); - b.ComplexProperty(e => e.OldLabel, b => b.ComplexProperty(e => e.Customer)); + b.ComplexProperty(e => e.Label); + b.ComplexProperty(e => e.OldLabel); }); var model = modelBuilder.FinalizeModel(); @@ -1712,7 +1712,7 @@ protected virtual void Mapping_throws_for_non_ignored_navigations_on_complex_typ Assert.Equal( CoreStrings.NavigationNotAddedComplexType( - "ComplexProperties.Customer#Customer", nameof(Customer.Details), typeof(CustomerDetails).ShortDisplayName()), + "ComplexProperties.Customer#Customer", nameof(Customer.Orders), "IEnumerable"), Assert.Throws(modelBuilder.FinalizeModel).Message); } @@ -1728,7 +1728,8 @@ protected virtual void Mapping_throws_for_empty_complex_types() .Ignore(c => c.Name) .Ignore(c => c.Title) .Ignore(c => c.Id) - .Ignore(c => c.AlternateKey); + .Ignore(c => c.AlternateKey) + .Ignore(c => c.Details); Assert.Equal( CoreStrings.EmptyComplexType( diff --git a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs index 72d99d97343..5713a191631 100644 --- a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs +++ b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs @@ -3955,39 +3955,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con b.Property("Shadow1"); b.Property("Shadow2"); - b.ComplexProperty( - e => e.Culture, b => - { - b.ComplexProperty( - e => e.License, b => - { - b.ComplexProperty(e => e.Tag); - b.ComplexProperty(e => e.Tog); - }); - b.ComplexProperty( - e => e.Manufacturer, b => - { - b.ComplexProperty(e => e.Tag); - b.ComplexProperty(e => e.Tog); - }); - }); - - b.ComplexProperty( - e => e.Milk, b => - { - b.ComplexProperty( - e => e.License, b => - { - b.ComplexProperty(e => e.Tag); - b.ComplexProperty(e => e.Tog); - }); - b.ComplexProperty( - e => e.Manufacturer, b => - { - b.ComplexProperty(e => e.Tag); - b.ComplexProperty(e => e.Tog); - }); - }); + b.ComplexProperty(e => e.Culture); + b.ComplexProperty(e => e.Milk); }); modelBuilder.Entity(); @@ -3995,8 +3964,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity(); modelBuilder.Entity( - b => b.ComplexCollection( - e => e.Departments, b => b.ComplexCollection(e => e.Courses))); + b => b.ComplexCollection(e => e.Departments)); } protected override Task SeedAsync(PoolableDbContext context) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 7b3fd29ff8a..72c25e09f8a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -3637,11 +3637,34 @@ [Name] nvarchar(max) NULL, [MyComplex_Prop] nvarchar(max) NULL, [MyComplex_MyNestedComplex_Bar] datetime2 NULL, [MyComplex_MyNestedComplex_Foo] int NULL, + [MyComplex_Nested_Bar] datetime2 NULL, + [MyComplex_Nested_Foo] int NULL, + [NestedCollection] nvarchar(max) NULL, CONSTRAINT [PK_Contacts] PRIMARY KEY ([Id]) ); """); } + public override async Task Create_table_with_optional_complex_type_with_required_properties() + { + await base.Create_table_with_optional_complex_type_with_required_properties(); + + AssertSql( + """ +CREATE TABLE [Suppliers] ( + [Id] int NOT NULL IDENTITY, + [Number] int NOT NULL, + [MyComplex_Prop] nvarchar(max) NULL, + [MyComplex_MyNestedComplex_Bar] datetime2 NULL, + [MyComplex_MyNestedComplex_Foo] int NULL, + [MyComplex_Nested_Bar] datetime2 NULL, + [MyComplex_Nested_Foo] int NULL, + [NestedCollection] nvarchar(max) NULL, + CONSTRAINT [PK_Suppliers] PRIMARY KEY ([Id]) +); +"""); + } + [ConditionalFact] public override async Task Add_required_primitve_collection_to_existing_table() { diff --git a/test/EFCore.SqlServer.FunctionalTests/PropertyValuesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/PropertyValuesSqlServerTest.cs index 0802d269a13..5532778ac42 100644 --- a/test/EFCore.SqlServer.FunctionalTests/PropertyValuesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/PropertyValuesSqlServerTest.cs @@ -6,9 +6,9 @@ namespace Microsoft.EntityFrameworkCore; #nullable disable public class PropertyValuesSqlServerTest(PropertyValuesSqlServerTest.PropertyValuesSqlServerFixture fixture) - : PropertyValuesTestBase(fixture) + : PropertyValuesRelationalTestBase(fixture) { - public class PropertyValuesSqlServerFixture : PropertyValuesFixtureBase + public class PropertyValuesSqlServerFixture : PropertyValuesRelationalFixture { protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs index b54b98d5c8d..c826250a10b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs @@ -2345,7 +2345,7 @@ private IRelationalModel CreateRelationalModel() var defaultTableMappings3 = new List>(); ownedType.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings3); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0 = new TableMappingBase(ownedType, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); - microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0, false); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0, null); defaultTableMappings3.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0); var tableMappings3 = new List(); @@ -2354,7 +2354,7 @@ private IRelationalModel CreateRelationalModel() { IsSharedTablePrincipal = false, }; - principalBaseTable.AddTypeMapping(principalBaseTableMapping0, false); + principalBaseTable.AddTypeMapping(principalBaseTableMapping0, null); tableMappings3.Add(principalBaseTableMapping0); principalBaseTable.AddRowInternalForeignKey(ownedType, RelationalModel.GetForeignKey(this, "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalBase.Owned#OwnedType", @@ -2414,7 +2414,7 @@ private IRelationalModel CreateRelationalModel() var defaultTableMappings5 = new List>(); ownedType0.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings5); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2 = new TableMappingBase(ownedType0, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); - microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2, false); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2, null); defaultTableMappings5.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2); var tableMappings5 = new List(); @@ -2423,7 +2423,7 @@ private IRelationalModel CreateRelationalModel() { IsSharedTablePrincipal = false, }; - principalBaseTable.AddTypeMapping(principalBaseTableMapping2, true); + principalBaseTable.AddTypeMapping(principalBaseTableMapping2, null); tableMappings5.Add(principalBaseTableMapping2); principalBaseTable.AddRowInternalForeignKey(ownedType0, RelationalModel.GetForeignKey(this, "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalDerived>.ManyOwned#OwnedType", diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs index ffff37251cc..e7e1b974016 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs @@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Scaffolding; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Update.Internal; #pragma warning disable 219, 612, 618 @@ -405,150 +406,9 @@ private IRelationalModel CreateRelationalModel() var flagsEnum2Column = new Column("FlagsEnum2", "int", principalBaseTable); principalBaseTable.Columns.Add("FlagsEnum2", flagsEnum2Column); flagsEnum2Column.Accessors = ColumnAccessorsFactory.CreateGeneric(flagsEnum2Column); - var manyOwned_DetailsColumn = new Column("ManyOwned_Details", "varchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Details", manyOwned_DetailsColumn); - manyOwned_DetailsColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_DetailsColumn); - var manyOwned_NumberColumn = new Column("ManyOwned_Number", "int", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Number", manyOwned_NumberColumn); - manyOwned_NumberColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_NumberColumn); - var manyOwned_Principal_AlternateIdColumn = new Column("ManyOwned_Principal_AlternateId", "uniqueidentifier", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_AlternateId", manyOwned_Principal_AlternateIdColumn); - manyOwned_Principal_AlternateIdColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_AlternateIdColumn); - var manyOwned_Principal_Enum1Column = new Column("ManyOwned_Principal_Enum1", "int", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_Enum1", manyOwned_Principal_Enum1Column); - manyOwned_Principal_Enum1Column.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_Enum1Column); - var manyOwned_Principal_Enum2Column = new Column("ManyOwned_Principal_Enum2", "int", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_Enum2", manyOwned_Principal_Enum2Column); - manyOwned_Principal_Enum2Column.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_Enum2Column); - var manyOwned_Principal_FlagsEnum1Column = new Column("ManyOwned_Principal_FlagsEnum1", "int", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_FlagsEnum1", manyOwned_Principal_FlagsEnum1Column); - manyOwned_Principal_FlagsEnum1Column.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_FlagsEnum1Column); - var manyOwned_Principal_FlagsEnum2Column = new Column("ManyOwned_Principal_FlagsEnum2", "int", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_FlagsEnum2", manyOwned_Principal_FlagsEnum2Column); - manyOwned_Principal_FlagsEnum2Column.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_FlagsEnum2Column); - var manyOwned_Principal_IdColumn = new Column("ManyOwned_Principal_Id", "bigint", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_Id", manyOwned_Principal_IdColumn); - manyOwned_Principal_IdColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_IdColumn); - var manyOwned_Principal_RefTypeArrayColumn = new Column("ManyOwned_Principal_RefTypeArray", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_RefTypeArray", manyOwned_Principal_RefTypeArrayColumn); - manyOwned_Principal_RefTypeArrayColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_RefTypeArrayColumn); - var manyOwned_Principal_RefTypeEnumerableColumn = new Column("ManyOwned_Principal_RefTypeEnumerable", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_RefTypeEnumerable", manyOwned_Principal_RefTypeEnumerableColumn); - manyOwned_Principal_RefTypeEnumerableColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_RefTypeEnumerableColumn); - var manyOwned_Principal_RefTypeIListColumn = new Column("ManyOwned_Principal_RefTypeIList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_RefTypeIList", manyOwned_Principal_RefTypeIListColumn); - manyOwned_Principal_RefTypeIListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_RefTypeIListColumn); - var manyOwned_Principal_RefTypeListColumn = new Column("ManyOwned_Principal_RefTypeList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_RefTypeList", manyOwned_Principal_RefTypeListColumn); - manyOwned_Principal_RefTypeListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_RefTypeListColumn); - var manyOwned_Principal_ValueTypeArrayColumn = new Column("ManyOwned_Principal_ValueTypeArray", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_ValueTypeArray", manyOwned_Principal_ValueTypeArrayColumn); - manyOwned_Principal_ValueTypeArrayColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_ValueTypeArrayColumn); - var manyOwned_Principal_ValueTypeEnumerableColumn = new Column("ManyOwned_Principal_ValueTypeEnumerable", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_ValueTypeEnumerable", manyOwned_Principal_ValueTypeEnumerableColumn); - manyOwned_Principal_ValueTypeEnumerableColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_ValueTypeEnumerableColumn); - var manyOwned_Principal_ValueTypeIListColumn = new Column("ManyOwned_Principal_ValueTypeIList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_ValueTypeIList", manyOwned_Principal_ValueTypeIListColumn); - manyOwned_Principal_ValueTypeIListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_ValueTypeIListColumn); - var manyOwned_Principal_ValueTypeListColumn = new Column("ManyOwned_Principal_ValueTypeList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_Principal_ValueTypeList", manyOwned_Principal_ValueTypeListColumn); - manyOwned_Principal_ValueTypeListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_Principal_ValueTypeListColumn); - var manyOwned_RefTypeArrayColumn = new Column("ManyOwned_RefTypeArray", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_RefTypeArray", manyOwned_RefTypeArrayColumn); - manyOwned_RefTypeArrayColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_RefTypeArrayColumn); - var manyOwned_RefTypeEnumerableColumn = new Column("ManyOwned_RefTypeEnumerable", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_RefTypeEnumerable", manyOwned_RefTypeEnumerableColumn); - manyOwned_RefTypeEnumerableColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_RefTypeEnumerableColumn); - var manyOwned_RefTypeIListColumn = new Column("ManyOwned_RefTypeIList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_RefTypeIList", manyOwned_RefTypeIListColumn); - manyOwned_RefTypeIListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_RefTypeIListColumn); - var manyOwned_RefTypeListColumn = new Column("ManyOwned_RefTypeList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_RefTypeList", manyOwned_RefTypeListColumn); - manyOwned_RefTypeListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_RefTypeListColumn); - var manyOwned_ValueTypeArrayColumn = new Column("ManyOwned_ValueTypeArray", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_ValueTypeArray", manyOwned_ValueTypeArrayColumn); - manyOwned_ValueTypeArrayColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_ValueTypeArrayColumn); - var manyOwned_ValueTypeEnumerableColumn = new Column("ManyOwned_ValueTypeEnumerable", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_ValueTypeEnumerable", manyOwned_ValueTypeEnumerableColumn); - manyOwned_ValueTypeEnumerableColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_ValueTypeEnumerableColumn); - var manyOwned_ValueTypeIListColumn = new Column("ManyOwned_ValueTypeIList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_ValueTypeIList", manyOwned_ValueTypeIListColumn); - manyOwned_ValueTypeIListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_ValueTypeIListColumn); - var manyOwned_ValueTypeListColumn = new Column("ManyOwned_ValueTypeList", "nvarchar(max)", principalBaseTable) - { - IsNullable = true - }; - principalBaseTable.Columns.Add("ManyOwned_ValueTypeList", manyOwned_ValueTypeListColumn); - manyOwned_ValueTypeListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwned_ValueTypeListColumn); + var manyOwnedColumn = new JsonColumn("ManyOwned", "nvarchar(max)", principalBaseTable); + principalBaseTable.Columns.Add("ManyOwned", manyOwnedColumn); + manyOwnedColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(manyOwnedColumn); var owned_NumberColumn = new Column("Owned_Number", "int", principalBaseTable); principalBaseTable.Columns.Add("Owned_Number", owned_NumberColumn); owned_NumberColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(owned_NumberColumn); @@ -1419,7 +1279,7 @@ private IRelationalModel CreateRelationalModel() var defaultTableMappings3 = new List>(); ownedCollection.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings3); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3 = new TableMappingBase(ownedCollection, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); - microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3, false); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3, null); defaultTableMappings3.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_DetailsColumnBase, ownedCollection.FindProperty("Details")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_NumberColumnBase, ownedCollection.FindProperty("Number")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); @@ -1435,25 +1295,15 @@ private IRelationalModel CreateRelationalModel() var tableMappings3 = new List(); ownedCollection.SetRuntimeAnnotation("Relational:TableMappings", tableMappings3); var principalBaseTableMapping3 = new TableMapping(ownedCollection, principalBaseTable, true); - principalBaseTable.AddTypeMapping(principalBaseTableMapping3, false); + principalBaseTable.AddTypeMapping(principalBaseTableMapping3, null); tableMappings3.Add(principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_DetailsColumn, ownedCollection.FindProperty("Details")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_NumberColumn, ownedCollection.FindProperty("Number")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_RefTypeArrayColumn, ownedCollection.FindProperty("RefTypeArray")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_RefTypeEnumerableColumn, ownedCollection.FindProperty("RefTypeEnumerable")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_RefTypeIListColumn, ownedCollection.FindProperty("RefTypeIList")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_RefTypeListColumn, ownedCollection.FindProperty("RefTypeList")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_ValueTypeArrayColumn, ownedCollection.FindProperty("ValueTypeArray")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_ValueTypeEnumerableColumn, ownedCollection.FindProperty("ValueTypeEnumerable")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_ValueTypeIListColumn, ownedCollection.FindProperty("ValueTypeIList")!, principalBaseTableMapping3); - RelationalModel.CreateColumnMapping(manyOwned_ValueTypeListColumn, ownedCollection.FindProperty("ValueTypeList")!, principalBaseTableMapping3); var principalBase1 = ownedCollection.FindComplexProperty("Principal")!.ComplexType; var defaultTableMappings4 = new List>(); principalBase1.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings4); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4 = new TableMappingBase(principalBase1, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); - microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4, false); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4, null); defaultTableMappings4.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_AlternateIdColumnBase, principalBase1.FindProperty("AlternateId")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_Enum1ColumnBase, principalBase1.FindProperty("Enum1")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); @@ -1473,22 +1323,8 @@ private IRelationalModel CreateRelationalModel() var tableMappings4 = new List(); principalBase1.SetRuntimeAnnotation("Relational:TableMappings", tableMappings4); var principalBaseTableMapping4 = new TableMapping(principalBase1, principalBaseTable, true); - principalBaseTable.AddTypeMapping(principalBaseTableMapping4, true); + principalBaseTable.AddTypeMapping(principalBaseTableMapping4, null); tableMappings4.Add(principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_AlternateIdColumn, principalBase1.FindProperty("AlternateId")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_Enum1Column, principalBase1.FindProperty("Enum1")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_Enum2Column, principalBase1.FindProperty("Enum2")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_FlagsEnum1Column, principalBase1.FindProperty("FlagsEnum1")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_FlagsEnum2Column, principalBase1.FindProperty("FlagsEnum2")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_IdColumn, principalBase1.FindProperty("Id")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_RefTypeArrayColumn, principalBase1.FindProperty("RefTypeArray")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_RefTypeEnumerableColumn, principalBase1.FindProperty("RefTypeEnumerable")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_RefTypeIListColumn, principalBase1.FindProperty("RefTypeIList")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_RefTypeListColumn, principalBase1.FindProperty("RefTypeList")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_ValueTypeArrayColumn, principalBase1.FindProperty("ValueTypeArray")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_ValueTypeEnumerableColumn, principalBase1.FindProperty("ValueTypeEnumerable")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_ValueTypeIListColumn, principalBase1.FindProperty("ValueTypeIList")!, principalBaseTableMapping4); - RelationalModel.CreateColumnMapping(manyOwned_Principal_ValueTypeListColumn, principalBase1.FindProperty("ValueTypeList")!, principalBaseTableMapping4); var fK_PrincipalBase_PrincipalBase_PrincipalBaseId = new ForeignKeyConstraint( "FK_PrincipalBase_PrincipalBase_PrincipalBaseId", principalBaseTable, principalBaseTable, new[] { principalBaseIdColumn }, diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs index 4f099dcd066..d04f9ce464a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs @@ -1030,6 +1030,7 @@ public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) PrincipalComplexProperty.Create(complexType); complexType.AddAnnotation("go", "brr"); + complexType.AddAnnotation("Relational:ContainerColumnName", "ManyOwned"); complexType.AddAnnotation("Relational:FunctionName", null); complexType.AddAnnotation("Relational:Schema", null); complexType.AddAnnotation("Relational:SqlQuery", "select * from PrincipalBase"); diff --git a/test/EFCore.SqlServer.Tests/ValueGeneration/SqlServerValueGeneratorSelectorTest.cs b/test/EFCore.SqlServer.Tests/ValueGeneration/SqlServerValueGeneratorSelectorTest.cs index bc0f8b27442..c56e8263af4 100644 --- a/test/EFCore.SqlServer.Tests/ValueGeneration/SqlServerValueGeneratorSelectorTest.cs +++ b/test/EFCore.SqlServer.Tests/ValueGeneration/SqlServerValueGeneratorSelectorTest.cs @@ -197,8 +197,8 @@ public void Throws_for_unsupported_combinations() builder.Entity( b => { - b.Property(e => e.Random).ValueGeneratedOnAdd(); - b.HasKey(e => e.Random); + b.Property(e => e.TimeSpan).ValueGeneratedOnAdd(); + b.HasKey(e => e.TimeSpan); }); var model = builder.FinalizeModel(); var entityType = model.FindEntityType(typeof(AnEntity)); @@ -206,9 +206,9 @@ public void Throws_for_unsupported_combinations() var selector = SqlServerTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); Assert.Equal( - CoreStrings.NoValueGenerator("Random", "AnEntity", "Something"), + CoreStrings.NoValueGenerator("TimeSpan", "AnEntity", "TimeSpan"), #pragma warning disable CS0618 // Type or member is obsolete - Assert.Throws(() => selector.Select(entityType.FindProperty("Random"), entityType)).Message); + Assert.Throws(() => selector.Select(entityType.FindProperty("TimeSpan"), entityType)).Message); #pragma warning restore CS0618 // Type or member is obsolete } @@ -219,15 +219,15 @@ public void Returns_null_for_unsupported_combinations() builder.Entity( b => { - b.Property(e => e.Random).ValueGeneratedOnAdd(); - b.HasKey(e => e.Random); + b.Property(e => e.TimeSpan).ValueGeneratedOnAdd(); + b.HasKey(e => e.TimeSpan); }); var model = builder.FinalizeModel(); var entityType = model.FindEntityType(typeof(AnEntity))!; var selector = SqlServerTestHelpers.Instance.CreateContextServices(model).GetRequiredService(); - Assert.False(selector.TrySelect(entityType.FindProperty("Random")!, entityType, out var valueGenerator)); + Assert.False(selector.TrySelect(entityType.FindProperty("TimeSpan")!, entityType, out var valueGenerator)); Assert.Null(valueGenerator); } @@ -271,6 +271,7 @@ private class AnEntity public byte[] Binary { get; set; } public float Float { get; set; } public decimal Decimal { get; set; } + public TimeSpan TimeSpan { get; set; } [NotMapped] public Something Random { get; set; } diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs index e3a82cc168d..c97206ec9af 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs @@ -2035,7 +2035,29 @@ public override async Task Create_table_with_complex_type_with_required_properti "Number" INTEGER NULL, "MyComplex_Prop" TEXT NULL, "MyComplex_MyNestedComplex_Bar" TEXT NULL, - "MyComplex_MyNestedComplex_Foo" INTEGER NULL + "MyComplex_MyNestedComplex_Foo" INTEGER NULL, + "MyComplex_Nested_Bar" TEXT NULL, + "MyComplex_Nested_Foo" INTEGER NULL, + "NestedCollection" TEXT NULL +); +"""); + } + + public override async Task Create_table_with_optional_complex_type_with_required_properties() + { + await base.Create_table_with_optional_complex_type_with_required_properties(); + + AssertSql( + """ +CREATE TABLE "Suppliers" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_Suppliers" PRIMARY KEY AUTOINCREMENT, + "Number" INTEGER NOT NULL, + "MyComplex_Prop" TEXT NULL, + "MyComplex_MyNestedComplex_Bar" TEXT NULL, + "MyComplex_MyNestedComplex_Foo" INTEGER NULL, + "MyComplex_Nested_Bar" TEXT NULL, + "MyComplex_Nested_Foo" INTEGER NULL, + "NestedCollection" TEXT NULL ); """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/PropertyValuesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/PropertyValuesSqliteTest.cs index 5c6f1a03ce8..3baabbdf2e7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/PropertyValuesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/PropertyValuesSqliteTest.cs @@ -6,9 +6,9 @@ namespace Microsoft.EntityFrameworkCore; #nullable disable public class PropertyValuesSqliteTest(PropertyValuesSqliteTest.PropertyValuesSqliteFixture fixture) - : PropertyValuesTestBase(fixture) + : PropertyValuesRelationalTestBase(fixture) { - public class PropertyValuesSqliteFixture : PropertyValuesFixtureBase + public class PropertyValuesSqliteFixture : PropertyValuesRelationalFixture { protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs index fea276cb73c..a780c188b8f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/DbContextModelBuilder.cs @@ -2419,7 +2419,7 @@ private IRelationalModel CreateRelationalModel() var defaultTableMappings4 = new List>(); ownedType.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings4); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0 = new TableMappingBase(ownedType, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); - microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0, false); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0, null); defaultTableMappings4.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase0); var tableMappings4 = new List(); @@ -2428,7 +2428,7 @@ private IRelationalModel CreateRelationalModel() { IsSharedTablePrincipal = false, }; - principalBaseTable.AddTypeMapping(principalBaseTableMapping0, false); + principalBaseTable.AddTypeMapping(principalBaseTableMapping0, null); tableMappings4.Add(principalBaseTableMapping0); principalBaseTable.AddRowInternalForeignKey(ownedType, RelationalModel.GetForeignKey(this, "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalBase.Owned#OwnedType", @@ -2490,7 +2490,7 @@ private IRelationalModel CreateRelationalModel() var defaultTableMappings6 = new List>(); ownedType0.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings6); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2 = new TableMappingBase(ownedType0, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); - microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2, false); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2, null); defaultTableMappings6.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase2); var tableMappings6 = new List(); @@ -2499,7 +2499,7 @@ private IRelationalModel CreateRelationalModel() { IsSharedTablePrincipal = false, }; - principalBaseTable.AddTypeMapping(principalBaseTableMapping2, true); + principalBaseTable.AddTypeMapping(principalBaseTableMapping2, null); tableMappings6.Add(principalBaseTableMapping2); principalBaseTable.AddRowInternalForeignKey(ownedType0, RelationalModel.GetForeignKey(this, "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalDerived>.ManyOwned#OwnedType", diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 057bd3d8285..02487eb760f 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -41,6 +41,22 @@ public virtual void Detects_well_known_concrete_collections_mapped_as_entity_typ LoggerFactory.Clear(); } + [ConditionalFact] + public void Logs_warning_when_non_collection_complex_property_has_collection_type() + { + var modelBuilder = CreateConventionlessModelBuilder(); + var entityTypeBuilder = modelBuilder.Entity(); + entityTypeBuilder.Property(e => e.Id); + entityTypeBuilder.HasKey(e => e.Id); + + var complexPropertyBuilder = entityTypeBuilder.ComplexProperty(e => e.Foo); + + VerifyWarning( + CoreResources.LogAccidentalComplexPropertyCollection(new TestLogger()) + .GenerateMessage(nameof(WithStructCollection), nameof(WithStructCollection.Foo), "List"), + modelBuilder); + } + [ConditionalFact] // Issue #33913 public virtual void Detects_well_known_concrete_collections_mapped_as_owned_entity_type() { @@ -1153,12 +1169,6 @@ protected class WithStructCollection public List Foo { get; set; } } - protected struct StructTag - { - public required string Key { get; set; } - public required string Value { get; set; } - } - [ConditionalFact] public virtual void Detects_non_list_complex_collection() { @@ -1198,6 +1208,23 @@ public virtual void Detects_shadow_complex_properties() modelBuilder); } + [ConditionalFact] + public virtual void Detects_shadow_properties_on_value_type_complex_types() + { + var modelBuilder = CreateConventionlessModelBuilder(); + var model = modelBuilder.Model; + var entityType = model.AddEntityType(typeof(SampleEntity)); + entityType.AddProperty(nameof(SampleEntity.Id), typeof(int)); + + var complexProperty = entityType.AddComplexProperty("Tag", typeof(StructTag), typeof(StructTag)); + + complexProperty.ComplexType.AddProperty("ShadowProperty", typeof(string)); + + VerifyError( + CoreStrings.ComplexValueTypeShadowProperty("SampleEntity.Tag#StructTag", "ShadowProperty"), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_indexer_complex_properties() { diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs index dd0db43f636..cb9081c173b 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs @@ -155,6 +155,14 @@ protected class SampleEntity public ReferencedEntity AnotherReferencedEntity { get; set; } public ICollection OtherSamples { get; set; } + + protected StructTag Tag { get; set; } + } + + protected struct StructTag + { + public required string Key { get; set; } + public required string Value { get; set; } } protected class AnotherSampleEntity