From 5b556d38d35d8fd9af851b0af3feac6eed23fd98 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Thu, 10 Aug 2023 16:54:20 +0100 Subject: [PATCH] Metadata for primitive collection mapping (#31351) --- ...mosPrimitiveCollectionBuilderExtensions.cs | 56 + ...ypePrimitiveCollectionBuilderExtensions.cs | 518 ++++ ...nalComplexTypePropertyBuilderExtensions.cs | 2 - .../RelationalElementTypeBuilderExtensions.cs | 131 + .../RelationalElementTypeExtensions.cs | 133 + ...nalPrimitiveCollectionBuilderExtensions.cs | 518 ++++ .../RelationalPropertyExtensions.cs | 13 +- .../Metadata/RelationalAnnotationNames.cs | 5 + .../Storage/RelationalTypeMappingInfo.cs | 78 +- .../Storage/RelationalTypeMappingSource.cs | 153 +- ...ypePrimitiveCollectionBuilderExtensions.cs | 53 + ...verPrimitiveCollectionBuilderExtensions.cs | 53 + ...ypePrimitiveCollectionBuilderExtensions.cs | 49 + ...iteComplexTypePropertyBuilderExtensions.cs | 46 + ...itePrimitiveCollectionBuilderExtensions.cs | 46 + .../Internal/CurrentValueComparerFactory.cs | 8 +- .../Internal/ValueComparerExtensions.cs | 20 +- .../Builders/ComplexPropertyBuilder.cs | 57 + .../Builders/ComplexPropertyBuilder`.cs | 14 + .../ComplexTypePrimitiveCollectionBuilder.cs | 434 +++ .../ComplexTypePrimitiveCollectionBuilder`.cs | 311 ++ .../Builders/ComplexTypePropertyBuilder.cs | 31 - .../Builders/ComplexTypePropertyBuilder`.cs | 25 - .../Metadata/Builders/ElementTypeBuilder.cs | 307 ++ .../Metadata/Builders/EntityTypeBuilder.cs | 59 + .../Metadata/Builders/EntityTypeBuilder`.cs | 15 + .../Builders/IConventionElementTypeBuilder.cs | 321 ++ .../Builders/IConventionPropertyBuilder.cs | 16 + .../Builders/OwnedNavigationBuilder.cs | 62 + .../Builders/OwnedNavigationBuilder`.cs | 25 + .../Builders/PrimitiveCollectionBuilder.cs | 436 +++ .../Builders/PrimitiveCollectionBuilder`.cs | 313 ++ .../Metadata/Builders/PropertyBuilder.cs | 55 +- .../Metadata/Builders/PropertyBuilder`.cs | 47 +- .../Metadata/Conventions/ConventionSet.cs | 15 + ...IElementTypeAnnotationChangedConvention.cs | 28 + ...ElementTypeNullabilityChangedConvention.cs | 22 + .../IPropertyElementTypeChangedConvention.cs | 26 + .../ConventionDispatcher.ConventionScope.cs | 14 + ...entionDispatcher.DelayedConventionScope.cs | 85 +- ...tionDispatcher.ImmediateConventionScope.cs | 230 +- .../Internal/ConventionDispatcher.cs | 40 + .../PropertyDiscoveryConvention.cs | 12 +- .../Conventions/RuntimeModelConvention.cs | 116 +- src/EFCore/Metadata/IConventionElementType.cs | 215 ++ src/EFCore/Metadata/IConventionProperty.cs | 29 +- src/EFCore/Metadata/IElementType.cs | 22 + src/EFCore/Metadata/IMutableElementType.cs | 116 + src/EFCore/Metadata/IMutableProperty.cs | 33 +- src/EFCore/Metadata/IProperty.cs | 7 +- src/EFCore/Metadata/IReadOnlyElementType.cs | 165 + src/EFCore/Metadata/IReadOnlyProperty.cs | 21 +- .../Metadata/Internal/CoreAnnotationNames.cs | 11 +- src/EFCore/Metadata/Internal/ElementType.cs | 971 ++++++ .../Metadata/Internal/IMemberClassifier.cs | 9 +- .../Internal/InternalElementTypeBuilder.cs | 648 ++++ .../Internal/InternalEntityTypeBuilder.cs | 11 +- .../Internal/InternalPropertyBuilder.cs | 218 +- .../Internal/InternalTypeBaseBuilder.cs | 67 + .../Metadata/Internal/MemberClassifier.cs | 12 +- src/EFCore/Metadata/Internal/Property.cs | 112 +- src/EFCore/Metadata/RuntimeElementType.cs | 230 ++ src/EFCore/Metadata/RuntimeProperty.cs | 64 +- src/EFCore/Metadata/RuntimeTypeBase.cs | 37 +- src/EFCore/Properties/CoreStrings.Designer.cs | 32 + src/EFCore/Properties/CoreStrings.resx | 66 +- src/EFCore/Storage/CoreTypeMapping.cs | 45 +- src/EFCore/Storage/ITypeMappingSource.cs | 7 + .../Storage/Json/JsonValueReaderWriter.cs | 36 +- src/EFCore/Storage/TypeMappingInfo.cs | 130 +- src/EFCore/Storage/TypeMappingSource.cs | 147 +- src/EFCore/Storage/TypeMappingSourceBase.cs | 12 +- .../CosmosApiConsistencyTest.cs | 1 + .../JsonTypesCosmosTest.cs | 233 +- .../CosmosModelBuilderGenericTest.cs | 86 +- .../CosmosTestModelBuilderExtensions.cs | 17 + .../Design/CSharpMigrationsGeneratorTest.cs | 8 +- .../JsonTypesInMemoryTest.cs | 26 +- .../JsonTypesRelationalTestBase.cs | 66 + .../Query/JsonQueryAdHocTestBase.cs | 49 +- .../RelationalTestModelBuilderExtensions.cs | 136 + .../RelationalApiConsistencyTest.cs | 123 + .../ApiConsistencyTestBase.cs | 15 +- .../JsonTypesTestBase.cs | 2645 +++++++++++------ .../JsonTypesSqlServerTest.cs | 42 +- .../SqlServerApiConsistencyTest.cs | 14 + .../SqlServerEndToEndTest.cs | 1 + .../SqlServerModelBuilderTestBase.cs | 81 +- .../JsonTypesSqliteTest.cs | 24 +- .../SqliteApiConsistencyTest.cs | 12 +- test/EFCore.Tests/ApiConsistencyTest.cs | 7 + .../Conventions/ConventionDispatcherTest.cs | 343 +++ .../Internal/ClrPropertyGetterFactoryTest.cs | 3 + .../Internal/ClrPropertySetterFactoryTest.cs | 3 + .../ModelBuilding/ComplexTypeTestBase.cs | 548 +++- .../ModelBuilding/ModelBuilderGenericTest.cs | 166 +- .../ModelBuilderNonGenericTest.cs | 170 +- .../ModelBuilding/ModelBuilderTestBase.cs | 124 +- .../ModelBuilding/NonRelationshipTestBase.cs | 1662 +++++++++-- test/EFCore.Tests/ModelBuilding/TestModel.cs | 76 +- 100 files changed, 13007 insertions(+), 2145 deletions(-) create mode 100644 src/EFCore.Cosmos/Extensions/CosmosPrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore.Relational/Extensions/RelationalComplexTypePrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore.Relational/Extensions/RelationalElementTypeBuilderExtensions.cs create mode 100644 src/EFCore.Relational/Extensions/RelationalElementTypeExtensions.cs create mode 100644 src/EFCore.Relational/Extensions/RelationalPrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs create mode 100644 src/EFCore.Sqlite.Core/Extensions/SqlitePrimitiveCollectionBuilderExtensions.cs create mode 100644 src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder`.cs create mode 100644 src/EFCore/Metadata/Builders/ElementTypeBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/IConventionElementTypeBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder.cs create mode 100644 src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder`.cs create mode 100644 src/EFCore/Metadata/Conventions/IElementTypeAnnotationChangedConvention.cs create mode 100644 src/EFCore/Metadata/Conventions/IElementTypeNullabilityChangedConvention.cs create mode 100644 src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs create mode 100644 src/EFCore/Metadata/IConventionElementType.cs create mode 100644 src/EFCore/Metadata/IElementType.cs create mode 100644 src/EFCore/Metadata/IMutableElementType.cs create mode 100644 src/EFCore/Metadata/IReadOnlyElementType.cs create mode 100644 src/EFCore/Metadata/Internal/ElementType.cs create mode 100644 src/EFCore/Metadata/Internal/InternalElementTypeBuilder.cs create mode 100644 src/EFCore/Metadata/RuntimeElementType.cs create mode 100644 test/EFCore.Relational.Specification.Tests/JsonTypesRelationalTestBase.cs diff --git a/src/EFCore.Cosmos/Extensions/CosmosPrimitiveCollectionBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..3c580622991 --- /dev/null +++ b/src/EFCore.Cosmos/Extensions/CosmosPrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,56 @@ +// 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; + +/// +/// Cosmos-specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing Azure Cosmos DB with EF Core for more information and examples. +/// +public static class CosmosPrimitiveCollectionBuilderExtensions +{ + /// + /// Configures the property name that the property is mapped to when targeting Azure Cosmos. + /// + /// + /// + /// If an empty string is supplied, the property will not be persisted. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The name of the property. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder ToJsonProperty( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string name) + { + Check.NotNull(name, nameof(name)); + + primitiveCollectionBuilder.Metadata.SetJsonPropertyName(name); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property name that the property is mapped to when targeting Azure Cosmos. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing Azure Cosmos DB with EF Core for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the property. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder ToJsonProperty( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string name) + => (PrimitiveCollectionBuilder)ToJsonProperty((PrimitiveCollectionBuilder)primitiveCollectionBuilder, name); +} diff --git a/src/EFCore.Relational/Extensions/RelationalComplexTypePrimitiveCollectionBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalComplexTypePrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..048a41845e0 --- /dev/null +++ b/src/EFCore.Relational/Extensions/RelationalComplexTypePrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,518 @@ +// 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; + +/// +/// Relational database specific extension methods for . +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public static class RelationalComplexTypePrimitiveCollectionBuilderExtensions +{ + /// + /// Configures the column that the property maps to when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// The name of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasColumnName( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + primitiveCollectionBuilder.Metadata.SetColumnName(name); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the column that the property maps to when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasColumnName( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + => (ComplexTypePrimitiveCollectionBuilder)HasColumnName((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, name); + + /// + /// Configures the order of the column the property is mapped to. + /// + /// The builder of the property being configured. + /// The column order. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasColumnOrder(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, int? order) + { + primitiveCollectionBuilder.Metadata.SetColumnOrder(order); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the order of the column the property is mapped to. + /// + /// The builder of the property being configured. + /// The column order. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasColumnOrder(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, int? order) + => (ComplexTypePrimitiveCollectionBuilder)HasColumnOrder((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, order); + + /// + /// Configures the data type of the column that the property maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasColumnType( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? typeName) + { + Check.NullButNotEmpty(typeName, nameof(typeName)); + + primitiveCollectionBuilder.Metadata.SetColumnType(typeName); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the data type of the column that the property maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasColumnType( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? typeName) + => (ComplexTypePrimitiveCollectionBuilder)HasColumnType((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, typeName); + + /// + /// Configures the property as capable of storing only fixed-length data, such as strings. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// A value indicating whether the property is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder IsFixedLength( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + bool fixedLength = true) + { + primitiveCollectionBuilder.Metadata.SetIsFixedLength(fixedLength); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property as capable of storing only fixed-length data, such as strings. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// A value indicating whether the property is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder IsFixedLength( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + bool fixedLength = true) + => (ComplexTypePrimitiveCollectionBuilder)IsFixedLength((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, fixedLength); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a + /// relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default value constraint of + /// some sort without needing to specify exactly what it is. This can be useful when mapping EF to an + /// existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + { + primitiveCollectionBuilder.Metadata.SetDefaultValueSql(string.Empty); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + primitiveCollectionBuilder.Metadata.SetDefaultValueSql(sql); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a + /// relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default value constraint of + /// some sort without needing to specify exactly what it is. This can be useful when mapping EF to an + /// existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValueSql((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValueSql((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sql); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// + /// When called with no arguments, this method tells EF that a column is computed without needing to + /// specify the actual SQL used to computed it. This can be useful when mapping EF to an existing + /// database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComputedColumnSql(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + { + primitiveCollectionBuilder.Metadata.SetComputedColumnSql(string.Empty); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComputedColumnSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + => HasComputedColumnSql(primitiveCollectionBuilder, sql, null); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// + /// If , the computed value is calculated on row modification and stored in the database like a regular column. + /// If , the value is computed when the value is read, and does not occupy any actual storage. + /// selects the database provider default. + /// + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComputedColumnSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + bool? stored) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + primitiveCollectionBuilder.Metadata.SetComputedColumnSql(sql); + + if (stored != null) + { + primitiveCollectionBuilder.Metadata.SetIsStored(stored); + } + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// + /// When called with no arguments, this method tells EF that a column is computed without needing to + /// specify the actual SQL used to computed it. This can be useful when mapping EF to an existing + /// database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComputedColumnSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + => (ComplexTypePrimitiveCollectionBuilder)HasComputedColumnSql((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComputedColumnSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + => HasComputedColumnSql(primitiveCollectionBuilder, sql, null); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// + /// If , the computed value is calculated on row modification and stored in the database like a regular column. + /// If , the value is computed when the value is read, and does not occupy any actual storage. + /// selects the database provider default. + /// + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComputedColumnSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + bool? stored) + => (ComplexTypePrimitiveCollectionBuilder)HasComputedColumnSql((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sql, stored); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default + /// value constraint of some sort without needing to specify exactly what it is. + /// This can be useful when mapping EF to an existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + { + primitiveCollectionBuilder.Metadata.SetDefaultValue(DBNull.Value); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value) + { + primitiveCollectionBuilder.Metadata.SetDefaultValue(value); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default + /// value constraint of some sort without needing to specify exactly what it is. + /// This can be useful when mapping EF to an existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValue((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value) + => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValue((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, value); + + /// + /// Configures a comment to be applied to the column + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// The comment for the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComment( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? comment) + { + primitiveCollectionBuilder.Metadata.SetComment(comment); + + return primitiveCollectionBuilder; + } + + /// + /// Configures a comment to be applied to the column + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The comment for the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasComment( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? comment) + => (ComplexTypePrimitiveCollectionBuilder)HasComment((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, comment); + + /// + /// Configures the property to use the given collation. The database column will be created with the given + /// collation, and it will be used implicitly in all collation-sensitive operations. + /// + /// + /// See Database collations for more information and examples. + /// + /// The builder for the property being configured. + /// The collation for the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder UseCollation(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, string? collation) + { + Check.NullButNotEmpty(collation, nameof(collation)); + + primitiveCollectionBuilder.Metadata.SetCollation(collation); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property to use the given collation. The database column will be created with the given + /// collation, and it will be used implicitly in all collation-sensitive operations. + /// + /// + /// See Database collations for more information and examples. + /// + /// The builder for the property being configured. + /// The collation for the column. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder UseCollation( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? collation) + => (ComplexTypePrimitiveCollectionBuilder)UseCollation((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, collation); + + /// + /// Configures the property of an entity mapped to a JSON column, mapping the entity property to a specific JSON property, + /// rather than using the entity property name. + /// + /// The builder for the property being configured. + /// JSON property name to be used. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasJsonPropertyName( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + primitiveCollectionBuilder.Metadata.SetJsonPropertyName(name); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property of an entity mapped to a JSON column, mapping the entity property to a specific JSON property, + /// rather than using the entity property name. + /// + /// The builder for the property being configured. + /// JSON property name to be used. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasJsonPropertyName( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + => (ComplexTypePrimitiveCollectionBuilder)HasJsonPropertyName((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, name); +} diff --git a/src/EFCore.Relational/Extensions/RelationalComplexTypePropertyBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalComplexTypePropertyBuilderExtensions.cs index e2fe7925290..6b28a917b6c 100644 --- a/src/EFCore.Relational/Extensions/RelationalComplexTypePropertyBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalComplexTypePropertyBuilderExtensions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Metadata.Internal; - // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore; diff --git a/src/EFCore.Relational/Extensions/RelationalElementTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalElementTypeBuilderExtensions.cs new file mode 100644 index 00000000000..b9ee3cd2bdd --- /dev/null +++ b/src/EFCore.Relational/Extensions/RelationalElementTypeBuilderExtensions.cs @@ -0,0 +1,131 @@ +// 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; + +/// +/// Relational database specific extension methods for . +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public static class RelationalElementTypeBuilderExtensions +{ + /// + /// Configures the data type of the elements of the collection. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the elements being configured. + /// The name of the data type of the elements. + /// The same builder instance so that multiple calls can be chained. + public static ElementTypeBuilder HasStoreType( + this ElementTypeBuilder elementTypeBuilder, + string? typeName) + { + Check.NullButNotEmpty(typeName, nameof(typeName)); + + elementTypeBuilder.Metadata.SetStoreType(typeName); + + return elementTypeBuilder; + } + + /// + /// Configures the data type of the elements of the collection. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// builder for the elements being configured. + /// The name of the data type of the elements. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance if the configuration was applied, otherwise. + public static IConventionElementTypeBuilder? HasStoreType( + this IConventionElementTypeBuilder elementTypeBuilder, + string? typeName, + bool fromDataAnnotation = false) + { + if (!elementTypeBuilder.CanSetStoreType(typeName, fromDataAnnotation)) + { + return null; + } + + elementTypeBuilder.Metadata.SetStoreType(typeName, fromDataAnnotation); + return elementTypeBuilder; + } + + /// + /// Returns a value indicating whether the given data type can be set for the elements. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// builder for the elements being configured. + /// The name of the data type of the elements. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given data type can be set for the property. + public static bool CanSetStoreType( + this IConventionElementTypeBuilder elementTypeBuilder, + string? typeName, + bool fromDataAnnotation = false) + => elementTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.StoreType, typeName, fromDataAnnotation); + + /// + /// Configures the elements as capable of storing only fixed-length data, such as strings. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the elements being configured. + /// A value indicating whether the elements are constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static ElementTypeBuilder IsFixedLength( + this ElementTypeBuilder elementTypeBuilder, + bool fixedLength = true) + { + elementTypeBuilder.Metadata.SetIsFixedLength(fixedLength); + + return elementTypeBuilder; + } + + /// + /// Configures the elements as capable of storing only fixed-length data, such as strings. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// builder for the elements being configured. + /// A value indicating whether the elements are constrained to fixed length values. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance if the configuration was applied, otherwise. + public static IConventionElementTypeBuilder? IsFixedLength( + this IConventionElementTypeBuilder elementTypeBuilder, + bool? fixedLength, + bool fromDataAnnotation = false) + { + if (!elementTypeBuilder.CanSetFixedLength(fixedLength, fromDataAnnotation)) + { + return null; + } + + elementTypeBuilder.Metadata.SetIsFixedLength(fixedLength, fromDataAnnotation); + return elementTypeBuilder; + } + + /// + /// Returns a value indicating whether the elements can be configured as being fixed length or not. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// builder for the elements being configured. + /// A value indicating whether the elements are constrained to fixed length values. + /// Indicates whether the configuration was specified using a data annotation. + /// if the elements can be configured as being fixed length or not. + public static bool CanSetFixedLength( + this IConventionElementTypeBuilder elementTypeBuilder, + bool? fixedLength, + bool fromDataAnnotation = false) + => elementTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength, fromDataAnnotation); +} diff --git a/src/EFCore.Relational/Extensions/RelationalElementTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalElementTypeExtensions.cs new file mode 100644 index 00000000000..c005562fe1f --- /dev/null +++ b/src/EFCore.Relational/Extensions/RelationalElementTypeExtensions.cs @@ -0,0 +1,133 @@ +// 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; + +/// +/// extension methods for relational database metadata. +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public static class RelationalElementTypeExtensions +{ + /// + /// Returns the database type of the elements, or if the database type could not be found. + /// + /// The element. + /// + /// The database type of the elements, or if the database type could not be found. + /// + public static string? GetStoreType(this IReadOnlyElementType elementType) + => (string?)(elementType.FindAnnotation(RelationalAnnotationNames.StoreType)?.Value + ?? elementType.FindRelationalTypeMapping()?.StoreType); + + /// + /// Returns the database type of the elements. + /// + /// The element. + /// The database type of the elements. + public static string GetStoreType(this IElementType elementType) + => ((IReadOnlyElementType)elementType).GetStoreType()!; + + /// + /// Sets the database type of the elements. + /// + /// The element. + /// The value to set. + public static void SetStoreType(this IMutableElementType elementType, string? value) + => elementType.SetOrRemoveAnnotation( + RelationalAnnotationNames.StoreType, + Check.NullButNotEmpty(value, nameof(value))); + + /// + /// Sets the database type of the elements. + /// + /// The element. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetStoreType( + this IConventionElementType elementType, + string? value, + bool fromDataAnnotation = false) + => (string?)elementType.SetOrRemoveAnnotation( + RelationalAnnotationNames.StoreType, + Check.NullButNotEmpty(value, nameof(value)), + fromDataAnnotation)?.Value; + + /// + /// Gets the for the database type. + /// + /// The element. + /// The for the column name. + public static ConfigurationSource? GetStoreTypeConfigurationSource(this IConventionElementType elementType) + => elementType.FindAnnotation(RelationalAnnotationNames.StoreType)?.GetConfigurationSource(); + + /// + /// Returns a flag indicating whether the elements are capable of storing only fixed-length data, such as strings. + /// + /// The element. + /// A flag indicating whether the elements arecapable of storing only fixed-length data, such as strings. + public static bool? IsFixedLength(this IReadOnlyElementType elementType) + => (bool?)elementType.FindAnnotation(RelationalAnnotationNames.IsFixedLength)?.Value; + + /// + /// Returns a flag indicating whether the elements are capable of storing only fixed-length data, such as strings. + /// + /// The element. + /// The identifier of the table-like store object containing the column. + /// A flag indicating whether the elements are capable of storing only fixed-length data, such as strings. + public static bool? IsFixedLength(this IReadOnlyElementType elementType, in StoreObjectIdentifier storeObject) + => (bool?)elementType.FindAnnotation(RelationalAnnotationNames.IsFixedLength)?.Value; + + /// + /// Sets a flag indicating whether the elements are capable of storing only fixed-length data, such as strings. + /// + /// The element. + /// A value indicating whether the elements are constrained to fixed length values. + public static void SetIsFixedLength(this IMutableElementType elementType, bool? fixedLength) + => elementType.SetOrRemoveAnnotation(RelationalAnnotationNames.IsFixedLength, fixedLength); + + /// + /// Sets a flag indicating whether the elements are capable of storing only fixed-length data, such as strings. + /// + /// The element. + /// A value indicating whether the element are constrained to fixed length values. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetIsFixedLength( + this IConventionElementType elementType, + bool? fixedLength, + bool fromDataAnnotation = false) + => (bool?)elementType.SetOrRemoveAnnotation( + RelationalAnnotationNames.IsFixedLength, + fixedLength, + fromDataAnnotation)?.Value; + + /// + /// Gets the for . + /// + /// The element. + /// The for . + public static ConfigurationSource? GetIsFixedLengthConfigurationSource(this IConventionElementType elementType) + => elementType.FindAnnotation(RelationalAnnotationNames.IsFixedLength)?.GetConfigurationSource(); + + /// + /// Returns the for the given element on a finalized model. + /// + /// The element. + /// The type mapping. + [DebuggerStepThrough] + public static RelationalTypeMapping GetRelationalTypeMapping(this IReadOnlyElementType elementType) + => (RelationalTypeMapping)elementType.GetTypeMapping(); + + /// + /// Returns the for the given element on a finalized model. + /// + /// The element. + /// The type mapping, or if none was found. + [DebuggerStepThrough] + public static RelationalTypeMapping? FindRelationalTypeMapping(this IReadOnlyElementType elementType) + => (RelationalTypeMapping?)elementType.FindTypeMapping(); +} diff --git a/src/EFCore.Relational/Extensions/RelationalPrimitiveCollectionBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..10980241fc8 --- /dev/null +++ b/src/EFCore.Relational/Extensions/RelationalPrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,518 @@ +// 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; + +/// +/// Relational database specific extension methods for . +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public static class RelationalPrimitiveCollectionBuilderExtensions +{ + /// + /// Configures the column that the property maps to when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// The name of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasColumnName( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + primitiveCollectionBuilder.Metadata.SetColumnName(name); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the column that the property maps to when targeting a relational database. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasColumnName( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + => (PrimitiveCollectionBuilder)HasColumnName((PrimitiveCollectionBuilder)primitiveCollectionBuilder, name); + + /// + /// Configures the order of the column the property is mapped to. + /// + /// The builder of the property being configured. + /// The column order. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasColumnOrder(this PrimitiveCollectionBuilder primitiveCollectionBuilder, int? order) + { + primitiveCollectionBuilder.Metadata.SetColumnOrder(order); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the order of the column the property is mapped to. + /// + /// The builder of the property being configured. + /// The column order. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasColumnOrder(this PrimitiveCollectionBuilder primitiveCollectionBuilder, int? order) + => (PrimitiveCollectionBuilder)HasColumnOrder((PrimitiveCollectionBuilder)primitiveCollectionBuilder, order); + + /// + /// Configures the data type of the column that the property maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasColumnType( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? typeName) + { + Check.NullButNotEmpty(typeName, nameof(typeName)); + + primitiveCollectionBuilder.Metadata.SetColumnType(typeName); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the data type of the column that the property maps to when targeting a relational database. + /// This should be the complete type name, including precision, scale, length, etc. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The name of the data type of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasColumnType( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? typeName) + => (PrimitiveCollectionBuilder)HasColumnType((PrimitiveCollectionBuilder)primitiveCollectionBuilder, typeName); + + /// + /// Configures the property as capable of storing only fixed-length data, such as strings. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// A value indicating whether the property is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static PrimitiveCollectionBuilder IsFixedLength( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + bool fixedLength = true) + { + primitiveCollectionBuilder.Metadata.SetIsFixedLength(fixedLength); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property as capable of storing only fixed-length data, such as strings. + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// A value indicating whether the property is constrained to fixed length values. + /// The same builder instance so that multiple configuration calls can be chained. + public static PrimitiveCollectionBuilder IsFixedLength( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + bool fixedLength = true) + => (PrimitiveCollectionBuilder)IsFixedLength((PrimitiveCollectionBuilder)primitiveCollectionBuilder, fixedLength); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a + /// relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default value constraint of + /// some sort without needing to specify exactly what it is. This can be useful when mapping EF to an + /// existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValueSql(this PrimitiveCollectionBuilder primitiveCollectionBuilder) + { + primitiveCollectionBuilder.Metadata.SetDefaultValueSql(string.Empty); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValueSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + primitiveCollectionBuilder.Metadata.SetDefaultValueSql(sql); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a + /// relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default value constraint of + /// some sort without needing to specify exactly what it is. This can be useful when mapping EF to an + /// existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValueSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder) + => (PrimitiveCollectionBuilder)HasDefaultValueSql((PrimitiveCollectionBuilder)primitiveCollectionBuilder); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValueSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + => (PrimitiveCollectionBuilder)HasDefaultValueSql((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sql); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// + /// When called with no arguments, this method tells EF that a column is computed without needing to + /// specify the actual SQL used to computed it. This can be useful when mapping EF to an existing + /// database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComputedColumnSql(this PrimitiveCollectionBuilder primitiveCollectionBuilder) + { + primitiveCollectionBuilder.Metadata.SetComputedColumnSql(string.Empty); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComputedColumnSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + => HasComputedColumnSql(primitiveCollectionBuilder, sql, null); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// + /// If , the computed value is calculated on row modification and stored in the database like a regular column. + /// If , the value is computed when the value is read, and does not occupy any actual storage. + /// selects the database provider default. + /// + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComputedColumnSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + bool? stored) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + primitiveCollectionBuilder.Metadata.SetComputedColumnSql(sql); + + if (stored != null) + { + primitiveCollectionBuilder.Metadata.SetIsStored(stored); + } + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// + /// When called with no arguments, this method tells EF that a column is computed without needing to + /// specify the actual SQL used to computed it. This can be useful when mapping EF to an existing + /// database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComputedColumnSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder) + => (PrimitiveCollectionBuilder)HasComputedColumnSql((PrimitiveCollectionBuilder)primitiveCollectionBuilder); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComputedColumnSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql) + => HasComputedColumnSql(primitiveCollectionBuilder, sql, null); + + /// + /// Configures the property to map to a computed column when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression that computes values for the column. + /// + /// If , the computed value is calculated on row modification and stored in the database like a regular column. + /// If , the value is computed when the value is read, and does not occupy any actual storage. + /// selects the database provider default. + /// + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComputedColumnSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + bool? stored) + => (PrimitiveCollectionBuilder)HasComputedColumnSql((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sql, stored); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default + /// value constraint of some sort without needing to specify exactly what it is. + /// This can be useful when mapping EF to an existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValue(this PrimitiveCollectionBuilder primitiveCollectionBuilder) + { + primitiveCollectionBuilder.Metadata.SetDefaultValue(DBNull.Value); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValue( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value) + { + primitiveCollectionBuilder.Metadata.SetDefaultValue(value); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// + /// When called with no argument, this method tells EF that a column has a default + /// value constraint of some sort without needing to specify exactly what it is. + /// This can be useful when mapping EF to an existing database. + /// + /// + /// See Database default values for more information and examples. + /// + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValue( + this PrimitiveCollectionBuilder primitiveCollectionBuilder) + => (PrimitiveCollectionBuilder)HasDefaultValue((PrimitiveCollectionBuilder)primitiveCollectionBuilder); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The default value of the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValue( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value) + => (PrimitiveCollectionBuilder)HasDefaultValue((PrimitiveCollectionBuilder)primitiveCollectionBuilder, value); + + /// + /// Configures a comment to be applied to the column + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The builder for the property being configured. + /// The comment for the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComment( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? comment) + { + primitiveCollectionBuilder.Metadata.SetComment(comment); + + return primitiveCollectionBuilder; + } + + /// + /// Configures a comment to be applied to the column + /// + /// + /// See Modeling entity types and relationships for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The comment for the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasComment( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? comment) + => (PrimitiveCollectionBuilder)HasComment((PrimitiveCollectionBuilder)primitiveCollectionBuilder, comment); + + /// + /// Configures the property to use the given collation. The database column will be created with the given + /// collation, and it will be used implicitly in all collation-sensitive operations. + /// + /// + /// See Database collations for more information and examples. + /// + /// The builder for the property being configured. + /// The collation for the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder UseCollation(this PrimitiveCollectionBuilder primitiveCollectionBuilder, string? collation) + { + Check.NullButNotEmpty(collation, nameof(collation)); + + primitiveCollectionBuilder.Metadata.SetCollation(collation); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property to use the given collation. The database column will be created with the given + /// collation, and it will be used implicitly in all collation-sensitive operations. + /// + /// + /// See Database collations for more information and examples. + /// + /// The builder for the property being configured. + /// The collation for the column. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder UseCollation( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? collation) + => (PrimitiveCollectionBuilder)UseCollation((PrimitiveCollectionBuilder)primitiveCollectionBuilder, collation); + + /// + /// Configures the property of an entity mapped to a JSON column, mapping the entity property to a specific JSON property, + /// rather than using the entity property name. + /// + /// The builder for the property being configured. + /// JSON property name to be used. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasJsonPropertyName( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + { + Check.NullButNotEmpty(name, nameof(name)); + + primitiveCollectionBuilder.Metadata.SetJsonPropertyName(name); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the property of an entity mapped to a JSON column, mapping the entity property to a specific JSON property, + /// rather than using the entity property name. + /// + /// The builder for the property being configured. + /// JSON property name to be used. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasJsonPropertyName( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? name) + => (PrimitiveCollectionBuilder)HasJsonPropertyName((PrimitiveCollectionBuilder)primitiveCollectionBuilder, name); +} diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index e8fded6f6e6..187f3297208 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -67,7 +67,7 @@ public static string GetColumnName(this IReadOnlyProperty property) { tableFound = true; } - else if(property.DeclaringType is IReadOnlyEntityType declaringEntityType) + else if (property.DeclaringType is IReadOnlyEntityType declaringEntityType) { foreach (var containingType in declaringEntityType.GetDerivedTypesInclusive()) { @@ -84,7 +84,7 @@ public static string GetColumnName(this IReadOnlyProperty property) return null; } } - else + else { var declaringEntityType = property.DeclaringType.ContainingEntityType; if (declaringEntityType.GetMappingStrategy() != RelationalAnnotationNames.TpcMappingStrategy) @@ -213,7 +213,6 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property) return sharedTablePrincipalConcurrencyProperty.GetColumnName(storeObject)!; } - StringBuilder? builder = null; var currentStoreObject = storeObject; if (property.DeclaringType is IReadOnlyEntityType entityType) @@ -242,8 +241,8 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property) } } else if (StoreObjectIdentifier.Create(property.DeclaringType, currentStoreObject.StoreObjectType) == currentStoreObject - || property.DeclaringType.GetMappingFragments(storeObject.StoreObjectType) - .Any(f => f.StoreObject == currentStoreObject)) + || property.DeclaringType.GetMappingFragments(storeObject.StoreObjectType) + .Any(f => f.StoreObject == currentStoreObject)) { var complexType = (IReadOnlyComplexType)property.DeclaringType; builder ??= new StringBuilder(); @@ -414,6 +413,8 @@ public static void SetColumnOrder(this IMutableProperty property, int? order) /// be found. /// public static string? GetColumnType(this IReadOnlyProperty property) + // Note that the type-mapped store type is used in preference to the annotation, since the annotation may + // be an incomplete type name like `varchar` which will become `varchar(64)` after the max length facet is required. => (string?)(property.FindRelationalTypeMapping()?.StoreType ?? property.FindAnnotation(RelationalAnnotationNames.ColumnType)?.Value); @@ -1176,7 +1177,7 @@ public static bool IsColumnNullable(this IReadOnlyProperty property, in StoreObj return property.IsNullable || (property.DeclaringType is IReadOnlyEntityType entityType && ((entityType.BaseType != null - && entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy) + && entityType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy) || IsOptionalSharingDependent(entityType, storeObject, 0))); } diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index 94972e366ef..2aee5c9cef4 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -329,4 +329,9 @@ public static class RelationalAnnotationNames /// The JSON property name for the element that the property/navigation maps to. /// public const string JsonPropertyName = Prefix + "JsonPropertyName"; + + /// + /// The name for store (database) type annotations. + /// + public const string StoreType = Prefix + "StoreType"; } diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs index 00c3f096b09..3c21d0202ed 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs @@ -27,6 +27,50 @@ public RelationalTypeMappingInfo(IProperty property) { } + /// + /// Creates a new instance of . + /// + /// The collection element for which mapping is needed. + /// The provider-specific relational type name for which mapping is needed. + /// The provider-specific relational type name, with any facets removed. + /// Specifies Unicode or ANSI for the mapping or for the default. + /// Specifies a fixed length mapping, or for the default. + /// + /// Specifies a size for the mapping, in case one isn't found at the core level, or for the + /// default. + /// + /// + /// Specifies a precision for the mapping, in case one isn't found at the core level, or + /// for the default. + /// + /// + /// Specifies a scale for the mapping, in case one isn't found at the core level, or for + /// the default. + /// + public RelationalTypeMappingInfo( + IElementType elementType, + string? storeTypeName = null, + string? storeTypeNameBase = null, + bool? fallbackUnicode = null, + bool? fallbackFixedLength = null, + int? fallbackSize = null, + int? fallbackPrecision = null, + int? fallbackScale = null) + { + _coreTypeMappingInfo = new TypeMappingInfo(elementType, fallbackUnicode, fallbackSize, fallbackPrecision, fallbackScale); + + fallbackFixedLength ??= elementType.IsFixedLength(); + storeTypeName ??= (string?)elementType[RelationalAnnotationNames.StoreType]; + + var customConverter = elementType.GetValueConverter(); + var mappingHints = customConverter?.MappingHints; + + IsFixedLength = fallbackFixedLength ?? (mappingHints as RelationalConverterMappingHints)?.IsFixedLength; + DbType = (mappingHints as RelationalConverterMappingHints)?.DbType; + StoreTypeName = storeTypeName; + StoreTypeNameBase = storeTypeNameBase; + } + /// /// Creates a new instance of . /// @@ -164,23 +208,6 @@ public RelationalTypeMappingInfo( DbType = source.DbType ?? (mappingHints as RelationalConverterMappingHints)?.DbType; } - /// - /// Creates a new instance of with the given - /// for collection elements. - /// - /// The source info. - /// The element mapping to use. - public RelationalTypeMappingInfo( - in RelationalTypeMappingInfo source, - RelationalTypeMapping elementMapping) - { - _coreTypeMappingInfo = source._coreTypeMappingInfo.WithElementTypeMapping(elementMapping); - StoreTypeName = source.StoreTypeName; - StoreTypeNameBase = source.StoreTypeNameBase; - IsFixedLength = source.IsFixedLength; - DbType = source.DbType; - } - /// /// Creates a new instance of . /// @@ -305,15 +332,6 @@ public Type? ClrType init => _coreTypeMappingInfo = _coreTypeMappingInfo with { ClrType = value }; } - /// - /// The element type mapping, if the mapping is for a collection of primitives, or otherwise. - /// - public CoreTypeMapping? ElementTypeMapping - { - get => _coreTypeMappingInfo.ElementTypeMapping; - init => _coreTypeMappingInfo = _coreTypeMappingInfo with { ElementTypeMapping = value }; - } - /// /// The JSON reader/writer, if one has been provided, or otherwise. /// @@ -330,12 +348,4 @@ public JsonValueReaderWriter? JsonValueReaderWriter /// The new mapping info. public RelationalTypeMappingInfo WithConverter(in ValueConverterInfo converterInfo) => new(this, converterInfo); - - /// - /// Returns a new with the given converter applied. - /// - /// The element mapping to use. - /// The new mapping info. - public RelationalTypeMappingInfo WithElementTypeMapping(in RelationalTypeMapping elementMapping) - => new(this, elementMapping); } diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs index 65e27444da9..681060684f2 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs @@ -30,8 +30,8 @@ namespace Microsoft.EntityFrameworkCore.Storage; /// public abstract class RelationalTypeMappingSource : TypeMappingSourceBase, IRelationalTypeMappingSource { - private readonly ConcurrentDictionary<(RelationalTypeMappingInfo, Type?, ValueConverter?), RelationalTypeMapping?> _explicitMappings - = new(); + private readonly ConcurrentDictionary<(RelationalTypeMappingInfo, Type?, ValueConverter?, CoreTypeMapping?), RelationalTypeMapping?> + _explicitMappings = new(); /// /// Initializes a new instance of this class. @@ -90,6 +90,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) { Type? providerClrType = null; ValueConverter? customConverter = null; + CoreTypeMapping? elementMapping = null; if (principals != null) { for (var i = 0; i < principals.Count; i++) @@ -112,10 +113,16 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) customConverter = converter; } } + + var element = principal.GetElementType(); + if (element != null) + { + elementMapping = FindMapping(element); + } } } - var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter, elementMapping); ValidateMapping(resolvedMapping, principals?[0]); @@ -125,74 +132,79 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) private RelationalTypeMapping? FindMappingWithConversion( RelationalTypeMappingInfo mappingInfo, Type? providerClrType, - ValueConverter? customConverter) + ValueConverter? customConverter, + CoreTypeMapping? elementMapping) => _explicitMappings.GetOrAdd( - (mappingInfo, providerClrType, customConverter), + (mappingInfo, providerClrType, customConverter, elementMapping), static (k, self) => { - var (info, providerType, converter) = k; - var mapping = providerType == null - || providerType == info.ClrType - ? self.FindMapping(info) - : null; + var (mappingInfo, providerClrType, customConverter, elementMapping) = k; - if (mapping == null) + var sourceType = mappingInfo.ClrType; + RelationalTypeMapping? mapping = null; + + if (elementMapping == null + || customConverter != null) { - var sourceType = info.ClrType; + mapping = providerClrType == null + || providerClrType == mappingInfo.ClrType + ? self.FindMapping(mappingInfo) + : null; - if (sourceType != null) + if (mapping == null) { - foreach (var converterInfo in self.Dependencies - .ValueConverterSelector - .Select(sourceType, providerType)) + if (sourceType != null) { - var mappingInfoUsed = info.WithConverter(converterInfo); - mapping = self.FindMapping(mappingInfoUsed); - - if (mapping == null - && providerType != null) + foreach (var converterInfo in self.Dependencies + .ValueConverterSelector + .Select(sourceType, providerClrType)) { - foreach (var secondConverterInfo in self.Dependencies - .ValueConverterSelector - .Select(providerType)) - { - mapping = self.FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); + var mappingInfoUsed = mappingInfo.WithConverter(converterInfo); + mapping = self.FindMapping(mappingInfoUsed); - if (mapping != null) + if (mapping == null + && providerClrType != null) + { + foreach (var secondConverterInfo in self.Dependencies + .ValueConverterSelector + .Select(providerClrType)) { - mapping = (RelationalTypeMapping)mapping.Clone( - secondConverterInfo.Create(), - mappingInfoUsed.ElementTypeMapping, - jsonValueReaderWriter: mappingInfoUsed.JsonValueReaderWriter); - break; + mapping = self.FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); + + if (mapping != null) + { + mapping = (RelationalTypeMapping)mapping.Clone( + secondConverterInfo.Create(), + jsonValueReaderWriter: mappingInfoUsed.JsonValueReaderWriter); + break; + } } } - } - if (mapping != null) - { - mapping = (RelationalTypeMapping)mapping.Clone( - converterInfo.Create(), - info.ElementTypeMapping, - jsonValueReaderWriter: info.JsonValueReaderWriter); - break; + if (mapping != null) + { + mapping = (RelationalTypeMapping)mapping.Clone( + converterInfo.Create(), + jsonValueReaderWriter: mappingInfo.JsonValueReaderWriter); + break; + } } - } - if (mapping == null) - { - mapping = self.TryFindCollectionMapping(info, sourceType, providerType); + mapping ??= self.TryFindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); } } } + else if (sourceType != null) + { + mapping = self.TryFindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); + } if (mapping != null - && converter != null) + && customConverter != null) { mapping = (RelationalTypeMapping)mapping.Clone( - converter, - info.ElementTypeMapping, - jsonValueReaderWriter: info.JsonValueReaderWriter); + customConverter, + jsonValueReaderWriter: mappingInfo.JsonValueReaderWriter); } return mapping; @@ -205,14 +217,15 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) /// The mapping info being used. /// The model type. /// The provider type. - /// The type mapping, or if none was found. + /// The element mapping, if known. + /// The type mapping, or if none was found. protected virtual RelationalTypeMapping? TryFindCollectionMapping( RelationalTypeMappingInfo info, Type modelType, - Type? providerType) + Type? providerType, + CoreTypeMapping? elementMapping) => TryFindJsonCollectionMapping( - info.CoreTypeMappingInfo, modelType, providerType, out var elementMapping, - out var collectionReaderWriter) + info.CoreTypeMappingInfo, modelType, providerType, ref elementMapping, out var collectionReaderWriter) ? (RelationalTypeMapping)FindMapping( info.WithConverter( // Note that the converter info is only used temporarily here and never creates an instance. @@ -267,6 +280,35 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) principals); } + /// + /// Finds the type mapping for the given . + /// + /// + /// Note: providers should typically not need to override this method. + /// + /// The collection element. + /// The type mapping, or if none was found. + public override CoreTypeMapping? FindMapping(IElementType elementType) + { + var storeTypeName = (string?)elementType[RelationalAnnotationNames.StoreType]; + var isFixedLength = elementType.IsFixedLength(); + bool? unicode = null; + int? size = null; + int? precision = null; + int? scale = null; + var storeTypeNameBase = ParseStoreTypeName(storeTypeName, ref unicode, ref size, ref precision, ref scale); + var providerClrType = elementType.GetProviderClrType(); + var customConverter = elementType.GetValueConverter(); + + var resolvedMapping = FindMappingWithConversion( + new RelationalTypeMappingInfo(elementType, storeTypeName, storeTypeNameBase, unicode, isFixedLength, size, precision, scale), + providerClrType, customConverter, null); + + ValidateMapping(resolvedMapping, null); + + return resolvedMapping; + } + /// /// Finds the type mapping for a given . /// @@ -338,12 +380,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) scale: scale); } - if (elementMapping != null) - { - mappingInfo = mappingInfo.WithElementTypeMapping((RelationalTypeMapping)elementMapping); - } - - return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + return FindMappingWithConversion(mappingInfo, providerClrType, customConverter, (RelationalTypeMapping?)elementMapping); } /// diff --git a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..afd29bd2087 --- /dev/null +++ b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,53 @@ +// 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; + +/// +/// SQL Server specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing SQL Server and SQL Azure databases with EF Core +/// for more information and examples. +/// +public static class SqlServerComplexTypePrimitiveCollectionBuilderExtensions +{ + /// + /// Configures whether the property's column is created as sparse when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// A value indicating whether the property's column is created as sparse. + /// A builder to further configure the property. + public static ComplexTypePrimitiveCollectionBuilder IsSparse(this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, bool sparse = true) + { + primitiveCollectionBuilder.Metadata.SetIsSparse(sparse); + + return primitiveCollectionBuilder; + } + + /// + /// Configures whether the property's column is created as sparse when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// A value indicating whether the property's column is created as sparse. + /// A builder to further configure the property. + public static ComplexTypePrimitiveCollectionBuilder IsSparse( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + bool sparse = true) + => (ComplexTypePrimitiveCollectionBuilder)IsSparse((ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sparse); +} diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..17dd2d26dfe --- /dev/null +++ b/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,53 @@ +// 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; + +/// +/// SQL Server specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing SQL Server and SQL Azure databases with EF Core +/// for more information and examples. +/// +public static class SqlServerPrimitiveCollectionBuilderExtensions +{ + /// + /// Configures whether the property's column is created as sparse when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// A value indicating whether the property's column is created as sparse. + /// A builder to further configure the property. + public static PrimitiveCollectionBuilder IsSparse(this PrimitiveCollectionBuilder primitiveCollectionBuilder, bool sparse = true) + { + primitiveCollectionBuilder.Metadata.SetIsSparse(sparse); + + return primitiveCollectionBuilder; + } + + /// + /// Configures whether the property's column is created as sparse when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// A value indicating whether the property's column is created as sparse. + /// A builder to further configure the property. + public static PrimitiveCollectionBuilder IsSparse( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + bool sparse = true) + => (PrimitiveCollectionBuilder)IsSparse((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sparse); +} diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePrimitiveCollectionBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..19fd5c79297 --- /dev/null +++ b/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,49 @@ +// 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; + +/// +/// SQLite-specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing SQLite databases with EF Core for more information and examples. +/// +public static class SqliteComplexTypePrimitiveCollectionBuilderExtensions +{ + /// + /// Configures the SRID of the column that the property maps to when targeting SQLite. + /// + /// + /// See Spatial data, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The SRID. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasSrid( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + int srid) + { + primitiveCollectionBuilder.Metadata.SetSrid(srid); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the SRID of the column that the property maps to when targeting SQLite. + /// + /// + /// See Spatial data, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The SRID. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasSrid( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + int srid) + => (ComplexTypePrimitiveCollectionBuilder)HasSrid( + (ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, srid); +} diff --git a/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs new file mode 100644 index 00000000000..17154e792eb --- /dev/null +++ b/src/EFCore.Sqlite.Core/Extensions/SqliteComplexTypePropertyBuilderExtensions.cs @@ -0,0 +1,46 @@ +// 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; + +/// +/// SQLite-specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing SQLite databases with EF Core for more information and examples. +/// +public static class SqliteComplexTypePropertyBuilderExtensions +{ + /// + /// Configures the SRID of the column that the property maps to when targeting SQLite. + /// + /// + /// See Spatial data, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The SRID. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePropertyBuilder HasSrid(this ComplexTypePropertyBuilder propertyBuilder, int srid) + { + propertyBuilder.Metadata.SetSrid(srid); + + return propertyBuilder; + } + + /// + /// Configures the SRID of the column that the property maps to when targeting SQLite. + /// + /// + /// See Spatial data, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The SRID. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePropertyBuilder HasSrid( + this ComplexTypePropertyBuilder propertyBuilder, + int srid) + => (ComplexTypePropertyBuilder)HasSrid((ComplexTypePropertyBuilder)propertyBuilder, srid); +} diff --git a/src/EFCore.Sqlite.Core/Extensions/SqlitePrimitiveCollectionBuilderExtensions.cs b/src/EFCore.Sqlite.Core/Extensions/SqlitePrimitiveCollectionBuilderExtensions.cs new file mode 100644 index 00000000000..0a675c038ff --- /dev/null +++ b/src/EFCore.Sqlite.Core/Extensions/SqlitePrimitiveCollectionBuilderExtensions.cs @@ -0,0 +1,46 @@ +// 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; + +/// +/// SQLite-specific extension methods for . +/// +/// +/// See Modeling entity types and relationships, and +/// Accessing SQLite databases with EF Core for more information and examples. +/// +public static class SqlitePrimitiveCollectionBuilderExtensions +{ + /// + /// Configures the SRID of the column that the property maps to when targeting SQLite. + /// + /// + /// See Spatial data, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The SRID. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasSrid(this PrimitiveCollectionBuilder primitiveCollectionBuilder, int srid) + { + primitiveCollectionBuilder.Metadata.SetSrid(srid); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the SRID of the column that the property maps to when targeting SQLite. + /// + /// + /// See Spatial data, and + /// Accessing SQLite databases with EF Core for more information and examples. + /// + /// The builder for the property being configured. + /// The SRID. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasSrid( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + int srid) + => (PrimitiveCollectionBuilder)HasSrid((PrimitiveCollectionBuilder)primitiveCollectionBuilder, srid); +} diff --git a/src/EFCore/ChangeTracking/Internal/CurrentValueComparerFactory.cs b/src/EFCore/ChangeTracking/Internal/CurrentValueComparerFactory.cs index 5df96374e28..cf00bc77dec 100644 --- a/src/EFCore/ChangeTracking/Internal/CurrentValueComparerFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/CurrentValueComparerFactory.cs @@ -49,10 +49,14 @@ public virtual IComparer Create(IPropertyBase propertyBase) var nonNullableProviderType = providerType.UnwrapNullableType(); if (IsGenericComparable(providerType, nonNullableProviderType)) { + var elementType = property.GetElementType(); + var modelBaseType = elementType != null + ? typeof(IEnumerable<>).MakeGenericType(elementType.ClrType) + : modelType; var comparerType = modelType.IsClass - ? typeof(NullableClassCurrentProviderValueComparer<,>).MakeGenericType(modelType, providerType) + ? typeof(NullableClassCurrentProviderValueComparer<,>).MakeGenericType(modelBaseType, providerType) : modelType == converter.ModelClrType - ? typeof(CurrentProviderValueComparer<,>).MakeGenericType(modelType, providerType) + ? typeof(CurrentProviderValueComparer<,>).MakeGenericType(modelBaseType, providerType) : typeof(NullableStructCurrentProviderValueComparer<,>).MakeGenericType( nonNullableModelType, providerType); diff --git a/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs b/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs index cde5d6d4a9f..b0beec1b922 100644 --- a/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs +++ b/src/EFCore/ChangeTracking/Internal/ValueComparerExtensions.cs @@ -17,25 +17,25 @@ public static class ValueComparerExtensions /// 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 static ValueComparer? ToNullableComparer(this ValueComparer? valueComparer, IReadOnlyProperty property) + public static ValueComparer? ToNullableComparer(this ValueComparer? valueComparer, Type clrType) { if (valueComparer == null - || !property.ClrType.IsNullableValueType() + || !clrType.IsNullableValueType() || valueComparer.Type.IsNullableValueType()) { return valueComparer; } - var newEqualsParam1 = Expression.Parameter(property.ClrType, "v1"); - var newEqualsParam2 = Expression.Parameter(property.ClrType, "v2"); - var newHashCodeParam = Expression.Parameter(property.ClrType, "v"); - var newSnapshotParam = Expression.Parameter(property.ClrType, "v"); - var hasValueProperty = property.ClrType.GetProperty("HasValue")!; + var newEqualsParam1 = Expression.Parameter(clrType, "v1"); + var newEqualsParam2 = Expression.Parameter(clrType, "v2"); + var newHashCodeParam = Expression.Parameter(clrType, "v"); + var newSnapshotParam = Expression.Parameter(clrType, "v"); + var hasValueProperty = clrType.GetProperty("HasValue")!; var v1HasValue = Expression.MakeMemberAccess(newEqualsParam1, hasValueProperty); var v2HasValue = Expression.MakeMemberAccess(newEqualsParam2, hasValueProperty); return (ValueComparer)Activator.CreateInstance( - typeof(ValueComparer<>).MakeGenericType(property.ClrType), + typeof(ValueComparer<>).MakeGenericType(clrType), Expression.Lambda( Expression.OrElse( Expression.AndAlso( @@ -61,8 +61,8 @@ public static class ValueComparerExtensions Expression.MakeMemberAccess(newSnapshotParam, hasValueProperty), Expression.Convert( valueComparer.ExtractSnapshotBody( - Expression.Convert(newSnapshotParam, valueComparer.Type)), property.ClrType), - Expression.Default(property.ClrType)), + Expression.Convert(newSnapshotParam, valueComparer.Type)), clrType), + Expression.Default(clrType)), newSnapshotParam))!; } } diff --git a/src/EFCore/Metadata/Builders/ComplexPropertyBuilder.cs b/src/EFCore/Metadata/Builders/ComplexPropertyBuilder.cs index 750d8db0368..706bf33d52f 100644 --- a/src/EFCore/Metadata/Builders/ComplexPropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/ComplexPropertyBuilder.cs @@ -202,6 +202,63 @@ public virtual ComplexTypePropertyBuilder Property(Type propertyType, string pro Check.NotNull(propertyType, nameof(propertyType)), Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + /// + /// Returns an object that can be used to configure a property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property with this overload the property name must match the + /// name of a CLR property or field on the complex type. This overload cannot be used to + /// add a new shadow state property. + /// + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual ComplexTypePrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => new( + TypeBuilder.PrimitiveCollection( + Check.NotEmpty(propertyName, nameof(propertyName)), + ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual ComplexTypePrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => new( + TypeBuilder.PrimitiveCollection( + typeof(TProperty), + Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a property of the complex type. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual ComplexTypePrimitiveCollectionBuilder PrimitiveCollection(Type propertyType, string propertyName) + => new( + TypeBuilder.PrimitiveCollection( + Check.NotNull(propertyType, nameof(propertyType)), + Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + /// /// Returns an object that can be used to configure a property of the complex type. /// If no property with the given name exists, then a new property will be added. diff --git a/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs b/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs index 01c9b8066de..f7a51ecbf4f 100644 --- a/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs +++ b/src/EFCore/Metadata/Builders/ComplexPropertyBuilder`.cs @@ -100,6 +100,20 @@ public virtual ComplexTypePropertyBuilder Property(Express Check.NotNull(propertyExpression, nameof(propertyExpression)).GetMemberAccess(), ConfigurationSource.Explicit)! .Metadata); + /// + /// Returns an object that can be used to configure a primitive collection property of the entity type. + /// If the specified property is not already part of the model, it will be added. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the property. + public virtual ComplexTypePrimitiveCollectionBuilder PrimitiveCollection(Expression> propertyExpression) + => new(TypeBuilder.PrimitiveCollection( + Check.NotNull(propertyExpression, nameof(propertyExpression)).GetMemberAccess(), ConfigurationSource.Explicit)! + .Metadata); + /// /// Configures a complex property of the complex type. /// If no property with the given name exists, then a new property will be added. diff --git a/src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder.cs b/src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder.cs new file mode 100644 index 00000000000..5d4c8dda771 --- /dev/null +++ b/src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder.cs @@ -0,0 +1,434 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +/// +/// See Modeling complex types and relationships for more information and +/// examples. +/// +/// +public class ComplexTypePrimitiveCollectionBuilder : IInfrastructure +{ + /// + /// 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 ComplexTypePrimitiveCollectionBuilder(IMutableProperty property) + { + Check.NotNull(property, nameof(property)); + + Builder = ((Property)property).Builder; + } + + /// + /// The internal builder being used to configure the property. + /// + IConventionPropertyBuilder IInfrastructure.Instance + => Builder; + + private InternalPropertyBuilder Builder { get; } + + /// + /// The property being configured. + /// + public virtual IMutableProperty Metadata + => Builder.Metadata; + + /// + /// Adds or updates an annotation on the property. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether this property must have a value assigned or is a valid value. + /// A property can only be configured as non-required if it is based on a CLR type that can be + /// assigned . + /// + /// A value indicating whether the property is required. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder IsRequired(bool required = true) + { + Builder.IsRequired(required, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// + /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasMaxLength(int maxLength) + { + Builder.HasMaxLength(maxLength, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the value that will be used to determine if the property has been set or not. If the property is set to the + /// sentinel value, then it is considered not set. By default, the sentinel value is the CLR default value for the type of + /// the property. + /// + /// The sentinel value. + /// The same builder instance if the configuration was applied, otherwise. + public virtual ComplexTypePrimitiveCollectionBuilder HasSentinel(object? sentinel) + { + Builder.HasSentinel(sentinel, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder IsUnicode(bool unicode = true) + { + Builder.IsUnicode(unicode, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasValueGenerator + <[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TGenerator>() + where TGenerator : ValueGenerator + { + Builder.HasValueGenerator(typeof(TGenerator), ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasValueGenerator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType) + { + Builder.HasValueGenerator(valueGeneratorType, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory + <[DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] TFactory>() + where TFactory : ValueGeneratorFactory + => HasValueGeneratorFactory(typeof(TFactory)); + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory( + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactoryType) + { + Builder.HasValueGeneratorFactory(valueGeneratorFactoryType, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether this property should be used as a concurrency token. When a property is configured + /// as a concurrency token the value in the database will be checked when an instance of this complex type + /// is updated or deleted during to ensure it has not changed since + /// the instance was retrieved from the database. If it has changed, an exception will be thrown and the + /// changes will not be applied to the database. + /// + /// A value indicating whether this property is a concurrency token. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder IsConcurrencyToken(bool concurrencyToken = true) + { + Builder.IsConcurrencyToken(concurrencyToken, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to never have a value generated by the database when an instance of this + /// complex type is saved. + /// + /// The same builder instance so that multiple configuration calls can be chained. + /// + /// Note that values may still be generated by a client-side value generator, if one is set explicitly or by a convention. + /// + public virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedNever() + { + Builder.ValueGenerated(ValueGenerated.Never, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated only when saving a new entity, unless a non-null, + /// non-temporary value has been set, in which case the set value will be saved instead. The value + /// may be generated by a client-side value generator or may be generated by the database as part + /// of saving the entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAdd() + { + Builder.ValueGenerated(ValueGenerated.OnAdd, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated when saving a new or existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + { + Builder.ValueGenerated(ValueGenerated.OnAddOrUpdate, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdate() + { + Builder.ValueGenerated(ValueGenerated.OnUpdate, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated under certain conditions when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdateSometimes() + { + Builder.ValueGenerated(ValueGenerated.OnUpdateSometimes, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Sets the backing field to use for this property. + /// + /// + /// + /// Backing fields are normally found by convention. + /// This method is useful for setting backing fields explicitly in cases where the + /// correct field is not found by convention. + /// + /// + /// By default, the backing field, if one is found or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. This can be changed by calling + /// . + /// + /// + /// See Backing fields for more information and examples. + /// + /// + /// The field name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder HasField(string fieldName) + { + Check.NotEmpty(fieldName, nameof(fieldName)); + + Builder.HasField(fieldName, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the elements of this collection. + /// + /// A builder to configure the collection element type. + public virtual ElementTypeBuilder ElementType() + => new((IMutableElementType)Builder.Metadata.GetElementType()!); + + /// + /// Configures the elements of this collection. + /// + /// An action that performs configuration of the collection element type. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder ElementType(Action builderAction) + { + builderAction(ElementType()); + + return this; + } + + /// + /// Sets the to use for this property. + /// + /// + /// + /// By default, the backing field, if one is found by convention or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. Calling this method will change that behavior + /// for this property as described in the enum. + /// + /// + /// Calling this method overrides for this property any access mode that was set on the + /// complex type or model. + /// + /// + /// The to use for this property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ComplexTypePrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + { + Builder.UsePropertyAccessMode(propertyAccessMode, ConfigurationSource.Explicit); + + return this; + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion +} diff --git a/src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder`.cs b/src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder`.cs new file mode 100644 index 00000000000..1ef05f1a311 --- /dev/null +++ b/src/EFCore/Metadata/Builders/ComplexTypePrimitiveCollectionBuilder`.cs @@ -0,0 +1,311 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +/// +/// See Modeling complex types and relationships for more information and +/// examples. +/// +/// +public class ComplexTypePrimitiveCollectionBuilder : ComplexTypePrimitiveCollectionBuilder +{ + /// + /// 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 ComplexTypePrimitiveCollectionBuilder(IMutableProperty property) + : base(property) + { + } + + /// + /// Adds or updates an annotation on the property. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + => (ComplexTypePrimitiveCollectionBuilder)base.HasAnnotation(annotation, value); + + /// + /// Configures whether this property must have a value assigned or whether null is a valid value. + /// A property can only be configured as non-required if it is based on a CLR type that can be + /// assigned . + /// + /// A value indicating whether the property is required. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder IsRequired(bool required = true) + => (ComplexTypePrimitiveCollectionBuilder)base.IsRequired(required); + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// + /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasMaxLength(int maxLength) + => (ComplexTypePrimitiveCollectionBuilder)base.HasMaxLength(maxLength); + + /// + /// Configures the value that will be used to determine if the property has been set or not. If the property is set to the + /// sentinel value, then it is considered not set. By default, the sentinel value is the CLR default value for the type of + /// the property. + /// + /// The sentinel value. + /// The same builder instance if the configuration was applied, otherwise. + public new virtual ComplexTypePrimitiveCollectionBuilder HasSentinel(object? sentinel) + => (ComplexTypePrimitiveCollectionBuilder)base.HasSentinel(sentinel); + + /// + /// Configures the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder IsUnicode(bool unicode = true) + => (ComplexTypePrimitiveCollectionBuilder)base.IsUnicode(unicode); + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasValueGenerator + <[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TGenerator>() + where TGenerator : ValueGenerator + => (ComplexTypePrimitiveCollectionBuilder)base.HasValueGenerator(); + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting null does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasValueGenerator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType) + => (ComplexTypePrimitiveCollectionBuilder)base.HasValueGenerator(valueGeneratorType); + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory + <[DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] TFactory>() + where TFactory : ValueGeneratorFactory + => (ComplexTypePrimitiveCollectionBuilder)base.HasValueGeneratorFactory(); + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the complex type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory( + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactoryType) + => (ComplexTypePrimitiveCollectionBuilder)base.HasValueGeneratorFactory(valueGeneratorFactoryType); + + /// + /// Configures whether this property should be used as a concurrency token. When a property is configured + /// as a concurrency token the value in the database will be checked when an instance of this complex type + /// is updated or deleted during to ensure it has not changed since + /// the instance was retrieved from the database. If it has changed, an exception will be thrown and the + /// changes will not be applied to the database. + /// + /// A value indicating whether this property is a concurrency token. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder IsConcurrencyToken(bool concurrencyToken = true) + => (ComplexTypePrimitiveCollectionBuilder)base.IsConcurrencyToken(concurrencyToken); + + /// + /// Configures a property to never have a value generated when an instance of this + /// complex type is saved. + /// + /// The same builder instance so that multiple configuration calls can be chained. + /// + /// Note that temporary values may still be generated for use internally before a + /// new entity is saved. + /// + public new virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedNever() + => (ComplexTypePrimitiveCollectionBuilder)base.ValueGeneratedNever(); + + /// + /// Configures a property to have a value generated only when saving a new entity, unless a non-null, + /// non-temporary value has been set, in which case the set value will be saved instead. The value + /// may be generated by a client-side value generator or may be generated by the database as part + /// of saving the entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAdd() + => (ComplexTypePrimitiveCollectionBuilder)base.ValueGeneratedOnAdd(); + + /// + /// Configures a property to have a value generated when saving a new or existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + => (ComplexTypePrimitiveCollectionBuilder)base.ValueGeneratedOnAddOrUpdate(); + + /// + /// Configures a property to have a value generated when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdate() + => (ComplexTypePrimitiveCollectionBuilder)base.ValueGeneratedOnUpdate(); + + /// + /// Configures a property to have a value generated under certain conditions when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdateSometimes() + => (ComplexTypePrimitiveCollectionBuilder)base.ValueGeneratedOnUpdateSometimes(); + + /// + /// Sets the backing field to use for this property. + /// + /// + /// + /// Backing fields are normally found by convention. + /// This method is useful for setting backing fields explicitly in cases where the + /// correct field is not found by convention. + /// + /// + /// By default, the backing field, if one is found or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. This can be changed by calling + /// . + /// + /// + /// See Backing fields for more information and examples. + /// + /// + /// The field name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder HasField(string fieldName) + => (ComplexTypePrimitiveCollectionBuilder)base.HasField(fieldName); + + /// + /// Configures the elements of this collection. + /// + /// An action that performs configuration of the collection element type. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder ElementType(Action builderAction) + { + builderAction(ElementType()); + + return this; + } + + /// + /// Sets the to use for this property. + /// + /// + /// + /// By default, the backing field, if one is found by convention or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. Calling this method will change that behavior + /// for this property as described in the enum. + /// + /// + /// Calling this method overrides for this property any access mode that was set on the + /// complex type or model. + /// + /// + /// The to use for this property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual ComplexTypePrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + => (ComplexTypePrimitiveCollectionBuilder)base.UsePropertyAccessMode(propertyAccessMode); +} diff --git a/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder.cs b/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder.cs index b6c57e0effb..47e6af20fda 100644 --- a/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder.cs @@ -228,37 +228,6 @@ public virtual ComplexTypePropertyBuilder HasValueGenerator( return this; } - /// - /// Configures a factory for creating a to use to generate values - /// for this property. - /// - /// - /// - /// Values are generated when the entity is added to the context using, for example, - /// . Values are generated only when the property is assigned - /// the CLR default value ( for string, 0 for int, - /// Guid.Empty for Guid, etc.). - /// - /// - /// This factory will be invoked once to create a single instance of the value generator, and - /// this will be used to generate values for this property in all instances of the complex type. - /// - /// - /// This method is intended for use with custom value generation. Value generation for common cases is - /// usually handled automatically by the database provider. - /// - /// - /// A delegate that will be used to create value generator instances. - /// The same builder instance so that multiple configuration calls can be chained. - public virtual ComplexTypePropertyBuilder HasValueGenerator(Func factory) - { - Check.NotNull(factory, nameof(factory)); - - Builder.HasValueGenerator(factory, ConfigurationSource.Explicit); - - return this; - } - /// /// Configures the for creating a /// to use to generate values for this property. diff --git a/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder`.cs b/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder`.cs index 7f0e24503b5..318c1966d9c 100644 --- a/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder`.cs +++ b/src/EFCore/Metadata/Builders/ComplexTypePropertyBuilder`.cs @@ -166,31 +166,6 @@ public ComplexTypePropertyBuilder(IMutableProperty property) [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType) => (ComplexTypePropertyBuilder)base.HasValueGenerator(valueGeneratorType); - /// - /// Configures a factory for creating a to use to generate values - /// for this property. - /// - /// - /// - /// Values are generated when the entity is added to the context using, for example, - /// . Values are generated only when the property is assigned - /// the CLR default value ( for string, 0 for int, - /// Guid.Empty for Guid, etc.). - /// - /// - /// This factory will be invoked once to create a single instance of the value generator, and - /// this will be used to generate values for this property in all instances of the complex type. - /// - /// - /// This method is intended for use with custom value generation. Value generation for common cases is - /// usually handled automatically by the database provider. - /// - /// - /// A delegate that will be used to create value generator instances. - /// The same builder instance so that multiple configuration calls can be chained. - public new virtual ComplexTypePropertyBuilder HasValueGenerator(Func factory) - => (ComplexTypePropertyBuilder)base.HasValueGenerator(factory); - /// /// Configures the for creating a /// to use to generate values for this property. diff --git a/src/EFCore/Metadata/Builders/ElementTypeBuilder.cs b/src/EFCore/Metadata/Builders/ElementTypeBuilder.cs new file mode 100644 index 00000000000..96aaab63982 --- /dev/null +++ b/src/EFCore/Metadata/Builders/ElementTypeBuilder.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring the of a primitive collection. +/// +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +/// +/// See Modeling entity types and relationships for more information and +/// examples. +/// +/// +public class ElementTypeBuilder : IInfrastructure +{ + /// + /// 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 ElementTypeBuilder(IMutableElementType elementType) + { + Check.NotNull(elementType, nameof(elementType)); + + Builder = ((ElementType)elementType).Builder; + } + + /// + /// The internal builder being used to configure the element type. + /// + IConventionElementTypeBuilder IInfrastructure.Instance + => Builder; + + private InternalElementTypeBuilder Builder { get; } + + /// + /// The element type being configured. + /// + public virtual IMutableElementType Metadata + => Builder.Metadata; + + /// + /// Adds or updates an annotation on the element type. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether elements of the collection must have a value or can be . + /// An element can only be configured as non-required if it is based on a CLR type that can be + /// assigned . + /// + /// A value indicating whether elements of the collection must not be . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder IsRequired(bool required = true) + { + Builder.IsRequired(required, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the maximum length of data that can be stored in elements of the collection. + /// + /// + /// The maximum length of data allowed in elements of the collection. A value of -1 indicates that elements of the + /// collection have no maximum length. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasMaxLength(int maxLength) + { + Builder.HasMaxLength(maxLength, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the precision and scale of elements of the collection. + /// + /// The precision of elements of the collection. + /// The scale of elements of the collection. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasPrecision(int precision, int scale) + { + Builder.HasPrecision(precision, ConfigurationSource.Explicit); + Builder.HasScale(scale, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the precision of elements of the collection. + /// + /// The precision of elements of the collection. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasPrecision(int precision) + { + Builder.HasPrecision(precision, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether elements of the collection are capable of persisting unicode characters. + /// + /// A value indicating whether elements of the collection can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder IsUnicode(bool unicode = true) + { + Builder.IsUnicode(unicode, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures elements of the collection so their values are converted before writing to the database and converted back + /// when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion>() + => HasConversion(typeof(TConversion)); + + /// + /// Configures elements of the collection so that their values are converted before writing to the database and converted back + /// when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? conversionType) + { + if (typeof(ValueConverter).IsAssignableFrom(conversionType)) + { + Builder.HasConverter(conversionType, ConfigurationSource.Explicit); + } + else + { + Builder.HasConversion(conversionType, ConfigurationSource.Explicit); + } + + return this; + } + + /// + /// Configures elements of the collection so that their values are converted to and from the database + /// using the given . + /// + /// The converter to use. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion(ValueConverter? converter) + => HasConversion(converter, null); + + /// + /// Configures elements of the collection so that their values are converted before + /// writing to the database and converted back when reading from the database. + /// + /// The comparer to use for values before conversion. + /// The type to convert to and from or a type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion>( + ValueComparer? valueComparer) + => HasConversion(typeof(TConversion), valueComparer); + + /// + /// Configures elements of the collection so that their values are converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// The comparer to use for values before conversion. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, + ValueComparer? valueComparer) + { + Check.NotNull(conversionType, nameof(conversionType)); + + if (typeof(ValueConverter).IsAssignableFrom(conversionType)) + { + Builder.HasConverter(conversionType, ConfigurationSource.Explicit); + } + else + { + Builder.HasConversion(conversionType, ConfigurationSource.Explicit); + } + + Builder.HasValueComparer(valueComparer, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures elements of the collection so that their values are converted before + /// using the given . + /// + /// The converter to use. + /// The comparer to use for values before conversion. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer) + { + Builder.HasConversion(converter, ConfigurationSource.Explicit); + Builder.HasValueComparer(valueComparer, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures elements of the collection so that their values are converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TComparer>() + where TComparer : ValueComparer + => HasConversion(typeof(TConversion), typeof(TComparer)); + + /// + /// Configures elements of the collection so that their values are converted before + /// writing to the database and converted back when reading from the database. + /// + /// The type to convert to and from or a type that inherits from . + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual ElementTypeBuilder HasConversion( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType) + { + Check.NotNull(conversionType, nameof(conversionType)); + + if (typeof(ValueConverter).IsAssignableFrom(conversionType)) + { + Builder.HasConverter(conversionType, ConfigurationSource.Explicit); + } + else + { + Builder.HasConversion(conversionType, ConfigurationSource.Explicit); + } + + Builder.HasValueComparer(comparerType, ConfigurationSource.Explicit); + + return this; + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion +} diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs index c0398c28246..1080d7fb1eb 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder.cs @@ -169,6 +169,65 @@ public virtual PropertyBuilder Property(Type propertyType, string propertyName) Check.NotNull(propertyType, nameof(propertyType)), Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + /// + /// Returns an object that can be used to configure a property of the entity type where that property represents + /// a collection of primitive values, such as strings or integers. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property with this overload the property name must match the + /// name of a CLR property or field on the entity type. This overload cannot be used to + /// add a new shadow state property. + /// + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => new( + Builder.PrimitiveCollection( + Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a property of the entity type where that property represents + /// a collection of primitive values, such as strings or integers. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => new( + Builder.PrimitiveCollection( + typeof(TProperty), + Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + + /// + /// Returns an object that can be used to configure a property of the entity type where that property represents + /// a collection of primitive values, such as strings or integers. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection(Type propertyType, string propertyName) + => new( + Builder.PrimitiveCollection( + Check.NotNull(propertyType, nameof(propertyType)), + Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + /// /// Returns an object that can be used to configure a property of the entity type. /// If no property with the given name exists, then a new property will be added. diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index 1168686bba9..1f1403d38c9 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -152,6 +152,21 @@ public virtual PropertyBuilder Property(Expression + /// Returns an object that can be used to configure a property of the entity type where that property represents + /// a collection of primitive values, such as strings or integers. + /// + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection( + Expression> propertyExpression) + => new( + Builder.PrimitiveCollection(Check.NotNull(propertyExpression, nameof(propertyExpression)).GetMemberAccess(), + ConfigurationSource.Explicit)!.Metadata); + /// /// Configures a complex property of the entity type. /// If no property with the given name exists, then a new property will be added. diff --git a/src/EFCore/Metadata/Builders/IConventionElementTypeBuilder.cs b/src/EFCore/Metadata/Builders/IConventionElementTypeBuilder.cs new file mode 100644 index 00000000000..0538d82a83c --- /dev/null +++ b/src/EFCore/Metadata/Builders/IConventionElementTypeBuilder.cs @@ -0,0 +1,321 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// +/// Provides a simple API surface for configuring an for a primitive collection +/// from conventions. +/// +/// +/// This interface is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IConventionElementTypeBuilder : IConventionAnnotatableBuilder +{ + /// + /// Gets the element type being configured. + /// + new IConventionElementType Metadata { get; } + + /// + /// Sets the annotation stored under the given name. Overwrites the existing annotation if an + /// annotation with the specified name already exists with same or lower . + /// + /// The name of the annotation to be set. + /// The value to be stored in the annotation. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder to continue configuration if the annotation was set, otherwise. + /// + new IConventionElementTypeBuilder? HasAnnotation(string name, object? value, bool fromDataAnnotation = false); + + /// + /// Sets the annotation stored under the given name. Overwrites the existing annotation if an + /// annotation with the specified name already exists with same or lower . + /// Removes the annotation if value is specified. + /// + /// The name of the annotation to be set. + /// The value to be stored in the annotation. to remove the annotations. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder to continue configuration if the annotation was set or removed, + /// otherwise. + /// + new IConventionElementTypeBuilder? HasNonNullAnnotation( + string name, + object? value, + bool fromDataAnnotation = false); + + /// + /// Removes the annotation with the given name from this object. + /// + /// The name of the annotation to remove. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder to continue configuration if the annotation was set, otherwise. + /// + new IConventionElementTypeBuilder? HasNoAnnotation(string name, bool fromDataAnnotation = false); + + /// + /// Configures whether elements of the collection must have a value or can be . + /// An element can only be configured as non-required if it is based on a CLR type that can be + /// assigned . + /// + /// A value indicating whether elements of the collection must not be . + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the requiredness was configured, + /// otherwise. + /// + IConventionElementTypeBuilder? IsRequired(bool? required, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether this element requiredness can be configured from the current configuration source. + /// + /// + /// A value indicating whether the elements are required, or to reset to default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// if the element requiredness can be configured. + bool CanSetIsRequired(bool? required, bool fromDataAnnotation = false); + + /// + /// Configures the maximum length of data that can be stored in elements of the collection. + /// + /// + /// The maximum length of data allowed in elements of the collection. A value of -1 indicates that elements of the + /// collection have no maximum length. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionElementTypeBuilder? HasMaxLength(int? maxLength, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the maximum length of elements can be set from the current configuration source. + /// + /// The maximum length of elements in the collection. + /// Indicates whether the configuration was specified using a data annotation. + /// if the maximum length of data allowed can be set for the elements. + bool CanSetMaxLength(int? maxLength, bool fromDataAnnotation = false); + + /// + /// Configures whether elements of the collection are capable of persisting unicode characters. + /// + /// A value indicating whether elements of the collection can contain unicode characters. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionElementTypeBuilder? IsUnicode(bool? unicode, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the elements can be configured as capable of persisting unicode characters + /// from the current configuration source. + /// + /// A value indicating whether the elements can contain unicode characters. + /// Indicates whether the configuration was specified using a data annotation. + /// if the capability of persisting unicode characters can be configured. + bool CanSetIsUnicode(bool? unicode, bool fromDataAnnotation = false); + + /// + /// Configures the precision of elements of the collection. + /// + /// The precision of elements of the collection. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionElementTypeBuilder? HasPrecision(int? precision, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the precision of elements can be set from the current configuration source. + /// + /// The precision of the elements. + /// Indicates whether the configuration was specified using a data annotation. + /// if the precision of data allowed can be set. + bool CanSetPrecision(int? precision, bool fromDataAnnotation = false); + + /// + /// Configures the scale of elements of the collection. + /// + /// The scale of elements of the collection. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionElementTypeBuilder? HasScale(int? scale, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the scale of elements can be set from the current configuration source. + /// + /// The scale of the elements. + /// Indicates whether the configuration was specified using a data annotation. + /// if the scale of data allowed can be set. + bool CanSetScale(int? scale, bool fromDataAnnotation = false); + + /// + /// Configures elements of the collection so their values are converted before writing to the database and converted back + /// when reading from the database. + /// + /// The converter to use. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionElementTypeBuilder? HasConversion(ValueConverter? converter, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the can be configured for the elements + /// from the current configuration source. + /// + /// The converter to use. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the can be configured. + /// + bool CanSetConversion(ValueConverter? converter, bool fromDataAnnotation = false); + + /// + /// Configures elements of the collection so their values are converted before writing to the database and converted back + /// when reading from the database. + /// + /// The type to convert to and from. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionElementTypeBuilder? HasConversion(Type? providerClrType, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given type to convert values to and from + /// can be configured for the elements from the current configuration source. + /// + /// The type to convert to and from. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given type to convert values to and from can be configured. + /// + bool CanSetConversion(Type? providerClrType, bool fromDataAnnotation = false); + + /// + /// Configures elements of the collection so their values are converted before writing to the database and converted back + /// when reading from the database. + /// + /// + /// A type that derives from , + /// or to remove any previously set converter. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, or otherwise. + /// + IConventionElementTypeBuilder? HasConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the can be configured for the elements + /// from the current configuration source. + /// + /// + /// A type that derives from , + /// or to remove any previously set converter. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the can be configured. + /// + bool CanSetConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + bool fromDataAnnotation = false); + + /// + /// Configures the for elements of the collection. + /// + /// The type mapping, or to remove any previously set type mapping. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, or otherwise. + /// + IConventionElementTypeBuilder? HasTypeMapping(CoreTypeMapping? typeMapping, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given + /// can be configured from the current configuration source. + /// + /// The type mapping. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given can be configured. + /// + bool CanSetTypeMapping(CoreTypeMapping typeMapping, bool fromDataAnnotation = false); + + /// + /// Configures the for elements of the collection. + /// + /// The comparer, or to remove any previously set comparer. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, or otherwise. + /// + IConventionElementTypeBuilder? HasValueComparer(ValueComparer? comparer, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given + /// can be configured from the current configuration source. + /// + /// The comparer, or to remove any previously set comparer. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given can be configured. + /// + bool CanSetValueComparer(ValueComparer? comparer, bool fromDataAnnotation = false); + + /// + /// Configures the for elements of the collection. + /// + /// + /// A type that derives from , or to remove any previously set comparer. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, or otherwise. + /// + IConventionElementTypeBuilder? HasValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given + /// can be configured from the current configuration source. + /// + /// + /// A type that derives from , or to remove any previously set comparer. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// + /// if the given can be configured. + /// + bool CanSetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + bool fromDataAnnotation = false); +} diff --git a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs index 7254760366e..f1a68be7ef6 100644 --- a/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/IConventionPropertyBuilder.cs @@ -538,4 +538,20 @@ bool CanSetValueComparer( bool CanSetProviderValueComparer( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, bool fromDataAnnotation = false); + + /// + /// 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. + /// + IConventionElementTypeBuilder? ElementType(bool elementType, bool fromDataAnnotation = false); + + /// + /// 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. + /// + bool CanSetElementType(bool elementType, bool fromDataAnnotation = false); } diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs index d86b245d5fe..aa2f5236586 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder.cs @@ -191,6 +191,68 @@ public virtual PropertyBuilder Property(Type propertyType, string propertyName) Check.NotNull(propertyType, nameof(propertyType)), Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata); + /// + /// Returns an object that can be used to configure a property of the owned type where that property represents + /// a collection of primitive values, such as strings or integers. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property with this overload the property name must match the + /// name of a CLR property or field on the entity type. This overload cannot be used to + /// add a new shadow state property. + /// + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => UpdateBuilder( + () => new PrimitiveCollectionBuilder( + DependentEntityType.Builder.PrimitiveCollection( + Check.NotEmpty(propertyName, nameof(propertyName)), + ConfigurationSource.Explicit)!.Metadata)); + + /// + /// Returns an object that can be used to configure a property of the owned type where that property represents + /// a collection of primitive values, such as strings or integers. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => UpdateBuilder( + () => new PrimitiveCollectionBuilder( + DependentEntityType.Builder.PrimitiveCollection( + typeof(TProperty), + Check.NotEmpty(propertyName, nameof(propertyName)), ConfigurationSource.Explicit)!.Metadata)); + + /// + /// Returns an object that can be used to configure a property of the owned type where that property represents + /// a collection of primitive values, such as strings or integers. + /// If no property with the given name exists, then a new property will be added. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// The name of the property to be configured. + /// An object that can be used to configure the property. + public virtual PrimitiveCollectionBuilder PrimitiveCollection(Type propertyType, string propertyName) + => new( + DependentEntityType.Builder.PrimitiveCollection( + Check.NotNull(propertyType, nameof(propertyType)), Check.NotEmpty(propertyName, nameof(propertyName)), + ConfigurationSource.Explicit)!.Metadata); + /// /// Returns an object that can be used to configure a property of the entity type. /// If no property with the given name exists, then a new property will be added. diff --git a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs index 9168293a31a..ef12acc2bd9 100644 --- a/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs +++ b/src/EFCore/Metadata/Builders/OwnedNavigationBuilder`.cs @@ -95,6 +95,31 @@ public virtual PropertyBuilder Property( Check.NotNull(propertyExpression, nameof(propertyExpression)).GetMemberAccess(), ConfigurationSource.Explicit)!.Metadata)); + /// + /// Returns an object that can be used to configure a property of the owned type where that property represents + /// a collection of primitive values, such as strings or integers. + /// + /// + /// When adding a new property, if a property with the same name exists in the entity class + /// then it will be added to the model. If no property exists in the entity class, then + /// a new shadow state property will be added. A shadow state property is one that does not have a + /// corresponding property in the entity class. The current value for the property is stored in + /// the rather than being stored in instances of the entity class. + /// + /// The type of the property to be configured. + /// + /// A lambda expression representing the property to be configured ( + /// blog => blog.Url). + /// + /// An object that can be used to configure the property. + public virtual PropertyBuilder PrimitiveCollection( + Expression> propertyExpression) + => UpdateBuilder( + () => new PropertyBuilder( + DependentEntityType.Builder.PrimitiveCollection( + Check.NotNull(propertyExpression, nameof(propertyExpression)).GetMemberAccess(), + ConfigurationSource.Explicit)!.Metadata)); + /// /// Returns an object that can be used to configure an existing navigation property /// from the owned type to its owner. It is an error for the navigation property diff --git a/src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder.cs b/src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder.cs new file mode 100644 index 00000000000..82d8e475714 --- /dev/null +++ b/src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder.cs @@ -0,0 +1,436 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +/// +/// See Modeling entity types and relationships for more information and +/// examples. +/// +/// +public class PrimitiveCollectionBuilder : IInfrastructure +{ + /// + /// 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 PrimitiveCollectionBuilder(IMutableProperty property) + { + Check.NotNull(property, nameof(property)); + + Builder = ((Property)property).Builder; + } + + /// + /// The internal builder being used to configure the property. + /// + IConventionPropertyBuilder IInfrastructure.Instance + => Builder; + + private InternalPropertyBuilder Builder { get; } + + /// + /// The property being configured. + /// + public virtual IMutableProperty Metadata + => Builder.Metadata; + + /// + /// Adds or updates an annotation on the property. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + { + Check.NotEmpty(annotation, nameof(annotation)); + + Builder.HasAnnotation(annotation, value, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether this property must have a value assigned or is a valid value. + /// A property can only be configured as non-required if it is based on a CLR type that can be + /// assigned . + /// + /// A value indicating whether the property is required. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder IsRequired(bool required = true) + { + Builder.IsRequired(required, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// + /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasMaxLength(int maxLength) + { + Builder.HasMaxLength(maxLength, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the value that will be used to determine if the property has been set or not. If the property is set to the + /// sentinel value, then it is considered not set. By default, the sentinel value is the CLR default value for the type of + /// the property. + /// + /// The sentinel value. + /// The same builder instance if the configuration was applied, otherwise. + public virtual PrimitiveCollectionBuilder HasSentinel(object? sentinel) + { + Builder.HasSentinel(sentinel, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder IsUnicode(bool unicode = true) + { + Builder.IsUnicode(unicode, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasValueGenerator + <[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TGenerator>() + where TGenerator : ValueGenerator + { + Builder.HasValueGenerator(typeof(TGenerator), ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasValueGenerator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? valueGeneratorType) + { + Builder.HasValueGenerator(valueGeneratorType, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasValueGeneratorFactory + <[DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] TFactory>() + where TFactory : ValueGeneratorFactory + => HasValueGeneratorFactory(typeof(TFactory)); + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasValueGeneratorFactory( + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactoryType) + { + Builder.HasValueGeneratorFactory(valueGeneratorFactoryType, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures whether this property should be used as a concurrency token. When a property is configured + /// as a concurrency token the value in the database will be checked when an instance of this entity type + /// is updated or deleted during to ensure it has not changed since + /// the instance was retrieved from the database. If it has changed, an exception will be thrown and the + /// changes will not be applied to the database. + /// + /// A value indicating whether this property is a concurrency token. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder IsConcurrencyToken(bool concurrencyToken = true) + { + Builder.IsConcurrencyToken(concurrencyToken, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to never have a value generated by the database when an instance of this + /// entity type is saved. + /// + /// The same builder instance so that multiple configuration calls can be chained. + /// + /// Note that values may still be generated by a client-side value generator, if one is set explicitly or by a convention. + /// + public virtual PrimitiveCollectionBuilder ValueGeneratedNever() + { + Builder.ValueGenerated(ValueGenerated.Never, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated only when saving a new entity, unless a non-null, + /// non-temporary value has been set, in which case the set value will be saved instead. The value + /// may be generated by a client-side value generator or may be generated by the database as part + /// of saving the entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder ValueGeneratedOnAdd() + { + Builder.ValueGenerated(ValueGenerated.OnAdd, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated when saving a new or existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + { + Builder.ValueGenerated(ValueGenerated.OnAddOrUpdate, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder ValueGeneratedOnUpdate() + { + Builder.ValueGenerated(ValueGenerated.OnUpdate, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures a property to have a value generated under certain conditions when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder ValueGeneratedOnUpdateSometimes() + { + Builder.ValueGenerated(ValueGenerated.OnUpdateSometimes, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Sets the backing field to use for this property. + /// + /// + /// + /// Backing fields are normally found by convention. + /// This method is useful for setting backing fields explicitly in cases where the + /// correct field is not found by convention. + /// + /// + /// By default, the backing field, if one is found or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. This can be changed by calling + /// . + /// + /// + /// See Backing fields for more information and examples. + /// + /// + /// The field name. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder HasField(string fieldName) + { + Check.NotEmpty(fieldName, nameof(fieldName)); + + Builder.HasField(fieldName, ConfigurationSource.Explicit); + + return this; + } + + /// + /// Configures the elements of this collection. + /// + /// A builder to configure the collection element type. + public virtual ElementTypeBuilder ElementType() + => new((IMutableElementType)Builder.Metadata.GetElementType()!); + + /// + /// Configures the elements of this collection. + /// + /// An action that performs configuration of the collection element type. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder ElementType(Action builderAction) + { + builderAction(ElementType()); + + return this; + } + + /// + /// Sets the to use for this property. + /// + /// + /// + /// By default, the backing field, if one is found by convention or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. Calling this method will change that behavior + /// for this property as described in the enum. + /// + /// + /// Calling this method overrides for this property any access mode that was set on the + /// entity type or model. + /// + /// + /// The to use for this property. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual PrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + { + Builder.UsePropertyAccessMode(propertyAccessMode, ConfigurationSource.Explicit); + + return this; + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion +} diff --git a/src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder`.cs b/src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder`.cs new file mode 100644 index 00000000000..c9547adf2ab --- /dev/null +++ b/src/EFCore/Metadata/Builders/PrimitiveCollectionBuilder`.cs @@ -0,0 +1,313 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API for configuring a . +/// +/// +/// +/// Instances of this class are returned from methods when using the API +/// and it is not designed to be directly constructed in your application code. +/// +/// +/// See Modeling entity types and relationships for more information and +/// examples. +/// +/// +public class PrimitiveCollectionBuilder : PrimitiveCollectionBuilder +{ + /// + /// 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 PrimitiveCollectionBuilder(IMutableProperty property) + : base(property) + { + } + + /// + /// Adds or updates an annotation on the property. If an annotation with the key specified in + /// already exists its value will be updated. + /// + /// The key of the annotation to be added or updated. + /// The value to be stored in the annotation. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + => (PrimitiveCollectionBuilder)base.HasAnnotation(annotation, value); + + /// + /// Configures whether this property must have a value assigned or whether null is a valid value. + /// A property can only be configured as non-required if it is based on a CLR type that can be + /// assigned . + /// + /// A value indicating whether the property is required. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder IsRequired(bool required = true) + => (PrimitiveCollectionBuilder)base.IsRequired(required); + + /// + /// Configures the maximum length of data that can be stored in this property. + /// Maximum length can only be set on array properties (including properties). + /// + /// + /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasMaxLength(int maxLength) + => (PrimitiveCollectionBuilder)base.HasMaxLength(maxLength); + + /// + /// Configures the value that will be used to determine if the property has been set or not. If the property is set to the + /// sentinel value, then it is considered not set. By default, the sentinel value is the CLR default value for the type of + /// the property. + /// + /// The sentinel value. + /// The same builder instance if the configuration was applied, otherwise. + public new virtual PrimitiveCollectionBuilder HasSentinel(object? sentinel) + => (PrimitiveCollectionBuilder)base.HasSentinel(sentinel); + + /// + /// Configures the property as capable of persisting unicode characters. + /// Can only be set on properties. + /// + /// A value indicating whether the property can contain unicode characters. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder IsUnicode(bool unicode = true) + => (PrimitiveCollectionBuilder)base.IsUnicode(unicode); + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasValueGenerator + <[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TGenerator>() + where TGenerator : ValueGenerator + => (PrimitiveCollectionBuilder)base.HasValueGenerator(); + + /// + /// Configures the that will generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting null does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasValueGenerator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? valueGeneratorType) + => (PrimitiveCollectionBuilder)base.HasValueGenerator(valueGeneratorType); + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasValueGeneratorFactory + <[DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] TFactory>() + where TFactory : ValueGeneratorFactory + => (PrimitiveCollectionBuilder)base.HasValueGeneratorFactory(); + + /// + /// Configures the for creating a + /// to use to generate values for this property. + /// + /// + /// + /// Values are generated when the entity is added to the context using, for example, + /// . Values are generated only when the property is assigned + /// the CLR default value ( for string, 0 for int, + /// Guid.Empty for Guid, etc.). + /// + /// + /// A single instance of this type will be created and used to generate values for this property in all + /// instances of the entity type. The type must be instantiable and have a parameterless constructor. + /// + /// + /// This method is intended for use with custom value generation. Value generation for common cases is + /// usually handled automatically by the database provider. + /// + /// + /// Setting does not disable value generation for this property, it just clears any generator explicitly + /// configured for this property. The database provider may still have a value generator for the property type. + /// + /// + /// A type that inherits from . + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasValueGeneratorFactory( + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactoryType) + => (PrimitiveCollectionBuilder)base.HasValueGeneratorFactory(valueGeneratorFactoryType); + + /// + /// Configures whether this property should be used as a concurrency token. When a property is configured + /// as a concurrency token the value in the database will be checked when an instance of this entity type + /// is updated or deleted during to ensure it has not changed since + /// the instance was retrieved from the database. If it has changed, an exception will be thrown and the + /// changes will not be applied to the database. + /// + /// A value indicating whether this property is a concurrency token. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder IsConcurrencyToken(bool concurrencyToken = true) + => (PrimitiveCollectionBuilder)base.IsConcurrencyToken(concurrencyToken); + + /// + /// Configures a property to never have a value generated when an instance of this + /// entity type is saved. + /// + /// The same builder instance so that multiple configuration calls can be chained. + /// + /// Note that temporary values may still be generated for use internally before a + /// new entity is saved. + /// + public new virtual PrimitiveCollectionBuilder ValueGeneratedNever() + => (PrimitiveCollectionBuilder)base.ValueGeneratedNever(); + + /// + /// Configures a property to have a value generated only when saving a new entity, unless a non-null, + /// non-temporary value has been set, in which case the set value will be saved instead. The value + /// may be generated by a client-side value generator or may be generated by the database as part + /// of saving the entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder ValueGeneratedOnAdd() + => (PrimitiveCollectionBuilder)base.ValueGeneratedOnAdd(); + + /// + /// Configures a property to have a value generated when saving a new or existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + => (PrimitiveCollectionBuilder)base.ValueGeneratedOnAddOrUpdate(); + + /// + /// Configures a property to have a value generated when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder ValueGeneratedOnUpdate() + => (PrimitiveCollectionBuilder)base.ValueGeneratedOnUpdate(); + + /// + /// Configures a property to have a value generated under certain conditions when saving an existing entity. + /// + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder ValueGeneratedOnUpdateSometimes() + => (PrimitiveCollectionBuilder)base.ValueGeneratedOnUpdateSometimes(); + + /// + /// Sets the backing field to use for this property. + /// + /// + /// + /// Backing fields are normally found by convention. + /// This method is useful for setting backing fields explicitly in cases where the + /// correct field is not found by convention. + /// + /// + /// By default, the backing field, if one is found or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. This can be changed by calling + /// . + /// + /// + /// See Backing fields for more information and examples. + /// + /// + /// The field name. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder HasField(string fieldName) + => (PrimitiveCollectionBuilder)base.HasField(fieldName); + + /// + /// Configures the elements of this collection. + /// + /// An action that performs configuration of the collection element type. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder ElementType(Action builderAction) + { + builderAction(ElementType()); + + return this; + } + + /// + /// Sets the to use for this property. + /// + /// + /// + /// By default, the backing field, if one is found by convention or has been specified, is used when + /// new objects are constructed, typically when entities are queried from the database. + /// Properties are used for all other accesses. Calling this method will change that behavior + /// for this property as described in the enum. + /// + /// + /// Calling this method overrides for this property any access mode that was set on the + /// entity type or model. + /// + /// + /// The to use for this property. + /// The same builder instance so that multiple configuration calls can be chained. + public new virtual PrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + => (PrimitiveCollectionBuilder)base.UsePropertyAccessMode(propertyAccessMode); +} diff --git a/src/EFCore/Metadata/Builders/PropertyBuilder.cs b/src/EFCore/Metadata/Builders/PropertyBuilder.cs index 1e8604eda01..aa4405b5ca6 100644 --- a/src/EFCore/Metadata/Builders/PropertyBuilder.cs +++ b/src/EFCore/Metadata/Builders/PropertyBuilder.cs @@ -85,7 +85,7 @@ public virtual PropertyBuilder IsRequired(bool required = true) /// Maximum length can only be set on array properties (including properties). /// /// - /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. + /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. /// /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasMaxLength(int maxLength) @@ -221,7 +221,8 @@ public virtual PropertyBuilder HasValueGenerator /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasValueGenerator( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? valueGeneratorType) { Builder.HasValueGenerator(valueGeneratorType, ConfigurationSource.Explicit); @@ -317,7 +318,8 @@ public virtual PropertyBuilder HasValueGeneratorFactory /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactoryType) + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactoryType) { Builder.HasValueGeneratorFactory(valueGeneratorFactoryType, ConfigurationSource.Explicit); @@ -462,7 +464,8 @@ public virtual PropertyBuilder UsePropertyAccessMode(PropertyAccessMode property /// /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. - public virtual PropertyBuilder HasConversion<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion>() + public virtual PropertyBuilder HasConversion< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion>() => HasConversion(typeof(TConversion)); /// @@ -472,7 +475,8 @@ public virtual PropertyBuilder UsePropertyAccessMode(PropertyAccessMode property /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? conversionType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? conversionType) { if (typeof(ValueConverter).IsAssignableFrom(conversionType)) { @@ -503,8 +507,9 @@ public virtual PropertyBuilder HasConversion(ValueConverter? converter) /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion>( - ValueComparer? valueComparer) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion>( + ValueComparer? valueComparer) => HasConversion(typeof(TConversion), valueComparer); /// @@ -529,7 +534,8 @@ public virtual PropertyBuilder HasConversion /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, ValueComparer? valueComparer) => HasConversion(conversionType, valueComparer, null); @@ -543,7 +549,8 @@ public virtual PropertyBuilder HasConversion( /// The comparer to use for the provider values. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, ValueComparer? valueComparer, ValueComparer? providerComparer) { @@ -599,8 +606,10 @@ public virtual PropertyBuilder HasConversion(ValueConverter? converter, ValueCom /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TComparer>() + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TComparer>() where TComparer : ValueComparer => HasConversion(typeof(TConversion), typeof(TComparer)); @@ -613,9 +622,12 @@ public virtual PropertyBuilder HasConversion< /// A type that inherits from to use for the provider values. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TComparer, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TProviderComparer>() + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TComparer, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TProviderComparer>() where TComparer : ValueComparer where TProviderComparer : ValueComparer => HasConversion(typeof(TConversion), typeof(TComparer), typeof(TProviderComparer)); @@ -628,8 +640,10 @@ public virtual PropertyBuilder HasConversion< /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType) => HasConversion(conversionType, comparerType, null); /// @@ -641,9 +655,12 @@ public virtual PropertyBuilder HasConversion( /// A type that inherits from to use for the provider values. /// The same builder instance so that multiple configuration calls can be chained. public virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? providerComparerType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? providerComparerType) { Check.NotNull(conversionType, nameof(conversionType)); diff --git a/src/EFCore/Metadata/Builders/PropertyBuilder`.cs b/src/EFCore/Metadata/Builders/PropertyBuilder`.cs index 19d1e0fbf6a..05c6550660f 100644 --- a/src/EFCore/Metadata/Builders/PropertyBuilder`.cs +++ b/src/EFCore/Metadata/Builders/PropertyBuilder`.cs @@ -57,7 +57,7 @@ public PropertyBuilder(IMutableProperty property) /// Maximum length can only be set on array properties (including properties). /// /// - /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. + /// The maximum length of data allowed in the property. A value of -1 indicates that the property has no maximum length. /// /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasMaxLength(int maxLength) @@ -163,7 +163,8 @@ public PropertyBuilder(IMutableProperty property) /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasValueGenerator( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? valueGeneratorType) => (PropertyBuilder)base.HasValueGenerator(valueGeneratorType); /// @@ -249,7 +250,8 @@ public PropertyBuilder(IMutableProperty property) /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactoryType) + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactoryType) => (PropertyBuilder)base.HasValueGeneratorFactory(valueGeneratorFactoryType); /// @@ -368,7 +370,8 @@ public PropertyBuilder(IMutableProperty property) /// The type to convert to and from or a type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? providerClrType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? providerClrType) => (PropertyBuilder)base.HasConversion(providerClrType); /// @@ -440,7 +443,8 @@ public virtual PropertyBuilder HasConversion(ValueConverte /// The comparer to use for values before conversion. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, ValueComparer? valueComparer) => (PropertyBuilder)base.HasConversion(conversionType, valueComparer); @@ -453,7 +457,8 @@ public virtual PropertyBuilder HasConversion(ValueConverte /// The comparer to use for the provider values. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, ValueComparer? valueComparer, ValueComparer? providerComparer) => (PropertyBuilder)base.HasConversion(conversionType, valueComparer, providerComparer); @@ -561,8 +566,10 @@ public virtual PropertyBuilder HasConversion( /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TComparer>() + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TComparer>() where TComparer : ValueComparer => (PropertyBuilder)base.HasConversion(); @@ -575,9 +582,12 @@ public virtual PropertyBuilder HasConversion( /// A type that inherits from to use for the provider values. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TConversion, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TComparer, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TProviderComparer>() + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TConversion, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TComparer, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TProviderComparer>() where TComparer : ValueComparer where TProviderComparer : ValueComparer => (PropertyBuilder)base.HasConversion(); @@ -590,8 +600,10 @@ public virtual PropertyBuilder HasConversion( /// A type that inherits from . /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType) => (PropertyBuilder)base.HasConversion(conversionType, comparerType); /// @@ -603,8 +615,11 @@ public virtual PropertyBuilder HasConversion( /// A type that inherits from to use for the provider values. /// The same builder instance so that multiple configuration calls can be chained. public new virtual PropertyBuilder HasConversion( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type conversionType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? providerComparerType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type conversionType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? providerComparerType) => (PropertyBuilder)base.HasConversion(conversionType, comparerType, providerComparerType); } diff --git a/src/EFCore/Metadata/Conventions/ConventionSet.cs b/src/EFCore/Metadata/Conventions/ConventionSet.cs index 05716c93bfc..3b314b0430b 100644 --- a/src/EFCore/Metadata/Conventions/ConventionSet.cs +++ b/src/EFCore/Metadata/Conventions/ConventionSet.cs @@ -257,6 +257,11 @@ public class ConventionSet /// public virtual List PropertyFieldChangedConventions { get; } = new(); + /// + /// Conventions to run when the field of a property is changed. + /// + public virtual List PropertyElementTypeChangedConventions { get; } = new(); + /// /// Conventions to run when an annotation is changed on a property. /// @@ -267,6 +272,16 @@ public class ConventionSet /// public virtual List PropertyRemovedConventions { get; } = new(); + /// + /// Conventions to run when the nullability of the element of a collection is changed. + /// + public virtual List ElementTypeNullabilityChangedConventions { get; } = new(); + + /// + /// Conventions to run when an annotation is changed on the element of a collection. + /// + public virtual List ElementTypeAnnotationChangedConventions { get; } = new(); + /// /// Replaces an existing convention with a derived convention. Also registers the new convention for any /// convention types not implemented by the existing convention. diff --git a/src/EFCore/Metadata/Conventions/IElementTypeAnnotationChangedConvention.cs b/src/EFCore/Metadata/Conventions/IElementTypeAnnotationChangedConvention.cs new file mode 100644 index 00000000000..99e17db1a0f --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IElementTypeAnnotationChangedConvention.cs @@ -0,0 +1,28 @@ +// 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.Metadata.Conventions; + +/// +/// Represents an operation that should be performed when an annotation is changed on the elements of a collection property. +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IElementTypeAnnotationChangedConvention : IConvention +{ + /// + /// Called after an annotation is changed on a . + /// + /// The builder for the property. + /// The annotation name. + /// The new annotation. + /// The old annotation. + /// Additional information associated with convention execution. + void ProcessElementTypeAnnotationChanged( + IConventionElementTypeBuilder builder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation, + IConventionContext context); +} diff --git a/src/EFCore/Metadata/Conventions/IElementTypeNullabilityChangedConvention.cs b/src/EFCore/Metadata/Conventions/IElementTypeNullabilityChangedConvention.cs new file mode 100644 index 00000000000..bd66efbec59 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IElementTypeNullabilityChangedConvention.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.Metadata.Conventions; + +/// +/// Represents an operation that should be performed when the nullability on the elements of a collection property has changed. +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IElementTypeNullabilityChangedConvention : IConvention +{ + /// + /// Called after the nullability for an is changed. + /// + /// The builder for the element. + /// Additional information associated with convention execution. + void ProcessElementTypeNullabilityChanged( + IConventionElementTypeBuilder builder, + IConventionContext context); +} diff --git a/src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs b/src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs new file mode 100644 index 00000000000..5a1ec6bfa01 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/IPropertyElementTypeChangedConvention.cs @@ -0,0 +1,26 @@ +// 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.Metadata.Conventions; + +/// +/// Represents an operation that should be performed when the for a property is changed. +/// +/// +/// See Model building conventions for more information and examples. +/// +public interface IPropertyElementTypeChangedConvention : IConvention +{ + /// + /// Called after the element type for a property is changed. + /// + /// The builder for the property. + /// The new element type. + /// The old element type. + /// Additional information associated with convention execution. + void ProcessPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType? newElementType, + IElementType? oldElementType, + IConventionContext context); +} diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs index ca87a070c5a..2cf04f3c32f 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ConventionScope.cs @@ -237,8 +237,22 @@ public int GetLeafCount() IConventionTypeBaseBuilder typeBaseBuilder, IConventionProperty property); + public abstract IElementType? OnPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType? newElementType, + IElementType? oldElementType); + public abstract IConventionTriggerBuilder? OnTriggerAdded(IConventionTriggerBuilder triggerBuilder); public abstract IConventionTrigger? OnTriggerRemoved(IConventionEntityTypeBuilder entityTypeBuilder, IConventionTrigger trigger); + + public abstract IConventionAnnotation? OnElementTypeAnnotationChanged( + IConventionElementTypeBuilder builder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation); + + public abstract bool? OnElementTypeNullabilityChanged( + IConventionElementTypeBuilder builder); } } diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs index 8057398c93c..321f5590d89 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.DelayedConventionScope.cs @@ -395,6 +395,12 @@ public override IConventionPropertyBuilder OnPropertyAdded(IConventionPropertyBu return propertyBuilder.Metadata.IsNullable; } + public override bool? OnElementTypeNullabilityChanged(IConventionElementTypeBuilder builder) + { + Add(new OnElementTypeNullabilityChangedNode(builder)); + return builder.Metadata.IsNullable; + } + public override FieldInfo? OnPropertyFieldChanged( IConventionPropertyBuilder propertyBuilder, FieldInfo? newFieldInfo, @@ -414,6 +420,16 @@ public override IConventionPropertyBuilder OnPropertyAdded(IConventionPropertyBu return annotation; } + public override IConventionAnnotation? OnElementTypeAnnotationChanged( + IConventionElementTypeBuilder builder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation) + { + Add(new OnElementTypeAnnotationChangedNode(builder, name, annotation, oldAnnotation)); + return annotation; + } + public override IConventionProperty OnPropertyRemoved( IConventionTypeBaseBuilder typeBaseBuilder, IConventionProperty property) @@ -421,6 +437,16 @@ public override IConventionProperty OnPropertyRemoved( Add(new OnPropertyRemovedNode(typeBaseBuilder, property)); return property; } + + public override IElementType? OnPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType? newElementType, + IElementType? oldElementType) + { + Add(new OnPropertyElementTypeChangedNode(propertyBuilder, newElementType, oldElementType)); + return newElementType; + + } } private sealed class OnModelAnnotationChangedNode : ConventionNode @@ -635,7 +661,9 @@ public override void Run(ConventionDispatcher dispatcher) private sealed class OnComplexPropertyFieldChangedNode : ConventionNode { public OnComplexPropertyFieldChangedNode( - IConventionComplexPropertyBuilder propertyBuilder, FieldInfo? newFieldInfo, FieldInfo? oldFieldInfo) + IConventionComplexPropertyBuilder propertyBuilder, + FieldInfo? newFieldInfo, + FieldInfo? oldFieldInfo) { PropertyBuilder = propertyBuilder; NewFieldInfo = newFieldInfo; @@ -1192,6 +1220,19 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnPropertyNullabilityChanged(PropertyBuilder); } + private sealed class OnElementTypeNullabilityChangedNode : ConventionNode + { + public OnElementTypeNullabilityChangedNode(IConventionElementTypeBuilder builder) + { + ElementTypeBuilder = builder; + } + + public IConventionElementTypeBuilder ElementTypeBuilder { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnElementTypeNullabilityChanged(ElementTypeBuilder); + } + private sealed class OnPropertyFieldChangedNode : ConventionNode { public OnPropertyFieldChangedNode(IConventionPropertyBuilder propertyBuilder, FieldInfo? newFieldInfo, FieldInfo? oldFieldInfo) @@ -1209,6 +1250,24 @@ public override void Run(ConventionDispatcher dispatcher) => dispatcher._immediateConventionScope.OnPropertyFieldChanged(PropertyBuilder, NewFieldInfo, OldFieldInfo); } + private sealed class OnPropertyElementTypeChangedNode : ConventionNode + { + public OnPropertyElementTypeChangedNode( + IConventionPropertyBuilder propertyBuilder, IElementType? newElementType, IElementType? oldElementType) + { + PropertyBuilder = propertyBuilder; + NewElementType = newElementType; + OldElementType = oldElementType; + } + + public IConventionPropertyBuilder PropertyBuilder { get; } + public IElementType? NewElementType { get; } + public IElementType? OldElementType { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnPropertyElementTypeChanged(PropertyBuilder, NewElementType, OldElementType); + } + private sealed class OnPropertyAnnotationChangedNode : ConventionNode { public OnPropertyAnnotationChangedNode( @@ -1233,6 +1292,30 @@ public override void Run(ConventionDispatcher dispatcher) PropertyBuilder, Name, Annotation, OldAnnotation); } + private sealed class OnElementTypeAnnotationChangedNode : ConventionNode + { + public OnElementTypeAnnotationChangedNode( + IConventionElementTypeBuilder elementTypeBuilder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation) + { + ElementTypeBuilder = elementTypeBuilder; + Name = name; + Annotation = annotation; + OldAnnotation = oldAnnotation; + } + + public IConventionElementTypeBuilder ElementTypeBuilder { get; } + public string Name { get; } + public IConventionAnnotation? Annotation { get; } + public IConventionAnnotation? OldAnnotation { get; } + + public override void Run(ConventionDispatcher dispatcher) + => dispatcher._immediateConventionScope.OnElementTypeAnnotationChanged( + ElementTypeBuilder, Name, Annotation, OldAnnotation); + } + private sealed class OnPropertyRemovedNode : ConventionNode { public OnPropertyRemovedNode( diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs index 2b2fa6e1c2c..700a841f157 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.ImmediateConventionScope.cs @@ -34,6 +34,7 @@ private sealed class ImmediateConventionScope : ConventionScope private readonly ConventionContext> _propertyListConventionContext; private readonly ConventionContext _stringConventionContext; private readonly ConventionContext _fieldInfoConventionContext; + private readonly ConventionContext _elementTypeConventionContext; private readonly ConventionContext _boolConventionContext; private readonly ConventionContext?> _boolListConventionContext; @@ -66,6 +67,7 @@ public ImmediateConventionScope(ConventionSet conventionSet, ConventionDispatche _propertyListConventionContext = new ConventionContext>(dispatcher); _stringConventionContext = new ConventionContext(dispatcher); _fieldInfoConventionContext = new ConventionContext(dispatcher); + _elementTypeConventionContext = new ConventionContext(dispatcher); _boolConventionContext = new ConventionContext(dispatcher); _boolListConventionContext = new ConventionContext?>(dispatcher); } @@ -133,7 +135,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB } #if DEBUG - Check.DebugAssert(initialValue == modelBuilder.Metadata[name], + Check.DebugAssert( + initialValue == modelBuilder.Metadata[name], $"Convention {modelConvention.GetType().Name} changed value without terminating"); #endif } @@ -158,7 +161,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _stringConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == modelBuilder.Metadata.IsIgnored(name), + Check.DebugAssert( + initialValue == modelBuilder.Metadata.IsIgnored(name), $"Convention {entityTypeConvention.GetType().Name} changed value without terminating"); #endif } @@ -188,7 +192,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _entityTypeBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == entityTypeBuilder.Metadata.IsInModel, + Check.DebugAssert( + initialValue == entityTypeBuilder.Metadata.IsInModel, $"Convention {entityTypeConvention.GetType().Name} changed value without terminating"); #endif } @@ -246,7 +251,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _stringConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == entityTypeBuilder.Metadata.IsIgnored(name), + Check.DebugAssert( + initialValue == entityTypeBuilder.Metadata.IsIgnored(name), $"Convention {entityTypeConvention.GetType().Name} changed value without terminating"); #endif } @@ -281,7 +287,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _entityTypeConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == entityTypeBuilder.Metadata.BaseType, + Check.DebugAssert( + initialValue == entityTypeBuilder.Metadata.BaseType, $"Convention {entityTypeConvention.GetType().Name} changed value without terminating"); #endif } @@ -316,7 +323,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _keyConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == entityTypeBuilder.Metadata.FindPrimaryKey(), + Check.DebugAssert( + initialValue == entityTypeBuilder.Metadata.FindPrimaryKey(), $"Convention {keyConvention.GetType().Name} changed value without terminating"); #endif } @@ -343,7 +351,6 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB _annotationConventionContext.ResetState(annotation); foreach (var entityTypeConvention in _conventionSet.EntityTypeAnnotationChangedConventions) { - entityTypeConvention.ProcessEntityTypeAnnotationChanged( entityTypeBuilder, name, annotation, oldAnnotation, _annotationConventionContext); if (_annotationConventionContext.ShouldStopProcessing()) @@ -351,7 +358,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(entityTypeBuilder.Metadata.IsInModel + Check.DebugAssert( + entityTypeBuilder.Metadata.IsInModel && initialValue == entityTypeBuilder.Metadata[name], $"Convention {entityTypeConvention.GetType().Name} changed value without terminating"); #endif @@ -389,7 +397,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _stringConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == propertyBuilder.Metadata.IsIgnored(name), + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.IsIgnored(name), $"Convention {entityTypeConvention.GetType().Name} changed value without terminating"); #endif } @@ -416,7 +425,6 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB _annotationConventionContext.ResetState(annotation); foreach (var complexTypeConvention in _conventionSet.ComplexTypeAnnotationChangedConventions) { - complexTypeConvention.ProcessComplexTypeAnnotationChanged( complexTypeBuilder, name, annotation, oldAnnotation, _annotationConventionContext); if (_annotationConventionContext.ShouldStopProcessing()) @@ -424,7 +432,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(complexTypeBuilder.Metadata.IsInModel + Check.DebugAssert( + complexTypeBuilder.Metadata.IsInModel && initialValue == complexTypeBuilder.Metadata[name], $"Convention {complexTypeConvention.GetType().Name} changed value without terminating"); #endif @@ -456,7 +465,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _complexPropertyBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == propertyBuilder.Metadata.IsInModel, + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.IsInModel, $"Convention {complexPropertyConvention.GetType().Name} changed value without terminating"); #endif } @@ -511,7 +521,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == propertyBuilder.Metadata.IsNullable, + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.IsNullable, $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif } @@ -543,7 +554,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _fieldInfoConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == propertyBuilder.Metadata.FieldInfo, + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.FieldInfo, $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif } @@ -569,7 +581,6 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB _annotationConventionContext.ResetState(annotation); foreach (var propertyConvention in _conventionSet.ComplexPropertyAnnotationChangedConventions) { - propertyConvention.ProcessComplexPropertyAnnotationChanged( propertyBuilder, name, annotation, oldAnnotation, _annotationConventionContext); if (_annotationConventionContext.ShouldStopProcessing()) @@ -577,7 +588,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(propertyBuilder.Metadata.IsInModel + Check.DebugAssert( + propertyBuilder.Metadata.IsInModel && initialValue == propertyBuilder.Metadata[name], $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif @@ -612,7 +624,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _relationshipBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(relationshipBuilder.Metadata.DeclaringEntityType.IsInModel + Check.DebugAssert( + relationshipBuilder.Metadata.DeclaringEntityType.IsInModel && relationshipBuilder.Metadata.PrincipalEntityType.IsInModel && relationshipBuilder.Metadata.IsInModel, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); @@ -672,9 +685,11 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _propertyListConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialProperties == relationshipBuilder.Metadata.Properties, + Check.DebugAssert( + initialProperties == relationshipBuilder.Metadata.Properties, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); - Check.DebugAssert(initialPrincipalKey == relationshipBuilder.Metadata.PrincipalKey, + Check.DebugAssert( + initialPrincipalKey == relationshipBuilder.Metadata.PrincipalKey, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); #endif } @@ -705,7 +720,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == relationshipBuilder.Metadata.IsUnique, + Check.DebugAssert( + initialValue == relationshipBuilder.Metadata.IsUnique, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); #endif } @@ -737,7 +753,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == relationshipBuilder.Metadata.IsRequired, + Check.DebugAssert( + initialValue == relationshipBuilder.Metadata.IsRequired, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); #endif } @@ -769,7 +786,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == relationshipBuilder.Metadata.IsRequiredDependent, + Check.DebugAssert( + initialValue == relationshipBuilder.Metadata.IsRequiredDependent, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); #endif } @@ -800,7 +818,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == relationshipBuilder.Metadata.IsOwnership, + Check.DebugAssert( + initialValue == relationshipBuilder.Metadata.IsOwnership, $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); #endif } @@ -859,8 +878,9 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(relationshipBuilder.Metadata.IsInModel - && initialValue == relationshipBuilder.Metadata[name], + Check.DebugAssert( + relationshipBuilder.Metadata.IsInModel + && initialValue == relationshipBuilder.Metadata[name], $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); #endif } @@ -895,7 +915,9 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _navigationConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == (pointsToPrincipal + Check.DebugAssert( + initialValue + == (pointsToPrincipal ? relationshipBuilder.Metadata.DependentToPrincipal : relationshipBuilder.Metadata.PrincipalToDependent), $"Convention {foreignKeyConvention.GetType().Name} changed value without terminating"); @@ -924,7 +946,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _navigationConventionBuilderContext.Result; } #if DEBUG - Check.DebugAssert(navigationBuilder.Metadata.IsInModel, + Check.DebugAssert( + navigationBuilder.Metadata.IsInModel, $"Convention {navigationConvention.GetType().Name} changed value without terminating"); #endif } @@ -960,7 +983,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(relationshipBuilder.Metadata.IsInModel + Check.DebugAssert( + relationshipBuilder.Metadata.IsInModel && relationshipBuilder.Metadata.GetNavigation(navigation.IsOnDependent) == navigation && initialValue == navigation[name], $"Convention {navigationConvention.GetType().Name} changed value without terminating"); @@ -1019,7 +1043,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _skipNavigationBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(navigationBuilder.Metadata.IsInModel, + Check.DebugAssert( + navigationBuilder.Metadata.IsInModel, $"Convention {skipNavigationConvention.GetType().Name} changed value without terminating"); #endif } @@ -1060,7 +1085,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(navigationBuilder.Metadata.IsInModel + Check.DebugAssert( + navigationBuilder.Metadata.IsInModel && initialValue == navigationBuilder.Metadata[name], $"Convention {skipNavigationConvention.GetType().Name} changed value without terminating"); #endif @@ -1100,7 +1126,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _foreignKeyConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == navigationBuilder.Metadata.ForeignKey, + Check.DebugAssert( + initialValue == navigationBuilder.Metadata.ForeignKey, $"Convention {skipNavigationConvention.GetType().Name} changed value without terminating"); #endif } @@ -1134,7 +1161,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _skipNavigationConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == navigationBuilder.Metadata.Inverse, + Check.DebugAssert( + initialValue == navigationBuilder.Metadata.Inverse, $"Convention {skipNavigationConvention.GetType().Name} changed value without terminating"); #endif } @@ -1187,7 +1215,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _triggerBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(triggerBuilder.Metadata.IsInModel, + Check.DebugAssert( + triggerBuilder.Metadata.IsInModel, $"Convention {triggerConvention.GetType().Name} changed value without terminating"); #endif } @@ -1237,7 +1266,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _keyBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(keyBuilder.Metadata.IsInModel, + Check.DebugAssert( + keyBuilder.Metadata.IsInModel, $"Convention {keyConvention.GetType().Name} changed value without terminating"); #endif } @@ -1290,7 +1320,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(keyBuilder.Metadata.IsInModel + Check.DebugAssert( + keyBuilder.Metadata.IsInModel && initialValue == keyBuilder.Metadata[name], $"Convention {keyConvention.GetType().Name} changed value without terminating"); #endif @@ -1323,7 +1354,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _indexBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(indexBuilder.Metadata.IsInModel, + Check.DebugAssert( + indexBuilder.Metadata.IsInModel, $"Convention {indexConvention.GetType().Name} changed value without terminating"); #endif } @@ -1371,7 +1403,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == indexBuilder.Metadata.IsUnique, + Check.DebugAssert( + initialValue == indexBuilder.Metadata.IsUnique, $"Convention {indexConvention.GetType().Name} changed value without terminating"); #endif } @@ -1401,7 +1434,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolListConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == indexBuilder.Metadata.IsDescending, + Check.DebugAssert( + initialValue == indexBuilder.Metadata.IsDescending, $"Convention {indexConvention.GetType().Name} changed value without terminating"); #endif } @@ -1435,7 +1469,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(indexBuilder.Metadata.IsInModel + Check.DebugAssert( + indexBuilder.Metadata.IsInModel && initialValue == indexBuilder.Metadata[name], $"Convention {indexConvention.GetType().Name} changed value without terminating"); #endif @@ -1468,7 +1503,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _propertyBuilderConventionContext.Result; } #if DEBUG - Check.DebugAssert(propertyBuilder.Metadata.IsInModel, + Check.DebugAssert( + propertyBuilder.Metadata.IsInModel, $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif } @@ -1502,7 +1538,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _boolConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == propertyBuilder.Metadata.IsNullable, + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.IsNullable, $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif } @@ -1511,6 +1548,41 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return !propertyBuilder.Metadata.IsInModel ? null : _boolConventionContext.Result; } + public override bool? OnElementTypeNullabilityChanged(IConventionElementTypeBuilder builder) + { + if (!builder.Metadata.CollectionProperty.IsInModel) + { + return null; + } +#if DEBUG + var initialValue = builder.Metadata.IsNullable; +#endif + using (_dispatcher.DelayConventions()) + { + _boolConventionContext.ResetState(builder.Metadata.IsNullable); + foreach (var elementConvention in _conventionSet.ElementTypeNullabilityChangedConventions) + { + if (!builder.Metadata.IsInModel) + { + return null; + } + + elementConvention.ProcessElementTypeNullabilityChanged(builder, _boolConventionContext); + if (_boolConventionContext.ShouldStopProcessing()) + { + return _boolConventionContext.Result; + } +#if DEBUG + Check.DebugAssert( + initialValue == builder.Metadata.IsNullable, + $"Convention {elementConvention.GetType().Name} changed value without terminating"); +#endif + } + } + + return !builder.Metadata.IsInModel ? null : _boolConventionContext.Result; + } + public override FieldInfo? OnPropertyFieldChanged( IConventionPropertyBuilder propertyBuilder, FieldInfo? newFieldInfo, @@ -1534,7 +1606,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _fieldInfoConventionContext.Result; } #if DEBUG - Check.DebugAssert(initialValue == propertyBuilder.Metadata.FieldInfo, + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.FieldInfo, $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif } @@ -1542,6 +1615,38 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _fieldInfoConventionContext.Result; } + public override IElementType? OnPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType? newElementType, + IElementType? oldElementType) + { + if (!propertyBuilder.Metadata.IsInModel + || !propertyBuilder.Metadata.DeclaringType.IsInModel) + { + return null; + } +#if DEBUG + var initialValue = propertyBuilder.Metadata.GetElementType(); +#endif + _elementTypeConventionContext.ResetState(newElementType); + foreach (var propertyConvention in _conventionSet.PropertyElementTypeChangedConventions) + { + propertyConvention.ProcessPropertyElementTypeChanged( + propertyBuilder, newElementType, oldElementType, _elementTypeConventionContext); + if (_elementTypeConventionContext.ShouldStopProcessing()) + { + return _elementTypeConventionContext.Result; + } +#if DEBUG + Check.DebugAssert( + initialValue == propertyBuilder.Metadata.GetElementType(), + $"Convention {propertyConvention.GetType().Name} changed value without terminating"); +#endif + } + + return _elementTypeConventionContext.Result; + } + public override IConventionAnnotation? OnPropertyAnnotationChanged( IConventionPropertyBuilder propertyBuilder, string name, @@ -1569,7 +1674,8 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return _annotationConventionContext.Result; } #if DEBUG - Check.DebugAssert(propertyBuilder.Metadata is { IsInModel: true, DeclaringType.IsInModel: true } + Check.DebugAssert( + propertyBuilder.Metadata is { IsInModel: true, DeclaringType.IsInModel: true } && initialValue == propertyBuilder.Metadata[name], $"Convention {propertyConvention.GetType().Name} changed value without terminating"); #endif @@ -1579,6 +1685,44 @@ public IConventionModelBuilder OnModelInitialized(IConventionModelBuilder modelB return !propertyBuilder.Metadata.IsInModel ? null : annotation; } + public override IConventionAnnotation? OnElementTypeAnnotationChanged( + IConventionElementTypeBuilder builder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation) + { + if (!builder.Metadata.IsInModel + || !builder.Metadata.CollectionProperty.IsInModel) + { + return null; + } +#if DEBUG + var initialValue = builder.Metadata[name]; +#endif + using (_dispatcher.DelayConventions()) + { + _annotationConventionContext.ResetState(annotation); + foreach (var elementConvention in _conventionSet.ElementTypeAnnotationChangedConventions) + { + elementConvention.ProcessElementTypeAnnotationChanged( + builder, name, annotation, oldAnnotation, _annotationConventionContext); + + if (_annotationConventionContext.ShouldStopProcessing()) + { + return _annotationConventionContext.Result; + } +#if DEBUG + Check.DebugAssert( + builder.Metadata is { IsInModel: true, CollectionProperty.IsInModel: true } + && initialValue == builder.Metadata[name], + $"Convention {elementConvention.GetType().Name} changed value without terminating"); +#endif + } + } + + return !builder.Metadata.IsInModel ? null : annotation; + } + public override IConventionProperty? OnPropertyRemoved( IConventionTypeBaseBuilder typeBaseBuilder, IConventionProperty property) diff --git a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs index 6d860e316d1..1f4f806de38 100644 --- a/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs +++ b/src/EFCore/Metadata/Conventions/Internal/ConventionDispatcher.cs @@ -668,6 +668,15 @@ public virtual IConventionModelBuilder OnModelFinalizing(IConventionModelBuilder public virtual bool? OnPropertyNullabilityChanged(IConventionPropertyBuilder propertyBuilder) => _scope.OnPropertyNullabilityChanged(propertyBuilder); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool? OnElementTypeNullabilityChanged(IConventionElementTypeBuilder builder) + => _scope.OnElementTypeNullabilityChanged(builder); + /// /// 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 @@ -680,6 +689,18 @@ public virtual IConventionModelBuilder OnModelFinalizing(IConventionModelBuilder FieldInfo? oldFieldInfo) => _scope.OnPropertyFieldChanged(propertyBuilder, newFieldInfo, oldFieldInfo); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IElementType? OnPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType? newElementType, + IElementType? oldElementType) + => _scope.OnPropertyElementTypeChanged(propertyBuilder, newElementType, oldElementType); + /// /// 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 @@ -704,6 +725,25 @@ public virtual IConventionModelBuilder OnModelFinalizing(IConventionModelBuilder oldAnnotation); } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionAnnotation? OnElementTypeAnnotationChanged( + IConventionElementTypeBuilder builder, + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation) + => CoreAnnotationNames.AllNames.Contains(name) + ? annotation + : _scope.OnElementTypeAnnotationChanged( + builder, + name, + annotation, + oldAnnotation); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs index 78d27ecf6fe..8cfb7acfd50 100644 --- a/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/PropertyDiscoveryConvention.cs @@ -45,7 +45,7 @@ public void ProcessComplexPropertyAdded( var model = complexType.Model; foreach (var propertyInfo in complexType.GetRuntimeProperties().Values) { - if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(propertyInfo, model)) + if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(propertyInfo, model, out _)) { continue; } @@ -55,7 +55,7 @@ public void ProcessComplexPropertyAdded( foreach (var fieldInfo in complexType.GetRuntimeFields().Values) { - if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(fieldInfo, model)) + if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(fieldInfo, model, out _)) { continue; } @@ -85,13 +85,17 @@ private void Process(IConventionEntityTypeBuilder entityTypeBuilder) var model = entityType.Model; foreach (var propertyInfo in entityType.GetRuntimeProperties().Values) { - if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(propertyInfo, model) + if (!Dependencies.MemberClassifier.IsCandidatePrimitiveProperty(propertyInfo, model, out var mapping) || ((Model)model).FindIsComplexConfigurationSource(propertyInfo.GetMemberType().UnwrapNullableType()) != null) { continue; } - entityTypeBuilder.Property(propertyInfo); + var propertyBuilder = entityTypeBuilder.Property(propertyInfo); + if (mapping?.ElementTypeMapping != null) + { + propertyBuilder?.ElementType(true); + } } } } diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index f158cbe1bca..ca406251699 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -29,7 +29,7 @@ public RuntimeModelConvention( /// protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } - /// + /// public virtual IModel ProcessModelFinalized(IModel model) => Create(model); @@ -58,6 +58,15 @@ protected virtual RuntimeModel Create(IModel model) CreateAnnotations( property, runtimeProperty, static (convention, annotations, source, target, runtime) => convention.ProcessPropertyAnnotations(annotations, source, target, runtime)); + + var elementType = property.GetElementType(); + if (elementType != null) + { + var runtimeElementType = Create(runtimeProperty, elementType); + CreateAnnotations( + elementType, runtimeElementType, static (convention, annotations, source, target, runtime) => + convention.ProcessElementTypeAnnotations(annotations, source, target, runtime)); + } } foreach (var serviceProperty in entityType.GetDeclaredServiceProperties()) @@ -389,6 +398,20 @@ private static RuntimeProperty Create(IProperty property, RuntimeTypeBase runtim property.GetJsonValueReaderWriter(), property.GetTypeMapping()); + private static RuntimeElementType Create(RuntimeProperty runtimeProperty, IElementType element) + => runtimeProperty.SetElementType( + element.ClrType, + element.IsNullable, + element.GetMaxLength(), + element.IsUnicode(), + element.GetPrecision(), + element.GetScale(), + element.GetProviderClrType(), + element.GetValueConverter(), + element.GetValueComparer(), + element.GetJsonValueReaderWriter(), + element.GetTypeMapping()); + /// /// Updates the property annotations that will be set on the read-only object. /// @@ -414,6 +437,31 @@ protected virtual void ProcessPropertyAnnotations( } } + /// + /// Updates the element type annotations that will be set on the read-only object. + /// + /// The annotations to be processed. + /// The source element type. + /// The target element type that will contain the annotations. + /// Indicates whether the given annotations are runtime annotations. + protected virtual void ProcessElementTypeAnnotations( + Dictionary annotations, + IElementType element, + RuntimeElementType runtimeElement, + bool runtime) + { + if (!runtime) + { + foreach (var (key, _) in annotations) + { + if (CoreAnnotationNames.AllNames.Contains(key)) + { + annotations.Remove(key); + } + } + } + } + private static RuntimeServiceProperty Create(IServiceProperty property, RuntimeEntityType runtimeEntityType) => runtimeEntityType.AddServiceProperty( property.Name, @@ -450,18 +498,18 @@ protected virtual void ProcessServicePropertyAnnotations( private RuntimeComplexProperty Create(IComplexProperty complexProperty, RuntimeEntityType runtimeEntityType) { var runtimeComplexProperty = runtimeEntityType.AddComplexProperty( - complexProperty.Name, - complexProperty.ClrType, - complexProperty.ComplexType.Name, - complexProperty.ComplexType.ClrType, - complexProperty.PropertyInfo, - complexProperty.FieldInfo, - complexProperty.GetPropertyAccessMode(), - complexProperty.IsNullable, - complexProperty.IsCollection, - complexProperty.ComplexType.GetChangeTrackingStrategy(), - complexProperty.ComplexType.FindIndexerPropertyInfo(), - complexProperty.ComplexType.IsPropertyBag); + complexProperty.Name, + complexProperty.ClrType, + complexProperty.ComplexType.Name, + complexProperty.ComplexType.ClrType, + complexProperty.PropertyInfo, + complexProperty.FieldInfo, + complexProperty.GetPropertyAccessMode(), + complexProperty.IsNullable, + complexProperty.IsCollection, + complexProperty.ComplexType.GetChangeTrackingStrategy(), + complexProperty.ComplexType.FindIndexerPropertyInfo(), + complexProperty.ComplexType.IsPropertyBag); var complexType = complexProperty.ComplexType; var runtimeComplexType = runtimeComplexProperty.ComplexType; @@ -472,6 +520,15 @@ private RuntimeComplexProperty Create(IComplexProperty complexProperty, RuntimeE CreateAnnotations( property, runtimeProperty, static (convention, annotations, source, target, runtime) => convention.ProcessPropertyAnnotations(annotations, source, target, runtime)); + + var elementType = property.GetElementType(); + if (elementType != null) + { + var runtimeElementType = Create(runtimeProperty, elementType); + CreateAnnotations( + elementType, runtimeElementType, static (convention, annotations, source, target, runtime) => + convention.ProcessElementTypeAnnotations(annotations, source, target, runtime)); + } } foreach (var property in complexType.GetComplexProperties()) @@ -488,18 +545,18 @@ private RuntimeComplexProperty Create(IComplexProperty complexProperty, RuntimeE private RuntimeComplexProperty Create(IComplexProperty complexProperty, RuntimeComplexType runtimeComplexType) { var runtimeComplexProperty = runtimeComplexType.AddComplexProperty( - complexProperty.Name, - complexProperty.ClrType, - complexProperty.ComplexType.Name, - complexProperty.ComplexType.ClrType, - complexProperty.PropertyInfo, - complexProperty.FieldInfo, - complexProperty.GetPropertyAccessMode(), - complexProperty.IsNullable, - complexProperty.IsCollection, - complexProperty.ComplexType.GetChangeTrackingStrategy(), - complexProperty.ComplexType.FindIndexerPropertyInfo(), - complexProperty.ComplexType.IsPropertyBag); + complexProperty.Name, + complexProperty.ClrType, + complexProperty.ComplexType.Name, + complexProperty.ComplexType.ClrType, + complexProperty.PropertyInfo, + complexProperty.FieldInfo, + complexProperty.GetPropertyAccessMode(), + complexProperty.IsNullable, + complexProperty.IsCollection, + complexProperty.ComplexType.GetChangeTrackingStrategy(), + complexProperty.ComplexType.FindIndexerPropertyInfo(), + complexProperty.ComplexType.IsPropertyBag); var complexType = complexProperty.ComplexType; var newRuntimeComplexType = runtimeComplexProperty.ComplexType; @@ -510,6 +567,15 @@ private RuntimeComplexProperty Create(IComplexProperty complexProperty, RuntimeC CreateAnnotations( property, runtimeProperty, static (convention, annotations, source, target, runtime) => convention.ProcessPropertyAnnotations(annotations, source, target, runtime)); + + var elementType = property.GetElementType(); + if (elementType != null) + { + var runtimeElementType = Create(runtimeProperty, elementType); + CreateAnnotations( + elementType, runtimeElementType, static (convention, annotations, source, target, runtime) => + convention.ProcessElementTypeAnnotations(annotations, source, target, runtime)); + } } foreach (var property in complexType.GetComplexProperties()) diff --git a/src/EFCore/Metadata/IConventionElementType.cs b/src/EFCore/Metadata/IConventionElementType.cs new file mode 100644 index 00000000000..5ff6fb71a66 --- /dev/null +++ b/src/EFCore/Metadata/IConventionElementType.cs @@ -0,0 +1,215 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents the elements of a collection property. +/// +/// +/// +/// This interface is used during model creation and allows the metadata to be modified. +/// Once the model is built, represents a read-only view of the same metadata. +/// +/// +/// See Model building conventions for more information and examples. +/// +/// +public interface IConventionElementType : IReadOnlyElementType, IConventionAnnotatable +{ + /// + /// Gets the collection property for which this represents the element. + /// + new IConventionProperty CollectionProperty { get; } + + /// + /// Returns the configuration source for this element. + /// + /// The configuration source. + ConfigurationSource GetConfigurationSource(); + + /// + /// Gets the builder that can be used to configure this element. + /// + /// If the element has been removed from the model. + new IConventionElementTypeBuilder Builder { get; } + + /// + /// Sets a value indicating whether elements in the collection can be . + /// + /// + /// A value indicating whether whether elements in the collection can be , or to + /// reset to the default. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + bool? SetIsNullable(bool? nullable, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetIsNullableConfigurationSource(); + + /// + /// Sets the for the given element. + /// + /// The for this element. + /// Indicates whether the configuration was specified using a data annotation. + CoreTypeMapping? SetTypeMapping(CoreTypeMapping typeMapping, bool fromDataAnnotation = false); + + /// + /// Gets the for of the element. + /// + /// The for of the element. + ConfigurationSource? GetTypeMappingConfigurationSource(); + + /// + /// Sets the maximum length of data that is allowed in elements of the collection. For example, if the element type is + /// a then this is the maximum number of characters. + /// + /// The maximum length of data that is allowed in each element. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured property. + int? SetMaxLength(int? maxLength, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetMaxLengthConfigurationSource(); + + /// + /// Sets the precision of data that is allowed in elements of the collection. + /// For example, if the element type is a , then this is the maximum number of digits. + /// + /// The maximum number of digits that is allowed in each element. + /// Indicates whether the configuration was specified using a data annotation. + int? SetPrecision(int? precision, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetPrecisionConfigurationSource(); + + /// + /// Sets the scale of data that is allowed in this elements of the collection. + /// For example, if the element type is a , then this is the maximum number of decimal places. + /// + /// The maximum number of decimal places that is allowed in each element. + /// Indicates whether the configuration was specified using a data annotation. + int? SetScale(int? scale, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetScaleConfigurationSource(); + + /// + /// Sets a value indicating whether elements of the collection can persist Unicode characters. + /// + /// + /// if the elements of the collection accept Unicode characters, if they do not, + /// or to clear the setting. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + bool? SetIsUnicode(bool? unicode, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetIsUnicodeConfigurationSource(); + + /// + /// Sets the custom for this elements of the collection. + /// + /// The converter, or to remove any previously set converter. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + ValueConverter? SetValueConverter(ValueConverter? converter, bool fromDataAnnotation = false); + + /// + /// Sets the custom for this elements of the collection. + /// + /// + /// A type that inherits from , or to remove any previously set converter. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + Type? SetValueConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetValueConverterConfigurationSource(); + + /// + /// Sets the type that the elements of the collection will be converted to before being sent to the database provider. + /// + /// The type to use, or to remove any previously set type. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + Type? SetProviderClrType(Type? providerClrType, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetProviderClrTypeConfigurationSource(); + + /// + /// Sets the custom for elements of the collection. + /// + /// The comparer, or to remove any previously set comparer. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + ValueComparer? SetValueComparer(ValueComparer? comparer, bool fromDataAnnotation = false); + + /// + /// Sets the custom for elements of the collection. + /// + /// + /// A type that inherits from , or to remove any previously set comparer. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? SetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetValueComparerConfigurationSource(); + + /// + /// Sets the type of to use for elements of the collection. + /// + /// + /// A type that inherits from , or to use the reader/writer + /// from the type mapping. + /// + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + Type? SetJsonValueReaderWriterType(Type? readerWriterType, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetJsonValueReaderWriterTypeConfigurationSource(); +} diff --git a/src/EFCore/Metadata/IConventionProperty.cs b/src/EFCore/Metadata/IConventionProperty.cs index a2674a41936..4bc664c3527 100644 --- a/src/EFCore/Metadata/IConventionProperty.cs +++ b/src/EFCore/Metadata/IConventionProperty.cs @@ -30,7 +30,8 @@ public interface IConventionProperty : IReadOnlyProperty, IConventionPropertyBas /// Gets the entity type that this property belongs to. /// [Obsolete("Use DeclaringType and cast to IConventionEntityType or IConventionComplexType")] - new IConventionEntityType DeclaringEntityType => (IConventionEntityType)DeclaringType; + new IConventionEntityType DeclaringEntityType + => (IConventionEntityType)DeclaringType; /// /// Returns the configuration source for . @@ -335,7 +336,8 @@ bool IsImplicitlyCreated() /// Indicates whether the configuration was specified using a data annotation. /// The configured value. Type? SetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactory, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactory, bool fromDataAnnotation = false); /// @@ -361,7 +363,8 @@ bool IsImplicitlyCreated() /// Indicates whether the configuration was specified using a data annotation. /// The configured value. Type? SetValueConverter( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, bool fromDataAnnotation = false); /// @@ -402,7 +405,8 @@ bool IsImplicitlyCreated() /// The configured value. [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? SetValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation = false); /// @@ -429,7 +433,8 @@ bool IsImplicitlyCreated() /// The configured value. [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? SetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation = false); /// @@ -454,4 +459,18 @@ bool IsImplicitlyCreated() /// /// The configuration source for . ConfigurationSource? GetJsonValueReaderWriterTypeConfigurationSource(); + + /// + /// Sets the configuration for elements of the primitive collection represented by this property. + /// + /// If , then this is a collection of primitive elements. + /// Indicates whether the configuration was specified using a data annotation. + /// The configuration for the elements. + IElementType? ElementType(bool primitiveCollection, bool fromDataAnnotation = false); + + /// + /// Returns the configuration source for . + /// + /// The configuration source for . + ConfigurationSource? GetElementTypeConfigurationSource(); } diff --git a/src/EFCore/Metadata/IElementType.cs b/src/EFCore/Metadata/IElementType.cs new file mode 100644 index 00000000000..e4e6637257d --- /dev/null +++ b/src/EFCore/Metadata/IElementType.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.Metadata; + +/// +/// Represents the elements of a collection property. +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public interface IElementType : IReadOnlyElementType, IAnnotatable +{ + /// + /// Gets the collection property for which this represents the element. + /// + new IProperty CollectionProperty + { + [DebuggerStepThrough] + get => (IProperty)((IReadOnlyElementType)this).CollectionProperty; + } +} diff --git a/src/EFCore/Metadata/IMutableElementType.cs b/src/EFCore/Metadata/IMutableElementType.cs new file mode 100644 index 00000000000..db729ca754d --- /dev/null +++ b/src/EFCore/Metadata/IMutableElementType.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents the elements of a collection property. +/// +/// +/// +/// This interface is used during model creation and allows the metadata to be modified. +/// Once the model is built, represents a read-only view of the same metadata. +/// +/// +/// See Modeling entity types and relationships for more information and +/// examples. +/// +/// +public interface IMutableElementType : IReadOnlyElementType, IMutableAnnotatable +{ + /// + /// Gets the collection property for which this represents the element. + /// + new IMutableProperty CollectionProperty { get; } + + /// + /// Gets or sets a value indicating whether elements of the collection can be . + /// + new bool IsNullable { get; set; } + + /// + /// Sets the maximum length of data that is allowed in elements of the collection. For example, if the element type is + /// a then this is the maximum number of characters. + /// + /// The maximum length of data that is allowed in this elements of the collection. + void SetMaxLength(int? maxLength); + + /// + /// Sets the precision of data that is allowed in elements of the collection. + /// For example, if the element type is a , then this is the maximum number of digits. + /// + /// The maximum number of digits that is allowed in each element. + void SetPrecision(int? precision); + + /// + /// Sets the scale of data that is allowed in this elements of the collection. + /// For example, if the element type is a , then this is the maximum number of decimal places. + /// + /// The maximum number of decimal places that is allowed in each element. + void SetScale(int? scale); + + /// + /// Sets a value indicating whether elements of the collection can persist Unicode characters. + /// + /// + /// if the elements of the collection accept Unicode characters, if they do not, + /// or to clear the setting. + /// + void SetIsUnicode(bool? unicode); + + /// + /// Sets the custom for this elements of the collection. + /// + /// The converter, or to remove any previously set converter. + void SetValueConverter(ValueConverter? converter); + + /// + /// Sets the custom for this elements of the collection. + /// + /// + /// A type that inherits from , or to remove any previously set converter. + /// + void SetValueConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType); + + /// + /// Sets the type that the elements of the collection will be converted to before being sent to the database provider. + /// + /// The type to use, or to remove any previously set type. + void SetProviderClrType(Type? providerClrType); + + /// + /// Sets the for the given element. + /// + /// The for this element. + void SetTypeMapping(CoreTypeMapping typeMapping); + + /// + /// Sets the custom for elements of the collection. + /// + /// The comparer, or to remove any previously set comparer. + void SetValueComparer(ValueComparer? comparer); + + /// + /// Sets the custom for elements of the collection. + /// + /// + /// A type that inherits from , or to remove any previously set comparer. + /// + void SetValueComparer([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType); + + /// + /// Sets the type of to use for elements of the collection. + /// + /// + /// A type that inherits from , or to use the reader/writer + /// from the type mapping. + /// + void SetJsonValueReaderWriterType(Type? readerWriterType); + + /// + bool IReadOnlyElementType.IsNullable + => IsNullable; +} diff --git a/src/EFCore/Metadata/IMutableProperty.cs b/src/EFCore/Metadata/IMutableProperty.cs index fa4cabc33bb..5b9ea2539c8 100644 --- a/src/EFCore/Metadata/IMutableProperty.cs +++ b/src/EFCore/Metadata/IMutableProperty.cs @@ -25,7 +25,8 @@ public interface IMutableProperty : IReadOnlyProperty, IMutablePropertyBase /// Gets the entity type that this property belongs to. /// [Obsolete("Use DeclaringType and cast to IMutableEntityType or IMutableComplexType")] - new IMutableEntityType DeclaringEntityType => (IMutableEntityType)DeclaringType; + new IMutableEntityType DeclaringEntityType + => (IMutableEntityType)DeclaringType; /// /// Gets or sets a value indicating whether this property can contain . @@ -204,7 +205,8 @@ public interface IMutableProperty : IReadOnlyProperty, IMutablePropertyBase /// clear any previously set factory. /// void SetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactory); + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactory); /// /// Sets the custom for this property. @@ -259,7 +261,8 @@ void SetValueGeneratorFactory( /// A type that derives from , or to remove any previously set comparer. /// void SetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType); + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType); /// /// Sets the type of to use for this property for this property. @@ -270,15 +273,21 @@ void SetProviderValueComparer( /// void SetJsonValueReaderWriterType(Type? readerWriterType); - /// - bool IReadOnlyProperty.IsNullable => - IsNullable; + /// + /// Sets the configuration for elements of the primitive collection represented by this property. + /// + /// If , then this is a collection of primitive elements. + void ElementType(bool elementType); + + /// + bool IReadOnlyProperty.IsNullable + => IsNullable; - /// - ValueGenerated IReadOnlyProperty.ValueGenerated => - ValueGenerated; + /// + ValueGenerated IReadOnlyProperty.ValueGenerated + => ValueGenerated; - /// - bool IReadOnlyProperty.IsConcurrencyToken => - IsConcurrencyToken; + /// + bool IReadOnlyProperty.IsConcurrencyToken + => IsConcurrencyToken; } diff --git a/src/EFCore/Metadata/IProperty.cs b/src/EFCore/Metadata/IProperty.cs index 8b7bc5337c2..cbf7f7c3429 100644 --- a/src/EFCore/Metadata/IProperty.cs +++ b/src/EFCore/Metadata/IProperty.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -17,7 +18,8 @@ public interface IProperty : IReadOnlyProperty, IPropertyBase /// Gets the entity type that this property belongs to. /// [Obsolete("Use DeclaringType and cast to IEntityType or IComplexType")] - new IEntityType DeclaringEntityType => (IEntityType)DeclaringType; + new IEntityType DeclaringEntityType + => (IEntityType)DeclaringType; /// /// Creates an for values of the given property type. @@ -98,8 +100,7 @@ IEqualityComparer CreateKeyEqualityComparer() /// The comparer. new ValueComparer GetProviderValueComparer(); - - internal const System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes DynamicallyAccessedMemberTypes = + internal const DynamicallyAccessedMemberTypes DynamicallyAccessedMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties diff --git a/src/EFCore/Metadata/IReadOnlyElementType.cs b/src/EFCore/Metadata/IReadOnlyElementType.cs new file mode 100644 index 00000000000..c4b4e79e4eb --- /dev/null +++ b/src/EFCore/Metadata/IReadOnlyElementType.cs @@ -0,0 +1,165 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents the elements of a collection property. +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public interface IReadOnlyElementType : IReadOnlyAnnotatable +{ + /// + /// Gets the collection property for which this represents the element. + /// + IReadOnlyProperty CollectionProperty { get; } + + /// + /// The type of elements in the collection. + /// + [DynamicallyAccessedMembers(IEntityType.DynamicallyAccessedMemberTypes | IProperty.DynamicallyAccessedMemberTypes)] + Type ClrType { get; } + + /// + /// Gets a value indicating whether elements of the collection can be . + /// + bool IsNullable { get; } + + /// + /// Returns the for the elements of the collection from a finalized model. + /// + /// The type mapping. + CoreTypeMapping GetTypeMapping() + { + var mapping = FindTypeMapping(); + if (mapping == null) + { + throw new InvalidOperationException(CoreStrings.ModelNotFinalized(nameof(GetTypeMapping))); + } + + return mapping; + } + + /// + /// Returns the type mapping for elements of the collection. + /// + /// The type mapping, or if none was found. + CoreTypeMapping? FindTypeMapping(); + + /// + /// Gets the maximum length of data that is allowed in elements of the collection. For example, if the element type is + /// a then this is the maximum number of characters. + /// + /// + /// The maximum length, -1 if the property has no maximum length, or if the maximum length hasn't been + /// set. + /// + int? GetMaxLength(); + + /// + /// Gets the precision of data that is allowed in elements of the collection. + /// For example, if the element type is a , then this is the maximum number of digits. + /// + /// The precision, or if none is defined. + int? GetPrecision(); + + /// + /// Gets the scale of data that is allowed in this elements of the collection. + /// For example, if the element type is a , then this is the maximum number of decimal places. + /// + /// The scale, or if none is defined. + int? GetScale(); + + /// + /// Gets a value indicating whether elements of the collection can persist Unicode characters. + /// + /// The Unicode setting, or if none is defined. + bool? IsUnicode(); + + /// + /// Gets the custom for this elements of the collection. + /// + /// The converter, or if none has been set. + ValueConverter? GetValueConverter(); + + /// + /// Gets the type that the elements of the collection will be converted to before being sent to the database provider. + /// + /// The provider type, or if none has been set. + Type? GetProviderClrType(); + + /// + /// Gets the custom for elements of the collection. + /// + /// The comparer, or if none has been set. + ValueComparer? GetValueComparer(); + + /// + /// Gets the type of to use for elements of the collection. + /// + /// The reader/writer, or if none has been set. + JsonValueReaderWriter? GetJsonValueReaderWriter(); + + /// + /// + /// Creates a human-readable representation of the given metadata. + /// + /// + /// Warning: Do not rely on the format of the returned string. + /// It is designed for debugging only and may change arbitrarily between releases. + /// + /// + /// Options for generating the string. + /// The number of indent spaces to use before each new line. + /// A human-readable representation. + string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOptions.ShortDefault, int indent = 0) + { + var builder = new StringBuilder(); + var indentString = new string(' ', indent); + + try + { + builder.Append(indentString); + + var singleLine = (options & MetadataDebugStringOptions.SingleLine) != 0; + if (singleLine) + { + builder.Append("Element type: "); + } + + builder.Append(ClrType.ShortDisplayName()); + + if (!IsNullable) + { + builder.Append(" Required"); + } + + if (GetMaxLength() != null) + { + builder.Append(" MaxLength(").Append(GetMaxLength()).Append(')'); + } + + if (IsUnicode() == false) + { + builder.Append(" ANSI"); + } + + if (!singleLine && (options & MetadataDebugStringOptions.IncludeAnnotations) != 0) + { + builder.Append(AnnotationsToDebugString(indent + 2)); + } + } + catch (Exception exception) + { + builder.AppendLine().AppendLine(CoreStrings.DebugViewError(exception.Message)); + } + + return builder.ToString(); + } +} diff --git a/src/EFCore/Metadata/IReadOnlyProperty.cs b/src/EFCore/Metadata/IReadOnlyProperty.cs index 79b18681f02..708ea5f2e46 100644 --- a/src/EFCore/Metadata/IReadOnlyProperty.cs +++ b/src/EFCore/Metadata/IReadOnlyProperty.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; -using System.Text; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -19,7 +19,8 @@ public interface IReadOnlyProperty : IReadOnlyPropertyBase /// Gets the entity type that this property belongs to. /// [Obsolete("Use DeclaringType and cast to IReadOnlyEntityType or IReadOnlyComplexType")] - IReadOnlyEntityType DeclaringEntityType => (IReadOnlyEntityType)DeclaringType; + IReadOnlyEntityType DeclaringEntityType + => (IReadOnlyEntityType)DeclaringType; /// /// Gets a value indicating whether this property can contain . @@ -70,8 +71,8 @@ CoreTypeMapping GetTypeMapping() /// then this is the maximum number of characters. /// /// - /// The maximum length, -1 if the property has no maximum length, or if the maximum length hasn't been - /// set. + /// The maximum length, -1 if the property has no maximum length, or if the maximum length hasn't been + /// set. /// int? GetMaxLength(); @@ -172,6 +173,12 @@ CoreTypeMapping GetTypeMapping() /// The reader/writer, or if none has been set. JsonValueReaderWriter? GetJsonValueReaderWriter(); + /// + /// Gets the configuration for elements of the primitive collection represented by this property. + /// + /// The configuration for the elements. + IElementType? GetElementType(); + /// /// Finds the first principal property that the given property is constrained by /// if the given property is part of a foreign key. @@ -413,6 +420,12 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt builder.Append(" PropertyAccessMode.").Append(GetPropertyAccessMode()); } + var elementType = GetElementType(); + if (elementType != null) + { + builder.Append(" Element type: ").Append(elementType.ToDebugString()); + } + if ((options & MetadataDebugStringOptions.IncludePropertyIndexes) != 0 && ((AnnotatableBase)this).IsReadOnly) { diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs index 998d7b70837..5d969565376 100644 --- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs +++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs @@ -332,6 +332,14 @@ public static class CoreAnnotationNames /// public const string JsonValueReaderWriterType = "JsonValueReaderWriterType"; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public const string ElementType = "ElementType"; + /// /// 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 @@ -381,6 +389,7 @@ public static class CoreAnnotationNames DuplicateServiceProperties, FullChangeTrackingNotificationsRequired, AdHocModel, - JsonValueReaderWriterType + JsonValueReaderWriterType, + ElementType }; } diff --git a/src/EFCore/Metadata/Internal/ElementType.cs b/src/EFCore/Metadata/Internal/ElementType.cs new file mode 100644 index 00000000000..da6e1cc5ee1 --- /dev/null +++ b/src/EFCore/Metadata/Internal/ElementType.cs @@ -0,0 +1,971 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class ElementType : ConventionAnnotatable, IMutableElementType, IConventionElementType, IElementType +{ + private InternalElementTypeBuilder? _builder; + + private bool? _isNullable; + private CoreTypeMapping? _typeMapping; + + private ConfigurationSource _configurationSource; + private ConfigurationSource? _isNullableConfigurationSource; + private ConfigurationSource? _typeMappingConfigurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ElementType( + Type clrType, + Property collectionProperty, + ConfigurationSource configurationSource) + { + ClrType = clrType; + CollectionProperty = collectionProperty; + _configurationSource = configurationSource; + _builder = new InternalElementTypeBuilder(this, collectionProperty.DeclaringType.Model.Builder); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource GetConfigurationSource() + => _configurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void UpdateConfigurationSource(ConfigurationSource configurationSource) + => _configurationSource = configurationSource.Max(_configurationSource); + + // Needed for a workaround before reference counting is implemented + // Issue #15898 + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void SetConfigurationSource(ConfigurationSource configurationSource) + => _configurationSource = configurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsInModel + => _builder is not null + && CollectionProperty.DeclaringType.IsInModel; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void SetRemovedFromModel() + => _builder = null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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. + /// + protected override IConventionAnnotation? OnAnnotationSet( + string name, + IConventionAnnotation? annotation, + IConventionAnnotation? oldAnnotation) + => CollectionProperty.DeclaringType.Model.ConventionDispatcher.OnElementTypeAnnotationChanged( + Builder, name, annotation, oldAnnotation); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual Property CollectionProperty { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DynamicallyAccessedMembers(IProperty.DynamicallyAccessedMemberTypes)] + public virtual Type ClrType { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override bool IsReadOnly + => CollectionProperty.IsReadOnly; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsNullable + { + get => _isNullable ?? DefaultIsNullable; + set => SetIsNullable(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool? SetIsNullable(bool? nullable, ConfigurationSource configurationSource) + { + EnsureMutable(); + + var isChanging = (nullable ?? DefaultIsNullable) != IsNullable; + if (nullable == null) + { + _isNullable = null; + _isNullableConfigurationSource = null; + if (isChanging) + { + OnElementTypeNullableChanged(); + } + + return nullable; + } + + if (nullable.Value && !ClrType.IsNullableType()) + { + throw new InvalidOperationException( + CoreStrings.CannotBeNullableElement( + CollectionProperty.DeclaringType.DisplayName(), CollectionProperty.Name, ClrType.ShortDisplayName())); + } + + _isNullableConfigurationSource = configurationSource.Max(_isNullableConfigurationSource); + + _isNullable = nullable; + + return isChanging + ? OnElementTypeNullableChanged() + : nullable; + } + + /// + /// 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. + /// + protected virtual bool? OnElementTypeNullableChanged() + => CollectionProperty.DeclaringType.Model.ConventionDispatcher.OnElementTypeNullabilityChanged(Builder); + + private bool DefaultIsNullable + => ClrType.IsNullableType(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetIsNullableConfigurationSource() + => _isNullableConfigurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int? SetMaxLength(int? maxLength, ConfigurationSource configurationSource) + { + if (maxLength is < -1) + { + throw new ArgumentOutOfRangeException(nameof(maxLength)); + } + + return (int?)SetOrRemoveAnnotation(CoreAnnotationNames.MaxLength, maxLength, configurationSource)?.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int? GetMaxLength() + => (int?)this[CoreAnnotationNames.MaxLength]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetMaxLengthConfigurationSource() + => FindAnnotation(CoreAnnotationNames.MaxLength)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool? SetIsUnicode(bool? unicode, ConfigurationSource configurationSource) + => (bool?)SetOrRemoveAnnotation(CoreAnnotationNames.Unicode, unicode, configurationSource)?.Value; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool? IsUnicode() + => (bool?)this[CoreAnnotationNames.Unicode]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetIsUnicodeConfigurationSource() + => FindAnnotation(CoreAnnotationNames.Unicode)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int? SetPrecision(int? precision, ConfigurationSource configurationSource) + { + if (precision != null && precision < 0) + { + throw new ArgumentOutOfRangeException(nameof(precision)); + } + + return (int?)SetOrRemoveAnnotation(CoreAnnotationNames.Precision, precision, configurationSource)?.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int? GetPrecision() + => (int?)this[CoreAnnotationNames.Precision]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetPrecisionConfigurationSource() + => FindAnnotation(CoreAnnotationNames.Precision)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int? SetScale(int? scale, ConfigurationSource configurationSource) + { + if (scale != null && scale < 0) + { + throw new ArgumentOutOfRangeException(nameof(scale)); + } + + return (int?)SetOrRemoveAnnotation(CoreAnnotationNames.Scale, scale, configurationSource)?.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual int? GetScale() + => (int?)this[CoreAnnotationNames.Scale]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetScaleConfigurationSource() + => FindAnnotation(CoreAnnotationNames.Scale)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ValueConverter? SetValueConverter( + ValueConverter? converter, + ConfigurationSource configurationSource) + { + var errorString = CheckValueConverter(converter); + if (errorString != null) + { + throw new InvalidOperationException(errorString); + } + + RemoveAnnotation(CoreAnnotationNames.ValueConverterType); + return (ValueConverter?)SetAnnotation(CoreAnnotationNames.ValueConverter, converter, configurationSource)?.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual Type? SetValueConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + ConfigurationSource configurationSource) + { + ValueConverter? converter = null; + if (converterType != null) + { + if (!typeof(ValueConverter).IsAssignableFrom(converterType)) + { + throw new InvalidOperationException( + CoreStrings.BadValueConverterType(converterType.ShortDisplayName(), typeof(ValueConverter).ShortDisplayName())); + } + + try + { + converter = (ValueConverter?)Activator.CreateInstance(converterType); + } + catch (Exception e) + { + throw new InvalidOperationException( + CoreStrings.CannotCreateValueConverter( + converterType.ShortDisplayName(), nameof(PropertyBuilder.HasConversion)), e); + } + } + + SetValueConverter(converter, configurationSource); + SetAnnotation(CoreAnnotationNames.ValueConverterType, converterType, configurationSource); + + return converterType; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ValueConverter? GetValueConverter() + => (ValueConverter?)FindAnnotation(CoreAnnotationNames.ValueConverter)?.Value; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetValueConverterConfigurationSource() + => FindAnnotation(CoreAnnotationNames.ValueConverter)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? CheckValueConverter(ValueConverter? converter) + => converter != null + && converter.ModelClrType.UnwrapNullableType() != ClrType.UnwrapNullableType() + ? CoreStrings.ConverterPropertyMismatchElement( + converter.ModelClrType.ShortDisplayName(), + CollectionProperty.DeclaringType.DisplayName(), + CollectionProperty.Name, + ClrType.ShortDisplayName()) + : null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 Type? SetProviderClrType(Type? providerClrType, ConfigurationSource configurationSource) + => (Type?)SetAnnotation(CoreAnnotationNames.ProviderClrType, providerClrType, configurationSource)?.Value; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual Type? GetProviderClrType() + => (Type?)FindAnnotation(CoreAnnotationNames.ProviderClrType)?.Value; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetProviderClrTypeConfigurationSource() + => FindAnnotation(CoreAnnotationNames.ProviderClrType)?.GetConfigurationSource(); + + /// + /// 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. + /// + [DisallowNull] + public virtual CoreTypeMapping? TypeMapping + { + get => IsReadOnly + ? NonCapturingLazyInitializer.EnsureInitialized( + ref _typeMapping, (IElementType)this, static elementType => + elementType.CollectionProperty.DeclaringType.Model.GetModelDependencies().TypeMappingSource.FindMapping(elementType)!) + : _typeMapping; + + set => SetTypeMapping(value, ConfigurationSource.Explicit); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual CoreTypeMapping? SetTypeMapping(CoreTypeMapping? typeMapping, ConfigurationSource configurationSource) + { + _typeMapping = typeMapping; + _typeMappingConfigurationSource = typeMapping is null + ? null + : configurationSource.Max(_typeMappingConfigurationSource); + + return typeMapping; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetTypeMappingConfigurationSource() + => _typeMappingConfigurationSource; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ValueComparer? SetValueComparer(ValueComparer? comparer, ConfigurationSource configurationSource) + { + var errorString = CheckValueComparer(comparer); + if (errorString != null) + { + throw new InvalidOperationException(errorString); + } + + RemoveAnnotation(CoreAnnotationNames.ValueComparerType); + return (ValueComparer?)SetOrRemoveAnnotation(CoreAnnotationNames.ValueComparer, comparer, configurationSource)?.Value; + } + + /// + /// 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. + /// + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + public virtual Type? SetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + ConfigurationSource configurationSource) + { + ValueComparer? comparer = null; + if (comparerType != null) + { + if (!typeof(ValueComparer).IsAssignableFrom(comparerType)) + { + throw new InvalidOperationException( + CoreStrings.BadValueComparerType(comparerType.ShortDisplayName(), typeof(ValueComparer).ShortDisplayName())); + } + + try + { + comparer = (ValueComparer?)Activator.CreateInstance(comparerType); + } + catch (Exception e) + { + throw new InvalidOperationException( + CoreStrings.CannotCreateValueComparer( + comparerType.ShortDisplayName(), nameof(PropertyBuilder.HasConversion)), e); + } + } + + SetValueComparer(comparer, configurationSource); + return (Type?)SetOrRemoveAnnotation(CoreAnnotationNames.ValueComparerType, comparerType, configurationSource)?.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ValueComparer? GetValueComparer() + => ((ValueComparer?)this[CoreAnnotationNames.ValueComparer] + ?? TypeMapping?.Comparer)?.ToNullableComparer(ClrType); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetValueComparerConfigurationSource() + => FindAnnotation(CoreAnnotationNames.ValueComparer)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? CheckValueComparer(ValueComparer? comparer) + => comparer != null + && comparer.Type.UnwrapNullableType() != ClrType.UnwrapNullableType() + ? CoreStrings.ComparerPropertyMismatchElement( + comparer.Type.ShortDisplayName(), + CollectionProperty.DeclaringType.DisplayName(), + CollectionProperty.Name, + ClrType.ShortDisplayName()) + : null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 JsonValueReaderWriter? GetJsonValueReaderWriter() + => JsonValueReaderWriter.CreateFromType((Type?)this[CoreAnnotationNames.JsonValueReaderWriterType]) + ?? TypeMapping?.JsonValueReaderWriter; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual Type? SetJsonValueReaderWriterType( + Type? readerWriterType, + ConfigurationSource configurationSource) + { + if (readerWriterType != null) + { + var genericType = readerWriterType.GetGenericTypeImplementations(typeof(JsonValueReaderWriter<>)).FirstOrDefault(); + if (genericType == null) + { + throw new InvalidOperationException(CoreStrings.BadJsonValueReaderWriterType(readerWriterType.ShortDisplayName())); + } + } + + return (Type?)SetOrRemoveAnnotation(CoreAnnotationNames.JsonValueReaderWriterType, readerWriterType, configurationSource)?.Value; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetJsonValueReaderWriterTypeConfigurationSource() + => FindAnnotation(CoreAnnotationNames.JsonValueReaderWriterType)?.GetConfigurationSource(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual DebugView DebugView + => new( + () => ((IReadOnlyElementType)this).ToDebugString(), + () => ((IReadOnlyElementType)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string ToString() + => ((IReadOnlyElementType)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + /// 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. + /// + IReadOnlyProperty IReadOnlyElementType.CollectionProperty + { + [DebuggerStepThrough] + get => CollectionProperty; + } + + /// + /// 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. + /// + IMutableProperty IMutableElementType.CollectionProperty + { + [DebuggerStepThrough] + get => CollectionProperty; + } + + /// + /// 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. + /// + IConventionProperty IConventionElementType.CollectionProperty + { + [DebuggerStepThrough] + get => CollectionProperty; + } + + /// + /// 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. + /// + IConventionElementTypeBuilder IConventionElementType.Builder + { + [DebuggerStepThrough] + get => Builder; + } + + /// + /// 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. + /// + [DebuggerStepThrough] + CoreTypeMapping? IReadOnlyElementType.FindTypeMapping() + => TypeMapping; + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetTypeMapping(CoreTypeMapping typeMapping) + => SetTypeMapping(typeMapping, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + CoreTypeMapping? IConventionElementType.SetTypeMapping(CoreTypeMapping typeMapping, bool fromDataAnnotation) + => SetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + bool? IConventionElementType.SetIsNullable(bool? nullable, bool fromDataAnnotation) + => SetIsNullable( + nullable, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetMaxLength(int? maxLength) + => SetMaxLength(maxLength, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + int? IConventionElementType.SetMaxLength(int? maxLength, bool fromDataAnnotation) + => SetMaxLength(maxLength, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetPrecision(int? precision) + => SetPrecision(precision, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + int? IConventionElementType.SetPrecision(int? precision, bool fromDataAnnotation) + => SetPrecision(precision, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetScale(int? scale) + => SetScale(scale, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + int? IConventionElementType.SetScale(int? scale, bool fromDataAnnotation) + => SetScale(scale, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetIsUnicode(bool? unicode) + => SetIsUnicode(unicode, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + bool? IConventionElementType.SetIsUnicode(bool? unicode, bool fromDataAnnotation) + => SetIsUnicode(unicode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetValueConverter(ValueConverter? converter) + => SetValueConverter(converter, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + ValueConverter? IConventionElementType.SetValueConverter(ValueConverter? converter, bool fromDataAnnotation) + => SetValueConverter( + converter, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetValueConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType) + => SetValueConverter(converterType, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + Type? IConventionElementType.SetValueConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + bool fromDataAnnotation) + => SetValueConverter( + converterType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetProviderClrType(Type? providerClrType) + => SetProviderClrType(providerClrType, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + Type? IConventionElementType.SetProviderClrType(Type? providerClrType, bool fromDataAnnotation) + => SetProviderClrType( + providerClrType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetValueComparer(ValueComparer? comparer) + => SetValueComparer(comparer, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + ValueComparer? IConventionElementType.SetValueComparer(ValueComparer? comparer, bool fromDataAnnotation) + => SetValueComparer( + comparer, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType) + => SetValueComparer(comparerType, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? IConventionElementType.SetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + bool fromDataAnnotation) + => SetValueComparer( + comparerType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableElementType.SetJsonValueReaderWriterType(Type? readerWriterType) + => SetJsonValueReaderWriterType(readerWriterType, ConfigurationSource.Explicit); + + /// + /// 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. + /// + [DebuggerStepThrough] + Type? IConventionElementType.SetJsonValueReaderWriterType( + Type? readerWriterType, + bool fromDataAnnotation) + => SetJsonValueReaderWriterType( + readerWriterType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore/Metadata/Internal/IMemberClassifier.cs b/src/EFCore/Metadata/Internal/IMemberClassifier.cs index d6d3dd2440c..139aac305fd 100644 --- a/src/EFCore/Metadata/Internal/IMemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/IMemberClassifier.cs @@ -40,7 +40,7 @@ public interface IMemberClassifier /// 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. /// - bool IsCandidatePrimitiveProperty(MemberInfo memberInfo, IConventionModel model); + bool IsCandidatePrimitiveProperty(MemberInfo memberInfo, IConventionModel model, out CoreTypeMapping? typeMapping); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,8 +48,11 @@ public interface IMemberClassifier /// 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. /// - bool IsCandidateComplexProperty(MemberInfo memberInfo, IConventionModel model, - out Type? elementType, out bool explicitlyConfigured); + bool IsCandidateComplexProperty( + MemberInfo memberInfo, + IConventionModel model, + out Type? elementType, + out bool explicitlyConfigured); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/InternalElementTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalElementTypeBuilder.cs new file mode 100644 index 00000000000..a05183517b2 --- /dev/null +++ b/src/EFCore/Metadata/Internal/InternalElementTypeBuilder.cs @@ -0,0 +1,648 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class InternalElementTypeBuilder : AnnotatableBuilder, IConventionElementTypeBuilder +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public InternalElementTypeBuilder(ElementType element, InternalModelBuilder modelBuilder) + : base(element, modelBuilder) + { + } + + /// + /// 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. + /// + protected virtual IConventionElementTypeBuilder This + => this; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? IsRequired(bool? required, ConfigurationSource configurationSource) + { + if (configurationSource != ConfigurationSource.Explicit + && !CanSetIsRequired(required, configurationSource)) + { + return null; + } + + Metadata.SetIsNullable(!required, configurationSource); + + return this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetIsRequired(bool? required, ConfigurationSource? configurationSource) + => ((configurationSource.HasValue + && configurationSource.Value.Overrides(Metadata.GetIsNullableConfigurationSource())) + || (Metadata.IsNullable == !required)) + && (required != false + || Metadata.ClrType.IsNullableType()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasMaxLength(int? maxLength, ConfigurationSource configurationSource) + { + if (CanSetMaxLength(maxLength, configurationSource)) + { + Metadata.SetMaxLength(maxLength, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetMaxLength(int? maxLength, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetMaxLengthConfigurationSource()) + || Metadata.GetMaxLength() == maxLength; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasPrecision(int? precision, ConfigurationSource configurationSource) + { + if (CanSetPrecision(precision, configurationSource)) + { + Metadata.SetPrecision(precision, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetPrecision(int? precision, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetPrecisionConfigurationSource()) + || Metadata.GetPrecision() == precision; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasScale(int? scale, ConfigurationSource configurationSource) + { + if (CanSetScale(scale, configurationSource)) + { + Metadata.SetScale(scale, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetScale(int? scale, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetScaleConfigurationSource()) + || Metadata.GetScale() == scale; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? IsUnicode(bool? unicode, ConfigurationSource configurationSource) + { + if (CanSetIsUnicode(unicode, configurationSource)) + { + Metadata.SetIsUnicode(unicode, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetIsUnicode(bool? unicode, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetIsUnicodeConfigurationSource()) + || Metadata.IsUnicode() == unicode; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasConversion(ValueConverter? converter, ConfigurationSource configurationSource) + { + if (CanSetConversion(converter, configurationSource)) + { + Metadata.SetProviderClrType(null, configurationSource); + Metadata.SetValueConverter(converter, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetConversion( + ValueConverter? converter, + ConfigurationSource? configurationSource) + => (configurationSource == ConfigurationSource.Explicit + || (configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()) + && Metadata.CheckValueConverter(converter) == null) + || (Metadata[CoreAnnotationNames.ValueConverterType] == null + && (ValueConverter?)Metadata[CoreAnnotationNames.ValueConverter] == converter)) + && configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasConversion(Type? providerClrType, ConfigurationSource configurationSource) + { + if (CanSetConversion(providerClrType, configurationSource)) + { + Metadata.SetValueConverter((ValueConverter?)null, configurationSource); + Metadata.SetProviderClrType(providerClrType, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetConversion(Type? providerClrType, ConfigurationSource? configurationSource) + => (configurationSource.Overrides(Metadata.GetProviderClrTypeConfigurationSource()) + || Metadata.GetProviderClrType() == providerClrType) + && configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + ConfigurationSource configurationSource) + { + if (CanSetConverter(converterType, configurationSource)) + { + Metadata.SetProviderClrType(null, configurationSource); + Metadata.SetValueConverter(converterType, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()) + || (Metadata[CoreAnnotationNames.ValueConverter] == null + && (Type?)Metadata[CoreAnnotationNames.ValueConverterType] == converterType); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasTypeMapping( + CoreTypeMapping? typeMapping, + ConfigurationSource configurationSource) + { + if (CanSetTypeMapping(typeMapping, configurationSource)) + { + Metadata.SetTypeMapping(typeMapping, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetTypeMapping(CoreTypeMapping? typeMapping, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetTypeMappingConfigurationSource()) + || Metadata.TypeMapping == typeMapping; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasValueComparer( + ValueComparer? comparer, + ConfigurationSource configurationSource) + { + if (CanSetValueComparer(comparer, configurationSource)) + { + Metadata.SetValueComparer(comparer, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetValueComparer(ValueComparer? comparer, ConfigurationSource? configurationSource) + { + if (configurationSource.Overrides(Metadata.GetValueComparerConfigurationSource())) + { + var errorString = Metadata.CheckValueComparer(comparer); + if (errorString != null) + { + if (configurationSource == ConfigurationSource.Explicit) + { + throw new InvalidOperationException(errorString); + } + + return false; + } + + return true; + } + + return Metadata[CoreAnnotationNames.ValueComparerType] == null + && Metadata[CoreAnnotationNames.ValueComparer] == comparer; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? HasValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + ConfigurationSource configurationSource) + { + if (CanSetValueComparer(comparerType, configurationSource)) + { + Metadata.SetValueComparer(comparerType, configurationSource); + + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetValueComparer(Type? comparerType, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetValueComparerConfigurationSource()) + || (Metadata[CoreAnnotationNames.ValueComparer] == null + && (Type?)Metadata[CoreAnnotationNames.ValueComparerType] == comparerType); + + /// + /// 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. + /// + IConventionElementType IConventionElementTypeBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + /// 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. + /// + [DebuggerStepThrough] + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasAnnotation(string name, object? value, bool fromDataAnnotation) + => base.HasAnnotation( + name, value, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + == null + ? null + : this; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DebuggerStepThrough] + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasNonNullAnnotation(string name, object? value, bool fromDataAnnotation) + => base.HasNonNullAnnotation( + name, value, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + == null + ? null + : this; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DebuggerStepThrough] + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasNoAnnotation(string name, bool fromDataAnnotation) + => base.HasNoAnnotation( + name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + == null + ? null + : this; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.IsRequired(bool? required, bool fromDataAnnotation) + => IsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetIsRequired(bool? required, bool fromDataAnnotation) + => CanSetIsRequired(required, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasMaxLength(int? maxLength, bool fromDataAnnotation) + => HasMaxLength(maxLength, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetMaxLength(int? maxLength, bool fromDataAnnotation) + => CanSetMaxLength(maxLength, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.IsUnicode(bool? unicode, bool fromDataAnnotation) + => IsUnicode(unicode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetIsUnicode(bool? unicode, bool fromDataAnnotation) + => CanSetIsUnicode(unicode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasPrecision(int? precision, bool fromDataAnnotation) + => HasPrecision(precision, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetPrecision(int? precision, bool fromDataAnnotation) + => CanSetPrecision(precision, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasScale(int? scale, bool fromDataAnnotation) + => HasScale(scale, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetScale(int? scale, bool fromDataAnnotation) + => CanSetScale(scale, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasConversion(ValueConverter? converter, bool fromDataAnnotation) + => HasConversion(converter, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetConversion(ValueConverter? converter, bool fromDataAnnotation) + => CanSetConversion(converter, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + bool fromDataAnnotation) + => HasConverter(converterType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, + bool fromDataAnnotation) + => CanSetConverter(converterType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasConversion(Type? providerClrType, bool fromDataAnnotation) + => HasConversion( + providerClrType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetConversion(Type? providerClrType, bool fromDataAnnotation) + => CanSetConversion(providerClrType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasTypeMapping( + CoreTypeMapping? typeMapping, + bool fromDataAnnotation) + => HasTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + bool IConventionElementTypeBuilder.CanSetTypeMapping(CoreTypeMapping typeMapping, bool fromDataAnnotation) + => CanSetTypeMapping(typeMapping, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasValueComparer(ValueComparer? comparer, bool fromDataAnnotation) + => HasValueComparer(comparer, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetValueComparer(ValueComparer? comparer, bool fromDataAnnotation) + => CanSetValueComparer(comparer, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionElementTypeBuilder.HasValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + bool fromDataAnnotation) + => HasValueComparer( + comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionElementTypeBuilder.CanSetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, + bool fromDataAnnotation) + => CanSetValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); +} diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index 505884263cc..e7c35712ca5 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -4317,12 +4317,11 @@ public virtual bool CanSetServiceOnlyConstructorBinding( discriminatorProperty = null; } - return (InternalPropertyBuilder?)Metadata.GetRootType().Builder.Property( - type ?? discriminatorProperty?.ClrType ?? DefaultDiscriminatorType, - name ?? discriminatorProperty?.Name ?? DefaultDiscriminatorName, - typeConfigurationSource: type != null ? configurationSource : null, - configurationSource) - ?.AfterSave(PropertySaveBehavior.Throw, ConfigurationSource.Convention); + return Metadata.GetRootType().Builder.Property( + type ?? discriminatorProperty?.ClrType ?? DefaultDiscriminatorType, + name ?? discriminatorProperty?.Name ?? DefaultDiscriminatorName, + typeConfigurationSource: type != null ? configurationSource : null, + configurationSource)?.AfterSave(PropertySaveBehavior.Throw, ConfigurationSource.Convention); } private DiscriminatorBuilder? DiscriminatorBuilder( diff --git a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs index 882374d2ac4..9339a4940c6 100644 --- a/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalPropertyBuilder.cs @@ -31,7 +31,8 @@ public InternalPropertyBuilder(Property property, InternalModelBuilder modelBuil /// 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. /// - protected override IConventionPropertyBuilder This => this; + protected override IConventionPropertyBuilder This + => this; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -385,7 +386,8 @@ public virtual bool CanSetAfterSave(PropertySaveBehavior? behavior, Configuratio /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalPropertyBuilder? HasValueGenerator( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? valueGeneratorType, ConfigurationSource configurationSource) { if (valueGeneratorType == null) @@ -443,7 +445,8 @@ public virtual bool CanSetAfterSave(PropertySaveBehavior? behavior, Configuratio /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalPropertyBuilder? HasValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? factory, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? factory, ConfigurationSource configurationSource) { if (CanSetValueGeneratorFactory(factory, configurationSource)) @@ -476,7 +479,8 @@ public virtual bool CanSetValueGenerator( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool CanSetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? factory, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? factory, ConfigurationSource? configurationSource) => configurationSource.Overrides(Metadata.GetValueGeneratorFactoryConfigurationSource()) || (Metadata[CoreAnnotationNames.ValueGeneratorFactory] == null @@ -554,11 +558,13 @@ public virtual bool CanSetConversion(Type? providerClrType, ConfigurationSource? /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalPropertyBuilder? HasConverter( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, ConfigurationSource configurationSource) { if (CanSetConverter(converterType, configurationSource)) { + Metadata.ElementType(false, configurationSource); Metadata.SetProviderClrType(null, configurationSource); Metadata.SetValueConverter(converterType, configurationSource); @@ -575,7 +581,8 @@ public virtual bool CanSetConversion(Type? providerClrType, ConfigurationSource? /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool CanSetConverter( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, ConfigurationSource? configurationSource) => configurationSource.Overrides(Metadata.GetValueConverterConfigurationSource()) || (Metadata[CoreAnnotationNames.ValueConverter] == null @@ -666,7 +673,8 @@ public virtual bool CanSetValueComparer(ValueComparer? comparer, ConfigurationSo /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalPropertyBuilder? HasValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, ConfigurationSource configurationSource) { if (CanSetValueComparer(comparerType, configurationSource)) @@ -734,7 +742,8 @@ public virtual bool CanSetProviderValueComparer(ValueComparer? comparer, Configu /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalPropertyBuilder? HasProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, ConfigurationSource configurationSource) { if (CanSetProviderValueComparer(comparerType, configurationSource)) @@ -754,12 +763,40 @@ public virtual bool CanSetProviderValueComparer(ValueComparer? comparer, Configu /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool CanSetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, ConfigurationSource? configurationSource) => configurationSource.Overrides(Metadata.GetProviderValueComparerConfigurationSource()) || (Metadata[CoreAnnotationNames.ProviderValueComparer] == null && (Type?)Metadata[CoreAnnotationNames.ProviderValueComparerType] == comparerType); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalElementTypeBuilder? ElementType(bool elementType, ConfigurationSource configurationSource) + { + if (CanSetElementType(elementType, configurationSource)) + { + Metadata.ElementType(elementType, configurationSource); + Metadata.SetValueConverter((Type?)null, configurationSource); + return new InternalElementTypeBuilder((ElementType)Metadata.GetElementType()!, ModelBuilder); + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 bool CanSetElementType(bool elementType, ConfigurationSource? configurationSource) + => configurationSource.Overrides(Metadata.GetElementTypeConfigurationSource()) + && (elementType != (Metadata.GetElementType() != null)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -868,6 +905,80 @@ public virtual bool CanSetProviderValueComparer( newPropertyBuilder.HasTypeMapping(Metadata.TypeMapping, oldTypeMappingConfigurationSource.Value); } + var oldElementType = (ElementType?)Metadata.GetElementType(); + if (oldElementType != null) + { + var newElementType = (ElementType?)newPropertyBuilder.Metadata.GetElementType(); + if (newElementType != null) + { + var newElementTypeBuilder = new InternalElementTypeBuilder(newElementType, ModelBuilder); + newElementTypeBuilder.MergeAnnotationsFrom(oldElementType); + + var oldElementNullableConfigurationSource = oldElementType.GetIsNullableConfigurationSource(); + if (oldElementNullableConfigurationSource.HasValue + && newElementTypeBuilder.CanSetIsRequired(!oldElementType.IsNullable, oldElementNullableConfigurationSource)) + { + newElementTypeBuilder.IsRequired(!oldElementType.IsNullable, oldElementNullableConfigurationSource.Value); + } + + var oldElementUnicodeConfigurationSource = oldElementType.GetIsUnicodeConfigurationSource(); + if (oldElementUnicodeConfigurationSource.HasValue + && newElementTypeBuilder.CanSetIsUnicode(oldElementType.IsNullable, oldElementUnicodeConfigurationSource)) + { + newElementTypeBuilder.IsUnicode(oldElementType.IsNullable, oldElementUnicodeConfigurationSource.Value); + } + + var oldElementProviderClrTypeConfigurationSource = oldElementType.GetProviderClrTypeConfigurationSource(); + if (oldElementProviderClrTypeConfigurationSource.HasValue + && newElementTypeBuilder.CanSetConversion(oldElementType.GetProviderClrType(), oldElementProviderClrTypeConfigurationSource)) + { + newElementTypeBuilder.HasConversion(oldElementType.GetProviderClrType(), oldElementProviderClrTypeConfigurationSource.Value); + } + + var oldElementConverterConfigurationSource = oldElementType.GetValueConverterConfigurationSource(); + if (oldElementConverterConfigurationSource.HasValue + && newElementTypeBuilder.CanSetConverter(oldElementType.GetValueConverter()?.GetType(), oldElementConverterConfigurationSource)) + { + newElementTypeBuilder.HasConverter(oldElementType.GetValueConverter()?.GetType(), oldElementConverterConfigurationSource.Value); + } + + var oldElementPrecisionConfigurationSource = oldElementType.GetPrecisionConfigurationSource(); + if (oldElementPrecisionConfigurationSource.HasValue + && newElementTypeBuilder.CanSetPrecision(oldElementType.GetPrecision(), oldElementPrecisionConfigurationSource)) + { + newElementTypeBuilder.HasPrecision(oldElementType.GetPrecision(), oldElementPrecisionConfigurationSource.Value); + } + + var oldElementScaleConfigurationSource = oldElementType.GetScaleConfigurationSource(); + if (oldElementScaleConfigurationSource.HasValue + && newElementTypeBuilder.CanSetScale(oldElementType.GetScale(), oldElementScaleConfigurationSource)) + { + newElementTypeBuilder.HasScale(oldElementType.GetScale(), oldElementScaleConfigurationSource.Value); + } + + var oldElementMaxLengthConfigurationSource = oldElementType.GetMaxLengthConfigurationSource(); + if (oldElementMaxLengthConfigurationSource.HasValue + && newElementTypeBuilder.CanSetMaxLength(oldElementType.GetMaxLength(), oldElementMaxLengthConfigurationSource)) + { + newElementTypeBuilder.HasMaxLength(oldElementType.GetMaxLength(), oldElementMaxLengthConfigurationSource.Value); + } + + var oldElementTypeMappingConfigurationSource = oldElementType.GetTypeMappingConfigurationSource(); + if (oldElementTypeMappingConfigurationSource.HasValue + && newElementTypeBuilder.CanSetTypeMapping(oldElementType.TypeMapping, oldElementTypeMappingConfigurationSource)) + { + newPropertyBuilder.HasTypeMapping(oldElementType.TypeMapping, oldElementTypeMappingConfigurationSource.Value); + } + + var oldElementComparerConfigurationSource = oldElementType.GetValueComparerConfigurationSource(); + if (oldElementComparerConfigurationSource.HasValue + && newElementTypeBuilder.CanSetValueComparer(oldElementType.GetValueComparer(), oldElementComparerConfigurationSource)) + { + newElementTypeBuilder.HasValueComparer(oldElementType.GetValueComparer(), oldElementComparerConfigurationSource.Value); + } + } + } + return newPropertyBuilder; } @@ -902,10 +1013,15 @@ IConventionProperty IConventionPropertyBuilder.Metadata /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasAnnotation(string name, object? value, bool fromDataAnnotation) + IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasAnnotation( + string name, + object? value, + bool fromDataAnnotation) => base.HasAnnotation( - name, value, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - == null ? null : this; + name, value, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + == null + ? null + : this; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -914,10 +1030,15 @@ IConventionProperty IConventionPropertyBuilder.Metadata /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasNonNullAnnotation(string name, object? value, bool fromDataAnnotation) + IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasNonNullAnnotation( + string name, + object? value, + bool fromDataAnnotation) => base.HasNonNullAnnotation( - name, value, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - == null ? null : this; + name, value, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + == null + ? null + : this; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -926,10 +1047,14 @@ IConventionProperty IConventionPropertyBuilder.Metadata /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasNoAnnotation(string name, bool fromDataAnnotation) + IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasNoAnnotation( + string name, + bool fromDataAnnotation) => base.HasNoAnnotation( - name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - == null ? null : this; + name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + == null + ? null + : this; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1012,7 +1137,9 @@ bool IConventionPropertyBuilder.CanSetSentinel(object? sentinel, bool fromDataAn /// 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. /// - IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasField(string? fieldName, bool fromDataAnnotation) + IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasField( + string? fieldName, + bool fromDataAnnotation) => HasField(fieldName, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// @@ -1021,7 +1148,9 @@ bool IConventionPropertyBuilder.CanSetSentinel(object? sentinel, bool fromDataAn /// 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. /// - IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasField(FieldInfo? fieldInfo, bool fromDataAnnotation) + IConventionPropertyBuilder? IConventionPropertyBaseBuilder.HasField( + FieldInfo? fieldInfo, + bool fromDataAnnotation) => HasField(fieldInfo, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// @@ -1060,7 +1189,9 @@ bool IConventionPropertyBaseBuilder.CanSetField(Fiel /// 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. /// - bool IConventionPropertyBaseBuilder.CanSetPropertyAccessMode(PropertyAccessMode? propertyAccessMode, bool fromDataAnnotation) + bool IConventionPropertyBaseBuilder.CanSetPropertyAccessMode( + PropertyAccessMode? propertyAccessMode, + bool fromDataAnnotation) => CanSetPropertyAccessMode( propertyAccessMode, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1179,7 +1310,8 @@ bool IConventionPropertyBuilder.CanSetAfterSave(PropertySaveBehavior? behavior, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// IConventionPropertyBuilder? IConventionPropertyBuilder.HasValueGenerator( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? valueGeneratorType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? valueGeneratorType, bool fromDataAnnotation) => HasValueGenerator( valueGeneratorType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1211,7 +1343,8 @@ bool IConventionPropertyBuilder.CanSetValueGenerator(Func IConventionPropertyBuilder? IConventionPropertyBuilder.HasValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactoryType, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactoryType, bool fromDataAnnotation) => HasValueGeneratorFactory( valueGeneratorFactoryType, @@ -1224,7 +1357,8 @@ bool IConventionPropertyBuilder.CanSetValueGenerator(Func bool IConventionPropertyBuilder.CanSetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactoryType, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactoryType, bool fromDataAnnotation) => CanSetValueGeneratorFactory( valueGeneratorFactoryType, @@ -1255,7 +1389,8 @@ bool IConventionPropertyBuilder.CanSetConversion(ValueConverter? converter, bool /// doing so can result in application failures when updating to a new Entity Framework Core release. /// IConventionPropertyBuilder? IConventionPropertyBuilder.HasConverter( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, bool fromDataAnnotation) => HasConverter(converterType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1266,7 +1401,8 @@ bool IConventionPropertyBuilder.CanSetConversion(ValueConverter? converter, bool /// doing so can result in application failures when updating to a new Entity Framework Core release. /// bool IConventionPropertyBuilder.CanSetConverter( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, bool fromDataAnnotation) => CanSetConverter(converterType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1321,7 +1457,8 @@ bool IConventionPropertyBuilder.CanSetValueComparer(ValueComparer? comparer, boo /// doing so can result in application failures when updating to a new Entity Framework Core release. /// IConventionPropertyBuilder? IConventionPropertyBuilder.HasValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation) => HasValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1332,7 +1469,8 @@ bool IConventionPropertyBuilder.CanSetValueComparer(ValueComparer? comparer, boo /// doing so can result in application failures when updating to a new Entity Framework Core release. /// bool IConventionPropertyBuilder.CanSetValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation) => CanSetValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1361,7 +1499,8 @@ bool IConventionPropertyBuilder.CanSetProviderValueComparer(ValueComparer? compa /// doing so can result in application failures when updating to a new Entity Framework Core release. /// IConventionPropertyBuilder? IConventionPropertyBuilder.HasProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation) => HasProviderValueComparer(comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -1372,8 +1511,27 @@ bool IConventionPropertyBuilder.CanSetProviderValueComparer(ValueComparer? compa /// doing so can result in application failures when updating to a new Entity Framework Core release. /// bool IConventionPropertyBuilder.CanSetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation) => CanSetProviderValueComparer( comparerType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + IConventionElementTypeBuilder? IConventionPropertyBuilder.ElementType(bool elementType, bool fromDataAnnotation) + => ElementType(elementType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + bool IConventionPropertyBuilder.CanSetElementType(bool elementType, bool fromDataAnnotation) + => CanSetElementType(elementType, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index 8dd00df5d6b..aa4a3b435e3 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -272,6 +272,72 @@ public static bool IsCompatible(MemberInfo? newMemberInfo, PropertyBase existing : Metadata.FindProperty(propertyName)?.Builder; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalPropertyBuilder? PrimitiveCollection( + Type? propertyType, + string propertyName, + ConfigurationSource? configurationSource) + => PrimitiveCollection(propertyType, propertyName, typeConfigurationSource: configurationSource, configurationSource: configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalPropertyBuilder? PrimitiveCollection( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type? propertyType, + string propertyName, + ConfigurationSource? typeConfigurationSource, + ConfigurationSource? configurationSource) + => PrimitiveCollection( + propertyType, propertyName, memberInfo: null, + typeConfigurationSource, + configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalPropertyBuilder? PrimitiveCollection(string propertyName, ConfigurationSource? configurationSource) + => PrimitiveCollection(propertyType: null, propertyName, memberInfo: null, typeConfigurationSource: null, configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalPropertyBuilder? PrimitiveCollection(MemberInfo memberInfo, ConfigurationSource? configurationSource) + => PrimitiveCollection(memberInfo.GetMemberType(), memberInfo.GetSimpleMemberName(), memberInfo, configurationSource, configurationSource); + + /// + /// 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. + /// + protected virtual InternalPropertyBuilder? PrimitiveCollection( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type? propertyType, + string propertyName, + MemberInfo? memberInfo, + ConfigurationSource? typeConfigurationSource, + ConfigurationSource? configurationSource) + { + var builder = Property(propertyType, propertyName, memberInfo, typeConfigurationSource, configurationSource); + + builder?.ElementType(true, configurationSource!.Value); + + return builder; + } + /// /// 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 @@ -559,6 +625,7 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( property.GetIdentifyingMemberInfo(), typeConfigurationSource.Overrides(ConfigurationSource.DataAnnotation) ? typeConfigurationSource : null, configurationSource); + if (builder == null) { return null; diff --git a/src/EFCore/Metadata/Internal/MemberClassifier.cs b/src/EFCore/Metadata/Internal/MemberClassifier.cs index 4591b604af6..e8253b14625 100644 --- a/src/EFCore/Metadata/Internal/MemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/MemberClassifier.cs @@ -167,8 +167,9 @@ private bool IsCandidateNavigationPropertyType( /// 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 bool IsCandidatePrimitiveProperty(MemberInfo memberInfo, IConventionModel model) + public virtual bool IsCandidatePrimitiveProperty(MemberInfo memberInfo, IConventionModel model, out CoreTypeMapping? typeMapping) { + typeMapping = null; if (!memberInfo.IsCandidateProperty()) { return false; @@ -176,7 +177,7 @@ public virtual bool IsCandidatePrimitiveProperty(MemberInfo memberInfo, IConvent var configurationType = ((Model)model).Configuration?.GetConfigurationType(memberInfo.GetMemberType()); return configurationType == TypeConfigurationType.Property - || (configurationType == null && _typeMappingSource.FindMapping(memberInfo) != null); + || (configurationType == null && (typeMapping = _typeMappingSource.FindMapping(memberInfo)) != null); } /// @@ -185,8 +186,11 @@ public virtual bool IsCandidatePrimitiveProperty(MemberInfo memberInfo, IConvent /// 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 bool IsCandidateComplexProperty(MemberInfo memberInfo, IConventionModel model, - out Type? elementType, out bool explicitlyConfigured) + public virtual bool IsCandidateComplexProperty( + MemberInfo memberInfo, + IConventionModel model, + out Type? elementType, + out bool explicitlyConfigured) { explicitlyConfigured = false; elementType = null; diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 70258360bb7..2d79b84fe61 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -998,7 +998,7 @@ public virtual CoreTypeMapping? TypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ValueComparer? GetValueComparer() - => (GetValueComparer(null) ?? TypeMapping?.Comparer).ToNullableComparer(this); + => (GetValueComparer(null) ?? TypeMapping?.Comparer).ToNullableComparer(ClrType); private ValueComparer? GetValueComparer(HashSet? checkedProperties) { @@ -1043,7 +1043,7 @@ public virtual CoreTypeMapping? TypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ValueComparer? GetKeyValueComparer() - => (GetValueComparer(null) ?? TypeMapping?.KeyComparer).ToNullableComparer(this); + => (GetValueComparer(null) ?? TypeMapping?.KeyComparer).ToNullableComparer(ClrType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1166,34 +1166,7 @@ public virtual CoreTypeMapping? TypeMapping /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual JsonValueReaderWriter? GetJsonValueReaderWriter() - { - return TryCreateReader((Type?)this[CoreAnnotationNames.JsonValueReaderWriterType]); - - static JsonValueReaderWriter? TryCreateReader(Type? readerWriterType) - { - if (readerWriterType != null) - { - var instanceProperty = readerWriterType.GetAnyProperty("Instance"); - try - { - return instanceProperty != null - && instanceProperty.IsStatic() - && instanceProperty.GetMethod?.IsPublic == true - && readerWriterType.IsAssignableFrom(instanceProperty.PropertyType) - ? (JsonValueReaderWriter?)instanceProperty.GetValue(null) - : (JsonValueReaderWriter?)Activator.CreateInstance(readerWriterType); - } - catch (Exception e) - { - throw new InvalidOperationException( - CoreStrings.CannotCreateJsonValueReaderWriter( - readerWriterType.ShortDisplayName()), e); - } - } - - return null; - } - } + => JsonValueReaderWriter.CreateFromType((Type?)this[CoreAnnotationNames.JsonValueReaderWriterType]); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -1226,6 +1199,69 @@ public virtual CoreTypeMapping? TypeMapping public virtual ConfigurationSource? GetJsonValueReaderWriterTypeConfigurationSource() => FindAnnotation(CoreAnnotationNames.JsonValueReaderWriterType)?.GetConfigurationSource(); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IElementType? GetElementType() + => (IElementType?)this[CoreAnnotationNames.ElementType]; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IElementType? ElementType( + bool elementType, + ConfigurationSource configurationSource) + { + var existingElementType = GetElementType(); + if (existingElementType == null + && elementType) + { + var elementClrType = ClrType.TryGetElementType(typeof(IEnumerable<>)); + if (elementClrType == null) + { + throw new InvalidOperationException(CoreStrings.NotCollection(ClrType.ShortDisplayName(), Name)); + } + var newElementType = new ElementType(elementClrType, this, configurationSource); + SetAnnotation(CoreAnnotationNames.ElementType, newElementType, configurationSource); + OnElementTypeSet(newElementType, null); + return newElementType; + } + + if (existingElementType != null && !elementType) + { + ((ElementType)existingElementType).SetRemovedFromModel(); + RemoveAnnotation(CoreAnnotationNames.ElementType); + OnElementTypeSet(null, existingElementType); + return null; + } + + return existingElementType; + } + + /// + /// 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. + /// + protected virtual IElementType? OnElementTypeSet(IElementType? newElementType, IElementType? oldElementType) + => DeclaringType.Model.ConventionDispatcher.OnPropertyElementTypeChanged(Builder, newElementType, oldElementType); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetElementTypeConfigurationSource() + => FindAnnotation(CoreAnnotationNames.ElementType)?.GetConfigurationSource(); + /// /// 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 @@ -1971,6 +2007,18 @@ void IMutableProperty.SetJsonValueReaderWriterType(Type? readerWriterType) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - JsonValueReaderWriter? IReadOnlyProperty.GetJsonValueReaderWriter() - => GetJsonValueReaderWriter(); + IElementType? IConventionProperty.ElementType(bool elementType, bool fromDataAnnotation) + => ElementType( + elementType, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + /// 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. + /// + [DebuggerStepThrough] + void IMutableProperty.ElementType(bool elementType) + => ElementType(elementType, ConfigurationSource.Explicit); } diff --git a/src/EFCore/Metadata/RuntimeElementType.cs b/src/EFCore/Metadata/RuntimeElementType.cs new file mode 100644 index 00000000000..80ba1155301 --- /dev/null +++ b/src/EFCore/Metadata/RuntimeElementType.cs @@ -0,0 +1,230 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.Metadata; + +/// +/// Represents the elements of a collection property. +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public class RuntimeElementType : AnnotatableBase, IElementType +{ + private readonly bool _isNullable; + private readonly ValueConverter? _valueConverter; + private readonly ValueComparer? _valueComparer; + private readonly JsonValueReaderWriter? _jsonValueReaderWriter; + private readonly CoreTypeMapping? _typeMapping; + + /// + /// 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 RuntimeElementType( + Type clrType, + RuntimeProperty collectionProperty, + bool nullable, + int? maxLength, + bool? unicode, + int? precision, + int? scale, + Type? providerClrType, + ValueConverter? valueConverter, + ValueComparer? valueComparer, + JsonValueReaderWriter? jsonValueReaderWriter, + CoreTypeMapping? typeMapping) + { + CollectionProperty = collectionProperty; + ClrType = clrType; + _isNullable = nullable; + _valueConverter = valueConverter; + + if (maxLength != null) + { + SetAnnotation(CoreAnnotationNames.MaxLength, maxLength); + } + + if (unicode != null) + { + SetAnnotation(CoreAnnotationNames.Unicode, unicode); + } + + if (precision != null) + { + SetAnnotation(CoreAnnotationNames.Precision, precision); + } + + if (scale != null) + { + SetAnnotation(CoreAnnotationNames.Scale, scale); + } + + if (providerClrType != null) + { + SetAnnotation(CoreAnnotationNames.ProviderClrType, providerClrType); + } + + _typeMapping = typeMapping; + _valueComparer = valueComparer; + _jsonValueReaderWriter = jsonValueReaderWriter; + } + + /// + /// Gets the collection property for which this represents the element. + /// + public virtual IProperty CollectionProperty { get; } + + /// + public virtual Type ClrType { get; } + + /// + /// Gets a value indicating whether elements of the collection can be . + /// + public virtual bool IsNullable + => _isNullable; + + /// + /// Returns the type mapping for elements of the collection. + /// + /// The type mapping, or if none was found. + public virtual CoreTypeMapping? FindTypeMapping() + => _typeMapping; + + /// + /// Gets the maximum length of data that is allowed in elements of the collection. For example, if the element type is + /// a then this is the maximum number of characters. + /// + /// + /// The maximum length, -1 if the property has no maximum length, or if the maximum length hasn't been + /// set. + /// + [DebuggerStepThrough] + public virtual int? GetMaxLength() + => (int?)this[CoreAnnotationNames.MaxLength]; + + /// + /// Gets the precision of data that is allowed in elements of the collection. + /// For example, if the element type is a , then this is the maximum number of digits. + /// + /// The precision, or if none is defined. + [DebuggerStepThrough] + public virtual int? GetPrecision() + => (int?)this[CoreAnnotationNames.Precision]; + + /// + /// Gets the scale of data that is allowed in this elements of the collection. + /// For example, if the element type is a , then this is the maximum number of decimal places. + /// + /// The scale, or if none is defined. + [DebuggerStepThrough] + public virtual int? GetScale() + => (int?)this[CoreAnnotationNames.Scale]; + + /// + /// Gets a value indicating whether elements of the collection can persist Unicode characters. + /// + /// The Unicode setting, or if none is defined. + [DebuggerStepThrough] + public virtual bool? IsUnicode() + => (bool?)this[CoreAnnotationNames.Unicode]; + + /// + /// Gets the custom for this elements of the collection. + /// + /// The converter, or if none has been set. + [DebuggerStepThrough] + public virtual ValueConverter? GetValueConverter() + => _valueConverter; + + /// + /// Gets the custom for elements of the collection. + /// + /// The comparer, or if none has been set. + [DebuggerStepThrough] + public virtual ValueComparer? GetValueComparer() + => _valueComparer; + + /// + /// Gets the type that the elements of the collection will be converted to before being sent to the database provider. + /// + /// The provider type, or if none has been set. + public virtual Type? GetProviderClrType() + => (Type?)FindAnnotation(CoreAnnotationNames.ProviderClrType)?.Value; + + /// + public virtual JsonValueReaderWriter? GetJsonValueReaderWriter() + => _jsonValueReaderWriter; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual DebugView DebugView + => new( + () => ((IReadOnlyElementType)this).ToDebugString(), + () => ((IReadOnlyElementType)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override string ToString() + => ((IReadOnlyElementType)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); + + /// + IReadOnlyProperty IReadOnlyElementType.CollectionProperty + => CollectionProperty; + + /// + bool IReadOnlyElementType.IsNullable + { + [DebuggerStepThrough] + get => _isNullable; + } + + /// + [DebuggerStepThrough] + int? IReadOnlyElementType.GetMaxLength() + => (int?)this[CoreAnnotationNames.MaxLength]; + + /// + [DebuggerStepThrough] + bool? IReadOnlyElementType.IsUnicode() + => (bool?)this[CoreAnnotationNames.Unicode]; + + /// + [DebuggerStepThrough] + int? IReadOnlyElementType.GetPrecision() + => (int?)this[CoreAnnotationNames.Precision]; + + /// + [DebuggerStepThrough] + int? IReadOnlyElementType.GetScale() + => (int?)this[CoreAnnotationNames.Scale]; + + /// + [DebuggerStepThrough] + ValueConverter? IReadOnlyElementType.GetValueConverter() + => _valueConverter; + + /// + [DebuggerStepThrough] + Type? IReadOnlyElementType.GetProviderClrType() + => (Type?)this[CoreAnnotationNames.ProviderClrType]; + + /// + [DebuggerStepThrough] + CoreTypeMapping IReadOnlyElementType.FindTypeMapping() + => FindTypeMapping()!; +} diff --git a/src/EFCore/Metadata/RuntimeProperty.cs b/src/EFCore/Metadata/RuntimeProperty.cs index f8776f1671f..6bdcfcc03cc 100644 --- a/src/EFCore/Metadata/RuntimeProperty.cs +++ b/src/EFCore/Metadata/RuntimeProperty.cs @@ -1,8 +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.ChangeTracking.Internal; using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; @@ -109,6 +109,55 @@ public RuntimeProperty( _jsonValueReaderWriter = jsonValueReaderWriter; } + /// + /// Sets the element type for this property. + /// + /// The type of value the property will hold. + /// A value indicating whether this property can contain . + /// The maximum length of data that is allowed in this property. + /// A value indicating whether or not the property can persist Unicode characters. + /// The precision of data that is allowed in this property. + /// The scale of data that is allowed in this property. + /// + /// The type that the property value will be converted to before being sent to the database provider. + /// + /// The custom set for this property. + /// The for this property. + /// The for this property. + /// The for this property. + /// The newly created property. + public virtual RuntimeElementType SetElementType( + Type clrType, + bool nullable = false, + int? maxLength = null, + bool? unicode = null, + int? precision = null, + int? scale = null, + Type? providerPropertyType = null, + ValueConverter? valueConverter = null, + ValueComparer? valueComparer = null, + JsonValueReaderWriter? jsonValueReaderWriter = null, + CoreTypeMapping? typeMapping = null) + { + var elementType = new RuntimeElementType( + clrType, + this, + nullable, + maxLength, + unicode, + precision, + scale, + providerPropertyType, + valueConverter, + valueComparer, + jsonValueReaderWriter, + typeMapping); + + SetAnnotation(CoreAnnotationNames.ElementType, elementType); + + return elementType; + } + /// /// Gets the type of value that this property-like object holds. /// @@ -180,11 +229,11 @@ public virtual CoreTypeMapping TypeMapping private ValueComparer GetValueComparer() => (GetValueComparer(null) ?? TypeMapping.Comparer) - .ToNullableComparer(this)!; + .ToNullableComparer(ClrType)!; private ValueComparer GetKeyValueComparer() => (GetKeyValueComparer(null) ?? TypeMapping.KeyComparer) - .ToNullableComparer(this)!; + .ToNullableComparer(ClrType)!; private ValueComparer? GetValueComparer(HashSet? checkedProperties) { @@ -214,7 +263,7 @@ private ValueComparer GetKeyValueComparer() private ValueComparer? GetKeyValueComparer(HashSet? checkedProperties) { - if ( _keyValueComparer != null) + if (_keyValueComparer != null) { return _keyValueComparer; } @@ -249,6 +298,13 @@ public override object? Sentinel public virtual JsonValueReaderWriter? GetJsonValueReaderWriter() => _jsonValueReaderWriter; + /// + /// Gets the configuration for elements of the primitive collection represented by this property. + /// + /// The configuration for the elements. + public virtual IElementType? GetElementType() + => (IElementType?)this[CoreAnnotationNames.ElementType]; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/RuntimeTypeBase.cs b/src/EFCore/Metadata/RuntimeTypeBase.cs index 8c194549d2e..b54adbc18a2 100644 --- a/src/EFCore/Metadata/RuntimeTypeBase.cs +++ b/src/EFCore/Metadata/RuntimeTypeBase.cs @@ -60,6 +60,7 @@ public RuntimeTypeBase( _baseType = baseType; baseType._directlyDerivedTypes.Add(this); } + _changeTrackingStrategy = changeTrackingStrategy; _indexerPropertyInfo = indexerPropertyInfo; _isPropertyBag = propertyBag; @@ -84,13 +85,15 @@ public RuntimeTypeBase( /// Gets the base type of this type. Returns if this is not a /// derived type in an inheritance hierarchy. /// - public virtual RuntimeTypeBase? BaseType => _baseType; + public virtual RuntimeTypeBase? BaseType + => _baseType; /// /// Gets all types in the model that directly derive from this type. /// /// The derived types. - public virtual SortedSet DirectlyDerivedTypes => _directlyDerivedTypes; + public virtual SortedSet DirectlyDerivedTypes + => _directlyDerivedTypes; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -288,7 +291,7 @@ private IEnumerable GetDerivedProperties() } /// - /// Gets the properties with the given name on this type, base types or derived types. + /// Gets the properties with the given name on this type, base types or derived types. /// /// Type properties. public virtual IEnumerable FindPropertiesInHierarchy(string propertyName) @@ -325,7 +328,8 @@ protected virtual IEnumerable GetProperties() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - protected virtual SortedDictionary Properties => _properties; + protected virtual SortedDictionary Properties + => _properties; /// [DebuggerStepThrough] @@ -433,7 +437,7 @@ public virtual IEnumerable GetComplexProperties() : _complexProperties.Values; /// - /// Gets the complex properties with the given name on this type, base types or derived types. + /// Gets the complex properties with the given name on this type, base types or derived types. /// /// Type complex properties. public virtual IEnumerable FindComplexPropertiesInHierarchy(string propertyName) @@ -448,7 +452,7 @@ private IEnumerable FindDerivedComplexProperties(string return _directlyDerivedTypes.Count == 0 ? Enumerable.Empty() : (IEnumerable)GetDerivedTypes() - .Select(et => et.FindDeclaredComplexProperty(propertyName)).Where(p => p != null); + .Select(et => et.FindDeclaredComplexProperty(propertyName)).Where(p => p != null); } /// @@ -491,9 +495,7 @@ private IEnumerable FindDerivedComplexProperties(string /// [EntityFrameworkInternal] public virtual void SetOriginalValuesFactory(Func factory) - { - _originalValuesFactory = factory; - } + => _originalValuesFactory = factory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -503,9 +505,7 @@ public virtual void SetOriginalValuesFactory(Func fac /// [EntityFrameworkInternal] public virtual void SetStoreGeneratedValuesFactory(Func factory) - { - _storeGeneratedValuesFactory = factory; - } + => _storeGeneratedValuesFactory = factory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -515,9 +515,7 @@ public virtual void SetStoreGeneratedValuesFactory(Func factory) /// [EntityFrameworkInternal] public virtual void SetTemporaryValuesFactory(Func factory) - { - _temporaryValuesFactory = factory; - } + => _temporaryValuesFactory = factory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -527,9 +525,7 @@ public virtual void SetTemporaryValuesFactory(Func fa /// [EntityFrameworkInternal] public virtual void SetShadowValuesFactory(Func factory) - { - _shadowValuesFactory = factory; - } + => _shadowValuesFactory = factory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -537,12 +533,9 @@ public virtual void SetShadowValuesFactory(Func factory) /// 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 virtual void SetEmptyShadowValuesFactory(Func factory) - { - _emptyShadowValuesFactory = factory; - } + => _emptyShadowValuesFactory = factory; /// /// Gets or sets the for the preferred constructor. diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 337ccf8e321..70ddeda2d8d 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -226,6 +226,14 @@ public static string CannotBeNullable(object? property, object? entityType, obje GetString("CannotBeNullable", "0_property", "1_entityType", nameof(propertyType)), property, entityType, propertyType); + /// + /// The element type of property '{entityType}.{property}' cannot be marked as nullable/optional because the type of the element is '{elementType}' which is not a nullable type. Any element type can be marked as non-nullable/required, but only elements of nullable types can be marked as nullable/optional. + /// + public static string CannotBeNullableElement(object? entityType, object? property, object? elementType) + => string.Format( + GetString("CannotBeNullableElement", "entityType", "property", "elementType"), + property, entityType); + /// /// The property '{1_entityType}.{0_property}' cannot be marked as nullable/optional because the property is a part of a key. Any property can be marked as non-nullable/required, but only properties of nullable types and which are not part of a key can be marked as nullable/optional. /// @@ -448,6 +456,14 @@ public static string ComparerPropertyMismatch(object? type, object? entityType, GetString("ComparerPropertyMismatch", nameof(type), nameof(entityType), nameof(propertyName), nameof(propertyType)), type, entityType, propertyName, propertyType); + /// + /// The comparer for element type '{type}' cannot be used for '{entityType}.{propertyName}' because its element type is '{elementType}'. + /// + public static string ComparerPropertyMismatchElement(object? type, object? entityType, object? propertyName, object? elementType) + => string.Format( + GetString("ComparerPropertyMismatchElement", nameof(type), nameof(entityType), nameof(propertyName), nameof(elementType)), + type, entityType, propertyName, elementType); + /// /// The type mapping used is incompatible with a compiled model. The mapping type must have a 'public static readonly {typeMapping} {typeMapping}.Default' property. /// @@ -634,6 +650,14 @@ public static string ConverterPropertyMismatch(object? converterType, object? en GetString("ConverterPropertyMismatch", nameof(converterType), nameof(entityType), nameof(propertyName), nameof(propertyType)), converterType, entityType, propertyName, propertyType); + /// + /// Converter for element type '{converterType}' cannot be used for '{entityType}.{propertyName}' because its element type is '{elementType}'. + /// + public static string ConverterPropertyMismatchElement(object? converterType, object? entityType, object? propertyName, object? elementType) + => string.Format( + GetString("ConverterPropertyMismatchElement", nameof(converterType), nameof(entityType), nameof(propertyName), nameof(elementType)), + converterType, entityType, propertyName, elementType); + /// /// Cannot compose converter from '{typeOneIn}' to '{typeOneOut}' with converter from '{typeTwoIn}' to '{typeTwoOut}' because the output type of the first converter doesn't match the input type of the second converter. /// @@ -2065,6 +2089,14 @@ public static string NotAnEFService(object? service) GetString("NotAnEFService", nameof(service)), service); + /// + /// The property '{entityType}.{property}' cannot be mapped as a collection since it does not implement 'IEnumerable<T>'. + /// + public static string NotCollection(object? entityType, object? property) + => string.Format( + GetString("NotCollection", nameof(entityType), nameof(property)), + entityType, property); + /// /// The database provider attempted to register an implementation of the '{service}' service. This is a service defined by Entity Framework and as such must not be registered using the 'TryAddProviderSpecificServices' method. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 0454c7f73d2..e44b43b1e73 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -192,6 +192,9 @@ The property '{1_entityType}.{0_property}' cannot be marked as nullable/optional because the type of the property is '{propertyType}' which is not a nullable type. Any property can be marked as non-nullable/required, but only properties of nullable types can be marked as nullable/optional. + + The element type of property '{entityType}.{property}' cannot be marked as nullable/optional because the type of the element is '{elementType}' which is not a nullable type. Any element type can be marked as non-nullable/required, but only elements of nullable types can be marked as nullable/optional. + The property '{1_entityType}.{0_property}' cannot be marked as nullable/optional because the property is a part of a key. Any property can be marked as non-nullable/required, but only properties of nullable types and which are not part of a key can be marked as nullable/optional. @@ -276,6 +279,9 @@ The comparer for type '{type}' cannot be used for '{entityType}.{propertyName}' because its type is '{propertyType}'. + + The comparer for element type '{type}' cannot be used for '{entityType}.{propertyName}' because its element type is '{elementType}'. + The type mapping used is incompatible with a compiled model. The mapping type must have a 'public static readonly {typeMapping} {typeMapping}.Default' property. @@ -348,6 +354,9 @@ Converter for model type '{converterType}' cannot be used for '{entityType}.{propertyName}' because its type is '{propertyType}'. + + Converter for element type '{converterType}' cannot be used for '{entityType}.{propertyName}' because its element type is '{elementType}'. + Cannot compose converter from '{typeOneIn}' to '{typeOneOut}' with converter from '{typeTwoIn}' to '{typeTwoOut}' because the output type of the first converter doesn't match the input type of the second converter. @@ -1206,6 +1215,9 @@ The database provider attempted to register an implementation of the '{service}' service. This is not a service defined by Entity Framework and as such must be registered as a provider-specific service using the 'TryAddProviderSpecificServices' method. + + The property '{entityType}.{property}' cannot be mapped as a collection since it does not implement 'IEnumerable<T>'. + The database provider attempted to register an implementation of the '{service}' service. This is a service defined by Entity Framework and as such must not be registered using the 'TryAddProviderSpecificServices' method. diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs index 9cb01a0cffe..85d32f3c09d 100644 --- a/src/EFCore/Storage/CoreTypeMapping.cs +++ b/src/EFCore/Storage/CoreTypeMapping.cs @@ -110,8 +110,8 @@ public CoreTypeMappingParameters( /// converter composed with any existing converter and set on the new parameter object. /// /// The converter. - /// The element mapping, or for non-collection mappings. - /// The JSON reader/writer, or to leave unchanged. + /// The element mapping, or for non-collection mappings. + /// The JSON reader/writer, or to leave unchanged. /// The new parameter object. public CoreTypeMappingParameters WithComposedConverter( ValueConverter? converter, @@ -137,39 +137,6 @@ public CoreTypeMappingParameters WithComposedConverter( JsonValueReaderWriter, converter)! : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel))); } - - /// - /// Creates a new parameter object with the given - /// element type mapping. - /// - /// The element type mapping. - /// The new parameter object. - public CoreTypeMappingParameters WithElementTypeMapping(CoreTypeMapping elementMapping) - => new( - ClrType, - Converter, - Comparer, - KeyComparer, - ProviderValueComparer, - ValueGeneratorFactory, - elementMapping, - JsonValueReaderWriter); - - /// - /// Creates a new parameter object with the given JSON reader/writer. - /// - /// The element type mapping. - /// The new parameter object. - public CoreTypeMappingParameters WithJsonValueReaderWriter(JsonValueReaderWriter jsonValueReaderWriter) - => new( - ClrType, - Converter, - Comparer, - KeyComparer, - ProviderValueComparer, - ValueGeneratorFactory, - ElementTypeMapping, - jsonValueReaderWriter); } private ValueComparer? _comparer; @@ -296,11 +263,13 @@ public virtual ValueComparer ProviderValueComparer /// added. /// /// The converter to use. - /// The element mapping, or for non-collection mappings. - /// The JSON reader/writer, or to leave unchanged. + /// The element mapping, or for non-collection mappings. + /// The JSON reader/writer, or to leave unchanged. /// A new type mapping public abstract CoreTypeMapping Clone( - ValueConverter? converter, CoreTypeMapping? elementMapping = null, JsonValueReaderWriter? jsonValueReaderWriter = null); + ValueConverter? converter, + CoreTypeMapping? elementMapping = null, + JsonValueReaderWriter? jsonValueReaderWriter = null); /// /// Clones the type mapping to update any parameter if needed. diff --git a/src/EFCore/Storage/ITypeMappingSource.cs b/src/EFCore/Storage/ITypeMappingSource.cs index fabfda2a008..944b3880cc6 100644 --- a/src/EFCore/Storage/ITypeMappingSource.cs +++ b/src/EFCore/Storage/ITypeMappingSource.cs @@ -36,6 +36,13 @@ public interface ITypeMappingSource /// The type mapping, or if none was found. CoreTypeMapping? FindMapping(IProperty property); + /// + /// Finds the type mapping for a given . + /// + /// The collection element. + /// The type mapping, or if none was found. + CoreTypeMapping? FindMapping(IElementType elementType); + /// /// Finds the type mapping for a given representing /// a field or a property of a CLR type. diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs index c9b54861e5e..15dc20343eb 100644 --- a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs +++ b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs @@ -64,7 +64,7 @@ internal JsonValueReaderWriter() public virtual object FromJsonString(string json, object? existingObject = null) { var readerManager = new Utf8JsonReaderManager(new JsonReaderData(Encoding.UTF8.GetBytes(json))); - return FromJson(ref readerManager); + return FromJson(ref readerManager, existingObject); } /// @@ -84,4 +84,38 @@ public virtual string ToJsonString(object value) return Encoding.UTF8.GetString(buffer); } + + /// + /// Creates a instance of the given type, using the Instance + /// property to get th singleton instance if possible. + /// + /// The type, which must inherit from . + /// The reader/writer instance./ + /// if the type does not represent a + /// that can be instantiated. + public static JsonValueReaderWriter? CreateFromType(Type? readerWriterType) + { + if (readerWriterType != null) + { + var instanceProperty = readerWriterType.GetAnyProperty("Instance"); + try + { + return instanceProperty != null + && instanceProperty.IsStatic() + && instanceProperty.GetMethod?.IsPublic == true + && readerWriterType.IsAssignableFrom(instanceProperty.PropertyType) + ? (JsonValueReaderWriter?)instanceProperty.GetValue(null) + : (JsonValueReaderWriter?)Activator.CreateInstance(readerWriterType); + } + catch (Exception e) + { + throw new InvalidOperationException( + CoreStrings.CannotCreateJsonValueReaderWriter( + readerWriterType.ShortDisplayName()), e); + } + } + + return null; + } + } diff --git a/src/EFCore/Storage/TypeMappingInfo.cs b/src/EFCore/Storage/TypeMappingInfo.cs index 32cef9c53b6..7407af2d4b5 100644 --- a/src/EFCore/Storage/TypeMappingInfo.cs +++ b/src/EFCore/Storage/TypeMappingInfo.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -23,6 +24,87 @@ public TypeMappingInfo(IProperty property) { } + /// + /// Creates a new instance of . + /// + /// The collection element for the property for which mapping is needed. + /// + /// Specifies Unicode or ANSI for the mapping or for the default. + /// + /// + /// Specifies a size for the mapping, in case one isn't found at the core level, or for the default. + /// + /// + /// Specifies a precision for the mapping, in case one isn't found at the core level, or for the default. + /// + /// + /// Specifies a scale for the mapping, in case one isn't found at the core level, or for the default. + /// + public TypeMappingInfo( + IElementType elementType, + bool? fallbackUnicode = null, + int? fallbackSize = null, + int? fallbackPrecision = null, + int? fallbackScale = null) + { + ValueConverter? customConverter = null; + if (customConverter == null) + { + var converter = elementType.GetValueConverter(); + if (converter != null) + { + customConverter = converter; + } + } + + if (fallbackSize == null) + { + var maxLength = elementType.GetMaxLength(); + if (maxLength != null) + { + fallbackSize = maxLength; + } + } + + if (fallbackPrecision == null) + { + var precisionFromProperty = elementType.GetPrecision(); + if (precisionFromProperty != null) + { + fallbackPrecision = precisionFromProperty; + } + } + + if (fallbackScale == null) + { + var scaleFromProperty = elementType.GetScale(); + if (scaleFromProperty != null) + { + fallbackScale = scaleFromProperty; + } + } + + if (fallbackUnicode == null) + { + var unicode = elementType.IsUnicode(); + if (unicode != null) + { + fallbackUnicode = unicode; + } + } + + var mappingHints = customConverter?.MappingHints; + + IsKeyOrIndex = false; + Size = fallbackSize ?? mappingHints?.Size; + IsUnicode = fallbackUnicode ?? mappingHints?.IsUnicode; + IsRowVersion = false; + ClrType = (customConverter?.ProviderClrType ?? elementType.ClrType).UnwrapNullableType(); + Scale = fallbackScale ?? mappingHints?.Scale; + Precision = fallbackPrecision ?? mappingHints?.Precision; + JsonValueReaderWriter = JsonValueReaderWriter.CreateFromType((Type?)elementType[CoreAnnotationNames.JsonValueReaderWriterType]); + } + /// /// Creates a new instance of . /// @@ -70,19 +152,19 @@ public TypeMappingInfo( if (fallbackPrecision == null) { - var precisionFromProperty = principal.GetPrecision(); - if (precisionFromProperty != null) + var precision = principal.GetPrecision(); + if (precision != null) { - fallbackPrecision = precisionFromProperty; + fallbackPrecision = precision; } } if (fallbackScale == null) { - var scaleFromProperty = principal.GetScale(); - if (scaleFromProperty != null) + var scale = principal.GetScale(); + if (scale != null) { - fallbackScale = scaleFromProperty; + fallbackScale = scale; } } @@ -106,7 +188,6 @@ public TypeMappingInfo( ClrType = (customConverter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); Scale = fallbackScale ?? mappingHints?.Scale; Precision = fallbackPrecision ?? mappingHints?.Precision; - ElementTypeMapping = null; // TODO: set from property JsonValueReaderWriter = property.GetJsonValueReaderWriter(); } @@ -190,28 +271,6 @@ public TypeMappingInfo( ClrType = converter.ProviderClrType.UnwrapNullableType(); - ElementTypeMapping = source.ElementTypeMapping; - JsonValueReaderWriter = source.JsonValueReaderWriter; - } - - /// - /// Creates a new instance of with the given . for collection - /// elements. - /// - /// The source info. - /// The element mapping to use. - public TypeMappingInfo( - TypeMappingInfo source, - CoreTypeMapping elementMapping) - { - IsRowVersion = source.IsRowVersion; - IsKeyOrIndex = source.IsKeyOrIndex; - Size = source.Size; - IsUnicode = source.IsUnicode; - Scale = source.Scale; - Precision = source.Precision; - ClrType = source.ClrType; - ElementTypeMapping = elementMapping; JsonValueReaderWriter = source.JsonValueReaderWriter; } @@ -223,14 +282,6 @@ public TypeMappingInfo( public TypeMappingInfo WithConverter(in ValueConverterInfo converterInfo) => new(this, converterInfo); - /// - /// Returns a new with the given converter applied. - /// - /// The element mapping to use. - /// The new mapping info. - public TypeMappingInfo WithElementTypeMapping(in CoreTypeMapping elementMapping) - => new(this, elementMapping); - /// /// Indicates whether or not the mapping is part of a key or index. /// @@ -267,11 +318,6 @@ public TypeMappingInfo WithElementTypeMapping(in CoreTypeMapping elementMapping) /// public Type? ClrType { get; init; } - /// - /// The element type mapping, if the mapping is for a collection of primitives, or otherwise. - /// - public CoreTypeMapping? ElementTypeMapping { get; init; } - /// /// The JSON reader/writer, if one has been provided, or otherwise. /// diff --git a/src/EFCore/Storage/TypeMappingSource.cs b/src/EFCore/Storage/TypeMappingSource.cs index 45d52c12c4f..cd06b5e562e 100644 --- a/src/EFCore/Storage/TypeMappingSource.cs +++ b/src/EFCore/Storage/TypeMappingSource.cs @@ -28,7 +28,8 @@ namespace Microsoft.EntityFrameworkCore.Storage; /// public abstract class TypeMappingSource : TypeMappingSourceBase { - private readonly ConcurrentDictionary<(TypeMappingInfo, Type?, ValueConverter?), CoreTypeMapping?> _explicitMappings = new(); + private readonly ConcurrentDictionary<(TypeMappingInfo, Type?, ValueConverter?, CoreTypeMapping?), CoreTypeMapping?> + _explicitMappings = new(); /// /// Initializes a new instance of this class. @@ -45,6 +46,7 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) { Type? providerClrType = null; ValueConverter? customConverter = null; + CoreTypeMapping? elementMapping = null; if (principals != null) { for (var i = 0; i < principals.Count; i++) @@ -67,10 +69,16 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) customConverter = converter; } } + + var element = principal.GetElementType(); + if (element != null) + { + elementMapping = FindMapping(element); + } } } - var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter, elementMapping); ValidateMapping(resolvedMapping, principals?[0]); @@ -80,73 +88,79 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) private CoreTypeMapping? FindMappingWithConversion( TypeMappingInfo mappingInfo, Type? providerClrType, - ValueConverter? customConverter) + ValueConverter? customConverter, + CoreTypeMapping? elementMapping) => _explicitMappings.GetOrAdd( - (mappingInfo, providerClrType, customConverter), + (mappingInfo, providerClrType, customConverter, elementMapping), static (k, self) => { - var (info, providerType, converter) = k; - var mapping = providerType == null - || providerType == info.ClrType - ? self.FindMapping(info) - : null; + var (mappingInfo, providerClrType, customConverter, elementMapping) = k; - if (mapping == null) + var sourceType = mappingInfo.ClrType; + CoreTypeMapping? mapping = null; + + if (elementMapping == null + || customConverter != null) { - var sourceType = info.ClrType; - if (sourceType != null) + mapping = providerClrType == null + || providerClrType == mappingInfo.ClrType + ? self.FindMapping(mappingInfo) + : null; + + if (mapping == null) { - foreach (var converterInfo in self.Dependencies - .ValueConverterSelector - .Select(sourceType, providerType)) + if (sourceType != null) { - var mappingInfoUsed = info.WithConverter(converterInfo); - mapping = self.FindMapping(mappingInfoUsed); - - if (mapping == null - && providerType != null) + foreach (var converterInfo in self.Dependencies + .ValueConverterSelector + .Select(sourceType, providerClrType)) { - foreach (var secondConverterInfo in self.Dependencies - .ValueConverterSelector - .Select(providerType)) - { - mapping = self.FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); + var mappingInfoUsed = mappingInfo.WithConverter(converterInfo); + mapping = self.FindMapping(mappingInfoUsed); - if (mapping != null) + if (mapping == null + && providerClrType != null) + { + foreach (var secondConverterInfo in self.Dependencies + .ValueConverterSelector + .Select(providerClrType)) { - mapping = mapping.Clone( - secondConverterInfo.Create(), - info.ElementTypeMapping, - jsonValueReaderWriter: mappingInfoUsed.JsonValueReaderWriter); - break; + mapping = self.FindMapping(mappingInfoUsed.WithConverter(secondConverterInfo)); + + if (mapping != null) + { + mapping = mapping.Clone( + secondConverterInfo.Create(), + jsonValueReaderWriter: mappingInfoUsed.JsonValueReaderWriter); + break; + } } } - } - if (mapping != null) - { - mapping = mapping.Clone( - converterInfo.Create(), - info.ElementTypeMapping, - jsonValueReaderWriter: info.JsonValueReaderWriter); - break; + if (mapping != null) + { + mapping = mapping.Clone( + converterInfo.Create(), + jsonValueReaderWriter: mappingInfo.JsonValueReaderWriter); + break; + } } - } - if (mapping == null) - { - mapping = self.TryFindCollectionMapping(info, sourceType, providerType); + mapping ??= self.TryFindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); } } } + else if (sourceType != null) + { + mapping = self.TryFindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); + } if (mapping != null - && converter != null) + && customConverter != null) { mapping = mapping.Clone( - converter, - info.ElementTypeMapping, - jsonValueReaderWriter: info.JsonValueReaderWriter); + customConverter, + jsonValueReaderWriter: mappingInfo.JsonValueReaderWriter); } return mapping; @@ -159,14 +173,15 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) /// The mapping info being used. /// The model type. /// The provider type. - /// The type mapping, or if none was found. + /// The element mapping, if known. + /// The type mapping, or if none was found. protected virtual CoreTypeMapping? TryFindCollectionMapping( TypeMappingInfo info, Type modelType, - Type? providerType) + Type? providerType, + CoreTypeMapping? elementMapping) => TryFindJsonCollectionMapping( - info, modelType, providerType, out var elementMapping, - out var collectionReaderWriter) + info, modelType, providerType, ref elementMapping, out var collectionReaderWriter) ? FindMapping( info.WithConverter( // Note that the converter info is only used temporarily here and never creates an instance. @@ -194,6 +209,29 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) return FindMappingWithConversion(new TypeMappingInfo(principals), principals); } + /// + /// Finds the type mapping for a given . + /// + /// + /// Note: providers should typically not need to override this method. + /// + /// The property. + /// The type mapping, or if none was found. + public override CoreTypeMapping? FindMapping(IElementType elementType) + { + var providerClrType = elementType.GetProviderClrType(); + var customConverter = elementType.GetValueConverter(); + + var resolvedMapping = FindMappingWithConversion( + new TypeMappingInfo( + elementType, elementType.IsUnicode(), elementType.GetMaxLength(), elementType.GetPrecision(), elementType.GetScale()), + providerClrType, customConverter, null); + + ValidateMapping(resolvedMapping, null); + + return resolvedMapping; + } + /// /// Finds the type mapping for a given . /// @@ -246,12 +284,7 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) scale: typeConfiguration.GetScale()); } - if (elementMapping != null) - { - mappingInfo = mappingInfo.WithElementTypeMapping(elementMapping); - } - - return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); + return FindMappingWithConversion(mappingInfo, providerClrType, customConverter, elementMapping); } /// diff --git a/src/EFCore/Storage/TypeMappingSourceBase.cs b/src/EFCore/Storage/TypeMappingSourceBase.cs index 4b6f787bff2..2bc79663a0c 100644 --- a/src/EFCore/Storage/TypeMappingSourceBase.cs +++ b/src/EFCore/Storage/TypeMappingSourceBase.cs @@ -87,6 +87,13 @@ protected virtual void ValidateMapping( /// The type mapping, or if none was found. public abstract CoreTypeMapping? FindMapping(IProperty property); + /// + /// Finds the type mapping for a given . + /// + /// The collection element. + /// The type mapping, or if none was found. + public abstract CoreTypeMapping? FindMapping(IElementType elementType); + /// /// Finds the type mapping for a given . /// @@ -142,7 +149,7 @@ protected virtual bool TryFindJsonCollectionMapping( TypeMappingInfo mappingInfo, Type modelClrType, Type? providerClrType, - out CoreTypeMapping? elementMapping, + ref CoreTypeMapping? elementMapping, out JsonValueReaderWriter? collectionReaderWriter) { if ((providerClrType == null || providerClrType == typeof(string)) @@ -150,8 +157,7 @@ protected virtual bool TryFindJsonCollectionMapping( && elementType != modelClrType && !modelClrType.GetGenericTypeImplementations(typeof(IDictionary<,>)).Any()) { - elementMapping = mappingInfo.ElementTypeMapping - ?? FindMapping(elementType); + elementMapping ??= FindMapping(elementType); if (elementMapping is { ElementTypeMapping: null, JsonValueReaderWriter: not null }) { diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosApiConsistencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosApiConsistencyTest.cs index 222d01e4f51..249a33a6cd1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosApiConsistencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosApiConsistencyTest.cs @@ -22,6 +22,7 @@ public class CosmosApiConsistencyFixture : ApiConsistencyFixtureBase { public override HashSet FluentApiTypes { get; } = new() { + typeof(CosmosPrimitiveCollectionBuilderExtensions), typeof(CosmosModelBuilderExtensions), typeof(CosmosPropertyBuilderExtensions), typeof(CosmosServiceCollectionExtensions), diff --git a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs index a175b10d3fa..4891eb90128 100644 --- a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs @@ -5,212 +5,115 @@ namespace Microsoft.EntityFrameworkCore.Cosmos; -public class JsonTypesCosmosTest : JsonTypesTestBase +public class JsonTypesCosmosTest : JsonTypesTestBase { - public JsonTypesCosmosTest(JsonTypesCosmosFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - } - public override void Can_read_write_point() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_point()); + => Assert.Throws(() => base.Can_read_write_point()); + + public override void Can_read_write_point_with_Z() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_with_Z()); + + public override void Can_read_write_point_with_M() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_with_M()); + + public override void Can_read_write_point_with_Z_and_M() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_with_Z_and_M()); public override void Can_read_write_line_string() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_line_string()); + => Assert.Throws(() => base.Can_read_write_line_string()); public override void Can_read_write_multi_line_string() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_multi_line_string()); + => Assert.Throws(() => base.Can_read_write_multi_line_string()); public override void Can_read_write_polygon() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_polygon()); + => Assert.Throws(() => base.Can_read_write_polygon()); public override void Can_read_write_polygon_typed_as_geometry() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_polygon_typed_as_geometry()); + => Assert.Throws(() => base.Can_read_write_polygon_typed_as_geometry()); public override void Can_read_write_point_as_GeoJson() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_point_as_GeoJson()); + => Assert.Throws(() => base.Can_read_write_point_as_GeoJson()); + + public override void Can_read_write_point_with_Z_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_with_Z_as_GeoJson()); + + public override void Can_read_write_point_with_M_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_with_M_as_GeoJson()); + + public override void Can_read_write_point_with_Z_and_M_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_with_Z_and_M_as_GeoJson()); public override void Can_read_write_line_string_as_GeoJson() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_line_string_as_GeoJson()); + => Assert.Throws(() => base.Can_read_write_line_string_as_GeoJson()); public override void Can_read_write_multi_line_string_as_GeoJson() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_multi_line_string_as_GeoJson()); + => Assert.Throws(() => base.Can_read_write_multi_line_string_as_GeoJson()); public override void Can_read_write_polygon_as_GeoJson() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_polygon_as_GeoJson()); + => Assert.Throws(() => base.Can_read_write_polygon_as_GeoJson()); public override void Can_read_write_polygon_typed_as_geometry_as_GeoJson() // No built-in JSON support for spatial types in the Cosmos provider - => Assert.Throws(() => base.Can_read_write_polygon_typed_as_geometry_as_GeoJson()); - - public override void Can_read_write_collection_of_sbyte_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_sbyte_JSON_values()); - - public override void Can_read_write_collection_of_short_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_short_JSON_values()); - - public override void Can_read_write_collection_of_int_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_int_JSON_values()); - - public override void Can_read_write_collection_of_long_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_long_JSON_values()); - - public override void Can_read_write_collection_of_byte_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_byte_JSON_values()); - - public override void Can_read_write_collection_of_uint_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_uint_JSON_values()); - - public override void Can_read_write_collection_of_float_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_float_JSON_values()); - - public override void Can_read_write_collection_of_double_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_double_JSON_values()); - - public override void Can_read_write_collection_of_decimal_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_decimal_JSON_values()); - - public override void Can_read_write_collection_of_DateOnly_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_DateOnly_JSON_values()); - - public override void Can_read_write_collection_of_TimeOnly_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_TimeOnly_JSON_values()); - - public override void Can_read_write_collection_of_DateTime_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_DateTime_JSON_values()); + => Assert.Throws(() => base.Can_read_write_polygon_typed_as_geometry_as_GeoJson()); - public override void Can_read_write_collection_of_DateTimeOffset_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_DateTimeOffset_JSON_values()); - - public override void Can_read_write_collection_of_TimeSpan_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_TimeSpan_JSON_values()); - - public override void Can_read_write_collection_of_bool_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_bool_JSON_values()); - - public override void Can_read_write_collection_of_char_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_char_JSON_values()); - - public override void Can_read_write_collection_of_string_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_string_JSON_values()); - - public override void Can_read_write_collection_of_binary_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_binary_JSON_values()); - - public override void Can_read_write_collection_of_nullable_sbyte_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_sbyte_JSON_values()); - - public override void Can_read_write_collection_of_nullable_short_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_short_JSON_values()); - - public override void Can_read_write_collection_of_nullable_int_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_int_JSON_values()); - - public override void Can_read_write_collection_of_nullable_long_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_long_JSON_values()); - - public override void Can_read_write_collection_of_nullable_byte_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_byte_JSON_values()); - - public override void Can_read_write_collection_of_nullable_uint_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_uint_JSON_values()); - - public override void Can_read_write_collection_of_nullable_float_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_float_JSON_values()); - - public override void Can_read_write_collection_of_nullable_double_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_double_JSON_values()); - - public override void Can_read_write_collection_of_nullable_decimal_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_decimal_JSON_values()); - - public override void Can_read_write_collection_of_nullable_DateOnly_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_DateOnly_JSON_values()); + public override void Can_read_write_nullable_point() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point()); - public override void Can_read_write_collection_of_nullable_TimeOnly_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_TimeOnly_JSON_values()); + public override void Can_read_write_nullable_line_string() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_line_string()); - public override void Can_read_write_collection_of_nullable_DateTime_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_DateTime_JSON_values()); + public override void Can_read_write_nullable_multi_line_string() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_multi_line_string()); - public override void Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values()); + public override void Can_read_write_nullable_polygon() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_polygon()); - public override void Can_read_write_collection_of_nullable_TimeSpan_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_TimeSpan_JSON_values()); + public override void Can_read_write_nullable_point_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_point_as_GeoJson()); - public override void Can_read_write_collection_of_nullable_bool_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_bool_JSON_values()); + public override void Can_read_write_nullable_line_string_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_line_string_as_GeoJson()); - public override void Can_read_write_collection_of_nullable_char_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_char_JSON_values()); + public override void Can_read_write_nullable_multi_line_string_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_multi_line_string_as_GeoJson()); - public override void Can_read_write_collection_of_nullable_GUID_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_string_JSON_values()); + public override void Can_read_write_nullable_polygon_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_polygon_as_GeoJson()); - public override void Can_read_write_collection_of_nullable_string_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_string_JSON_values()); + public override void Can_read_write_polygon_typed_as_nullable_geometry() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_polygon_typed_as_nullable_geometry()); - public override void Can_read_write_collection_of_nullable_binary_JSON_values() - // Cosmos currently uses a different mechanism for primitive collections - => Assert.Throws(() => base.Can_read_write_collection_of_nullable_binary_JSON_values()); + public override void Can_read_write_polygon_typed_as_nullable_geometry_as_GeoJson() + // No built-in JSON support for spatial types in the Cosmos provider + => Assert.Throws(() => base.Can_read_write_polygon_typed_as_nullable_geometry_as_GeoJson()); - public class JsonTypesCosmosFixture : JsonTypesFixtureBase + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - protected override ITestStoreFactory TestStoreFactory - => CosmosTestStoreFactory.Instance; - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Ignore(); - modelBuilder.Ignore(); - } + var store = CosmosTestStore.GetOrCreate(nameof(JsonTypesCosmosTest)); + base.OnConfiguring(optionsBuilder.UseCosmos(store.ConnectionUri, store.AuthToken, store.Name)); } } diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs index 5085c43a96c..4b04a27d661 100644 --- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs +++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosModelBuilderGenericTest.cs @@ -164,7 +164,7 @@ public virtual void No_id_property_created_if_another_property_mapped_to_id_in_p var model = modelBuilder.FinalizeModel(); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; Assert.Null(entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)); Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey())); @@ -243,6 +243,90 @@ public virtual void No_alternate_key_is_created_if_id_is_partition_key() Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey())); } + public override void Primitive_collections_can_be_made_concurrency_tokens() + => Assert.Equal( + CosmosStrings.NonETagConcurrencyToken(nameof(CollectionQuarks), "Charm"), + Assert.Throws( + () => base.Primitive_collections_can_be_made_concurrency_tokens()).Message); + + [ConditionalFact] + public virtual void Primitive_collections_key_is_added_to_the_keys() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .Ignore(b => b.Details) + .Ignore(b => b.Orders) + .HasPartitionKey(b => b.Notes) + .PrimitiveCollection(b => b.Notes); + + var model = modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + + Assert.Equal( + new[] { nameof(Customer.Id), nameof(Customer.Notes) }, + entity.FindPrimaryKey()!.Properties.Select(p => p.Name)); + Assert.Equal( + new[] { StoreKeyConvention.DefaultIdPropertyName, nameof(Customer.Notes) }, + entity.GetKeys().First(k => k != entity.FindPrimaryKey()).Properties.Select(p => p.Name)); + + var idProperty = entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)!; + Assert.Single(idProperty.GetContainingKeys()); + Assert.NotNull(idProperty.GetValueGeneratorFactory()); + } + + [ConditionalFact] + public virtual void No_id_property_created_if_another_primitive_collection_mapped_to_id() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .PrimitiveCollection(c => c.Notes) + .ToJsonProperty(StoreKeyConvention.IdPropertyJsonName); + modelBuilder.Entity() + .Ignore(b => b.Details) + .Ignore(b => b.Orders); + + var model = modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + + Assert.Null(entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)); + Assert.Single(entity.GetKeys().Where(k => k != entity.FindPrimaryKey())); + + var idProperty = entity.GetDeclaredProperties() + .Single(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); + Assert.Single(idProperty.GetContainingKeys()); + Assert.Null(idProperty.GetValueGeneratorFactory()); + } + + [ConditionalFact] + public virtual void No_id_property_created_if_another_primitive_collection_to_id_in_pk() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity() + .PrimitiveCollection(c => c.Notes) + .ToJsonProperty(StoreKeyConvention.IdPropertyJsonName); + modelBuilder.Entity() + .Ignore(c => c.Details) + .Ignore(c => c.Orders) + .HasKey(c => c.Notes); + + var model = modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + + Assert.Null(entity.FindProperty(StoreKeyConvention.DefaultIdPropertyName)); + Assert.Empty(entity.GetKeys().Where(k => k != entity.FindPrimaryKey())); + + var idProperty = entity.GetDeclaredProperties() + .Single(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); + Assert.Single(idProperty.GetContainingKeys()); + Assert.Null(idProperty.GetValueGeneratorFactory()); + } + protected override TestModelBuilder CreateModelBuilder(Action configure = null) => CreateTestModelBuilder(CosmosTestHelpers.Instance, configure); } diff --git a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosTestModelBuilderExtensions.cs b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosTestModelBuilderExtensions.cs index aa54bcc4f9d..a8274349adc 100644 --- a/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosTestModelBuilderExtensions.cs +++ b/test/EFCore.Cosmos.Tests/ModelBuilding/CosmosTestModelBuilderExtensions.cs @@ -58,4 +58,21 @@ public static ModelBuilderTest.TestPropertyBuilder ToJsonProperty ToJsonProperty( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string name) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.ToJsonProperty(name); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.ToJsonProperty(name); + break; + } + + return builder; + } } diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 5c5d872aefb..d798e071d2a 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -98,6 +98,7 @@ public void Test_new_annotations_handled_for_entity_types() #pragma warning disable CS0618 RelationalAnnotationNames.ContainerColumnTypeMapping, #pragma warning restore CS0618 + RelationalAnnotationNames.StoreType }; // Add a line here if the code generator is supposed to handle this annotation @@ -269,6 +270,7 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.ContainerColumnTypeMapping, #pragma warning restore CS0618 RelationalAnnotationNames.JsonPropertyName, + RelationalAnnotationNames.StoreType, }; var columnMapping = $@"{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasColumnType)}(""default_int_mapping"")"; @@ -560,7 +562,7 @@ public void Migrations_compile() }, Array.Empty()); Assert.Equal( -""" + """ using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -631,7 +633,7 @@ protected override void Down(MigrationBuilder migrationBuilder) "20150511161616_MyMigration", finalizedModel); Assert.Equal( -""" + """ // using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore; @@ -767,7 +769,7 @@ public void Snapshots_compile() "MySnapshot", finalizedModel); Assert.Equal( -""" + """ // using System; using System.Text.RegularExpressions; diff --git a/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs index 7a04e6d82f6..b5364a02350 100644 --- a/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs @@ -5,17 +5,24 @@ namespace Microsoft.EntityFrameworkCore; -public class JsonTypesInMemoryTest : JsonTypesTestBase +public class JsonTypesInMemoryTest : JsonTypesTestBase { - public JsonTypesInMemoryTest(JsonTypesInMemoryFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - } - public override void Can_read_write_point() // No built-in JSON support for spatial types in the in-memory provider => Assert.Throws(() => base.Can_read_write_point()); + public override void Can_read_write_point_with_M() + // No built-in JSON support for spatial types in the in-memory provider + => Assert.Throws(() => base.Can_read_write_point_with_M()); + + public override void Can_read_write_point_with_Z() + // No built-in JSON support for spatial types in the in-memory provider + => Assert.Throws(() => base.Can_read_write_point_with_Z()); + + public override void Can_read_write_point_with_Z_and_M() + // No built-in JSON support for spatial types in the in-memory provider + => Assert.Throws(() => base.Can_read_write_point_with_Z_and_M()); + public override void Can_read_write_line_string() // No built-in JSON support for spatial types in the in-memory provider => Assert.Throws(() => base.Can_read_write_line_string()); @@ -32,9 +39,6 @@ public override void Can_read_write_polygon_typed_as_geometry() // No built-in JSON support for spatial types in the in-memory provider => Assert.Throws(() => base.Can_read_write_polygon_typed_as_geometry()); - public class JsonTypesInMemoryFixture : JsonTypesFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => InMemoryTestStoreFactory.Instance; - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseInMemoryDatabase("X")); } diff --git a/test/EFCore.Relational.Specification.Tests/JsonTypesRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/JsonTypesRelationalTestBase.cs new file mode 100644 index 00000000000..92b4e05db8d --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/JsonTypesRelationalTestBase.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class JsonTypesRelationalTestBase : JsonTypesTestBase +{ + [ConditionalTheory] + [InlineData(null)] + public virtual void Can_read_write_collection_of_fixed_length_string_JSON_values(object? storeType) + => Can_read_and_write_JSON_collection_value>( + b => b.ElementType().IsFixedLength().HasMaxLength(32), + nameof(StringCollectionType.String), + new List + { + "MinValue", + "Value", + "MaxValue" + }, + """{"Prop":["MinValue","Value","MaxValue"]}""", + facets: new Dictionary + { + { RelationalAnnotationNames.IsFixedLength, true }, + { RelationalAnnotationNames.StoreType, storeType }, + { CoreAnnotationNames.MaxLength, 32 } + }); + + [ConditionalTheory] + [InlineData(null)] + public virtual void Can_read_write_collection_of_ASCII_string_JSON_values(object? storeType) + => Can_read_and_write_JSON_collection_value>( + b => b.ElementType().IsUnicode(false), + nameof(StringCollectionType.String), + new List + { + "MinValue", + "Value", + "MaxValue" + }, + """{"Prop":["MinValue","Value","MaxValue"]}""", + facets: new Dictionary + { + { RelationalAnnotationNames.StoreType, storeType }, + { CoreAnnotationNames.Unicode, false } + }); + + protected override void AssertElementFacets(IElementType element, Dictionary? facets) + { + base.AssertElementFacets(element, facets); + + Assert.Same(element.FindTypeMapping(), element.FindRelationalTypeMapping()); + Assert.Equal(FacetValue(RelationalAnnotationNames.IsFixedLength), element.IsFixedLength()); + + var expectedStoreType = FacetValue(RelationalAnnotationNames.StoreType) + ?? element.FindRelationalTypeMapping()!.StoreType; + + Assert.Equal(expectedStoreType, element.GetStoreType()); + + object? FacetValue(string facetName) + => facets?.TryGetValue(facetName, out var facet) == true ? facet : null; + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs index 784209f9d19..360cd51c519 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs @@ -296,14 +296,12 @@ public virtual async Task Predicate_based_on_element_of_json_array_of_primitives { var query = context.Entities.Where(x => x.Reference.IntArray[0] == 1); - if (async) - { - await Assert.ThrowsAsync(() => query.ToListAsync()); - } - else - { - Assert.Throws(() => query.ToList()); - } + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal(1, result[0].Reference.IntArray[0]); } } @@ -318,14 +316,12 @@ public virtual async Task Predicate_based_on_element_of_json_array_of_primitives { var query = context.Entities.Where(x => x.Reference.ListOfString[1] == "Bar"); - if (async) - { - await Assert.ThrowsAsync(() => query.ToListAsync()); - } - else - { - Assert.Throws(() => query.ToList()); - } + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal("Bar", result[0].Reference.ListOfString[1]); } } @@ -338,17 +334,18 @@ public virtual async Task Predicate_based_on_element_of_json_array_of_primitives using (var context = contextFactory.CreateContext()) { - var query = context.Entities.Where(x => x.Reference.IntArray.AsQueryable().ElementAt(0) == 1 - || x.Reference.ListOfString.AsQueryable().ElementAt(1) == "Bar"); + var query = context.Entities.Where( + x => x.Reference.IntArray.AsQueryable().ElementAt(0) == 1 + || x.Reference.ListOfString.AsQueryable().ElementAt(1) == "Bar") + .OrderBy(e => e.Id); - if (async) - { - await Assert.ThrowsAsync(() => query.ToListAsync()); - } - else - { - Assert.Throws(() => query.ToList()); - } + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal(1, result[0].Reference.IntArray[0]); + Assert.Equal("Bar", result[0].Reference.ListOfString[1]); } } diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index 19a84b92361..a4a9e4007d1 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -144,6 +144,142 @@ public static ModelBuilderTest.TestPropertyBuilder HasJsonPropertyNam return builder; } + public static ModelBuilderTest.TestPrimitiveCollectionBuilder HasColumnName( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string? name) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasColumnName(name); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.HasColumnName(name); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder HasColumnType( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string typeName) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasColumnType(typeName); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.HasColumnType(typeName); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder HasDefaultValueSql( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string sql) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasDefaultValueSql(sql); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.HasDefaultValueSql(sql); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder HasComputedColumnSql( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string sql) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasComputedColumnSql(sql); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.HasComputedColumnSql(sql); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder HasDefaultValue( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + object value) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasDefaultValue(value); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.HasDefaultValue(value); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder IsFixedLength( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + bool fixedLength = true) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.IsFixedLength(fixedLength); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.IsFixedLength(fixedLength); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder UseCollation( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string? collation) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.UseCollation(collation); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.UseCollation(collation); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestPrimitiveCollectionBuilder HasJsonPropertyName( + this ModelBuilderTest.TestPrimitiveCollectionBuilder builder, + string? name) + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasJsonPropertyName(name); + break; + case IInfrastructure nonGenericBuilder: + nonGenericBuilder.Instance.HasJsonPropertyName(name); + break; + } + + return builder; + } + public static ModelBuilderTest.TestEntityTypeBuilder UseTpcMappingStrategy( this ModelBuilderTest.TestEntityTypeBuilder builder) where TEntity : class diff --git a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs index b6769a7640f..29ce50355a3 100644 --- a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs +++ b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs @@ -146,6 +146,8 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase typeof(RelationalOwnedNavigationBuilderExtensions), typeof(RelationalComplexTypeExtensions), typeof(RelationalComplexTypePropertyBuilderExtensions), + typeof(RelationalPrimitiveCollectionBuilderExtensions), + typeof(RelationalComplexTypePrimitiveCollectionBuilderExtensions), typeof(DbFunctionBuilder), typeof(DbFunctionParameterBuilder), typeof(TableBuilder), @@ -303,6 +305,16 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase null, null ) + }, + { + typeof(IReadOnlyElementType), + ( + typeof(RelationalElementTypeExtensions), + null, + null, + typeof(RelationalEntityTypeBuilderExtensions), + null + ) } }; @@ -385,6 +397,111 @@ public class RelationalApiConsistencyFixture : ApiConsistencyFixtureBase new[] { typeof(bool), typeof(bool) }) }; + public override Dictionary> UnmatchedMirrorMethods { get; } = new() + { + { + typeof(PrimitiveCollectionBuilder), new() + { + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasPrecision), new[] { typeof(int) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasPrecision), new[] { typeof(int), typeof(int) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasValueGenerator), new[] { typeof(Func) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.IsRowVersion), Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, (_, _) => Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder), nameof(PropertyBuilder.HasConversion), genericParameterCount: 2, (_, _) => Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder), nameof(PropertyBuilder.HasConversion), genericParameterCount: 3, (_, _) => Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, (_, _) => new[] { typeof(ValueComparer) }), + GetMethod(typeof(PropertyBuilder), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, (_, _) => new[] { typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueConverter) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueConverter), typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueConverter), typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(Type) }), + typeof(PropertyBuilder).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(Type), typeof(Type) }), + + } + }, + { + typeof(PrimitiveCollectionBuilder<>), new() + { + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasPrecision), new[] { typeof(int) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasPrecision), new[] { typeof(int), typeof(int) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasValueGenerator), new[] { typeof(Func) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.IsRowVersion), Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, (_, _) => Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 2, (_, _) => Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 3, (_, _) => Type.EmptyTypes), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, (_, _) => new[] { typeof(ValueComparer) }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, (_, _) => new[] { typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueConverter) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueConverter), typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(ValueConverter), typeof(ValueComparer), typeof(ValueComparer) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(Type) }), + typeof(PropertyBuilder<>).GetMethod(nameof(PropertyBuilder.HasConversion), new[] { typeof(Type), typeof(Type), typeof(Type) }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, + (typeGenerics, methodGenerics) => (typeGenerics.Length < 1 || methodGenerics.Length < 1) + ? new[] { typeof(Random) } + : new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(typeGenerics[0], methodGenerics[0])), + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodGenerics[0], typeGenerics[0])) + }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, + (typeGenerics, methodGenerics) => (typeGenerics.Length < 1 || methodGenerics.Length < 1) + ? new[] { typeof(Random) } + : new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(typeGenerics[0], methodGenerics[0])), + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodGenerics[0], typeGenerics[0])), + typeof(ValueComparer) + }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, + (typeGenerics, methodGenerics) => (typeGenerics.Length < 1 || methodGenerics.Length < 1) + ? new[] { typeof(Random) } + : new[] + { + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(typeGenerics[0], methodGenerics[0])), + typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(methodGenerics[0], typeGenerics[0])), + typeof(ValueComparer), + typeof(ValueComparer) + }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, + (typeGenerics, methodGenerics) => (typeGenerics.Length < 1 || methodGenerics.Length < 1) + ? new[] { typeof(Random) } + : new[] + { + typeof(ValueConverter<,>).MakeGenericType(typeGenerics[0], methodGenerics[0]) + }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, + (typeGenerics, methodGenerics) => (typeGenerics.Length < 1 || methodGenerics.Length < 1) + ? new[] { typeof(Random) } + : new[] + { + typeof(ValueConverter<,>).MakeGenericType(typeGenerics[0], methodGenerics[0]), + typeof(ValueComparer) + }), + GetMethod(typeof(PropertyBuilder<>), nameof(PropertyBuilder.HasConversion), genericParameterCount: 1, + (typeGenerics, methodGenerics) => (typeGenerics.Length < 1 || methodGenerics.Length < 1) + ? new[] { typeof(Random) } + : new[] + { + typeof(ValueConverter<,>).MakeGenericType(typeGenerics[0], methodGenerics[0]), + typeof(ValueComparer), + typeof(ValueComparer) + }) + } + } + + }; + public override HashSet AsyncMethodExceptions { get; } = new() { typeof(RelationalDatabaseFacadeExtensions).GetMethod(nameof(RelationalDatabaseFacadeExtensions.CloseConnectionAsync)), @@ -467,6 +584,12 @@ protected override void Initialize() MirrorTypes.Add(typeof(StoredProcedureBuilder), typeof(OwnedNavigationStoredProcedureBuilder)); MirrorTypes.Add(typeof(StoredProcedureBuilder<>), typeof(OwnedNavigationStoredProcedureBuilder<,>)); MirrorTypes.Add(typeof(RelationalComplexTypePropertyBuilderExtensions), typeof(RelationalPropertyBuilderExtensions)); + MirrorTypes.Add(typeof(PropertyBuilder), typeof(PrimitiveCollectionBuilder)); + MirrorTypes.Add(typeof(PropertyBuilder<>), typeof(PrimitiveCollectionBuilder<>)); + MirrorTypes.Add(typeof(PrimitiveCollectionBuilder), typeof(ComplexTypePrimitiveCollectionBuilder)); + MirrorTypes.Add(typeof(PrimitiveCollectionBuilder<>), typeof(ComplexTypePrimitiveCollectionBuilder<>)); + MirrorTypes.Add(typeof(RelationalPrimitiveCollectionBuilderExtensions), typeof(RelationalPropertyBuilderExtensions)); + MirrorTypes.Add(typeof(RelationalComplexTypePrimitiveCollectionBuilderExtensions), typeof(RelationalComplexTypePropertyBuilderExtensions)); base.Initialize(); } diff --git a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs index e0c6cbb387c..8a7ad920641 100644 --- a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs +++ b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs @@ -183,7 +183,9 @@ public void Builders_have_matching_methods() foreach (var method in tuple.Key.GetMethods(PublicInstance | BindingFlags.DeclaredOnly)) { - if (!Fixture.UnmatchedMetadataMethods.Contains(method)) + if (!Fixture.UnmatchedMetadataMethods.Contains(method) + && !(Fixture.UnmatchedMirrorMethods.TryGetValue(tuple.Value, out var unmatchedMirrorMethods) + && unmatchedMirrorMethods.Contains(method))) { MethodInfo matchingMethod = null; foreach (var targetMethod in tuple.Value.GetMethods(PublicInstance | BindingFlags.DeclaredOnly)) @@ -1263,7 +1265,9 @@ protected ApiConsistencyFixtureBase() { typeof(OwnedEntityTypeBuilder), typeof(OwnedEntityTypeBuilder<>) }, { typeof(OwnershipBuilder), typeof(OwnershipBuilder<,>) }, { typeof(PropertyBuilder), typeof(PropertyBuilder<>) }, + { typeof(PrimitiveCollectionBuilder), typeof(PrimitiveCollectionBuilder<>) }, { typeof(ComplexTypePropertyBuilder), typeof(ComplexTypePropertyBuilder<>) }, + { typeof(ComplexTypePrimitiveCollectionBuilder), typeof(ComplexTypePrimitiveCollectionBuilder<>) }, { typeof(ReferenceCollectionBuilder), typeof(ReferenceCollectionBuilder<,>) }, { typeof(ReferenceNavigationBuilder), typeof(ReferenceNavigationBuilder<,>) }, { typeof(ReferenceReferenceBuilder), typeof(ReferenceReferenceBuilder<,>) }, @@ -1276,6 +1280,7 @@ protected ApiConsistencyFixtureBase() public virtual HashSet NotAnnotatedMethods { get; } = new(); public virtual HashSet AsyncMethodExceptions { get; } = new(); public virtual HashSet UnmatchedMetadataMethods { get; } = new(); + public virtual Dictionary> UnmatchedMirrorMethods { get; } = new(); public virtual Dictionary MetadataMethodNameTransformers { get; } = new(); public virtual HashSet MetadataMethodExceptions { get; } = new(); @@ -1394,6 +1399,12 @@ protected ApiConsistencyFixtureBase() typeof(IConventionPropertyBase), typeof(IConventionPropertyBaseBuilder<>), typeof(IPropertyBase)) + }, + { + typeof(IReadOnlyElementType), (typeof(IMutableElementType), + typeof(IConventionElementType), + typeof(IConventionElementTypeBuilder), + typeof(IElementType)) } }; @@ -1427,7 +1438,7 @@ protected static MethodInfo GetMethod( || (mi.IsGenericMethod && mi.GetGenericArguments().Length == genericParameterCount)) && mi.GetParameters().Select(e => e.ParameterType).SequenceEqual( parameterGenerator( - type.IsGenericType ? type.GenericTypeArguments : Array.Empty(), + type.IsGenericType ? type.GetGenericArguments() : Array.Empty(), mi.IsGenericMethod ? mi.GetGenericArguments() : Array.Empty()))); protected virtual void Initialize() diff --git a/test/EFCore.Specification.Tests/JsonTypesTestBase.cs b/test/EFCore.Specification.Tests/JsonTypesTestBase.cs index c45a2f3012b..1f371592339 100644 --- a/test/EFCore.Specification.Tests/JsonTypesTestBase.cs +++ b/test/EFCore.Specification.Tests/JsonTypesTestBase.cs @@ -3,11 +3,13 @@ #nullable enable +using System.Collections; using System.Collections.ObjectModel; using System.Globalization; using System.Net; using System.Net.NetworkInformation; using System.Text.Json; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; using NetTopologySuite; using NetTopologySuite.Geometries; @@ -17,26 +19,20 @@ namespace Microsoft.EntityFrameworkCore; -public abstract class JsonTypesTestBase : IClassFixture - where TFixture : JsonTypesTestBase.JsonTypesFixtureBase, new() +public abstract class JsonTypesTestBase { - protected JsonTypesTestBase(TFixture fixture) - { - Fixture = fixture; - } - - protected TFixture Fixture { get; } - - protected DbContext CreateContext() - => Fixture.CreateContext(); - [ConditionalTheory] [InlineData(sbyte.MinValue, """{"Prop":-128}""")] [InlineData(sbyte.MaxValue, """{"Prop":127}""")] [InlineData((sbyte)0, """{"Prop":0}""")] [InlineData((sbyte)1, """{"Prop":1}""")] public virtual void Can_read_write_sbyte_JSON_values(sbyte value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Int8)), value, json); + => Can_read_and_write_JSON_value(nameof(Int8Type.Int8), value, json); + + protected class Int8Type + { + public sbyte Int8 { get; set; } + } [ConditionalTheory] [InlineData(short.MinValue, """{"Prop":-32768}""")] @@ -44,7 +40,12 @@ public virtual void Can_read_write_sbyte_JSON_values(sbyte value, string json) [InlineData((short)0, """{"Prop":0}""")] [InlineData((short)1, """{"Prop":1}""")] public virtual void Can_read_write_short_JSON_values(short value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Int16)), value, json); + => Can_read_and_write_JSON_value(nameof(Int16Type.Int16), value, json); + + protected class Int16Type + { + public short Int16 { get; set; } + } [ConditionalTheory] [InlineData(int.MinValue, """{"Prop":-2147483648}""")] @@ -52,7 +53,12 @@ public virtual void Can_read_write_short_JSON_values(short value, string json) [InlineData(0, """{"Prop":0}""")] [InlineData(1, """{"Prop":1}""")] public virtual void Can_read_write_int_JSON_values(int value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Int32)), value, json); + => Can_read_and_write_JSON_value(nameof(Int32Type.Int32), value, json); + + protected class Int32Type + { + public int Int32 { get; set; } + } [ConditionalTheory] [InlineData(long.MinValue, """{"Prop":-9223372036854775808}""")] @@ -60,35 +66,60 @@ public virtual void Can_read_write_int_JSON_values(int value, string json) [InlineData((long)0, """{"Prop":0}""")] [InlineData((long)1, """{"Prop":1}""")] public virtual void Can_read_write_long_JSON_values(long value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Int64)), value, json); + => Can_read_and_write_JSON_value(nameof(Int64Type.Int64), value, json); + + protected class Int64Type + { + public long Int64 { get; set; } + } [ConditionalTheory] [InlineData(byte.MinValue, """{"Prop":0}""")] [InlineData(byte.MaxValue, """{"Prop":255}""")] [InlineData((byte)1, """{"Prop":1}""")] public virtual void Can_read_write_byte_JSON_values(byte value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.UInt8)), value, json); + => Can_read_and_write_JSON_value(nameof(UInt8Type.UInt8), value, json); + + protected class UInt8Type + { + public byte UInt8 { get; set; } + } [ConditionalTheory] [InlineData(ushort.MinValue, """{"Prop":0}""")] [InlineData(ushort.MaxValue, """{"Prop":65535}""")] [InlineData((ushort)1, """{"Prop":1}""")] public virtual void Can_read_write_ushort_JSON_values(ushort value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.UInt16)), value, json); + => Can_read_and_write_JSON_value(nameof(UInt16Type.UInt16), value, json); + + protected class UInt16Type + { + public ushort UInt16 { get; set; } + } [ConditionalTheory] [InlineData(uint.MinValue, """{"Prop":0}""")] [InlineData(uint.MaxValue, """{"Prop":4294967295}""")] [InlineData((uint)1, """{"Prop":1}""")] public virtual void Can_read_write_uint_JSON_values(uint value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.UInt32)), value, json); + => Can_read_and_write_JSON_value(nameof(UInt32Type.UInt32), value, json); + + protected class UInt32Type + { + public uint UInt32 { get; set; } + } [ConditionalTheory] [InlineData(ulong.MinValue, """{"Prop":0}""")] [InlineData(ulong.MaxValue, """{"Prop":18446744073709551615}""")] [InlineData((ulong)1, """{"Prop":1}""")] public virtual void Can_read_write_ulong_JSON_values(ulong value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.UInt64)), value, json); + => Can_read_and_write_JSON_value(nameof(UInt64Type.UInt64), value, json); + + protected class UInt64Type + { + public ulong UInt64 { get; set; } + } [ConditionalTheory] [InlineData(float.MinValue, """{"Prop":-3.4028235E+38}""")] @@ -96,7 +127,12 @@ public virtual void Can_read_write_ulong_JSON_values(ulong value, string json) [InlineData((float)0.0, """{"Prop":0}""")] [InlineData((float)1.1, """{"Prop":1.1}""")] public virtual void Can_read_write_float_JSON_values(float value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Float)), value, json); + => Can_read_and_write_JSON_value(nameof(FloatType.Float), value, json); + + protected class FloatType + { + public float Float { get; set; } + } [ConditionalTheory] [InlineData(double.MinValue, """{"Prop":-1.7976931348623157E+308}""")] @@ -104,7 +140,12 @@ public virtual void Can_read_write_float_JSON_values(float value, string json) [InlineData(0.0, """{"Prop":0}""")] [InlineData(1.1, """{"Prop":1.1}""")] public virtual void Can_read_write_double_JSON_values(double value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Double)), value, json); + => Can_read_and_write_JSON_value(nameof(DoubleType.Double), value, json); + + protected class DoubleType + { + public double Double { get; set; } + } [ConditionalTheory] [InlineData("-79228162514264337593543950335", """{"Prop":-79228162514264337593543950335}""")] @@ -112,59 +153,92 @@ public virtual void Can_read_write_double_JSON_values(double value, string json) [InlineData("0.0", """{"Prop":0.0}""")] [InlineData("1.1", """{"Prop":1.1}""")] public virtual void Can_read_write_decimal_JSON_values(decimal value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Decimal)), value, json); + => Can_read_and_write_JSON_value(nameof(DecimalType.Decimal), value, json); + + protected class DecimalType + { + public decimal Decimal { get; set; } + } [ConditionalTheory] [InlineData("1/1/0001", """{"Prop":"0001-01-01"}""")] [InlineData("12/31/9999", """{"Prop":"9999-12-31"}""")] [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] public virtual void Can_read_write_DateOnly_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.DateOnly)), + => Can_read_and_write_JSON_value(nameof(DateOnlyType.DateOnly), DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + protected class DateOnlyType + { + public DateOnly DateOnly { get; set; } + } + [ConditionalTheory] [InlineData("00:00:00.0000000", """{"Prop":"00:00:00.0000000"}""")] [InlineData("23:59:59.9999999", """{"Prop":"23:59:59.9999999"}""")] [InlineData("11:05:12.3456789", """{"Prop":"11:05:12.3456789"}""")] public virtual void Can_read_write_TimeOnly_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.TimeOnly)), + => Can_read_and_write_JSON_value( + nameof(TimeOnlyType.TimeOnly), TimeOnly.Parse(value, CultureInfo.InvariantCulture), json); + protected class TimeOnlyType + { + public TimeOnly TimeOnly { get; set; } + } + [ConditionalTheory] [InlineData("0001-01-01T00:00:00.0000000", """{"Prop":"0001-01-01T00:00:00"}""")] [InlineData("9999-12-31T23:59:59.9999999", """{"Prop":"9999-12-31T23:59:59.9999999"}""")] [InlineData("2023-05-29T10:52:47.2064353", """{"Prop":"2023-05-29T10:52:47.2064353"}""")] public virtual void Can_read_write_DateTime_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.DateTime)), + => Can_read_and_write_JSON_value(nameof(DateTimeType.DateTime), DateTime.Parse(value, CultureInfo.InvariantCulture), json); + protected class DateTimeType + { + public DateTime DateTime { get; set; } + } + [ConditionalTheory] [InlineData("0001-01-01T00:00:00.0000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] [InlineData("9999-12-31T23:59:59.9999999+02:00", """{"Prop":"9999-12-31T23:59:59.9999999+02:00"}""")] [InlineData("0001-01-01T00:00:00.0000000-03:00", """{"Prop":"0001-01-01T00:00:00-03:00"}""")] [InlineData("2023-05-29T11:11:15.5672854+04:00", """{"Prop":"2023-05-29T11:11:15.5672854+04:00"}""")] public virtual void Can_read_write_DateTimeOffset_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.DateTimeOffset)), + => Can_read_and_write_JSON_value(nameof(DateTimeOffsetType.DateTimeOffset), DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + protected class DateTimeOffsetType + { + public DateTimeOffset DateTimeOffset { get; set; } + } + [ConditionalTheory] [InlineData("-10675199.02:48:05.4775808", """{"Prop":"-10675199:2:48:05.4775808"}""")] [InlineData("10675199.02:48:05.4775807", """{"Prop":"10675199:2:48:05.4775807"}""")] [InlineData("00:00:00", """{"Prop":"0:00:00"}""")] [InlineData("12:23:23.8018854", """{"Prop":"12:23:23.8018854"}""")] public virtual void Can_read_write_TimeSpan_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.TimeSpan)), TimeSpan.Parse(value), json); + => Can_read_and_write_JSON_value( + nameof(TimeSpanType.TimeSpan), + TimeSpan.Parse(value), json); + + protected class TimeSpanType + { + public TimeSpan TimeSpan { get; set; } + } [ConditionalTheory] [InlineData(false, """{"Prop":false}""")] [InlineData(true, """{"Prop":true}""")] public virtual void Can_read_write_bool_JSON_values(bool value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Boolean)), value, json); + => Can_read_and_write_JSON_value(nameof(BooleanType.Boolean), value, json); + + protected class BooleanType + { + public bool Boolean { get; set; } + } [ConditionalTheory] [InlineData(char.MinValue, """{"Prop":"\u0000"}""")] @@ -172,14 +246,24 @@ public virtual void Can_read_write_bool_JSON_values(bool value, string json) [InlineData(' ', """{"Prop":" "}""")] [InlineData("Z", """{"Prop":"Z"}""")] public virtual void Can_read_write_char_JSON_values(char value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Character)), value, json); + => Can_read_and_write_JSON_value(nameof(CharacterType.Character), value, json); + + protected class CharacterType + { + public char Character { get; set; } + } [ConditionalTheory] [InlineData("00000000-0000-0000-0000-000000000000", """{"Prop":"00000000-0000-0000-0000-000000000000"}""")] [InlineData("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", """{"Prop":"ffffffff-ffff-ffff-ffff-ffffffffffff"}""")] [InlineData("8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD", """{"Prop":"8c44242f-8e3f-4a20-8be8-98c7c1aadebd"}""")] public virtual void Can_read_write_GUID_JSON_values(Guid value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Guid)), value, json); + => Can_read_and_write_JSON_value(nameof(GuidType.Guid), value, json); + + protected class GuidType + { + public Guid Guid { get; set; } + } [ConditionalTheory] [InlineData("MinValue", """{"Prop":"MinValue"}""")] @@ -189,7 +273,12 @@ public virtual void Can_read_write_GUID_JSON_values(Guid value, string json) "❤❥웃유♋☮✌☏☢☠✔☑♚▲♪฿Ɖ⛏♥❣♂♀☿👍✍✉☣☤✘☒♛▼♫⌘⌛¡♡ღツ☼☁❅♾️✎©®™Σ✪✯☭➳Ⓐ✞℃℉°✿⚡☃☂✄¢€£∞✫★½☯✡☪", @"{""Prop"":""\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A""}")] public virtual void Can_read_write_string_JSON_values(string value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.String)), value, json); + => Can_read_and_write_JSON_value(nameof(StringType.String), value, json); + + protected class StringType + { + public string String { get; set; } = null!; + } [ConditionalTheory] [InlineData("0,0,0,1", """{"Prop":"AAAAAQ=="}""")] @@ -197,18 +286,27 @@ public virtual void Can_read_write_string_JSON_values(string value, string json) [InlineData("", """{"Prop":""}""")] [InlineData("1,2,3,4", """{"Prop":"AQIDBA=="}""")] public virtual void Can_read_write_binary_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.Bytes)), + => Can_read_and_write_JSON_value( + nameof(BytesType.Bytes), value == "" ? Array.Empty() : value.Split(',').Select(e => byte.Parse(e)).ToArray(), json); + protected class BytesType + { + public byte[] Bytes { get; set; } = null!; + } + [ConditionalTheory] [InlineData( "https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", """{"Prop":"https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1\u0026q2=v2#FragmentName"}""")] [InlineData("file:///C:/test/path/file.txt", """{"Prop":"file:///C:/test/path/file.txt"}""")] public virtual void Can_read_write_URI_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.Uri)), new Uri(value), json); + => Can_read_and_write_JSON_value(nameof(UriType.Uri), new Uri(value), json); + + protected class UriType + { + public Uri Uri { get; set; } = null!; + } [ConditionalTheory] [InlineData("127.0.0.1", """{"Prop":"127.0.0.1"}""")] @@ -219,16 +317,26 @@ public virtual void Can_read_write_URI_JSON_values(string value, string json) [InlineData("::", """{"Prop":"::"}""")] [InlineData("2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577", """{"Prop":"2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"}""")] public virtual void Can_read_write_IP_address_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.IpAddress)), IPAddress.Parse(value), json); + => Can_read_and_write_JSON_value(nameof(IPAddressType.IpAddress), IPAddress.Parse(value), json); + + protected class IPAddressType + { + public IPAddress IpAddress { get; set; } = null!; + } [ConditionalTheory] [InlineData("001122334455", """{"Prop":"001122334455"}""")] [InlineData("00-11-22-33-44-55", """{"Prop":"001122334455"}""")] [InlineData("0011.2233.4455", """{"Prop":"001122334455"}""")] public virtual void Can_read_write_physical_address_JSON_values(string value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.PhysicalAddress)), PhysicalAddress.Parse(value), json); + => Can_read_and_write_JSON_value( + nameof(PhysicalAddressType.PhysicalAddress), + PhysicalAddress.Parse(value), json); + + protected class PhysicalAddressType + { + public PhysicalAddress PhysicalAddress { get; set; } = null!; + } [ConditionalTheory] [InlineData((sbyte)Enum8.Min, """{"Prop":-128}""")] @@ -236,7 +344,12 @@ public virtual void Can_read_write_physical_address_JSON_values(string value, st [InlineData((sbyte)Enum8.Default, """{"Prop":0}""")] [InlineData((sbyte)Enum8.One, """{"Prop":1}""")] public virtual void Can_read_write_sbyte_enum_JSON_values(Enum8 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Enum8)), value, json); + => Can_read_and_write_JSON_value(nameof(Enum8Type.Enum8), value, json); + + protected class Enum8Type + { + public Enum8 Enum8 { get; set; } + } [ConditionalTheory] [InlineData((short)Enum16.Min, """{"Prop":-32768}""")] @@ -244,7 +357,12 @@ public virtual void Can_read_write_sbyte_enum_JSON_values(Enum8 value, string js [InlineData((short)Enum16.Default, """{"Prop":0}""")] [InlineData((short)Enum16.One, """{"Prop":1}""")] public virtual void Can_read_write_short_enum_JSON_values(Enum16 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Enum16)), value, json); + => Can_read_and_write_JSON_value(nameof(Enum16Type.Enum16), value, json); + + protected class Enum16Type + { + public Enum16 Enum16 { get; set; } + } [ConditionalTheory] [InlineData((int)Enum32.Min, """{"Prop":-2147483648}""")] @@ -252,7 +370,12 @@ public virtual void Can_read_write_short_enum_JSON_values(Enum16 value, string j [InlineData((int)Enum32.Default, """{"Prop":0}""")] [InlineData((int)Enum32.One, """{"Prop":1}""")] public virtual void Can_read_write_int_enum_JSON_values(Enum32 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Enum32)), value, json); + => Can_read_and_write_JSON_value(nameof(Enum32Type.Enum32), value, json); + + protected class Enum32Type + { + public Enum32 Enum32 { get; set; } + } [ConditionalTheory] [InlineData((long)Enum64.Min, """{"Prop":-9223372036854775808}""")] @@ -260,35 +383,60 @@ public virtual void Can_read_write_int_enum_JSON_values(Enum32 value, string jso [InlineData((long)Enum64.Default, """{"Prop":0}""")] [InlineData((long)Enum64.One, """{"Prop":1}""")] public virtual void Can_read_write_long_enum_JSON_values(Enum64 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.Enum64)), value, json); + => Can_read_and_write_JSON_value(nameof(Enum64Type.Enum64), value, json); + + protected class Enum64Type + { + public Enum64 Enum64 { get; set; } + } [ConditionalTheory] [InlineData((byte)EnumU8.Min, """{"Prop":0}""")] [InlineData((byte)EnumU8.Max, """{"Prop":255}""")] [InlineData((byte)EnumU8.One, """{"Prop":1}""")] public virtual void Can_read_write_byte_enum_JSON_values(EnumU8 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.EnumU8)), value, json); + => Can_read_and_write_JSON_value(nameof(EnumU8Type.EnumU8), value, json); + + protected class EnumU8Type + { + public EnumU8 EnumU8 { get; set; } + } [ConditionalTheory] [InlineData((ushort)EnumU16.Min, """{"Prop":0}""")] [InlineData((ushort)EnumU16.Max, """{"Prop":65535}""")] [InlineData((ushort)EnumU16.One, """{"Prop":1}""")] public virtual void Can_read_write_ushort_enum_JSON_values(EnumU16 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.EnumU16)), value, json); + => Can_read_and_write_JSON_value(nameof(EnumU16Type.EnumU16), value, json); + + protected class EnumU16Type + { + public EnumU16 EnumU16 { get; set; } + } [ConditionalTheory] [InlineData((uint)EnumU32.Min, """{"Prop":0}""")] [InlineData((uint)EnumU32.Max, """{"Prop":4294967295}""")] [InlineData((uint)EnumU32.One, """{"Prop":1}""")] public virtual void Can_read_write_uint_enum_JSON_values(EnumU32 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.EnumU32)), value, json); + => Can_read_and_write_JSON_value(nameof(EnumU32Type.EnumU32), value, json); + + protected class EnumU32Type + { + public EnumU32 EnumU32 { get; set; } + } [ConditionalTheory] [InlineData((ulong)EnumU64.Min, """{"Prop":0}""")] [InlineData((ulong)EnumU64.Max, """{"Prop":18446744073709551615}""")] [InlineData((ulong)EnumU64.One, """{"Prop":1}""")] public virtual void Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json) - => Can_read_and_write_JSON_value(Fixture.EntityType().GetProperty(nameof(Types.EnumU64)), value, json); + => Can_read_and_write_JSON_value(nameof(EnumU64Type.EnumU64), value, json); + + protected class EnumU64Type + { + public EnumU64 EnumU64 { get; set; } + } [ConditionalTheory] [InlineData(sbyte.MinValue, """{"Prop":-128}""")] @@ -297,8 +445,12 @@ public virtual void Can_read_write_ulong_enum_JSON_values(EnumU64 value, string [InlineData((sbyte)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_sbyte_JSON_values(sbyte? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Int8)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableInt8Type.Int8), value, json); + + protected class NullableInt8Type + { + public sbyte Int8 { get; set; } + } [ConditionalTheory] [InlineData(short.MinValue, """{"Prop":-32768}""")] @@ -307,8 +459,12 @@ public virtual void Can_read_write_nullable_sbyte_JSON_values(sbyte? value, stri [InlineData((short)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_short_JSON_values(short? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Int16)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableInt16Type.Int16), value, json); + + protected class NullableInt16Type + { + public short Int16 { get; set; } + } [ConditionalTheory] [InlineData(int.MinValue, """{"Prop":-2147483648}""")] @@ -317,8 +473,12 @@ public virtual void Can_read_write_nullable_short_JSON_values(short? value, stri [InlineData(1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_int_JSON_values(int? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Int32)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableInt32Type.Int32), value, json); + + protected class NullableInt32Type + { + public int Int32 { get; set; } + } [ConditionalTheory] [InlineData(long.MinValue, """{"Prop":-9223372036854775808}""")] @@ -327,8 +487,12 @@ public virtual void Can_read_write_nullable_int_JSON_values(int? value, string j [InlineData((long)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_long_JSON_values(long? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Int64)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableInt64Type.Int64), value, json); + + protected class NullableInt64Type + { + public long Int64 { get; set; } + } [ConditionalTheory] [InlineData(byte.MinValue, """{"Prop":0}""")] @@ -336,8 +500,12 @@ public virtual void Can_read_write_nullable_long_JSON_values(long? value, string [InlineData((byte)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_byte_JSON_values(byte? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.UInt8)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableUInt8Type.UInt8), value, json); + + protected class NullableUInt8Type + { + public byte UInt8 { get; set; } + } [ConditionalTheory] [InlineData(ushort.MinValue, """{"Prop":0}""")] @@ -345,8 +513,12 @@ public virtual void Can_read_write_nullable_byte_JSON_values(byte? value, string [InlineData((ushort)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ushort_JSON_values(ushort? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.UInt16)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableUInt16Type.UInt16), value, json); + + protected class NullableUInt16Type + { + public ushort UInt16 { get; set; } + } [ConditionalTheory] [InlineData(uint.MinValue, """{"Prop":0}""")] @@ -354,8 +526,12 @@ public virtual void Can_read_write_nullable_ushort_JSON_values(ushort? value, st [InlineData((uint)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_uint_JSON_values(uint? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.UInt32)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableUInt32Type.UInt32), value, json); + + protected class NullableUInt32Type + { + public uint UInt32 { get; set; } + } [ConditionalTheory] [InlineData(ulong.MinValue, """{"Prop":0}""")] @@ -363,8 +539,12 @@ public virtual void Can_read_write_nullable_uint_JSON_values(uint? value, string [InlineData((ulong)1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ulong_JSON_values(ulong? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.UInt64)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableUInt64Type.UInt64), value, json); + + protected class NullableUInt64Type + { + public ulong UInt64 { get; set; } + } [ConditionalTheory] [InlineData(float.MinValue, """{"Prop":-3.4028235E+38}""")] @@ -373,8 +553,12 @@ public virtual void Can_read_write_nullable_ulong_JSON_values(ulong? value, stri [InlineData((float)1.1, """{"Prop":1.1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_float_JSON_values(float? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Float)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableFloatType.Float), value, json); + + protected class NullableFloatType + { + public float Float { get; set; } + } [ConditionalTheory] [InlineData(double.MinValue, """{"Prop":-1.7976931348623157E+308}""")] @@ -383,8 +567,12 @@ public virtual void Can_read_write_nullable_float_JSON_values(float? value, stri [InlineData(1.1, """{"Prop":1.1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_double_JSON_values(double? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Double)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableDoubleType.Double), value, json); + + protected class NullableDoubleType + { + public double Double { get; set; } + } [ConditionalTheory] [InlineData("-79228162514264337593543950335", """{"Prop":-79228162514264337593543950335}""")] @@ -393,40 +581,56 @@ public virtual void Can_read_write_nullable_double_JSON_values(double? value, st [InlineData("1.1", """{"Prop":1.1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_decimal_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Decimal)), + => Can_read_and_write_JSON_value(nameof(NullableDecimalType.Decimal), value == null ? default(decimal?) : decimal.Parse(value, CultureInfo.InvariantCulture), json); + protected class NullableDecimalType + { + public decimal Decimal { get; set; } + } + [ConditionalTheory] [InlineData("1/1/0001", """{"Prop":"0001-01-01"}""")] [InlineData("12/31/9999", """{"Prop":"9999-12-31"}""")] [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_DateOnly_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.DateOnly)), + => Can_read_and_write_JSON_value(nameof(NullableDateOnlyType.DateOnly), value == null ? default(DateOnly?) : DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + protected class NullableDateOnlyType + { + public DateOnly DateOnly { get; set; } + } + [ConditionalTheory] [InlineData("00:00:00.0000000", """{"Prop":"00:00:00.0000000"}""")] [InlineData("23:59:59.9999999", """{"Prop":"23:59:59.9999999"}""")] [InlineData("11:05:12.3456789", """{"Prop":"11:05:12.3456789"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_TimeOnly_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.TimeOnly)), + => Can_read_and_write_JSON_value(nameof(NullableTimeOnlyType.TimeOnly), value == null ? default(TimeOnly?) : TimeOnly.Parse(value, CultureInfo.InvariantCulture), json); + protected class NullableTimeOnlyType + { + public TimeOnly TimeOnly { get; set; } + } + [ConditionalTheory] [InlineData("0001-01-01T00:00:00.0000000", """{"Prop":"0001-01-01T00:00:00"}""")] [InlineData("9999-12-31T23:59:59.9999999", """{"Prop":"9999-12-31T23:59:59.9999999"}""")] [InlineData("2023-05-29T10:52:47.2064353", """{"Prop":"2023-05-29T10:52:47.2064353"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_DateTime_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.DateTime)), + => Can_read_and_write_JSON_value(nameof(NullableDateTimeType.DateTime), value == null ? default(DateTime?) : DateTime.Parse(value, CultureInfo.InvariantCulture), json); + protected class NullableDateTimeType + { + public DateTime DateTime { get; set; } + } + [ConditionalTheory] [InlineData("0001-01-01T00:00:00.0000000-01:00", """{"Prop":"0001-01-01T00:00:00-01:00"}""")] [InlineData("9999-12-31T23:59:59.9999999+02:00", """{"Prop":"9999-12-31T23:59:59.9999999+02:00"}""")] @@ -434,10 +638,14 @@ public virtual void Can_read_write_nullable_DateTime_JSON_values(string? value, [InlineData("2023-05-29T11:11:15.5672854+04:00", """{"Prop":"2023-05-29T11:11:15.5672854+04:00"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_DateTimeOffset_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.DateTimeOffset)), + => Can_read_and_write_JSON_value(nameof(NullableDateTimeOffsetType.DateTimeOffset), value == null ? default(DateTimeOffset?) : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + protected class NullableDateTimeOffsetType + { + public DateTimeOffset DateTimeOffset { get; set; } + } + [ConditionalTheory] [InlineData("-10675199.02:48:05.4775808", """{"Prop":"-10675199:2:48:05.4775808"}""")] [InlineData("10675199.02:48:05.4775807", """{"Prop":"10675199:2:48:05.4775807"}""")] @@ -445,17 +653,25 @@ public virtual void Can_read_write_nullable_DateTimeOffset_JSON_values(string? v [InlineData("12:23:23.8018854", """{"Prop":"12:23:23.8018854"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_TimeSpan_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.TimeSpan)), + => Can_read_and_write_JSON_value(nameof(NullableTimeSpanType.TimeSpan), value == null ? default(TimeSpan?) : TimeSpan.Parse(value), json); + protected class NullableTimeSpanType + { + public TimeSpan TimeSpan { get; set; } + } + [ConditionalTheory] [InlineData(false, """{"Prop":false}""")] [InlineData(true, """{"Prop":true}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_bool_JSON_values(bool? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Boolean)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableBooleanType.Boolean), value, json); + + protected class NullableBooleanType + { + public bool Boolean { get; set; } + } [ConditionalTheory] [InlineData(char.MinValue, """{"Prop":"\u0000"}""")] @@ -464,8 +680,12 @@ public virtual void Can_read_write_nullable_bool_JSON_values(bool? value, string [InlineData('Z', """{"Prop":"Z"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_char_JSON_values(char? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Character)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableCharacterType.Character), value, json); + + protected class NullableCharacterType + { + public char Character { get; set; } + } [ConditionalTheory] [InlineData("00000000-0000-0000-0000-000000000000", """{"Prop":"00000000-0000-0000-0000-000000000000"}""")] @@ -473,9 +693,13 @@ public virtual void Can_read_write_nullable_char_JSON_values(char? value, string [InlineData("8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD", """{"Prop":"8c44242f-8e3f-4a20-8be8-98c7c1aadebd"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_GUID_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Guid)), - value == null ? default(Guid?) : Guid.Parse(value, CultureInfo.InvariantCulture), json); + => Can_read_and_write_JSON_value(nameof(NullableGuidType.Guid), + value == null ? null : Guid.Parse(value, CultureInfo.InvariantCulture), json); + + protected class NullableGuidType + { + public Guid Guid { get; set; } + } [ConditionalTheory] [InlineData("MinValue", """{"Prop":"MinValue"}""")] @@ -486,8 +710,12 @@ public virtual void Can_read_write_nullable_GUID_JSON_values(string? value, stri @"{""Prop"":""\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A""}")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.String)), value, json); + => Can_read_and_write_JSON_value(nameof(NullableStringType.String), value, json); + + protected class NullableStringType + { + public string? String { get; set; } + } [ConditionalTheory] [InlineData("0,0,0,1", """{"Prop":"AAAAAQ=="}""")] @@ -496,14 +724,18 @@ public virtual void Can_read_write_nullable_string_JSON_values(string? value, st [InlineData("1,2,3,4", """{"Prop":"AQIDBA=="}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_binary_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Bytes)), + => Can_read_and_write_JSON_value(nameof(NullableBytesType.Bytes), value == null ? default : value == "" ? Array.Empty() : value.Split(',').Select(e => byte.Parse(e)).ToArray(), json); + protected class NullableBytesType + { + public byte[]? Bytes { get; set; } + } + [ConditionalTheory] [InlineData( "https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", @@ -511,10 +743,14 @@ public virtual void Can_read_write_nullable_binary_JSON_values(string? value, st [InlineData("file:///C:/test/path/file.txt", """{"Prop":"file:///C:/test/path/file.txt"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_URI_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Uri)), + => Can_read_and_write_JSON_value(nameof(NullableUriType.Uri), value == null ? default : new Uri(value), json); + protected class NullableUriType + { + public Uri? Uri { get; set; } + } + [ConditionalTheory] [InlineData("127.0.0.1", """{"Prop":"127.0.0.1"}""")] [InlineData("0.0.0.0", """{"Prop":"0.0.0.0"}""")] @@ -525,20 +761,28 @@ public virtual void Can_read_write_nullable_URI_JSON_values(string? value, strin [InlineData("2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577", """{"Prop":"2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_IP_address_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.IpAddress)), + => Can_read_and_write_JSON_value(nameof(NullableIPAddressType.IpAddress), value == null ? default : IPAddress.Parse(value), json); + protected class NullableIPAddressType + { + public IPAddress? IpAddress { get; set; } + } + [ConditionalTheory] [InlineData("001122334455", """{"Prop":"001122334455"}""")] [InlineData("00-11-22-33-44-55", """{"Prop":"001122334455"}""")] [InlineData("0011.2233.4455", """{"Prop":"001122334455"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_physical_address_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.PhysicalAddress)), + => Can_read_and_write_JSON_value(nameof(NullablePhysicalAddressType.PhysicalAddress), value == null ? default : PhysicalAddress.Parse(value), json); + protected class NullablePhysicalAddressType + { + public PhysicalAddress PhysicalAddress { get; set; } = null!; + } + [ConditionalTheory] [InlineData((sbyte)Enum8.Min, """{"Prop":-128}""")] [InlineData((sbyte)Enum8.Max, """{"Prop":127}""")] @@ -546,10 +790,14 @@ public virtual void Can_read_write_nullable_physical_address_JSON_values(string? [InlineData((sbyte)Enum8.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_sbyte_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Enum8)), + => Can_read_and_write_JSON_value(nameof(NullableEnum8Type.Enum8), value == null ? default(Enum8?) : (Enum8)value, json); + protected class NullableEnum8Type + { + public Enum8 Enum8 { get; set; } + } + [ConditionalTheory] [InlineData((short)Enum16.Min, """{"Prop":-32768}""")] [InlineData((short)Enum16.Max, """{"Prop":32767}""")] @@ -557,10 +805,14 @@ public virtual void Can_read_write_nullable_sbyte_enum_JSON_values(object? value [InlineData((short)Enum16.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_short_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Enum16)), + => Can_read_and_write_JSON_value(nameof(NullableEnum16Type.Enum16), value == null ? default(Enum16?) : (Enum16)value, json); + protected class NullableEnum16Type + { + public Enum16 Enum16 { get; set; } + } + [ConditionalTheory] [InlineData((int)Enum32.Min, """{"Prop":-2147483648}""")] [InlineData((int)Enum32.Max, """{"Prop":2147483647}""")] @@ -568,10 +820,14 @@ public virtual void Can_read_write_nullable_short_enum_JSON_values(object? value [InlineData((int)Enum32.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_int_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Enum32)), + => Can_read_and_write_JSON_value(nameof(NullableEnum32Type.Enum32), value == null ? default(Enum32?) : (Enum32)value, json); + protected class NullableEnum32Type + { + public Enum32 Enum32 { get; set; } + } + [ConditionalTheory] [InlineData((long)Enum64.Min, """{"Prop":-9223372036854775808}""")] [InlineData((long)Enum64.Max, """{"Prop":9223372036854775807}""")] @@ -579,50 +835,70 @@ public virtual void Can_read_write_nullable_int_enum_JSON_values(object? value, [InlineData((long)Enum64.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_long_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.Enum64)), + => Can_read_and_write_JSON_value(nameof(NullableEnum64Type.Enum64), value == null ? default(Enum64?) : (Enum64)value, json); + protected class NullableEnum64Type + { + public Enum64 Enum64 { get; set; } + } + [ConditionalTheory] [InlineData((byte)EnumU8.Min, """{"Prop":0}""")] [InlineData((byte)EnumU8.Max, """{"Prop":255}""")] [InlineData((byte)EnumU8.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_byte_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.EnumU8)), + => Can_read_and_write_JSON_value(nameof(NullableEnumU8Type.EnumU8), value == null ? default(EnumU8?) : (EnumU8)value, json); + protected class NullableEnumU8Type + { + public EnumU8 EnumU8 { get; set; } + } + [ConditionalTheory] [InlineData((ushort)EnumU16.Min, """{"Prop":0}""")] [InlineData((ushort)EnumU16.Max, """{"Prop":65535}""")] [InlineData((ushort)EnumU16.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ushort_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.EnumU16)), + => Can_read_and_write_JSON_value(nameof(NullableEnumU16Type.EnumU16), value == null ? default(EnumU16?) : (EnumU16)value, json); + protected class NullableEnumU16Type + { + public EnumU16 EnumU16 { get; set; } + } + [ConditionalTheory] [InlineData((uint)EnumU32.Min, """{"Prop":0}""")] [InlineData((uint)EnumU32.Max, """{"Prop":4294967295}""")] [InlineData((uint)EnumU32.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_uint_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.EnumU32)), + => Can_read_and_write_JSON_value(nameof(NullableEnumU32Type.EnumU32), value == null ? default(EnumU32?) : (EnumU32)value, json); - [ConditionalTheory] - [InlineData((ulong)EnumU64.Min, """{"Prop":0}""")] + protected class NullableEnumU32Type + { + public EnumU32 EnumU32 { get; set; } + } + + [ConditionalTheory] + [InlineData((ulong)EnumU64.Min, """{"Prop":0}""")] [InlineData((ulong)EnumU64.Max, """{"Prop":18446744073709551615}""")] [InlineData((ulong)EnumU64.One, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ulong_enum_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.EnumU64)), + => Can_read_and_write_JSON_value(nameof(NullableEnumU64Type.EnumU64), value == null ? default(EnumU64?) : (EnumU64)value, json); + protected class NullableEnumU64Type + { + public EnumU64 EnumU64 { get; set; } + } + [ConditionalTheory] [InlineData(sbyte.MinValue, """{"Prop":"-128"}""")] [InlineData(sbyte.MaxValue, """{"Prop":"127"}""")] @@ -630,8 +906,9 @@ public virtual void Can_read_write_nullable_ulong_enum_JSON_values(object? value [InlineData((sbyte)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_sbyte_as_string_JSON_values(sbyte? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Int8)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableInt8Type.Int8), value, json); [ConditionalTheory] [InlineData(short.MinValue, """{"Prop":"-32768"}""")] @@ -640,8 +917,9 @@ public virtual void Can_read_write_nullable_sbyte_as_string_JSON_values(sbyte? v [InlineData((short)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_short_as_string_JSON_values(short? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Int16)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableInt16Type.Int16), value, json); [ConditionalTheory] [InlineData(int.MinValue, """{"Prop":"-2147483648"}""")] @@ -650,8 +928,9 @@ public virtual void Can_read_write_nullable_short_as_string_JSON_values(short? v [InlineData(1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_int_as_string_JSON_values(int? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Int32)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableInt32Type.Int32), value, json); [ConditionalTheory] [InlineData(long.MinValue, """{"Prop":"-9223372036854775808"}""")] @@ -660,8 +939,9 @@ public virtual void Can_read_write_nullable_int_as_string_JSON_values(int? value [InlineData((long)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_long_as_string_JSON_values(long? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Int64)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableInt64Type.Int64), value, json); [ConditionalTheory] [InlineData(byte.MinValue, """{"Prop":"0"}""")] @@ -669,8 +949,9 @@ public virtual void Can_read_write_nullable_long_as_string_JSON_values(long? val [InlineData((byte)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_byte_as_string_JSON_values(byte? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.UInt8)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableUInt8Type.UInt8), value, json); [ConditionalTheory] [InlineData(ushort.MinValue, """{"Prop":"0"}""")] @@ -678,8 +959,9 @@ public virtual void Can_read_write_nullable_byte_as_string_JSON_values(byte? val [InlineData((ushort)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ushort_as_string_JSON_values(ushort? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.UInt16)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableUInt16Type.UInt16), value, json); [ConditionalTheory] [InlineData(uint.MinValue, """{"Prop":"0"}""")] @@ -687,8 +969,9 @@ public virtual void Can_read_write_nullable_ushort_as_string_JSON_values(ushort? [InlineData((uint)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_uint_as_string_JSON_values(uint? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.UInt32)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableUInt32Type.UInt32), value, json); [ConditionalTheory] [InlineData(ulong.MinValue, """{"Prop":"0"}""")] @@ -696,8 +979,9 @@ public virtual void Can_read_write_nullable_uint_as_string_JSON_values(uint? val [InlineData((ulong)1, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ulong_as_string_JSON_values(ulong? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.UInt64)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableUInt64Type.UInt64), value, json); [ConditionalTheory] [InlineData(float.MinValue, """{"Prop":"-3.4028235E\u002B38"}""")] @@ -706,8 +990,9 @@ public virtual void Can_read_write_nullable_ulong_as_string_JSON_values(ulong? v [InlineData((float)1.1, """{"Prop":"1.1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_float_as_string_JSON_values(float? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Float)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableFloatType.Float), value, json); [ConditionalTheory] [InlineData(double.MinValue, """{"Prop":"-1.7976931348623157E\u002B308"}""")] @@ -716,8 +1001,9 @@ public virtual void Can_read_write_nullable_float_as_string_JSON_values(float? v [InlineData(1.1, """{"Prop":"1.1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_double_as_string_JSON_values(double? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Double)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableDoubleType.Double), value, json); [ConditionalTheory] [InlineData("-79228162514264337593543950335", """{"Prop":"-79228162514264337593543950335"}""")] @@ -726,8 +1012,9 @@ public virtual void Can_read_write_nullable_double_as_string_JSON_values(double? [InlineData("1.1", """{"Prop":"1.1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_decimal_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Decimal)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableDecimalType.Decimal), value == null ? default(decimal?) : decimal.Parse(value, CultureInfo.InvariantCulture), json); [ConditionalTheory] @@ -736,8 +1023,9 @@ public virtual void Can_read_write_nullable_decimal_as_string_JSON_values(string [InlineData("5/29/2023", """{"Prop":"2023-05-29"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_DateOnly_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.DateOnly)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableDateOnlyType.DateOnly), value == null ? default(DateOnly?) : DateOnly.Parse(value, CultureInfo.InvariantCulture), json); [ConditionalTheory] @@ -746,8 +1034,9 @@ public virtual void Can_read_write_nullable_DateOnly_as_string_JSON_values(strin [InlineData("11:05:12.3456789", """{"Prop":"11:05:12.3456789"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_TimeOnly_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.TimeOnly)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableTimeOnlyType.TimeOnly), value == null ? default(TimeOnly?) : TimeOnly.Parse(value, CultureInfo.InvariantCulture), json); [ConditionalTheory] @@ -756,8 +1045,9 @@ public virtual void Can_read_write_nullable_TimeOnly_as_string_JSON_values(strin [InlineData("2023-05-29T10:52:47.2064353", """{"Prop":"2023-05-29 10:52:47.2064353"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_DateTime_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.DateTime)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableDateTimeType.DateTime), value == null ? default(DateTime?) : DateTime.Parse(value, CultureInfo.InvariantCulture), json); [ConditionalTheory] @@ -767,8 +1057,9 @@ public virtual void Can_read_write_nullable_DateTime_as_string_JSON_values(strin [InlineData("2023-05-29T11:11:15.5672854+04:00", """{"Prop":"2023-05-29 11:11:15.5672854\u002B04:00"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_DateTimeOffset_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.DateTimeOffset)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableDateTimeOffsetType.DateTimeOffset), value == null ? default(DateTimeOffset?) : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); [ConditionalTheory] @@ -778,8 +1069,9 @@ public virtual void Can_read_write_nullable_DateTimeOffset_as_string_JSON_values [InlineData("12:23:23.8018854", """{"Prop":"12:23:23.8018854"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_TimeSpan_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.TimeSpan)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableTimeSpanType.TimeSpan), value == null ? default(TimeSpan?) : TimeSpan.Parse(value), json); [ConditionalTheory] @@ -787,8 +1079,9 @@ public virtual void Can_read_write_nullable_TimeSpan_as_string_JSON_values(strin [InlineData(true, """{"Prop":"1"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_bool_as_string_JSON_values(bool? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Boolean)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableBooleanType.Boolean), value, json); [ConditionalTheory] [InlineData(char.MinValue, """{"Prop":"\u0000"}""")] @@ -797,8 +1090,9 @@ public virtual void Can_read_write_nullable_bool_as_string_JSON_values(bool? val [InlineData('Z', """{"Prop":"Z"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_char_as_string_JSON_values(char? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Character)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableCharacterType.Character), value, json); [ConditionalTheory] [InlineData("00000000-0000-0000-0000-000000000000", """{"Prop":"00000000-0000-0000-0000-000000000000"}""")] @@ -806,8 +1100,9 @@ public virtual void Can_read_write_nullable_char_as_string_JSON_values(char? val [InlineData("8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD", """{"Prop":"8c44242f-8e3f-4a20-8be8-98c7c1aadebd"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_as_string_GUID_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Guid)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableGuidType.Guid), value == null ? default(Guid?) : Guid.Parse(value, CultureInfo.InvariantCulture), json); [ConditionalTheory] @@ -819,8 +1114,9 @@ public virtual void Can_read_write_nullable_as_string_GUID_JSON_values(string? v @"{""Prop"":""\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A""}")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_string_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.String)), value, json); + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableStringType.String), value, json); [ConditionalTheory] [InlineData("0,0,0,1", """{"Prop":"AAAAAQ=="}""")] @@ -829,8 +1125,9 @@ public virtual void Can_read_write_nullable_string_as_string_JSON_values(string? [InlineData("1,2,3,4", """{"Prop":"AQIDBA=="}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_binary_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Bytes)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableBytesType.Bytes), value == null ? default : value == "" @@ -844,8 +1141,9 @@ public virtual void Can_read_write_nullable_binary_as_string_JSON_values(string? [InlineData("file:///C:/test/path/file.txt", """{"Prop":"file:///C:/test/path/file.txt"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_URI_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Uri)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableUriType.Uri), value == null ? default : new Uri(value), json); [ConditionalTheory] @@ -858,8 +1156,9 @@ public virtual void Can_read_write_nullable_URI_as_string_JSON_values(string? va [InlineData("2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577", """{"Prop":"2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_IP_address_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.IpAddress)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableIPAddressType.IpAddress), value == null ? default : IPAddress.Parse(value), json); [ConditionalTheory] @@ -868,8 +1167,9 @@ public virtual void Can_read_write_nullable_IP_address_as_string_JSON_values(str [InlineData("0011.2233.4455", """{"Prop":"001122334455"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_physical_address_as_string_JSON_values(string? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.PhysicalAddress)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullablePhysicalAddressType.PhysicalAddress), value == null ? default : PhysicalAddress.Parse(value), json); [ConditionalTheory] @@ -880,8 +1180,9 @@ public virtual void Can_read_write_nullable_physical_address_as_string_JSON_valu [InlineData((sbyte)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_sbyte_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Enum8)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnum8Type.Enum8), value == null ? default(Enum8?) : (Enum8)value, json); [ConditionalTheory] @@ -892,8 +1193,9 @@ public virtual void Can_read_write_nullable_sbyte_enum_as_string_JSON_values(obj [InlineData((short)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_short_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Enum16)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnum16Type.Enum16), value == null ? default(Enum16?) : (Enum16)value, json); [ConditionalTheory] @@ -904,8 +1206,9 @@ public virtual void Can_read_write_nullable_short_enum_as_string_JSON_values(obj [InlineData(77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_int_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Enum32)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnum32Type.Enum32), value == null ? default(Enum32?) : (Enum32)value, json); [ConditionalTheory] @@ -916,8 +1219,9 @@ public virtual void Can_read_write_nullable_int_enum_as_string_JSON_values(objec [InlineData((long)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_long_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.Enum64)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnum64Type.Enum64), value == null ? default(Enum64?) : (Enum64)value, json); [ConditionalTheory] @@ -927,8 +1231,9 @@ public virtual void Can_read_write_nullable_long_enum_as_string_JSON_values(obje [InlineData((byte)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_byte_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.EnumU8)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnumU8Type.EnumU8), value == null ? default(EnumU8?) : (EnumU8)value, json); [ConditionalTheory] @@ -938,8 +1243,9 @@ public virtual void Can_read_write_nullable_byte_enum_as_string_JSON_values(obje [InlineData((ushort)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ushort_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.EnumU16)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnumU16Type.EnumU16), value == null ? default(EnumU16?) : (EnumU16)value, json); [ConditionalTheory] @@ -949,8 +1255,9 @@ public virtual void Can_read_write_nullable_ushort_enum_as_string_JSON_values(ob [InlineData((uint)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_uint_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.EnumU32)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnumU32Type.EnumU32), value == null ? default(EnumU32?) : (EnumU32)value, json); [ConditionalTheory] @@ -960,67 +1267,122 @@ public virtual void Can_read_write_nullable_uint_enum_as_string_JSON_values(obje [InlineData((ulong)77, """{"Prop":"77"}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_ulong_enum_as_string_JSON_values(object? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(TypesAsStrings.EnumU64)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion(), + nameof(NullableEnumU64Type.EnumU64), value == null ? default(EnumU64?) : (EnumU64)value, json); [ConditionalFact] public virtual void Can_read_write_point() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.Point)), + Can_read_and_write_JSON_value( + nameof(PointType.Point), factory.CreatePoint(new Coordinate(2, 4)), """{"Prop":"POINT (2 4)"}"""); + } + + public class PointType + { + public Point? Point { get; set; } + } + + [ConditionalFact] + public virtual void Can_read_write_nullable_point() + => Can_read_and_write_JSON_value( + nameof(NullablePointType.Point), + null, + """{"Prop":null}"""); + + public class NullablePointType + { + public Point? Point { get; set; } + } + + [ConditionalFact] + public virtual void Can_read_write_point_with_Z() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.PointZ)), + Can_read_and_write_JSON_value( + nameof(PointZType.PointZ), factory.CreatePoint(new CoordinateZ(2, 4, 6)), """{"Prop":"POINT Z(2 4 6)"}"""); + } + + public class PointZType + { + public Point PointZ { get; set; } = null!; + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.PointM)), + [ConditionalFact] + public virtual void Can_read_write_point_with_M() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + Can_read_and_write_JSON_value( + nameof(PointMType.PointM), factory.CreatePoint(new CoordinateM(2, 4, 6)), """{"Prop":"POINT (2 4)"}"""); + } + + public class PointMType + { + public Point PointM { get; set; } = null!; + } + + [ConditionalFact] + public virtual void Can_read_write_point_with_Z_and_M() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.PointZM)), + Can_read_and_write_JSON_value( + nameof(PointZMType.PointZM), factory.CreatePoint(new CoordinateZM(1, 2, 3, 4)), """{"Prop":"POINT Z(1 2 3)"}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.Point)), - null, - """{"Prop":null}"""); + public class PointZMType + { + public Point PointZM { get; set; } = null!; } [ConditionalFact] public virtual void Can_read_write_line_string() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.LineString)), + Can_read_and_write_JSON_value( + nameof(LineStringType.LineString), factory.CreateLineString(new[] { new Coordinate(0, 0), new Coordinate(1, 0) }), """{"Prop":"LINESTRING (0 0, 1 0)"}"""); + } + + public class LineStringType + { + public LineString? LineString { get; set; } + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.LineString)), + [ConditionalFact] + public virtual void Can_read_write_nullable_line_string() + => Can_read_and_write_JSON_value( + nameof(NullableLineStringType.LineString), null, """{"Prop":null}"""); + + public class NullableLineStringType + { + public LineString? LineString { get; set; } } [ConditionalFact] public virtual void Can_read_write_multi_line_string() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.MultiLineString)), + Can_read_and_write_JSON_value( + nameof(MultiLineStringType.MultiLineString), factory.CreateMultiLineString( new[] { @@ -1030,104 +1392,165 @@ public virtual void Can_read_write_multi_line_string() new[] { new Coordinate(1, 0), new Coordinate(1, 1) }) }), """{"Prop":"MULTILINESTRING ((0 0, 0 1), (1 0, 1 1))"}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.MultiLineString)), + public class MultiLineStringType + { + public MultiLineString MultiLineString { get; set; } = null!; + } + + [ConditionalFact] + public virtual void Can_read_write_nullable_multi_line_string() + => Can_read_and_write_JSON_value( + nameof(NullableMultiLineStringType.MultiLineString), null, """{"Prop":null}"""); + + public class NullableMultiLineStringType + { + public MultiLineString? MultiLineString { get; set; } } [ConditionalFact] public virtual void Can_read_write_polygon() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.Polygon)), + Can_read_and_write_JSON_value( + nameof(PolygonType.Polygon), factory.CreatePolygon(new[] { new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0) }), """{"Prop":"POLYGON ((0 0, 1 0, 0 1, 0 0))"}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.Polygon)), + public class PolygonType + { + public Polygon Polygon { get; set; } = null!; + } + + [ConditionalFact] + public virtual void Can_read_write_nullable_polygon() + => Can_read_and_write_JSON_value( + nameof(NullablePolygonType.Polygon), null, """{"Prop":null}"""); + + public class NullablePolygonType + { + public Polygon? Polygon { get; set; } } [ConditionalFact] public virtual void Can_read_write_polygon_typed_as_geometry() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.Geometry)), + Can_read_and_write_JSON_value( + nameof(GeometryType.Geometry), factory.CreatePolygon(new[] { new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0) }), """{"Prop":"POLYGON ((0 0, 1 0, 0 1, 0 0))"}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypes.Geometry)), + public class GeometryType + { + public Geometry Geometry { get; set; } = null!; + } + + [ConditionalFact] + public virtual void Can_read_write_polygon_typed_as_nullable_geometry() + => Can_read_and_write_JSON_value( + nameof(NullableGeometryType.Geometry), null, """{"Prop":null}"""); + + public class NullableGeometryType + { + public Geometry? Geometry { get; set; } } [ConditionalFact] public virtual void Can_read_write_point_as_GeoJson() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.Point)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(PointType.Point), factory.CreatePoint(new Coordinate(2, 4)), """{"Prop":{"type":"Point","coordinates":[2.0,4.0]}}"""); + } + + [ConditionalFact] + public virtual void Can_read_write_nullable_point_as_GeoJson() + => Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(NullablePointType.Point), + null, + """{"Prop":null}"""); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.PointZ)), + [ConditionalFact] + public virtual void Can_read_write_point_with_Z_as_GeoJson() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); + + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(PointZType.PointZ), factory.CreatePoint(new CoordinateZ(2, 4, 6)), """{"Prop":{"type":"Point","coordinates":[2.0,4.0]}}"""); + } + + [ConditionalFact] + public virtual void Can_read_write_point_with_M_as_GeoJson() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.PointM)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(PointMType.PointM), factory.CreatePoint(new CoordinateM(2, 4, 6)), """{"Prop":{"type":"Point","coordinates":[2.0,4.0]}}"""); + } + + [ConditionalFact] + public virtual void Can_read_write_point_with_Z_and_M_as_GeoJson() + { + var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.PointZM)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(PointZMType.PointZM), factory.CreatePoint(new CoordinateZM(1, 2, 3, 4)), """{"Prop":{"type":"Point","coordinates":[1.0,2.0]}}"""); - - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.Point)), - null, - """{"Prop":null}"""); } [ConditionalFact] public virtual void Can_read_write_line_string_as_GeoJson() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.LineString)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(LineStringType.LineString), factory.CreateLineString(new[] { new Coordinate(0, 0), new Coordinate(1, 0) }), """{"Prop":{"type":"LineString","coordinates":[[0.0,0.0],[1.0,0.0]]}}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.LineString)), + [ConditionalFact] + public virtual void Can_read_write_nullable_line_string_as_GeoJson() + => Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(NullableLineStringType.LineString), null, """{"Prop":null}"""); - } [ConditionalFact] public virtual void Can_read_write_multi_line_string_as_GeoJson() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.MultiLineString)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(MultiLineStringType.MultiLineString), factory.CreateMultiLineString( new[] { @@ -1137,46 +1560,55 @@ public virtual void Can_read_write_multi_line_string_as_GeoJson() new[] { new Coordinate(1, 0), new Coordinate(1, 1) }) }), """{"Prop":{"type":"MultiLineString","coordinates":[[[0.0,0.0],[0.0,1.0]],[[1.0,0.0],[1.0,1.0]]]}}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.MultiLineString)), + [ConditionalFact] + public virtual void Can_read_write_nullable_multi_line_string_as_GeoJson() + => Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(NullableMultiLineStringType.MultiLineString), null, """{"Prop":null}"""); - } [ConditionalFact] public virtual void Can_read_write_polygon_as_GeoJson() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.Polygon)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(PolygonType.Polygon), factory.CreatePolygon(new[] { new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0) }), """{"Prop":{"type":"Polygon","coordinates":[[[0.0,0.0],[1.0,0.0],[0.0,1.0],[0.0,0.0]]]}}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.Polygon)), + [ConditionalFact] + public virtual void Can_read_write_nullable_polygon_as_GeoJson() + => Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(NullablePolygonType.Polygon), null, """{"Prop":null}"""); - } [ConditionalFact] public virtual void Can_read_write_polygon_typed_as_geometry_as_GeoJson() { var factory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); - var entityType = Fixture.EntityType(); - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.Geometry)), + Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(GeometryType.Geometry), factory.CreatePolygon(new[] { new Coordinate(0, 0), new Coordinate(1, 0), new Coordinate(0, 1), new Coordinate(0, 0) }), """{"Prop":{"type":"Polygon","coordinates":[[[0.0,0.0],[1.0,0.0],[0.0,1.0],[0.0,0.0]]]}}"""); + } - Can_read_and_write_JSON_value( - entityType.GetProperty(nameof(GeometryTypesAsGeoJson.Geometry)), + [ConditionalFact] + public virtual void Can_read_write_polygon_typed_as_nullable_geometry_as_GeoJson() + => Can_read_and_write_JSON_property_value( + b => b.Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)), + nameof(NullableGeometryType.Geometry), null, """{"Prop":null}"""); - } [ConditionalTheory] [InlineData(int.MinValue, """{"Prop":-2147483648}""")] @@ -1184,8 +1616,16 @@ public virtual void Can_read_write_polygon_typed_as_geometry_as_GeoJson() [InlineData(0, """{"Prop":0}""")] [InlineData(1, """{"Prop":1}""")] public virtual void Can_read_write_converted_type_JSON_values(int value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(Types.DddId)), new DddId { Id = value }, json); + => Can_read_and_write_JSON_value( + b => b.Entity().HasNoKey().Property(e => e.DddId), + b => b.Properties().HaveConversion(), + nameof(DddIdType.DddId), + new DddId { Id = value }, json); + + protected class DddIdType + { + public DddId DddId { get; set; } + } [ConditionalTheory] [InlineData(int.MinValue, """{"Prop":-2147483648}""")] @@ -1194,98 +1634,139 @@ public virtual void Can_read_write_converted_type_JSON_values(int value, string [InlineData(1, """{"Prop":1}""")] [InlineData(null, """{"Prop":null}""")] public virtual void Can_read_write_nullable_converted_type_JSON_values(int? value, string json) - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullableTypes.DddId)), - value == null ? default(DddId?) : new DddId { Id = value.Value }, json); + => Can_read_and_write_JSON_value( + b => b.Entity().HasNoKey().Property(e => e.DddId), + b => b.Properties().HaveConversion(), + nameof(NullableDddIdType.DddId), + value == null ? null : new DddId { Id = value.Value }, json); + + protected class NullableDddIdType + { + public DddId? DddId { get; set; } + } [ConditionalFact] public virtual void Can_read_write_collection_of_sbyte_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Int8)), + => Can_read_and_write_JSON_value>(nameof(Int8CollectionType.Int8), new List { sbyte.MinValue, 0, sbyte.MaxValue }, - """{"Prop":[-128,0,127]}"""); + """{"Prop":[-128,0,127]}""", + mappedCollection: true); + + protected class Int8CollectionType + { + public sbyte[] Int8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_short_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Int16)), + => Can_read_and_write_JSON_value>(nameof(Int16CollectionType.Int16), new List { short.MinValue, 0, short.MaxValue }, - """{"Prop":[-32768,0,32767]}"""); + """{"Prop":[-32768,0,32767]}""", + mappedCollection: true); + + protected class Int16CollectionType + { + public IList Int16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_int_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Int32)), + => Can_read_and_write_JSON_value>(nameof(Int32CollectionType.Int32), new List { int.MinValue, 0, int.MaxValue }, - """{"Prop":[-2147483648,0,2147483647]}"""); + """{"Prop":[-2147483648,0,2147483647]}""", + mappedCollection: true); + + protected class Int32CollectionType + { + public List Int32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_long_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Int64)), + => Can_read_and_write_JSON_value>(nameof(Int64CollectionType.Int64), new List { long.MinValue, 0, long.MaxValue }, - """{"Prop":[-9223372036854775808,0,9223372036854775807]}"""); + """{"Prop":[-9223372036854775808,0,9223372036854775807]}""", + mappedCollection: true); + + protected class Int64CollectionType + { + public IList Int64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_byte_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.UInt8)), + => Can_read_and_write_JSON_value>(nameof(UInt8CollectionType.UInt8), new List { byte.MinValue, 1, byte.MaxValue }, - """{"Prop":[0,1,255]}"""); + """{"Prop":[0,1,255]}""", + mappedCollection: true); + + protected class UInt8CollectionType + { + public List UInt8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_ushort_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.UInt16)), + => Can_read_and_write_JSON_value>(nameof(UInt16CollectionType.UInt16), new List { ushort.MinValue, 1, ushort.MaxValue }, - """{"Prop":[0,1,65535]}"""); + """{"Prop":[0,1,65535]}""", + mappedCollection: true); + + protected class UInt16CollectionType + { + public Collection UInt16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_uint_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.UInt32)), + => Can_read_and_write_JSON_value>(nameof(UInt32CollectionType.UInt32), new List { uint.MinValue, 1, uint.MaxValue }, - """{"Prop":[0,1,4294967295]}"""); + """{"Prop":[0,1,4294967295]}""", + mappedCollection: true); + + protected class UInt32CollectionType + { + public List UInt32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_ulong_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.UInt64)), + => Can_read_and_write_JSON_value>(nameof(UInt64CollectionType.UInt64), new List { ulong.MinValue, @@ -1293,60 +1774,85 @@ public virtual void Can_read_write_collection_of_ulong_JSON_values() ulong.MaxValue }, """{"Prop":[0,1,18446744073709551615]}""", + mappedCollection: true, new ObservableCollection()); + protected class UInt64CollectionType + { + public ObservableCollection UInt64 { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_float_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Float)), + => Can_read_and_write_JSON_value>(nameof(FloatCollectionType.Float), new List { float.MinValue, 0, float.MaxValue }, - """{"Prop":[-3.4028235E+38,0,3.4028235E+38]}"""); + """{"Prop":[-3.4028235E+38,0,3.4028235E+38]}""", + mappedCollection: true); + + protected class FloatCollectionType + { + public float[] Float { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_double_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Double)), + => Can_read_and_write_JSON_value>(nameof(DoubleCollectionType.Double), new List { double.MinValue, 0, double.MaxValue }, - """{"Prop":[-1.7976931348623157E+308,0,1.7976931348623157E+308]}"""); + """{"Prop":[-1.7976931348623157E+308,0,1.7976931348623157E+308]}""", + mappedCollection: true); + + protected class DoubleCollectionType + { + public double[] Double { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_decimal_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Decimal)), + => Can_read_and_write_JSON_value>(nameof(DecimalCollectionType.Decimal), new List { decimal.MinValue, 0, decimal.MaxValue }, - """{"Prop":[-79228162514264337593543950335,0,79228162514264337593543950335]}"""); + """{"Prop":[-79228162514264337593543950335,0,79228162514264337593543950335]}""", + mappedCollection: true); + + protected class DecimalCollectionType + { + public decimal[] Decimal { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_DateOnly_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.DateOnly)), + => Can_read_and_write_JSON_value>(nameof(DateOnlyCollectionType.DateOnly), new List { DateOnly.MinValue, new(2023, 5, 29), DateOnly.MaxValue }, - """{"Prop":["0001-01-01","2023-05-29","9999-12-31"]}"""); + """{"Prop":["0001-01-01","2023-05-29","9999-12-31"]}""", + mappedCollection: true); + + protected class DateOnlyCollectionType + { + public IList DateOnly { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_TimeOnly_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.TimeOnly)), + => Can_read_and_write_JSON_value>(nameof(TimeOnlyCollectionType.TimeOnly), new List { TimeOnly.MinValue, @@ -1354,24 +1860,35 @@ public virtual void Can_read_write_collection_of_TimeOnly_JSON_values() TimeOnly.MaxValue }, """{"Prop":["00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}""", + mappedCollection: true, new List()); + protected class TimeOnlyCollectionType + { + public IList TimeOnly { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_DateTime_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.DateTime)), + => Can_read_and_write_JSON_value>(nameof(DateTimeCollectionType.DateTime), new List { DateTime.MinValue, new(2023, 5, 29, 10, 52, 47), DateTime.MaxValue }, - """{"Prop":["0001-01-01T00:00:00","2023-05-29T10:52:47","9999-12-31T23:59:59.9999999"]}"""); + """{"Prop":["0001-01-01T00:00:00","2023-05-29T10:52:47","9999-12-31T23:59:59.9999999"]}""", + mappedCollection: true); + + protected class DateTimeCollectionType + { + public IList DateTime { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_DateTimeOffset_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.DateTimeOffset)), + => Can_read_and_write_JSON_value>( + nameof(DateTimeOffsetCollectionType.DateTimeOffset), new List { DateTimeOffset.MinValue, @@ -1380,67 +1897,98 @@ public virtual void Can_read_write_collection_of_DateTimeOffset_JSON_values() new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(2, 0, 0)), DateTimeOffset.MaxValue }, - """{"Prop":["0001-01-01T00:00:00+00:00","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47+00:00","2023-05-29T10:52:47+02:00","9999-12-31T23:59:59.9999999+00:00"]}"""); + """{"Prop":["0001-01-01T00:00:00+00:00","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47+00:00","2023-05-29T10:52:47+02:00","9999-12-31T23:59:59.9999999+00:00"]}""", + mappedCollection: true); + + protected class DateTimeOffsetCollectionType + { + public IList DateTimeOffset { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_TimeSpan_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.TimeSpan)), + => Can_read_and_write_JSON_value>(nameof(TimeSpanCollectionType.TimeSpan), new List { TimeSpan.MinValue, new(1, 2, 3, 4, 5), TimeSpan.MaxValue }, - """{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807"]}"""); + """{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807"]}""", + mappedCollection: true); + + protected class TimeSpanCollectionType + { + public IList TimeSpan { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_bool_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Boolean)), + => Can_read_and_write_JSON_value>( + nameof(BooleanCollectionType.Boolean), new List { false, true }, - """{"Prop":[false,true]}"""); + """{"Prop":[false,true]}""", + mappedCollection: true); + + protected class BooleanCollectionType + { + public IList Boolean { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_char_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Character)), + => Can_read_and_write_JSON_value>(nameof(CharacterCollectionType.Character), new List { char.MinValue, 'X', char.MaxValue }, - """{"Prop":["\u0000","X","\uFFFF"]}"""); + """{"Prop":["\u0000","X","\uFFFF"]}""", + mappedCollection: true); + + protected class CharacterCollectionType + { + public IList Character { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_GUID_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Guid)), + => Can_read_and_write_JSON_value>(nameof(GuidCollectionType.Guid), new List { new(), new("8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD"), Guid.Parse("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") }, - """{"Prop":["00000000-0000-0000-0000-000000000000","8c44242f-8e3f-4a20-8be8-98c7c1aadebd","ffffffff-ffff-ffff-ffff-ffffffffffff"]}"""); + """{"Prop":["00000000-0000-0000-0000-000000000000","8c44242f-8e3f-4a20-8be8-98c7c1aadebd","ffffffff-ffff-ffff-ffff-ffffffffffff"]}""", + mappedCollection: true); + + protected class GuidCollectionType + { + public IList Guid { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_string_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.String)), + => Can_read_and_write_JSON_value>(nameof(StringCollectionType.String), new List { "MinValue", "❤❥웃유♋☮✌☏☢☠✔☑♚▲♪฿Ɖ⛏♥❣♂♀☿👍✍✉☣☤✘☒♛▼♫⌘⌛¡♡ღツ☼☁❅♾️✎©®™Σ✪✯☭➳Ⓐ✞℃℉°✿⚡☃☂✄¢€£∞✫★½☯✡☪", "MaxValue" }, - """{"Prop":["MinValue","\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A","MaxValue"]}"""); + """{"Prop":["MinValue","\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A","MaxValue"]}""", + mappedCollection: true); + + protected class StringCollectionType + { + public IList String { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_binary_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Bytes)), + => Can_read_and_write_JSON_value>(nameof(BytesCollectionType.Bytes), new List { new byte[] { 0, 0, 0, 1 }, @@ -1448,23 +1996,33 @@ public virtual void Can_read_write_collection_of_binary_JSON_values() Array.Empty(), new byte[] { 1, 2, 3, 4 } }, - """{"Prop":["AAAAAQ==","/////w==","","AQIDBA=="]}"""); + """{"Prop":["AAAAAQ==","/////w==","","AQIDBA=="]}""", + mappedCollection: true); + + protected class BytesCollectionType + { + public IList Bytes { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_URI_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Uri)), + => Can_read_and_write_JSON_value>(nameof(UriCollectionType.Uri), new List { new("https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName"), new("file:///C:/test/path/file.txt") }, - """{"Prop":["https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1\u0026q2=v2#FragmentName","file:///C:/test/path/file.txt"]}"""); + """{"Prop":["https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1\u0026q2=v2#FragmentName","file:///C:/test/path/file.txt"]}""", + mappedCollection: true); + + protected class UriCollectionType + { + public List Uri { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_IP_address_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.IpAddress)), + => Can_read_and_write_JSON_value>(nameof(IpAddressCollectionType.IpAddress), new List { IPAddress.Parse("127.0.0.1"), @@ -1475,12 +2033,18 @@ public virtual void Can_read_write_collection_of_IP_address_JSON_values() IPAddress.Parse("::"), IPAddress.Parse("2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"), }, - """{"Prop":["127.0.0.1","0.0.0.0","255.255.255.255","192.168.1.156","::1","::","2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"]}"""); + """{"Prop":["127.0.0.1","0.0.0.0","255.255.255.255","192.168.1.156","::1","::","2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"]}""", + mappedCollection: true); + + protected class IpAddressCollectionType + { + public List IpAddress { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_physical_address_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.PhysicalAddress)), + => Can_read_and_write_JSON_value>( + nameof(PhysicalAddressCollectionType.PhysicalAddress), new List { PhysicalAddress.None, @@ -1488,12 +2052,17 @@ public virtual void Can_read_write_collection_of_physical_address_JSON_values() PhysicalAddress.Parse("00-11-22-33-44-55"), PhysicalAddress.Parse("0011.2233.4455") }, - """{"Prop":["","001122334455","001122334455","001122334455"]}"""); + """{"Prop":["","001122334455","001122334455","001122334455"]}""", + mappedCollection: true); + + protected class PhysicalAddressCollectionType + { + public List PhysicalAddress { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_sbyte_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Enum8)), + => Can_read_and_write_JSON_value>(nameof(Enum8CollectionType.Enum8), new List { Enum8.Min, @@ -1502,12 +2071,17 @@ public virtual void Can_read_write_collection_of_sbyte_enum_JSON_values() Enum8.One, (Enum8)(-8) }, - """{"Prop":[-128,127,0,1,-8]}"""); + """{"Prop":[-128,127,0,1,-8]}""", + mappedCollection: true); + + protected class Enum8CollectionType + { + public List Enum8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_short_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Enum16)), + => Can_read_and_write_JSON_value>(nameof(Enum16CollectionType.Enum16), new List { Enum16.Min, @@ -1516,12 +2090,17 @@ public virtual void Can_read_write_collection_of_short_enum_JSON_values() Enum16.One, (Enum16)(-8) }, - """{"Prop":[-32768,32767,0,1,-8]}"""); + """{"Prop":[-32768,32767,0,1,-8]}""", + mappedCollection: true); + + protected class Enum16CollectionType + { + public List Enum16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_int_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Enum32)), + => Can_read_and_write_JSON_value>(nameof(Enum32CollectionType.Enum32), new List { Enum32.Min, @@ -1530,12 +2109,17 @@ public virtual void Can_read_write_collection_of_int_enum_JSON_values() Enum32.One, (Enum32)(-8) }, - """{"Prop":[-2147483648,2147483647,0,1,-8]}"""); + """{"Prop":[-2147483648,2147483647,0,1,-8]}""", + mappedCollection: true); + + protected class Enum32CollectionType + { + public List Enum32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_long_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Enum64)), + => Can_read_and_write_JSON_value>(nameof(Enum64CollectionType.Enum64), new List { Enum64.Min, @@ -1544,12 +2128,17 @@ public virtual void Can_read_write_collection_of_long_enum_JSON_values() Enum64.One, (Enum64)(-8) }, - """{"Prop":[-9223372036854775808,9223372036854775807,0,1,-8]}"""); + """{"Prop":[-9223372036854775808,9223372036854775807,0,1,-8]}""", + mappedCollection: true); + + protected class Enum64CollectionType + { + public List Enum64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_byte_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.EnumU8)), + => Can_read_and_write_JSON_value>(nameof(EnumU8CollectionType.EnumU8), new List { EnumU8.Min, @@ -1558,12 +2147,17 @@ public virtual void Can_read_write_collection_of_byte_enum_JSON_values() EnumU8.One, (EnumU8)8 }, - """{"Prop":[0,255,0,1,8]}"""); + """{"Prop":[0,255,0,1,8]}""", + mappedCollection: true); + + protected class EnumU8CollectionType + { + public IList EnumU8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_ushort_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.EnumU16)), + => Can_read_and_write_JSON_value>(nameof(EnumU16CollectionType.EnumU16), new List { EnumU16.Min, @@ -1572,12 +2166,17 @@ public virtual void Can_read_write_collection_of_ushort_enum_JSON_values() EnumU16.One, (EnumU16)8 }, - """{"Prop":[0,65535,0,1,8]}"""); + """{"Prop":[0,65535,0,1,8]}""", + mappedCollection: true); + + protected class EnumU16CollectionType + { + public IList EnumU16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_uint_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.EnumU32)), + => Can_read_and_write_JSON_value>(nameof(EnumU32CollectionType.EnumU32), new List { EnumU32.Min, @@ -1587,12 +2186,17 @@ public virtual void Can_read_write_collection_of_uint_enum_JSON_values() (EnumU32)8 }, """{"Prop":[0,4294967295,0,1,8]}""", + mappedCollection: true, new ObservableCollection()); + protected class EnumU32CollectionType + { + public IList EnumU32 { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.EnumU64)), + => Can_read_and_write_JSON_value>(nameof(EnumU64CollectionType.EnumU64), new List { EnumU64.Min, @@ -1601,12 +2205,17 @@ public virtual void Can_read_write_collection_of_ulong_enum_JSON_values() EnumU64.One, (EnumU64)8 }, - """{"Prop":[0,18446744073709551615,0,1,8]}"""); + """{"Prop":[0,18446744073709551615,0,1,8]}""", + mappedCollection: true); + + protected class EnumU64CollectionType + { + public IList EnumU64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_sbyte_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Int8)), + => Can_read_and_write_JSON_value>(nameof(NullableInt8CollectionType.Int8), new List { null, @@ -1614,25 +2223,35 @@ public virtual void Can_read_write_collection_of_nullable_sbyte_JSON_values() 0, sbyte.MaxValue }, - """{"Prop":[null,-128,0,127]}"""); + """{"Prop":[null,-128,0,127]}""", + mappedCollection: true); - [ConditionalFact] - public virtual void Can_read_write_collection_of_nullable_short_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Int16)), - new List + protected class NullableInt8CollectionType + { + public sbyte?[] Int8 { get; set; } = null!; + } + + [ConditionalFact] + public virtual void Can_read_write_collection_of_nullable_short_JSON_values() + => Can_read_and_write_JSON_value>(nameof(NullableInt16CollectionType.Int16), + new List { short.MinValue, null, 0, short.MaxValue }, - """{"Prop":[-32768,null,0,32767]}"""); + """{"Prop":[-32768,null,0,32767]}""", + mappedCollection: true); + + protected class NullableInt16CollectionType + { + public IList Int16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_int_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Int32)), + => Can_read_and_write_JSON_value>(nameof(NullableInt32CollectionType.Int32), new List { int.MinValue, @@ -1640,12 +2259,17 @@ public virtual void Can_read_write_collection_of_nullable_int_JSON_values() null, int.MaxValue }, - """{"Prop":[-2147483648,0,null,2147483647]}"""); + """{"Prop":[-2147483648,0,null,2147483647]}""", + mappedCollection: true); + + protected class NullableInt32CollectionType + { + public List Int32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_long_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Int64)), + => Can_read_and_write_JSON_value>(nameof(NullableInt64CollectionType.Int64), new List { long.MinValue, @@ -1653,12 +2277,17 @@ public virtual void Can_read_write_collection_of_nullable_long_JSON_values() long.MaxValue, null }, - """{"Prop":[-9223372036854775808,0,9223372036854775807,null]}"""); + """{"Prop":[-9223372036854775808,0,9223372036854775807,null]}""", + mappedCollection: true); + + protected class NullableInt64CollectionType + { + public IList Int64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_byte_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.UInt8)), + => Can_read_and_write_JSON_value>(nameof(NullableUInt8CollectionType.UInt8), new List { null, @@ -1666,12 +2295,17 @@ public virtual void Can_read_write_collection_of_nullable_byte_JSON_values() 1, byte.MaxValue }, - """{"Prop":[null,0,1,255]}"""); + """{"Prop":[null,0,1,255]}""", + mappedCollection: true); + + protected class NullableUInt8CollectionType + { + public List UInt8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_ushort_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.UInt16)), + => Can_read_and_write_JSON_value>(nameof(NullableUInt16CollectionType.UInt16), new List { ushort.MinValue, @@ -1679,12 +2313,17 @@ public virtual void Can_read_write_collection_of_nullable_ushort_JSON_values() 1, ushort.MaxValue }, - """{"Prop":[0,null,1,65535]}"""); + """{"Prop":[0,null,1,65535]}""", + mappedCollection: true); + + protected class NullableUInt16CollectionType + { + public Collection UInt16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_uint_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.UInt32)), + => Can_read_and_write_JSON_value>(nameof(NullableUInt32CollectionType.UInt32), new List { uint.MinValue, @@ -1692,12 +2331,17 @@ public virtual void Can_read_write_collection_of_nullable_uint_JSON_values() null, uint.MaxValue }, - """{"Prop":[0,1,null,4294967295]}"""); + """{"Prop":[0,1,null,4294967295]}""", + mappedCollection: true); + + protected class NullableUInt32CollectionType + { + public List UInt32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_ulong_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.UInt64)), + => Can_read_and_write_JSON_value>(nameof(NullableUInt64CollectionType.UInt64), new List { ulong.MinValue, @@ -1705,12 +2349,17 @@ public virtual void Can_read_write_collection_of_nullable_ulong_JSON_values() ulong.MaxValue, null }, - """{"Prop":[0,1,18446744073709551615,null]}"""); + """{"Prop":[0,1,18446744073709551615,null]}""", + mappedCollection: true); + + protected class NullableUInt64CollectionType + { + public ObservableCollection UInt64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_float_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Float)), + => Can_read_and_write_JSON_value>(nameof(NullableFloatCollectionType.Float), new List { null, @@ -1718,12 +2367,17 @@ public virtual void Can_read_write_collection_of_nullable_float_JSON_values() 0, float.MaxValue }, - """{"Prop":[null,-3.4028235E+38,0,3.4028235E+38]}"""); + """{"Prop":[null,-3.4028235E+38,0,3.4028235E+38]}""", + mappedCollection: true); + + protected class NullableFloatCollectionType + { + public float?[] Float { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_double_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Double)), + => Can_read_and_write_JSON_value>(nameof(NullableDoubleCollectionType.Double), new List { double.MinValue, @@ -1731,12 +2385,17 @@ public virtual void Can_read_write_collection_of_nullable_double_JSON_values() 0, double.MaxValue }, - """{"Prop":[-1.7976931348623157E+308,null,0,1.7976931348623157E+308]}"""); + """{"Prop":[-1.7976931348623157E+308,null,0,1.7976931348623157E+308]}""", + mappedCollection: true); + + protected class NullableDoubleCollectionType + { + public double?[] Double { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_decimal_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Decimal)), + => Can_read_and_write_JSON_value>(nameof(NullableDecimalCollectionType.Decimal), new List { decimal.MinValue, @@ -1744,12 +2403,17 @@ public virtual void Can_read_write_collection_of_nullable_decimal_JSON_values() null, decimal.MaxValue }, - """{"Prop":[-79228162514264337593543950335,0,null,79228162514264337593543950335]}"""); + """{"Prop":[-79228162514264337593543950335,0,null,79228162514264337593543950335]}""", + mappedCollection: true); + + protected class NullableDecimalCollectionType + { + public decimal?[] Decimal { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_DateOnly_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.DateOnly)), + => Can_read_and_write_JSON_value>(nameof(NullableDateOnlyCollectionType.DateOnly), new List { DateOnly.MinValue, @@ -1757,12 +2421,17 @@ public virtual void Can_read_write_collection_of_nullable_DateOnly_JSON_values() DateOnly.MaxValue, null }, - """{"Prop":["0001-01-01","2023-05-29","9999-12-31",null]}"""); + """{"Prop":["0001-01-01","2023-05-29","9999-12-31",null]}""", + mappedCollection: true); + + protected class NullableDateOnlyCollectionType + { + public IList DateOnly { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_TimeOnly_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.TimeOnly)), + => Can_read_and_write_JSON_value>(nameof(NullableTimeOnlyCollectionType.TimeOnly), new List { null, @@ -1770,12 +2439,17 @@ public virtual void Can_read_write_collection_of_nullable_TimeOnly_JSON_values() new(11, 5, 2, 3, 4), TimeOnly.MaxValue }, - """{"Prop":[null,"00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}"""); + """{"Prop":[null,"00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}""", + mappedCollection: true); + + protected class NullableTimeOnlyCollectionType + { + public IList TimeOnly { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_DateTime_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.DateTime)), + => Can_read_and_write_JSON_value>(nameof(NullableDateTimeCollectionType.DateTime), new List { DateTime.MinValue, @@ -1783,12 +2457,18 @@ public virtual void Can_read_write_collection_of_nullable_DateTime_JSON_values() new(2023, 5, 29, 10, 52, 47), DateTime.MaxValue }, - """{"Prop":["0001-01-01T00:00:00",null,"2023-05-29T10:52:47","9999-12-31T23:59:59.9999999"]}"""); + """{"Prop":["0001-01-01T00:00:00",null,"2023-05-29T10:52:47","9999-12-31T23:59:59.9999999"]}""", + mappedCollection: true); + + protected class NullableDateTimeCollectionType + { + public IList DateTime { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_DateTimeOffset_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.DateTimeOffset)), + => Can_read_and_write_JSON_value>( + nameof(NullableDateTimeOffsetCollectionType.DateTimeOffset), new List { DateTimeOffset.MinValue, @@ -1798,12 +2478,17 @@ public virtual void Can_read_write_collection_of_nullable_DateTimeOffset_JSON_va new(new DateTime(2023, 5, 29, 10, 52, 47), new TimeSpan(2, 0, 0)), DateTimeOffset.MaxValue }, - """{"Prop":["0001-01-01T00:00:00+00:00","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47+00:00",null,"2023-05-29T10:52:47+02:00","9999-12-31T23:59:59.9999999+00:00"]}"""); + """{"Prop":["0001-01-01T00:00:00+00:00","2023-05-29T10:52:47-02:00","2023-05-29T10:52:47+00:00",null,"2023-05-29T10:52:47+02:00","9999-12-31T23:59:59.9999999+00:00"]}""", + mappedCollection: true); + + protected class NullableDateTimeOffsetCollectionType + { + public IList DateTimeOffset { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_TimeSpan_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.TimeSpan)), + => Can_read_and_write_JSON_value>(nameof(NullableTimeSpanCollectionType.TimeSpan), new List { TimeSpan.MinValue, @@ -1811,24 +2496,34 @@ public virtual void Can_read_write_collection_of_nullable_TimeSpan_JSON_values() TimeSpan.MaxValue, null }, - """{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807",null]}"""); + """{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807",null]}""", + mappedCollection: true); + + protected class NullableTimeSpanCollectionType + { + public IList TimeSpan { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_bool_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Boolean)), + => Can_read_and_write_JSON_value>(nameof(NullableBooleanCollectionType.Boolean), new List { false, null, true }, - """{"Prop":[false,null,true]}"""); + """{"Prop":[false,null,true]}""", + mappedCollection: true); + + protected class NullableBooleanCollectionType + { + public IList Boolean { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_char_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Character)), + => Can_read_and_write_JSON_value>(nameof(NullableCharacterCollectionType.Character), new List { char.MinValue, @@ -1836,12 +2531,17 @@ public virtual void Can_read_write_collection_of_nullable_char_JSON_values() char.MaxValue, null }, - """{"Prop":["\u0000","X","\uFFFF",null]}"""); + """{"Prop":["\u0000","X","\uFFFF",null]}""", + mappedCollection: true); + + protected class NullableCharacterCollectionType + { + public IList Character { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_GUID_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Guid)), + => Can_read_and_write_JSON_value>(nameof(NullableGuidCollectionType.Guid), new List { new(), @@ -1849,12 +2549,17 @@ public virtual void Can_read_write_collection_of_nullable_GUID_JSON_values() new("8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD"), Guid.Parse("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") }, - """{"Prop":["00000000-0000-0000-0000-000000000000",null,"8c44242f-8e3f-4a20-8be8-98c7c1aadebd","ffffffff-ffff-ffff-ffff-ffffffffffff"]}"""); + """{"Prop":["00000000-0000-0000-0000-000000000000",null,"8c44242f-8e3f-4a20-8be8-98c7c1aadebd","ffffffff-ffff-ffff-ffff-ffffffffffff"]}""", + mappedCollection: true); + + protected class NullableGuidCollectionType + { + public IList Guid { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_string_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.String)), + => Can_read_and_write_JSON_value>(nameof(NullableStringCollectionType.String), new List { "MinValue", @@ -1862,12 +2567,17 @@ public virtual void Can_read_write_collection_of_nullable_string_JSON_values() "❤❥웃유♋☮✌☏☢☠✔☑♚▲♪฿Ɖ⛏♥❣♂♀☿👍✍✉☣☤✘☒♛▼♫⌘⌛¡♡ღツ☼☁❅♾️✎©®™Σ✪✯☭➳Ⓐ✞℃℉°✿⚡☃☂✄¢€£∞✫★½☯✡☪", "MaxValue" }, - """{"Prop":["MinValue",null,"\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A","MaxValue"]}"""); + """{"Prop":["MinValue",null,"\u2764\u2765\uC6C3\uC720\u264B\u262E\u270C\u260F\u2622\u2620\u2714\u2611\u265A\u25B2\u266A\u0E3F\u0189\u26CF\u2665\u2763\u2642\u2640\u263F\uD83D\uDC4D\u270D\u2709\u2623\u2624\u2718\u2612\u265B\u25BC\u266B\u2318\u231B\u00A1\u2661\u10E6\u30C4\u263C\u2601\u2745\u267E\uFE0F\u270E\u00A9\u00AE\u2122\u03A3\u272A\u272F\u262D\u27B3\u24B6\u271E\u2103\u2109\u00B0\u273F\u26A1\u2603\u2602\u2704\u00A2\u20AC\u00A3\u221E\u272B\u2605\u00BD\u262F\u2721\u262A","MaxValue"]}""", + mappedCollection: true); + + protected class NullableStringCollectionType + { + public IList String { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_binary_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Bytes)), + => Can_read_and_write_JSON_value>(nameof(NullableBytesCollectionType.Bytes), new List { new byte[] { 0, 0, 0, 1 }, @@ -1876,24 +2586,35 @@ public virtual void Can_read_write_collection_of_nullable_binary_JSON_values() Array.Empty(), new byte[] { 1, 2, 3, 4 } }, - """{"Prop":["AAAAAQ==",null,"/////w==","","AQIDBA=="]}"""); + """{"Prop":["AAAAAQ==",null,"/////w==","","AQIDBA=="]}""", + mappedCollection: true); + + protected class NullableBytesCollectionType + { + public IList Bytes { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_URI_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Uri)), + => Can_read_and_write_JSON_value>(nameof(NullableUriCollectionType.Uri), new List { new("https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName"), null, new("file:///C:/test/path/file.txt") }, - """{"Prop":["https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1\u0026q2=v2#FragmentName",null,"file:///C:/test/path/file.txt"]}"""); + """{"Prop":["https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1\u0026q2=v2#FragmentName",null,"file:///C:/test/path/file.txt"]}""", + mappedCollection: true); + + protected class NullableUriCollectionType + { + public List Uri { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_IP_address_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.IpAddress)), + => Can_read_and_write_JSON_value>( + nameof(NullableIpAddressCollectionType.IpAddress), new List { IPAddress.Parse("127.0.0.1"), @@ -1905,12 +2626,18 @@ public virtual void Can_read_write_collection_of_nullable_IP_address_JSON_values IPAddress.Parse("::"), IPAddress.Parse("2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"), }, - """{"Prop":["127.0.0.1",null,"0.0.0.0","255.255.255.255","192.168.1.156","::1","::","2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"]}"""); + """{"Prop":["127.0.0.1",null,"0.0.0.0","255.255.255.255","192.168.1.156","::1","::","2a00:23c7:c60f:4f01:ba43:6d5a:e648:7577"]}""", + mappedCollection: true); + + protected class NullableIpAddressCollectionType + { + public List IpAddress { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_physical_address_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.PhysicalAddress)), + => Can_read_and_write_JSON_value>( + nameof(NullablePhysicalAddressCollectionType.PhysicalAddress), new List { PhysicalAddress.None, @@ -1919,12 +2646,17 @@ public virtual void Can_read_write_collection_of_nullable_physical_address_JSON_ PhysicalAddress.Parse("00-11-22-33-44-55"), PhysicalAddress.Parse("0011.2233.4455") }, - """{"Prop":["",null,"001122334455","001122334455","001122334455"]}"""); + """{"Prop":["",null,"001122334455","001122334455","001122334455"]}""", + mappedCollection: true); + + protected class NullablePhysicalAddressCollectionType + { + public List PhysicalAddress { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_sbyte_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Enum8)), + => Can_read_and_write_JSON_value>(nameof(NullableEnum8CollectionType.Enum8), new List { Enum8.Min, @@ -1934,12 +2666,17 @@ public virtual void Can_read_write_collection_of_nullable_sbyte_enum_JSON_values Enum8.One, (Enum8)(-8) }, - """{"Prop":[-128,null,127,0,1,-8]}"""); + """{"Prop":[-128,null,127,0,1,-8]}""", + mappedCollection: true); + + protected class NullableEnum8CollectionType + { + public List Enum8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_short_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Enum16)), + => Can_read_and_write_JSON_value>(nameof(NullableEnum16CollectionType.Enum16), new List { Enum16.Min, @@ -1949,12 +2686,17 @@ public virtual void Can_read_write_collection_of_nullable_short_enum_JSON_values Enum16.One, (Enum16)(-8) }, - """{"Prop":[-32768,null,32767,0,1,-8]}"""); + """{"Prop":[-32768,null,32767,0,1,-8]}""", + mappedCollection: true); + + protected class NullableEnum16CollectionType + { + public List Enum16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_int_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Enum32)), + => Can_read_and_write_JSON_value>(nameof(NullableEnum32CollectionType.Enum32), new List { Enum32.Min, @@ -1964,12 +2706,17 @@ public virtual void Can_read_write_collection_of_nullable_int_enum_JSON_values() Enum32.One, (Enum32)(-8) }, - """{"Prop":[-2147483648,null,2147483647,0,1,-8]}"""); + """{"Prop":[-2147483648,null,2147483647,0,1,-8]}""", + mappedCollection: true); + + protected class NullableEnum32CollectionType + { + public List Enum32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_long_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Enum64)), + => Can_read_and_write_JSON_value>(nameof(NullableEnum64CollectionType.Enum64), new List { Enum64.Min, @@ -1979,12 +2726,17 @@ public virtual void Can_read_write_collection_of_nullable_long_enum_JSON_values( Enum64.One, (Enum64)(-8) }, - """{"Prop":[-9223372036854775808,null,9223372036854775807,0,1,-8]}"""); + """{"Prop":[-9223372036854775808,null,9223372036854775807,0,1,-8]}""", + mappedCollection: true); + + protected class NullableEnum64CollectionType + { + public List Enum64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_byte_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.EnumU8)), + => Can_read_and_write_JSON_value>(nameof(NullableEnumU8CollectionType.EnumU8), new List { EnumU8.Min, @@ -1992,14 +2744,19 @@ public virtual void Can_read_write_collection_of_nullable_byte_enum_JSON_values( EnumU8.Max, EnumU8.Default, EnumU8.One, - (EnumU8)8 + (EnumU8?)8 }, - """{"Prop":[0,null,255,0,1,8]}"""); + """{"Prop":[0,null,255,0,1,8]}""", + mappedCollection: true); + + protected class NullableEnumU8CollectionType + { + public IList EnumU8 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_ushort_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.EnumU16)), + => Can_read_and_write_JSON_value>(nameof(NullableEnumU16CollectionType.EnumU16), new List { EnumU16.Min, @@ -2007,14 +2764,19 @@ public virtual void Can_read_write_collection_of_nullable_ushort_enum_JSON_value EnumU16.Max, EnumU16.Default, EnumU16.One, - (EnumU16)8 + (EnumU16?)8 }, - """{"Prop":[0,null,65535,0,1,8]}"""); + """{"Prop":[0,null,65535,0,1,8]}""", + mappedCollection: true); + + protected class NullableEnumU16CollectionType + { + public IList EnumU16 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_uint_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.EnumU32)), + => Can_read_and_write_JSON_value>(nameof(NullableEnumU32CollectionType.EnumU32), new List { EnumU32.Min, @@ -2022,14 +2784,19 @@ public virtual void Can_read_write_collection_of_nullable_uint_enum_JSON_values( EnumU32.Max, EnumU32.Default, EnumU32.One, - (EnumU32)8 + (EnumU32?)8 }, - """{"Prop":[0,null,4294967295,0,1,8]}"""); + """{"Prop":[0,null,4294967295,0,1,8]}""", + mappedCollection: true); + + protected class NullableEnumU32CollectionType + { + public IList EnumU32 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.EnumU64)), + => Can_read_and_write_JSON_value>(nameof(NullableEnumU64CollectionType.EnumU64), new List { EnumU64.Min, @@ -2037,21 +2804,34 @@ public virtual void Can_read_write_collection_of_nullable_ulong_enum_JSON_values EnumU64.Max, EnumU64.Default, EnumU64.One, - (EnumU64)8 + (EnumU64?)8 }, - """{"Prop":[0,null,18446744073709551615,0,1,8]}"""); + """{"Prop":[0,null,18446744073709551615,0,1,8]}""", + mappedCollection: true); + + protected class NullableEnumU64CollectionType + { + public IList EnumU64 { get; set; } = null!; + } [ConditionalFact] public virtual void Can_read_write_collection_of_sbyte_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Int8Converted)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion, CustomCollectionComparer>(), + nameof(Int8ConvertedType.Int8Converted), new sbyte[] { sbyte.MinValue, 0, sbyte.MaxValue }, """{"Prop":"[-128,0,127]"}"""); + protected class Int8ConvertedType + { + public sbyte[] Int8Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_int_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Int32Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, int>, CustomCollectionComparer, int>>(), + nameof(Int32ConvertedType.Int32Converted), new List { int.MinValue, @@ -2060,10 +2840,17 @@ public virtual void Can_read_write_collection_of_int_values_with_converter_as_JS }, """{"Prop":"[-2147483648,0,2147483647]"}"""); + protected class Int32ConvertedType + { + public List Int32Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_ulong_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.UInt64Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, ulong>, + CustomCollectionComparer, ulong>>(), + nameof(UInt64ConvertedType.UInt64Converted), new ObservableCollection { ulong.MinValue, @@ -2072,17 +2859,30 @@ public virtual void Can_read_write_collection_of_ulong_values_with_converter_as_ }, """{"Prop":"[0,1,18446744073709551615]"}"""); + protected class UInt64ConvertedType + { + public ObservableCollection UInt64Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_double_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.DoubleConverted)), - new[] { double.MinValue, 0, double.MaxValue }, + => Can_read_and_write_JSON_property_value( + b => b.HasConversion, CustomCollectionComparer>(), + nameof(DoubleConvertedType.DoubleConverted), + new double[] { double.MinValue, 0, double.MaxValue }, """{"Prop":"[-1.7976931348623157E\u002B308,0,1.7976931348623157E\u002B308]"}"""); + protected class DoubleConvertedType + { + public double[] DoubleConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_DateOnly_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.DateOnlyConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, DateOnly>, + CustomCollectionComparer, DateOnly>>(), + nameof(DateOnlyConvertedType.DateOnlyConverted), new List { DateOnly.MinValue, @@ -2091,10 +2891,16 @@ public virtual void Can_read_write_collection_of_DateOnly_values_with_converter_ }, """{"Prop":"[\u00220001-01-01\u0022,\u00222023-05-29\u0022,\u00229999-12-31\u0022]"}"""); + protected class DateOnlyConvertedType + { + public IList DateOnlyConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_DateTime_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.DateTimeConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, DateTime>, CustomCollectionComparer, DateTime>>(), + nameof(DateTimeConvertedType.DateTimeConverted), new List { DateTime.MinValue, @@ -2103,17 +2909,33 @@ public virtual void Can_read_write_collection_of_DateTime_values_with_converter_ }, """{"Prop":"[\u00220001-01-01T00:00:00\u0022,\u00222023-05-29T10:52:47\u0022,\u00229999-12-31T23:59:59.9999999\u0022]"}"""); + protected class DateTimeConvertedType + { + public IList DateTimeConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_bool_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.BooleanConverted)), - new List { false, true }, + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, bool>, CustomCollectionComparer, bool>>(), + nameof(BooleanConvertedType.BooleanConverted), + new List + { + false, + true + }, """{"Prop":"[false,true]"}"""); + protected class BooleanConvertedType + { + public IList BooleanConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_char_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.CharacterConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, char>, CustomCollectionComparer, char>>(), + nameof(CharacterConvertedType.CharacterConverted), new List { char.MinValue, @@ -2122,10 +2944,16 @@ public virtual void Can_read_write_collection_of_char_values_with_converter_as_J }, """{"Prop":"[\u0022\\u0000\u0022,\u0022X\u0022,\u0022\\uFFFF\u0022]"}"""); + protected class CharacterConvertedType + { + public IList CharacterConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_string_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.StringConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, string>, CustomCollectionComparer, string>>(), + nameof(StringConvertedType.StringConverted), new List { "MinValue", @@ -2134,10 +2962,16 @@ public virtual void Can_read_write_collection_of_string_values_with_converter_as }, """{"Prop":"[\u0022MinValue\u0022,\u0022\\u2764\\u2765\\uC6C3\\uC720\\u264B\\u262E\\u270C\\u260F\\u2622\\u2620\\u2714\\u2611\\u265A\\u25B2\\u266A\\u0E3F\\u0189\\u26CF\\u2665\\u2763\\u2642\\u2640\\u263F\\uD83D\\uDC4D\\u270D\\u2709\\u2623\\u2624\\u2718\\u2612\\u265B\\u25BC\\u266B\\u2318\\u231B\\u00A1\\u2661\\u10E6\\u30C4\\u263C\\u2601\\u2745\\u267E\\uFE0F\\u270E\\u00A9\\u00AE\\u2122\\u03A3\\u272A\\u272F\\u262D\\u27B3\\u24B6\\u271E\\u2103\\u2109\\u00B0\\u273F\\u26A1\\u2603\\u2602\\u2704\\u00A2\\u20AC\\u00A3\\u221E\\u272B\\u2605\\u00BD\\u262F\\u2721\\u262A\u0022,\u0022MaxValue\u0022]"}"""); + protected class StringConvertedType + { + public IList StringConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_binary_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.BytesConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, byte[]>, CustomCollectionComparer, byte[]>>(), + nameof(BytesConvertedType.BytesConverted), new List { new byte[] { 0, 0, 0, 1 }, @@ -2147,10 +2981,16 @@ public virtual void Can_read_write_collection_of_binary_values_with_converter_as }, """{"Prop":"[\u0022AAAAAQ==\u0022,\u0022/////w==\u0022,\u0022\u0022,\u0022AQIDBA==\u0022]"}"""); + protected class BytesConvertedType + { + public IList BytesConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_int_enum_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.Enum32Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, Enum32>, CustomCollectionComparer, Enum32>>(), + nameof(Enum32ConvertedType.Enum32Converted), new List { Enum32.Min, @@ -2161,10 +3001,16 @@ public virtual void Can_read_write_collection_of_int_enum_values_with_converter_ }, """{"Prop":"[-2147483648,2147483647,0,1,-8]"}"""); + protected class Enum32ConvertedType + { + public List Enum32Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_ulong_enum_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.EnumU64Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, EnumU64>, CustomCollectionComparer, EnumU64>>(), + nameof(EnumU64ConvertedType.EnumU64Converted), new List { EnumU64.Min, @@ -2175,17 +3021,30 @@ public virtual void Can_read_write_collection_of_ulong_enum_values_with_converte }, """{"Prop":"[0,18446744073709551615,0,1,8]"}"""); + protected class EnumU64ConvertedType + { + public IList EnumU64Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_sbyte_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Int8Converted)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion, CustomCollectionComparer>(), + nameof(NullableInt8ConvertedType.Int8Converted), new sbyte?[] { null, sbyte.MinValue, 0, sbyte.MaxValue }, """{"Prop":"[null,-128,0,127]"}"""); + protected class NullableInt8ConvertedType + { + public sbyte?[] Int8Converted { get; set; } = null!; + } + + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_int_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Int32Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, int?>, CustomCollectionComparer, int?>>(), + nameof(NullableInt32ConvertedType.Int32Converted), new List { int.MinValue, @@ -2195,10 +3054,17 @@ public virtual void Can_read_write_collection_of_nullable_int_values_with_conver }, """{"Prop":"[-2147483648,0,null,2147483647]"}"""); + protected class NullableInt32ConvertedType + { + public List Int32Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_ulong_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.UInt64Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, ulong?>, + CustomCollectionComparer, ulong?>>(), + nameof(NullableUInt64ConvertedType.UInt64Converted), new ObservableCollection { ulong.MinValue, @@ -2208,17 +3074,30 @@ public virtual void Can_read_write_collection_of_nullable_ulong_values_with_conv }, """{"Prop":"[0,1,18446744073709551615,null]"}"""); + protected class NullableUInt64ConvertedType + { + public ObservableCollection UInt64Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_double_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.DoubleConverted)), + => Can_read_and_write_JSON_property_value( + b => b.HasConversion, CustomCollectionComparer>(), + nameof(NullableDoubleConvertedType.DoubleConverted), new double?[] { double.MinValue, null, 0, double.MaxValue }, """{"Prop":"[-1.7976931348623157E\u002B308,null,0,1.7976931348623157E\u002B308]"}"""); + protected class NullableDoubleConvertedType + { + public double?[] DoubleConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_DateOnly_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.DateOnlyConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, DateOnly?>, + CustomCollectionComparer, DateOnly?>>(), + nameof(NullableDateOnlyConvertedType.DateOnlyConverted), new List { DateOnly.MinValue, @@ -2228,10 +3107,16 @@ public virtual void Can_read_write_collection_of_nullable_DateOnly_values_with_c }, """{"Prop":"[\u00220001-01-01\u0022,\u00222023-05-29\u0022,\u00229999-12-31\u0022,null]"}"""); + protected class NullableDateOnlyConvertedType + { + public IList DateOnlyConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_DateTime_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.DateTimeConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, DateTime?>, CustomCollectionComparer, DateTime?>>(), + nameof(NullableDateTimeConvertedType.DateTimeConverted), new List { DateTime.MinValue, @@ -2241,10 +3126,16 @@ public virtual void Can_read_write_collection_of_nullable_DateTime_values_with_c }, """{"Prop":"[\u00220001-01-01T00:00:00\u0022,null,\u00222023-05-29T10:52:47\u0022,\u00229999-12-31T23:59:59.9999999\u0022]"}"""); + protected class NullableDateTimeConvertedType + { + public IList DateTimeConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_bool_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.BooleanConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, bool?>, CustomCollectionComparer, bool?>>(), + nameof(NullableBooleanConvertedType.BooleanConverted), new List { false, @@ -2253,10 +3144,16 @@ public virtual void Can_read_write_collection_of_nullable_bool_values_with_conve }, """{"Prop":"[false,null,true]"}"""); + protected class NullableBooleanConvertedType + { + public IList BooleanConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_char_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.CharacterConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, char?>, CustomCollectionComparer, char?>>(), + nameof(NullableCharacterConvertedType.CharacterConverted), new List { char.MinValue, @@ -2266,10 +3163,16 @@ public virtual void Can_read_write_collection_of_nullable_char_values_with_conve }, """{"Prop":"[\u0022\\u0000\u0022,\u0022X\u0022,\u0022\\uFFFF\u0022,null]"}"""); + protected class NullableCharacterConvertedType + { + public IList CharacterConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_string_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.StringConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, string?>, CustomCollectionComparer, string?>>(), + nameof(NullableStringConvertedType.StringConverted), new List { "MinValue", @@ -2279,10 +3182,16 @@ public virtual void Can_read_write_collection_of_nullable_string_values_with_con }, """{"Prop":"[\u0022MinValue\u0022,null,\u0022\\u2764\\u2765\\uC6C3\\uC720\\u264B\\u262E\\u270C\\u260F\\u2622\\u2620\\u2714\\u2611\\u265A\\u25B2\\u266A\\u0E3F\\u0189\\u26CF\\u2665\\u2763\\u2642\\u2640\\u263F\\uD83D\\uDC4D\\u270D\\u2709\\u2623\\u2624\\u2718\\u2612\\u265B\\u25BC\\u266B\\u2318\\u231B\\u00A1\\u2661\\u10E6\\u30C4\\u263C\\u2601\\u2745\\u267E\\uFE0F\\u270E\\u00A9\\u00AE\\u2122\\u03A3\\u272A\\u272F\\u262D\\u27B3\\u24B6\\u271E\\u2103\\u2109\\u00B0\\u273F\\u26A1\\u2603\\u2602\\u2704\\u00A2\\u20AC\\u00A3\\u221E\\u272B\\u2605\\u00BD\\u262F\\u2721\\u262A\u0022,\u0022MaxValue\u0022]"}"""); + protected class NullableStringConvertedType + { + public IList StringConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_binary_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.BytesConverted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, byte[]?>, CustomCollectionComparer, byte[]?>>(), + nameof(NullableBytesConvertedType.BytesConverted), new List { new byte[] { 0, 0, 0, 1 }, @@ -2293,10 +3202,16 @@ public virtual void Can_read_write_collection_of_nullable_binary_values_with_con }, """{"Prop":"[\u0022AAAAAQ==\u0022,null,\u0022/////w==\u0022,\u0022\u0022,\u0022AQIDBA==\u0022]"}"""); + protected class NullableBytesConvertedType + { + public IList BytesConverted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_int_enum_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.Enum32Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, Enum32?>, CustomCollectionComparer, Enum32?>>(), + nameof(NullableEnum32ConvertedType.Enum32Converted), new List { Enum32.Min, @@ -2308,10 +3223,16 @@ public virtual void Can_read_write_collection_of_nullable_int_enum_values_with_c }, """{"Prop":"[-2147483648,null,2147483647,0,1,-8]"}"""); + protected class NullableEnum32ConvertedType + { + public List Enum32Converted { get; set; } = null!; + } + [ConditionalFact] public virtual void Can_read_write_collection_of_nullable_ulong_enum_values_with_converter_as_JSON_string() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.EnumU64Converted)), + => Can_read_and_write_JSON_property_value>( + b => b.HasConversion, EnumU64?>, CustomCollectionComparer, EnumU64?>>(), + nameof(NullableEnumU64ConvertedType.EnumU64Converted), new List { EnumU64.Min, @@ -2323,193 +3244,275 @@ public virtual void Can_read_write_collection_of_nullable_ulong_enum_values_with }, """{"Prop":"[0,null,18446744073709551615,0,1,8]"}"""); - protected virtual void Can_read_and_write_JSON_value( - IProperty property, - TModel value, - string json, - object? existingObject = null) + protected class NullableEnumU64ConvertedType { - using var stream = new MemoryStream(); - using var writer = new Utf8JsonWriter(stream); + public IList EnumU64Converted { get; set; } = null!; + } - var jsonReaderWriter = property.GetJsonValueReaderWriter() - ?? property.GetTypeMapping().JsonValueReaderWriter!; + [ConditionalFact] + public virtual void Can_read_write_collection_of_int_with_converter_JSON_values() + => Can_read_and_write_JSON_collection_value>( + b => b.ElementType( + b => + { + b.HasConversion(); + b.IsRequired(); + }), + nameof(DddIdCollectionType.DddId), + new List + { + new() { Id = int.MinValue }, + new() { Id = 0 }, + new() { Id = int.MaxValue } + }, + """{"Prop":[-2147483648,0,2147483647]}""", + facets: new Dictionary { { CoreAnnotationNames.ValueConverter, typeof(DddIdConverter) } }); - writer.WriteStartObject(); - writer.WritePropertyName("Prop"); - if (value == null) - { - writer.WriteNullValue(); - } - else - { - jsonReaderWriter.ToJson(writer, value); - } + protected class DddIdCollectionType + { + public IList DddId { get; set; } = null!; + } - writer.WriteEndObject(); - writer.Flush(); + [ConditionalFact] + public virtual void Can_read_write_collection_of_nullable_int_with_converter_JSON_values() + => Can_read_and_write_JSON_collection_value>( + b => b.ElementType().HasConversion(), + nameof(NullableDddIdCollectionType.DddId), + new List + { + null, + new() { Id = int.MinValue }, + null, + new() { Id = 0 }, + new() { Id = int.MaxValue } + }, + """{"Prop":[null,-2147483648,null,0,2147483647]}""", + facets: new Dictionary { { CoreAnnotationNames.ValueConverter, typeof(DddIdConverter) } }); - var buffer = stream.ToArray(); + protected class NullableDddIdCollectionType + { + public IList DddId { get; set; } = null!; + } - var actual = Encoding.UTF8.GetString(buffer); + [ConditionalFact] + public virtual void Can_read_write_binary_as_collection() + => Can_read_and_write_JSON_collection_value( + _ => { }, + nameof(BinaryAsJsonType.BinaryAsJson), + new byte[] { 77, 78, 79, 80 }, + """{"Prop":[77,78,79,80]}"""); - Assert.Equal(json, actual); + protected class BinaryAsJsonType + { + public byte[] BinaryAsJson { get; set; } = null!; + } - var readerManager = new Utf8JsonReaderManager(new JsonReaderData(buffer)); - readerManager.MoveNext(); - readerManager.MoveNext(); - readerManager.MoveNext(); + [ConditionalFact] + public virtual void Can_read_write_collection_of_decimal_with_precision_and_scale_JSON_values() + => Can_read_and_write_JSON_collection_value>( + b => b.ElementType().HasPrecision(12, 6), + nameof(DecimalCollectionType.Decimal), + new List + { + decimal.MinValue, + 0, + decimal.MaxValue + }, + """{"Prop":[-79228162514264337593543950335,0,79228162514264337593543950335]}""", + facets: new Dictionary { { CoreAnnotationNames.Precision, 12 }, { CoreAnnotationNames.Scale, 6 } }); - if (readerManager.CurrentReader.TokenType == JsonTokenType.Null) + [ConditionalFact] + public virtual void Can_read_write_collection_of_Guid_converted_to_bytes_JSON_values() + => Can_read_and_write_JSON_collection_value>( + b => b.ElementType().HasConversion(), + nameof(GuidCollectionType.Guid), + new List + { + new(), + new("8C44242F-8E3F-4A20-8BE8-98C7C1AADEBD"), + Guid.Parse("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") + }, + """{"Prop":["AAAAAAAAAAAAAAAAAAAAAA==","LyREjD+OIEqL6JjHwarevQ==","/////////////////////w=="]}""", + facets: new Dictionary { { CoreAnnotationNames.ProviderClrType, typeof(byte[]) } }); + + protected virtual void Can_read_and_write_JSON_value( + string propertyName, + TModel value, + string json, + bool mappedCollection = false, + object? existingObject = null, + Dictionary? facets = null) + where TEntity : class + { + if (mappedCollection) { - Assert.Null(value); + Can_read_and_write_JSON_value( + b => b.Entity().HasNoKey().PrimitiveCollection(propertyName), + null, + propertyName, + value, + json, + mappedCollection, + existingObject, + facets); } else { - var fromJson = jsonReaderWriter.FromJson(ref readerManager, existingObject); - if (existingObject != null) - { - Assert.Same(fromJson, existingObject); - } - - Assert.Equal(value, fromJson); + Can_read_and_write_JSON_value( + b => b.Entity().HasNoKey().Property(propertyName), + null, + propertyName, + value, + json, + mappedCollection, + existingObject, + facets); } } - protected class Types + protected virtual void Can_read_and_write_JSON_property_value( + Action buildProperty, + string propertyName, + TModel value, + string json, + object? existingObject = null, + Dictionary? facets = null) + where TEntity : class + => Can_read_and_write_JSON_value( + b => buildProperty(b.Entity().HasNoKey().Property(propertyName)), + null, + propertyName, + value, + json, + mappedCollection: false, + existingObject, + facets); + + protected virtual void Can_read_and_write_JSON_collection_value( + Action buildCollection, + string propertyName, + TModel value, + string json, + object? existingObject = null, + Dictionary? facets = null) + where TEntity : class + => Can_read_and_write_JSON_value( + b => buildCollection(b.Entity().HasNoKey().PrimitiveCollection(propertyName)), + null, + propertyName, + value, + json, + mappedCollection: true, + existingObject, + facets); + + protected virtual void Can_read_and_write_JSON_value( + Action buildModel, + Action? configureConventions, + string propertyName, + TModel value, + string json, + bool mappedCollection = false, + object? existingObject = null, + Dictionary? facets = null) + where TEntity : class { - public sbyte Int8 { get; set; } - public short Int16 { get; set; } - public int Int32 { get; set; } - public long Int64 { get; set; } + using var context = new SingleTypeDbContext(OnConfiguring, buildModel, configureConventions); + var property = context.Model.FindEntityType(typeof(TEntity))!.GetProperty(propertyName); - public byte UInt8 { get; set; } - public ushort UInt16 { get; set; } - public uint UInt32 { get; set; } - public ulong UInt64 { get; set; } - - public float Float { get; set; } - public double Double { get; set; } - public decimal Decimal { get; set; } - - public DateTime DateTime { get; set; } - public DateTimeOffset DateTimeOffset { get; set; } - public TimeSpan TimeSpan { get; set; } - public DateOnly DateOnly { get; set; } - public TimeOnly TimeOnly { get; set; } - - public Guid Guid { get; set; } - public string String { get; set; } = null!; - public byte[] Bytes { get; set; } = null!; + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); - public bool Boolean { get; set; } - public char Character { get; set; } + var jsonReaderWriter = property.GetJsonValueReaderWriter() + ?? property.GetTypeMapping().JsonValueReaderWriter!; - public Uri Uri { get; set; } = null!; - public PhysicalAddress PhysicalAddress { get; set; } = null!; - public IPAddress IpAddress { get; set; } = null!; + var actual = ToJsonPropertyString(jsonReaderWriter, value); + Assert.Equal(json, actual); - public Enum8 Enum8 { get; set; } - public Enum16 Enum16 { get; set; } - public Enum32 Enum32 { get; set; } - public Enum64 Enum64 { get; set; } + var fromJson = FromJsonPropertyString(jsonReaderWriter, actual, existingObject); + if (existingObject != null) + { + Assert.Same(fromJson, existingObject); + } + Assert.Equal(value, fromJson); - public EnumU8 EnumU8 { get; set; } - public EnumU16 EnumU16 { get; set; } - public EnumU32 EnumU32 { get; set; } - public EnumU64 EnumU64 { get; set; } + var element = property.GetElementType(); + if (mappedCollection) + { + Assert.NotNull(element); + + Assert.Equal(typeof(TModel).GetSequenceType(), element.ClrType); + Assert.Same(property, element.CollectionProperty); + Assert.Equal(json.Contains("null", StringComparison.Ordinal) || !element.ClrType.IsValueType, element.IsNullable); + Assert.Null(element.FindTypeMapping()!.ElementTypeMapping); + + var comparer = element.GetValueComparer()!; + var elementReaderWriter = element.GetJsonValueReaderWriter()!; + foreach (var item in (IEnumerable)value!) + { + Assert.True(comparer.Equals(item, comparer.Snapshot(item))); + Assert.True( + comparer.Equals( + item, FromJsonPropertyString( + elementReaderWriter, ToJsonPropertyString(elementReaderWriter, item)))); + } - public DddId DddId { get; set; } + AssertElementFacets(element, facets); + } + else + { + Assert.Null(element); + } } - protected class NullableTypes + protected virtual void AssertElementFacets(IElementType element, Dictionary? facets) { - public sbyte? Int8 { get; set; } - public short? Int16 { get; set; } - public int? Int32 { get; set; } - public long? Int64 { get; set; } - - public byte? UInt8 { get; set; } - public ushort? UInt16 { get; set; } - public uint? UInt32 { get; set; } - public ulong? UInt64 { get; set; } - - public float? Float { get; set; } - public double? Double { get; set; } - public decimal? Decimal { get; set; } - - public DateTime? DateTime { get; set; } - public DateTimeOffset? DateTimeOffset { get; set; } - public TimeSpan? TimeSpan { get; set; } - public DateOnly? DateOnly { get; set; } - public TimeOnly? TimeOnly { get; set; } - - public Guid? Guid { get; set; } - public string? String { get; set; } - public byte[]? Bytes { get; set; } - - public bool? Boolean { get; set; } - public char? Character { get; set; } - - public Uri? Uri { get; set; } - public PhysicalAddress? PhysicalAddress { get; set; } - public IPAddress? IpAddress { get; set; } - - public Enum8? Enum8 { get; set; } - public Enum16? Enum16 { get; set; } - public Enum32? Enum32 { get; set; } - public Enum64? Enum64 { get; set; } - - public EnumU8? EnumU8 { get; set; } - public EnumU16? EnumU16 { get; set; } - public EnumU32? EnumU32 { get; set; } - public EnumU64? EnumU64 { get; set; } - - public DddId? DddId { get; set; } + Assert.Equal(FacetValue(CoreAnnotationNames.Precision), element.GetPrecision()); + Assert.Equal(FacetValue(CoreAnnotationNames.Scale), element.GetScale()); + Assert.Equal(FacetValue(CoreAnnotationNames.MaxLength), element.GetMaxLength()); + Assert.Equal(FacetValue(CoreAnnotationNames.ProviderClrType), element.GetProviderClrType()); + Assert.Equal(FacetValue(CoreAnnotationNames.Unicode), element.IsUnicode()); + Assert.Equal(FacetValue(CoreAnnotationNames.ValueConverter), element.GetValueConverter()?.GetType()); + + object? FacetValue(string facetName) + => facets?.TryGetValue(facetName, out var facet) == true ? facet : null; } - protected class TypesAsStrings + protected string ToJsonPropertyString(JsonValueReaderWriter jsonReaderWriter, object? value) { - public sbyte? Int8 { get; set; } - public short? Int16 { get; set; } - public int? Int32 { get; set; } - public long? Int64 { get; set; } - - public byte? UInt8 { get; set; } - public ushort? UInt16 { get; set; } - public uint? UInt32 { get; set; } - public ulong? UInt64 { get; set; } - - public float? Float { get; set; } - public double? Double { get; set; } - public decimal? Decimal { get; set; } + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); - public DateTime? DateTime { get; set; } - public DateTimeOffset? DateTimeOffset { get; set; } - public TimeSpan? TimeSpan { get; set; } - public DateOnly? DateOnly { get; set; } - public TimeOnly? TimeOnly { get; set; } + writer.WriteStartObject(); + writer.WritePropertyName("Prop"); + if (value == null) + { + writer.WriteNullValue(); + } + else + { + jsonReaderWriter.ToJson(writer, value); + } - public Guid? Guid { get; set; } - public string? String { get; set; } - public byte[]? Bytes { get; set; } + writer.WriteEndObject(); + writer.Flush(); - public bool? Boolean { get; set; } - public char? Character { get; set; } + var buffer = stream.ToArray(); - public Uri? Uri { get; set; } - public PhysicalAddress? PhysicalAddress { get; set; } - public IPAddress? IpAddress { get; set; } + return Encoding.UTF8.GetString(buffer); + } - public Enum8? Enum8 { get; set; } - public Enum16? Enum16 { get; set; } - public Enum32? Enum32 { get; set; } - public Enum64? Enum64 { get; set; } + protected object? FromJsonPropertyString(JsonValueReaderWriter jsonReaderWriter, string value, object? existingValue = null) + { + var buffer = Encoding.UTF8.GetBytes(value); + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(buffer)); + readerManager.MoveNext(); + readerManager.MoveNext(); + readerManager.MoveNext(); - public EnumU8? EnumU8 { get; set; } - public EnumU16? EnumU16 { get; set; } - public EnumU32? EnumU32 { get; set; } - public EnumU64? EnumU64 { get; set; } + return readerManager.CurrentReader.TokenType == JsonTokenType.Null + ? null + : jsonReaderWriter.FromJson(ref readerManager, existingValue); } protected readonly struct DddId @@ -2525,32 +3528,6 @@ public DddIdConverter() } } - public class GeometryTypes - { - public int Id { get; set; } - public Geometry? Geometry { get; set; } - public Point? Point { get; set; } - public Point PointZ { get; set; } = null!; - public Point PointM { get; set; } = null!; - public Point PointZM { get; set; } = null!; - public Polygon? Polygon { get; set; } - public MultiLineString? MultiLineString { get; set; } - public LineString? LineString { get; set; } - } - - public class GeometryTypesAsGeoJson - { - public int Id { get; set; } - public Geometry? Geometry { get; set; } - public Point? Point { get; set; } - public Point PointZ { get; set; } = null!; - public Point PointM { get; set; } = null!; - public Point PointZM { get; set; } = null!; - public Polygon? Polygon { get; set; } - public MultiLineString? MultiLineString { get; set; } - public LineString? LineString { get; set; } - } - public enum Enum8 : sbyte { Min = sbyte.MinValue, @@ -2615,124 +3592,6 @@ public enum EnumU64 : ulong Max = ulong.MaxValue } - protected class PrimitiveTypeCollections - { - public sbyte[] Int8 { get; set; } = null!; - public IList Int16 { get; set; } = null!; - public List Int32 { get; set; } = null!; - public IList Int64 { get; set; } = null!; - - public List UInt8 { get; set; } = null!; - public Collection UInt16 { get; set; } = null!; - public List UInt32 { get; set; } = null!; - public ObservableCollection UInt64 { get; set; } = null!; - - public float[] Float { get; set; } = null!; - public double[] Double { get; set; } = null!; - public decimal[] Decimal { get; set; } = null!; - - public IList DateTime { get; set; } = null!; - public IList DateTimeOffset { get; set; } = null!; - public IList TimeSpan { get; set; } = null!; - public IList DateOnly { get; set; } = null!; - public IList TimeOnly { get; set; } = null!; - - public IList Guid { get; set; } = null!; - public IList String { get; set; } = null!; - public IList Bytes { get; set; } = null!; - - public IList Boolean { get; set; } = null!; - public IList Character { get; set; } = null!; - - public List Uri { get; set; } = null!; - public List PhysicalAddress { get; set; } = null!; - public List IpAddress { get; set; } = null!; - - public List Enum8 { get; set; } = null!; - public List Enum16 { get; set; } = null!; - public List Enum32 { get; set; } = null!; - public List Enum64 { get; set; } = null!; - - public IList EnumU8 { get; set; } = null!; - public IList EnumU16 { get; set; } = null!; - public IList EnumU32 { get; set; } = null!; - public IList EnumU64 { get; set; } = null!; - - public sbyte[] Int8Converted { get; set; } = null!; - public List Int32Converted { get; set; } = null!; - public ObservableCollection UInt64Converted { get; set; } = null!; - public double[] DoubleConverted { get; set; } = null!; - public IList DateTimeConverted { get; set; } = null!; - public IList DateOnlyConverted { get; set; } = null!; - public IList StringConverted { get; set; } = null!; - public IList BytesConverted { get; set; } = null!; - public IList BooleanConverted { get; set; } = null!; - public IList CharacterConverted { get; set; } = null!; - public List Enum32Converted { get; set; } = null!; - public IList EnumU64Converted { get; set; } = null!; - - //public IList DddId { get; set; } = null!; // TODO Custom collection element - } - - protected class NullablePrimitiveTypeCollections - { - public sbyte?[] Int8 { get; set; } = null!; - public IList Int16 { get; set; } = null!; - public List Int32 { get; set; } = null!; - public IList Int64 { get; set; } = null!; - - public List UInt8 { get; set; } = null!; - public Collection UInt16 { get; set; } = null!; - public List UInt32 { get; set; } = null!; - public ObservableCollection UInt64 { get; set; } = null!; - - public float?[] Float { get; set; } = null!; - public double?[] Double { get; set; } = null!; - public decimal?[] Decimal { get; set; } = null!; - - public IList DateTime { get; set; } = null!; - public IList DateTimeOffset { get; set; } = null!; - public IList TimeSpan { get; set; } = null!; - public IList DateOnly { get; set; } = null!; - public IList TimeOnly { get; set; } = null!; - - public IList Guid { get; set; } = null!; - public IList String { get; set; } = null!; - public IList Bytes { get; set; } = null!; - - public IList Boolean { get; set; } = null!; - public IList Character { get; set; } = null!; - - public List Uri { get; set; } = null!; - public List PhysicalAddress { get; set; } = null!; - public List IpAddress { get; set; } = null!; - - public List Enum8 { get; set; } = null!; - public List Enum16 { get; set; } = null!; - public List Enum32 { get; set; } = null!; - public List Enum64 { get; set; } = null!; - - public IList EnumU8 { get; set; } = null!; - public IList EnumU16 { get; set; } = null!; - public IList EnumU32 { get; set; } = null!; - public IList EnumU64 { get; set; } = null!; - - public sbyte?[] Int8Converted { get; set; } = null!; - public List Int32Converted { get; set; } = null!; - public ObservableCollection UInt64Converted { get; set; } = null!; - public double?[] DoubleConverted { get; set; } = null!; - public IList DateTimeConverted { get; set; } = null!; - public IList DateOnlyConverted { get; set; } = null!; - public IList StringConverted { get; set; } = null!; - public IList BytesConverted { get; set; } = null!; - public IList BooleanConverted { get; set; } = null!; - public IList CharacterConverted { get; set; } = null!; - public List Enum32Converted { get; set; } = null!; - public IList EnumU64Converted { get; set; } = null!; - - //public IList DddId { get; set; } = null!; // TODO Custom collection element - } - public class CustomCollectionConverter : ValueConverter where T : class, IList { @@ -2842,166 +3701,44 @@ public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) } } - public abstract class JsonTypesFixtureBase : SharedStoreFixtureBase + protected class SingleTypeDbContext : DbContext { - private DbContext? _staticContext; + private readonly Action _buildOptions; + private readonly Action? _configureConventions; + private readonly Action _buildModel; + + public SingleTypeDbContext( + Action buildOptions, + Action buildModel, + Action? configureConventions = null) + { + _buildOptions = buildOptions; + _configureConventions = configureConventions; + _buildModel = buildModel; + } - protected override string StoreName - => "JsonTypes"; + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => _buildOptions(optionsBuilder.ReplaceService()); - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings( - w => w.Ignore( - CoreEventId.MappedEntityTypeIgnoredWarning, - CoreEventId.MappedPropertyIgnoredWarning, - CoreEventId.MappedNavigationIgnoredWarning)); + protected override void OnModelCreating(ModelBuilder modelBuilder) + => _buildModel(modelBuilder); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - => configurationBuilder.Properties().HaveConversion(); + => _configureConventions?.Invoke(configurationBuilder); - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + private class DegenerateCacheKeyFactory : IModelCacheKeyFactory { - modelBuilder.Entity( - b => - { - b.HasNoKey(); - }); + private static int _value; - modelBuilder.Entity( - b => - { - b.HasNoKey(); - }); - - modelBuilder.Entity( - b => - { - b.HasNoKey(); - b.Property(e => e.Int8).HasConversion(); - b.Property(e => e.Int16).HasConversion(); - b.Property(e => e.Int32).HasConversion(); - b.Property(e => e.Int64).HasConversion(); - b.Property(e => e.UInt8).HasConversion(); - b.Property(e => e.UInt16).HasConversion(); - b.Property(e => e.UInt32).HasConversion(); - b.Property(e => e.UInt64).HasConversion(); - b.Property(e => e.Float).HasConversion(); - b.Property(e => e.Double).HasConversion(); - b.Property(e => e.Decimal).HasConversion(); - b.Property(e => e.DateTime).HasConversion(); - b.Property(e => e.DateTimeOffset).HasConversion(); - b.Property(e => e.TimeSpan).HasConversion(); - b.Property(e => e.DateOnly).HasConversion(); - b.Property(e => e.TimeOnly).HasConversion(); - b.Property(e => e.Guid).HasConversion(); - b.Property(e => e.String).HasConversion(); - b.Property(e => e.Bytes).HasConversion(); - b.Property(e => e.Boolean).HasConversion(); - b.Property(e => e.Character).HasConversion(); - b.Property(e => e.Uri).HasConversion(); - b.Property(e => e.PhysicalAddress).HasConversion(); - b.Property(e => e.IpAddress).HasConversion(); - b.Property(e => e.Enum8).HasConversion(); - b.Property(e => e.Enum16).HasConversion(); - b.Property(e => e.Enum32).HasConversion(); - b.Property(e => e.Enum64).HasConversion(); - b.Property(e => e.EnumU8).HasConversion(); - b.Property(e => e.EnumU16).HasConversion(); - b.Property(e => e.EnumU32).HasConversion(); - b.Property(e => e.EnumU64).HasConversion(); - }); - - modelBuilder.Entity().HasNoKey(); - - modelBuilder.Entity( - b => - { - b.Property(e => e.Point).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.PointZ).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.PointM).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.PointZM).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.Geometry).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.LineString).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.MultiLineString).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - b.Property(e => e.Polygon).Metadata.SetJsonValueReaderWriterType(typeof(JsonGeoJsonReaderWriter)); - }); - - modelBuilder.Entity( - b => - { - b.HasNoKey(); - b.Property(e => e.Int8Converted) - .HasConversion, CustomCollectionComparer>(); - b.Property(e => e.Int32Converted) - .HasConversion, int>, CustomCollectionComparer, int>>(); - b.Property(e => e.UInt64Converted) - .HasConversion, ulong>, - CustomCollectionComparer, ulong>>(); - b.Property(e => e.DoubleConverted) - .HasConversion, CustomCollectionComparer>(); - b.Property(e => e.DateTimeConverted) - .HasConversion, DateTime>, - CustomCollectionComparer, DateTime>>(); - b.Property(e => e.DateOnlyConverted) - .HasConversion, DateOnly>, - CustomCollectionComparer, DateOnly>>(); - b.Property(e => e.StringConverted) - .HasConversion, string>, CustomCollectionComparer, string>>(); - b.Property(e => e.BytesConverted) - .HasConversion, byte[]>, CustomCollectionComparer, byte[]>>(); - b.Property(e => e.BooleanConverted) - .HasConversion, bool>, CustomCollectionComparer, bool>>(); - b.Property(e => e.CharacterConverted) - .HasConversion, char>, CustomCollectionComparer, char>>(); - b.Property(e => e.Enum32Converted) - .HasConversion, Enum32>, CustomCollectionComparer, Enum32>>(); - b.Property(e => e.EnumU64Converted) - .HasConversion, EnumU64>, - CustomCollectionComparer, EnumU64>>(); - }); - - modelBuilder.Entity( - b => - { - b.HasNoKey(); - b.Property(e => e.Int8Converted) - .HasConversion, CustomCollectionComparer>(); - b.Property(e => e.Int32Converted) - .HasConversion, int?>, CustomCollectionComparer, int?>>(); - b.Property(e => e.UInt64Converted) - .HasConversion, ulong?>, - CustomCollectionComparer, ulong?>>(); - b.Property(e => e.DoubleConverted) - .HasConversion, CustomCollectionComparer>(); - b.Property(e => e.DateTimeConverted) - .HasConversion, DateTime?>, - CustomCollectionComparer, DateTime?>>(); - b.Property(e => e.DateOnlyConverted) - .HasConversion, DateOnly?>, - CustomCollectionComparer, DateOnly?>>(); - b.Property(e => e.StringConverted) - .HasConversion, string?>, - CustomCollectionComparer, string?>>(); - b.Property(e => e.BytesConverted) - .HasConversion, byte[]?>, - CustomCollectionComparer, byte[]?>>(); - b.Property(e => e.BooleanConverted) - .HasConversion, bool?>, CustomCollectionComparer, bool?>>(); - b.Property(e => e.CharacterConverted) - .HasConversion, char?>, CustomCollectionComparer, char?>>(); - b.Property(e => e.Enum32Converted) - .HasConversion, Enum32?>, - CustomCollectionComparer, Enum32?>>(); - b.Property(e => e.EnumU64Converted) - .HasConversion, EnumU64?>, - CustomCollectionComparer, EnumU64?>>(); - }); + public object Create(DbContext context, bool designTime) + => _value++; } - - public DbContext StaticContext - => _staticContext ??= CreateContext(); - - public IEntityType EntityType() - => StaticContext.Model.FindEntityType(typeof(TEntity))!; } + + protected virtual void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.ConfigureWarnings( + w => w.Ignore( + CoreEventId.MappedEntityTypeIgnoredWarning, + CoreEventId.MappedPropertyIgnoredWarning, + CoreEventId.MappedNavigationIgnoredWarning)); } diff --git a/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs index e869ccb85c1..6966ab238d1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs @@ -5,13 +5,8 @@ namespace Microsoft.EntityFrameworkCore; -public class JsonTypesSqlServerTest : JsonTypesTestBase +public class JsonTypesSqlServerTest : JsonTypesRelationalTestBase { - public JsonTypesSqlServerTest(JsonTypesSqlServerFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - } - public override void Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json) { if (value == EnumU64.Max) @@ -33,8 +28,7 @@ public override void Can_read_write_nullable_ulong_enum_JSON_values(object? valu } public override void Can_read_write_collection_of_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(PrimitiveTypeCollections.EnumU64)), + => Can_read_and_write_JSON_value>(nameof(EnumU64CollectionType.EnumU64), new List { EnumU64.Min, @@ -43,11 +37,11 @@ public override void Can_read_write_collection_of_ulong_enum_JSON_values() EnumU64.One, (EnumU64)8 }, - """{"Prop":[0,-1,0,1,8]}"""); // Because ulong is converted to long on SQL Server + """{"Prop":[0,-1,0,1,8]}""", // Because ulong is converted to long on SQL Server + mappedCollection: true); public override void Can_read_write_collection_of_nullable_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value( - Fixture.EntityType().GetProperty(nameof(NullablePrimitiveTypeCollections.EnumU64)), + => Can_read_and_write_JSON_value>(nameof(NullableEnumU64CollectionType.EnumU64), new List { EnumU64.Min, @@ -55,25 +49,17 @@ public override void Can_read_write_collection_of_nullable_ulong_enum_JSON_value EnumU64.Max, EnumU64.Default, EnumU64.One, - (EnumU64)8 + (EnumU64?)8 }, - """{"Prop":[0,null,-1,0,1,8]}"""); // Because ulong is converted to long on SQL Server + """{"Prop":[0,null,-1,0,1,8]}""", // Because ulong is converted to long on SQL Server + mappedCollection: true); - public class JsonTypesSqlServerFixture : JsonTypesFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => SqlServerTestStoreFactory.Instance; + public override void Can_read_write_collection_of_fixed_length_string_JSON_values(object? storeType) + => base.Can_read_write_collection_of_fixed_length_string_JSON_values("nchar(32)"); - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new SqlServerDbContextOptionsBuilder(builder).UseNetTopologySuite(); - var options = base.AddOptions(builder).ConfigureWarnings( - c => c.Log(SqlServerEventId.DecimalTypeDefaultWarning)); - - return options; - } + public override void Can_read_write_collection_of_ASCII_string_JSON_values(object? storeType) + => base.Can_read_write_collection_of_ASCII_string_JSON_values("varchar(max)"); - protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection.AddEntityFrameworkSqlServerNetTopologySuite()); - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseSqlServer(b => b.UseNetTopologySuite())); } diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerApiConsistencyTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerApiConsistencyTest.cs index 2943e7541aa..09850be295b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerApiConsistencyTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerApiConsistencyTest.cs @@ -29,6 +29,8 @@ public class SqlServerApiConsistencyFixture : ApiConsistencyFixtureBase typeof(SqlServerKeyBuilderExtensions), typeof(SqlServerModelBuilderExtensions), typeof(SqlServerPropertyBuilderExtensions), + typeof(SqlServerPrimitiveCollectionBuilderExtensions), + typeof(SqlServerComplexTypePrimitiveCollectionBuilderExtensions), typeof(SqlServerEntityTypeBuilderExtensions), typeof(SqlServerServiceCollectionExtensions), typeof(SqlServerDbFunctionsExtensions), @@ -102,6 +104,16 @@ public override typeof(SqlServerIndexBuilderExtensions), null ) + }, + { + typeof(IReadOnlyElementType), + ( + null, + null, + null, + typeof(SqlServerEntityTypeBuilderExtensions), + null + ) } }; @@ -114,6 +126,8 @@ protected override void Initialize() MirrorTypes.Add(typeof(TemporalTableBuilder<>), typeof(OwnedNavigationTemporalTableBuilder<,>)); MirrorTypes.Add(typeof(TemporalPeriodPropertyBuilder), typeof(OwnedNavigationTemporalPeriodPropertyBuilder)); MirrorTypes.Add(typeof(SqlServerPropertyBuilderExtensions), typeof(SqlServerComplexTypePropertyBuilderExtensions)); + MirrorTypes.Add(typeof(SqlServerPrimitiveCollectionBuilderExtensions), typeof(SqlServerPropertyBuilderExtensions)); + MirrorTypes.Add(typeof(SqlServerComplexTypePrimitiveCollectionBuilderExtensions), typeof(SqlServerComplexTypePropertyBuilderExtensions)); base.Initialize(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs index d820f27c429..467a1ac6725 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerEndToEndTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; // ReSharper disable StringStartsWithIsCultureSpecific // ReSharper disable VirtualMemberCallInConstructor diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs index 4a5b9142fa1..82c1a10d146 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -3,6 +3,8 @@ #nullable enable +using System.Collections.ObjectModel; + namespace Microsoft.EntityFrameworkCore.ModelBuilding; public class SqlServerModelBuilderTestBase : RelationalModelBuilderTest @@ -194,6 +196,73 @@ public virtual void Can_set_collation_for_property_type() Assert.Equal("Latin1_General_BIN", entityType.FindProperty("Bottom")!.GetCollation()); } + [ConditionalFact] + public virtual void Can_set_store_type_for_primitive_collection() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).HasColumnType("national character varying(255)"); + b.PrimitiveCollection(e => e.Down).HasColumnType("nchar(10)"); + b.PrimitiveCollection("Charm").HasColumnType("nvarchar(25)"); + b.PrimitiveCollection("Strange").HasColumnType("text"); + b.PrimitiveCollection>("Top").HasColumnType("char(100)");; + b.PrimitiveCollection?>("Bottom").HasColumnType("varchar(max)");; + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Equal("int", entityType.FindProperty(nameof(CollectionQuarks.Id))!.GetColumnType()); + Assert.Equal("national character varying(255)", entityType.FindProperty("Up")!.GetColumnType()); + Assert.Equal("nchar(10)", entityType.FindProperty("Down")!.GetColumnType()); + Assert.Equal("nvarchar(25)", entityType.FindProperty("Charm")!.GetColumnType()); + Assert.Equal("text", entityType.FindProperty("Strange")!.GetColumnType()); + Assert.Equal("char(100)", entityType.FindProperty("Top")!.GetColumnType()); + Assert.Equal("varchar(max)", entityType.FindProperty("Bottom")!.GetColumnType()); + } + + [ConditionalFact] + public virtual void Can_set_fixed_length_for_primitive_collection() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).IsFixedLength(false); + b.PrimitiveCollection(e => e.Down).IsFixedLength(); + b.PrimitiveCollection("Charm").IsFixedLength(true); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.False(entityType.FindProperty("Up")!.IsFixedLength()); + Assert.True(entityType.FindProperty("Down")!.IsFixedLength()); + Assert.True(entityType.FindProperty("Charm")!.IsFixedLength()); + } + + [ConditionalFact] + public virtual void Can_set_collation_for_primitive_collection() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).UseCollation("Latin1_General_CS_AS_KS_WS"); + b.PrimitiveCollection(e => e.Down).UseCollation("Latin1_General_BIN"); + b.PrimitiveCollection("Charm").UseCollation("Latin1_General_CI_AI"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Equal("Latin1_General_CS_AS_KS_WS", entityType.FindProperty("Up")!.GetCollation()); + Assert.Equal("Latin1_General_BIN", entityType.FindProperty("Down")!.GetCollation()); + Assert.Equal("Latin1_General_CI_AI", entityType.FindProperty("Charm")!.GetCollation()); + } + protected override TestModelBuilder CreateModelBuilder(Action? configure = null) => CreateTestModelBuilder(SqlServerTestHelpers.Instance, configure); } @@ -1257,7 +1326,7 @@ public virtual void Temporal_table_with_history_table_configuration() var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.Equal(5, entity.GetProperties().Count()); + Assert.Equal(6, entity.GetProperties().Count()); Assert.Equal("HistoryTable", entity.GetHistoryTableName()); Assert.Equal("historySchema", entity.GetHistoryTableSchema()); @@ -1306,7 +1375,7 @@ public virtual void Temporal_table_with_changed_configuration() var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.Equal(5, entity.GetProperties().Count()); + Assert.Equal(6, entity.GetProperties().Count()); Assert.Equal("ChangedHistoryTable", entity.GetHistoryTableName()); Assert.Equal("changedHistorySchema", entity.GetHistoryTableSchema()); @@ -1355,7 +1424,7 @@ public virtual void Temporal_table_with_period_column_names_changed_configuratio var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.Equal(5, entity.GetProperties().Count()); + Assert.Equal(6, entity.GetProperties().Count()); Assert.Equal("ChangedHistoryTable", entity.GetHistoryTableName()); Assert.Equal("changedHistorySchema", entity.GetHistoryTableSchema()); @@ -1405,7 +1474,7 @@ public virtual void Temporal_table_with_explicit_properties_mapped_to_the_period var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.Equal(5, entity.GetProperties().Count()); + Assert.Equal(6, entity.GetProperties().Count()); Assert.Equal("HistoryTable", entity.GetHistoryTableName()); @@ -1453,7 +1522,7 @@ public virtual void var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.Equal(7, entity.GetProperties().Count()); + Assert.Equal(8, entity.GetProperties().Count()); Assert.Equal("HistoryTable", entity.GetHistoryTableName()); @@ -1496,7 +1565,7 @@ public virtual void Switching_from_temporal_to_non_temporal_default_settings() Assert.False(entity.IsTemporal()); Assert.Null(entity.GetPeriodStartPropertyName()); Assert.Null(entity.GetPeriodEndPropertyName()); - Assert.Equal(3, entity.GetProperties().Count()); + Assert.Equal(4, entity.GetProperties().Count()); } [ConditionalFact] diff --git a/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs index c11b5149af7..edcc2558762 100644 --- a/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs @@ -6,26 +6,8 @@ namespace Microsoft.EntityFrameworkCore; [SpatialiteRequired] -public class JsonTypesSqliteTest : JsonTypesTestBase +public class JsonTypesSqliteTest : JsonTypesRelationalTestBase { - public JsonTypesSqliteTest(JsonTypesSqliteFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - } - - public class JsonTypesSqliteFixture : JsonTypesFixtureBase - { - protected override ITestStoreFactory TestStoreFactory - => SqliteTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - new SqliteDbContextOptionsBuilder(builder).UseNetTopologySuite(); - - return base.AddOptions(builder); - } - - protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection.AddEntityFrameworkSqliteNetTopologySuite()); - } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseSqlite(b => b.UseNetTopologySuite())); } diff --git a/test/EFCore.Sqlite.FunctionalTests/SqliteApiConsistencyTest.cs b/test/EFCore.Sqlite.FunctionalTests/SqliteApiConsistencyTest.cs index f9f6ce90fcf..44f972be99a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/SqliteApiConsistencyTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/SqliteApiConsistencyTest.cs @@ -25,7 +25,8 @@ public class SqliteApiConsistencyFixture : ApiConsistencyFixtureBase typeof(SqliteServiceCollectionExtensions), typeof(SqliteDbContextOptionsBuilderExtensions), typeof(SqliteDbContextOptionsBuilder), - typeof(SqlitePropertyBuilderExtensions) + typeof(SqlitePropertyBuilderExtensions), + typeof(SqlitePrimitiveCollectionBuilderExtensions) }; public override @@ -49,5 +50,14 @@ public override ) } }; + + protected override void Initialize() + { + MirrorTypes.Add(typeof(SqlitePropertyBuilderExtensions), typeof(SqliteComplexTypePropertyBuilderExtensions)); + MirrorTypes.Add(typeof(SqlitePrimitiveCollectionBuilderExtensions), typeof(SqlitePropertyBuilderExtensions)); + MirrorTypes.Add(typeof(SqliteComplexTypePrimitiveCollectionBuilderExtensions), typeof(SqliteComplexTypePropertyBuilderExtensions)); + + base.Initialize(); + } } } diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 045a14b0cf7..72ddd5f9649 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -46,8 +46,11 @@ protected override void Initialize() typeof(DiscriminatorBuilder<>), typeof(EntityTypeBuilder), typeof(EntityTypeBuilder<>), + typeof(ElementTypeBuilder), typeof(ComplexPropertyBuilder), typeof(ComplexPropertyBuilder<>), + typeof(ComplexTypePrimitiveCollectionBuilder), + typeof(ComplexTypePrimitiveCollectionBuilder<>), typeof(IndexBuilder), typeof(IndexBuilder<>), typeof(TriggerBuilder), @@ -64,6 +67,8 @@ protected override void Initialize() typeof(OwnershipBuilder<,>), typeof(PropertyBuilder), typeof(PropertyBuilder<>), + typeof(PrimitiveCollectionBuilder), + typeof(PrimitiveCollectionBuilder<>), typeof(ComplexTypePropertyBuilder), typeof(ComplexTypePropertyBuilder<>), typeof(ReferenceCollectionBuilder), @@ -118,6 +123,8 @@ protected override void Initialize() public override HashSet UnmatchedMetadataMethods { get; } = new() { + typeof(PropertyBuilder).GetMethod( + nameof(PropertyBuilder.HasValueGenerator), 0, new[] { typeof(Func) }), typeof(ComplexPropertyBuilder).GetMethod( nameof(ComplexPropertyBuilder.ComplexProperty), 0, new[] { typeof(string) }), typeof(ComplexPropertyBuilder).GetMethod( diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index 4390dc6f752..177d4c9bbe4 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -3663,6 +3663,106 @@ public void ProcessPropertyFieldChanged( } } + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnPropertyElementTypeChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new PropertyElementTypeChangedConvention(terminate: false); + var convention2 = new PropertyElementTypeChangedConvention(terminate: true); + var convention3 = new PropertyElementTypeChangedConvention(terminate: false); + conventions.PropertyElementTypeChangedConventions.Add(convention1); + conventions.PropertyElementTypeChangedConventions.Add(convention2); + conventions.PropertyElementTypeChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var entityBuilder = builder.Entity(typeof(Order), ConfigurationSource.Convention)!; + var propertyBuilder = entityBuilder.Property(Order.OrderIdsProperty, ConfigurationSource.Convention)!; + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + ElementType elementType; + + if (useBuilder) + { + Assert.NotNull(propertyBuilder.ElementType(true, ConfigurationSource.Convention)); + elementType = (ElementType)propertyBuilder.Metadata.GetElementType()!; + } + else + { + elementType = (ElementType)propertyBuilder.Metadata.ElementType(true, ConfigurationSource.Convention); + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new (object, object)[] { (null, elementType) }, convention1.Calls); + Assert.Equal(new (object, object)[] { (null, elementType) }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.Null(propertyBuilder.ElementType(true, ConfigurationSource.Convention)); + elementType = (ElementType)propertyBuilder.Metadata.GetElementType()!; + } + else + { + elementType = (ElementType)propertyBuilder.Metadata.ElementType(true, ConfigurationSource.Convention); + } + + Assert.Equal(new (object, object)[] { (null, elementType) }, convention1.Calls); + Assert.Equal(new (object, object)[] { (null, elementType) }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(propertyBuilder.ElementType(false, ConfigurationSource.Convention)); + } + else + { + Assert.Null(propertyBuilder.Metadata.ElementType(false, ConfigurationSource.Convention)); + } + + Assert.Equal(new (object, object)[] { (null, elementType), (elementType, null) }, convention1.Calls); + Assert.Equal(new (object, object)[] { (null, elementType), (elementType, null) }, convention2.Calls); + Assert.Empty(convention3.Calls); + } + + private class PropertyElementTypeChangedConvention : IPropertyElementTypeChangedConvention + { + private readonly bool _terminate; + public readonly List<(object, object)> Calls = new(); + + public PropertyElementTypeChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessPropertyElementTypeChanged( + IConventionPropertyBuilder propertyBuilder, + IElementType newElementType, + IElementType oldElementType, + IConventionContext context) + { + Assert.True(propertyBuilder.Metadata.IsInModel); + + Calls.Add((oldElementType, newElementType)); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] @@ -4950,9 +5050,250 @@ public void ProcessComplexTypeMemberIgnored( } } + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnElementTypeAnnotationChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new ElementTypeAnnotationChangedConvention(false); + var convention2 = new ElementTypeAnnotationChangedConvention(true); + var convention3 = new ElementTypeAnnotationChangedConvention(false); + conventions.ElementTypeAnnotationChangedConventions.Add(convention1); + conventions.ElementTypeAnnotationChangedConventions.Add(convention2); + conventions.ElementTypeAnnotationChangedConventions.Add(convention3); + + var builder = new InternalModelBuilder(new Model(conventions)); + var elementTypeBuilder = builder.Entity(typeof(SpecialOrder), ConfigurationSource.Convention)! + .Property(nameof(SpecialOrder.OrderIds), ConfigurationSource.Convention)! + .ElementType(true, ConfigurationSource.Convention)!; + + var scope = useScope ? builder.Metadata.ConventionDispatcher.DelayConventions() : null; + + if (useBuilder) + { + Assert.NotNull(elementTypeBuilder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + elementTypeBuilder.Metadata["foo"] = "bar"; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + scope.Dispose(); + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(elementTypeBuilder.HasAnnotation("foo", "bar", ConfigurationSource.Convention)); + } + else + { + elementTypeBuilder.Metadata["foo"] = "bar"; + } + + Assert.Equal(new[] { "bar" }, convention1.Calls); + Assert.Equal(new[] { "bar" }, convention2.Calls); + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + Assert.NotNull(elementTypeBuilder.HasAnnotation("foo", null, ConfigurationSource.Convention)); + } + else + { + elementTypeBuilder.Metadata.RemoveAnnotation("foo"); + } + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + Assert.Equal(new[] { "bar", null }, convention2.Calls); + Assert.Empty(convention3.Calls); + + elementTypeBuilder.Metadata[CoreAnnotationNames.AfterSaveBehavior] = PropertySaveBehavior.Ignore; + + Assert.Equal(new[] { "bar", null }, convention1.Calls); + } + + private class ElementTypeAnnotationChangedConvention : IElementTypeAnnotationChangedConvention + { + private readonly bool _terminate; + public readonly List Calls = new(); + + public ElementTypeAnnotationChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessElementTypeAnnotationChanged( + IConventionElementTypeBuilder builder, + string name, + IConventionAnnotation annotation, + IConventionAnnotation oldAnnotation, + IConventionContext context) + { + Assert.True(builder.Metadata.IsInModel); + + Calls.Add(annotation?.Value); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + [ConditionalTheory] + public void OnElementTypeNullabilityChanged_calls_conventions_in_order(bool useBuilder, bool useScope) + { + var conventions = new ConventionSet(); + + var convention1 = new ElementTypeNullabilityChangedConvention(false); + var convention2 = new ElementTypeNullabilityChangedConvention(true); + var convention3 = new ElementTypeNullabilityChangedConvention(false); + conventions.ElementTypeNullabilityChangedConventions.Add(convention1); + conventions.ElementTypeNullabilityChangedConventions.Add(convention2); + conventions.ElementTypeNullabilityChangedConventions.Add(convention3); + + var model = new Model(conventions); + var scope = useScope ? model.DelayConventions() : null; + + var builder = new InternalModelBuilder(model); + var elementTypeBuilder = builder.Entity(typeof(SpecialOrder), ConfigurationSource.Convention)! + .Property(nameof(SpecialOrder.Notes), ConfigurationSource.Convention)! + .ElementType(true, ConfigurationSource.Convention)!; + + if (useBuilder) + { + elementTypeBuilder.IsRequired(true, ConfigurationSource.Convention); + } + else + { + elementTypeBuilder.Metadata.IsNullable = false; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + } + else + { + Assert.Equal(new bool?[] { false }, convention1.Calls); + Assert.Equal(new bool?[] { false }, convention2.Calls); + } + + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + elementTypeBuilder.IsRequired(false, ConfigurationSource.Convention); + } + else + { + elementTypeBuilder.Metadata.IsNullable = true; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + } + else + { + Assert.Equal(new bool?[] { false, true }, convention1.Calls); + Assert.Equal(new bool?[] { false, true }, convention2.Calls); + } + + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + elementTypeBuilder.IsRequired(false, ConfigurationSource.Convention); + } + else + { + elementTypeBuilder.Metadata.IsNullable = true; + } + + if (useScope) + { + Assert.Empty(convention1.Calls); + Assert.Empty(convention2.Calls); + } + else + { + Assert.Equal(new bool?[] { false, true }, convention1.Calls); + Assert.Equal(new bool?[] { false, true }, convention2.Calls); + } + + Assert.Empty(convention3.Calls); + + if (useBuilder) + { + elementTypeBuilder.IsRequired(true, ConfigurationSource.Convention); + } + else + { + elementTypeBuilder.Metadata.IsNullable = false; + } + + scope?.Dispose(); + + if (useScope) + { + Assert.Equal(new bool?[] { false, false, false }, convention1.Calls); + Assert.Equal(new bool?[] { false, false, false }, convention2.Calls); + } + else + { + Assert.Equal(new bool?[] { false, true, false }, convention1.Calls); + Assert.Equal(new bool?[] { false, true, false }, convention2.Calls); + } + + Assert.Empty(convention3.Calls); + } + + private class ElementTypeNullabilityChangedConvention : IElementTypeNullabilityChangedConvention + { + public readonly List Calls = new(); + private readonly bool _terminate; + + public ElementTypeNullabilityChangedConvention(bool terminate) + { + _terminate = terminate; + } + + public void ProcessElementTypeNullabilityChanged( + IConventionElementTypeBuilder builder, + IConventionContext context) + { + Calls.Add(builder.Metadata.IsNullable); + + if (_terminate) + { + context.StopProcessing(); + } + } + } + private class Order { public static readonly PropertyInfo OrderIdProperty = typeof(Order).GetProperty(nameof(OrderId)); + public static readonly PropertyInfo OrderIdsProperty = typeof(Order).GetProperty(nameof(OrderIds)); public static readonly PropertyInfo OrderDetailsProperty = typeof(Order).GetProperty(nameof(OrderDetails)); public static readonly PropertyInfo OtherOrderDetailsProperty = typeof(Order).GetProperty(nameof(OtherOrderDetails)); @@ -4960,6 +5301,8 @@ private class Order public readonly OrderDetails OrderDetailsField = default; public int OrderId { get; set; } + public int[] OrderIds { get; set; } + public string[] Notes { get; set; } public string Name { get; set; } diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index 087a06dfcf6..79f741b22b6 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -82,6 +82,9 @@ public ValueComparer GetProviderValueComparer() public JsonValueReaderWriter GetJsonValueReaderWriter() => throw new NotImplementedException(); + public IElementType GetElementType() + => throw new NotImplementedException(); + public bool IsForeignKey() => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs index 73b8bf1a464..482694c17f7 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs @@ -100,6 +100,9 @@ public ValueComparer GetProviderValueComparer() public JsonValueReaderWriter GetJsonValueReaderWriter() => throw new NotImplementedException(); + public IElementType GetElementType() + => throw new NotImplementedException(); + public bool IsForeignKey() => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs b/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs index 8433d7c4629..a14ff66393d 100644 --- a/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.ObjectModel; using System.Dynamic; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -31,14 +32,14 @@ public virtual void Can_set_complex_property_annotation() Assert.Equal("bar", complexProperty.ComplexType["foo"]); Assert.Equal("bar2", complexProperty["foo2"]); Assert.Equal(typeof(Customer).Name, complexProperty.Name); - Assert.Equal(""" -Customer (Customer) Required + Assert.Equal( +@"Customer (Customer) Required ComplexType: ComplexProperties.Customer#Customer - Properties: + Properties: " + @" AlternateKey (Guid) Required Id (int) Required Name (string) -""", complexProperty.ToDebugString(), ignoreLineEndingDifferences: true); + Notes (List)", complexProperty.ToDebugString(), ignoreLineEndingDifferences: true); } [ConditionalFact] @@ -396,22 +397,40 @@ public virtual void Properties_can_have_access_mode_set() .Ignore() .Ignore() .Entity() - .ComplexProperty(e => e.Quarks, + .ComplexProperty( + e => e.Quarks, b => { b.Property(e => e.Up); b.Property(e => e.Down).HasField("_forDown").UsePropertyAccessMode(PropertyAccessMode.Field); b.Property("Charm").UsePropertyAccessMode(PropertyAccessMode.Property); b.Property("Strange").UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + }) + .ComplexProperty( + e => e.CollectionQuarks, + b => + { + b.UsePropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction); + b.UseDefaultPropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + b.PrimitiveCollection(e => e.Up).UsePropertyAccessMode(PropertyAccessMode.Property); + b.PrimitiveCollection(e => e.Down).HasField("_forDown"); }); var model = modelBuilder.FinalizeModel(); - var complexType = model.FindEntityType(typeof(ComplexProperties)).GetComplexProperties().Single().ComplexType; - Assert.Equal(PropertyAccessMode.PreferField, complexType.FindProperty("Up").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Field, complexType.FindProperty("Down").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Property, complexType.FindProperty("Charm").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.FieldDuringConstruction, complexType.FindProperty("Strange").GetPropertyAccessMode()); + var quarksType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties() + .Single(p => p.Name == nameof(Quarks)).ComplexType; + + Assert.Equal(PropertyAccessMode.PreferField, quarksType.FindProperty("Up")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Field, quarksType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Charm")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Strange")!.GetPropertyAccessMode()); + + quarksType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties() + .Single(p => p.Name == nameof(CollectionQuarks)).ComplexType; + + Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Up")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Down")!.GetPropertyAccessMode()); } [ConditionalFact] @@ -434,22 +453,33 @@ public virtual void Access_mode_can_be_overridden_at_entity_and_property_levels( b.UseDefaultPropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); b.Property(e => e.Up).UsePropertyAccessMode(PropertyAccessMode.Property); b.Property(e => e.Down).HasField("_forDown"); + }) + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.UsePropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction); + b.UseDefaultPropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + b.PrimitiveCollection(e => e.Up).UsePropertyAccessMode(PropertyAccessMode.Property); + b.PrimitiveCollection(e => e.Down).HasField("_forDown"); }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(ComplexProperties)); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; Assert.Equal(PropertyAccessMode.Field, model.GetPropertyAccessMode()); - var customerType = entityType.FindComplexProperty(nameof(ComplexProperties.Customer)).ComplexType; - Assert.Equal(PropertyAccessMode.Field, customerType.GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Field, customerType.FindProperty("Id").GetPropertyAccessMode()); - - var quarksProperty = entityType.FindComplexProperty(nameof(ComplexProperties.Quarks)); + var quarksProperty = entityType.FindComplexProperty(nameof(ComplexProperties.Quarks))!; var quarksType = quarksProperty.ComplexType; Assert.Equal(PropertyAccessMode.PreferFieldDuringConstruction, quarksProperty.GetPropertyAccessMode()); Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Down").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Up").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Up")!.GetPropertyAccessMode()); + + quarksProperty = entityType.FindComplexProperty(nameof(ComplexProperties.CollectionQuarks))!; + quarksType = quarksProperty.ComplexType; + Assert.Equal(PropertyAccessMode.PreferFieldDuringConstruction, quarksProperty.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Up")!.GetPropertyAccessMode()); } [ConditionalFact] @@ -1282,7 +1312,6 @@ public virtual void Can_set_custom_value_generator_for_properties() { b.Property(e => e.Up).HasValueGenerator(); b.Property(e => e.Down).HasValueGenerator(typeof(CustomValueGenerator)); - b.Property("Charm").HasValueGenerator((_, __) => new CustomValueGenerator()); b.Property("Strange").HasValueGenerator(); b.Property("Top").HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)); b.Property("Bottom").HasValueGeneratorFactory(); @@ -1295,7 +1324,6 @@ public virtual void Can_set_custom_value_generator_for_properties() Assert.Null(complexType.FindProperty(Customer.IdProperty.Name).GetValueGeneratorFactory()); Assert.IsType(complexType.FindProperty("Up").GetValueGeneratorFactory()(null, null)); Assert.IsType(complexType.FindProperty("Down").GetValueGeneratorFactory()(null, null)); - Assert.IsType(complexType.FindProperty("Charm").GetValueGeneratorFactory()(null, null)); Assert.IsType(complexType.FindProperty("Strange").GetValueGeneratorFactory()(null, null)); Assert.IsType(complexType.FindProperty("Top").GetValueGeneratorFactory()(null, null)); Assert.IsType(complexType.FindProperty("Bottom").GetValueGeneratorFactory()(null, null)); @@ -1465,7 +1493,6 @@ public virtual void PropertyBuilder_methods_can_be_chained() .HasValueGenerator(typeof(CustomValueGenerator)) .HasValueGeneratorFactory() .HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)) - .HasValueGenerator((_, __) => null) .IsRequired(); [ConditionalFact] @@ -1481,7 +1508,7 @@ public virtual void Can_call_Property_on_a_field() var model = modelBuilder.FinalizeModel(); var complexType = model.FindEntityType(typeof(ComplexProperties)).GetComplexProperties().Single().ComplexType; - Assert.Equal(3, complexType.GetProperties().Count()); + Assert.Equal(6, complexType.GetProperties().Count()); var property = complexType.FindProperty(nameof(EntityWithFields.Id)); Assert.Null(property.PropertyInfo); Assert.NotNull(property.FieldInfo); @@ -1503,7 +1530,7 @@ public virtual void Can_ignore_a_field() var model = modelBuilder.FinalizeModel(); var complexProperty = model.FindEntityType(typeof(ComplexProperties)).GetComplexProperties().Single(); - Assert.Equal(2, complexProperty.ComplexType.GetProperties().Count()); + Assert.Equal(5, complexProperty.ComplexType.GetProperties().Count()); } [ConditionalFact] @@ -1640,6 +1667,7 @@ protected virtual void Mapping_throws_for_empty_complex_types() modelBuilder .Entity() .ComplexProperty(e => e.Customer) + .Ignore(c => c.Notes) .Ignore(c => c.Name) .Ignore(c => c.Id) .Ignore(c => c.AlternateKey); @@ -1649,5 +1677,479 @@ protected virtual void Mapping_throws_for_empty_complex_types() "ComplexProperties.Customer#Customer"), Assert.Throws(modelBuilder.FinalizeModel).Message); } + + [ConditionalFact] + public virtual void Can_set_primitive_collection_annotation_when_no_clr_property() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.Customer) + .Ignore(c => c.Details) + .Ignore(c => c.Orders) + .PrimitiveCollection("Ints").HasAnnotation("foo", "bar"); + + var model = modelBuilder.FinalizeModel(); + var complexProperty = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single(); + var property = complexProperty.ComplexType.FindProperty("Ints")!; + + Assert.Equal("bar", property["foo"]); + } + + [ConditionalFact] + public virtual void Primitive_collections_are_required_by_default_only_if_CLR_type_is_nullable() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange"); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.False(complexType.FindProperty("Up")!.IsNullable); + Assert.True(complexType.FindProperty("Down")!.IsNullable); + Assert.True(complexType.FindProperty("Charm")!.IsNullable); // Because we can't detect the non-nullable reference type + Assert.True(complexType.FindProperty("Strange")!.IsNullable); + } + + public virtual void Can_ignore_shadow_primitive_collections_when_they_have_been_added_explicitly() + { + var modelBuilder = CreateModelBuilder(); + + var complexPropertyBuilder = modelBuilder + .Ignore() + .Entity() + .ComplexProperty(e => e.Customer, b => b.Ignore(c => c.Details).Ignore(c => c.Orders)); + complexPropertyBuilder.PrimitiveCollection("Shadow"); + complexPropertyBuilder.Ignore("Shadow"); + + var model = modelBuilder.FinalizeModel(); + + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + Assert.Null(complexType.FindProperty("Shadow")); + } + + [ConditionalFact] + public virtual void Can_add_shadow_primitive_collections_when_they_have_been_ignored() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.Customer, + b => + { + b.Ignore(c => c.Details); + b.Ignore(c => c.Orders); + b.Ignore("Shadow"); + b.PrimitiveCollection("Shadow"); + }); + + var model = modelBuilder.FinalizeModel(); + + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + Assert.NotNull(complexType.FindProperty("Shadow")); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_made_required() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).IsRequired(); + b.PrimitiveCollection(e => e.Down).IsRequired(); + b.PrimitiveCollection>("Charm").IsRequired(); + b.PrimitiveCollection>("Strange").IsRequired(); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.False(complexType.FindProperty("Up")!.IsNullable); + Assert.False(complexType.FindProperty("Down")!.IsNullable); + Assert.False(complexType.FindProperty("Charm")!.IsNullable); + Assert.False(complexType.FindProperty("Strange")!.IsNullable); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_made_optional() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).IsRequired(false); + b.PrimitiveCollection(e => e.Down).IsRequired(false); + b.PrimitiveCollection>("Charm").IsRequired(false); + b.PrimitiveCollection>("Strange").IsRequired(false); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.True(complexType.FindProperty("Up")!.IsNullable); + Assert.True(complexType.FindProperty("Down")!.IsNullable); + Assert.True(complexType.FindProperty("Charm")!.IsNullable); + Assert.True(complexType.FindProperty("Strange")!.IsNullable); + } + + [ConditionalFact] + public virtual void Primitive_collections_specified_by_string_are_shadow_properties_unless_already_known_to_be_CLR_properties() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection>("Up"); + b.PrimitiveCollection>("Down"); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange"); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.False(complexType.FindProperty("Up")!.IsShadowProperty()); + Assert.False(complexType.FindProperty("Down")!.IsShadowProperty()); + Assert.True(complexType.FindProperty("Charm")!.IsShadowProperty()); + Assert.True(complexType.FindProperty("Strange")!.IsShadowProperty()); + + Assert.Equal(-1, complexType.FindProperty("Up")!.GetShadowIndex()); + Assert.Equal(-1, complexType.FindProperty("Down")!.GetShadowIndex()); + Assert.NotEqual(-1, complexType.FindProperty("Charm")!.GetShadowIndex()); + Assert.NotEqual(-1, complexType.FindProperty("Strange")!.GetShadowIndex()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_made_concurrency_tokens() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).IsConcurrencyToken(); + b.PrimitiveCollection(e => e.Down).IsConcurrencyToken(false); + b.PrimitiveCollection>("Charm").IsConcurrencyToken(); + b.PrimitiveCollection>("Strange").IsConcurrencyToken(false); + b.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties()!.Single().ComplexType; + + Assert.True(complexType.FindProperty("Up")!.IsConcurrencyToken); + Assert.False(complexType.FindProperty("Down")!.IsConcurrencyToken); + Assert.True(complexType.FindProperty("Charm")!.IsConcurrencyToken); + Assert.False(complexType.FindProperty("Strange")!.IsConcurrencyToken); + + Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, complexType.GetChangeTrackingStrategy()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_have_field_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection>("Up").HasField("_forUp"); + b.PrimitiveCollection(e => e.Down).HasField("_forDown"); + b.PrimitiveCollection>("_forWierd").HasField("_forWierd"); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.Equal("_forUp", complexType.FindProperty("Up")!.GetFieldName()); + Assert.Equal("_forDown", complexType.FindProperty("Down")!.GetFieldName()); + Assert.Equal("_forWierd", complexType.FindProperty("_forWierd")!.GetFieldName()); + } + + [ConditionalFact] + public virtual void HasField_for_primitive_collection_throws_if_field_is_not_found() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + Assert.Equal( + CoreStrings.MissingBackingField("_notFound", nameof(CollectionQuarks.Down), "ComplexProperties.CollectionQuarks#CollectionQuarks"), + Assert.Throws(() => b.Property(e => e.Down).HasField("_notFound")).Message); + }); + } + + [ConditionalFact] + public virtual void HasField_for_primitive_collection_throws_if_field_is_wrong_type() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + Assert.Equal( + CoreStrings.BadBackingFieldType("_forUp", "ObservableCollection", nameof(CollectionQuarks), nameof(CollectionQuarks.Down), "ObservableCollection"), + Assert.Throws(() => b.Property(e => e.Down).HasField("_forUp")).Message); + }); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_set_to_generate_values_on_Add() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).ValueGeneratedOnAddOrUpdate(); + b.PrimitiveCollection(e => e.Down).ValueGeneratedNever(); + b.PrimitiveCollection>("Charm").Metadata.ValueGenerated = ValueGenerated.OnUpdateSometimes; + b.PrimitiveCollection>("Strange").ValueGeneratedNever(); + b.PrimitiveCollection>("Top").ValueGeneratedOnAddOrUpdate(); + b.PrimitiveCollection>("Bottom").ValueGeneratedOnUpdate(); + }); + + var model = modelBuilder.FinalizeModel(); + + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + Assert.Equal(ValueGenerated.OnAddOrUpdate, complexType.FindProperty("Up")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, complexType.FindProperty("Down")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdateSometimes, complexType.FindProperty("Charm")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, complexType.FindProperty("Strange")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnAddOrUpdate, complexType.FindProperty("Top")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdate, complexType.FindProperty("Bottom")!.ValueGenerated); + } + + [ConditionalFact] + public virtual void Can_set_max_length_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).HasMaxLength(0); + b.PrimitiveCollection(e => e.Down).HasMaxLength(100); + b.PrimitiveCollection>("Charm").HasMaxLength(0); + b.PrimitiveCollection>("Strange").HasMaxLength(-1); + b.PrimitiveCollection("Top").HasMaxLength(0); + b.PrimitiveCollection("Bottom").HasMaxLength(100); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.Equal(0, complexType.FindProperty("Up")!.GetMaxLength()); + Assert.Equal(100, complexType.FindProperty("Down")!.GetMaxLength()); + Assert.Equal(0, complexType.FindProperty("Charm")!.GetMaxLength()); + Assert.Equal(-1, complexType.FindProperty("Strange")!.GetMaxLength()); + Assert.Equal(0, complexType.FindProperty("Top")!.GetMaxLength()); + Assert.Equal(100, complexType.FindProperty("Bottom")!.GetMaxLength()); + } + + [ConditionalFact] + public virtual void Can_set_sentinel_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).HasSentinel(1); + b.PrimitiveCollection(e => e.Down).HasSentinel("100"); + b.PrimitiveCollection("Charm").HasSentinel(-1); + b.PrimitiveCollection>("Strange").HasSentinel("-1"); + b.PrimitiveCollection("Top").HasSentinel(77); + b.PrimitiveCollection>("Bottom").HasSentinel("100"); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.Equal(0, complexType.FindProperty(nameof(CollectionQuarks.Id))!.Sentinel); + Assert.Equal(1, complexType.FindProperty("Up")!.Sentinel); + Assert.Equal("100", complexType.FindProperty("Down")!.Sentinel); + Assert.Equal(-1, complexType.FindProperty("Charm")!.Sentinel); + Assert.Equal("-1", complexType.FindProperty("Strange")!.Sentinel); + Assert.Equal(77, complexType.FindProperty("Top")!.Sentinel); + Assert.Equal("100", complexType.FindProperty("Bottom")!.Sentinel); + } + + [ConditionalFact] + public virtual void Can_set_custom_value_generator_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).HasValueGenerator(); + b.PrimitiveCollection(e => e.Down).HasValueGenerator(typeof(CustomValueGenerator)); + b.PrimitiveCollection>("Strange").HasValueGenerator(); + b.PrimitiveCollection("Top").HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)); + b.PrimitiveCollection>("Bottom").HasValueGeneratorFactory(); + }); + + var model = modelBuilder.FinalizeModel(); + + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.IsType(complexType.FindProperty("Up")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(complexType.FindProperty("Down")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(complexType.FindProperty("Strange")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(complexType.FindProperty("Top")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(complexType.FindProperty("Bottom")!.GetValueGeneratorFactory()!(null!, null!)); + } + + [ConditionalFact] + public virtual void Throws_for_primitive_collection_with_bad_value_generator_type() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + Assert.Equal( + CoreStrings.BadValueGeneratorType(nameof(Random), nameof(ValueGenerator)), + Assert.Throws(() => b.Property(e => e.Down).HasValueGenerator(typeof(Random))).Message); + }); + } + + [ConditionalFact] + public virtual void Can_set_unicode_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.CollectionQuarks, + b => + { + b.PrimitiveCollection(e => e.Up).IsUnicode(); + b.PrimitiveCollection(e => e.Down).IsUnicode(false); + b.PrimitiveCollection("Charm").IsUnicode(); + b.PrimitiveCollection>("Strange").IsUnicode(false); + b.PrimitiveCollection("Top").IsUnicode(); + b.PrimitiveCollection>("Bottom").IsUnicode(false); + }); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + + Assert.True(complexType.FindProperty("Up")!.IsUnicode()); + Assert.False(complexType.FindProperty("Down")!.IsUnicode()); + Assert.True(complexType.FindProperty("Charm")!.IsUnicode()); + Assert.False(complexType.FindProperty("Strange")!.IsUnicode()); + Assert.True(complexType.FindProperty("Top")!.IsUnicode()); + Assert.False(complexType.FindProperty("Bottom")!.IsUnicode()); + } + + [ConditionalFact] + public virtual void PrimitiveCollectionBuilder_methods_can_be_chained() + => CreateModelBuilder() + .Entity() + .ComplexProperty(e => e.CollectionQuarks) + .PrimitiveCollection(e => e.Up) + .IsRequired() + .HasAnnotation("A", "V") + .IsConcurrencyToken() + .ValueGeneratedNever() + .ValueGeneratedOnAdd() + .ValueGeneratedOnAddOrUpdate() + .ValueGeneratedOnUpdate() + .IsUnicode() + .HasMaxLength(100) + .HasSentinel(null) + .HasValueGenerator() + .HasValueGenerator(typeof(CustomValueGenerator)) + .HasValueGeneratorFactory() + .HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)) + .IsRequired(); + + [ConditionalFact] + public virtual void Can_call_PrimitiveCollection_on_a_field() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity() + .ComplexProperty(e => e.EntityWithFields).PrimitiveCollection(e => e.CollectionId); + + var model = modelBuilder.FinalizeModel(); + var complexType = model.FindEntityType(typeof(ComplexProperties))!.GetComplexProperties().Single().ComplexType; + Assert.Equal(6, complexType.GetProperties().Count()); + var property = complexType.FindProperty(nameof(EntityWithFields.CollectionId))!; + Assert.Null(property.PropertyInfo); + Assert.NotNull(property.FieldInfo); + } } } diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 4d12d811509..e8c8cc46475 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -197,6 +197,9 @@ protected virtual TestEntityTypeBuilder Wrap(EntityTypeBuilder protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) => new GenericTestPropertyBuilder(propertyBuilder); + protected virtual TestPrimitiveCollectionBuilder Wrap(PrimitiveCollectionBuilder propertyBuilder) + => new GenericTestPrimitiveCollectionBuilder(propertyBuilder); + public override TestEntityTypeBuilder HasAnnotation(string annotation, object? value) => Wrap(EntityTypeBuilder.HasAnnotation(annotation, value)); @@ -228,6 +231,13 @@ public override TestPropertyBuilder Property(Expression Property(string propertyName) => Wrap(EntityTypeBuilder.Property(propertyName)); + public override TestPrimitiveCollectionBuilder PrimitiveCollection(Expression> propertyExpression) + where TProperty : default + => Wrap(EntityTypeBuilder.PrimitiveCollection(propertyExpression)); + + public override TestPrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => Wrap(EntityTypeBuilder.PrimitiveCollection(propertyName)); + public override TestPropertyBuilder IndexerProperty(string propertyName) => Wrap(EntityTypeBuilder.IndexerProperty(propertyName)); @@ -487,6 +497,9 @@ protected virtual TestComplexPropertyBuilder Wrap(ComplexPropertyBuilder Wrap(ComplexTypePropertyBuilder propertyBuilder) => new GenericTestComplexTypePropertyBuilder(propertyBuilder); + protected virtual TestComplexTypePrimitiveCollectionBuilder Wrap(ComplexTypePrimitiveCollectionBuilder propertyBuilder) + => new GenericTestComplexTypePrimitiveCollectionBuilder(propertyBuilder); + public override TestComplexPropertyBuilder HasPropertyAnnotation(string annotation, object? value) => Wrap(PropertyBuilder.HasPropertyAnnotation(annotation, value)); @@ -500,6 +513,13 @@ public override TestComplexTypePropertyBuilder Property(Ex public override TestComplexTypePropertyBuilder Property(string propertyName) => Wrap(PropertyBuilder.Property(propertyName)); + public override TestComplexTypePrimitiveCollectionBuilder PrimitiveCollection(Expression> propertyExpression) + where TProperty : default + => Wrap(PropertyBuilder.PrimitiveCollection(propertyExpression)); + + public override TestComplexTypePrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => Wrap(PropertyBuilder.PrimitiveCollection(propertyName)); + public override TestComplexTypePropertyBuilder IndexerProperty(string propertyName) => Wrap(PropertyBuilder.IndexerProperty(propertyName)); @@ -759,6 +779,80 @@ PropertyBuilder IInfrastructure>.Instance => PropertyBuilder; } + protected class GenericTestPrimitiveCollectionBuilder + : TestPrimitiveCollectionBuilder, IInfrastructure> + { + public GenericTestPrimitiveCollectionBuilder(PrimitiveCollectionBuilder primitiveCollectionBuilder) + { + PrimitiveCollectionBuilder = primitiveCollectionBuilder; + } + + protected PrimitiveCollectionBuilder PrimitiveCollectionBuilder { get; } + + public override IMutableProperty Metadata + => PrimitiveCollectionBuilder.Metadata; + + public override TestElementTypeBuilder ElementType() + => new(PrimitiveCollectionBuilder.ElementType()); + + public override TestPrimitiveCollectionBuilder ElementType(Action builderAction) + => Wrap(PrimitiveCollectionBuilder.ElementType(b => builderAction(new TestElementTypeBuilder(b)))); + + protected virtual TestPrimitiveCollectionBuilder Wrap(PrimitiveCollectionBuilder primitiveCollectionBuilder) + => new GenericTestPrimitiveCollectionBuilder(primitiveCollectionBuilder); + + public override TestPrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + => Wrap(PrimitiveCollectionBuilder.HasAnnotation(annotation, value)); + + public override TestPrimitiveCollectionBuilder IsRequired(bool isRequired = true) + => Wrap(PrimitiveCollectionBuilder.IsRequired(isRequired)); + + public override TestPrimitiveCollectionBuilder HasMaxLength(int maxLength) + => Wrap(PrimitiveCollectionBuilder.HasMaxLength(maxLength)); + + public override TestPrimitiveCollectionBuilder HasSentinel(object? sentinel) + => Wrap(PrimitiveCollectionBuilder.HasSentinel(sentinel)); + + public override TestPrimitiveCollectionBuilder IsUnicode(bool unicode = true) + => Wrap(PrimitiveCollectionBuilder.IsUnicode(unicode)); + + public override TestPrimitiveCollectionBuilder IsConcurrencyToken(bool isConcurrencyToken = true) + => Wrap(PrimitiveCollectionBuilder.IsConcurrencyToken(isConcurrencyToken)); + + public override TestPrimitiveCollectionBuilder ValueGeneratedNever() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedNever()); + + public override TestPrimitiveCollectionBuilder ValueGeneratedOnAdd() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAdd()); + + public override TestPrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAddOrUpdate()); + + public override TestPrimitiveCollectionBuilder ValueGeneratedOnUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnUpdate()); + + public override TestPrimitiveCollectionBuilder HasValueGenerator() + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator()); + + public override TestPrimitiveCollectionBuilder HasValueGenerator(Type valueGeneratorType) + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator(valueGeneratorType)); + + public override TestPrimitiveCollectionBuilder HasValueGeneratorFactory() + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory()); + + public override TestPrimitiveCollectionBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType) + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); + + public override TestPrimitiveCollectionBuilder HasField(string fieldName) + => Wrap(PrimitiveCollectionBuilder.HasField(fieldName)); + + public override TestPrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + => Wrap(PrimitiveCollectionBuilder.UsePropertyAccessMode(propertyAccessMode)); + + PrimitiveCollectionBuilder IInfrastructure>.Instance + => PrimitiveCollectionBuilder; + } + protected class GenericTestComplexTypePropertyBuilder : TestComplexTypePropertyBuilder, IInfrastructure> { @@ -820,10 +914,6 @@ public override TestComplexTypePropertyBuilder HasValueGenerator HasValueGenerator(Type valueGeneratorType) => Wrap(PropertyBuilder.HasValueGenerator(valueGeneratorType)); - public override TestComplexTypePropertyBuilder HasValueGenerator( - Func factory) - => Wrap(PropertyBuilder.HasValueGenerator(factory)); - public override TestComplexTypePropertyBuilder HasValueGeneratorFactory() => Wrap(PropertyBuilder.HasValueGeneratorFactory()); @@ -913,6 +1003,74 @@ ComplexTypePropertyBuilder IInfrastructure PropertyBuilder; } + protected class GenericTestComplexTypePrimitiveCollectionBuilder : + TestComplexTypePrimitiveCollectionBuilder, IInfrastructure> + { + public GenericTestComplexTypePrimitiveCollectionBuilder(ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + { + PrimitiveCollectionBuilder = primitiveCollectionBuilder; + } + + protected ComplexTypePrimitiveCollectionBuilder PrimitiveCollectionBuilder { get; } + + public override IMutableProperty Metadata + => PrimitiveCollectionBuilder.Metadata; + + protected virtual TestComplexTypePrimitiveCollectionBuilder Wrap(ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + => new GenericTestComplexTypePrimitiveCollectionBuilder(primitiveCollectionBuilder); + + public override TestComplexTypePrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + => Wrap(PrimitiveCollectionBuilder.HasAnnotation(annotation, value)); + + public override TestComplexTypePrimitiveCollectionBuilder IsRequired(bool isRequired = true) + => Wrap(PrimitiveCollectionBuilder.IsRequired(isRequired)); + + public override TestComplexTypePrimitiveCollectionBuilder HasMaxLength(int maxLength) + => Wrap(PrimitiveCollectionBuilder.HasMaxLength(maxLength)); + + public override TestComplexTypePrimitiveCollectionBuilder HasSentinel(object? sentinel) + => Wrap(PrimitiveCollectionBuilder.HasSentinel(sentinel)); + + public override TestComplexTypePrimitiveCollectionBuilder IsUnicode(bool unicode = true) + => Wrap(PrimitiveCollectionBuilder.IsUnicode(unicode)); + + public override TestComplexTypePrimitiveCollectionBuilder IsConcurrencyToken(bool isConcurrencyToken = true) + => Wrap(PrimitiveCollectionBuilder.IsConcurrencyToken(isConcurrencyToken)); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedNever() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedNever()); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAdd() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAdd()); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAddOrUpdate()); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnUpdate()); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGenerator() + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator()); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGenerator(Type valueGeneratorType) + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator(valueGeneratorType)); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory() + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory()); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType) + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); + + public override TestComplexTypePrimitiveCollectionBuilder HasField(string fieldName) + => Wrap(PrimitiveCollectionBuilder.HasField(fieldName)); + + public override TestComplexTypePrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + => Wrap(PrimitiveCollectionBuilder.UsePropertyAccessMode(propertyAccessMode)); + + ComplexTypePrimitiveCollectionBuilder IInfrastructure>.Instance + => PrimitiveCollectionBuilder; + } + protected class GenericTestKeyBuilder : TestKeyBuilder, IInfrastructure> { public GenericTestKeyBuilder(KeyBuilder keyBuilder) diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index d9a779b3924..c0078917fca 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -221,6 +221,9 @@ protected virtual NonGenericTestEntityTypeBuilder Wrap(EntityTypeBuilde protected virtual TestPropertyBuilder Wrap(PropertyBuilder propertyBuilder) => new NonGenericTestPropertyBuilder(propertyBuilder); + protected virtual TestPrimitiveCollectionBuilder Wrap(PrimitiveCollectionBuilder propertyBuilder) + => new NonGenericTestPrimitiveCollectionBuilder(propertyBuilder); + public override TestEntityTypeBuilder HasAnnotation(string annotation, object? value) => Wrap(EntityTypeBuilder.HasAnnotation(annotation, value)); @@ -257,6 +260,16 @@ public override TestPropertyBuilder Property(Expression Property(string propertyName) => Wrap(EntityTypeBuilder.Property(propertyName)); + public override TestPrimitiveCollectionBuilder PrimitiveCollection( + Expression> propertyExpression) + { + var memberInfo = propertyExpression.GetMemberAccess(); + return Wrap(EntityTypeBuilder.PrimitiveCollection(memberInfo.GetMemberType(), memberInfo.GetSimpleMemberName())); + } + + public override TestPrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => Wrap(EntityTypeBuilder.PrimitiveCollection(propertyName)); + public override TestPropertyBuilder IndexerProperty(string propertyName) => Wrap(EntityTypeBuilder.IndexerProperty(propertyName)); @@ -561,6 +574,9 @@ protected virtual NonGenericTestComplexPropertyBuilder Wrap(ComplexPropert protected virtual TestComplexTypePropertyBuilder Wrap(ComplexTypePropertyBuilder propertyBuilder) => new NonGenericTestComplexTypePropertyBuilder(propertyBuilder); + protected virtual TestComplexTypePrimitiveCollectionBuilder Wrap(ComplexTypePrimitiveCollectionBuilder propertyBuilder) + => new NonGenericTestComplexTypePrimitiveCollectionBuilder(propertyBuilder); + public override TestComplexPropertyBuilder HasPropertyAnnotation(string annotation, object? value) => Wrap(PropertyBuilder.HasPropertyAnnotation(annotation, value)); @@ -576,6 +592,15 @@ public override TestComplexTypePropertyBuilder Property(Ex public override TestComplexTypePropertyBuilder Property(string propertyName) => Wrap(PropertyBuilder.Property(propertyName)); + public override TestComplexTypePrimitiveCollectionBuilder PrimitiveCollection(Expression> propertyExpression) + { + var memberInfo = propertyExpression.GetMemberAccess(); + return Wrap(PropertyBuilder.PrimitiveCollection(memberInfo.GetMemberType(), memberInfo.GetSimpleMemberName())); + } + + public override TestComplexTypePrimitiveCollectionBuilder PrimitiveCollection(string propertyName) + => Wrap(PropertyBuilder.PrimitiveCollection(propertyName)); + public override TestComplexTypePropertyBuilder IndexerProperty(string propertyName) => Wrap(PropertyBuilder.IndexerProperty(propertyName)); @@ -843,6 +868,79 @@ PropertyBuilder IInfrastructure.Instance => PropertyBuilder; } + protected class NonGenericTestPrimitiveCollectionBuilder : TestPrimitiveCollectionBuilder, IInfrastructure + { + public NonGenericTestPrimitiveCollectionBuilder(PrimitiveCollectionBuilder primitiveCollectionBuilder) + { + PrimitiveCollectionBuilder = primitiveCollectionBuilder; + } + + private PrimitiveCollectionBuilder PrimitiveCollectionBuilder { get; } + + public override IMutableProperty Metadata + => PrimitiveCollectionBuilder.Metadata; + + public override TestElementTypeBuilder ElementType() + => new(PrimitiveCollectionBuilder.ElementType()); + + public override TestPrimitiveCollectionBuilder ElementType(Action builderAction) + => Wrap(PrimitiveCollectionBuilder.ElementType(b => builderAction(new TestElementTypeBuilder(b)))); + + protected virtual TestPrimitiveCollectionBuilder Wrap(PrimitiveCollectionBuilder primitiveCollectionBuilder) + => new NonGenericTestPrimitiveCollectionBuilder(primitiveCollectionBuilder); + + public override TestPrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + => Wrap(PrimitiveCollectionBuilder.HasAnnotation(annotation, value)); + + public override TestPrimitiveCollectionBuilder IsRequired(bool isRequired = true) + => Wrap(PrimitiveCollectionBuilder.IsRequired(isRequired)); + + public override TestPrimitiveCollectionBuilder HasMaxLength(int maxLength) + => Wrap(PrimitiveCollectionBuilder.HasMaxLength(maxLength)); + + public override TestPrimitiveCollectionBuilder HasSentinel(object? sentinel) + => Wrap(PrimitiveCollectionBuilder.HasSentinel(sentinel)); + + public override TestPrimitiveCollectionBuilder IsUnicode(bool unicode = true) + => Wrap(PrimitiveCollectionBuilder.IsUnicode(unicode)); + + public override TestPrimitiveCollectionBuilder IsConcurrencyToken(bool isConcurrencyToken = true) + => Wrap(PrimitiveCollectionBuilder.IsConcurrencyToken(isConcurrencyToken)); + + public override TestPrimitiveCollectionBuilder ValueGeneratedNever() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedNever()); + + public override TestPrimitiveCollectionBuilder ValueGeneratedOnAdd() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAdd()); + + public override TestPrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAddOrUpdate()); + + public override TestPrimitiveCollectionBuilder ValueGeneratedOnUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnUpdate()); + + public override TestPrimitiveCollectionBuilder HasValueGenerator() + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator()); + + public override TestPrimitiveCollectionBuilder HasValueGenerator(Type valueGeneratorType) + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator(valueGeneratorType)); + + public override TestPrimitiveCollectionBuilder HasValueGeneratorFactory() + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory()); + + public override TestPrimitiveCollectionBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType) + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); + + public override TestPrimitiveCollectionBuilder HasField(string fieldName) + => Wrap(PrimitiveCollectionBuilder.HasField(fieldName)); + + public override TestPrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + => Wrap(PrimitiveCollectionBuilder.UsePropertyAccessMode(propertyAccessMode)); + + PrimitiveCollectionBuilder IInfrastructure.Instance + => PrimitiveCollectionBuilder; + } + protected class NonGenericTestComplexTypePropertyBuilder : TestComplexTypePropertyBuilder, IInfrastructure { @@ -904,10 +1002,6 @@ public override TestComplexTypePropertyBuilder HasValueGenerator HasValueGenerator(Type valueGeneratorType) => Wrap(PropertyBuilder.HasValueGenerator(valueGeneratorType)); - public override TestComplexTypePropertyBuilder HasValueGenerator( - Func factory) - => Wrap(PropertyBuilder.HasValueGenerator(factory)); - public override TestComplexTypePropertyBuilder HasValueGeneratorFactory() => Wrap(PropertyBuilder.HasValueGeneratorFactory()); @@ -994,6 +1088,74 @@ ComplexTypePropertyBuilder IInfrastructure.Instance => PropertyBuilder; } + protected class NonGenericTestComplexTypePrimitiveCollectionBuilder : + TestComplexTypePrimitiveCollectionBuilder, IInfrastructure + { + public NonGenericTestComplexTypePrimitiveCollectionBuilder(ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + { + PrimitiveCollectionBuilder = primitiveCollectionBuilder; + } + + private ComplexTypePrimitiveCollectionBuilder PrimitiveCollectionBuilder { get; } + + public override IMutableProperty Metadata + => PrimitiveCollectionBuilder.Metadata; + + protected virtual TestComplexTypePrimitiveCollectionBuilder Wrap(ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder) + => new NonGenericTestComplexTypePrimitiveCollectionBuilder(primitiveCollectionBuilder); + + public override TestComplexTypePrimitiveCollectionBuilder HasAnnotation(string annotation, object? value) + => Wrap(PrimitiveCollectionBuilder.HasAnnotation(annotation, value)); + + public override TestComplexTypePrimitiveCollectionBuilder IsRequired(bool isRequired = true) + => Wrap(PrimitiveCollectionBuilder.IsRequired(isRequired)); + + public override TestComplexTypePrimitiveCollectionBuilder HasMaxLength(int maxLength) + => Wrap(PrimitiveCollectionBuilder.HasMaxLength(maxLength)); + + public override TestComplexTypePrimitiveCollectionBuilder HasSentinel(object? sentinel) + => Wrap(PrimitiveCollectionBuilder.HasSentinel(sentinel)); + + public override TestComplexTypePrimitiveCollectionBuilder IsUnicode(bool unicode = true) + => Wrap(PrimitiveCollectionBuilder.IsUnicode(unicode)); + + public override TestComplexTypePrimitiveCollectionBuilder IsConcurrencyToken(bool isConcurrencyToken = true) + => Wrap(PrimitiveCollectionBuilder.IsConcurrencyToken(isConcurrencyToken)); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedNever() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedNever()); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAdd() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAdd()); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnAddOrUpdate()); + + public override TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdate() + => Wrap(PrimitiveCollectionBuilder.ValueGeneratedOnUpdate()); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGenerator() + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator()); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGenerator(Type valueGeneratorType) + => Wrap(PrimitiveCollectionBuilder.HasValueGenerator(valueGeneratorType)); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory() + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory()); + + public override TestComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType) + => Wrap(PrimitiveCollectionBuilder.HasValueGeneratorFactory(valueGeneratorFactoryType)); + + public override TestComplexTypePrimitiveCollectionBuilder HasField(string fieldName) + => Wrap(PrimitiveCollectionBuilder.HasField(fieldName)); + + public override TestComplexTypePrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode) + => Wrap(PrimitiveCollectionBuilder.UsePropertyAccessMode(propertyAccessMode)); + + ComplexTypePrimitiveCollectionBuilder IInfrastructure.Instance + => PrimitiveCollectionBuilder; + } + protected class NonGenericTestNavigationBuilder : TestNavigationBuilder { public NonGenericTestNavigationBuilder(NavigationBuilder navigationBuilder) diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 900429fc617..00196331793 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -199,6 +199,12 @@ public abstract TestPropertyBuilder Property( Expression> propertyExpression); public abstract TestPropertyBuilder Property(string propertyName); + + public abstract TestPrimitiveCollectionBuilder PrimitiveCollection( + Expression> propertyExpression); + + public abstract TestPrimitiveCollectionBuilder PrimitiveCollection(string propertyName); + public abstract TestPropertyBuilder IndexerProperty(string propertyName); public abstract TestComplexPropertyBuilder ComplexProperty(string propertyName); @@ -364,6 +370,12 @@ public abstract TestComplexTypePropertyBuilder Property( Expression> propertyExpression); public abstract TestComplexTypePropertyBuilder Property(string propertyName); + + public abstract TestComplexTypePrimitiveCollectionBuilder PrimitiveCollection( + Expression> propertyExpression); + + public abstract TestComplexTypePrimitiveCollectionBuilder PrimitiveCollection(string propertyName); + public abstract TestComplexTypePropertyBuilder IndexerProperty(string propertyName); public abstract TestComplexPropertyBuilder ComplexProperty(string propertyName); @@ -513,6 +525,92 @@ public abstract TestPropertyBuilder HasConversion + { + public abstract TestElementTypeBuilder ElementType(); + public abstract TestPrimitiveCollectionBuilder ElementType(Action builderAction); + + public abstract IMutableProperty Metadata { get; } + public abstract TestPrimitiveCollectionBuilder HasAnnotation(string annotation, object? value); + public abstract TestPrimitiveCollectionBuilder IsRequired(bool isRequired = true); + public abstract TestPrimitiveCollectionBuilder HasMaxLength(int maxLength); + public abstract TestPrimitiveCollectionBuilder HasSentinel(object? sentinel); + public abstract TestPrimitiveCollectionBuilder IsUnicode(bool unicode = true); + public abstract TestPrimitiveCollectionBuilder IsConcurrencyToken(bool isConcurrencyToken = true); + + public abstract TestPrimitiveCollectionBuilder ValueGeneratedNever(); + public abstract TestPrimitiveCollectionBuilder ValueGeneratedOnAdd(); + public abstract TestPrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate(); + public abstract TestPrimitiveCollectionBuilder ValueGeneratedOnUpdate(); + + public abstract TestPrimitiveCollectionBuilder HasValueGenerator() + where TGenerator : ValueGenerator; + + public abstract TestPrimitiveCollectionBuilder HasValueGenerator(Type valueGeneratorType); + + public abstract TestPrimitiveCollectionBuilder HasValueGeneratorFactory() + where TFactory : ValueGeneratorFactory; + + public abstract TestPrimitiveCollectionBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType); + + public abstract TestPrimitiveCollectionBuilder HasField(string fieldName); + public abstract TestPrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode); + } + + public class TestElementTypeBuilder(ElementTypeBuilder elementTypeBuilder) + { + public virtual ElementTypeBuilder ElementTypeBuilder { get; } = elementTypeBuilder; + + public virtual IMutableElementType Metadata + => ElementTypeBuilder.Metadata; + + protected virtual TestElementTypeBuilder Wrap(ElementTypeBuilder elementTypeBuilder) + => new(elementTypeBuilder); + + public virtual TestElementTypeBuilder HasAnnotation(string annotation, object? value) + => Wrap(ElementTypeBuilder.HasAnnotation(annotation, value)); + + public virtual TestElementTypeBuilder IsRequired(bool required = true) + => Wrap(ElementTypeBuilder.IsRequired(required)); + + public virtual TestElementTypeBuilder HasMaxLength(int maxLength) + => Wrap(ElementTypeBuilder.HasMaxLength(maxLength)); + + public virtual TestElementTypeBuilder HasPrecision(int precision, int scale) + => Wrap(ElementTypeBuilder.HasPrecision(precision, scale)); + + public virtual TestElementTypeBuilder HasPrecision(int precision) + => Wrap(ElementTypeBuilder.HasPrecision(precision)); + + public virtual TestElementTypeBuilder IsUnicode(bool unicode = true) + => Wrap(ElementTypeBuilder.IsUnicode(unicode)); + + public virtual TestElementTypeBuilder HasConversion() + => Wrap(ElementTypeBuilder.HasConversion()); + + public virtual TestElementTypeBuilder HasConversion(Type? conversionType) + => Wrap(ElementTypeBuilder.HasConversion(conversionType)); + + public virtual TestElementTypeBuilder HasConversion(ValueConverter? converter) + => Wrap(ElementTypeBuilder.HasConversion(converter)); + + public virtual TestElementTypeBuilder HasConversion(ValueComparer? valueComparer) + => Wrap(ElementTypeBuilder.HasConversion(valueComparer)); + + public virtual TestElementTypeBuilder HasConversion(Type conversionType, ValueComparer? valueComparer) + => Wrap(ElementTypeBuilder.HasConversion(conversionType, valueComparer)); + + public virtual TestElementTypeBuilder HasConversion(ValueConverter? converter, ValueComparer? valueComparer) + => Wrap(ElementTypeBuilder.HasConversion(converter, valueComparer)); + + public virtual TestElementTypeBuilder HasConversion() + where TComparer : ValueComparer + => Wrap(ElementTypeBuilder.HasConversion()); + + public virtual TestElementTypeBuilder HasConversion(Type conversionType, Type? comparerType) + => Wrap(ElementTypeBuilder.HasConversion(conversionType, comparerType)); + } + public abstract class TestComplexTypePropertyBuilder { public abstract IMutableProperty Metadata { get; } @@ -536,9 +634,6 @@ public abstract TestComplexTypePropertyBuilder HasValueGenerator HasValueGenerator(Type valueGeneratorType); - public abstract TestComplexTypePropertyBuilder HasValueGenerator( - Func factory); - public abstract TestComplexTypePropertyBuilder HasValueGeneratorFactory() where TFactory : ValueGeneratorFactory; @@ -596,6 +691,29 @@ public abstract TestComplexTypePropertyBuilder HasConversion + { + public abstract IMutableProperty Metadata { get; } + public abstract TestComplexTypePrimitiveCollectionBuilder HasAnnotation(string annotation, object? value); + public abstract TestComplexTypePrimitiveCollectionBuilder IsRequired(bool isRequired = true); + public abstract TestComplexTypePrimitiveCollectionBuilder HasMaxLength(int maxLength); + public abstract TestComplexTypePrimitiveCollectionBuilder HasSentinel(object? sentinel); + public abstract TestComplexTypePrimitiveCollectionBuilder IsUnicode(bool unicode = true); + public abstract TestComplexTypePrimitiveCollectionBuilder IsConcurrencyToken(bool isConcurrencyToken = true); + public abstract TestComplexTypePrimitiveCollectionBuilder ValueGeneratedNever(); + public abstract TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAdd(); + public abstract TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnAddOrUpdate(); + public abstract TestComplexTypePrimitiveCollectionBuilder ValueGeneratedOnUpdate(); + public abstract TestComplexTypePrimitiveCollectionBuilder HasValueGenerator() + where TGenerator : ValueGenerator; + public abstract TestComplexTypePrimitiveCollectionBuilder HasValueGenerator(Type valueGeneratorType); + public abstract TestComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory() + where TFactory : ValueGeneratorFactory; + public abstract TestComplexTypePrimitiveCollectionBuilder HasValueGeneratorFactory(Type valueGeneratorFactoryType); + public abstract TestComplexTypePrimitiveCollectionBuilder HasField(string fieldName); + public abstract TestComplexTypePrimitiveCollectionBuilder UsePropertyAccessMode(PropertyAccessMode propertyAccessMode); + } + public abstract class TestNavigationBuilder { public abstract TestNavigationBuilder HasAnnotation(string annotation, object? value); diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index c0751dab34e..9413ae7ec64 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + +using System.Collections.ObjectModel; using System.ComponentModel; using System.Dynamic; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -22,7 +25,7 @@ public void Can_set_model_annotation() Assert.NotNull(modelBuilder); var model = modelBuilder.FinalizeModel(); - Assert.Equal("Ro", model.FindAnnotation("Fus").Value); + Assert.Equal("Ro", model.FindAnnotation("Fus")!.Value); } [ConditionalFact] @@ -44,7 +47,7 @@ public virtual void Can_get_entity_builder_for_clr_type() var entityBuilder = modelBuilder.Entity(); Assert.NotNull(entityBuilder); - Assert.Equal(typeof(Customer).FullName, model.FindEntityType(typeof(Customer)).Name); + Assert.Equal(typeof(Customer).FullName, model.FindEntityType(typeof(Customer))!.Name); } [ConditionalFact] @@ -55,10 +58,10 @@ public virtual void Can_set_entity_key_from_clr_property() modelBuilder.Entity().HasKey(b => b.Id); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; - Assert.Equal(1, entity.FindPrimaryKey().Properties.Count); - Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey().Properties.First().Name); + Assert.Equal(1, entity.FindPrimaryKey()!.Properties.Count); + Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey()!.Properties.First().Name); } [ConditionalFact] @@ -71,8 +74,8 @@ public virtual void Entity_key_on_shadow_property_is_discovered_by_convention() var model = modelBuilder.FinalizeModel(); - var entity = model.FindEntityType(typeof(Order)); - Assert.Equal("Id", entity.FindPrimaryKey().Properties.Single().Name); + var entity = model.FindEntityType(typeof(Order))!; + Assert.Equal("Id", entity.FindPrimaryKey()!.Properties.Single().Name); } [ConditionalFact] @@ -86,8 +89,8 @@ public virtual void Entity_key_on_secondary_property_is_discovered_by_convention .Ignore(s => s.Id); modelBuilder.FinalizeModel(); - var entity = modelBuilder.Model.FindEntityType(typeof(SelfRef)); - Assert.Equal(nameof(SelfRef.SelfRefId), entity.FindPrimaryKey().Properties.Single().Name); + var entity = modelBuilder.Model.FindEntityType(typeof(SelfRef))!; + Assert.Equal(nameof(SelfRef.SelfRefId), entity.FindPrimaryKey()!.Properties.Single().Name); } [ConditionalFact] @@ -105,10 +108,10 @@ public virtual void Can_set_entity_key_from_property_name_when_no_clr_property() b.HasKey(Customer.IdProperty.Name + 1); }); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; - Assert.Equal(1, entity.FindPrimaryKey().Properties.Count); - Assert.Equal(Customer.IdProperty.Name + 1, entity.FindPrimaryKey().Properties.First().Name); + Assert.Equal(1, entity.FindPrimaryKey()!.Properties.Count); + Assert.Equal(Customer.IdProperty.Name + 1, entity.FindPrimaryKey()!.Properties.First().Name); } [ConditionalFact] @@ -124,10 +127,10 @@ public virtual void Can_set_entity_key_from_clr_property_when_property_ignored_o b.HasKey(e => e.Id); }); - var entity = modelBuilder.Model.FindEntityType(typeof(Customer)); + var entity = modelBuilder.Model.FindEntityType(typeof(Customer))!; - Assert.Equal(1, entity.FindPrimaryKey().Properties.Count); - Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey().Properties.First().Name); + Assert.Equal(1, entity.FindPrimaryKey()!.Properties.Count); + Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey()!.Properties.First().Name); } [ConditionalFact] @@ -141,11 +144,11 @@ public virtual void Can_set_composite_entity_key_from_clr_properties() .HasKey( e => new { e.Id, e.Name }); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; - Assert.Equal(2, entity.FindPrimaryKey().Properties.Count); - Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey().Properties.First().Name); - Assert.Equal(Customer.NameProperty.Name, entity.FindPrimaryKey().Properties.Last().Name); + Assert.Equal(2, entity.FindPrimaryKey()!.Properties.Count); + Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey()!.Properties.First().Name); + Assert.Equal(Customer.NameProperty.Name, entity.FindPrimaryKey()!.Properties.Last().Name); } [ConditionalFact] @@ -163,11 +166,11 @@ public virtual void Can_set_composite_entity_key_from_property_names_when_mixed_ b.HasKey(Customer.IdProperty.Name, Customer.NameProperty.Name + "Shadow"); }); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; - Assert.Equal(2, entity.FindPrimaryKey().Properties.Count); - Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey().Properties.First().Name); - Assert.Equal(Customer.NameProperty.Name + "Shadow", entity.FindPrimaryKey().Properties.Last().Name); + Assert.Equal(2, entity.FindPrimaryKey()!.Properties.Count); + Assert.Equal(Customer.IdProperty.Name, entity.FindPrimaryKey()!.Properties.First().Name); + Assert.Equal(Customer.NameProperty.Name + "Shadow", entity.FindPrimaryKey()!.Properties.Last().Name); } [ConditionalFact] @@ -184,10 +187,10 @@ public virtual void Can_set_entity_key_with_annotations() keyBuilder.HasAnnotation("A1", "V1") .HasAnnotation("A2", "V2"); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; Assert.Equal( - new[] { Customer.IdProperty.Name, Customer.NameProperty.Name }, entity.FindPrimaryKey().Properties.Select(p => p.Name)); + new[] { Customer.IdProperty.Name, Customer.NameProperty.Name }, entity.FindPrimaryKey()!.Properties.Select(p => p.Name)); Assert.Equal("V1", keyBuilder.Metadata["A1"]); Assert.Equal("V2", keyBuilder.Metadata["A2"]); } @@ -202,19 +205,19 @@ public virtual void Can_upgrade_candidate_key_to_primary_key() modelBuilder.Ignore(); modelBuilder.Ignore(); - var entity = modelBuilder.Model.FindEntityType(typeof(Customer)); - var key = entity.FindKey(entity.FindProperty(Customer.NameProperty)); + var entity = modelBuilder.Model.FindEntityType(typeof(Customer))!; + var key = entity.FindKey(entity.FindProperty(Customer.NameProperty)!); modelBuilder.Entity().HasKey(b => b.Name); modelBuilder.FinalizeModel(); - var nameProperty = entity.FindPrimaryKey().Properties.Single(); + var nameProperty = entity.FindPrimaryKey()!.Properties.Single(); Assert.Equal(Customer.NameProperty.Name, nameProperty.Name); Assert.False(nameProperty.RequiresValueGenerator()); Assert.Equal(ValueGenerated.Never, nameProperty.ValueGenerated); - var idProperty = (IReadOnlyProperty)entity.FindProperty(Customer.IdProperty); + var idProperty = (IReadOnlyProperty)entity.FindProperty(Customer.IdProperty)!; Assert.Equal(ValueGenerated.Never, idProperty.ValueGenerated); } @@ -226,7 +229,7 @@ public virtual void Can_set_alternate_key_from_clr_property() modelBuilder.Entity().HasAlternateKey(b => b.AlternateKey); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; Assert.Equal( Customer.AlternateKeyProperty.Name, @@ -246,7 +249,7 @@ public virtual void Can_set_alternate_key_from_property_name_when_no_clr_propert b.HasAlternateKey(Customer.AlternateKeyProperty.Name + 1); }); - var entity = model.FindEntityType(typeof(Customer)); + var entity = model.FindEntityType(typeof(Customer))!; Assert.Equal( Customer.AlternateKeyProperty.Name + 1, @@ -264,7 +267,7 @@ public virtual void Can_set_alternate_key_from_clr_property_when_property_ignore b.HasAlternateKey(e => e.AlternateKey); }); - var entity = modelBuilder.Model.FindEntityType(typeof(Customer)); + var entity = modelBuilder.Model.FindEntityType(typeof(Customer))!; Assert.Equal( Customer.AlternateKeyProperty.Name, @@ -277,8 +280,8 @@ public virtual void Setting_alternate_key_makes_properties_required() var modelBuilder = CreateModelBuilder(); var entityBuilder = modelBuilder.Entity(); - var entity = modelBuilder.Model.FindEntityType(typeof(Customer)); - var alternateKeyProperty = entity.FindProperty(nameof(Customer.Name)); + var entity = modelBuilder.Model.FindEntityType(typeof(Customer))!; + var alternateKeyProperty = entity.FindProperty(nameof(Customer.Name))!; Assert.True(alternateKeyProperty.IsNullable); entityBuilder.HasAlternateKey(e => e.Name); @@ -308,7 +311,7 @@ public virtual void Can_set_property_annotation() .Entity() .Property(c => c.Name).HasAnnotation("foo", "bar"); - var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)).FindProperty(nameof(Customer.Name)); + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!.FindProperty(nameof(Customer.Name))!; Assert.Equal("bar", property["foo"]); } @@ -323,7 +326,7 @@ public virtual void Can_set_property_annotation_when_no_clr_property() .Entity() .Property(Customer.NameProperty.Name).HasAnnotation("foo", "bar"); - var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)).FindProperty(nameof(Customer.Name)); + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!.FindProperty(nameof(Customer.Name))!; Assert.Equal("bar", property["foo"]); } @@ -338,7 +341,7 @@ public virtual void Can_set_property_annotation_by_type() .Entity() .Property(c => c.Name); - var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)).FindProperty(nameof(Customer.Name)); + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!.FindProperty(nameof(Customer.Name))!; Assert.Equal("bar", property["foo"]); } @@ -359,14 +362,14 @@ public virtual void Properties_are_required_by_default_only_if_CLR_type_is_nulla b.Property("Bottom"); }); - var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks))!; - Assert.False(entityType.FindProperty("Up").IsNullable); - Assert.True(entityType.FindProperty("Down").IsNullable); - Assert.False(entityType.FindProperty("Charm").IsNullable); - Assert.True(entityType.FindProperty("Strange").IsNullable); - Assert.False(entityType.FindProperty("Top").IsNullable); - Assert.True(entityType.FindProperty("Bottom").IsNullable); + Assert.False(entityType.FindProperty("Up")!.IsNullable); + Assert.True(entityType.FindProperty("Down")!.IsNullable); + Assert.False(entityType.FindProperty("Charm")!.IsNullable); + Assert.True(entityType.FindProperty("Strange")!.IsNullable); + Assert.False(entityType.FindProperty("Top")!.IsNullable); + Assert.True(entityType.FindProperty("Bottom")!.IsNullable); } [ConditionalFact] @@ -386,7 +389,7 @@ public virtual void Properties_can_be_ignored() b.Ignore("Shadow"); }); - var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks))!; Assert.Contains(nameof(Quarks.Id), entityType.GetProperties().Select(p => p.Name)); Assert.DoesNotContain(nameof(Quarks.Up), entityType.GetProperties().Select(p => p.Name)); Assert.DoesNotContain(nameof(Quarks.Down), entityType.GetProperties().Select(p => p.Name)); @@ -400,7 +403,7 @@ public virtual void Properties_can_be_ignored_by_type() modelBuilder.Ignore(); modelBuilder.Entity(); - var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer)); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!; Assert.Null(entityType.FindProperty(nameof(Customer.AlternateKey))); } @@ -451,7 +454,7 @@ public virtual void Conventions_can_be_replaced() c => c.Conventions.Replace( s => - new TestDbSetFindingConvention(s.GetService()))); + new TestDbSetFindingConvention(s.GetService()!))); var model = modelBuilder.FinalizeModel(); @@ -530,7 +533,7 @@ public virtual void Can_add_shadow_properties_when_they_have_been_ignored() var model = modelBuilder.FinalizeModel(); - Assert.NotNull(model.FindEntityType(typeof(Customer)).FindProperty("Shadow")); + Assert.NotNull(model.FindEntityType(typeof(Customer))!.FindProperty("Shadow")); } [ConditionalFact] @@ -540,7 +543,7 @@ public virtual void Can_override_navigations_as_properties() var model = modelBuilder.Model; modelBuilder.Entity(); - var customer = model.FindEntityType(typeof(Customer)); + var customer = model.FindEntityType(typeof(Customer))!; Assert.NotNull(customer.FindNavigation(nameof(Customer.Orders))); modelBuilder.Entity().Property(c => c.Orders); @@ -615,14 +618,14 @@ public virtual void Properties_can_be_made_required() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - Assert.False(entityType.FindProperty("Up").IsNullable); - Assert.False(entityType.FindProperty("Down").IsNullable); - Assert.False(entityType.FindProperty("Charm").IsNullable); - Assert.False(entityType.FindProperty("Strange").IsNullable); - Assert.False(entityType.FindProperty("Top").IsNullable); - Assert.False(entityType.FindProperty("Bottom").IsNullable); + Assert.False(entityType.FindProperty("Up")!.IsNullable); + Assert.False(entityType.FindProperty("Down")!.IsNullable); + Assert.False(entityType.FindProperty("Charm")!.IsNullable); + Assert.False(entityType.FindProperty("Strange")!.IsNullable); + Assert.False(entityType.FindProperty("Top")!.IsNullable); + Assert.False(entityType.FindProperty("Bottom")!.IsNullable); } [ConditionalFact] @@ -639,11 +642,11 @@ public virtual void Properties_can_be_made_optional() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - Assert.True(entityType.FindProperty("Down").IsNullable); - Assert.True(entityType.FindProperty("Strange").IsNullable); - Assert.True(entityType.FindProperty("Bottom").IsNullable); + Assert.True(entityType.FindProperty("Down")!.IsNullable); + Assert.True(entityType.FindProperty("Strange")!.IsNullable); + Assert.True(entityType.FindProperty("Bottom")!.IsNullable); } [ConditionalFact] @@ -682,11 +685,11 @@ public virtual void Non_nullable_properties_cannot_be_made_optional() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - Assert.False(entityType.FindProperty("Up").IsNullable); - Assert.False(entityType.FindProperty("Charm").IsNullable); - Assert.False(entityType.FindProperty("Top").IsNullable); + Assert.False(entityType.FindProperty("Up")!.IsNullable); + Assert.False(entityType.FindProperty("Charm")!.IsNullable); + Assert.False(entityType.FindProperty("Top")!.IsNullable); } [ConditionalFact] @@ -704,18 +707,18 @@ public virtual void Properties_specified_by_string_are_shadow_properties_unless_ }); var model = modelBuilder.FinalizeModel(); - var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks))!; - Assert.False(entityType.FindProperty("Up").IsShadowProperty()); - Assert.False(entityType.FindProperty("Down").IsShadowProperty()); - Assert.True(entityType.FindProperty("Gluon").IsShadowProperty()); - Assert.True(entityType.FindProperty("Photon").IsShadowProperty()); + Assert.False(entityType.FindProperty("Up")!.IsShadowProperty()); + Assert.False(entityType.FindProperty("Down")!.IsShadowProperty()); + Assert.True(entityType.FindProperty("Gluon")!.IsShadowProperty()); + Assert.True(entityType.FindProperty("Photon")!.IsShadowProperty()); - Assert.Equal(-1, entityType.FindProperty("Up").GetShadowIndex()); - Assert.Equal(-1, entityType.FindProperty("Down").GetShadowIndex()); - Assert.NotEqual(-1, entityType.FindProperty("Gluon").GetShadowIndex()); - Assert.NotEqual(-1, entityType.FindProperty("Photon").GetShadowIndex()); - Assert.NotEqual(entityType.FindProperty("Gluon").GetShadowIndex(), entityType.FindProperty("Photon").GetShadowIndex()); + Assert.Equal(-1, entityType.FindProperty("Up")!.GetShadowIndex()); + Assert.Equal(-1, entityType.FindProperty("Down")!.GetShadowIndex()); + Assert.NotEqual(-1, entityType.FindProperty("Gluon")!.GetShadowIndex()); + Assert.NotEqual(-1, entityType.FindProperty("Photon")!.GetShadowIndex()); + Assert.NotEqual(entityType.FindProperty("Gluon")!.GetShadowIndex(), entityType.FindProperty("Photon")!.GetShadowIndex()); } [ConditionalFact] @@ -736,23 +739,23 @@ public virtual void Properties_can_be_made_concurrency_tokens() }); var model = modelBuilder.FinalizeModel(); - var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks)); - - Assert.False(entityType.FindProperty(Customer.IdProperty.Name).IsConcurrencyToken); - Assert.True(entityType.FindProperty("Up").IsConcurrencyToken); - Assert.False(entityType.FindProperty("Down").IsConcurrencyToken); - Assert.True(entityType.FindProperty("Charm").IsConcurrencyToken); - Assert.False(entityType.FindProperty("Strange").IsConcurrencyToken); - Assert.True(entityType.FindProperty("Top").IsConcurrencyToken); - Assert.False(entityType.FindProperty("Bottom").IsConcurrencyToken); - - Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name).GetOriginalValueIndex()); - Assert.Equal(3, entityType.FindProperty("Up").GetOriginalValueIndex()); - Assert.Equal(-1, entityType.FindProperty("Down").GetOriginalValueIndex()); - Assert.Equal(1, entityType.FindProperty("Charm").GetOriginalValueIndex()); - Assert.Equal(-1, entityType.FindProperty("Strange").GetOriginalValueIndex()); - Assert.Equal(2, entityType.FindProperty("Top").GetOriginalValueIndex()); - Assert.Equal(-1, entityType.FindProperty("Bottom").GetOriginalValueIndex()); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(Quarks))!; + + Assert.False(entityType.FindProperty(Customer.IdProperty.Name)!.IsConcurrencyToken); + Assert.True(entityType.FindProperty("Up")!.IsConcurrencyToken); + Assert.False(entityType.FindProperty("Down")!.IsConcurrencyToken); + Assert.True(entityType.FindProperty("Charm")!.IsConcurrencyToken); + Assert.False(entityType.FindProperty("Strange")!.IsConcurrencyToken); + Assert.True(entityType.FindProperty("Top")!.IsConcurrencyToken); + Assert.False(entityType.FindProperty("Bottom")!.IsConcurrencyToken); + + Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name)!.GetOriginalValueIndex()); + Assert.Equal(3, entityType.FindProperty("Up")!.GetOriginalValueIndex()); + Assert.Equal(-1, entityType.FindProperty("Down")!.GetOriginalValueIndex()); + Assert.Equal(1, entityType.FindProperty("Charm")!.GetOriginalValueIndex()); + Assert.Equal(-1, entityType.FindProperty("Strange")!.GetOriginalValueIndex()); + Assert.Equal(2, entityType.FindProperty("Top")!.GetOriginalValueIndex()); + Assert.Equal(-1, entityType.FindProperty("Bottom")!.GetOriginalValueIndex()); Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, entityType.GetChangeTrackingStrategy()); } @@ -772,12 +775,12 @@ public virtual void Properties_can_have_access_mode_set() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - Assert.Equal(PropertyAccessMode.PreferField, entityType.FindProperty("Up").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Field, entityType.FindProperty("Down").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Property, entityType.FindProperty("Charm").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.FieldDuringConstruction, entityType.FindProperty("Strange").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.PreferField, entityType.FindProperty("Up")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Field, entityType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, entityType.FindProperty("Charm")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, entityType.FindProperty("Strange")!.GetPropertyAccessMode()); } [ConditionalFact] @@ -805,14 +808,14 @@ public virtual void Access_mode_can_be_overridden_at_entity_and_property_levels( var model = modelBuilder.FinalizeModel(); Assert.Equal(PropertyAccessMode.Field, model.GetPropertyAccessMode()); - var hobsType = (IReadOnlyEntityType)model.FindEntityType(typeof(Hob)); + var hobsType = (IReadOnlyEntityType)model.FindEntityType(typeof(Hob))!; Assert.Equal(PropertyAccessMode.Field, hobsType.GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Field, hobsType.FindProperty("Id1").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Field, hobsType.FindProperty("Id1")!.GetPropertyAccessMode()); - var quarksType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var quarksType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Down").GetPropertyAccessMode()); - Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Up").GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, quarksType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, quarksType.FindProperty("Up")!.GetPropertyAccessMode()); } [ConditionalFact] @@ -833,28 +836,28 @@ public virtual void Properties_can_have_provider_type_set() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - var up = entityType.FindProperty("Up"); + var up = entityType.FindProperty("Up")!; Assert.Null(up.GetProviderClrType()); Assert.IsType>(up.GetValueComparer()); - var down = entityType.FindProperty("Down"); + var down = entityType.FindProperty("Down")!; Assert.Same(typeof(byte[]), down.GetProviderClrType()); Assert.IsType>(down.GetValueComparer()); Assert.IsType>(down.GetProviderValueComparer()); - var charm = entityType.FindProperty("Charm"); + var charm = entityType.FindProperty("Charm")!; Assert.Same(typeof(long), charm.GetProviderClrType()); Assert.IsType>(charm.GetValueComparer()); Assert.IsType>(charm.GetProviderValueComparer()); - var strange = entityType.FindProperty("Strange"); + var strange = entityType.FindProperty("Strange")!; Assert.Null(strange.GetProviderClrType()); Assert.IsType>(strange.GetValueComparer()); Assert.IsType>(strange.GetProviderValueComparer()); - var top = entityType.FindProperty("Top"); + var top = entityType.FindProperty("Top")!; Assert.Same(typeof(string), top.GetProviderClrType()); Assert.IsType>(top.GetValueComparer()); Assert.IsType>(top.GetProviderValueComparer()); @@ -875,12 +878,12 @@ public virtual void Properties_can_have_provider_type_set_for_type() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - Assert.Null(entityType.FindProperty("Up").GetProviderClrType()); - Assert.Same(typeof(byte[]), entityType.FindProperty("Down").GetProviderClrType()); - Assert.Null(entityType.FindProperty("Charm").GetProviderClrType()); - Assert.Same(typeof(byte[]), entityType.FindProperty("Strange").GetProviderClrType()); + Assert.Null(entityType.FindProperty("Up")!.GetProviderClrType()); + Assert.Same(typeof(byte[]), entityType.FindProperty("Down")!.GetProviderClrType()); + Assert.Null(entityType.FindProperty("Charm")!.GetProviderClrType()); + Assert.Same(typeof(byte[]), entityType.FindProperty("Strange")!.GetProviderClrType()); } [ConditionalFact] @@ -902,21 +905,21 @@ public virtual void Properties_can_have_non_generic_value_converter_set() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - Assert.Null(entityType.FindProperty("Up").GetValueConverter()); + Assert.Null(entityType.FindProperty("Up")!.GetValueConverter()); - var down = entityType.FindProperty("Down"); + var down = entityType.FindProperty("Down")!; Assert.Same(stringConverter, down.GetValueConverter()); Assert.IsType>(down.GetValueComparer()); Assert.IsType>(down.GetProviderValueComparer()); - var charm = entityType.FindProperty("Charm"); + var charm = entityType.FindProperty("Charm")!; Assert.Same(intConverter, charm.GetValueConverter()); Assert.IsType>(charm.GetValueComparer()); Assert.IsType>(charm.GetProviderValueComparer()); - Assert.Null(entityType.FindProperty("Strange").GetValueConverter()); + Assert.Null(entityType.FindProperty("Strange")!.GetValueConverter()); } [ConditionalFact] @@ -936,25 +939,25 @@ public virtual void Properties_can_have_custom_type_value_converter_type_set() }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Quarks))!; - var up = entityType.FindProperty("Up"); + var up = entityType.FindProperty("Up")!; Assert.Equal(typeof(int), up.GetProviderClrType()); Assert.Null(up.GetValueConverter()); Assert.IsType>(up.GetValueComparer()); Assert.IsType>(up.GetProviderValueComparer()); - var down = entityType.FindProperty("Down"); + var down = entityType.FindProperty("Down")!; Assert.IsType(down.GetValueConverter()); Assert.IsType>(down.GetValueComparer()); Assert.IsType>(down.GetProviderValueComparer()); - var charm = entityType.FindProperty("Charm"); + var charm = entityType.FindProperty("Charm")!; Assert.IsType>(charm.GetValueConverter()); Assert.IsType>(charm.GetValueComparer()); Assert.IsType>(charm.GetProviderValueComparer()); - var strange = entityType.FindProperty("Strange"); + var strange = entityType.FindProperty("Strange")!; Assert.Null(strange.GetValueConverter()); Assert.IsType>(strange.GetValueComparer()); Assert.IsType>(strange.GetProviderValueComparer()); @@ -985,32 +988,32 @@ public virtual void Properties_can_have_value_converter_set_inline() b => { b.Property(e => e.Up); - b.Property(e => e.Down).HasConversion(v => int.Parse(v), v => v.ToString()); + 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()); b.Property("Strange").HasConversion( v => (double)v, v => (float)v, new CustomValueComparer(), new CustomValueComparer()); }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - var up = entityType.FindProperty("Up"); + var up = entityType.FindProperty("Up")!; Assert.Null(up.GetProviderClrType()); Assert.Null(up.GetValueConverter()); Assert.IsType>(up.GetValueComparer()); Assert.IsType>(up.GetProviderValueComparer()); - var down = entityType.FindProperty("Down"); + var down = entityType.FindProperty("Down")!; Assert.IsType>(down.GetValueConverter()); Assert.IsType>(down.GetValueComparer()); Assert.IsType>(down.GetProviderValueComparer()); - var charm = entityType.FindProperty("Charm"); + var charm = entityType.FindProperty("Charm")!; Assert.IsType>(charm.GetValueConverter()); Assert.IsType>(charm.GetValueComparer()); Assert.IsType>(charm.GetProviderValueComparer()); - var strange = entityType.FindProperty("Strange"); + var strange = entityType.FindProperty("Strange")!; Assert.IsType>(strange.GetValueConverter()); Assert.IsType>(strange.GetValueComparer()); Assert.IsType>(strange.GetProviderValueComparer()); @@ -1026,7 +1029,7 @@ public virtual void Properties_can_have_value_converter_set() { b.Property(e => e.Up); b.Property(e => e.Down).HasConversion( - new ValueConverter(v => int.Parse(v), v => v.ToString())); + new ValueConverter(v => int.Parse(v), v => v.ToString())!); b.Property("Charm").HasConversion( new ValueConverter(v => v, v => (int)v), new CustomValueComparer()); b.Property("Strange").HasConversion( @@ -1035,25 +1038,25 @@ public virtual void Properties_can_have_value_converter_set() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - var up = entityType.FindProperty("Up"); + var up = entityType.FindProperty("Up")!; Assert.Null(up.GetProviderClrType()); Assert.Null(up.GetValueConverter()); Assert.IsType>(up.GetValueComparer()); Assert.IsType>(up.GetProviderValueComparer()); - var down = entityType.FindProperty("Down"); + var down = entityType.FindProperty("Down")!; Assert.IsType>(down.GetValueConverter()); Assert.IsType>(down.GetValueComparer()); Assert.IsType>(down.GetProviderValueComparer()); - var charm = entityType.FindProperty("Charm"); + var charm = entityType.FindProperty("Charm")!; Assert.IsType>(charm.GetValueConverter()); Assert.IsType>(charm.GetValueComparer()); Assert.IsType>(charm.GetProviderValueComparer()); - var strange = entityType.FindProperty("Strange"); + var strange = entityType.FindProperty("Strange")!; Assert.IsType>(strange.GetValueConverter()); Assert.IsType>(strange.GetValueComparer()); Assert.IsType>(strange.GetProviderValueComparer()); @@ -1068,10 +1071,10 @@ public virtual void IEnumerable_properties_with_value_converter_set_are_not_disc b => { b.Property(e => e.ExpandoObject).HasConversion( - v => (string)((IDictionary)v)["Value"], v => DeserializeExpandoObject(v)); + v => (string)((IDictionary)v!)["Value"], v => DeserializeExpandoObject(v)); var comparer = new ValueComparer( - (v1, v2) => v1.SequenceEqual(v2), + (v1, v2) => v1!.SequenceEqual(v2!), v => v.GetHashCode()); b.Property(e => e.ExpandoObject).Metadata.SetValueComparer(comparer); @@ -1080,8 +1083,8 @@ public virtual void IEnumerable_properties_with_value_converter_set_are_not_disc var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.GetEntityTypes().Single(); - Assert.NotNull(entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)).GetValueConverter()); - Assert.NotNull(entityType.FindProperty(nameof(DynamicProperty.ExpandoObject)).GetValueComparer()); + Assert.NotNull(entityType.FindProperty(nameof(DynamicProperty.ExpandoObject))!.GetValueConverter()); + Assert.NotNull(entityType.FindProperty(nameof(DynamicProperty.ExpandoObject))!.GetValueComparer()); } private static ExpandoObject DeserializeExpandoObject(string value) @@ -1095,7 +1098,7 @@ private static ExpandoObject DeserializeExpandoObject(string value) private class ExpandoObjectConverter : ValueConverter { public ExpandoObjectConverter() - : base(v => (string)((IDictionary)v)["Value"], v => DeserializeExpandoObject(v)) + : base(v => (string)((IDictionary)v!)["Value"], v => DeserializeExpandoObject(v)) { } } @@ -1103,7 +1106,7 @@ public ExpandoObjectConverter() private class ExpandoObjectComparer : ValueComparer { public ExpandoObjectComparer() - : base((v1, v2) => v1.SequenceEqual(v2), v => v.GetHashCode()) + : base((v1, v2) => v1!.SequenceEqual(v2!), v => v.GetHashCode()) { } } @@ -1124,7 +1127,7 @@ public virtual void Properties_can_have_value_converter_configured_by_type() var model = modelBuilder.FinalizeModel(); var entityType = (IReadOnlyEntityType)model.GetEntityTypes().Single(); - var wrappedProperty = entityType.FindProperty(nameof(WrappedStringEntity.WrappedString)); + var wrappedProperty = entityType.FindProperty(nameof(WrappedStringEntity.WrappedString))!; Assert.False(wrappedProperty.IsUnicode()); Assert.Equal(20, wrappedProperty.GetMaxLength()); Assert.IsType(wrappedProperty.GetValueConverter()); @@ -1147,13 +1150,13 @@ public virtual void Value_converter_configured_on_non_nullable_type_is_applied() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - var id = entityType.FindProperty("Id"); + var id = entityType.FindProperty("Id")!; Assert.IsType>(id.GetValueConverter()); Assert.IsType>(id.GetValueComparer()); - var wierd = entityType.FindProperty("Wierd"); + var wierd = entityType.FindProperty("Wierd")!; Assert.IsType>(wierd.GetValueConverter()); Assert.IsType>(wierd.GetValueComparer()); } @@ -1176,14 +1179,14 @@ public virtual void Value_converter_configured_on_nullable_type_overrides_non_nu }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - var id = entityType.FindProperty("Id"); + var id = entityType.FindProperty("Id")!; Assert.IsType>(id.GetValueConverter()); Assert.IsType>(id.GetValueComparer()); Assert.IsType>(id.GetProviderValueComparer()); - var wierd = entityType.FindProperty("Wierd"); + var wierd = entityType.FindProperty("Wierd")!; Assert.IsType>(wierd.GetValueConverter()); Assert.IsType>(wierd.GetValueComparer()); Assert.IsType>(wierd.GetProviderValueComparer()); @@ -1211,7 +1214,7 @@ public virtual void Value_converter_configured_on_base_type_is_not_applied() private class WrappedStringToStringConverter : ValueConverter { public WrappedStringToStringConverter() - : base(v => v.Value, v => new WrappedString { Value = v }) + : base(v => v.Value!, v => new WrappedString { Value = v }) { } } @@ -1249,8 +1252,8 @@ public virtual void Value_converter_type_is_checked() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); - Assert.Null(entityType.FindProperty("Up").GetValueConverter()); + var entityType = model.FindEntityType(typeof(Quarks))!; + Assert.Null(entityType.FindProperty("Up")!.GetValueConverter()); } [ConditionalFact] @@ -1267,11 +1270,11 @@ public virtual void Properties_can_have_field_set() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Equal("_forUp", entityType.FindProperty("Up").GetFieldName()); - Assert.Equal("_forDown", entityType.FindProperty("Down").GetFieldName()); - Assert.Equal("_forWierd", entityType.FindProperty("_forWierd").GetFieldName()); + Assert.Equal("_forUp", entityType.FindProperty("Up")!.GetFieldName()); + Assert.Equal("_forDown", entityType.FindProperty("Down")!.GetFieldName()); + Assert.Equal("_forWierd", entityType.FindProperty("_forWierd")!.GetFieldName()); } [ConditionalFact] @@ -1320,14 +1323,14 @@ public virtual void Properties_can_be_set_to_generate_values_on_Add() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); - Assert.Equal(ValueGenerated.OnAdd, entityType.FindProperty(Customer.IdProperty.Name).ValueGenerated); - Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Up").ValueGenerated); - Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Down").ValueGenerated); - Assert.Equal(ValueGenerated.OnUpdateSometimes, entityType.FindProperty("Charm").ValueGenerated); - Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Strange").ValueGenerated); - Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Top").ValueGenerated); - Assert.Equal(ValueGenerated.OnUpdate, entityType.FindProperty("Bottom").ValueGenerated); + var entityType = model.FindEntityType(typeof(Quarks))!; + Assert.Equal(ValueGenerated.OnAdd, entityType.FindProperty(Customer.IdProperty.Name)!.ValueGenerated); + Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Up")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Down")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdateSometimes, entityType.FindProperty("Charm")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Strange")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Top")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdate, entityType.FindProperty("Bottom")!.ValueGenerated); } [ConditionalFact] @@ -1346,15 +1349,15 @@ public virtual void Properties_can_set_row_version() var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Up").ValueGenerated); - Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Down").ValueGenerated); - Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Charm").ValueGenerated); + Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Up")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Down")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Charm")!.ValueGenerated); - Assert.True(entityType.FindProperty("Up").IsConcurrencyToken); - Assert.False(entityType.FindProperty("Down").IsConcurrencyToken); - Assert.True(entityType.FindProperty("Charm").IsConcurrencyToken); + Assert.True(entityType.FindProperty("Up")!.IsConcurrencyToken); + Assert.False(entityType.FindProperty("Down")!.IsConcurrencyToken); + Assert.True(entityType.FindProperty("Charm")!.IsConcurrencyToken); } [ConditionalFact] @@ -1374,15 +1377,15 @@ public virtual void Can_set_max_length_for_properties() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Up").GetMaxLength()); - Assert.Equal(100, entityType.FindProperty("Down").GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Charm").GetMaxLength()); - Assert.Equal(-1, entityType.FindProperty("Strange").GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Top").GetMaxLength()); - Assert.Equal(100, entityType.FindProperty("Bottom").GetMaxLength()); + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name)!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Up")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Down")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Charm")!.GetMaxLength()); + Assert.Equal(-1, entityType.FindProperty("Strange")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Top")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Bottom")!.GetMaxLength()); } [ConditionalFact] @@ -1405,15 +1408,15 @@ public virtual void Can_set_max_length_for_property_type() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name).GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Up").GetMaxLength()); - Assert.Equal(100, entityType.FindProperty("Down").GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Charm").GetMaxLength()); - Assert.Equal(100, entityType.FindProperty("Strange").GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Top").GetMaxLength()); - Assert.Equal(100, entityType.FindProperty("Bottom").GetMaxLength()); + Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name)!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Up")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Down")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Charm")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Strange")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Top")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Bottom")!.GetMaxLength()); } [ConditionalFact] @@ -1495,15 +1498,15 @@ public virtual void Can_set_unbounded_max_length_for_property_type() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name).GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Up").GetMaxLength()); - Assert.Equal(-1, entityType.FindProperty("Down").GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Charm").GetMaxLength()); - Assert.Equal(-1, entityType.FindProperty("Strange").GetMaxLength()); - Assert.Equal(0, entityType.FindProperty("Top").GetMaxLength()); - Assert.Equal(-1, entityType.FindProperty("Bottom").GetMaxLength()); + Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name)!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Up")!.GetMaxLength()); + Assert.Equal(-1, entityType.FindProperty("Down")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Charm")!.GetMaxLength()); + Assert.Equal(-1, entityType.FindProperty("Strange")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Top")!.GetMaxLength()); + Assert.Equal(-1, entityType.FindProperty("Bottom")!.GetMaxLength()); } [ConditionalFact] @@ -1523,22 +1526,22 @@ public virtual void Can_set_precision_and_scale_for_properties() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetPrecision()); - Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetScale()); - Assert.Equal(1, entityType.FindProperty("Up").GetPrecision()); - Assert.Equal(0, entityType.FindProperty("Up").GetScale()); - Assert.Equal(100, entityType.FindProperty("Down").GetPrecision()); - Assert.Equal(10, entityType.FindProperty("Down").GetScale()); - Assert.Equal(1, entityType.FindProperty("Charm").GetPrecision()); - Assert.Equal(0, entityType.FindProperty("Charm").GetScale()); - Assert.Equal(100, entityType.FindProperty("Strange").GetPrecision()); - Assert.Equal(10, entityType.FindProperty("Strange").GetScale()); - Assert.Equal(1, entityType.FindProperty("Top").GetPrecision()); - Assert.Equal(0, entityType.FindProperty("Top").GetScale()); - Assert.Equal(100, entityType.FindProperty("Bottom").GetPrecision()); - Assert.Equal(10, entityType.FindProperty("Bottom").GetScale()); + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name)!.GetPrecision()); + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name)!.GetScale()); + Assert.Equal(1, entityType.FindProperty("Up")!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Up")!.GetScale()); + Assert.Equal(100, entityType.FindProperty("Down")!.GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Down")!.GetScale()); + Assert.Equal(1, entityType.FindProperty("Charm")!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Charm")!.GetScale()); + Assert.Equal(100, entityType.FindProperty("Strange")!.GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Strange")!.GetScale()); + Assert.Equal(1, entityType.FindProperty("Top")!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Top")!.GetScale()); + Assert.Equal(100, entityType.FindProperty("Bottom")!.GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Bottom")!.GetScale()); } [ConditionalFact] @@ -1561,22 +1564,22 @@ public virtual void Can_set_precision_and_scale_for_property_type() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Equal(1, entityType.FindProperty(Customer.IdProperty.Name).GetPrecision()); - Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name).GetScale()); - Assert.Equal(1, entityType.FindProperty("Up").GetPrecision()); - Assert.Equal(0, entityType.FindProperty("Up").GetScale()); - Assert.Equal(100, entityType.FindProperty("Down").GetPrecision()); - Assert.Equal(10, entityType.FindProperty("Down").GetScale()); - Assert.Equal(1, entityType.FindProperty("Charm").GetPrecision()); - Assert.Equal(0, entityType.FindProperty("Charm").GetScale()); - Assert.Equal(100, entityType.FindProperty("Strange").GetPrecision()); - Assert.Equal(10, entityType.FindProperty("Strange").GetScale()); - Assert.Equal(1, entityType.FindProperty("Top").GetPrecision()); - Assert.Equal(0, entityType.FindProperty("Top").GetScale()); - Assert.Equal(100, entityType.FindProperty("Bottom").GetPrecision()); - Assert.Equal(10, entityType.FindProperty("Bottom").GetScale()); + Assert.Equal(1, entityType.FindProperty(Customer.IdProperty.Name)!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty(Customer.IdProperty.Name)!.GetScale()); + Assert.Equal(1, entityType.FindProperty("Up")!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Up")!.GetScale()); + Assert.Equal(100, entityType.FindProperty("Down")!.GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Down")!.GetScale()); + Assert.Equal(1, entityType.FindProperty("Charm")!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Charm")!.GetScale()); + Assert.Equal(100, entityType.FindProperty("Strange")!.GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Strange")!.GetScale()); + Assert.Equal(1, entityType.FindProperty("Top")!.GetPrecision()); + Assert.Equal(0, entityType.FindProperty("Top")!.GetScale()); + Assert.Equal(100, entityType.FindProperty("Bottom")!.GetPrecision()); + Assert.Equal(10, entityType.FindProperty("Bottom")!.GetScale()); } [ConditionalFact] @@ -1597,15 +1600,15 @@ public virtual void Can_set_custom_value_generator_for_properties() var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).GetValueGeneratorFactory()); - Assert.IsType(entityType.FindProperty("Up").GetValueGeneratorFactory()(null, null)); - Assert.IsType(entityType.FindProperty("Down").GetValueGeneratorFactory()(null, null)); - Assert.IsType(entityType.FindProperty("Charm").GetValueGeneratorFactory()(null, null)); - Assert.IsType(entityType.FindProperty("Strange").GetValueGeneratorFactory()(null, null)); - Assert.IsType(entityType.FindProperty("Top").GetValueGeneratorFactory()(null, null)); - Assert.IsType(entityType.FindProperty("Bottom").GetValueGeneratorFactory()(null, null)); + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name)!.GetValueGeneratorFactory()); + Assert.IsType(entityType.FindProperty("Up")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Down")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Charm")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Strange")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Top")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Bottom")!.GetValueGeneratorFactory()!(null!, null!)); } private class CustomValueGenerator : ValueGenerator @@ -1650,17 +1653,17 @@ public virtual void Throws_for_value_generator_that_cannot_be_constructed() b.Property(e => e.Down).HasValueGenerator(); }); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; Assert.Equal( CoreStrings.CannotCreateValueGenerator(nameof(BadCustomValueGenerator1), "HasValueGenerator"), Assert.Throws( - () => entityType.FindProperty("Up").GetValueGeneratorFactory()(null, null)).Message); + () => entityType.FindProperty("Up")!.GetValueGeneratorFactory()!(null!, null!)).Message); Assert.Equal( CoreStrings.CannotCreateValueGenerator(nameof(BadCustomValueGenerator2), "HasValueGenerator"), Assert.Throws( - () => entityType.FindProperty("Down").GetValueGeneratorFactory()(null, null)).Message); + () => entityType.FindProperty("Down")!.GetValueGeneratorFactory()!(null!, null!)).Message); } private class BadCustomValueGenerator1 : CustomValueGenerator @@ -1676,7 +1679,7 @@ private abstract class BadCustomValueGenerator2 : CustomValueGenerator protected class StringCollectionEntity { - public ICollection Property { get; set; } + public ICollection? Property { get; set; } } [ConditionalFact] @@ -1710,7 +1713,7 @@ protected virtual void Mapping_ignores_ignored_array() var model = modelBuilder.FinalizeModel(); - Assert.Null(model.FindEntityType(typeof(OneDee)).FindProperty("One")); + Assert.Null(model.FindEntityType(typeof(OneDee))!.FindProperty("One")); } [ConditionalFact] @@ -1722,7 +1725,7 @@ protected virtual void Mapping_ignores_ignored_two_dimensional_array() var model = modelBuilder.FinalizeModel(); - Assert.Null(model.FindEntityType(typeof(TwoDee)).FindProperty("Two")); + Assert.Null(model.FindEntityType(typeof(TwoDee))!.FindProperty("Two")); } [ConditionalFact] @@ -1747,28 +1750,7 @@ protected virtual void Mapping_ignores_ignored_three_dimensional_array() var model = modelBuilder.FinalizeModel(); - Assert.Null(model.FindEntityType(typeof(ThreeDee)).FindProperty("Three")); - } - - protected class OneDee - { - public int Id { get; set; } - - public int[] One { get; set; } - } - - protected class TwoDee - { - public int Id { get; set; } - - public int[,] Two { get; set; } - } - - protected class ThreeDee - { - public int Id { get; set; } - - public int[,,] Three { get; set; } + Assert.Null(model.FindEntityType(typeof(ThreeDee))!.FindProperty("Three")); } [ConditionalFact] @@ -1782,7 +1764,7 @@ public virtual void Private_property_is_not_discovered_by_convention() var model = modelBuilder.FinalizeModel(); Assert.Empty( - model.FindEntityType(typeof(Gamma)).GetProperties() + model.FindEntityType(typeof(Gamma))!.GetProperties() .Where(p => p.Name == "PrivateProperty")); } @@ -1802,7 +1784,7 @@ protected virtual void Throws_for_int_keyed_dictionary() protected class IntDict { public int Id { get; set; } - public Dictionary Notes { get; set; } + public Dictionary? Notes { get; set; } } [ConditionalFact] @@ -1822,15 +1804,15 @@ public virtual void Can_set_unicode_for_properties() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.Null(entityType.FindProperty(Customer.IdProperty.Name).IsUnicode()); - Assert.True(entityType.FindProperty("Up").IsUnicode()); - Assert.False(entityType.FindProperty("Down").IsUnicode()); - Assert.True(entityType.FindProperty("Charm").IsUnicode()); - Assert.False(entityType.FindProperty("Strange").IsUnicode()); - Assert.True(entityType.FindProperty("Top").IsUnicode()); - Assert.False(entityType.FindProperty("Bottom").IsUnicode()); + Assert.Null(entityType.FindProperty(Customer.IdProperty.Name)!.IsUnicode()); + Assert.True(entityType.FindProperty("Up")!.IsUnicode()); + Assert.False(entityType.FindProperty("Down")!.IsUnicode()); + Assert.True(entityType.FindProperty("Charm")!.IsUnicode()); + Assert.False(entityType.FindProperty("Strange")!.IsUnicode()); + Assert.True(entityType.FindProperty("Top")!.IsUnicode()); + Assert.False(entityType.FindProperty("Bottom")!.IsUnicode()); } [ConditionalFact] @@ -1853,15 +1835,15 @@ public virtual void Can_set_unicode_for_property_type() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Quarks)); + var entityType = model.FindEntityType(typeof(Quarks))!; - Assert.True(entityType.FindProperty(Customer.IdProperty.Name).IsUnicode()); - Assert.True(entityType.FindProperty("Up").IsUnicode()); - Assert.False(entityType.FindProperty("Down").IsUnicode()); - Assert.True(entityType.FindProperty("Charm").IsUnicode()); - Assert.False(entityType.FindProperty("Strange").IsUnicode()); - Assert.True(entityType.FindProperty("Top").IsUnicode()); - Assert.False(entityType.FindProperty("Bottom").IsUnicode()); + Assert.True(entityType.FindProperty(Customer.IdProperty.Name)!.IsUnicode()); + Assert.True(entityType.FindProperty("Up")!.IsUnicode()); + Assert.False(entityType.FindProperty("Down")!.IsUnicode()); + Assert.True(entityType.FindProperty("Charm")!.IsUnicode()); + Assert.False(entityType.FindProperty("Strange")!.IsUnicode()); + Assert.True(entityType.FindProperty("Top")!.IsUnicode()); + Assert.False(entityType.FindProperty("Bottom")!.IsUnicode()); } [ConditionalFact] @@ -1884,7 +1866,7 @@ public virtual void PropertyBuilder_methods_can_be_chained() .HasValueGenerator(typeof(CustomValueGenerator)) .HasValueGeneratorFactory() .HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)) - .HasValueGenerator((_, __) => null) + .HasValueGenerator((_, __) => null!) .IsRequired(); [ConditionalFact] @@ -1898,7 +1880,7 @@ public virtual void Can_add_index() .HasIndex(ix => ix.Name); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Customer)); + var entityType = model.FindEntityType(typeof(Customer))!; var index = entityType.GetIndexes().Single(); Assert.Equal(Customer.NameProperty.Name, index.Properties.Single().Name); @@ -1919,7 +1901,7 @@ public virtual void Can_add_index_when_no_clr_property() }); var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Customer)); + var entityType = model.FindEntityType(typeof(Customer))!; var index = entityType.GetIndexes().Single(); Assert.Equal("Index", index.Properties.Single().Name); @@ -1939,19 +1921,19 @@ public virtual void Can_add_multiple_indexes() var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(Customer)); - var idProperty = entityType.FindProperty(nameof(Customer.Id)); - var nameProperty = entityType.FindProperty(nameof(Customer.Name)); + var entityType = model.FindEntityType(typeof(Customer))!; + var idProperty = entityType.FindProperty(nameof(Customer.Id))!; + var nameProperty = entityType.FindProperty(nameof(Customer.Name))!; Assert.Equal(4, entityType.GetIndexes().Count()); - var firstIndex = entityType.FindIndex(idProperty); + var firstIndex = entityType.FindIndex(idProperty)!; Assert.True(firstIndex.IsUnique); - var secondIndex = entityType.FindIndex(nameProperty); + var secondIndex = entityType.FindIndex(nameProperty)!; Assert.False(secondIndex.IsUnique); Assert.Equal("V1", secondIndex["A1"]); - var namedIndex = entityType.FindIndex("Named"); + var namedIndex = entityType.FindIndex("Named")!; Assert.False(namedIndex.IsUnique); - var descendingIndex = entityType.FindIndex("Descending"); + var descendingIndex = entityType.FindIndex("Descending")!; Assert.Equal(Array.Empty(), descendingIndex.IsDescending); } @@ -1968,7 +1950,7 @@ public virtual void Can_add_contained_indexes() ix => new { ix.Id }); var model = modelBuilder.FinalizeModel(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Customer)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(Customer))!; Assert.Equal(2, entityType.GetIndexes().Count()); Assert.True(firstIndexBuilder.Metadata.IsUnique); @@ -1983,14 +1965,14 @@ public virtual void Can_set_primary_key_by_convention_for_user_specified_shadow_ var entityBuilder = modelBuilder.Entity(); - var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(EntityWithoutId)); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(EntityWithoutId))!; Assert.Null(entityType.FindPrimaryKey()); entityBuilder.Property("Id"); Assert.NotNull(entityType.FindPrimaryKey()); - AssertEqual(new[] { "Id" }, entityType.FindPrimaryKey().Properties.Select(p => p.Name)); + AssertEqual(new[] { "Id" }, entityType.FindPrimaryKey()!.Properties.Select(p => p.Name)); } [ConditionalFact] @@ -2001,13 +1983,13 @@ public virtual void Can_ignore_explicit_interface_implementation_property() Assert.DoesNotContain( nameof(IEntityBase.Target), - modelBuilder.Model.FindEntityType(typeof(EntityBase)).GetProperties().Select(p => p.Name)); + modelBuilder.Model.FindEntityType(typeof(EntityBase))!.GetProperties().Select(p => p.Name)); modelBuilder.Entity().Property(e => ((IEntityBase)e).Target); Assert.Contains( nameof(IEntityBase.Target), - modelBuilder.Model.FindEntityType(typeof(EntityBase)).GetProperties().Select(p => p.Name)); + modelBuilder.Model.FindEntityType(typeof(EntityBase))!.GetProperties().Select(p => p.Name)); } [ConditionalFact] @@ -2018,7 +2000,7 @@ public virtual void Can_set_key_on_an_entity_with_fields() modelBuilder.Entity().HasKey(e => e.Id); var model = modelBuilder.FinalizeModel(); - var entity = model.FindEntityType(typeof(EntityWithFields)); + var entity = model.FindEntityType(typeof(EntityWithFields))!; var primaryKey = entity.FindPrimaryKey(); Assert.NotNull(primaryKey); var property = Assert.Single(primaryKey.Properties); @@ -2035,8 +2017,8 @@ public virtual void Can_set_composite_key_on_an_entity_with_fields() modelBuilder.Entity().HasKey(e => new { e.TenantId, e.CompanyId }); var model = modelBuilder.FinalizeModel(); - var entity = model.FindEntityType(typeof(EntityWithFields)); - var primaryKeyProperties = entity.FindPrimaryKey().Properties; + var entity = model.FindEntityType(typeof(EntityWithFields))!; + var primaryKeyProperties = entity.FindPrimaryKey()!.Properties; Assert.Equal(2, primaryKeyProperties.Count); var first = primaryKeyProperties[0]; var second = primaryKeyProperties[1]; @@ -2055,7 +2037,7 @@ public virtual void Can_set_alternate_key_on_an_entity_with_fields() modelBuilder.Entity().HasAlternateKey(e => e.CompanyId); - var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)); + var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields))!; var properties = entity.GetProperties(); Assert.Single(properties); var property = properties.Single(); @@ -2074,7 +2056,7 @@ public virtual void Can_set_composite_alternate_key_on_an_entity_with_fields() modelBuilder.Entity().HasAlternateKey(e => new { e.TenantId, e.CompanyId }); - var keys = modelBuilder.Model.FindEntityType(typeof(EntityWithFields)).GetKeys(); + var keys = modelBuilder.Model.FindEntityType(typeof(EntityWithFields))!.GetKeys(); Assert.Single(keys); var properties = keys.Single().Properties; Assert.Equal(2, properties.Count); @@ -2096,7 +2078,7 @@ public virtual void Can_call_Property_on_an_entity_with_fields() modelBuilder.Entity().Property(e => e.Id); var model = modelBuilder.FinalizeModel(); - var properties = model.FindEntityType(typeof(EntityWithFields)).GetProperties(); + var properties = model.FindEntityType(typeof(EntityWithFields))!.GetProperties(); var property = Assert.Single(properties); Assert.Equal(nameof(EntityWithFields.Id), property.Name); Assert.Null(property.PropertyInfo); @@ -2111,7 +2093,7 @@ public virtual void Can_set_index_on_an_entity_with_fields() modelBuilder.Entity().HasNoKey().HasIndex(e => e.CompanyId); var model = modelBuilder.FinalizeModel(); - var indexes = model.FindEntityType(typeof(EntityWithFields)).GetIndexes(); + var indexes = model.FindEntityType(typeof(EntityWithFields))!.GetIndexes(); var index = Assert.Single(indexes); var property = Assert.Single(index.Properties); Assert.Null(property.PropertyInfo); @@ -2126,7 +2108,7 @@ public virtual void Can_set_composite_index_on_an_entity_with_fields() modelBuilder.Entity().HasNoKey().HasIndex(e => new { e.TenantId, e.CompanyId }); var model = modelBuilder.FinalizeModel(); - var indexes = model.FindEntityType(typeof(EntityWithFields)).GetIndexes(); + var indexes = model.FindEntityType(typeof(EntityWithFields))!.GetIndexes(); var index = Assert.Single(indexes); Assert.Equal(2, index.Properties.Count); var properties = index.Properties; @@ -2150,7 +2132,7 @@ public virtual void Can_ignore_a_field_on_an_entity_with_fields() .HasKey(e => e.Id); var model = modelBuilder.FinalizeModel(); - var entity = model.FindEntityType(typeof(EntityWithFields)); + var entity = model.FindEntityType(typeof(EntityWithFields))!; var property = Assert.Single(entity.GetProperties()); Assert.Equal(nameof(EntityWithFields.Id), property.Name); } @@ -2166,7 +2148,7 @@ public virtual void Can_ignore_a_field_on_a_keyless_entity_with_fields() .Property(e => e.LastName); var model = modelBuilder.FinalizeModel(); - var entity = model.FindEntityType(typeof(KeylessEntityWithFields)); + var entity = model.FindEntityType(typeof(KeylessEntityWithFields))!; var property = Assert.Single(entity.GetProperties()); Assert.Equal(nameof(KeylessEntityWithFields.LastName), property.Name); } @@ -2188,7 +2170,7 @@ public virtual void Can_add_seed_data_objects() var finalModel = modelBuilder.FinalizeModel(); - var customer = finalModel.FindEntityType(typeof(Beta)); + var customer = finalModel.FindEntityType(typeof(Beta))!; var data = customer.GetSeedData(); Assert.Equal(2, data.Count()); Assert.Equal(-1, data.First()[nameof(Beta.Id)]); @@ -2214,7 +2196,7 @@ public virtual void Can_add_seed_data_anonymous_objects() var model = modelBuilder.FinalizeModel(); - var customer = model.FindEntityType(typeof(Beta)); + var customer = model.FindEntityType(typeof(Beta))!; var data = customer.GetSeedData(); Assert.Equal(2, data.Count()); Assert.Equal(-1, data.First().Values.Single()); @@ -2238,7 +2220,7 @@ public virtual void Can_add_seed_data_objects_indexed_property() var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(IndexedClass)); + var entityType = model.FindEntityType(typeof(IndexedClass))!; var data = Assert.Single(entityType.GetSeedData()); Assert.Equal(-1, data["Id"]); Assert.Equal(2, data["Required"]); @@ -2260,7 +2242,7 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property() var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(IndexedClass)); + var entityType = model.FindEntityType(typeof(IndexedClass))!; var data = Assert.Single(entityType.GetSeedData()); Assert.Equal(-1, data["Id"]); Assert.Equal(2, data["Required"]); @@ -2283,7 +2265,7 @@ public virtual void Can_add_seed_data_objects_indexed_property_dictionary() var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(IndexedClassByDictionary)); + var entityType = model.FindEntityType(typeof(IndexedClassByDictionary))!; var data = Assert.Single(entityType.GetSeedData()); Assert.Equal(-1, data["Id"]); Assert.Equal(2, data["Required"]); @@ -2304,7 +2286,7 @@ public virtual void Can_add_seed_data_anonymous_objects_indexed_property_diction var model = modelBuilder.FinalizeModel(); - var entityType = model.FindEntityType(typeof(IndexedClassByDictionary)); + var entityType = model.FindEntityType(typeof(IndexedClassByDictionary))!; var data = Assert.Single(entityType.GetSeedData()); Assert.Equal(-1, data["Id"]); Assert.Equal(2, data["Required"]); @@ -2323,7 +2305,7 @@ public virtual void EntityType_name_is_stored_culture_invariantly() var model = modelBuilder.FinalizeModel(); Assert.Equal(2, model.GetEntityTypes().Count()); - Assert.Equal(2, model.FindEntityType(typeof(Entityss)).GetNavigations().Count()); + Assert.Equal(2, model.FindEntityType(typeof(Entityss))!.GetNavigations().Count()); } protected class Entityß @@ -2334,8 +2316,8 @@ protected class Entityß protected class Entityss { public int Id { get; set; } - public Entityß Navigationß { get; set; } - public Entityß Navigationss { get; set; } + public Entityß? Navigationß { get; set; } + public Entityß? Navigationss { get; set; } } [ConditionalFact] @@ -2365,18 +2347,18 @@ public virtual void Can_add_shared_type_entity_type() Assert.NotNull(shared1); Assert.True(shared1.HasSharedClrType); Assert.Null(shared1.FindProperty("Id")); - Assert.Equal(typeof(int), shared1.FindProperty("Keys").ClrType); - Assert.Equal(typeof(byte[]), shared1.FindProperty("Values").ClrType); - Assert.Equal(typeof(string), shared1.FindProperty("Count").ClrType); + Assert.Equal(typeof(int), shared1.FindProperty("Keys")!.ClrType); + Assert.Equal(typeof(byte[]), shared1.FindProperty("Values")!.ClrType); + Assert.Equal(typeof(string), shared1.FindProperty("Count")!.ClrType); var shared2 = model.FindEntityType("Shared2"); Assert.NotNull(shared2); Assert.True(shared2.HasSharedClrType); Assert.NotNull(shared2.FindProperty("Id")); - var indexer = shared1.FindIndexerPropertyInfo(); - Assert.True(model.IsIndexerMethod(indexer.GetMethod)); - Assert.True(model.IsIndexerMethod(indexer.SetMethod)); + var indexer = shared1.FindIndexerPropertyInfo()!; + Assert.True(model.IsIndexerMethod(indexer.GetMethod!)); + Assert.True(model.IsIndexerMethod(indexer.SetMethod!)); Assert.Same(indexer, shared2.FindIndexerPropertyInfo()); } @@ -2391,5 +2373,1051 @@ public virtual void Cannot_add_shared_type_when_non_shared_exists() CoreStrings.ClashingNonSharedType("Shared1", nameof(Customer)), Assert.Throws(() => modelBuilder.SharedTypeEntity("Shared1")).Message); } + + [ConditionalFact] + public virtual void Can_set_primitive_collection_annotation() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder + .Entity() + .PrimitiveCollection(c => c.Notes).HasAnnotation("foo", "bar"); + + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!.FindProperty(nameof(Customer.Notes))!; + + Assert.Equal("bar", property["foo"]); + } + + [ConditionalFact] + public virtual void Can_set_primitive_collection_annotation_when_no_clr_property() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder + .Entity() + .PrimitiveCollection>(nameof(Customer.Notes)).HasAnnotation("foo", "bar"); + + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!.FindProperty(nameof(Customer.Notes))!; + + Assert.Equal("bar", property["foo"]); + } + + [ConditionalFact] + public virtual void Can_set_primitive_collection_annotation_by_type() + { + var modelBuilder = CreateModelBuilder(c => c.Properties().HaveAnnotation("foo", "bar")); + + modelBuilder.Ignore(); + var propertyBuilder = modelBuilder + .Entity() + .PrimitiveCollection(c => c.Notes); + + var property = modelBuilder.FinalizeModel().FindEntityType(typeof(Customer))!.FindProperty(nameof(Customer.Name))!; + + Assert.Equal("bar", property["foo"]); + } + + [ConditionalFact] + public virtual void Primitive_collections_are_required_by_default_only_if_CLR_type_is_nullable() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection?>("Strange"); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.False(entityType.FindProperty("Up")!.IsNullable); + Assert.True(entityType.FindProperty("Down")!.IsNullable); + Assert.True(entityType.FindProperty("Charm")!.IsNullable); // Because we can't detect the non-nullable reference type + Assert.True(entityType.FindProperty("Strange")!.IsNullable); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_ignored() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.Ignore(e => e.Up); + b.Ignore(e => e.Down); + b.Ignore("Charm"); + b.Ignore("Strange"); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + Assert.Contains(nameof(CollectionQuarks.Id), entityType.GetProperties().Select(p => p.Name)); + Assert.DoesNotContain(nameof(CollectionQuarks.Up), entityType.GetProperties().Select(p => p.Name)); + Assert.DoesNotContain(nameof(CollectionQuarks.Down), entityType.GetProperties().Select(p => p.Name)); + } + + [ConditionalFact] + public virtual void Can_override_navigations_as_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + modelBuilder.Entity(); + + var customer = model.FindEntityType(typeof(Customer))!; + Assert.NotNull(customer.FindNavigation(nameof(Customer.Orders))); + + modelBuilder.Entity().PrimitiveCollection(c => c.Orders); + + Assert.Null(customer.FindNavigation(nameof(Customer.Orders))); + var property = customer.FindProperty(nameof(Customer.Orders)); + Assert.NotNull(property); + Assert.NotNull(property.GetElementType()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_made_required() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).IsRequired(); + b.PrimitiveCollection(e => e.Down).IsRequired(); + b.PrimitiveCollection>("Charm").IsRequired(); + b.PrimitiveCollection?>("Strange").IsRequired(); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.False(entityType.FindProperty("Up")!.IsNullable); + Assert.False(entityType.FindProperty("Down")!.IsNullable); + Assert.False(entityType.FindProperty("Charm")!.IsNullable); + Assert.False(entityType.FindProperty("Strange")!.IsNullable); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_made_optional() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).IsRequired(false); + b.PrimitiveCollection(e => e.Down).IsRequired(false); + b.PrimitiveCollection>("Charm").IsRequired(false); + b.PrimitiveCollection?>("Strange").IsRequired(false); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.True(entityType.FindProperty("Up")!.IsNullable); + Assert.True(entityType.FindProperty("Down")!.IsNullable); + Assert.True(entityType.FindProperty("Charm")!.IsNullable); + Assert.True(entityType.FindProperty("Strange")!.IsNullable); + } + + [ConditionalFact] + public virtual void PrimitiveCollection_Key_properties_cannot_be_made_optional() + => Assert.Equal( + CoreStrings.KeyPropertyCannotBeNullable(nameof(CollectionQuarks.Down), nameof(CollectionQuarks), "{'" + nameof(CollectionQuarks.Down) + "'}"), + Assert.Throws( + () => + CreateModelBuilder().Entity( + b => + { + b.HasAlternateKey( + e => new { e.Down }); + b.PrimitiveCollection(e => e.Down).IsRequired(false); + })).Message); + + [ConditionalFact] + public virtual void Primitive_collections_specified_by_string_are_shadow_properties_unless_already_known_to_be_CLR_properties() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection>("Up"); + b.PrimitiveCollection?>("Down"); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection?>("Strange"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.False(entityType.FindProperty("Up")!.IsShadowProperty()); + Assert.False(entityType.FindProperty("Down")!.IsShadowProperty()); + Assert.True(entityType.FindProperty("Charm")!.IsShadowProperty()); + Assert.True(entityType.FindProperty("Strange")!.IsShadowProperty()); + + Assert.Equal(-1, entityType.FindProperty("Up")!.GetShadowIndex()); + Assert.Equal(-1, entityType.FindProperty("Down")!.GetShadowIndex()); + Assert.NotEqual(-1, entityType.FindProperty("Charm")!.GetShadowIndex()); + Assert.NotEqual(-1, entityType.FindProperty("Strange")!.GetShadowIndex()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_made_concurrency_tokens() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).IsConcurrencyToken(); + b.PrimitiveCollection(e => e.Down).IsConcurrencyToken(false); + b.PrimitiveCollection>("Charm").IsConcurrencyToken(); + b.PrimitiveCollection?>("Strange").IsConcurrencyToken(false); + b.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.True(entityType.FindProperty("Up")!.IsConcurrencyToken); + Assert.False(entityType.FindProperty("Down")!.IsConcurrencyToken); + Assert.True(entityType.FindProperty("Charm")!.IsConcurrencyToken); + Assert.False(entityType.FindProperty("Strange")!.IsConcurrencyToken); + + Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, entityType.GetChangeTrackingStrategy()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_have_access_mode_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down).HasField("_forDown").UsePropertyAccessMode(PropertyAccessMode.Field); + b.PrimitiveCollection>("Charm").UsePropertyAccessMode(PropertyAccessMode.Property); + b.PrimitiveCollection?>("Strange").UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Equal(PropertyAccessMode.PreferField, entityType.FindProperty("Up")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Field, entityType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, entityType.FindProperty("Charm")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, entityType.FindProperty("Strange")!.GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Access_mode_can_be_overridden_at_entity_and_primitive_collection_levels() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field); + + modelBuilder.Entity( + b => + { + b.UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); + b.PrimitiveCollection(e => e.Up).UsePropertyAccessMode(PropertyAccessMode.Property); + b.PrimitiveCollection(e => e.Down).HasField("_forDown"); + }); + + var model = modelBuilder.FinalizeModel(); + Assert.Equal(PropertyAccessMode.Field, model.GetPropertyAccessMode()); + + var collectionQuarksType = (IReadOnlyEntityType)model.FindEntityType(typeof(CollectionQuarks))!; + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, collectionQuarksType.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.FieldDuringConstruction, collectionQuarksType.FindProperty("Down")!.GetPropertyAccessMode()); + Assert.Equal(PropertyAccessMode.Property, collectionQuarksType.FindProperty("Up")!.GetPropertyAccessMode()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_have_field_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection>("Up").HasField("_forUp"); + b.PrimitiveCollection(e => e.Down).HasField("_forDown"); + b.PrimitiveCollection?>("_forWierd").HasField("_forWierd"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Equal("_forUp", entityType.FindProperty("Up")!.GetFieldName()); + Assert.Equal("_forDown", entityType.FindProperty("Down")!.GetFieldName()); + Assert.Equal("_forWierd", entityType.FindProperty("_forWierd")!.GetFieldName()); + } + + [ConditionalFact] + public virtual void HasField_for_primitive_collection_throws_if_field_is_not_found() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + Assert.Equal( + CoreStrings.MissingBackingField("_notFound", nameof(CollectionQuarks.Down), nameof(CollectionQuarks)), + Assert.Throws(() => b.PrimitiveCollection(e => e.Down).HasField("_notFound")).Message); + }); + } + + [ConditionalFact] + public virtual void HasField_for_primitive_collection_throws_if_field_is_wrong_type() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + Assert.Equal( + CoreStrings.BadBackingFieldType( + "_forUp", "ObservableCollection", nameof(CollectionQuarks), nameof(CollectionQuarks.Down), + "ObservableCollection"), + Assert.Throws(() => b.PrimitiveCollection(e => e.Down).HasField("_forUp")).Message); + }); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_be_set_to_generate_values_on_Add() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.HasKey(e => e.Id); + b.PrimitiveCollection(e => e.Up).ValueGeneratedOnAddOrUpdate(); + b.PrimitiveCollection(e => e.Down).ValueGeneratedNever(); + b.PrimitiveCollection>("Charm").Metadata.ValueGenerated = ValueGenerated.OnUpdateSometimes; + b.PrimitiveCollection>("Strange").ValueGeneratedNever(); + b.PrimitiveCollection>("Top").ValueGeneratedOnAddOrUpdate(); + b.PrimitiveCollection>("Bottom").ValueGeneratedOnUpdate(); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Up")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Down")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdateSometimes, entityType.FindProperty("Charm")!.ValueGenerated); + Assert.Equal(ValueGenerated.Never, entityType.FindProperty("Strange")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnAddOrUpdate, entityType.FindProperty("Top")!.ValueGenerated); + Assert.Equal(ValueGenerated.OnUpdate, entityType.FindProperty("Bottom")!.ValueGenerated); + } + + [ConditionalFact] + public virtual void Can_set_max_length_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).HasMaxLength(0); + b.PrimitiveCollection(e => e.Down).HasMaxLength(100); + b.PrimitiveCollection>("Charm").HasMaxLength(0); + b.PrimitiveCollection>("Strange").HasMaxLength(-1); + b.PrimitiveCollection("Top").HasMaxLength(0); + b.PrimitiveCollection("Bottom").HasMaxLength(100); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Id))!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Up")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Down")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Charm")!.GetMaxLength()); + Assert.Equal(-1, entityType.FindProperty("Strange")!.GetMaxLength()); + Assert.Equal(0, entityType.FindProperty("Top")!.GetMaxLength()); + Assert.Equal(100, entityType.FindProperty("Bottom")!.GetMaxLength()); + } + + [ConditionalFact] + public virtual void Can_set_sentinel_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).HasSentinel(1); + b.PrimitiveCollection(e => e.Down).HasSentinel("100"); + b.PrimitiveCollection("Charm").HasSentinel(-1); + b.PrimitiveCollection>("Strange").HasSentinel("-1"); + b.PrimitiveCollection("Top").HasSentinel(77); + b.PrimitiveCollection>("Bottom").HasSentinel("100"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Equal(0, entityType.FindProperty(nameof(CollectionQuarks.Id))!.Sentinel); + Assert.Equal(1, entityType.FindProperty("Up")!.Sentinel); + Assert.Equal("100", entityType.FindProperty("Down")!.Sentinel); + Assert.Equal(-1, entityType.FindProperty("Charm")!.Sentinel); + Assert.Equal("-1", entityType.FindProperty("Strange")!.Sentinel); + Assert.Equal(77, entityType.FindProperty("Top")!.Sentinel); + Assert.Equal("100", entityType.FindProperty("Bottom")!.Sentinel); + } + + [ConditionalFact] + public virtual void Can_set_custom_value_generator_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).HasValueGenerator(); + b.PrimitiveCollection(e => e.Down).HasValueGenerator(typeof(CustomValueGenerator)); + b.PrimitiveCollection>("Strange").HasValueGenerator(); + b.PrimitiveCollection("Top").HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)); + b.PrimitiveCollection>("Bottom").HasValueGeneratorFactory(); + }); + + var model = modelBuilder.FinalizeModel(); + + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Id))!.GetValueGeneratorFactory()); + Assert.IsType(entityType.FindProperty("Up")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Down")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Strange")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Top")!.GetValueGeneratorFactory()!(null!, null!)); + Assert.IsType(entityType.FindProperty("Bottom")!.GetValueGeneratorFactory()!(null!, null!)); + } + + [ConditionalFact] + public virtual void Throws_for_bad_value_generator_type_for_primitive_collection() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + Assert.Equal( + CoreStrings.BadValueGeneratorType(nameof(Random), nameof(ValueGenerator)), + Assert.Throws(() => b.PrimitiveCollection(e => e.Down).HasValueGenerator(typeof(Random))).Message); + }); + } + + [ConditionalFact] + public virtual void Throws_for_primitive_collection_for_value_generator_that_cannot_be_constructed() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).HasValueGenerator(); + b.PrimitiveCollection(e => e.Down).HasValueGenerator(); + }); + + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Equal( + CoreStrings.CannotCreateValueGenerator(nameof(BadCustomValueGenerator1), "HasValueGenerator"), + Assert.Throws( + () => entityType.FindProperty("Up")!.GetValueGeneratorFactory()!(null!, null!)).Message); + + Assert.Equal( + CoreStrings.CannotCreateValueGenerator(nameof(BadCustomValueGenerator2), "HasValueGenerator"), + Assert.Throws( + () => entityType.FindProperty("Down")!.GetValueGeneratorFactory()!(null!, null!)).Message); + } + + [ConditionalFact] + protected virtual void Mapping_for_primitive_collection_ignores_ignored_array() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity().Ignore(e => e.One); + + var model = modelBuilder.FinalizeModel(); + + Assert.Null(model.FindEntityType(typeof(OneDee))!.FindProperty("One")); + } + + [ConditionalFact] + public virtual void Private_primitive_collection_is_not_discovered_by_convention() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + + Assert.Empty( + model.FindEntityType(typeof(Gamma))!.GetProperties() + .Where(p => p.Name == "PrivateCollection")); + } + + [ConditionalFact] + public virtual void Can_set_unicode_for_primitive_collections() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).IsUnicode(); + b.PrimitiveCollection(e => e.Down).IsUnicode(false); + b.PrimitiveCollection("Charm").IsUnicode(); + b.PrimitiveCollection>("Strange").IsUnicode(false); + b.PrimitiveCollection("Top").IsUnicode(); + b.PrimitiveCollection?>("Bottom").IsUnicode(false); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Id))!.IsUnicode()); + Assert.True(entityType.FindProperty("Up")!.IsUnicode()); + Assert.False(entityType.FindProperty("Down")!.IsUnicode()); + Assert.True(entityType.FindProperty("Charm")!.IsUnicode()); + Assert.False(entityType.FindProperty("Strange")!.IsUnicode()); + Assert.True(entityType.FindProperty("Top")!.IsUnicode()); + Assert.False(entityType.FindProperty("Bottom")!.IsUnicode()); + } + + [ConditionalFact] + public virtual void PrimitiveCollectionBuilder_methods_can_be_chained() + => CreateModelBuilder() + .Entity() + .PrimitiveCollection(e => e.Up) + .IsRequired() + .HasAnnotation("A", "V") + .IsConcurrencyToken() + .ValueGeneratedNever() + .ValueGeneratedOnAdd() + .ValueGeneratedOnAddOrUpdate() + .ValueGeneratedOnUpdate() + .IsUnicode() + .HasMaxLength(100) + .HasSentinel(null) + .HasValueGenerator() + .HasValueGenerator(typeof(CustomValueGenerator)) + .HasValueGeneratorFactory() + .HasValueGeneratorFactory(typeof(CustomValueGeneratorFactory)) + .IsRequired(); + + [ConditionalFact] + public virtual void Can_set_primary_key_by_convention_for_user_specified_shadow_primitive_collection() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + var entityBuilder = modelBuilder.Entity(); + + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(EntityWithoutId))!; + + Assert.Null(entityType.FindPrimaryKey()); + + entityBuilder.PrimitiveCollection>("Id"); + + Assert.NotNull(entityType.FindPrimaryKey()); + AssertEqual(new[] { "Id" }, entityType.FindPrimaryKey()!.Properties.Select(p => p.Name)); + } + + [ConditionalFact] + public virtual void Can_set_key_for_primitive_collection_on_an_entity_with_fields() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity().HasKey(e => e.Id); + + var model = modelBuilder.FinalizeModel(); + var entity = model.FindEntityType(typeof(EntityWithFields))!; + var primaryKey = entity.FindPrimaryKey(); + Assert.NotNull(primaryKey); + var property = Assert.Single(primaryKey.Properties); + Assert.Equal(nameof(EntityWithFields.Id), property.Name); + Assert.Null(property.PropertyInfo); + Assert.NotNull(property.FieldInfo); + } + + [ConditionalFact] + public virtual void Can_set_composite_key_for_primitive_collection_on_an_entity_with_fields() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.CollectionCompanyId); // Issue #31417 + b.PrimitiveCollection(e => e.CollectionTenantId); + b.HasKey(e => new { e.CollectionCompanyId, e.CollectionTenantId }); + }); + + var model = modelBuilder.FinalizeModel(); + var entity = model.FindEntityType(typeof(EntityWithFields))!; + var primaryKeyProperties = entity.FindPrimaryKey()!.Properties; + Assert.Equal(2, primaryKeyProperties.Count); + var first = primaryKeyProperties[0]; + var second = primaryKeyProperties[1]; + Assert.Equal(nameof(EntityWithFields.CollectionCompanyId), first.Name); + Assert.Null(first.PropertyInfo); + Assert.NotNull(first.FieldInfo); + Assert.NotNull(first.GetElementType()); + Assert.Equal(nameof(EntityWithFields.CollectionTenantId), second.Name); + Assert.Null(second.PropertyInfo); + Assert.NotNull(second.FieldInfo); + Assert.NotNull(second.GetElementType()); + } + + [ConditionalFact] + public virtual void Can_set_alternate_key_for_primitive_collection_on_an_entity_with_fields() + { + var modelBuilder = CreateTestModelBuilder(InMemoryTestHelpers.Instance); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.CollectionCompanyId); + b.HasAlternateKey(e => e.CollectionCompanyId); + }); + + var entity = modelBuilder.Model.FindEntityType(typeof(EntityWithFields))!; + var properties = entity.GetProperties(); + Assert.Single(properties); + var property = properties.Single(); + Assert.Equal(nameof(EntityWithFields.CollectionCompanyId), property.Name); + Assert.Null(property.PropertyInfo); + Assert.NotNull(property.FieldInfo); + Assert.NotNull(property.GetElementType()); + var keys = entity.GetKeys(); + var key = Assert.Single(keys); + Assert.Equal(properties, key.Properties); + } + + [ConditionalFact] + public virtual void Can_call_PrimitiveCollection_on_an_entity_with_fields() + { + var modelBuilder = CreateTestModelBuilder(InMemoryTestHelpers.Instance); + + modelBuilder.Entity().PrimitiveCollection(e => e.CollectionId); + modelBuilder.Entity().HasKey(e => e.CollectionId); + + var model = modelBuilder.FinalizeModel(); + var properties = model.FindEntityType(typeof(EntityWithFields))!.GetProperties(); + var property = Assert.Single(properties); + Assert.Equal(nameof(EntityWithFields.CollectionId), property.Name); + Assert.Null(property.PropertyInfo); + Assert.NotNull(property.FieldInfo); + Assert.NotNull(property.GetElementType()); + } + + [ConditionalFact] + public virtual void Can_set_element_type_annotation() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Ignore(); + modelBuilder + .Entity() + .PrimitiveCollection(c => c.Notes) + .ElementType() + .HasAnnotation("foo", "bar"); + + var elementType = modelBuilder.FinalizeModel() + .FindEntityType(typeof(Customer))! + .FindProperty(nameof(Customer.Notes))! + .GetElementType()!; + + Assert.Equal("bar", elementType["foo"]); + } + + [ConditionalFact] + public virtual void Element_types_are_nullable_by_default_if_the_type_is_nullable() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange"); + b.PrimitiveCollection>("Stranger"); // Still optional since no NRT metadata available + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.False(entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!.IsNullable); + Assert.True(entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!.IsNullable); // Issue #31416 + Assert.True(entityType.FindProperty("Charm")!.GetElementType()!.IsNullable); + Assert.True(entityType.FindProperty("Strange")!.GetElementType()!.IsNullable); + Assert.True(entityType.FindProperty("Stranger")!.GetElementType()!.IsNullable); + } + + [ConditionalFact] + public virtual void Element_types_can_be_made_required() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).ElementType().IsRequired(); + b.PrimitiveCollection(e => e.Down).ElementType().IsRequired(false); + b.PrimitiveCollection>("Charm").ElementType().IsRequired();; + b.PrimitiveCollection>("Strange").ElementType().IsRequired();; + b.PrimitiveCollection>("Stranger").ElementType().IsRequired();; // Still optional since no NRT metadata available + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.False(entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!.IsNullable); + Assert.True(entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!.IsNullable); + Assert.False(entityType.FindProperty("Charm")!.GetElementType()!.IsNullable); + Assert.False(entityType.FindProperty("Strange")!.GetElementType()!.IsNullable); + Assert.False(entityType.FindProperty("Stranger")!.GetElementType()!.IsNullable); + } + + [ConditionalFact] + public virtual void Element_types_have_no_max_length_by_default() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange"); + b.PrimitiveCollection>("Stranger"); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!.GetMaxLength()); + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!.GetMaxLength()); + Assert.Null(entityType.FindProperty("Charm")!.GetElementType()!.GetMaxLength()); + Assert.Null(entityType.FindProperty("Strange")!.GetElementType()!.GetMaxLength()); + Assert.Null(entityType.FindProperty("Stranger")!.GetElementType()!.GetMaxLength()); + } + + [ConditionalFact] + public virtual void Element_types_can_have_max_length() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Down).ElementType().HasMaxLength(-1); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange").ElementType().HasMaxLength(512); + b.PrimitiveCollection>("Stranger").ElementType().HasMaxLength(int.MaxValue); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!.GetMaxLength()); + Assert.Equal(-1, entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!.GetMaxLength()); + Assert.Null(entityType.FindProperty("Charm")!.GetElementType()!.GetMaxLength()); + Assert.Equal(512, entityType.FindProperty("Strange")!.GetElementType()!.GetMaxLength()); + Assert.Equal(int.MaxValue, entityType.FindProperty("Stranger")!.GetElementType()!.GetMaxLength()); + } + + [ConditionalFact] + public virtual void Element_types_have_default_precision_and_scale() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange"); + b.PrimitiveCollection>("Stranger"); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + var property = entityType.FindProperty(nameof(CollectionQuarks.Up))!; + Assert.Null(property.GetElementType()!.GetPrecision()); + Assert.Null(property.GetElementType()!.GetScale()); + property = entityType.FindProperty(nameof(CollectionQuarks.Down))!; + Assert.Null(property.GetElementType()!.GetPrecision()); + Assert.Null(property.GetElementType()!.GetScale()); + property = entityType.FindProperty("Charm")!; + Assert.Null(property.GetElementType()!.GetPrecision()); + Assert.Null(property.GetElementType()!.GetScale()); + property = entityType.FindProperty("Strange")!; + Assert.Null(property.GetElementType()!.GetPrecision()); + Assert.Null(property.GetElementType()!.GetScale()); + property = entityType.FindProperty("Stranger")!; + Assert.Null(property.GetElementType()!.GetPrecision()); + Assert.Null(property.GetElementType()!.GetScale()); + } + + [ConditionalFact] + public virtual void Element_types_can_have_precision_and_scale() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection>("Charm").ElementType(b => b.HasPrecision(5,6)); + b.PrimitiveCollection>("Strange").ElementType(b => b.HasPrecision(12)); + b.PrimitiveCollection>("Stranger"); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + var elementType = entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!; + Assert.Null(elementType.GetPrecision()); + Assert.Null(elementType.GetScale()); + + elementType = entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!; + Assert.Null(elementType.GetPrecision()); + Assert.Null(elementType.GetScale()); + + elementType = entityType.FindProperty("Charm")!.GetElementType()!; + Assert.Equal(5, elementType.GetPrecision()); + Assert.Equal(6, elementType.GetScale()); + + elementType = entityType.FindProperty("Strange")!.GetElementType()!; + Assert.Equal(12, elementType.GetPrecision()); + Assert.Null(elementType.GetScale()); + + elementType = entityType.FindProperty("Stranger")!.GetElementType()!; + Assert.Null(elementType.GetPrecision()); + Assert.Null(elementType.GetScale()); + } + + [ConditionalFact] + public virtual void Element_types_have_default_unicode() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange"); + b.PrimitiveCollection>("Stranger"); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!.IsUnicode()); + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!.IsUnicode()); + Assert.Null(entityType.FindProperty("Charm")!.GetElementType()!.IsUnicode()); + Assert.Null(entityType.FindProperty("Strange")!.GetElementType()!.IsUnicode()); + Assert.Null(entityType.FindProperty("Stranger")!.GetElementType()!.IsUnicode()); + } + + [ConditionalFact] + public virtual void Element_types_can_have_unicode_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Down).ElementType().IsUnicode(false); + b.PrimitiveCollection>("Charm"); + b.PrimitiveCollection>("Strange").ElementType().IsUnicode(); + b.PrimitiveCollection>("Stranger").ElementType().IsUnicode(false); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty(nameof(CollectionQuarks.Up))!.GetElementType()!.IsUnicode()); + Assert.False(entityType.FindProperty(nameof(CollectionQuarks.Down))!.GetElementType()!.IsUnicode()); + Assert.Null(entityType.FindProperty("Charm")!.GetElementType()!.IsUnicode()); + Assert.True(entityType.FindProperty("Strange")!.GetElementType()!.IsUnicode()); + Assert.False(entityType.FindProperty("Stranger")!.GetElementType()!.IsUnicode()); + } + + [ConditionalFact] + public virtual void Element_types_can_have_provider_type_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down).ElementType().HasConversion(); + b.PrimitiveCollection>("Charm").ElementType().HasConversion>(); + b.PrimitiveCollection("Strange").ElementType().HasConversion((ValueConverter?)null); + b.PrimitiveCollection>("Top").ElementType().HasConversion(new CustomValueComparer()); + }); + + var entityType = modelBuilder.FinalizeModel().FindEntityType(typeof(CollectionQuarks))!; + + var up = entityType.FindProperty("Up")!.GetElementType()!; + Assert.Null(up.GetProviderClrType()); + Assert.IsType>(up.GetValueComparer()); + + var down = entityType.FindProperty("Down")!.GetElementType()!; + Assert.Same(typeof(byte[]), down.GetProviderClrType()); + Assert.IsType>(down.GetValueComparer()); + + var charm = entityType.FindProperty("Charm")!.GetElementType()!; + Assert.Same(typeof(long), charm.GetProviderClrType()); + Assert.IsType>(charm.GetValueComparer()); + + var strange = entityType.FindProperty("Strange")!.GetElementType()!; + Assert.Null(strange.GetProviderClrType()); + Assert.IsType>(strange.GetValueComparer()); + + var top = entityType.FindProperty("Top")!.GetElementType()!; + Assert.Same(typeof(string), top.GetProviderClrType()); + Assert.IsType>(top.GetValueComparer()); + } + + [ConditionalFact] + public virtual void Element_types_can_have_non_generic_value_converter_set() + { + var modelBuilder = CreateModelBuilder(); + + ValueConverter stringConverter = new StringToBytesConverter(Encoding.UTF8); + ValueConverter intConverter = new CastingConverter(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down).ElementType().HasConversion(stringConverter); + b.PrimitiveCollection("Charm").ElementType().HasConversion(intConverter, null); + b.PrimitiveCollection>("Strange").ElementType().HasConversion(stringConverter); + b.PrimitiveCollection>("Strange").ElementType().HasConversion((ValueConverter?)null); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(CollectionQuarks))!; + + Assert.Null(entityType.FindProperty("Up")!.GetElementType()!.GetValueConverter()); + + var down = entityType.FindProperty("Down")!.GetElementType()!; + Assert.Same(stringConverter, down.GetValueConverter()); + Assert.IsType>(down.GetValueComparer()); + + var charm = entityType.FindProperty("Charm")!.GetElementType()!; + Assert.Same(intConverter, charm.GetValueConverter()); + Assert.IsType>(charm.GetValueComparer()); + + Assert.Null(entityType.FindProperty("Strange")!.GetElementType()!.GetValueConverter()); + } + + [ConditionalFact] + public virtual void Element_types_can_have_custom_type_value_converter_type_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up).ElementType().HasConversion>(); + b.PrimitiveCollection(e => e.Down).ElementType().HasConversion>(); + b.PrimitiveCollection("Charm").ElementType().HasConversion, CustomValueComparer>(); + b.PrimitiveCollection>("Strange").ElementType().HasConversion>(); + b.PrimitiveCollection>("Strange").ElementType().HasConversion((ValueConverter?)null); + b.PrimitiveCollection("Top").ElementType().HasConversion(new CustomValueComparer()); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = (IReadOnlyEntityType)model.FindEntityType(typeof(CollectionQuarks))!; + + var up = entityType.FindProperty("Up")!.GetElementType()!; + Assert.Equal(typeof(int), up.GetProviderClrType()); + Assert.Null(up.GetValueConverter()); + Assert.IsType>(up.GetValueComparer()); + + var down = entityType.FindProperty("Down")!.GetElementType()!; + Assert.IsType(down.GetValueConverter()); + Assert.IsType>(down.GetValueComparer()); + + var charm = entityType.FindProperty("Charm")!.GetElementType()!; + Assert.IsType>(charm.GetValueConverter()); + Assert.IsType>(charm.GetValueComparer()); + + var strange = entityType.FindProperty("Strange")!.GetElementType()!; + Assert.Null(strange.GetValueConverter()); + Assert.IsType>(strange.GetValueComparer()); + + var top = entityType.FindProperty("Top")!.GetElementType()!; + Assert.Null(top.GetValueConverter()); + Assert.IsType>(top.GetValueComparer()); + } + + [ConditionalFact] + public virtual void Primitive_collections_can_have_value_converter_set() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + b.PrimitiveCollection(e => e.Up); + b.PrimitiveCollection(e => e.Down).ElementType().HasConversion( + new ValueConverter(v => int.Parse(v), v => v.ToString())!); + b.PrimitiveCollection>("Charm").ElementType().HasConversion( + new ValueConverter(v => v, v => (int)v), new CustomValueComparer()); + b.PrimitiveCollection("Strange").ElementType().HasConversion( + new ValueConverter(v => v, v => (float)v), new CustomValueComparer()); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + + var up = entityType.FindProperty("Up")!.GetElementType()!; + Assert.Null(up.GetProviderClrType()); + Assert.Null(up.GetValueConverter()); + Assert.IsType>(up.GetValueComparer()); + + var down = entityType.FindProperty("Down")!.GetElementType()!; + Assert.IsType>(down.GetValueConverter()); + Assert.IsType>(down.GetValueComparer()); + + var charm = entityType.FindProperty("Charm")!.GetElementType()!; + Assert.IsType>(charm.GetValueConverter()); + Assert.IsType>(charm.GetValueComparer()); + + var strange = entityType.FindProperty("Strange")!.GetElementType()!; + Assert.IsType>(strange.GetValueConverter()); + Assert.IsType>(strange.GetValueComparer()); + } + + [ConditionalFact] + public virtual void Value_converter_type_on_primitive_collection_is_checked() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity( + b => + { + Assert.Equal( + CoreStrings.ConverterPropertyMismatchElement("string", "CollectionQuarks", "Up", "int"), + Assert.Throws( + () => b.PrimitiveCollection(e => e.Up).ElementType().HasConversion( + new StringToBytesConverter(Encoding.UTF8))).Message); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(CollectionQuarks))!; + Assert.Null(entityType.FindProperty("Up")!.GetElementType()!.GetValueConverter()); + } } } diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index 8586e21d450..1773bf787f5 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Collections.ObjectModel; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -97,6 +98,8 @@ protected class Customer public IEnumerable? Orders { get; set; } + public List? Notes { get; set; } + [NotMapped] public ICollection? SomeOrders { get; set; } @@ -165,8 +168,8 @@ private class OrderProduct public int OrderId { get; set; } public int ProductId { get; set; } - public required virtual Order Order { get; set; } - public required virtual Product Product { get; set; } + public virtual required Order Order { get; set; } = null!; + public virtual required Product Product { get; set; } = null!; } protected class UniProduct @@ -296,6 +299,41 @@ public string? Down set => _forDown = value; } +#pragma warning disable 67 + public event PropertyChangingEventHandler? PropertyChanging; + public event PropertyChangedEventHandler? PropertyChanged; +#pragma warning restore 67 + } + + // INotify interfaces not really implemented; just marking the classes to test metadata construction + protected class CollectionQuarks : INotifyPropertyChanging, INotifyPropertyChanged + { + private ObservableCollection _forUp = null!; + private ObservableCollection? _forDown; +#pragma warning disable IDE0044 // Add readonly modifier +#pragma warning disable IDE0051 // Remove unused private members +#pragma warning disable CS0169 // Remove unused private fields + private ObservableCollection? _forWierd; +#pragma warning restore CS0169 // Remove unused private fields +#pragma warning restore IDE0051 // Remove unused private members +#pragma warning restore IDE0044 // Add readonly modifier + + public int Id { get; set; } + + // ReSharper disable once ConvertToAutoProperty + public ObservableCollection Up + { + get => _forUp; + set => _forUp = value; + } + + // ReSharper disable once ConvertToAutoProperty + public ObservableCollection? Down + { + get => _forDown; + set => _forDown = value; + } + #pragma warning disable 67 public event PropertyChangingEventHandler? PropertyChanging; public event PropertyChangedEventHandler? PropertyChanged; @@ -486,6 +524,7 @@ protected class Gamma { public int Id { get; set; } private int PrivateProperty { get; set; } + private List PrivateCollection { get; set; } = null!; public List? Alphas { get; set; } } @@ -739,6 +778,9 @@ public class EntityWithFields public long Id; public int CompanyId; public int TenantId; + public long[] CollectionId = null!; + public int[] CollectionCompanyId = null!; + public int[] CollectionTenantId = null!; public KeylessEntityWithFields? KeylessEntity; } @@ -857,14 +899,15 @@ protected class ComplexProperties public int Id { get; set; } public required Customer Customer { get; set; } public required DoubleProperty DoubleProperty { get; set; } - public required IndexedClass IndexedClass { get; set; } - public required Quarks Quarks { get; set; } + public required IndexedClass IndexedClass { get; set; } + public required Quarks Quarks { get; set; } + public CollectionQuarks CollectionQuarks { get; set; } = null!; [NotMapped] - public required DynamicProperty DynamicProperty { get; set; } + public required DynamicProperty DynamicProperty { get; set; } [NotMapped] - public required EntityWithFields EntityWithFields { get; set; } + public required EntityWithFields EntityWithFields { get; set; } [NotMapped] public required WrappedStringEntity WrappedStringEntity { get; set; } @@ -1308,4 +1351,25 @@ protected class OwnedOtter { public decimal Number { get; set; } } + + protected class OneDee + { + public int Id { get; set; } + + public int[]? One { get; set; } + } + + protected class TwoDee + { + public int Id { get; set; } + + public int[,]? Two { get; set; } + } + + protected class ThreeDee + { + public int Id { get; set; } + + public int[,,]? Three { get; set; } + } }