diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs index 8d89854cb4e..7eadf9f059d 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMapping.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Storage.Json; + namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; /// @@ -20,13 +22,15 @@ public class CosmosTypeMapping : CoreTypeMapping public CosmosTypeMapping( Type clrType, ValueComparer? comparer = null, - ValueComparer? keyComparer = null) + ValueComparer? keyComparer = null, + JsonValueReaderWriter? jsonValueReaderWriter = null) : base( new CoreTypeMappingParameters( clrType, converter: null, comparer, - keyComparer)) + keyComparer, + jsonValueReaderWriter: jsonValueReaderWriter)) { } diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs index f849b45c42d..f555011281c 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs @@ -26,7 +26,13 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) : base(dependencies) { _clrTypeMappings - = new Dictionary { { typeof(JObject), new CosmosTypeMapping(typeof(JObject)) } }; + = new Dictionary + { + { + typeof(JObject), new CosmosTypeMapping( + typeof(JObject), jsonValueReaderWriter: dependencies.JsonValueReaderWriterSource.FindReaderWriter(typeof(JObject))) + } + }; } /// @@ -47,7 +53,7 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) ?? base.FindMapping(mappingInfo)); } - private static CoreTypeMapping? FindPrimitiveMapping(in TypeMappingInfo mappingInfo) + private CoreTypeMapping? FindPrimitiveMapping(in TypeMappingInfo mappingInfo) { var clrType = mappingInfo.ClrType!; if ((clrType.IsValueType @@ -55,13 +61,14 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) && !clrType.IsEnum) || clrType == typeof(string)) { - return new CosmosTypeMapping(clrType); + return new CosmosTypeMapping( + clrType, jsonValueReaderWriter: Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType)); } return null; } - private static CoreTypeMapping? FindCollectionMapping(in TypeMappingInfo mappingInfo) + private CoreTypeMapping? FindCollectionMapping(in TypeMappingInfo mappingInfo) { var clrType = mappingInfo.ClrType!; var elementType = clrType.TryGetSequenceType(); @@ -70,6 +77,8 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) return null; } + var jsonValueReaderWriter = Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType); + if (clrType.IsArray) { var elementMappingInfo = new TypeMappingInfo(elementType); @@ -77,7 +86,8 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) ?? FindCollectionMapping(elementMappingInfo); return elementMapping == null ? null - : new CosmosTypeMapping(clrType, CreateArrayComparer(elementMapping, elementType)); + : new CosmosTypeMapping( + clrType, CreateArrayComparer(elementMapping, elementType), jsonValueReaderWriter: jsonValueReaderWriter); } if (clrType.IsGenericType @@ -93,7 +103,8 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) ?? FindCollectionMapping(elementMappingInfo); return elementMapping == null ? null - : new CosmosTypeMapping(clrType, CreateListComparer(elementMapping, elementType, clrType)); + : new CosmosTypeMapping( + clrType, CreateListComparer(elementMapping, elementType, clrType), jsonValueReaderWriter: jsonValueReaderWriter); } if (genericTypeDefinition == typeof(Dictionary<,>) @@ -112,7 +123,9 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies) ?? FindCollectionMapping(elementMappingInfo); return elementMapping == null ? null - : new CosmosTypeMapping(clrType, CreateStringDictionaryComparer(elementMapping, elementType, clrType)); + : new CosmosTypeMapping( + clrType, CreateStringDictionaryComparer(elementMapping, elementType, clrType), + jsonValueReaderWriter: jsonValueReaderWriter); } } diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 3c4ccfbd94a..1f9f21fe697 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -834,6 +834,31 @@ private void Create( .Append("()"); } + var jsonValueReaderWriterType = (Type?)property[CoreAnnotationNames.JsonValueReaderWriterType]; + if (jsonValueReaderWriterType != null) + { + AddNamespace(jsonValueReaderWriterType, parameters.Namespaces); + + var instanceProperty = jsonValueReaderWriterType.GetAnyProperty("Instance"); + if (instanceProperty != null + && instanceProperty.IsStatic() + && instanceProperty.GetMethod?.IsPublic == true + && jsonValueReaderWriterType.IsAssignableFrom(instanceProperty.PropertyType)) + { + mainBuilder.AppendLine(",") + .Append("jsonValueReaderWriter: ") + .Append(_code.Reference(jsonValueReaderWriterType)) + .Append(".Instance"); + } + else + { + mainBuilder.AppendLine(",") + .Append("jsonValueReaderWriter: new ") + .Append(_code.Reference(jsonValueReaderWriterType)) + .Append("()"); + } + } + var sentinel = property.Sentinel; if (sentinel != null) { @@ -900,8 +925,9 @@ private void Create( } return i == ForeignKey.LongestFkChainAllowedLength - ? throw new InvalidOperationException(CoreStrings.RelationshipCycle( - property.DeclaringEntityType.DisplayName(), property.Name, "ValueConverterType")) + ? throw new InvalidOperationException( + CoreStrings.RelationshipCycle( + property.DeclaringEntityType.DisplayName(), property.Name, "ValueConverterType")) : null; } @@ -1474,18 +1500,13 @@ private static void CreateAnnotations( { process( annotatable, - parameters with - { - Annotations = annotatable.GetAnnotations().ToDictionary(a => a.Name, a => a.Value), - IsRuntime = false - }); + parameters with { Annotations = annotatable.GetAnnotations().ToDictionary(a => a.Name, a => a.Value), IsRuntime = false }); process( annotatable, parameters with { - Annotations = annotatable.GetRuntimeAnnotations().ToDictionary(a => a.Name, a => a.Value), - IsRuntime = true + Annotations = annotatable.GetRuntimeAnnotations().ToDictionary(a => a.Name, a => a.Value), IsRuntime = true }); } diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMapping.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMapping.cs index c4fc21b2160..1ddf5222a25 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMapping.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMapping.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Storage.Json; + namespace Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; /// @@ -20,13 +22,15 @@ public class InMemoryTypeMapping : CoreTypeMapping public InMemoryTypeMapping( Type clrType, ValueComparer? comparer = null, - ValueComparer? keyComparer = null) + ValueComparer? keyComparer = null, + JsonValueReaderWriter? jsonValueReaderWriter = null) : base( new CoreTypeMappingParameters( clrType, converter: null, comparer, - keyComparer)) + keyComparer, + jsonValueReaderWriter: jsonValueReaderWriter)) { } diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs index b7b2a014637..cff9a52809d 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTypeMappingSource.cs @@ -33,11 +33,14 @@ public InMemoryTypeMappingSource(TypeMappingSourceDependencies dependencies) var clrType = mappingInfo.ClrType; Check.DebugAssert(clrType != null, "ClrType is null"); + var jsonValueReaderWriter = Dependencies.JsonValueReaderWriterSource.FindReaderWriter(clrType); + if (clrType.IsValueType || clrType == typeof(string) || clrType == typeof(byte[])) { - return new InMemoryTypeMapping(clrType); + return new InMemoryTypeMapping( + clrType, jsonValueReaderWriter: jsonValueReaderWriter); } if (clrType.FullName == "NetTopologySuite.Geometries.Geometry" @@ -48,7 +51,8 @@ public InMemoryTypeMappingSource(TypeMappingSourceDependencies dependencies) return new InMemoryTypeMapping( clrType, comparer, - comparer); + comparer, + jsonValueReaderWriter); } return base.FindMapping(mappingInfo); diff --git a/src/EFCore.Relational/Storage/BoolTypeMapping.cs b/src/EFCore.Relational/Storage/BoolTypeMapping.cs index 9446e6ee780..75b359affd2 100644 --- a/src/EFCore.Relational/Storage/BoolTypeMapping.cs +++ b/src/EFCore.Relational/Storage/BoolTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class BoolTypeMapping : RelationalTypeMapping public BoolTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Boolean) - : base(storeType, typeof(bool), dbType) + : base(storeType, typeof(bool), dbType, jsonValueReaderWriter: JsonBoolReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/ByteArrayTypeMapping.cs b/src/EFCore.Relational/Storage/ByteArrayTypeMapping.cs index c83eb5093c8..022e6620ffb 100644 --- a/src/EFCore.Relational/Storage/ByteArrayTypeMapping.cs +++ b/src/EFCore.Relational/Storage/ByteArrayTypeMapping.cs @@ -4,6 +4,7 @@ using System.Data; using System.Globalization; using System.Text; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -35,7 +36,8 @@ public ByteArrayTypeMapping( : base( new RelationalTypeMappingParameters( new CoreTypeMappingParameters( - typeof(byte[])), storeType, StoreTypePostfix.None, dbType, unicode: false, size)) + typeof(byte[]), jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance), storeType, StoreTypePostfix.None, dbType, + unicode: false, size)) { } diff --git a/src/EFCore.Relational/Storage/ByteTypeMapping.cs b/src/EFCore.Relational/Storage/ByteTypeMapping.cs index 8dd24ace7f9..6a8a4ef9cc4 100644 --- a/src/EFCore.Relational/Storage/ByteTypeMapping.cs +++ b/src/EFCore.Relational/Storage/ByteTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class ByteTypeMapping : RelationalTypeMapping public ByteTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Byte) - : base(storeType, typeof(byte), dbType) + : base(storeType, typeof(byte), dbType, jsonValueReaderWriter: JsonByteReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/CharTypeMapping.cs b/src/EFCore.Relational/Storage/CharTypeMapping.cs index e57e04b18fa..ec64f623121 100644 --- a/src/EFCore.Relational/Storage/CharTypeMapping.cs +++ b/src/EFCore.Relational/Storage/CharTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class CharTypeMapping : RelationalTypeMapping public CharTypeMapping( string storeType, DbType? dbType = System.Data.DbType.String) - : base(storeType, typeof(char), dbType) + : base(storeType, typeof(char), dbType, jsonValueReaderWriter: JsonCharReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs b/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs index 475e755094c..007b7d93fd0 100644 --- a/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs +++ b/src/EFCore.Relational/Storage/DateOnlyTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -30,7 +31,7 @@ public class DateOnlyTypeMapping : RelationalTypeMapping public DateOnlyTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Date) - : base(storeType, typeof(DateOnly), dbType) + : base(storeType, typeof(DateOnly), dbType, jsonValueReaderWriter: JsonDateOnlyReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/DateTimeOffsetTypeMapping.cs b/src/EFCore.Relational/Storage/DateTimeOffsetTypeMapping.cs index 3202cc3f201..2f1a357398a 100644 --- a/src/EFCore.Relational/Storage/DateTimeOffsetTypeMapping.cs +++ b/src/EFCore.Relational/Storage/DateTimeOffsetTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -30,7 +31,7 @@ public class DateTimeOffsetTypeMapping : RelationalTypeMapping public DateTimeOffsetTypeMapping( string storeType, DbType? dbType = System.Data.DbType.DateTimeOffset) - : base(storeType, typeof(DateTimeOffset), dbType) + : base(storeType, typeof(DateTimeOffset), dbType, jsonValueReaderWriter: JsonDateTimeOffsetReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/DateTimeTypeMapping.cs b/src/EFCore.Relational/Storage/DateTimeTypeMapping.cs index 66ba3012470..be52f8b1e33 100644 --- a/src/EFCore.Relational/Storage/DateTimeTypeMapping.cs +++ b/src/EFCore.Relational/Storage/DateTimeTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -30,7 +31,7 @@ public class DateTimeTypeMapping : RelationalTypeMapping public DateTimeTypeMapping( string storeType, DbType? dbType = System.Data.DbType.DateTime) - : base(storeType, typeof(DateTime), dbType) + : base(storeType, typeof(DateTime), dbType, jsonValueReaderWriter: JsonDateTimeReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/DecimalTypeMapping.cs b/src/EFCore.Relational/Storage/DecimalTypeMapping.cs index f10e531a11a..d4279754135 100644 --- a/src/EFCore.Relational/Storage/DecimalTypeMapping.cs +++ b/src/EFCore.Relational/Storage/DecimalTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -34,7 +35,8 @@ public DecimalTypeMapping( DbType? dbType = System.Data.DbType.Decimal, int? precision = null, int? scale = null) - : base(storeType, typeof(decimal), dbType, precision: precision, scale: scale) + : base( + storeType, typeof(decimal), dbType, precision: precision, scale: scale, jsonValueReaderWriter: JsonDecimalReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/DoubleTypeMapping.cs b/src/EFCore.Relational/Storage/DoubleTypeMapping.cs index d787b6b3367..837a7dfa225 100644 --- a/src/EFCore.Relational/Storage/DoubleTypeMapping.cs +++ b/src/EFCore.Relational/Storage/DoubleTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data; using System.Globalization; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -29,7 +30,7 @@ public class DoubleTypeMapping : RelationalTypeMapping public DoubleTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Double) - : base(storeType, typeof(double), dbType) + : base(storeType, typeof(double), dbType, jsonValueReaderWriter: JsonDoubleReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/FloatTypeMapping.cs b/src/EFCore.Relational/Storage/FloatTypeMapping.cs index 496f4ee4d7e..7b6a73255bf 100644 --- a/src/EFCore.Relational/Storage/FloatTypeMapping.cs +++ b/src/EFCore.Relational/Storage/FloatTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data; using System.Globalization; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -29,7 +30,7 @@ public class FloatTypeMapping : RelationalTypeMapping public FloatTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Single) - : base(storeType, typeof(float), dbType) + : base(storeType, typeof(float), dbType, jsonValueReaderWriter: JsonFloatReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/GuidTypeMapping.cs b/src/EFCore.Relational/Storage/GuidTypeMapping.cs index a3b2bb2e16d..e4deba15f14 100644 --- a/src/EFCore.Relational/Storage/GuidTypeMapping.cs +++ b/src/EFCore.Relational/Storage/GuidTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class GuidTypeMapping : RelationalTypeMapping public GuidTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Guid) - : base(storeType, typeof(Guid), dbType) + : base(storeType, typeof(Guid), dbType, jsonValueReaderWriter: JsonGuidReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/IntTypeMapping.cs b/src/EFCore.Relational/Storage/IntTypeMapping.cs index 3744e673446..3a2eeab3697 100644 --- a/src/EFCore.Relational/Storage/IntTypeMapping.cs +++ b/src/EFCore.Relational/Storage/IntTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class IntTypeMapping : RelationalTypeMapping public IntTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Int32) - : base(storeType, typeof(int), dbType) + : base(storeType, typeof(int), dbType, jsonValueReaderWriter: JsonInt32ReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/LongTypeMapping.cs b/src/EFCore.Relational/Storage/LongTypeMapping.cs index e0b03c8cf39..6730c4a4ad1 100644 --- a/src/EFCore.Relational/Storage/LongTypeMapping.cs +++ b/src/EFCore.Relational/Storage/LongTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class LongTypeMapping : RelationalTypeMapping public LongTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Int64) - : base(storeType, typeof(long), dbType) + : base(storeType, typeof(long), dbType, jsonValueReaderWriter: JsonInt64ReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs b/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs index 8d5082b2ccb..7e3aa4f1356 100644 --- a/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs +++ b/src/EFCore.Relational/Storage/RelationalGeometryTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -20,11 +21,13 @@ public abstract class RelationalGeometryTypeMapping : Rela /// Creates a new instance of the class. /// /// The converter to use when converting to and from database types. + /// Handles reading and writing JSON values for instances of the mapped type. /// The store type name. protected RelationalGeometryTypeMapping( ValueConverter? converter, + JsonValueReaderWriter? jsonValueReaderWriter, string storeType) - : base(CreateRelationalTypeMappingParameters(storeType)) + : base(CreateRelationalTypeMappingParameters(storeType, jsonValueReaderWriter)) { SpatialConverter = converter; } @@ -54,7 +57,9 @@ parameters.CoreParameters with ? (ValueComparer)Activator.CreateInstance(typeof(GeometryValueComparer<>).MakeGenericType(providerType))! : null; - private static RelationalTypeMappingParameters CreateRelationalTypeMappingParameters(string storeType) + private static RelationalTypeMappingParameters CreateRelationalTypeMappingParameters( + string storeType, + JsonValueReaderWriter? jsonValueReaderWriter) { var comparer = new GeometryValueComparer(); @@ -64,7 +69,8 @@ private static RelationalTypeMappingParameters CreateRelationalTypeMappingParame null, comparer, comparer, - CreateProviderValueComparer(typeof(TGeometry))), + CreateProviderValueComparer(typeof(TGeometry)), + jsonValueReaderWriter: jsonValueReaderWriter), storeType); } diff --git a/src/EFCore.Relational/Storage/RelationalTypeMapping.cs b/src/EFCore.Relational/Storage/RelationalTypeMapping.cs index 1ea795e72ea..484428e7be3 100644 --- a/src/EFCore.Relational/Storage/RelationalTypeMapping.cs +++ b/src/EFCore.Relational/Storage/RelationalTypeMapping.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using System.Data; using System.Globalization; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -270,7 +271,7 @@ private static MethodInfo GetDataReaderMethod(string name) private sealed class NullTypeMapping : RelationalTypeMapping { public NullTypeMapping(string storeType) - : base(storeType, typeof(object)) + : base(storeType, typeof(object), jsonValueReaderWriter: JsonNullReaderWriter.Instance) { } @@ -316,6 +317,7 @@ static string GetBaseName(string storeType) /// A value indicating whether the type has fixed length data or not. /// The precision of data the property is configured to store, or null if no precision is configured. /// The scale of data the property is configured to store, or null if no scale is configured. + /// Handles reading and writing JSON values for instances of the mapped type. protected RelationalTypeMapping( string storeType, Type clrType, @@ -324,10 +326,12 @@ protected RelationalTypeMapping( int? size = null, bool fixedLength = false, int? precision = null, - int? scale = null) + int? scale = null, + JsonValueReaderWriter? jsonValueReaderWriter = null) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(clrType), storeType, StoreTypePostfix.None, dbType, unicode, size, fixedLength, precision, + new CoreTypeMappingParameters(clrType, jsonValueReaderWriter: jsonValueReaderWriter), storeType, StoreTypePostfix.None, + dbType, unicode, size, fixedLength, precision, scale)) { } diff --git a/src/EFCore.Relational/Storage/SByteTypeMapping.cs b/src/EFCore.Relational/Storage/SByteTypeMapping.cs index 78aaecf305d..1cad5258e92 100644 --- a/src/EFCore.Relational/Storage/SByteTypeMapping.cs +++ b/src/EFCore.Relational/Storage/SByteTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class SByteTypeMapping : RelationalTypeMapping public SByteTypeMapping( string storeType, DbType? dbType = System.Data.DbType.SByte) - : base(storeType, typeof(sbyte), dbType) + : base(storeType, typeof(sbyte), dbType, jsonValueReaderWriter: JsonSByteReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/ShortTypeMapping.cs b/src/EFCore.Relational/Storage/ShortTypeMapping.cs index 286ca3ba2e6..85abe1730ad 100644 --- a/src/EFCore.Relational/Storage/ShortTypeMapping.cs +++ b/src/EFCore.Relational/Storage/ShortTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class ShortTypeMapping : RelationalTypeMapping public ShortTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Int16) - : base(storeType, typeof(short), dbType) + : base(storeType, typeof(short), dbType, jsonValueReaderWriter: JsonInt16ReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/StringTypeMapping.cs b/src/EFCore.Relational/Storage/StringTypeMapping.cs index dd0cfab8bc5..b04449e51f0 100644 --- a/src/EFCore.Relational/Storage/StringTypeMapping.cs +++ b/src/EFCore.Relational/Storage/StringTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -35,7 +36,8 @@ public StringTypeMapping( : base( new RelationalTypeMappingParameters( new CoreTypeMappingParameters( - typeof(string)), storeType, StoreTypePostfix.None, dbType, unicode, size)) + typeof(string), jsonValueReaderWriter: JsonStringReaderWriter.Instance), storeType, StoreTypePostfix.None, dbType, + unicode, size)) { } diff --git a/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs b/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs index f925ddc6327..1e906afdbf4 100644 --- a/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs +++ b/src/EFCore.Relational/Storage/TimeOnlyTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class TimeOnlyTypeMapping : RelationalTypeMapping public TimeOnlyTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Time) - : base(storeType, typeof(TimeOnly), dbType) + : base(storeType, typeof(TimeOnly), dbType, jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/TimeSpanTypeMapping.cs b/src/EFCore.Relational/Storage/TimeSpanTypeMapping.cs index b57494383c0..128bec8c55c 100644 --- a/src/EFCore.Relational/Storage/TimeSpanTypeMapping.cs +++ b/src/EFCore.Relational/Storage/TimeSpanTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class TimeSpanTypeMapping : RelationalTypeMapping public TimeSpanTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Time) - : base(storeType, typeof(TimeSpan), dbType) + : base(storeType, typeof(TimeSpan), dbType, jsonValueReaderWriter: JsonTimeSpanReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/UIntTypeMapping.cs b/src/EFCore.Relational/Storage/UIntTypeMapping.cs index 9a4b9b7f13e..9e3f8995967 100644 --- a/src/EFCore.Relational/Storage/UIntTypeMapping.cs +++ b/src/EFCore.Relational/Storage/UIntTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class UIntTypeMapping : RelationalTypeMapping public UIntTypeMapping( string storeType, DbType? dbType = System.Data.DbType.UInt32) - : base(storeType, typeof(uint), dbType) + : base(storeType, typeof(uint), dbType, jsonValueReaderWriter: JsonUInt32ReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/ULongTypeMapping.cs b/src/EFCore.Relational/Storage/ULongTypeMapping.cs index 868238bb399..e7f8b51e433 100644 --- a/src/EFCore.Relational/Storage/ULongTypeMapping.cs +++ b/src/EFCore.Relational/Storage/ULongTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class ULongTypeMapping : RelationalTypeMapping public ULongTypeMapping( string storeType, DbType? dbType = System.Data.DbType.UInt64) - : base(storeType, typeof(ulong), dbType) + : base(storeType, typeof(ulong), dbType, jsonValueReaderWriter: JsonUInt64ReaderWriter.Instance) { } diff --git a/src/EFCore.Relational/Storage/UShortTypeMapping.cs b/src/EFCore.Relational/Storage/UShortTypeMapping.cs index 040a2e79f0f..bcd48d71035 100644 --- a/src/EFCore.Relational/Storage/UShortTypeMapping.cs +++ b/src/EFCore.Relational/Storage/UShortTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -28,7 +29,7 @@ public class UShortTypeMapping : RelationalTypeMapping public UShortTypeMapping( string storeType, DbType? dbType = System.Data.DbType.UInt16) - : base(storeType, typeof(ushort), dbType) + : base(storeType, typeof(ushort), dbType, jsonValueReaderWriter: JsonUInt16ReaderWriter.Instance) { } diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMapping.cs b/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMapping.cs index 4c8af78e50a..7ddb0514cc4 100644 --- a/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMapping.cs +++ b/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMapping.cs @@ -5,6 +5,7 @@ using System.Data.Common; using System.Linq.Expressions; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; using Microsoft.EntityFrameworkCore.SqlServer.Storage.ValueConversion.Internal; using Microsoft.EntityFrameworkCore.Storage; @@ -20,10 +21,10 @@ public class SqlServerHierarchyIdTypeMapping : RelationalTypeMapping { private const string HierarchyIdFormatConst = "hierarchyid::Parse('{0}')"; - private static readonly ConstructorInfo _hierarchyIdConstructor + private static readonly ConstructorInfo HierarchyIdConstructor = typeof(HierarchyId).GetConstructor(new[] { typeof(string) })!; - private static readonly SqlServerHierarchyIdValueConverter _valueConverter = new(); + private static readonly SqlServerHierarchyIdValueConverter ValueConverter = new(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,11 +34,12 @@ private static readonly ConstructorInfo _hierarchyIdConstructor /// public SqlServerHierarchyIdTypeMapping(string storeType) : this( - new RelationalTypeMappingParameters( - new CoreTypeMappingParameters( - typeof(HierarchyId), - _valueConverter), - storeType)) + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(HierarchyId), + ValueConverter, + jsonValueReaderWriter: SqlServerJsonHierarchyIdReaderWriter.Instance), + storeType)) { } @@ -83,7 +85,7 @@ protected override void ConfigureParameter(DbParameter parameter) /// public override Expression GenerateCodeLiteral(object value) => Expression.New( - _hierarchyIdConstructor, + HierarchyIdConstructor, Expression.Constant(((HierarchyId)value).ToString())); /// diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMappingSourcePlugin.cs b/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMappingSourcePlugin.cs index 16a9e604756..56da9f596b9 100644 --- a/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMappingSourcePlugin.cs +++ b/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerHierarchyIdTypeMappingSourcePlugin.cs @@ -35,18 +35,21 @@ public class SqlServerHierarchyIdTypeMappingSourcePlugin : IRelationalTypeMappin { return _hierarchyId; } - else if (clrType == typeof(SqlHierarchyId)) + + if (clrType == typeof(SqlHierarchyId)) { return _sqlHierarchyId; } return null; } - else if (clrType == typeof(HierarchyId)) + + if (clrType == typeof(HierarchyId)) { return _hierarchyId; } - else if (clrType == typeof(SqlHierarchyId)) + + if (clrType == typeof(SqlHierarchyId)) { return _sqlHierarchyId; } diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerSqlHierarchyIdTypeMapping.cs b/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerSqlHierarchyIdTypeMapping.cs index a863601b294..9a26f2b6e58 100644 --- a/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerSqlHierarchyIdTypeMapping.cs +++ b/src/EFCore.SqlServer.HierarchyId/Storage/Internal/SqlServerSqlHierarchyIdTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data.SqlTypes; using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.SqlServer.Types; @@ -18,7 +19,7 @@ public class SqlServerSqlHierarchyIdTypeMapping : RelationalTypeMapping { private const string SqlHierarchyIdFormatConst = "hierarchyid::Parse('{0}')"; - private static readonly MethodInfo _sqlHierarchyIdParseMethod + private static readonly MethodInfo SqlHierarchyIdParseMethod = typeof(SqlHierarchyId).GetRuntimeMethod(nameof(SqlHierarchyId.Parse), new[] { typeof(SqlString) })!; /// @@ -28,7 +29,7 @@ private static readonly MethodInfo _sqlHierarchyIdParseMethod /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public SqlServerSqlHierarchyIdTypeMapping(string storeType) - : base(storeType, typeof(SqlHierarchyId)) + : base(storeType, typeof(SqlHierarchyId), jsonValueReaderWriter: SqlServerJsonSqlHierarchyIdReaderWriter.Instance) { } @@ -69,6 +70,6 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p /// public override Expression GenerateCodeLiteral(object value) => Expression.Call( - _sqlHierarchyIdParseMethod, + SqlHierarchyIdParseMethod, Expression.Convert(Expression.Constant(((SqlHierarchyId)value).ToString()), typeof(SqlString))); } diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/Json/SqlServerJsonHierarchyIdReaderWriter.cs b/src/EFCore.SqlServer.HierarchyId/Storage/Json/SqlServerJsonHierarchyIdReaderWriter.cs new file mode 100644 index 00000000000..f63967a751b --- /dev/null +++ b/src/EFCore.SqlServer.HierarchyId/Storage/Json/SqlServerJsonHierarchyIdReaderWriter.cs @@ -0,0 +1,30 @@ +// 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.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class SqlServerJsonHierarchyIdReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static SqlServerJsonHierarchyIdReaderWriter Instance { get; } = new(); + + private SqlServerJsonHierarchyIdReaderWriter() + { + } + + /// + public override HierarchyId FromJsonTyped(ref Utf8JsonReaderManager manager) + => new(manager.CurrentReader.GetString()!); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, HierarchyId value) + => writer.WriteStringValue(value.ToString()); +} diff --git a/src/EFCore.SqlServer.HierarchyId/Storage/Json/SqlServerJsonSqlHierarchyIdReaderWriter.cs b/src/EFCore.SqlServer.HierarchyId/Storage/Json/SqlServerJsonSqlHierarchyIdReaderWriter.cs new file mode 100644 index 00000000000..4ae43f1027b --- /dev/null +++ b/src/EFCore.SqlServer.HierarchyId/Storage/Json/SqlServerJsonSqlHierarchyIdReaderWriter.cs @@ -0,0 +1,31 @@ +// 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.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; +using Microsoft.SqlServer.Types; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class SqlServerJsonSqlHierarchyIdReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static SqlServerJsonSqlHierarchyIdReaderWriter Instance { get; } = new(); + + private SqlServerJsonSqlHierarchyIdReaderWriter() + { + } + + /// + public override SqlHierarchyId FromJsonTyped(ref Utf8JsonReaderManager manager) + => SqlHierarchyId.Parse(manager.CurrentReader.GetString()!); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, SqlHierarchyId value) + => writer.WriteStringValue(value.ToString()); +} diff --git a/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs b/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs index 80763882ca2..1ba2732c92c 100644 --- a/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs +++ b/src/EFCore.SqlServer.NTS/Storage/Internal/SqlServerGeometryTypeMapping.cs @@ -6,6 +6,7 @@ using System.Text; using JetBrains.Annotations; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; using Microsoft.EntityFrameworkCore.SqlServer.Storage.ValueConversion.Internal; using NetTopologySuite.Geometries; using NetTopologySuite.IO; @@ -41,6 +42,7 @@ public SqlServerGeometryTypeMapping(NtsGeometryServices geometryServices, string new GeometryValueConverter( CreateReader(geometryServices, IsGeography(storeType)), CreateWriter(IsGeography(storeType))), + SqlServerJsonGeometryWktReaderWriter.Instance, storeType) { _isGeography = IsGeography(storeType); diff --git a/src/EFCore.SqlServer.NTS/Storage/Json/SqlServerJsonGeometryWktReaderWriter.cs b/src/EFCore.SqlServer.NTS/Storage/Json/SqlServerJsonGeometryWktReaderWriter.cs new file mode 100644 index 00000000000..a1a7d24521f --- /dev/null +++ b/src/EFCore.SqlServer.NTS/Storage/Json/SqlServerJsonGeometryWktReaderWriter.cs @@ -0,0 +1,34 @@ +// 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.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; +using NetTopologySuite.Geometries; +using NetTopologySuite.IO; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; + +/// +/// Reads and writes JSON using the well-known-text format for values. +/// +public sealed class SqlServerJsonGeometryWktReaderWriter : JsonValueReaderWriter +{ + private static readonly WKTReader WktReader = new(); + + /// + /// The singleton instance of this stateless reader/writer. + /// + public static SqlServerJsonGeometryWktReaderWriter Instance { get; } = new(); + + private SqlServerJsonGeometryWktReaderWriter() + { + } + + /// + public override Geometry FromJsonTyped(ref Utf8JsonReaderManager manager) + => WktReader.Read(manager.CurrentReader.GetString()); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) + => writer.WriteStringValue(value.ToText()); +} diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerByteArrayTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerByteArrayTypeMapping.cs index 3b7472a4002..3e6fe74b0dc 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerByteArrayTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerByteArrayTypeMapping.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Text; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -35,7 +36,7 @@ public SqlServerByteArrayTypeMapping( StoreTypePostfix? storeTypePostfix = null) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(byte[]), null, comparer), + new CoreTypeMappingParameters(typeof(byte[]), null, comparer, jsonValueReaderWriter: JsonByteArrayReaderWriter.Instance), storeType ?? (fixedLength ? "binary" : "varbinary"), storeTypePostfix ?? StoreTypePostfix.Size, System.Data.DbType.Binary, diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs index 6cc2a29dcf8..59fb13e6ab0 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeOffsetTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -39,7 +40,7 @@ public SqlServerDateTimeOffsetTypeMapping( StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(DateTimeOffset)), + new CoreTypeMappingParameters(typeof(DateTimeOffset), jsonValueReaderWriter: JsonDateTimeOffsetReaderWriter.Instance), storeType, storeTypePostfix, dbType)) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs index 9139d297df2..1a01041cfba 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDateTimeTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -47,7 +48,7 @@ public SqlServerDateTimeTypeMapping( StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(DateTime)), + new CoreTypeMappingParameters(typeof(DateTime), jsonValueReaderWriter: JsonDateTimeReaderWriter.Instance), storeType, storeTypePostfix, dbType), diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDecimalTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDecimalTypeMapping.cs index 02d3e867b3c..53b62662c91 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDecimalTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDecimalTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -31,7 +32,7 @@ public SqlServerDecimalTypeMapping( StoreTypePostfix storeTypePostfix = StoreTypePostfix.PrecisionAndScale) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(decimal)), + new CoreTypeMappingParameters(typeof(decimal), jsonValueReaderWriter: JsonDecimalReaderWriter.Instance), storeType, storeTypePostfix, dbType) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDoubleTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDoubleTypeMapping.cs index ebbe44d90ea..893f39ca7b9 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDoubleTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDoubleTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -28,7 +29,7 @@ public SqlServerDoubleTypeMapping( StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(double)), + new CoreTypeMappingParameters(typeof(double), jsonValueReaderWriter: JsonDoubleReaderWriter.Instance), storeType, storeTypePostfix, dbType)) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerFloatTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerFloatTypeMapping.cs index ce39416cd7e..a3d1b9a3b3e 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerFloatTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerFloatTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -25,7 +26,7 @@ public SqlServerFloatTypeMapping( StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(float)), + new CoreTypeMappingParameters(typeof(float), jsonValueReaderWriter: JsonFloatReaderWriter.Instance), storeType, storeTypePostfix, dbType)) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerLongTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerLongTypeMapping.cs index 04e089b4df4..5db7e8f286a 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerLongTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerLongTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -27,7 +28,8 @@ public SqlServerLongTypeMapping( DbType? dbType = System.Data.DbType.Int64) : this( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(long), converter, comparer, providerValueComparer), + new CoreTypeMappingParameters( + typeof(long), converter, comparer, providerValueComparer, jsonValueReaderWriter: JsonInt64ReaderWriter.Instance), storeType, dbType: dbType)) { diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs index 751da51b988..2c254084f07 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerStringTypeMapping.cs @@ -4,6 +4,7 @@ using System.Data; using System.Text; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -44,7 +45,8 @@ public SqlServerStringTypeMapping( new CoreTypeMappingParameters( typeof(string), comparer: useKeyComparison ? CaseInsensitiveValueComparer : null, - keyComparer: useKeyComparison ? CaseInsensitiveValueComparer : null), + keyComparer: useKeyComparison ? CaseInsensitiveValueComparer : null, + jsonValueReaderWriter: JsonStringReaderWriter.Instance), storeType ?? GetDefaultStoreName(unicode, fixedLength), storeTypePostfix ?? StoreTypePostfix.Size, GetDbType(unicode, fixedLength), diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeOnlyTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeOnlyTypeMapping.cs index 38da833b5d9..784767199c7 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeOnlyTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeOnlyTypeMapping.cs @@ -4,6 +4,7 @@ using System.Data; using System.Globalization; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -32,7 +33,7 @@ public class SqlServerTimeOnlyTypeMapping : TimeOnlyTypeMapping internal SqlServerTimeOnlyTypeMapping(string storeType, StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(TimeOnly)), + new CoreTypeMappingParameters(typeof(TimeOnly), jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance), storeType, storeTypePostfix, System.Data.DbType.Time)) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs index ea5f8602f46..d3ebfe40acd 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTimeSpanTypeMapping.cs @@ -4,6 +4,7 @@ using System.Data; using System.Globalization; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -41,7 +42,7 @@ public SqlServerTimeSpanTypeMapping( StoreTypePostfix storeTypePostfix = StoreTypePostfix.Precision) : base( new RelationalTypeMappingParameters( - new CoreTypeMappingParameters(typeof(TimeSpan)), + new CoreTypeMappingParameters(typeof(TimeSpan), jsonValueReaderWriter: JsonTimeSpanReaderWriter.Instance), storeType, storeTypePostfix, dbType)) diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeOffsetTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeOffsetTypeMapping.cs index b10edc13967..fb854d4c4ac 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeOffsetTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeOffsetTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; @@ -24,7 +25,11 @@ public class SqliteDateTimeOffsetTypeMapping : DateTimeOffsetTypeMapping public SqliteDateTimeOffsetTypeMapping( string storeType, DbType? dbType = System.Data.DbType.DateTimeOffset) - : base(storeType, dbType) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(DateTimeOffset), jsonValueReaderWriter: JsonDateTimeOffsetReaderWriter.Instance), + storeType, + dbType: dbType)) { } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeTypeMapping.cs index e2db03b507f..4c3e64324a0 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteDateTimeTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; @@ -24,7 +25,11 @@ public class SqliteDateTimeTypeMapping : DateTimeTypeMapping public SqliteDateTimeTypeMapping( string storeType, DbType? dbType = System.Data.DbType.DateTime) - : base(storeType, dbType) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(DateTime), jsonValueReaderWriter: JsonDateTimeReaderWriter.Instance), + storeType, + dbType: dbType)) { } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteGuidTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteGuidTypeMapping.cs index ff744145215..92f35bc73e6 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteGuidTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteGuidTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; @@ -22,7 +23,13 @@ public class SqliteGuidTypeMapping : GuidTypeMapping public SqliteGuidTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Guid) - : base(storeType, dbType) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(Guid), + jsonValueReaderWriter: JsonGuidReaderWriter.Instance), + storeType, + dbType: dbType)) { } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs index 590417f511f..edbbbfa60a9 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteStringTypeMapping.cs @@ -3,6 +3,7 @@ using System.Data; using System.Text; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; @@ -26,7 +27,11 @@ public SqliteStringTypeMapping( DbType? dbType = null, bool unicode = false, int? size = null) - : base(storeType, dbType, unicode, size) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters( + typeof(string), jsonValueReaderWriter: JsonStringReaderWriter.Instance), storeType, StoreTypePostfix.None, dbType, + unicode, size)) { } diff --git a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs index 4350ffd5b1b..a0e18953465 100644 --- a/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs +++ b/src/EFCore.Sqlite.Core/Storage/Internal/SqliteTimeOnlyTypeMapping.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Internal; @@ -22,7 +23,11 @@ public class SqliteTimeOnlyTypeMapping : TimeOnlyTypeMapping public SqliteTimeOnlyTypeMapping( string storeType, DbType? dbType = System.Data.DbType.Time) - : base(storeType, dbType) + : base( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(TimeOnly), jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance), + storeType, + dbType: dbType)) { } diff --git a/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs b/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs index ff39d1787b6..db6dc144feb 100644 --- a/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs +++ b/src/EFCore.Sqlite.NTS/Storage/Internal/SqliteGeometryTypeMapping.cs @@ -4,6 +4,7 @@ using System.Text; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Sqlite.Internal; +using Microsoft.EntityFrameworkCore.Sqlite.Storage.Json; using Microsoft.EntityFrameworkCore.Sqlite.Storage.ValueConversion.Internal; using NetTopologySuite.Geometries; using NetTopologySuite.IO; @@ -31,7 +32,9 @@ private static readonly MethodInfo _getBytes /// [UsedImplicitly] public SqliteGeometryTypeMapping(NtsGeometryServices geometryServices, string storeType) - : base(new GeometryValueConverter(CreateReader(geometryServices), CreateWriter(storeType)), storeType) + : base( + new GeometryValueConverter(CreateReader(geometryServices), CreateWriter(storeType)), + SqliteJsonGeometryWktReaderWriter.Instance, storeType) { } diff --git a/src/EFCore.Sqlite.NTS/Storage/Json/SqliteJsonGeometryWktReaderWriter.cs b/src/EFCore.Sqlite.NTS/Storage/Json/SqliteJsonGeometryWktReaderWriter.cs new file mode 100644 index 00000000000..790384a5d37 --- /dev/null +++ b/src/EFCore.Sqlite.NTS/Storage/Json/SqliteJsonGeometryWktReaderWriter.cs @@ -0,0 +1,34 @@ +// 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.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; +using NetTopologySuite.Geometries; +using NetTopologySuite.IO; + +namespace Microsoft.EntityFrameworkCore.Sqlite.Storage.Json; + +/// +/// Reads and writes JSON using the well-known-text format for values. +/// +public sealed class SqliteJsonGeometryWktReaderWriter : JsonValueReaderWriter +{ + private static readonly WKTReader WktReader = new(); + + /// + /// The singleton instance of this stateless reader/writer. + /// + public static SqliteJsonGeometryWktReaderWriter Instance { get; } = new(); + + private SqliteJsonGeometryWktReaderWriter() + { + } + + /// + public override Geometry FromJsonTyped(ref Utf8JsonReaderManager manager) + => WktReader.Read(manager.CurrentReader.GetString()); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) + => writer.WriteStringValue(value.ToText()); +} diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index 067d3d3aa75..ff4b7b79262 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Storage.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Update.Internal; using Microsoft.Extensions.Caching.Memory; @@ -83,6 +84,7 @@ public static readonly IDictionary CoreServices { typeof(IEvaluatableExpressionFilter), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(INavigationExpansionExtensibilityHelper), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IExceptionDetector), new ServiceCharacteristics(ServiceLifetime.Singleton) }, + { typeof(IJsonValueReaderWriterSource), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IProviderConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IDiagnosticsLogger<>), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -303,6 +305,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); TryAdd( p => p.GetService()?.FindExtension()?.DbContextLogger @@ -328,6 +331,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() + .AddDependencySingleton() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 6ceb5e42a67..2f0a65583ed 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -356,6 +356,7 @@ private static RuntimeProperty Create(IProperty property, RuntimeEntityType runt property.GetValueComparer(), property.GetKeyValueComparer(), property.GetProviderValueComparer(), + property.GetJsonValueReaderWriter(), property.GetTypeMapping()); /// diff --git a/src/EFCore/Metadata/IConventionProperty.cs b/src/EFCore/Metadata/IConventionProperty.cs index 5d2e9dd0e79..9cd79384d95 100644 --- a/src/EFCore/Metadata/IConventionProperty.cs +++ b/src/EFCore/Metadata/IConventionProperty.cs @@ -2,6 +2,7 @@ // 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; @@ -333,7 +334,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); /// @@ -359,7 +361,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); /// @@ -400,7 +403,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); /// @@ -427,7 +431,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); /// @@ -435,4 +440,21 @@ bool IsImplicitlyCreated() /// /// The configuration source for . ConfigurationSource? GetProviderValueComparerConfigurationSource(); + + /// + /// Sets the type of to use for this property. + /// + /// + /// 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/IMutableProperty.cs b/src/EFCore/Metadata/IMutableProperty.cs index 1b042624a34..936c2616a91 100644 --- a/src/EFCore/Metadata/IMutableProperty.cs +++ b/src/EFCore/Metadata/IMutableProperty.cs @@ -2,6 +2,7 @@ // 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; @@ -202,7 +203,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. @@ -256,5 +258,15 @@ void SetValueGeneratorFactory( /// /// A type that derives from , or to remove any previously set comparer. /// - void SetProviderValueComparer([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType); + void SetProviderValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType); + + /// + /// Sets the type of to use for this property for this property. + /// + /// + /// A type that derives from , or to use the reader/writer + /// from the type mapping. + /// + void SetJsonValueReaderWriterType(Type? readerWriterType); } diff --git a/src/EFCore/Metadata/IReadOnlyProperty.cs b/src/EFCore/Metadata/IReadOnlyProperty.cs index ff6117c03be..08bbfe85bf8 100644 --- a/src/EFCore/Metadata/IReadOnlyProperty.cs +++ b/src/EFCore/Metadata/IReadOnlyProperty.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -68,8 +69,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(); @@ -164,6 +165,12 @@ CoreTypeMapping GetTypeMapping() /// The comparer, or if none has been set. ValueComparer? GetProviderValueComparer(); + /// + /// Gets the for this property, or if none is set. + /// + /// The reader/writer, or if none has been set. + JsonValueReaderWriter? GetJsonValueReaderWriter(); + /// /// Finds the first principal property that the given property is constrained by /// if the given property is part of a foreign key. diff --git a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs index 3df816e2e66..8062c2725f0 100644 --- a/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs +++ b/src/EFCore/Metadata/Internal/CoreAnnotationNames.cs @@ -316,6 +316,14 @@ public static class CoreAnnotationNames /// public const string AdHocModel = "AdHocModel"; + /// + /// 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 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 @@ -363,6 +371,7 @@ public static class CoreAnnotationNames AmbiguousField, DuplicateServiceProperties, FullChangeTrackingNotificationsRequired, - AdHocModel + AdHocModel, + JsonValueReaderWriterType }; } diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index 00032e5b913..bd79039e222 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -585,7 +586,8 @@ public virtual PropertySaveBehavior GetAfterSaveBehavior() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Type? SetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? factoryType, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? factoryType, ConfigurationSource configurationSource) { if (factoryType != null) @@ -667,7 +669,8 @@ public virtual PropertySaveBehavior GetAfterSaveBehavior() /// 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, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, ConfigurationSource configurationSource) { ValueConverter? converter = null; @@ -749,8 +752,9 @@ public virtual PropertySaveBehavior GetAfterSaveBehavior() } return i == ForeignKey.LongestFkChainAllowedLength - ? throw new InvalidOperationException(CoreStrings.RelationshipCycle( - DeclaringEntityType.DisplayName(), Name, "ValueConverter")) + ? throw new InvalidOperationException( + CoreStrings.RelationshipCycle( + DeclaringEntityType.DisplayName(), Name, "ValueConverter")) : null; } @@ -840,8 +844,9 @@ public virtual PropertySaveBehavior GetAfterSaveBehavior() } return i == ForeignKey.LongestFkChainAllowedLength - ? throw new InvalidOperationException(CoreStrings.RelationshipCycle( - DeclaringEntityType.DisplayName(), Name, "ProviderClrType")) + ? throw new InvalidOperationException( + CoreStrings.RelationshipCycle( + DeclaringEntityType.DisplayName(), Name, "ProviderClrType")) : null; } @@ -927,7 +932,8 @@ public virtual CoreTypeMapping? TypeMapping /// [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public virtual Type? SetValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, ConfigurationSource configurationSource) { ValueComparer? comparer = null; @@ -1029,7 +1035,8 @@ public virtual CoreTypeMapping? TypeMapping /// [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public virtual Type? SetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, ConfigurationSource configurationSource) { ValueComparer? comparer = null; @@ -1122,6 +1129,74 @@ public virtual CoreTypeMapping? TypeMapping 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() + { + return TryCreateReader((Type?)this[CoreAnnotationNames.JsonValueReaderWriterType]) + ?? TypeMapping?.JsonValueReaderWriter; + + 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; + } + } + + /// + /// 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 @@ -1707,7 +1782,8 @@ void IMutableProperty.SetValueGeneratorFactory(Func [DebuggerStepThrough] void IMutableProperty.SetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactory) + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactory) => SetValueGeneratorFactory(valueGeneratorFactory, ConfigurationSource.Explicit); /// @@ -1718,7 +1794,8 @@ void IMutableProperty.SetValueGeneratorFactory( /// [DebuggerStepThrough] Type? IConventionProperty.SetValueGeneratorFactory( - [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] Type? valueGeneratorFactory, + [DynamicallyAccessedMembers(ValueGeneratorFactory.DynamicallyAccessedMemberTypes)] + Type? valueGeneratorFactory, bool fromDataAnnotation) => SetValueGeneratorFactory( valueGeneratorFactory, @@ -1753,7 +1830,8 @@ void IMutableProperty.SetValueConverter(ValueConverter? converter) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - void IMutableProperty.SetValueConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType) + void IMutableProperty.SetValueConverter( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType) => SetValueConverter(converterType, ConfigurationSource.Explicit); /// @@ -1764,7 +1842,8 @@ void IMutableProperty.SetValueConverter([DynamicallyAccessedMembers(DynamicallyA /// [DebuggerStepThrough] Type? IConventionProperty.SetValueConverter( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? converterType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? converterType, bool fromDataAnnotation) => SetValueConverter( converterType, @@ -1821,7 +1900,8 @@ void IMutableProperty.SetValueComparer(ValueComparer? comparer) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - void IMutableProperty.SetValueComparer([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType) + void IMutableProperty.SetValueComparer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType) => SetValueComparer(comparerType, ConfigurationSource.Explicit); /// @@ -1833,7 +1913,8 @@ void IMutableProperty.SetValueComparer([DynamicallyAccessedMembers(DynamicallyAc [DebuggerStepThrough] [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? IConventionProperty.SetValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation) => SetValueComparer( comparerType, @@ -1888,7 +1969,8 @@ void IMutableProperty.SetProviderValueComparer(ValueComparer? comparer) /// [DebuggerStepThrough] void IMutableProperty.SetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType) => SetProviderValueComparer(comparerType, ConfigurationSource.Explicit); /// @@ -1900,7 +1982,8 @@ void IMutableProperty.SetProviderValueComparer( [DebuggerStepThrough] [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? IConventionProperty.SetProviderValueComparer( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type? comparerType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type? comparerType, bool fromDataAnnotation) => SetProviderValueComparer( comparerType, @@ -1916,6 +1999,40 @@ void IMutableProperty.SetProviderValueComparer( ValueComparer IProperty.GetProviderValueComparer() => GetProviderValueComparer()!; + /// + /// 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.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? IConventionProperty.SetJsonValueReaderWriterType( + Type? readerWriterType, + bool fromDataAnnotation) + => SetJsonValueReaderWriterType( + readerWriterType, + 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] + JsonValueReaderWriter? IReadOnlyProperty.GetJsonValueReaderWriter() + => GetJsonValueReaderWriter(); + /// /// Gets the sentinel value that indicates that this property is not set. /// diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 863a8e20c03..4aedc345306 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -605,6 +606,7 @@ private IEnumerable GetIndexes() /// The for this property. /// The to use with keys for this property. /// The to use for the provider values for this property. + /// The for this property. /// The for this property. /// The newly created property. public virtual RuntimeProperty AddProperty( @@ -629,6 +631,7 @@ public virtual RuntimeProperty AddProperty( ValueComparer? valueComparer = null, ValueComparer? keyValueComparer = null, ValueComparer? providerValueComparer = null, + JsonValueReaderWriter? jsonValueReaderWriter = null, CoreTypeMapping? typeMapping = null) { var property = new RuntimeProperty( @@ -654,6 +657,7 @@ public virtual RuntimeProperty AddProperty( valueComparer, keyValueComparer, providerValueComparer, + jsonValueReaderWriter, typeMapping); _properties.Add(property.Name, property); @@ -727,7 +731,7 @@ private IEnumerable GetProperties() /// The name of the property to add. /// The corresponding CLR property or for a shadow property. /// The corresponding CLR field or for a shadow property. - /// The type of the service, or to use the type of the member. + /// The type of the service, or to use the type of the member. /// The used for this property. /// The newly created service property. public virtual RuntimeServiceProperty AddServiceProperty( diff --git a/src/EFCore/Metadata/RuntimeProperty.cs b/src/EFCore/Metadata/RuntimeProperty.cs index 7ecc327ab3e..983b00982c4 100644 --- a/src/EFCore/Metadata/RuntimeProperty.cs +++ b/src/EFCore/Metadata/RuntimeProperty.cs @@ -1,10 +1,11 @@ // 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; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -27,6 +28,7 @@ public class RuntimeProperty : RuntimePropertyBase, IProperty private ValueComparer? _valueComparer; private ValueComparer? _keyValueComparer; private readonly ValueComparer? _providerValueComparer; + private readonly JsonValueReaderWriter? _jsonValueReaderWriter; private CoreTypeMapping? _typeMapping; /// @@ -59,6 +61,7 @@ public RuntimeProperty( ValueComparer? valueComparer, ValueComparer? keyValueComparer, ValueComparer? providerValueComparer, + JsonValueReaderWriter? jsonValueReaderWriter, CoreTypeMapping? typeMapping) : base(name, propertyInfo, fieldInfo, propertyAccessMode) { @@ -102,6 +105,7 @@ public RuntimeProperty( _valueComparer = valueComparer; _keyValueComparer = keyValueComparer ?? valueComparer; _providerValueComparer = providerValueComparer; + _jsonValueReaderWriter = jsonValueReaderWriter; } /// @@ -209,7 +213,7 @@ private ValueComparer GetKeyValueComparer() private ValueComparer? GetKeyValueComparer(HashSet? checkedProperties) { - if ( _keyValueComparer != null) + if (_keyValueComparer != null) { return _keyValueComparer; } @@ -233,6 +237,13 @@ private ValueComparer GetKeyValueComparer() return principal.GetKeyValueComparer(checkedProperties); } + /// + /// Gets the for this property, or if none is set. + /// + /// The reader/writer, or if none has been set. + public virtual JsonValueReaderWriter? GetJsonValueReaderWriter() + => _jsonValueReaderWriter; + /// public override object? Sentinel => _sentinel; diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 356730b18cd..0ede52adaa7 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -178,6 +178,14 @@ public static string BadFilterOwnedType(object? filter, object? entityType) GetString("BadFilterOwnedType", nameof(filter), nameof(entityType)), filter, entityType); + /// + /// The type '{givenType}' cannot be used as a 'JsonValueReaderWriter' because it does not inherit from the generic 'JsonValueReaderWriter<TValue>'. Make sure to inherit json reader/writers from 'JsonValueReaderWriter<TValue>'. + /// + public static string BadJsonValueReaderWriterType(object? givenType) + => string.Format( + GetString("BadJsonValueReaderWriterType", nameof(givenType)), + givenType); + /// /// The type '{givenType}' cannot be used as a value comparer because it does not inherit from '{expectedType}'. Make sure to inherit value comparers from '{expectedType}'. /// @@ -232,6 +240,14 @@ public static string CannotConvertEnumValue(object? value, object? enumType) public static string CannotConvertQueryableToEnumerableMethod => GetString("CannotConvertQueryableToEnumerableMethod"); + /// + /// Cannot create an instance of reade/writer type '{readerWriterType}'. Ensure that the type can be instantiated and has a public parameterless constructor, or has a public static 'Instance' field returning the singleton instance to use. + /// + public static string CannotCreateJsonValueReaderWriter(object? readerWriterType) + => string.Format( + GetString("CannotCreateJsonValueReaderWriter", nameof(readerWriterType)), + readerWriterType); + /// /// Cannot create an instance of value comparer type '{generatorType}'. Ensure that the type can be instantiated and has a parameterless constructor, or use the overload of '{method}' that accepts a delegate. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index a1165178f89..6892e37b88a 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1,17 +1,17 @@  - @@ -174,6 +174,9 @@ The filter expression '{filter}' cannot be specified for owned entity type '{entityType}'. A filter may only be applied to an entity type that is not owned. See https://aka.ms/efcore-docs-owned for more information and examples. + + The type '{givenType}' cannot be used as a 'JsonValueReaderWriter' because it does not inherit from the generic 'JsonValueReaderWriter<TValue>'. Make sure to inherit json reader/writers from 'JsonValueReaderWriter<TValue>'. + The type '{givenType}' cannot be used as a value comparer because it does not inherit from '{expectedType}'. Make sure to inherit value comparers from '{expectedType}'. @@ -195,6 +198,9 @@ Unable to convert a queryable method to an enumerable method. This is likely an issue in Entity Framework, please file an issue at https://go.microsoft.com/fwlink/?linkid=2142044. + + Cannot create an instance of reade/writer type '{readerWriterType}'. Ensure that the type can be instantiated and has a public parameterless constructor, or has a public static 'Instance' field returning the singleton instance to use. + Cannot create an instance of value comparer type '{generatorType}'. Ensure that the type can be instantiated and has a parameterless constructor, or use the overload of '{method}' that accepts a delegate. diff --git a/src/EFCore/Storage/CoreTypeMapping.cs b/src/EFCore/Storage/CoreTypeMapping.cs index 364aa275bad..a5eb87daeec 100644 --- a/src/EFCore/Storage/CoreTypeMapping.cs +++ b/src/EFCore/Storage/CoreTypeMapping.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Storage; @@ -38,6 +39,7 @@ protected readonly record struct CoreTypeMappingParameters /// /// If this type mapping represents a primitive collection, this holds the element's type mapping. /// + /// Handles reading and writing JSON values for instances of the mapped type. public CoreTypeMappingParameters( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type clrType, ValueConverter? converter = null, @@ -45,7 +47,8 @@ public CoreTypeMappingParameters( ValueComparer? keyComparer = null, ValueComparer? providerValueComparer = null, Func? valueGeneratorFactory = null, - CoreTypeMapping? elementTypeMapping = null) + CoreTypeMapping? elementTypeMapping = null, + JsonValueReaderWriter? jsonValueReaderWriter = null) { ClrType = clrType; Converter = converter; @@ -54,6 +57,7 @@ public CoreTypeMappingParameters( ProviderValueComparer = providerValueComparer; ValueGeneratorFactory = valueGeneratorFactory; ElementTypeMapping = elementTypeMapping; + JsonValueReaderWriter = jsonValueReaderWriter; } /// @@ -91,7 +95,12 @@ public CoreTypeMappingParameters( /// /// If this type mapping represents a primitive collection, this holds the element's type mapping. /// - public CoreTypeMapping? ElementTypeMapping { get; } + public CoreTypeMapping? ElementTypeMapping { get; init; } + + /// + /// Handles reading and writing JSON values for instances of the mapped type. + /// + public JsonValueReaderWriter? JsonValueReaderWriter { get; init; } /// /// Creates a new parameter object with the given @@ -107,7 +116,8 @@ public CoreTypeMappingParameters WithComposedConverter(ValueConverter? converter KeyComparer, ProviderValueComparer, ValueGeneratorFactory, - ElementTypeMapping); + ElementTypeMapping, + JsonValueReaderWriter); /// /// Creates a new parameter object with the given @@ -123,7 +133,24 @@ public CoreTypeMappingParameters WithElementTypeMapping(CoreTypeMapping elementT KeyComparer, ProviderValueComparer, ValueGeneratorFactory, - elementTypeMapping); + elementTypeMapping, + 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; @@ -184,7 +211,8 @@ protected CoreTypeMapping(CoreTypeMappingParameters parameters) /// /// Gets the .NET type used in the EF model. /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods + [DynamicallyAccessedMembers( + DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] public virtual Type ClrType { get; } @@ -257,4 +285,10 @@ public virtual Expression GenerateCodeLiteral(object value) /// public virtual CoreTypeMapping? ElementTypeMapping => Parameters.ElementTypeMapping; + + /// + /// Handles reading and writing JSON values for instances of the mapped type. + /// + public virtual JsonValueReaderWriter? JsonValueReaderWriter + => Parameters.JsonValueReaderWriter; } diff --git a/src/EFCore/Storage/Json/IJsonValueReaderWriterSource.cs b/src/EFCore/Storage/Json/IJsonValueReaderWriterSource.cs new file mode 100644 index 00000000000..c5cfb0cb1b2 --- /dev/null +++ b/src/EFCore/Storage/Json/IJsonValueReaderWriterSource.cs @@ -0,0 +1,30 @@ +// 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.Storage.Json; + +/// +/// +/// Attempts to find a for a given CLR type. +/// +/// +/// +/// +/// The service lifetime is . This means a single instance +/// is used by many instances. The implementation must be thread-safe. +/// This service cannot depend on services registered as . +/// +/// +/// See Implementation of database providers and extensions +/// for more information and examples. +/// +/// +public interface IJsonValueReaderWriterSource +{ + /// + /// Attempts to find a for a given CLR type. + /// + /// The type. + /// The found , or if none is available. + JsonValueReaderWriter? FindReaderWriter(Type type); +} diff --git a/src/EFCore/Storage/Json/JsonBoolReaderWriter.cs b/src/EFCore/Storage/Json/JsonBoolReaderWriter.cs new file mode 100644 index 00000000000..6885f72f19e --- /dev/null +++ b/src/EFCore/Storage/Json/JsonBoolReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonBoolReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonBoolReaderWriter Instance { get; } = new(); + + private JsonBoolReaderWriter() + { + } + + /// + public override bool FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetBoolean(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, bool value) + => writer.WriteBooleanValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonByteArrayReaderWriter.cs b/src/EFCore/Storage/Json/JsonByteArrayReaderWriter.cs new file mode 100644 index 00000000000..ebeba2777f8 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonByteArrayReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON as base64 for array values. +/// +public sealed class JsonByteArrayReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonByteArrayReaderWriter Instance { get; } = new(); + + private JsonByteArrayReaderWriter() + { + } + + /// + public override byte[] FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetBytesFromBase64(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, byte[] value) + => writer.WriteBase64StringValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonByteReaderWriter.cs b/src/EFCore/Storage/Json/JsonByteReaderWriter.cs new file mode 100644 index 00000000000..7e91c285e8d --- /dev/null +++ b/src/EFCore/Storage/Json/JsonByteReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonByteReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonByteReaderWriter Instance { get; } = new(); + + private JsonByteReaderWriter() + { + } + + /// + public override byte FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetByte(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, byte value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonCharReaderWriter.cs b/src/EFCore/Storage/Json/JsonCharReaderWriter.cs new file mode 100644 index 00000000000..d5e515ef71a --- /dev/null +++ b/src/EFCore/Storage/Json/JsonCharReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonCharReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonCharReaderWriter Instance { get; } = new(); + + private JsonCharReaderWriter() + { + } + + /// + public override char FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()![0]; + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, char value) + => writer.WriteStringValue(value.ToString()); +} diff --git a/src/EFCore/Storage/Json/JsonDateOnlyReaderWriter.cs b/src/EFCore/Storage/Json/JsonDateOnlyReaderWriter.cs new file mode 100644 index 00000000000..dd429d6b39b --- /dev/null +++ b/src/EFCore/Storage/Json/JsonDateOnlyReaderWriter.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonDateOnlyReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonDateOnlyReaderWriter Instance { get; } = new(); + + private JsonDateOnlyReaderWriter() + { + } + + /// + public override DateOnly FromJsonTyped(ref Utf8JsonReaderManager manager) + => DateOnly.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, DateOnly value) + => writer.WriteStringValue(value.ToString("o", CultureInfo.InvariantCulture)); +} diff --git a/src/EFCore/Storage/Json/JsonDateTimeOffsetReaderWriter.cs b/src/EFCore/Storage/Json/JsonDateTimeOffsetReaderWriter.cs new file mode 100644 index 00000000000..41ad92faa5c --- /dev/null +++ b/src/EFCore/Storage/Json/JsonDateTimeOffsetReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonDateTimeOffsetReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonDateTimeOffsetReaderWriter Instance { get; } = new(); + + private JsonDateTimeOffsetReaderWriter() + { + } + + /// + public override DateTimeOffset FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetDateTimeOffset(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, DateTimeOffset value) + => writer.WriteStringValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonDateTimeReaderWriter.cs b/src/EFCore/Storage/Json/JsonDateTimeReaderWriter.cs new file mode 100644 index 00000000000..8e06bfe76fd --- /dev/null +++ b/src/EFCore/Storage/Json/JsonDateTimeReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonDateTimeReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonDateTimeReaderWriter Instance { get; } = new(); + + private JsonDateTimeReaderWriter() + { + } + + /// + public override DateTime FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetDateTime(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, DateTime value) + => writer.WriteStringValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonDecimalReaderWriter.cs b/src/EFCore/Storage/Json/JsonDecimalReaderWriter.cs new file mode 100644 index 00000000000..d05339dccbc --- /dev/null +++ b/src/EFCore/Storage/Json/JsonDecimalReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonDecimalReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonDecimalReaderWriter Instance { get; } = new(); + + private JsonDecimalReaderWriter() + { + } + + /// + public override decimal FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetDecimal(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, decimal value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonDoubleReaderWriter.cs b/src/EFCore/Storage/Json/JsonDoubleReaderWriter.cs new file mode 100644 index 00000000000..38bbc5412d8 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonDoubleReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonDoubleReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonDoubleReaderWriter Instance { get; } = new(); + + private JsonDoubleReaderWriter() + { + } + + /// + public override double FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetDouble(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, double value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonFloatReaderWriter.cs b/src/EFCore/Storage/Json/JsonFloatReaderWriter.cs new file mode 100644 index 00000000000..2fe32561426 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonFloatReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonFloatReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonFloatReaderWriter Instance { get; } = new(); + + private JsonFloatReaderWriter() + { + } + + /// + public override float FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetSingle(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, float value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonGuidReaderWriter.cs b/src/EFCore/Storage/Json/JsonGuidReaderWriter.cs new file mode 100644 index 00000000000..de54f133ca6 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonGuidReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonGuidReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonGuidReaderWriter Instance { get; } = new(); + + private JsonGuidReaderWriter() + { + } + + /// + public override Guid FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetGuid(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, Guid value) + => writer.WriteStringValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonInt16ReaderWriter.cs b/src/EFCore/Storage/Json/JsonInt16ReaderWriter.cs new file mode 100644 index 00000000000..4c7a048dbbe --- /dev/null +++ b/src/EFCore/Storage/Json/JsonInt16ReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonInt16ReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonInt16ReaderWriter Instance { get; } = new(); + + private JsonInt16ReaderWriter() + { + } + + /// + public override short FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetInt16(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, short value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonInt32ReaderWriter.cs b/src/EFCore/Storage/Json/JsonInt32ReaderWriter.cs new file mode 100644 index 00000000000..49171c8dc49 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonInt32ReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonInt32ReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonInt32ReaderWriter Instance { get; } = new(); + + private JsonInt32ReaderWriter() + { + } + + /// + public override int FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetInt32(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, int value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonInt64ReaderWriter.cs b/src/EFCore/Storage/Json/JsonInt64ReaderWriter.cs new file mode 100644 index 00000000000..ee6f4b314b8 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonInt64ReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonInt64ReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonInt64ReaderWriter Instance { get; } = new(); + + private JsonInt64ReaderWriter() + { + } + + /// + public override long FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetInt64(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, long value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonNullReaderWriter.cs b/src/EFCore/Storage/Json/JsonNullReaderWriter.cs new file mode 100644 index 00000000000..8134d322e1f --- /dev/null +++ b/src/EFCore/Storage/Json/JsonNullReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonNullReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonNullReaderWriter Instance { get; } = new(); + + private JsonNullReaderWriter() + { + } + + /// + public override object? FromJsonTyped(ref Utf8JsonReaderManager manager) + => null; + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, object? value) + => writer.WriteNullValue(); +} diff --git a/src/EFCore/Storage/Json/JsonReaderData.cs b/src/EFCore/Storage/Json/JsonReaderData.cs new file mode 100644 index 00000000000..99bac1cb877 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonReaderData.cs @@ -0,0 +1,93 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Contains state for use with a , abstracting the reading from a or a buffer. +/// +public class JsonReaderData +{ + private readonly Stream? _stream; + private byte[] _buffer; + private int _positionInBuffer; + private int _bytesAvailable; + private JsonReaderState _readerState; + + /// + /// Creates a new object to read JSON from the given buffer. + /// + /// The buffer containing UTF8 JSON bytes. + public JsonReaderData(byte[] buffer) + { + _buffer = buffer; + _bytesAvailable = buffer.Length; + } + + /// + /// Creates a new object to read JSON from the given stream. + /// + /// The stream providing UTF8 JSON bytes. + public JsonReaderData(Stream stream) + { + _stream = stream; + _buffer = new byte[256]; + ReadBytes(0, default); + } + + /// + /// Called to capture the state of the given so that a new + /// can later be created to pick up at the same position in the JSON document. + /// + /// The manager. + public virtual void CaptureState(ref Utf8JsonReaderManager manager) + { + _positionInBuffer += (int)manager.CurrentReader.BytesConsumed; + _readerState = manager.CurrentReader.CurrentState; + } + + /// + /// Called to read bytes from the stream. + /// + /// The bytes consumed so far. + /// The current . + /// The new , having read my bytes from the stream. + public virtual Utf8JsonReader ReadBytes(int bytesConsumed, JsonReaderState state) + { + Check.DebugAssert(_stream != null, "Only needed when buffer doesn't contain full JSON document."); + + var buffer = _buffer; + var totalConsumed = bytesConsumed + _positionInBuffer; + if (_bytesAvailable != 0 && totalConsumed < buffer.Length) + { + var leftover = buffer.AsSpan(totalConsumed); + + if (leftover.Length == buffer.Length) + { + Array.Resize(ref buffer, buffer.Length * 2); + } + + leftover.CopyTo(buffer); + _bytesAvailable = _stream.Read(buffer.AsSpan(leftover.Length)) + leftover.Length; + } + else + { + _bytesAvailable = _stream.Read(buffer); + } + + _buffer = buffer; + _positionInBuffer = 0; + _readerState = state; + + return CreateReader(); + } + + /// + /// Creates a for the current captured state. + /// + /// The new reader. + public virtual Utf8JsonReader CreateReader() + => new(_buffer.AsSpan(_positionInBuffer), isFinalBlock: _bytesAvailable != _buffer.Length, _readerState); +} diff --git a/src/EFCore/Storage/Json/JsonSByteReaderWriter.cs b/src/EFCore/Storage/Json/JsonSByteReaderWriter.cs new file mode 100644 index 00000000000..5aad0f21656 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonSByteReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonSByteReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonSByteReaderWriter Instance { get; } = new(); + + private JsonSByteReaderWriter() + { + } + + /// + public override sbyte FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetSByte(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, sbyte value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonSignedEnumReaderWriter.cs b/src/EFCore/Storage/Json/JsonSignedEnumReaderWriter.cs new file mode 100644 index 00000000000..e48ad7dc8c8 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonSignedEnumReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values backed by a signed integer. +/// +public sealed class JsonSignedEnumReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonSignedEnumReaderWriter Instance { get; } = new(); + + private JsonSignedEnumReaderWriter() + { + } + + /// + public override TEnum FromJsonTyped(ref Utf8JsonReaderManager manager) + => (TEnum)Convert.ChangeType(manager.CurrentReader.GetInt64(), typeof(TEnum).GetEnumUnderlyingType()); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, TEnum value) + => writer.WriteNumberValue((long)Convert.ChangeType(value, typeof(long))!); +} diff --git a/src/EFCore/Storage/Json/JsonStringReaderWriter.cs b/src/EFCore/Storage/Json/JsonStringReaderWriter.cs new file mode 100644 index 00000000000..01efe26f639 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonStringReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonStringReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonStringReaderWriter Instance { get; } = new(); + + private JsonStringReaderWriter() + { + } + + /// + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonTimeOnlyReaderWriter.cs b/src/EFCore/Storage/Json/JsonTimeOnlyReaderWriter.cs new file mode 100644 index 00000000000..83971665c24 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonTimeOnlyReaderWriter.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonTimeOnlyReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonTimeOnlyReaderWriter Instance { get; } = new(); + + private JsonTimeOnlyReaderWriter() + { + } + + /// + public override TimeOnly FromJsonTyped(ref Utf8JsonReaderManager manager) + => TimeOnly.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, TimeOnly value) + => writer.WriteStringValue(value.ToString("o", CultureInfo.InvariantCulture)); +} diff --git a/src/EFCore/Storage/Json/JsonTimeSpanReaderWriter.cs b/src/EFCore/Storage/Json/JsonTimeSpanReaderWriter.cs new file mode 100644 index 00000000000..8a9731255a4 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonTimeSpanReaderWriter.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonTimeSpanReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonTimeSpanReaderWriter Instance { get; } = new(); + + private JsonTimeSpanReaderWriter() + { + } + + /// + public override TimeSpan FromJsonTyped(ref Utf8JsonReaderManager manager) + => TimeSpan.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, TimeSpan value) + => writer.WriteStringValue(value.ToString("g", CultureInfo.InvariantCulture)); +} diff --git a/src/EFCore/Storage/Json/JsonUInt16ReaderWriter.cs b/src/EFCore/Storage/Json/JsonUInt16ReaderWriter.cs new file mode 100644 index 00000000000..d02c223c134 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonUInt16ReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonUInt16ReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonUInt16ReaderWriter Instance { get; } = new(); + + private JsonUInt16ReaderWriter() + { + } + + /// + public override ushort FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetUInt16(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, ushort value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonUInt32ReaderWriter.cs b/src/EFCore/Storage/Json/JsonUInt32ReaderWriter.cs new file mode 100644 index 00000000000..09a61c82eb1 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonUInt32ReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonUInt32ReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonUInt32ReaderWriter Instance { get; } = new(); + + private JsonUInt32ReaderWriter() + { + } + + /// + public override uint FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetUInt32(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, uint value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonUInt64ReaderWriter.cs b/src/EFCore/Storage/Json/JsonUInt64ReaderWriter.cs new file mode 100644 index 00000000000..11219ac6c30 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonUInt64ReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values. +/// +public sealed class JsonUInt64ReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonUInt64ReaderWriter Instance { get; } = new(); + + private JsonUInt64ReaderWriter() + { + } + + /// + public override ulong FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetUInt64(); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, ulong value) + => writer.WriteNumberValue(value); +} diff --git a/src/EFCore/Storage/Json/JsonUnsignedEnumReaderWriter.cs b/src/EFCore/Storage/Json/JsonUnsignedEnumReaderWriter.cs new file mode 100644 index 00000000000..01f71b27ca6 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonUnsignedEnumReaderWriter.cs @@ -0,0 +1,29 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes JSON for values backed by an unsigned integer. +/// +public sealed class JsonUnsignedEnumReaderWriter : JsonValueReaderWriter +{ + /// + /// The singleton instance of this stateless reader/writer. + /// + public static JsonUnsignedEnumReaderWriter Instance { get; } = new(); + + private JsonUnsignedEnumReaderWriter() + { + } + + /// + public override TEnum FromJsonTyped(ref Utf8JsonReaderManager manager) + => (TEnum)Convert.ChangeType(manager.CurrentReader.GetUInt64(), typeof(TEnum).GetEnumUnderlyingType()); + + /// + public override void ToJsonTyped(Utf8JsonWriter writer, TEnum value) + => writer.WriteNumberValue((ulong)Convert.ChangeType(value, typeof(ulong))!); +} diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs new file mode 100644 index 00000000000..b11023d7478 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs @@ -0,0 +1,55 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes the JSON value for a given model or provider value. +/// +/// +/// Implementations of this type must inherit from the generic +/// +public abstract class JsonValueReaderWriter +{ + /// + /// Ensures the external types extend from the generic + /// + internal JsonValueReaderWriter() + { + } + + /// + /// Reads the value from JSON. + /// + /// + /// + /// The is at the node that contains the value to be read. The value should be read + /// as appropriate from the JSON, and then further converted as necessary. + /// + /// + /// Nulls are handled externally to this reader. That is, this method will never be called if the JSON value is "null". + /// + /// + /// In most cases, the value is represented in the JSON document as a simple property value--e.g. a number, boolean, or string. + /// However, it could be an array or sub-document. In this case, the should be used to parse + /// the JSON as appropriate. + /// + /// + /// The for the JSON being read. + /// The read value. + public abstract object FromJson(ref Utf8JsonReaderManager manager); + + /// + /// Writes the value to JSON. + /// + /// The into which the value should be written. + /// The value to write. + public abstract void ToJson(Utf8JsonWriter writer, object value); + + /// + /// The type of the value being read/written. + /// + public abstract Type ValueType { get; } +} diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriterSource.cs b/src/EFCore/Storage/Json/JsonValueReaderWriterSource.cs new file mode 100644 index 00000000000..046ec7540aa --- /dev/null +++ b/src/EFCore/Storage/Json/JsonValueReaderWriterSource.cs @@ -0,0 +1,158 @@ +// 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.Storage.Json; + +/// +/// +/// Attempts to find a for a given CLR type. +/// +/// +/// +/// +/// The service lifetime is . This means a single instance +/// is used by many instances. The implementation must be thread-safe. +/// This service cannot depend on services registered as . +/// +/// +/// See Implementation of database providers and extensions +/// for more information and examples. +/// +/// +public class JsonValueReaderWriterSource : IJsonValueReaderWriterSource +{ + /// + /// Initializes a new instance of the class. + /// + /// Parameter object containing dependencies for this service. + public JsonValueReaderWriterSource(JsonValueReaderWriterSourceDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual JsonValueReaderWriterSourceDependencies Dependencies { get; } + + /// + public virtual JsonValueReaderWriter? FindReaderWriter(Type type) + { + if (type == typeof(int)) + { + return JsonInt32ReaderWriter.Instance; + } + + if (type == typeof(string)) + { + return JsonStringReaderWriter.Instance; + } + + if (type == typeof(Guid)) + { + return JsonGuidReaderWriter.Instance; + } + + if (type == typeof(bool)) + { + return JsonBoolReaderWriter.Instance; + } + + if (type == typeof(DateTime)) + { + return JsonDateTimeReaderWriter.Instance; + } + + if (type == typeof(DateTimeOffset)) + { + return JsonDateTimeOffsetReaderWriter.Instance; + } + + if (type == typeof(decimal)) + { + return JsonDecimalReaderWriter.Instance; + } + + if (type == typeof(double)) + { + return JsonDoubleReaderWriter.Instance; + } + + if (type == typeof(long)) + { + return JsonInt64ReaderWriter.Instance; + } + + if (type == typeof(DateOnly)) + { + return JsonDateOnlyReaderWriter.Instance; + } + + if (type == typeof(TimeOnly)) + { + return JsonTimeOnlyReaderWriter.Instance; + } + + if (type == typeof(byte[])) + { + return JsonByteArrayReaderWriter.Instance; + } + + if (type == typeof(ulong)) + { + return JsonUInt64ReaderWriter.Instance; + } + + if (type == typeof(uint)) + { + return JsonUInt32ReaderWriter.Instance; + } + + if (type == typeof(byte)) + { + return JsonByteReaderWriter.Instance; + } + + if (type == typeof(char)) + { + return JsonCharReaderWriter.Instance; + } + + if (type == typeof(float)) + { + return JsonFloatReaderWriter.Instance; + } + + if (type == typeof(short)) + { + return JsonInt16ReaderWriter.Instance; + } + + if (type == typeof(sbyte)) + { + return JsonSByteReaderWriter.Instance; + } + + if (type == typeof(ushort)) + { + return JsonUInt16ReaderWriter.Instance; + } + + if (type == typeof(TimeSpan)) + { + return JsonTimeSpanReaderWriter.Instance; + } + + if (type.IsEnum) + { + var readerWriterType = + (type.GetEnumUnderlyingType().IsSignedInteger() + ? typeof(JsonSignedEnumReaderWriter<>) + : typeof(JsonUnsignedEnumReaderWriter<>)) + .MakeGenericType(type); + return (JsonValueReaderWriter?)readerWriterType.GetAnyProperty("Instance")!.GetValue(null); + } + + return null; + } +} diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriterSourceDependencies.cs b/src/EFCore/Storage/Json/JsonValueReaderWriterSourceDependencies.cs new file mode 100644 index 00000000000..2d349d0c5ee --- /dev/null +++ b/src/EFCore/Storage/Json/JsonValueReaderWriterSourceDependencies.cs @@ -0,0 +1,51 @@ +// 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.Storage.Json; + +/// +/// +/// Service dependencies parameter class for +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +/// +/// +/// Do not construct instances of this class directly from either provider or application code as the +/// constructor signature may change as new dependencies are added. Instead, use this type in +/// your constructor so that an instance will be created and injected automatically by the +/// dependency injection container. To create an instance with some dependent services replaced, +/// first resolve the object from the dependency injection container, then replace selected +/// services using the C# 'with' operator. Do not call the constructor at any point in this process. +/// +/// +/// The service lifetime is . +/// This means a single instance of each service is used by many instances. +/// The implementation must be thread-safe. +/// This service cannot depend on services registered as . +/// +/// +public sealed record JsonValueReaderWriterSourceDependencies +{ + /// + /// 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. + /// + /// + /// Do not call this constructor directly from either provider or application code as it may change + /// as new dependencies are added. Instead, use this type in your constructor so that an instance + /// will be created and injected automatically by the dependency injection container. To create + /// an instance with some dependent services replaced, first resolve the object from the dependency + /// injection container, then replace selected services using the C# 'with' operator. Do not call + /// the constructor at any point in this process. + /// + [EntityFrameworkInternal] + public JsonValueReaderWriterSourceDependencies() + { + } +} diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs new file mode 100644 index 00000000000..2fd2c602c69 --- /dev/null +++ b/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs @@ -0,0 +1,52 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Reads and writes the JSON value for a given model or provider value. +/// +public abstract class JsonValueReaderWriter : JsonValueReaderWriter +{ + /// + public sealed override object FromJson(ref Utf8JsonReaderManager manager) + => FromJsonTyped(ref manager)!; + + /// + public sealed override void ToJson(Utf8JsonWriter writer, object value) + => ToJsonTyped(writer, (TValue)value!); + + /// + public sealed override Type ValueType + => typeof(TValue); + + /// + /// Reads the value from JSON. + /// + /// + /// + /// The is at the node that contains the value to be read. The value should be read + /// as appropriate from the JSON, and then further converted as necessary. + /// + /// + /// Nulls are handled externally to this reader. That is, this method will never be called if the JSON value is "null". + /// + /// + /// In most cases, the value is represented in the JSON document as a simple property value--e.g. a number, boolean, or string. + /// However, it could be an array or sub-document. In this case, the should be used to parse + /// the JSON as appropriate. + /// + /// + /// The for the JSON being read. + /// The read value. + public abstract TValue FromJsonTyped(ref Utf8JsonReaderManager manager); + + /// + /// Writes the value to JSON. + /// + /// The into which the value should be written. + /// The value to write. + public abstract void ToJsonTyped(Utf8JsonWriter writer, TValue value); +} diff --git a/src/EFCore/Storage/Json/Utf8JsonReaderManager.cs b/src/EFCore/Storage/Json/Utf8JsonReaderManager.cs new file mode 100644 index 00000000000..b2f655c9ab5 --- /dev/null +++ b/src/EFCore/Storage/Json/Utf8JsonReaderManager.cs @@ -0,0 +1,59 @@ +// 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.Json; + +namespace Microsoft.EntityFrameworkCore.Storage.Json; + +/// +/// Manages buffering underneath a . +/// +/// +/// The consumer should call to advance to the next token in the JSON document, which may involve reading +/// more data from the stream and creating a new instance in . +/// +public ref struct Utf8JsonReaderManager +{ + /// + /// Tracks state and underlying stream or buffer of UTF8 bytes. + /// + public readonly JsonReaderData Data; + + /// + /// The set to the next token to be consumed. + /// + public Utf8JsonReader CurrentReader; + + /// + /// Creates a new instance that will start reading at the position in the JSON document + /// captured in the given + /// + /// The data. + public Utf8JsonReaderManager(JsonReaderData data) + { + Data = data; + CurrentReader = data.CreateReader(); + } + + /// + /// Moves to the next token, which may involve reading more data from the stream and creating a new + /// instance in . + /// + /// The token type of the current token. + public JsonTokenType MoveNext() + { + while (!CurrentReader.Read()) + { + CurrentReader = Data.ReadBytes((int)CurrentReader.BytesConsumed, CurrentReader.CurrentState); + } + + return CurrentReader.TokenType; + } + + /// + /// Called to capture the state of this into the associated so + /// that a new can later be created to pick up at the same position in the JSON document. + /// + public void CaptureState() + => Data.CaptureState(ref this); +} diff --git a/src/EFCore/Storage/TypeMappingSourceDependencies.cs b/src/EFCore/Storage/TypeMappingSourceDependencies.cs index fe998cced35..48b287ad437 100644 --- a/src/EFCore/Storage/TypeMappingSourceDependencies.cs +++ b/src/EFCore/Storage/TypeMappingSourceDependencies.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Storage.Json; + namespace Microsoft.EntityFrameworkCore.Storage; /// @@ -47,9 +49,11 @@ public sealed record TypeMappingSourceDependencies [EntityFrameworkInternal] public TypeMappingSourceDependencies( IValueConverterSelector valueConverterSelector, + IJsonValueReaderWriterSource jsonValueReaderWriterSource, IEnumerable plugins) { ValueConverterSelector = valueConverterSelector; + JsonValueReaderWriterSource = jsonValueReaderWriterSource; Plugins = plugins; } @@ -58,6 +62,11 @@ public TypeMappingSourceDependencies( /// public IValueConverterSelector ValueConverterSelector { get; init; } + /// + /// Used to find the for a type. + /// + public IJsonValueReaderWriterSource JsonValueReaderWriterSource { get; } + /// /// Gets the plugins. /// diff --git a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs new file mode 100644 index 00000000000..6f0bdd3644e --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs @@ -0,0 +1,68 @@ +// 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.Cosmos; + +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()); + + 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()); + + 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()); + + 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()); + + 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()); + + 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()); + + 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()); + + 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()); + + 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()); + + 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 class JsonTypesCosmosFixture : JsonTypesFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Ignore(); + modelBuilder.Ignore(); + } + } +} diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index 2a639d81564..8bdec2f1f41 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -4,6 +4,7 @@ using System.Collections; using System.ComponentModel; using System.Data; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; @@ -15,6 +16,7 @@ using Microsoft.EntityFrameworkCore.Sqlite.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; using NetTopologySuite; using NetTopologySuite.Geometries; @@ -65,7 +67,7 @@ public void Empty_model() code, c => AssertFileContents( "EmptyContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -99,7 +101,7 @@ static EmptyContextModel() c), c => AssertFileContents( "EmptyContextModelBuilder.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -466,7 +468,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity( "MyEntity", e => { - e.Property("Id").Metadata.SetTypeMapping(new InMemoryTypeMapping(typeof(int))); + e.Property("Id").Metadata.SetTypeMapping( + new InMemoryTypeMapping(typeof(int), jsonValueReaderWriter: JsonInt32ReaderWriter.Instance)); e.HasKey("Id"); }); } @@ -547,7 +550,7 @@ public void Fully_qualified_model() code, c => AssertFileContents( "DbContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -581,7 +584,7 @@ static DbContextModel() c), c => AssertFileContents( "DbContextModelBuilder.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -612,7 +615,7 @@ partial void Initialize() c), c => AssertFileContents( "IndexEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -663,7 +666,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) c => AssertFileContents( "InternalEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -713,7 +716,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) c), c => AssertFileContents( "IdentityUserEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -869,7 +872,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) c => AssertFileContents( "IdentityUser0EntityType.cs", -""" + """ // using System; using System.Reflection; @@ -944,7 +947,7 @@ public void BigModel() code, c => AssertFileContents( "BigContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -977,7 +980,7 @@ static BigContextModel() """, c), c => AssertFileContents( "BigContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -1633,7 +1636,7 @@ private IRelationalModel CreateRelationalModel() """, c), c => AssertFileContents( "DependentBaseEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -1760,7 +1763,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalBaseEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -1768,6 +1771,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using NetTopologySuite.Geometries; @@ -1811,6 +1815,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField("AlternateId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.FieldDuringConstruction, afterSaveBehavior: PropertySaveBehavior.Throw, + jsonValueReaderWriter: JsonGuidReaderWriter.Instance, sentinel: new Guid("00000000-0000-0000-0000-000000000000")); alternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); @@ -1916,7 +1921,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "OwnedTypeEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -2069,7 +2074,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "OwnedType0EntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -2181,7 +2186,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalBasePrincipalDerivedDependentBasebyteEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -2295,7 +2300,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "DependentDerivedEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -2359,7 +2364,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalDerivedEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -2903,7 +2908,7 @@ public void BigModel_with_JSON_columns() code, c => AssertFileContents( "BigContextWithJsonModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -2936,7 +2941,7 @@ static BigContextWithJsonModel() """, c), c => AssertFileContents( "BigContextWithJsonModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -3433,7 +3438,7 @@ private IRelationalModel CreateRelationalModel() """, c), c => AssertFileContents( "DependentBaseEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -3560,7 +3565,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalBaseEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -3602,6 +3607,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? ba fieldInfo: typeof(CSharpRuntimeModelCodeGeneratorTest.PrincipalBase).GetField("AlternateId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), propertyAccessMode: PropertyAccessMode.FieldDuringConstruction, afterSaveBehavior: PropertySaveBehavior.Throw, + jsonValueReaderWriter: new CSharpRuntimeModelCodeGeneratorTest.MyJsonGuidReaderWriter(), sentinel: new Guid("00000000-0000-0000-0000-000000000000")); alternateId.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); @@ -3715,7 +3721,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "OwnedTypeEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -3827,7 +3833,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "OwnedType0EntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -3939,7 +3945,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalBasePrincipalDerivedDependentBasebyteEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -4053,7 +4059,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "DependentDerivedEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -4117,7 +4123,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalDerivedEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -4679,6 +4685,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.HasAlternateKey(e => e.Id); + eb.Property(e => e.AlternateId).Metadata.SetJsonValueReaderWriterType( + _jsonColumns + ? typeof(MyJsonGuidReaderWriter) + : typeof(JsonGuidReaderWriter)); + eb.OwnsOne( e => e.Owned, ob => { @@ -4713,7 +4724,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.Navigation(e => e.Owned).IsRequired().HasField("_ownedField") .UsePropertyAccessMode(PropertyAccessMode.Field); - if (!_jsonColumns) { eb.HasData(new PrincipalBase { Id = 1, AlternateId = new Guid() }); @@ -4731,7 +4741,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) eb.Navigation(e => e.Dependent).AutoInclude().EnableLazyLoading(false); - eb.OwnsMany(typeof(OwnedType).FullName, "ManyOwned", ob => + eb.OwnsMany( + typeof(OwnedType).FullName, "ManyOwned", ob => { if (_jsonColumns) { @@ -4809,7 +4820,7 @@ public void TPC_model() code, c => AssertFileContents( "TpcContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -4842,7 +4853,7 @@ static TpcContextModel() """, c), c => AssertFileContents( "TpcContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -5464,7 +5475,7 @@ private IRelationalModel CreateRelationalModel() """, c), c => AssertFileContents( "DependentBaseEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -5555,7 +5566,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalBaseEntityType.cs", -""" + """ // using System; using System.Collections.Generic; @@ -5773,7 +5784,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) """, c), c => AssertFileContents( "PrincipalDerivedEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -5934,7 +5945,8 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("PrincipalBase_Insert", insertSproc.Name); Assert.Equal("TPC", insertSproc.Schema); Assert.Equal( - new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2", "Id" }, insertSproc.Parameters.Select(p => p.PropertyName)); + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2", "Id" }, + insertSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(insertSproc.ResultColumns); Assert.False(insertSproc.IsRowsAffectedReturned); Assert.Equal("bar1", insertSproc["foo"]); @@ -5947,7 +5959,8 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("PrincipalBase_Update", updateSproc.Name); Assert.Equal("TPC", updateSproc.Schema); Assert.Equal( - new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2", "Id" }, + updateSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(updateSproc.ResultColumns); Assert.False(updateSproc.IsRowsAffectedReturned); Assert.Empty(updateSproc.GetAnnotations()); @@ -5987,7 +6000,9 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) insertSproc = principalDerived.GetInsertStoredProcedure()!; Assert.Equal("Derived_Insert", insertSproc.Name); Assert.Equal("TPC", insertSproc.Schema); - Assert.Equal(new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2" }, insertSproc.Parameters.Select(p => p.PropertyName)); + Assert.Equal( + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2" }, + insertSproc.Parameters.Select(p => p.PropertyName)); Assert.Equal(new[] { "Id" }, insertSproc.ResultColumns.Select(p => p.PropertyName)); Assert.Null(insertSproc["foo"]); Assert.Same(principalDerived, insertSproc.EntityType); @@ -6004,7 +6019,8 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) Assert.Equal("Derived_Update", updateSproc.Name); Assert.Equal("Derived", updateSproc.Schema); Assert.Equal( - new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2", "Id" }, updateSproc.Parameters.Select(p => p.PropertyName)); + new[] { "PrincipalBaseId", "PrincipalDerivedId", "Enum1", "Enum2", "FlagsEnum1", "FlagsEnum2", "Id" }, + updateSproc.Parameters.Select(p => p.PropertyName)); Assert.Empty(updateSproc.ResultColumns); Assert.Empty(updateSproc.GetAnnotations()); Assert.Same(principalDerived, updateSproc.EntityType); @@ -6196,6 +6212,15 @@ public enum AFlagsEnum C = 4, } + public sealed class MyJsonGuidReaderWriter : JsonValueReaderWriter + { + public override Guid FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetGuid(); + + public override void ToJsonTyped(Utf8JsonWriter writer, Guid value) + => writer.WriteStringValue(value); + } + public class PrincipalBase : AbstractBase { public new long? Id { get; set; } @@ -6276,7 +6301,7 @@ public void DbFunctions() code, c => AssertFileContents( "DbFunctionContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -6310,7 +6335,7 @@ static DbFunctionContextModel() c), c => AssertFileContents( "DbFunctionContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -6544,7 +6569,7 @@ private IRelationalModel CreateRelationalModel() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -6595,7 +6620,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) c), c => AssertFileContents( "ObjectEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -6863,7 +6888,7 @@ public void Sequences() code, c => AssertFileContents( "SequencesContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -6897,7 +6922,7 @@ static SequencesContextModel() c), c => AssertFileContents( "SequencesContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -7002,7 +7027,7 @@ private IRelationalModel CreateRelationalModel() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -7126,7 +7151,7 @@ public void Key_sequences() code, c => AssertFileContents( "KeySequencesContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -7160,7 +7185,7 @@ static KeySequencesContextModel() c), c => AssertFileContents( "KeySequencesContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -7252,7 +7277,7 @@ private IRelationalModel CreateRelationalModel() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -7360,7 +7385,7 @@ public void CheckConstraints() code, c => AssertFileContents( "ConstraintsContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -7394,7 +7419,7 @@ static ConstraintsContextModel() c), c => AssertFileContents( "ConstraintsContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -7476,7 +7501,7 @@ private IRelationalModel CreateRelationalModel() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -7573,7 +7598,7 @@ public void Triggers() code, c => AssertFileContents( "TriggersContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -7607,7 +7632,7 @@ static TriggersContextModel() c), c => AssertFileContents( "TriggersContextModelBuilder.cs", -""" + """ // using System; using System.Collections.Generic; @@ -7691,7 +7716,7 @@ private IRelationalModel CreateRelationalModel() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -7799,7 +7824,7 @@ public void Sqlite() code, c => AssertFileContents( "SqliteContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -7832,7 +7857,7 @@ static SqliteContextModel() c), c => AssertFileContents( "SqliteContextModelBuilder.cs", -""" + """ // using System.Collections.Generic; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -7921,7 +7946,7 @@ private IRelationalModel CreateRelationalModel() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -8045,7 +8070,7 @@ public void Cosmos() code, c => AssertFileContents( "CosmosContextModel.cs", -""" + """ // using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -8079,7 +8104,7 @@ static CosmosContextModel() c), c => AssertFileContents( "CosmosContextModelBuilder.cs", -""" + """ // using System; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -8106,7 +8131,7 @@ partial void Initialize() c), c => AssertFileContents( "DataEntityType.cs", -""" + """ // using System; using System.Reflection; @@ -8426,9 +8451,10 @@ protected void Test( var assembly = build.BuildInMemory(); var modelTypeName = options.ContextType.Name + "Model"; - var modelType = assembly.GetType(string.IsNullOrEmpty(options.ModelNamespace) - ? modelTypeName - : options.ModelNamespace + "." + modelTypeName); + var modelType = assembly.GetType( + string.IsNullOrEmpty(options.ModelNamespace) + ? modelTypeName + : options.ModelNamespace + "." + modelTypeName); var instancePropertyInfo = modelType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static); var compiledModel = (IModel)instancePropertyInfo.GetValue(null); diff --git a/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs new file mode 100644 index 00000000000..7a04e6d82f6 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/JsonTypesInMemoryTest.cs @@ -0,0 +1,40 @@ +// 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 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_line_string() + // No built-in JSON support for spatial types in the in-memory provider + => 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 in-memory provider + => 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 in-memory provider + => 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 in-memory provider + => Assert.Throws(() => base.Can_read_write_polygon_typed_as_geometry()); + + public class JsonTypesInMemoryFixture : JsonTypesFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.Relational.Tests/Storage/RelationalGeometryTypeMappingTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalGeometryTypeMappingTest.cs index 6384c7a1387..42a903c43ca 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalGeometryTypeMappingTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalGeometryTypeMappingTest.cs @@ -20,7 +20,7 @@ public void Comparer_uses_exact_comparison() private class FakeRelationalGeometryTypeMapping : RelationalGeometryTypeMapping { public FakeRelationalGeometryTypeMapping() - : base(new NullValueConverter(), "geometry") + : base(new NullValueConverter(), null, "geometry") { } diff --git a/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj b/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj index 44b1d53b5d8..93be1790334 100644 --- a/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj +++ b/test/EFCore.Specification.Tests/EFCore.Specification.Tests.csproj @@ -49,6 +49,7 @@ + diff --git a/test/EFCore.Specification.Tests/JsonTypesTestBase.cs b/test/EFCore.Specification.Tests/JsonTypesTestBase.cs new file mode 100644 index 00000000000..8cfa853c098 --- /dev/null +++ b/test/EFCore.Specification.Tests/JsonTypesTestBase.cs @@ -0,0 +1,1705 @@ +// 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.Globalization; +using System.Net; +using System.Net.NetworkInformation; +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; +using NetTopologySuite; +using NetTopologySuite.Geometries; +using NetTopologySuite.IO; +using Newtonsoft.Json; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class JsonTypesTestBase : IClassFixture + where TFixture : JsonTypesTestBase.JsonTypesFixtureBase, new() +{ + 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(PrimitiveTypes.Int8)), value, json); + + [ConditionalTheory] + [InlineData(short.MinValue, "{\"Prop\":-32768}")] + [InlineData(short.MaxValue, "{\"Prop\":32767}")] + [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(PrimitiveTypes.Int16)), value, json); + + [ConditionalTheory] + [InlineData(int.MinValue, "{\"Prop\":-2147483648}")] + [InlineData(int.MaxValue, "{\"Prop\":2147483647}")] + [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(PrimitiveTypes.Int32)), value, json); + + [ConditionalTheory] + [InlineData(long.MinValue, "{\"Prop\":-9223372036854775808}")] + [InlineData(long.MaxValue, "{\"Prop\":9223372036854775807}")] + [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(PrimitiveTypes.Int64)), value, json); + + [ConditionalTheory] + [InlineData(byte.MinValue, "{\"Prop\":0}")] + [InlineData(byte.MaxValue, "{\"Prop\":255}")] + [InlineData((byte)0, "{\"Prop\":0}")] + [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(PrimitiveTypes.UInt8)), value, json); + + [ConditionalTheory] + [InlineData(ushort.MinValue, "{\"Prop\":0}")] + [InlineData(ushort.MaxValue, "{\"Prop\":65535}")] + [InlineData((ushort)0, "{\"Prop\":0}")] + [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(PrimitiveTypes.UInt16)), value, json); + + [ConditionalTheory] + [InlineData(uint.MinValue, "{\"Prop\":0}")] + [InlineData(uint.MaxValue, "{\"Prop\":4294967295}")] + [InlineData((uint)0, "{\"Prop\":0}")] + [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(PrimitiveTypes.UInt32)), value, json); + + [ConditionalTheory] + [InlineData(ulong.MinValue, "{\"Prop\":0}")] + [InlineData(ulong.MaxValue, "{\"Prop\":18446744073709551615}")] + [InlineData((ulong)0, "{\"Prop\":0}")] + [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(PrimitiveTypes.UInt64)), value, json); + + [ConditionalTheory] + [InlineData(float.MinValue, "{\"Prop\":-3.4028235E+38}")] + [InlineData(float.MaxValue, "{\"Prop\":3.4028235E+38}")] + [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(PrimitiveTypes.Float)), value, json); + + [ConditionalTheory] + [InlineData(double.MinValue, "{\"Prop\":-1.7976931348623157E+308}")] + [InlineData(double.MaxValue, "{\"Prop\":1.7976931348623157E+308}")] + [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(PrimitiveTypes.Double)), value, json); + + [ConditionalTheory] + [InlineData("-79228162514264337593543950335", "{\"Prop\":-79228162514264337593543950335}")] + [InlineData("79228162514264337593543950335", "{\"Prop\":79228162514264337593543950335}")] + [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(PrimitiveTypes.Decimal)), value, json); + + [ConditionalTheory] + [InlineData("1/1/0001", "{\"Prop\":\"0001-01-01\"}")] + [InlineData("12/31/9999", "{\"Prop\":\"9999-12-31\"}")] + [InlineData("1/1/0001", "{\"Prop\":\"0001-01-01\"}")] + [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(PrimitiveTypes.DateOnly)), + DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("00:00:00.0000000", "{\"Prop\":\"00:00:00.0000000\"}")] + [InlineData("23:59:59.9999999", "{\"Prop\":\"23:59:59.9999999\"}")] + [InlineData("00:00:00.0000000", "{\"Prop\":\"00:00:00.0000000\"}")] + [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(PrimitiveTypes.TimeOnly)), + TimeOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [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("0001-01-01T00:00:00.0000000", "{\"Prop\":\"0001-01-01T00:00:00\"}")] + [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(PrimitiveTypes.DateTime)), + DateTime.Parse(value, CultureInfo.InvariantCulture), json); + + [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(PrimitiveTypes.DateTimeOffset)), + DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + + [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(PrimitiveTypes.TimeSpan)), TimeSpan.Parse(value), json); + + [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(PrimitiveTypes.Boolean)), value, json); + + [ConditionalTheory] + [InlineData(char.MinValue, "{\"Prop\":\"\\u0000\"}")] + [InlineData(char.MaxValue, "{\"Prop\":\"\\uFFFF\"}")] + [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(PrimitiveTypes.Character)), value, json); + + [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("00000000-0000-0000-0000-000000000000", "{\"Prop\":\"00000000-0000-0000-0000-000000000000\"}")] + [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(PrimitiveTypes.Guid)), value, json); + + [ConditionalTheory] + [InlineData("MinValue", "{\"Prop\":\"MinValue\"}")] + [InlineData("MaxValue", "{\"Prop\":\"MaxValue\"}")] + [InlineData("", "{\"Prop\":\"\"}")] + [InlineData( + "❤❥웃유♋☮✌☏☢☠✔☑♚▲♪฿Ɖ⛏♥❣♂♀☿👍✍✉☣☤✘☒♛▼♫⌘⌛¡♡ღツ☼☁❅♾️✎©®™Σ✪✯☭➳Ⓐ✞℃℉°✿⚡☃☂✄¢€£∞✫★½☯✡☪", + @"{""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(PrimitiveTypes.String)), value, json); + + [ConditionalTheory] + [InlineData("0,0,0,1", "{\"Prop\":\"AAAAAQ==\"}")] + [InlineData("255,255,255,255", "{\"Prop\":\"/////w==\"}")] + [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(PrimitiveTypes.Bytes)), + value == "" ? Array.Empty() : value.Split(',').Select(e => byte.Parse(e)).ToArray(), json); + + [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&q2=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(PrimitiveTypes.Uri)), new Uri(value), json); + + [ConditionalTheory] + [InlineData("127.0.0.1", "{\"Prop\":\"127.0.0.1\"}")] + [InlineData("0.0.0.0", "{\"Prop\":\"0.0.0.0\"}")] + [InlineData("255.255.255.255", "{\"Prop\":\"255.255.255.255\"}")] + [InlineData("192.168.1.156", "{\"Prop\":\"192.168.1.156\"}")] + [InlineData("::1", "{\"Prop\":\"::1\"}")] + [InlineData("::", "{\"Prop\":\"::\"}")] + [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(PrimitiveTypes.IpAddress)), IPAddress.Parse(value), json); + + [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(PrimitiveTypes.PhysicalAddress)), PhysicalAddress.Parse(value), json); + + [ConditionalTheory] + [InlineData((sbyte)Enum8.Min, "{\"Prop\":-128}")] + [InlineData((sbyte)Enum8.Max, "{\"Prop\":127}")] + [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(PrimitiveTypes.Enum8)), value, json); + + [ConditionalTheory] + [InlineData((short)Enum16.Min, "{\"Prop\":-32768}")] + [InlineData((short)Enum16.Max, "{\"Prop\":32767}")] + [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(PrimitiveTypes.Enum16)), value, json); + + [ConditionalTheory] + [InlineData((int)Enum32.Min, "{\"Prop\":-2147483648}")] + [InlineData((int)Enum32.Max, "{\"Prop\":2147483647}")] + [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(PrimitiveTypes.Enum32)), value, json); + + [ConditionalTheory] + [InlineData((long)Enum64.Min, "{\"Prop\":-9223372036854775808}")] + [InlineData((long)Enum64.Max, "{\"Prop\":9223372036854775807}")] + [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(PrimitiveTypes.Enum64)), value, json); + + [ConditionalTheory] + [InlineData((byte)EnumU8.Min, "{\"Prop\":0}")] + [InlineData((byte)EnumU8.Max, "{\"Prop\":255}")] + [InlineData((byte)EnumU8.Default, "{\"Prop\":0}")] + [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(PrimitiveTypes.EnumU8)), value, json); + + [ConditionalTheory] + [InlineData((ushort)EnumU16.Min, "{\"Prop\":0}")] + [InlineData((ushort)EnumU16.Max, "{\"Prop\":65535}")] + [InlineData((ushort)EnumU16.Default, "{\"Prop\":0}")] + [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(PrimitiveTypes.EnumU16)), value, json); + + [ConditionalTheory] + [InlineData((uint)EnumU32.Min, "{\"Prop\":0}")] + [InlineData((uint)EnumU32.Max, "{\"Prop\":4294967295}")] + [InlineData((uint)EnumU32.Default, "{\"Prop\":0}")] + [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(PrimitiveTypes.EnumU32)), value, json); + + [ConditionalTheory] + [InlineData((ulong)EnumU64.Min, "{\"Prop\":0}")] + [InlineData((ulong)EnumU64.Max, "{\"Prop\":18446744073709551615}")] + [InlineData((ulong)EnumU64.Default, "{\"Prop\":0}")] + [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(PrimitiveTypes.EnumU64)), value, json); + + [ConditionalTheory] + [InlineData(sbyte.MinValue, "{\"Prop\":-128}")] + [InlineData(sbyte.MaxValue, "{\"Prop\":127}")] + [InlineData((sbyte)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Int8)), value, json); + + [ConditionalTheory] + [InlineData(short.MinValue, "{\"Prop\":-32768}")] + [InlineData(short.MaxValue, "{\"Prop\":32767}")] + [InlineData((short)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Int16)), value, json); + + [ConditionalTheory] + [InlineData(int.MinValue, "{\"Prop\":-2147483648}")] + [InlineData(int.MaxValue, "{\"Prop\":2147483647}")] + [InlineData(0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Int32)), value, json); + + [ConditionalTheory] + [InlineData(long.MinValue, "{\"Prop\":-9223372036854775808}")] + [InlineData(long.MaxValue, "{\"Prop\":9223372036854775807}")] + [InlineData((long)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Int64)), value, json); + + [ConditionalTheory] + [InlineData(byte.MinValue, "{\"Prop\":0}")] + [InlineData(byte.MaxValue, "{\"Prop\":255}")] + [InlineData((byte)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.UInt8)), value, json); + + [ConditionalTheory] + [InlineData(ushort.MinValue, "{\"Prop\":0}")] + [InlineData(ushort.MaxValue, "{\"Prop\":65535}")] + [InlineData((ushort)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.UInt16)), value, json); + + [ConditionalTheory] + [InlineData(uint.MinValue, "{\"Prop\":0}")] + [InlineData(uint.MaxValue, "{\"Prop\":4294967295}")] + [InlineData((uint)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.UInt32)), value, json); + + [ConditionalTheory] + [InlineData(ulong.MinValue, "{\"Prop\":0}")] + [InlineData(ulong.MaxValue, "{\"Prop\":18446744073709551615}")] + [InlineData((ulong)0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.UInt64)), value, json); + + [ConditionalTheory] + [InlineData(float.MinValue, "{\"Prop\":-3.4028235E+38}")] + [InlineData(float.MaxValue, "{\"Prop\":3.4028235E+38}")] + [InlineData((float)0.0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Float)), value, json); + + [ConditionalTheory] + [InlineData(double.MinValue, "{\"Prop\":-1.7976931348623157E+308}")] + [InlineData(double.MaxValue, "{\"Prop\":1.7976931348623157E+308}")] + [InlineData(0.0, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Double)), value, json); + + [ConditionalTheory] + [InlineData("-79228162514264337593543950335", "{\"Prop\":-79228162514264337593543950335}")] + [InlineData("79228162514264337593543950335", "{\"Prop\":79228162514264337593543950335}")] + [InlineData("0.0", "{\"Prop\":0.0}")] + [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(NullablePrimitiveTypes.Decimal)), + value == null ? default(decimal?) : decimal.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("1/1/0001", "{\"Prop\":\"0001-01-01\"}")] + [InlineData("12/31/9999", "{\"Prop\":\"9999-12-31\"}")] + [InlineData("1/1/0001", "{\"Prop\":\"0001-01-01\"}")] + [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(NullablePrimitiveTypes.DateOnly)), + value == null ? default(DateOnly?) : DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("00:00:00.0000000", "{\"Prop\":\"00:00:00.0000000\"}")] + [InlineData("23:59:59.9999999", "{\"Prop\":\"23:59:59.9999999\"}")] + [InlineData("00:00:00.0000000", "{\"Prop\":\"00:00:00.0000000\"}")] + [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(NullablePrimitiveTypes.TimeOnly)), + value == null ? default(TimeOnly?) : TimeOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [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("0001-01-01T00:00:00.0000000", "{\"Prop\":\"0001-01-01T00:00:00\"}")] + [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(NullablePrimitiveTypes.DateTime)), + value == null ? default(DateTime?) : DateTime.Parse(value, CultureInfo.InvariantCulture), json); + + [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\"}")] + [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(NullablePrimitiveTypes.DateTimeOffset)), + value == null ? default(DateTimeOffset?) : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + + [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\"}")] + [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(NullablePrimitiveTypes.TimeSpan)), + value == null ? default(TimeSpan?) : TimeSpan.Parse(value), json); + + [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(NullablePrimitiveTypes.Boolean)), value, json); + + [ConditionalTheory] + [InlineData(char.MinValue, "{\"Prop\":\"\\u0000\"}")] + [InlineData(char.MaxValue, "{\"Prop\":\"\\uFFFF\"}")] + [InlineData(' ', "{\"Prop\":\" \"}")] + [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(NullablePrimitiveTypes.Character)), value, json); + + [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("00000000-0000-0000-0000-000000000000", "{\"Prop\":\"00000000-0000-0000-0000-000000000000\"}")] + [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(NullablePrimitiveTypes.Guid)), + value == null ? default(Guid?) : Guid.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("MinValue", "{\"Prop\":\"MinValue\"}")] + [InlineData("MaxValue", "{\"Prop\":\"MaxValue\"}")] + [InlineData("", "{\"Prop\":\"\"}")] + [InlineData( + "❤❥웃유♋☮✌☏☢☠✔☑♚▲♪฿Ɖ⛏♥❣♂♀☿👍✍✉☣☤✘☒♛▼♫⌘⌛¡♡ღツ☼☁❅♾️✎©®™Σ✪✯☭➳Ⓐ✞℃℉°✿⚡☃☂✄¢€£∞✫★½☯✡☪", + @"{""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(NullablePrimitiveTypes.String)), value, json); + + [ConditionalTheory] + [InlineData("0,0,0,1", "{\"Prop\":\"AAAAAQ==\"}")] + [InlineData("255,255,255,255", "{\"Prop\":\"/////w==\"}")] + [InlineData("", "{\"Prop\":\"\"}")] + [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(NullablePrimitiveTypes.Bytes)), + value == null + ? default + : value == "" + ? Array.Empty() + : value.Split(',').Select(e => byte.Parse(e)).ToArray(), json); + + [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&q2=v2#FragmentName\"}")] + [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(NullablePrimitiveTypes.Uri)), + value == null ? default : new Uri(value), json); + + [ConditionalTheory] + [InlineData("127.0.0.1", "{\"Prop\":\"127.0.0.1\"}")] + [InlineData("0.0.0.0", "{\"Prop\":\"0.0.0.0\"}")] + [InlineData("255.255.255.255", "{\"Prop\":\"255.255.255.255\"}")] + [InlineData("192.168.1.156", "{\"Prop\":\"192.168.1.156\"}")] + [InlineData("::1", "{\"Prop\":\"::1\"}")] + [InlineData("::", "{\"Prop\":\"::\"}")] + [InlineData("::", "{\"Prop\":\"::\"}")] + [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(NullablePrimitiveTypes.IpAddress)), + value == null ? default : IPAddress.Parse(value), json); + + [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(NullablePrimitiveTypes.PhysicalAddress)), + value == null ? default : PhysicalAddress.Parse(value), json); + + [ConditionalTheory] + [InlineData((sbyte)Enum8.Min, "{\"Prop\":-128}")] + [InlineData((sbyte)Enum8.Max, "{\"Prop\":127}")] + [InlineData((sbyte)Enum8.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Enum8)), + value == null ? default(Enum8?) : (Enum8)value, json); + + [ConditionalTheory] + [InlineData((short)Enum16.Min, "{\"Prop\":-32768}")] + [InlineData((short)Enum16.Max, "{\"Prop\":32767}")] + [InlineData((short)Enum16.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Enum16)), + value == null ? default(Enum16?) : (Enum16)value, json); + + [ConditionalTheory] + [InlineData((int)Enum32.Min, "{\"Prop\":-2147483648}")] + [InlineData((int)Enum32.Max, "{\"Prop\":2147483647}")] + [InlineData((int)Enum32.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Enum32)), + value == null ? default(Enum32?) : (Enum32)value, json); + + [ConditionalTheory] + [InlineData((long)Enum64.Min, "{\"Prop\":-9223372036854775808}")] + [InlineData((long)Enum64.Max, "{\"Prop\":9223372036854775807}")] + [InlineData((long)Enum64.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.Enum64)), + value == null ? default(Enum64?) : (Enum64)value, json); + + [ConditionalTheory] + [InlineData((byte)EnumU8.Min, "{\"Prop\":0}")] + [InlineData((byte)EnumU8.Max, "{\"Prop\":255}")] + [InlineData((byte)EnumU8.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.EnumU8)), + value == null ? default(EnumU8?) : (EnumU8)value, json); + + [ConditionalTheory] + [InlineData((ushort)EnumU16.Min, "{\"Prop\":0}")] + [InlineData((ushort)EnumU16.Max, "{\"Prop\":65535}")] + [InlineData((ushort)EnumU16.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.EnumU16)), + value == null ? default(EnumU16?) : (EnumU16)value, json); + + [ConditionalTheory] + [InlineData((uint)EnumU32.Min, "{\"Prop\":0}")] + [InlineData((uint)EnumU32.Max, "{\"Prop\":4294967295}")] + [InlineData((uint)EnumU32.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.EnumU32)), + value == null ? default(EnumU32?) : (EnumU32)value, json); + + [ConditionalTheory] + [InlineData((ulong)EnumU64.Min, "{\"Prop\":0}")] + [InlineData((ulong)EnumU64.Max, "{\"Prop\":18446744073709551615}")] + [InlineData((ulong)EnumU64.Default, "{\"Prop\":0}")] + [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(NullablePrimitiveTypes.EnumU64)), + value == null ? default(EnumU64?) : (EnumU64)value, json); + + [ConditionalTheory] + [InlineData(sbyte.MinValue, "{\"Prop\":\"-128\"}")] + [InlineData(sbyte.MaxValue, "{\"Prop\":\"127\"}")] + [InlineData((sbyte)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Int8)), value, json); + + [ConditionalTheory] + [InlineData(short.MinValue, "{\"Prop\":\"-32768\"}")] + [InlineData(short.MaxValue, "{\"Prop\":\"32767\"}")] + [InlineData((short)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Int16)), value, json); + + [ConditionalTheory] + [InlineData(int.MinValue, "{\"Prop\":\"-2147483648\"}")] + [InlineData(int.MaxValue, "{\"Prop\":\"2147483647\"}")] + [InlineData(0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Int32)), value, json); + + [ConditionalTheory] + [InlineData(long.MinValue, "{\"Prop\":\"-9223372036854775808\"}")] + [InlineData(long.MaxValue, "{\"Prop\":\"9223372036854775807\"}")] + [InlineData((long)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Int64)), value, json); + + [ConditionalTheory] + [InlineData(byte.MinValue, "{\"Prop\":\"0\"}")] + [InlineData(byte.MaxValue, "{\"Prop\":\"255\"}")] + [InlineData((byte)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.UInt8)), value, json); + + [ConditionalTheory] + [InlineData(ushort.MinValue, "{\"Prop\":\"0\"}")] + [InlineData(ushort.MaxValue, "{\"Prop\":\"65535\"}")] + [InlineData((ushort)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.UInt16)), value, json); + + [ConditionalTheory] + [InlineData(uint.MinValue, "{\"Prop\":\"0\"}")] + [InlineData(uint.MaxValue, "{\"Prop\":\"4294967295\"}")] + [InlineData((uint)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.UInt32)), value, json); + + [ConditionalTheory] + [InlineData(ulong.MinValue, "{\"Prop\":\"0\"}")] + [InlineData(ulong.MaxValue, "{\"Prop\":\"18446744073709551615\"}")] + [InlineData((ulong)0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.UInt64)), value, json); + + [ConditionalTheory] + [InlineData(float.MinValue, "{\"Prop\":\"-3.4028235E+38\"}")] + [InlineData(float.MaxValue, "{\"Prop\":\"3.4028235E+38\"}")] + [InlineData((float)0.0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Float)), value, json); + + [ConditionalTheory] + [InlineData(double.MinValue, "{\"Prop\":\"-1.7976931348623157E+308\"}")] + [InlineData(double.MaxValue, "{\"Prop\":\"1.7976931348623157E+308\"}")] + [InlineData(0.0, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Double)), value, json); + + [ConditionalTheory] + [InlineData("-79228162514264337593543950335", "{\"Prop\":\"-79228162514264337593543950335\"}")] + [InlineData("79228162514264337593543950335", "{\"Prop\":\"79228162514264337593543950335\"}")] + [InlineData("0.0", "{\"Prop\":\"0.0\"}")] + [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(PrimitiveTypesAsStrings.Decimal)), + value == null ? default(decimal?) : decimal.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("1/1/0001", "{\"Prop\":\"0001-01-01\"}")] + [InlineData("12/31/9999", "{\"Prop\":\"9999-12-31\"}")] + [InlineData("1/1/0001", "{\"Prop\":\"0001-01-01\"}")] + [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(PrimitiveTypesAsStrings.DateOnly)), + value == null ? default(DateOnly?) : DateOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("00:00:00.0000000", "{\"Prop\":\"00:00:00\"}")] + [InlineData("23:59:59.9999999", "{\"Prop\":\"23:59:59.9999999\"}")] + [InlineData("00:00:00.0000000", "{\"Prop\":\"00:00:00\"}")] + [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(PrimitiveTypesAsStrings.TimeOnly)), + value == null ? default(TimeOnly?) : TimeOnly.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00.0000000", "{\"Prop\":\"0001-01-01 00:00:00\"}")] + [InlineData("9999-12-31T23:59:59.9999999", "{\"Prop\":\"9999-12-31 23:59:59.9999999\"}")] + [InlineData("0001-01-01T00:00:00.0000000", "{\"Prop\":\"0001-01-01 00:00:00\"}")] + [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(PrimitiveTypesAsStrings.DateTime)), + value == null ? default(DateTime?) : DateTime.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("0001-01-01T00:00:00.0000000-01:00", "{\"Prop\":\"0001-01-01 00:00:00-01:00\"}")] + [InlineData("9999-12-31T23:59:59.9999999+02:00", "{\"Prop\":\"9999-12-31 23:59:59.9999999+02:00\"}")] + [InlineData("0001-01-01T00:00:00.0000000-03:00", "{\"Prop\":\"0001-01-01 00:00:00-03:00\"}")] + [InlineData("2023-05-29T11:11:15.5672854+04:00", "{\"Prop\":\"2023-05-29 11:11:15.5672854+04: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(PrimitiveTypesAsStrings.DateTimeOffset)), + value == null ? default(DateTimeOffset?) : DateTimeOffset.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("-10675199.02:48:05.4775808", "{\"Prop\":\"-10675199.02:48:05.4775808\"}")] + [InlineData("10675199.02:48:05.4775807", "{\"Prop\":\"10675199.02:48:05.4775807\"}")] + [InlineData("00:00:00", "{\"Prop\":\"00:00:00\"}")] + [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(PrimitiveTypesAsStrings.TimeSpan)), + value == null ? default(TimeSpan?) : TimeSpan.Parse(value), json); + + [ConditionalTheory] + [InlineData(false, "{\"Prop\":\"0\"}")] + [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(PrimitiveTypesAsStrings.Boolean)), value, json); + + [ConditionalTheory] + [InlineData(char.MinValue, "{\"Prop\":\"\\u0000\"}")] + [InlineData(char.MaxValue, "{\"Prop\":\"\\uFFFF\"}")] + [InlineData(' ', "{\"Prop\":\" \"}")] + [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(PrimitiveTypesAsStrings.Character)), value, json); + + [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("00000000-0000-0000-0000-000000000000", "{\"Prop\":\"00000000-0000-0000-0000-000000000000\"}")] + [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(PrimitiveTypesAsStrings.Guid)), + value == null ? default(Guid?) : Guid.Parse(value, CultureInfo.InvariantCulture), json); + + [ConditionalTheory] + [InlineData("MinValue", "{\"Prop\":\"MinValue\"}")] + [InlineData("MaxValue", "{\"Prop\":\"MaxValue\"}")] + [InlineData("", "{\"Prop\":\"\"}")] + [InlineData( + "❤❥웃유♋☮✌☏☢☠✔☑♚▲♪฿Ɖ⛏♥❣♂♀☿👍✍✉☣☤✘☒♛▼♫⌘⌛¡♡ღツ☼☁❅♾️✎©®™Σ✪✯☭➳Ⓐ✞℃℉°✿⚡☃☂✄¢€£∞✫★½☯✡☪", + @"{""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(PrimitiveTypesAsStrings.String)), value, json); + + [ConditionalTheory] + [InlineData("0,0,0,1", "{\"Prop\":\"AAAAAQ==\"}")] + [InlineData("255,255,255,255", "{\"Prop\":\"/////w==\"}")] + [InlineData("", "{\"Prop\":\"\"}")] + [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(PrimitiveTypesAsStrings.Bytes)), + value == null + ? default + : value == "" + ? Array.Empty() + : value.Split(',').Select(e => byte.Parse(e)).ToArray(), json); + + [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&q2=v2#FragmentName\"}")] + [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(PrimitiveTypesAsStrings.Uri)), + value == null ? default : new Uri(value), json); + + [ConditionalTheory] + [InlineData("127.0.0.1", "{\"Prop\":\"127.0.0.1\"}")] + [InlineData("0.0.0.0", "{\"Prop\":\"0.0.0.0\"}")] + [InlineData("255.255.255.255", "{\"Prop\":\"255.255.255.255\"}")] + [InlineData("192.168.1.156", "{\"Prop\":\"192.168.1.156\"}")] + [InlineData("::1", "{\"Prop\":\"::1\"}")] + [InlineData("::", "{\"Prop\":\"::\"}")] + [InlineData("::", "{\"Prop\":\"::\"}")] + [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(PrimitiveTypesAsStrings.IpAddress)), + value == null ? default : IPAddress.Parse(value), json); + + [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_as_string_JSON_values(string? value, string json) + => Can_read_and_write_JSON_value( + Fixture.EntityType().GetProperty(nameof(PrimitiveTypesAsStrings.PhysicalAddress)), + value == null ? default : PhysicalAddress.Parse(value), json); + + [ConditionalTheory] + [InlineData((sbyte)Enum8.Min, "{\"Prop\":\"Min\"}")] + [InlineData((sbyte)Enum8.Max, "{\"Prop\":\"Max\"}")] + [InlineData((sbyte)Enum8.Default, "{\"Prop\":\"Default\"}")] + [InlineData((sbyte)Enum8.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.Enum8)), + value == null ? default(Enum8?) : (Enum8)value, json); + + [ConditionalTheory] + [InlineData((short)Enum16.Min, "{\"Prop\":\"Min\"}")] + [InlineData((short)Enum16.Max, "{\"Prop\":\"Max\"}")] + [InlineData((short)Enum16.Default, "{\"Prop\":\"Default\"}")] + [InlineData((short)Enum16.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.Enum16)), + value == null ? default(Enum16?) : (Enum16)value, json); + + [ConditionalTheory] + [InlineData((int)Enum32.Min, "{\"Prop\":\"Min\"}")] + [InlineData((int)Enum32.Max, "{\"Prop\":\"Max\"}")] + [InlineData((int)Enum32.Default, "{\"Prop\":\"Default\"}")] + [InlineData((int)Enum32.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.Enum32)), + value == null ? default(Enum32?) : (Enum32)value, json); + + [ConditionalTheory] + [InlineData((long)Enum64.Min, "{\"Prop\":\"Min\"}")] + [InlineData((long)Enum64.Max, "{\"Prop\":\"Max\"}")] + [InlineData((long)Enum64.Default, "{\"Prop\":\"Default\"}")] + [InlineData((long)Enum64.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.Enum64)), + value == null ? default(Enum64?) : (Enum64)value, json); + + [ConditionalTheory] + [InlineData((byte)EnumU8.Min, "{\"Prop\":\"Min\"}")] + [InlineData((byte)EnumU8.Max, "{\"Prop\":\"Max\"}")] + [InlineData((byte)EnumU8.Default, "{\"Prop\":\"Min\"}")] + [InlineData((byte)EnumU8.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.EnumU8)), + value == null ? default(EnumU8?) : (EnumU8)value, json); + + [ConditionalTheory] + [InlineData((ushort)EnumU16.Min, "{\"Prop\":\"Min\"}")] + [InlineData((ushort)EnumU16.Max, "{\"Prop\":\"Max\"}")] + [InlineData((ushort)EnumU16.Default, "{\"Prop\":\"Min\"}")] + [InlineData((ushort)EnumU16.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.EnumU16)), + value == null ? default(EnumU16?) : (EnumU16)value, json); + + [ConditionalTheory] + [InlineData((uint)EnumU32.Min, "{\"Prop\":\"Min\"}")] + [InlineData((uint)EnumU32.Max, "{\"Prop\":\"Max\"}")] + [InlineData((uint)EnumU32.Default, "{\"Prop\":\"Min\"}")] + [InlineData((uint)EnumU32.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.EnumU32)), + value == null ? default(EnumU32?) : (EnumU32)value, json); + + [ConditionalTheory] + [InlineData((ulong)EnumU64.Min, "{\"Prop\":\"Min\"}")] + [InlineData((ulong)EnumU64.Max, "{\"Prop\":\"Max\"}")] + [InlineData((ulong)EnumU64.Default, "{\"Prop\":\"Min\"}")] + [InlineData((ulong)EnumU64.One, "{\"Prop\":\"One\"}")] + [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(PrimitiveTypesAsStrings.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)), + factory.CreatePoint(new Coordinate(2, 4)), + "{\"Prop\":\"POINT (2 4)\"}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypes.PointZ)), + factory.CreatePoint(new CoordinateZ(2, 4, 6)), + "{\"Prop\":\"POINT Z(2 4 6)\"}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypes.PointM)), + factory.CreatePoint(new CoordinateM(2, 4, 6)), + "{\"Prop\":\"POINT (2 4)\"}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypes.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}"); + } + + [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)), + factory.CreateLineString(new[] { new Coordinate(0, 0), new Coordinate(1, 0) }), + "{\"Prop\":\"LINESTRING (0 0, 1 0)\"}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypes.LineString)), + null, + "{\"Prop\":null}"); + } + + [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)), + factory.CreateMultiLineString( + new[] + { + factory.CreateLineString( + new[] { new Coordinate(0, 0), new Coordinate(0, 1) }), + factory.CreateLineString( + 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)), + null, + "{\"Prop\":null}"); + } + + [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)), + 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)), + null, + "{\"Prop\":null}"); + } + + [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)), + 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)), + null, + "{\"Prop\":null}"); + } + + [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)), + factory.CreatePoint(new Coordinate(2, 4)), + "{\"Prop\":{\"type\":\"Point\",\"coordinates\":[2.0,4.0]}}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypesAsGeoJson.PointZ)), + factory.CreatePoint(new CoordinateZ(2, 4, 6)), + "{\"Prop\":{\"type\":\"Point\",\"coordinates\":[2.0,4.0]}}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypesAsGeoJson.PointM)), + factory.CreatePoint(new CoordinateM(2, 4, 6)), + "{\"Prop\":{\"type\":\"Point\",\"coordinates\":[2.0,4.0]}}"); + + Can_read_and_write_JSON_value( + entityType.GetProperty(nameof(GeometryTypesAsGeoJson.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)), + 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)), + 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)), + factory.CreateMultiLineString( + new[] + { + factory.CreateLineString( + new[] { new Coordinate(0, 0), new Coordinate(0, 1) }), + factory.CreateLineString( + 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)), + 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)), + 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)), + 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)), + 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)), + null, + "{\"Prop\":null}"); + } + + [ConditionalTheory] + [InlineData(int.MinValue, "{\"Prop\":-2147483648}")] + [InlineData(int.MaxValue, "{\"Prop\":2147483647}")] + [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(ConvertedTypes.Id)), new DddId { Id = value }, json); + + [ConditionalTheory] + [InlineData(int.MinValue, "{\"Prop\":-2147483648}")] + [InlineData(int.MaxValue, "{\"Prop\":2147483647}")] + [InlineData(0, "{\"Prop\":0}")] + [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(ConvertedTypes.NullableId)), + value == null ? default(DddId?) : new DddId { Id = value.Value }, json); + + protected virtual void Can_read_and_write_JSON_value(IProperty property, TModel value, string json) + { + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + var jsonReaderWriter = property.GetJsonValueReaderWriter()!; + var valueConverter = property.GetTypeMapping().Converter; + + writer.WriteStartObject(); + writer.WritePropertyName("Prop"); + if (value == null) + { + writer.WriteNullValue(); + } + else + { + if (valueConverter == null) + { + jsonReaderWriter.ToJson(writer, value); + } + else + { + jsonReaderWriter.ToJson(writer, valueConverter.ConvertToProvider(value)!); + } + } + + writer.WriteEndObject(); + writer.Flush(); + + var buffer = stream.ToArray(); + + var actual = Encoding.UTF8.GetString(buffer); + actual = actual.Replace("\\u002B", "+"); // Why does GetString not do this? + actual = actual.Replace("\\u0026", "&"); + + Assert.Equal(json, actual); + + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(buffer)); + readerManager.MoveNext(); + readerManager.MoveNext(); + readerManager.MoveNext(); + + Assert.Equal( + value, + property.IsNullable + && readerManager.CurrentReader.TokenType == JsonTokenType.Null + ? default! + : valueConverter == null + ? jsonReaderWriter.FromJson(ref readerManager) + : valueConverter.ConvertFromProvider(jsonReaderWriter.FromJson(ref readerManager))!); + } + + protected class PrimitiveTypes + { + 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; } = null!; + public byte[] Bytes { get; set; } = null!; + + public bool Boolean { get; set; } + public char Character { get; set; } + + public Uri Uri { get; set; } = null!; + public PhysicalAddress PhysicalAddress { get; set; } = null!; + public IPAddress IpAddress { get; set; } = null!; + + 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; } + } + + protected class NullablePrimitiveTypes + { + 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; } + } + + protected class PrimitiveTypesAsStrings + { + 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; } + } + + protected class ConvertedTypes + { + public DddId Id { get; set; } + public DddId? NullableId { get; set; } + } + + protected readonly struct DddId + { + public int Id { get; init; } + } + + protected class DddIdConverter : ValueConverter + { + public DddIdConverter() + : base(v => v.Id, v => new DddId { Id = v }) + { + } + } + + 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, + Default = 0, + One = 1, + Max = sbyte.MaxValue + } + + public enum Enum16 : short + { + Min = short.MinValue, + Default = 0, + One = 1, + Max = short.MaxValue + } + + public enum Enum32 + { + Min = int.MinValue, + Default = 0, + One = 1, + Max = int.MaxValue + } + + public enum Enum64 : long + { + Min = long.MinValue, + Default = 0, + One = 1, + Max = long.MaxValue + } + + public enum EnumU8 : byte + { + Min = byte.MinValue, + Default = 0, + One = 1, + Max = byte.MaxValue + } + + public enum EnumU16 : ushort + { + Min = ushort.MinValue, + Default = 0, + One = 1, + Max = ushort.MaxValue + } + + public enum EnumU32 : uint + { + Min = uint.MinValue, + Default = 0, + One = 1, + Max = uint.MaxValue + } + + public enum EnumU64 : ulong + { + Min = ulong.MinValue, + Default = 0, + One = 1, + Max = ulong.MaxValue + } + + public sealed class JsonGeoJsonReaderWriter : JsonValueReaderWriter + { + public static JsonGeoJsonReaderWriter Instance { get; } = new(); + + private JsonGeoJsonReaderWriter() + { + } + + public override Geometry FromJsonTyped(ref Utf8JsonReaderManager manager) + { + var builder = new StringBuilder("{"); + var depth = 1; + var comma = false; + while (depth > 0) + { + manager.MoveNext(); + + switch (manager.CurrentReader.TokenType) + { + case JsonTokenType.EndObject: + depth--; + builder.Append('}'); + comma = true; + break; + case JsonTokenType.PropertyName: + builder.Append(comma ? ",\"" : "\"").Append(manager.CurrentReader.GetString()).Append("\":"); + comma = false; + break; + case JsonTokenType.StartObject: + depth++; + builder.Append(comma ? ",{" : "{"); + comma = false; + break; + case JsonTokenType.String: + builder.Append(comma ? ",\"" : "\"").Append(manager.CurrentReader.GetString()).Append('"'); + comma = true; + break; + case JsonTokenType.Number: + builder.Append(comma ? "," : "").Append(manager.CurrentReader.GetDecimal()); + comma = true; + break; + case JsonTokenType.True: + builder.Append(comma ? ",true" : "true"); + comma = true; + break; + case JsonTokenType.False: + builder.Append(comma ? ",false" : "false"); + comma = true; + break; + case JsonTokenType.Null: + builder.Append(comma ? ",null" : "null"); + comma = true; + break; + case JsonTokenType.StartArray: + builder.Append(comma ? ",[" : "["); + comma = false; + break; + case JsonTokenType.EndArray: + builder.Append(']'); + comma = true; + break; + case JsonTokenType.None: + break; + case JsonTokenType.Comment: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + var serializer = GeoJsonSerializer.Create(); + using var stringReader = new StringReader(builder.ToString()); + using var jsonReader = new JsonTextReader(stringReader); + return serializer.Deserialize(jsonReader)!; + } + + public override void ToJsonTyped(Utf8JsonWriter writer, Geometry value) + { + var serializer = GeoJsonSerializer.Create(); + using var stringWriter = new StringWriter(); + using var jsonWriter = new JsonTextWriter(stringWriter); + serializer.Serialize(jsonWriter, value); + writer.WriteRawValue(stringWriter.ToString()); + } + } + + public abstract class JsonTypesFixtureBase : SharedStoreFixtureBase + { + private DbContext? _staticContext; + + protected override string StoreName + => "JsonTypes"; + + 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, DbContext context) + { + modelBuilder.Entity().HasNoKey(); + modelBuilder.Entity().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( + b => + { + b.HasNoKey(); + b.Property(e => e.Id).HasConversion(); + b.Property(e => e.NullableId).HasConversion(); + }); + + modelBuilder.Entity().HasNoKey(); + + modelBuilder.Entity( + b => + { + b.HasNoKey(); + 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)); + }); + } + + public DbContext StaticContext + => _staticContext ??= CreateContext(); + + public IEntityType EntityType() + => StaticContext.Model.FindEntityType(typeof(TEntity))!; + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs new file mode 100644 index 00000000000..103583fcd7c --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/JsonTypesSqlServerTest.cs @@ -0,0 +1,52 @@ +// 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 class JsonTypesSqlServerTest : JsonTypesTestBase +{ + 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) + { + 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 class JsonTypesSqlServerFixture : JsonTypesFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + new SqlServerDbContextOptionsBuilder(builder).UseNetTopologySuite(); + var options = base.AddOptions(builder).ConfigureWarnings( + c => c.Log(SqlServerEventId.DecimalTypeDefaultWarning)); + + return options; + } + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection.AddEntityFrameworkSqlServerNetTopologySuite()); + } +} diff --git a/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs b/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs index 627c0f45d23..819aea7c3a6 100644 --- a/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs +++ b/test/EFCore.SqlServer.HierarchyId.Tests/TypeMappingTests.cs @@ -1,8 +1,13 @@ // 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 System.Text.Json; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Json; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Json; +using Microsoft.SqlServer.Types; using Xunit; namespace Microsoft.EntityFrameworkCore.SqlServer; @@ -37,20 +42,71 @@ public void Maps_hierarchyid_column() scale: null)); AssertMapping(mapping); + + Assert.Same(SqlServerJsonHierarchyIdReaderWriter.Instance, mapping!.JsonValueReaderWriter); } - private static void AssertMapping( - RelationalTypeMapping mapping) + [ConditionalFact] + public void Convert_HierarchyId_to_and_from_JSON() + => Convert_to_and_from_JSON( + SqlServerJsonHierarchyIdReaderWriter.Instance, + HierarchyId.GetRoot(), new HierarchyId("/1/"), new HierarchyId("/1/3/"), + "{\"Prop1\":\"/\",\"Prop2\":\"/1/\",\"Prop3\":\"/1/3/\"}"); + + [ConditionalFact] + public void Convert_SqlHierarchyId_to_and_from_JSON() + => Convert_to_and_from_JSON( + SqlServerJsonSqlHierarchyIdReaderWriter.Instance, + SqlHierarchyId.GetRoot(), SqlHierarchyId.Parse("/1/"), SqlHierarchyId.Parse("/1/3/"), + "{\"Prop1\":\"/\",\"Prop2\":\"/1/\",\"Prop3\":\"/1/3/\"}"); + + private void Convert_to_and_from_JSON( + JsonValueReaderWriter jsonReaderWriter, + TValue one, + TValue two, + TValue three, + string json) { - AssertMapping(typeof(T), mapping); + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream); + + writer.WriteStartObject(); + writer.WritePropertyName("Prop1"); + jsonReaderWriter.ToJson(writer, one); + writer.WritePropertyName("Prop2"); + jsonReaderWriter.ToJson(writer, two); + writer.WritePropertyName("Prop3"); + jsonReaderWriter.ToJson(writer, three); + writer.WriteEndObject(); + writer.Flush(); + + var buffer = stream.ToArray(); + + var actual = Encoding.UTF8.GetString(buffer); + + Assert.Equal(json, actual); + + var readerManager = new Utf8JsonReaderManager(new JsonReaderData(buffer)); + readerManager.MoveNext(); + readerManager.MoveNext(); + readerManager.MoveNext(); + Assert.Equal(one, jsonReaderWriter.FromJson(ref readerManager)); + readerManager.MoveNext(); + readerManager.MoveNext(); + Assert.Equal(two, jsonReaderWriter.FromJson(ref readerManager)); + readerManager.MoveNext(); + readerManager.MoveNext(); + Assert.Equal(three, jsonReaderWriter.FromJson(ref readerManager)); } + private static void AssertMapping( + RelationalTypeMapping mapping) + => AssertMapping(typeof(T), mapping); + private static void AssertMapping( Type type, RelationalTypeMapping mapping) - { - Assert.Same(type, mapping.ClrType); - } + => Assert.Same(type, mapping.ClrType); private static IRelationalTypeMappingSourcePlugin CreateMapper() => new SqlServerHierarchyIdTypeMappingSourcePlugin(); diff --git a/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs b/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs index 885236eeee8..6ec6b2c0487 100644 --- a/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs +++ b/test/EFCore.SqlServer.Tests/Design/Internal/SqlServerAnnotationCodeGeneratorTest.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.SqlServer.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; namespace Microsoft.EntityFrameworkCore.Design.Internal; @@ -390,6 +391,7 @@ private SqlServerAnnotationCodeGenerator CreateGenerator() new TypeMappingSourceDependencies( new ValueConverterSelector( new ValueConverterSelectorDependencies()), + new JsonValueReaderWriterSource(new JsonValueReaderWriterSourceDependencies()), Array.Empty()), new RelationalTypeMappingSourceDependencies( Array.Empty()), diff --git a/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs new file mode 100644 index 00000000000..c11b5149af7 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/JsonTypesSqliteTest.cs @@ -0,0 +1,31 @@ +// 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; + +[SpatialiteRequired] +public class JsonTypesSqliteTest : JsonTypesTestBase +{ + 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()); + } +} diff --git a/test/EFCore.Tests/ApiConsistencyTest.cs b/test/EFCore.Tests/ApiConsistencyTest.cs index 8a0dca01f8f..bfbce84c6e0 100644 --- a/test/EFCore.Tests/ApiConsistencyTest.cs +++ b/test/EFCore.Tests/ApiConsistencyTest.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.Json; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; @@ -82,7 +83,10 @@ protected override void Initialize() typeof(DiagnosticsLogger<>).GetMethod("NeedsEventData", AnyInstance), typeof(ChangeDetector).GetMethod("DetectValueChange"), typeof(ChangeDetector).GetMethod("DetectNavigationChange"), - typeof(StateManager).GetMethod("get_ChangeDetector") + typeof(StateManager).GetMethod("get_ChangeDetector"), + typeof(JsonValueReaderWriter<>).GetMethod(nameof(JsonValueReaderWriter.FromJson)), + typeof(JsonValueReaderWriter<>).GetMethod(nameof(JsonValueReaderWriter.ToJson)), + typeof(JsonValueReaderWriter<>).GetMethod("get_ValueType") }; public override HashSet NotAnnotatedMethods { get; } = new() diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index 62b61185462..bc65fb04777 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -3,6 +3,8 @@ // ReSharper disable InconsistentNaming +using Microsoft.EntityFrameworkCore.Storage.Json; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class ClrPropertyGetterFactoryTest @@ -77,6 +79,9 @@ public ValueComparer GetKeyValueComparer() public ValueComparer GetProviderValueComparer() => throw new NotImplementedException(); + public JsonValueReaderWriter GetJsonValueReaderWriter() + => 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 9a146889038..96a0412884c 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertySetterFactoryTest.cs @@ -5,6 +5,8 @@ // ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable InconsistentNaming +using Microsoft.EntityFrameworkCore.Storage.Json; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class ClrPropertySetterFactoryTest @@ -96,6 +98,9 @@ public ValueComparer GetKeyValueComparer() public ValueComparer GetProviderValueComparer() => throw new NotImplementedException(); + public JsonValueReaderWriter GetJsonValueReaderWriter() + => throw new NotImplementedException(); + public bool IsForeignKey() => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs index 2f006817a66..5d3a18581ff 100644 --- a/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/PropertyTest.cs @@ -7,6 +7,9 @@ // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable InconsistentNaming +using System.Text.Json; +using Microsoft.EntityFrameworkCore.Storage.Json; + namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public class PropertyTest @@ -463,6 +466,175 @@ public NonParameterlessValueComparer(bool favorStructuralComparison) } } + [ConditionalTheory] + [InlineData(typeof(SimpleJasonValueReaderWriter))] + [InlineData(typeof(JasonValueReaderWriterWithPrivateInstance))] + [InlineData(typeof(JasonValueReaderWriterWithBadInstance))] + public void Creates_instance_of_JsonValueReaderWriter_using_constructor(Type type) + { + var model = CreateModel(); + var entityType = model.AddEntityType(typeof(object)); + var property = entityType.AddProperty("Kake", typeof(string)); + property.SetJsonValueReaderWriterType(type); + + var instance1 = property.GetJsonValueReaderWriter(); + var instance2 = property.GetJsonValueReaderWriter(); + Assert.NotNull(instance1); + Assert.NotEqual(instance1, instance2); + } + + [ConditionalTheory] + [InlineData(typeof(SimpleJasonValueReaderWriterWithInstance))] + [InlineData(typeof(SimpleJasonValueReaderWriterWithInstanceAndPrivateConstructor))] + public void Creates_instance_of_JsonValueReaderWriter_using_instance(Type type) + { + var model = CreateModel(); + var entityType = model.AddEntityType(typeof(object)); + var property = entityType.AddProperty("Kake", typeof(string)); + property.SetJsonValueReaderWriterType(type); + + var instance1 = property.GetJsonValueReaderWriter(); + var instance2 = property.GetJsonValueReaderWriter(); + Assert.NotNull(instance1); + Assert.Same(instance1, instance2); + } + + [ConditionalTheory] + [InlineData(typeof(NonDerivedJsonValueReaderWriter))] + [InlineData(typeof(NonGenericJsonValueReaderWriter))] + public void Throws_when_JsonValueReaderWriter_type_is_invalid(Type type) + { + var model = CreateModel(); + var entityType = model.AddEntityType(typeof(object)); + var property = entityType.AddProperty("Kake", typeof(string)); + + Assert.Equal( + CoreStrings.BadJsonValueReaderWriterType(type.ShortDisplayName()), + Assert.Throws( + () => + property.SetJsonValueReaderWriterType(type)).Message); + } + + [ConditionalTheory] + [InlineData(typeof(AbstractJasonValueReaderWriter))] + [InlineData(typeof(NonParameterlessJsonValueReaderWriter))] + [InlineData(typeof(PrivateJasonValueReaderWriter))] + public void Throws_when_JsonValueReaderWriter_instance_cannot_be_created(Type type) + { + var model = CreateModel(); + var entityType = model.AddEntityType(typeof(object)); + var property = entityType.AddProperty("Kake", typeof(string)); + property.SetJsonValueReaderWriterType(type); + + Assert.Equal( + CoreStrings.CannotCreateJsonValueReaderWriter(type.ShortDisplayName()), + Assert.Throws( + () => property.GetJsonValueReaderWriter()).Message); + } + + private class SimpleJasonValueReaderWriter : JsonValueReaderWriter + { + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + + private class JasonValueReaderWriterWithPrivateInstance : JsonValueReaderWriter + { + private static JasonValueReaderWriterWithPrivateInstance Instance { get; } = new(); + + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + + private class JasonValueReaderWriterWithBadInstance : JsonValueReaderWriter + { + public static object Instance { get; } = new(); + + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + + private class SimpleJasonValueReaderWriterWithInstance : JsonValueReaderWriter + { + public static SimpleJasonValueReaderWriterWithInstance Instance { get; } = new(); + + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + + private class SimpleJasonValueReaderWriterWithInstanceAndPrivateConstructor : JsonValueReaderWriter + { + public static SimpleJasonValueReaderWriterWithInstanceAndPrivateConstructor Instance { get; } = new(); + + private SimpleJasonValueReaderWriterWithInstanceAndPrivateConstructor() + { + } + + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + + private class NonDerivedJsonValueReaderWriter + { + } + + private class NonGenericJsonValueReaderWriter : JsonValueReaderWriter + { + public override object FromJson(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJson(Utf8JsonWriter writer, object value) + => writer.WriteStringValue((string)value); + + public override Type ValueType + => typeof(string); + } + + private abstract class AbstractJasonValueReaderWriter : JsonValueReaderWriter + { + } + + private class PrivateJasonValueReaderWriter : JsonValueReaderWriter + { + private PrivateJasonValueReaderWriter() + { + } + + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + + private class NonParameterlessJsonValueReaderWriter : JsonValueReaderWriter + { + public NonParameterlessJsonValueReaderWriter(bool _) + { + } + + public override string FromJsonTyped(ref Utf8JsonReaderManager manager) + => manager.CurrentReader.GetString()!; + + public override void ToJsonTyped(Utf8JsonWriter writer, string value) + => writer.WriteStringValue(value); + } + private static IMutableModel CreateModel() => new Model();