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()