diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs index ce9dd461aaf..d01c7aedd18 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs @@ -71,6 +71,12 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) private CoreTypeMapping? FindCollectionMapping(in TypeMappingInfo mappingInfo) { var clrType = mappingInfo.ClrType!; + + if (mappingInfo.ElementTypeMapping != null) + { + return null; + } + var elementType = clrType.TryGetSequenceType(); if (elementType == null) { diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs index cff9a52809d..f5e83539fb6 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs @@ -37,7 +37,7 @@ public InMemoryTypeMappingSource(TypeMappingSourceDependencies dependencies) if (clrType.IsValueType || clrType == typeof(string) - || clrType == typeof(byte[])) + || (clrType == typeof(byte[]) && mappingInfo.ElementTypeMapping == null)) { return new InMemoryTypeMapping( clrType, jsonValueReaderWriter: jsonValueReaderWriter); diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs index 3c21d0202ed..4804af11bd0 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingInfo.cs @@ -151,7 +151,7 @@ public RelationalTypeMappingInfo( int? scale) { // Note: Empty string is allowed for store type name because SQLite - _coreTypeMappingInfo = new TypeMappingInfo(null, false, unicode, size, null, precision, scale); + _coreTypeMappingInfo = new TypeMappingInfo(null, null, false, unicode, size, null, precision, scale); StoreTypeName = storeTypeName; StoreTypeNameBase = storeTypeNameBase; IsFixedLength = null; @@ -161,6 +161,7 @@ public RelationalTypeMappingInfo( /// Creates a new instance of . /// /// The property or field for which mapping is needed. + /// The type mapping for elements, if known. /// 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 mapping, or for default. @@ -169,6 +170,7 @@ public RelationalTypeMappingInfo( /// Specifies a scale for the mapping, or for default. public RelationalTypeMappingInfo( MemberInfo member, + RelationalTypeMapping? elementTypeMapping = null, string? storeTypeName = null, string? storeTypeNameBase = null, bool? unicode = null, @@ -176,7 +178,7 @@ public RelationalTypeMappingInfo( int? precision = null, int? scale = null) { - _coreTypeMappingInfo = new TypeMappingInfo(member, unicode, size, precision, scale); + _coreTypeMappingInfo = new TypeMappingInfo(member, elementTypeMapping, unicode, size, precision, scale); StoreTypeName = storeTypeName; StoreTypeNameBase = storeTypeNameBase; @@ -212,6 +214,7 @@ public RelationalTypeMappingInfo( /// Creates a new instance of . /// /// The CLR type in the model for which mapping is needed. + /// The type mapping for elements, if known. /// The database type name. /// The provider-specific relational type name, with any facets removed. /// If , then a special mapping for a key or index may be returned. @@ -224,6 +227,7 @@ public RelationalTypeMappingInfo( /// The suggested , or for default. public RelationalTypeMappingInfo( Type? type = null, + RelationalTypeMapping? elementTypeMapping = null, string? storeTypeName = null, string? storeTypeNameBase = null, bool keyOrIndex = false, @@ -235,7 +239,7 @@ public RelationalTypeMappingInfo( int? scale = null, DbType? dbType = null) { - _coreTypeMappingInfo = new TypeMappingInfo(type, keyOrIndex, unicode, size, rowVersion, precision, scale); + _coreTypeMappingInfo = new TypeMappingInfo(type, elementTypeMapping, keyOrIndex, unicode, size, rowVersion, precision, scale); IsFixedLength = fixedLength; StoreTypeName = storeTypeName; @@ -341,6 +345,15 @@ public JsonValueReaderWriter? JsonValueReaderWriter init => _coreTypeMappingInfo = _coreTypeMappingInfo with { JsonValueReaderWriter = value }; } + /// + /// The element type of the mapping, if any. + /// + public RelationalTypeMapping? ElementTypeMapping + { + get => (RelationalTypeMapping?)_coreTypeMappingInfo.ElementTypeMapping; + init => _coreTypeMappingInfo = _coreTypeMappingInfo with { ElementTypeMapping = value }; + } + /// /// Returns a new with the given converter applied. /// diff --git a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs index 46aa60e1b2a..cc5cb6fe9f1 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMappingSource.cs @@ -85,7 +85,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) RelationalStrings.NoneRelationalTypeMappingOnARelationalTypeMappingSource); private RelationalTypeMapping? FindMappingWithConversion( - in RelationalTypeMappingInfo mappingInfo, + RelationalTypeMappingInfo mappingInfo, IReadOnlyList? principals) { Type? providerClrType = null; @@ -114,15 +114,19 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) } } - var element = principal.GetElementType(); - if (element != null) + if (elementMapping == null) { - elementMapping = FindMapping(element); + var element = principal.GetElementType(); + if (element != null) + { + elementMapping = FindMapping(element); + mappingInfo = mappingInfo with { ElementTypeMapping = (RelationalTypeMapping?)elementMapping }; + } } } } - var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter, elementMapping); + var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter); ValidateMapping(resolvedMapping, principals?[0]); @@ -132,26 +136,23 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) private RelationalTypeMapping? FindMappingWithConversion( RelationalTypeMappingInfo mappingInfo, Type? providerClrType, - ValueConverter? customConverter, - CoreTypeMapping? elementMapping) + ValueConverter? customConverter) => _explicitMappings.GetOrAdd( - (mappingInfo, providerClrType, customConverter, elementMapping), + (mappingInfo, providerClrType, customConverter, mappingInfo.ElementTypeMapping), static (k, self) => { var (mappingInfo, providerClrType, customConverter, elementMapping) = k; var sourceType = mappingInfo.ClrType; - RelationalTypeMapping? mapping = null; + var mapping = providerClrType == null + || providerClrType == mappingInfo.ClrType + ? self.FindMapping(mappingInfo) + : null; - if (elementMapping == null - || customConverter != null) + if (mapping == null) { - mapping = providerClrType == null - || providerClrType == mappingInfo.ClrType - ? self.FindMapping(mappingInfo) - : null; - - if (mapping == null) + if (elementMapping == null + || customConverter != null) { if (sourceType != null) { @@ -193,10 +194,10 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) mapping ??= self.FindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); } } - } - else if (sourceType != null) - { - mapping = self.FindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); + else if (sourceType != null) + { + mapping = self.FindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); + } } if (mapping != null @@ -312,7 +313,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) var resolvedMapping = FindMappingWithConversion( new RelationalTypeMappingInfo(elementType, storeTypeName, storeTypeNameBase, unicode, isFixedLength, size, precision, scale), - providerClrType, customConverter, null); + providerClrType, customConverter); ValidateMapping(resolvedMapping, null); @@ -357,7 +358,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) ValueConverter? customConverter = null; if (typeConfiguration == null) { - mappingInfo = new RelationalTypeMappingInfo(type); + mappingInfo = new RelationalTypeMappingInfo(type, (RelationalTypeMapping?)elementMapping); } else { @@ -379,6 +380,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) var isFixedLength = (bool?)typeConfiguration[RelationalAnnotationNames.IsFixedLength]; mappingInfo = new RelationalTypeMappingInfo( customConverter?.ProviderClrType ?? type, + (RelationalTypeMapping?)elementMapping, storeTypeName, storeTypeBaseName, keyOrIndex: false, @@ -390,7 +392,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) scale: scale); } - return FindMappingWithConversion(mappingInfo, providerClrType, customConverter, (RelationalTypeMapping?)elementMapping); + return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); } /// @@ -422,7 +424,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) attribute.TypeName, ref unicode, ref size, ref precision, ref scale); return FindMappingWithConversion( - new RelationalTypeMappingInfo(member, storeTypeName, storeTypeNameBase, unicode, size, precision, scale), null); + new RelationalTypeMappingInfo(member, null, storeTypeName, storeTypeNameBase, unicode, size, precision, scale), null); } return FindMappingWithConversion(new RelationalTypeMappingInfo(member), null); @@ -496,7 +498,7 @@ protected override CoreTypeMapping FindMapping(in TypeMappingInfo mappingInfo) return FindMappingWithConversion( new RelationalTypeMappingInfo( - type, storeTypeName, storeTypeBaseName, keyOrIndex, unicode, size, rowVersion, fixedLength, precision, scale), null); + type, null, storeTypeName, storeTypeBaseName, keyOrIndex, unicode, size, rowVersion, fixedLength, precision, scale), null); } /// diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index fea04b1d908..0a718f8f829 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -349,20 +349,23 @@ public SqlServerTypeMappingSource( return Rowversion; } - var isFixedLength = mappingInfo.IsFixedLength == true; - - var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? 900 : null); - if (size is < 0 or > 8000) + if (mappingInfo.ElementTypeMapping == null) { - size = isFixedLength ? 8000 : null; - } + var isFixedLength = mappingInfo.IsFixedLength == true; - return size == null - ? VariableLengthMaxBinary - : new SqlServerByteArrayTypeMapping( - size: size, - fixedLength: isFixedLength, - storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None); + var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? 900 : null); + if (size is < 0 or > 8000) + { + size = isFixedLength ? 8000 : null; + } + + return size == null + ? VariableLengthMaxBinary + : new SqlServerByteArrayTypeMapping( + size: size, + fixedLength: isFixedLength, + storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None); + } } } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs index f4c85a07c64..23a4aad518f 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTypeMappingSource.cs @@ -136,6 +136,11 @@ public static bool IsSpatialiteType(string columnType) private RelationalTypeMapping? FindRawMapping(RelationalTypeMappingInfo mappingInfo) { var clrType = mappingInfo.ClrType; + if (clrType == typeof(byte[]) && mappingInfo.ElementTypeMapping != null) + { + return null; + } + if (clrType != null && _clrTypeMappings.TryGetValue(clrType, out var mapping)) { diff --git a/src/EFCore/Storage/TypeMappingInfo.cs b/src/EFCore/Storage/TypeMappingInfo.cs index 7407af2d4b5..5cec9557220 100644 --- a/src/EFCore/Storage/TypeMappingInfo.cs +++ b/src/EFCore/Storage/TypeMappingInfo.cs @@ -181,6 +181,7 @@ public TypeMappingInfo( var mappingHints = customConverter?.MappingHints; var property = principals[0]; + ElementTypeMapping = property.GetElementType()?.FindTypeMapping(); IsKeyOrIndex = property.IsKey() || property.IsForeignKey() || property.IsIndex(); Size = fallbackSize ?? mappingHints?.Size; IsUnicode = fallbackUnicode ?? mappingHints?.IsUnicode; @@ -195,17 +196,19 @@ public TypeMappingInfo( /// Creates a new instance of . /// /// The property or field for which mapping is needed. + /// The type mapping for elements, if known. /// Specifies Unicode or ANSI mapping, or for default. /// Specifies a size for the mapping, or for default. /// Specifies a precision for the mapping, or for default. /// Specifies a scale for the mapping, or for default. public TypeMappingInfo( MemberInfo member, + CoreTypeMapping? elementTypeMapping = null, bool? unicode = null, int? size = null, int? precision = null, int? scale = null) - : this(member.GetMemberType()) + : this(member.GetMemberType(), elementTypeMapping) { IsUnicode = unicode; Size = size; @@ -217,6 +220,7 @@ public TypeMappingInfo( /// Creates a new instance of . /// /// The CLR type in the model for which mapping is needed. + /// The type mapping for elements, if known. /// If , then a special mapping for a key or index may be returned. /// Specifies Unicode or ANSI mapping, or for default. /// Specifies a size for the mapping, or for default. @@ -225,6 +229,7 @@ public TypeMappingInfo( /// Specifies a scale for the mapping, or for default. public TypeMappingInfo( Type? type = null, + CoreTypeMapping? elementTypeMapping = null, bool keyOrIndex = false, bool? unicode = null, int? size = null, @@ -233,6 +238,7 @@ public TypeMappingInfo( int? scale = null) { ClrType = type?.UnwrapNullableType(); + ElementTypeMapping = elementTypeMapping; IsKeyOrIndex = keyOrIndex; Size = size; @@ -272,8 +278,14 @@ public TypeMappingInfo( ClrType = converter.ProviderClrType.UnwrapNullableType(); JsonValueReaderWriter = source.JsonValueReaderWriter; + ElementTypeMapping = source.ElementTypeMapping; } + /// + /// The element type mapping of the mapping, if any. + /// + public CoreTypeMapping? ElementTypeMapping { get; init; } + /// /// Returns a new with the given converter applied. /// diff --git a/src/EFCore/Storage/TypeMappingSource.cs b/src/EFCore/Storage/TypeMappingSource.cs index 407f991bc5c..855ba977af8 100644 --- a/src/EFCore/Storage/TypeMappingSource.cs +++ b/src/EFCore/Storage/TypeMappingSource.cs @@ -41,7 +41,7 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) } private CoreTypeMapping? FindMappingWithConversion( - in TypeMappingInfo mappingInfo, + TypeMappingInfo mappingInfo, IReadOnlyList? principals) { Type? providerClrType = null; @@ -70,15 +70,19 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) } } - var element = principal.GetElementType(); - if (element != null) + if (elementMapping == null) { - elementMapping = FindMapping(element); + var element = principal.GetElementType(); + if (element != null) + { + elementMapping = FindMapping(element); + mappingInfo = mappingInfo with { ElementTypeMapping = elementMapping }; + } } } } - var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter, elementMapping); + var resolvedMapping = FindMappingWithConversion(mappingInfo, providerClrType, customConverter); ValidateMapping(resolvedMapping, principals?[0]); @@ -88,26 +92,23 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) private CoreTypeMapping? FindMappingWithConversion( TypeMappingInfo mappingInfo, Type? providerClrType, - ValueConverter? customConverter, - CoreTypeMapping? elementMapping) + ValueConverter? customConverter) => _explicitMappings.GetOrAdd( - (mappingInfo, providerClrType, customConverter, elementMapping), + (mappingInfo, providerClrType, customConverter, mappingInfo.ElementTypeMapping), static (k, self) => { var (mappingInfo, providerClrType, customConverter, elementMapping) = k; var sourceType = mappingInfo.ClrType; - CoreTypeMapping? mapping = null; + var mapping = providerClrType == null + || providerClrType == mappingInfo.ClrType + ? self.FindMapping(mappingInfo) + : null; - if (elementMapping == null - || customConverter != null) + if (mapping == null) { - mapping = providerClrType == null - || providerClrType == mappingInfo.ClrType - ? self.FindMapping(mappingInfo) - : null; - - if (mapping == null) + if (elementMapping == null + || customConverter != null) { if (sourceType != null) { @@ -149,10 +150,10 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) mapping ??= self.FindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); } } - } - else if (sourceType != null) - { - mapping = self.FindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); + else if (sourceType != null) + { + mapping = self.FindCollectionMapping(mappingInfo, sourceType, providerClrType, elementMapping); + } } if (mapping != null @@ -232,7 +233,7 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) var resolvedMapping = FindMappingWithConversion( new TypeMappingInfo( elementType, elementType.IsUnicode(), elementType.GetMaxLength(), elementType.GetPrecision(), elementType.GetScale()), - providerClrType, customConverter, null); + providerClrType, customConverter); ValidateMapping(resolvedMapping, null); @@ -277,7 +278,7 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) ValueConverter? customConverter = null; if (typeConfiguration == null) { - mappingInfo = new TypeMappingInfo(type); + mappingInfo = new TypeMappingInfo(type, elementMapping); } else { @@ -285,13 +286,14 @@ protected TypeMappingSource(TypeMappingSourceDependencies dependencies) customConverter = typeConfiguration.GetValueConverter(); mappingInfo = new TypeMappingInfo( customConverter?.ProviderClrType ?? type, + elementMapping, unicode: typeConfiguration.IsUnicode(), size: typeConfiguration.GetMaxLength(), precision: typeConfiguration.GetPrecision(), scale: typeConfiguration.GetScale()); } - return FindMappingWithConversion(mappingInfo, providerClrType, customConverter, elementMapping); + return FindMappingWithConversion(mappingInfo, providerClrType, customConverter); } /// diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index b2b9d76b638..c691be7f9f7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -668,27 +668,31 @@ await Can_add_update_delete_with_collection>( "1" }); - // See #25343 - await Can_add_update_delete_with_collection( - new List - { - EntityType.Base, - EntityType.Derived, - EntityType.Derived - }, - c => + await Assert.ThrowsAsync( // #31616 + async () => { - c.Collection.Clear(); - c.Collection.Add(EntityType.Base); - }, - new List { EntityType.Base }, - modelBuilder => modelBuilder.Entity>>( - c => - c.Property(s => s.Collection) - .HasConversion( - m => m.Select(v => (int)v).ToList(), p => p.Select(v => (EntityType)v).ToList(), - new ListComparer>( - ValueComparer.CreateDefault(typeof(EntityType), false), readOnly: false)))); + // See #25343 + await Can_add_update_delete_with_collection( + new List + { + EntityType.Base, + EntityType.Derived, + EntityType.Derived + }, + c => + { + c.Collection.Clear(); + c.Collection.Add(EntityType.Base); + }, + new List { EntityType.Base }, + modelBuilder => modelBuilder.Entity>>( + c => + c.Property(s => s.Collection) + .HasConversion( + m => m.Select(v => (int)v).ToList(), p => p.Select(v => (EntityType)v).ToList(), + new ListComparer>( + ValueComparer.CreateDefault(typeof(EntityType), false), readOnly: false)))); + }); await Can_add_update_delete_with_collection( new[] { 1f, 2 }, diff --git a/test/EFCore.SqlServer.FunctionalTests/JsonTypesCustomMappingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/JsonTypesCustomMappingSqlServerTest.cs new file mode 100644 index 00000000000..4992c1549d7 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/JsonTypesCustomMappingSqlServerTest.cs @@ -0,0 +1,148 @@ +// 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.SqlServer.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class JsonTypesCustomMappingSqlServerTest : JsonTypesSqlServerTestBase +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.ReplaceService()); + + private class TestSqlServerTypeMappingSource( + TypeMappingSourceDependencies dependencies, + RelationalTypeMappingSourceDependencies relationalDependencies) + : SqlServerTypeMappingSource(dependencies, relationalDependencies) + { + protected override RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) + { + var mapping = base.FindMapping(in mappingInfo); + + if ((mapping == null + || (mappingInfo.CoreTypeMappingInfo.ElementTypeMapping != null + && mapping.ElementTypeMapping == null)) + && mappingInfo.ClrType != null + && mappingInfo.ClrType != typeof(string)) + { + var elementClrType = TryGetElementType(mappingInfo.ClrType, typeof(IEnumerable<>))!; + + mapping = CustomFindCollectionMapping( + mappingInfo, mappingInfo.ClrType, + null, + mappingInfo.CoreTypeMappingInfo.ElementTypeMapping ?? FindMapping(elementClrType)); + } + + return mapping; + } + + protected override RelationalTypeMapping? FindCollectionMapping( + RelationalTypeMappingInfo info, + Type modelType, + Type? providerType, + CoreTypeMapping? elementMapping) + => null; + + private RelationalTypeMapping? CustomFindCollectionMapping( + RelationalTypeMappingInfo info, + Type modelType, + Type? providerType, + CoreTypeMapping? elementMapping) + { + if (TryFindJsonCollectionMapping( + info.CoreTypeMappingInfo, modelType, providerType, ref elementMapping, out var collectionReaderWriter)) + { + var elementType = TryGetElementType(modelType, typeof(IEnumerable<>))!; + + return (RelationalTypeMapping)FindMapping( + info.WithConverter( + // Note that the converter info is only used temporarily here and never creates an instance. + new ValueConverterInfo(modelType, typeof(string), _ => null!)))! + .Clone( + (ValueConverter)Activator.CreateInstance( + typeof(CollectionToJsonStringConverter<>).MakeGenericType(elementType), collectionReaderWriter!)!, + (ValueComparer?)Activator.CreateInstance( + IsNullableValueType(elementType) + ? typeof(NullableValueTypeListComparer<>).MakeGenericType(UnwrapNullableType(elementType)) + : typeof(ListComparer<>).MakeGenericType(elementMapping!.Comparer.Type), + elementMapping!.Comparer), + elementMapping, + collectionReaderWriter); + } + + return null; + } + + private static Type? TryGetElementType(Type type, Type interfaceOrBaseType) + { + if (type.IsGenericTypeDefinition) + { + return null; + } + + var types = GetGenericTypeImplementations(type, interfaceOrBaseType); + + Type? singleImplementation = null; + foreach (var implementation in types) + { + if (singleImplementation == null) + { + singleImplementation = implementation; + } + else + { + singleImplementation = null; + break; + } + } + + return singleImplementation?.GenericTypeArguments.FirstOrDefault(); + } + + private static IEnumerable GetGenericTypeImplementations(Type type, Type interfaceOrBaseType) + { + var typeInfo = type.GetTypeInfo(); + if (!typeInfo.IsGenericTypeDefinition) + { + var baseTypes = interfaceOrBaseType.GetTypeInfo().IsInterface + ? typeInfo.ImplementedInterfaces + : GetBaseTypes(type); + foreach (var baseType in baseTypes) + { + if (baseType.IsGenericType + && baseType.GetGenericTypeDefinition() == interfaceOrBaseType) + { + yield return baseType; + } + } + + if (type.IsGenericType + && type.GetGenericTypeDefinition() == interfaceOrBaseType) + { + yield return type; + } + } + } + + private static IEnumerable GetBaseTypes(Type type) + { + var currentType = type.BaseType; + + while (currentType != null) + { + yield return currentType; + + currentType = currentType.BaseType; + } + } + + private static Type UnwrapNullableType(Type type) + => Nullable.GetUnderlyingType(type) ?? type; + + private static bool IsNullableValueType(Type type) + => type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs index 6966ab238d1..70f66a1c679 100644 --- a/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs @@ -5,61 +5,6 @@ namespace Microsoft.EntityFrameworkCore; -public class JsonTypesSqlServerTest : JsonTypesRelationalTestBase +public class JsonTypesSqlServerTest : JsonTypesSqlServerTestBase { - public override void Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json) - { - if (value == EnumU64.Max) - { - json = """{"Prop":-1}"""; // Because ulong is converted to long on SQL Server - } - - base.Can_read_write_ulong_enum_JSON_values(value, json); - } - - public override void Can_read_write_nullable_ulong_enum_JSON_values(object? value, string json) - { - if (Equals(value, ulong.MaxValue)) - { - json = """{"Prop":-1}"""; // Because ulong is converted to long on SQL Server - } - - base.Can_read_write_nullable_ulong_enum_JSON_values(value, json); - } - - public override void Can_read_write_collection_of_ulong_enum_JSON_values() - => Can_read_and_write_JSON_value>(nameof(EnumU64CollectionType.EnumU64), - new List - { - EnumU64.Min, - EnumU64.Max, - EnumU64.Default, - EnumU64.One, - (EnumU64)8 - }, - """{"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>(nameof(NullableEnumU64CollectionType.EnumU64), - new List - { - EnumU64.Min, - null, - EnumU64.Max, - EnumU64.Default, - EnumU64.One, - (EnumU64?)8 - }, - """{"Prop":[0,null,-1,0,1,8]}""", // Because ulong is converted to long on SQL Server - mappedCollection: true); - - 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 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 void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => base.OnConfiguring(optionsBuilder.UseSqlServer(b => b.UseNetTopologySuite())); } diff --git a/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTestBase.cs new file mode 100644 index 00000000000..737c7520e27 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTestBase.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Microsoft.EntityFrameworkCore; + +public abstract class JsonTypesSqlServerTestBase : JsonTypesRelationalTestBase +{ + public override void Can_read_write_ulong_enum_JSON_values(EnumU64 value, string json) + { + if (value == EnumU64.Max) + { + json = """{"Prop":-1}"""; // Because ulong is converted to long on SQL Server + } + + base.Can_read_write_ulong_enum_JSON_values(value, json); + } + + public override void Can_read_write_nullable_ulong_enum_JSON_values(object? value, string json) + { + if (Equals(value, ulong.MaxValue)) + { + json = """{"Prop":-1}"""; // Because ulong is converted to long on SQL Server + } + + base.Can_read_write_nullable_ulong_enum_JSON_values(value, json); + } + + public override void Can_read_write_collection_of_ulong_enum_JSON_values() + => Can_read_and_write_JSON_value>(nameof(EnumU64CollectionType.EnumU64), + new List + { + EnumU64.Min, + EnumU64.Max, + EnumU64.Default, + EnumU64.One, + (EnumU64)8 + }, + """{"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>(nameof(NullableEnumU64CollectionType.EnumU64), + new List + { + EnumU64.Min, + null, + EnumU64.Max, + EnumU64.Default, + EnumU64.One, + (EnumU64?)8 + }, + """{"Prop":[0,null,-1,0,1,8]}""", // Because ulong is converted to long on SQL Server + mappedCollection: true); + + 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 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 void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring(optionsBuilder.UseSqlServer(b => b.UseNetTopologySuite())); +} diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs index d919e81acfa..39136084ef7 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs @@ -476,12 +476,16 @@ public void Does_IndexAttribute_column_SQL_Server_string_mapping(bool? unicode, public void Does_IndexAttribute_column_SQL_Server_primitive_collection_mapping(bool? unicode, bool? fixedLength) { var entityType = CreateEntityType(); - var property = entityType.FindProperty("Ints"); + var property = entityType.FindProperty("Ints")!; property.SetIsUnicode(unicode); property.SetIsFixedLength(fixedLength); - entityType.Model.FinalizeModel(); - var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); + var model = entityType.Model.FinalizeModel(); + var typeMappingSource = CreateRelationalTypeMappingSource(); + model.ModelDependencies = new RuntimeModelDependencies(typeMappingSource, null!, null!); + + var runtimeProperty = model.FindEntityType(typeof(MyTypeWithIndexAttributeOnCollection))!.FindProperty("Ints")!; + var typeMapping = typeMappingSource.GetMapping(runtimeProperty); Assert.Equal(DbType.String, typeMapping.DbType); Assert.Equal("nvarchar(450)", typeMapping.StoreType); @@ -861,12 +865,16 @@ public void Does_IndexAttribute_column_SQL_Server_string_mapping_ansi(bool? fixe public void Does_IndexAttribute_column_SQL_Server_primitive_collection_mapping_ansi(bool? fixedLength) { var entityType = CreateEntityType(); - var property = entityType.FindProperty("Ints"); + var property = entityType.FindProperty("Ints")!; property.SetIsUnicode(false); property.SetIsFixedLength(fixedLength); - entityType.Model.FinalizeModel(); - var typeMapping = CreateRelationalTypeMappingSource().GetMapping((IProperty)property); + var model = entityType.Model.FinalizeModel(); + var typeMappingSource = CreateRelationalTypeMappingSource(); + model.ModelDependencies = new RuntimeModelDependencies(typeMappingSource, null!, null!); + + var runtimeProperty = model.FindEntityType(typeof(MyTypeWithIndexAttributeOnCollection))!.FindProperty("Ints")!; + var typeMapping = typeMappingSource.GetMapping(runtimeProperty); Assert.Equal(DbType.AnsiString, typeMapping.DbType); Assert.Equal("varchar(900)", typeMapping.StoreType); diff --git a/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs index 59186dd2a28..a5b749768c7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs @@ -15,20 +15,14 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => base.OnConfiguring(optionsBuilder.UseSqlite(b => b.UseNetTopologySuite())); public override void Can_read_write_binary_JSON_values(string value, string json) - { - // Cannot override since the base test contains [InlineData] attributes which still apply, and which contain data we need - // to override. See Can_read_write_binary_JSON_values_sqlite instead. - } - - [ConditionalTheory] - [InlineData("0,0,0,1", """{"Prop":"00000001"}""")] - [InlineData("255,255,255,255", """{"Prop":"FFFFFFFF"}""")] - [InlineData("", """{"Prop":""}""")] - [InlineData("1,2,3,4", """{"Prop":"01020304"}""")] - public virtual void Can_read_write_binary_JSON_values_sqlite(string value, string json) - => Can_read_and_write_JSON_value( - nameof(BytesType.Bytes), - value == "" ? Array.Empty() : value.Split(',').Select(e => byte.Parse(e)).ToArray(), json); + => base.Can_read_write_binary_JSON_values(value, value switch + { + "" => json, + "0,0,0,1" => """{"Prop":"00000001"}""", + "1,2,3,4" => """{"Prop":"01020304"}""", + "255,255,255,255" => """{"Prop":"FFFFFFFF"}""", + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }); [ConditionalFact] public override void Can_read_write_collection_of_decimal_JSON_values()