From 908faf1df6dd689932587d3079aaaee25b9ad5a6 Mon Sep 17 00:00:00 2001 From: Roman Marusyk Date: Sun, 7 Mar 2021 04:13:25 +0200 Subject: [PATCH] Add Support for CosmosSerializer and CosmosSerializationOptions in the Cosmos Provider --- .../CosmosDbContextOptionsBuilder.cs | 16 +++++ .../Internal/CosmosDbOptionExtension.cs | 63 +++++++++++++++++++ .../Internal/CosmosSingletonOptions.cs | 20 ++++++ .../Internal/ICosmosSingletonOptions.cs | 16 +++++ .../Properties/CosmosStrings.Designer.cs | 6 ++ .../Properties/CosmosStrings.resx | 3 + .../Internal/SingletonCosmosClientWrapper.cs | 12 +++- .../ConfigPatternsCosmosTest.cs | 28 +++++++++ .../CosmosDbContextOptionsExtensionsTests.cs | 61 ++++++++++++++++++ 9 files changed, 224 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs index 5a6368395cf..c9e96792025 100644 --- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs +++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs @@ -105,6 +105,22 @@ public virtual CosmosDbContextOptionsBuilder IdleTcpConnectionTimeout(TimeSpan t public virtual CosmosDbContextOptionsBuilder GatewayModeMaxConnectionLimit(int connectionLimit) => WithOption(e => e.WithGatewayModeMaxConnectionLimit(Check.NotNull(connectionLimit, nameof(connectionLimit)))); + /// + /// Configures an optional JSON serializer. The client will use it to serialize or + /// de-serialize user's cosmos request/responses. SDK owned types such as DatabaseProperties + /// and ContainerProperties will always use the SDK default serializer. + /// + /// The JSON serializer. + public virtual CosmosDbContextOptionsBuilder Serializer(CosmosSerializer serializer) + => WithOption(e => e.WithSerializer(serializer)); + + /// + /// Configures the optional serializer options. + /// + /// Provides a way to configure basic serializer settings. + public virtual CosmosDbContextOptionsBuilder SerializationOptions(CosmosSerializationOptions serializationOptions) + => WithOption(e => e.WithSerializationOptions(serializationOptions)); + /// /// Configures the maximum number of TCP connections that may be opened to each Cosmos DB back-end. /// Together with MaxRequestsPerTcpConnection, this setting limits the number of requests that are diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs index 97fcfa28b66..31047fcf0b9 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; @@ -43,6 +44,8 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension private int? _maxRequestsPerTcpConnection; private bool? _enableContentResponseOnWrite; private DbContextOptionsExtensionInfo? _info; + private CosmosSerializer? _serializer; + private CosmosSerializationOptions? _serializationOptions; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -77,6 +80,8 @@ protected CosmosOptionsExtension([NotNull] CosmosOptionsExtension copyFrom) _gatewayModeMaxConnectionLimit = copyFrom._gatewayModeMaxConnectionLimit; _maxTcpConnectionsPerEndpoint = copyFrom._maxTcpConnectionsPerEndpoint; _maxRequestsPerTcpConnection = copyFrom._maxRequestsPerTcpConnection; + _serializer = copyFrom._serializer; + _serializationOptions = copyFrom._serializationOptions; } /// @@ -146,6 +151,62 @@ public virtual CosmosOptionsExtension WithAccountKey([CanBeNull] string? account return clone; } + /// + /// 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 CosmosSerializer? Serializer => _serializer; + + /// + /// 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 CosmosOptionsExtension WithSerializer([CanBeNull] CosmosSerializer? serializer) + { + if (serializer is not null && (_serializationOptions != null)) + { + throw new InvalidOperationException(CosmosStrings.SerializerOptionsConflictingSerializer); + } + + var clone = Clone(); + + clone._serializer = serializer ?? new JsonCosmosSerializer(); + + return clone; + } + + /// + /// 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 CosmosSerializationOptions? SerializationOptions => _serializationOptions; + + /// + /// 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 CosmosOptionsExtension WithSerializationOptions([CanBeNull] CosmosSerializationOptions? serializationOptions) + { + if (serializationOptions is not null && (_serializer != null)) + { + throw new InvalidOperationException(CosmosStrings.SerializerOptionsConflictingSerializer); + } + + var clone = Clone(); + + clone._serializationOptions = serializationOptions; + + return clone; + } + /// /// 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 @@ -560,6 +621,8 @@ public override long GetServiceProviderHashCode() hashCode = (hashCode * 131) ^ (Extension._gatewayModeMaxConnectionLimit?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ (Extension._maxTcpConnectionsPerEndpoint?.GetHashCode() ?? 0); hashCode = (hashCode * 131) ^ (Extension._maxRequestsPerTcpConnection?.GetHashCode() ?? 0); + hashCode = (hashCode * 131) ^ (Extension._serializer?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Extension._serializationOptions?.GetHashCode() ?? 0); _serviceProviderHash = hashCode; } diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs index 0d2379957cd..4fe321ae03a 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs @@ -139,6 +139,22 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions /// public virtual bool? EnableContentResponseOnWrite { get; private set; } + /// + /// 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 CosmosSerializer? Serializer { get; private set; } + + /// + /// 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 CosmosSerializationOptions? SerializationOptions { get; private set; } + /// /// 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 @@ -164,6 +180,8 @@ public virtual void Initialize(IDbContextOptions options) MaxTcpConnectionsPerEndpoint = cosmosOptions.MaxTcpConnectionsPerEndpoint; MaxRequestsPerTcpConnection = cosmosOptions.MaxRequestsPerTcpConnection; EnableContentResponseOnWrite = cosmosOptions.EnableContentResponseOnWrite; + Serializer = cosmosOptions.Serializer; + SerializationOptions = cosmosOptions.SerializationOptions; } } @@ -192,6 +210,8 @@ public virtual void Validate(IDbContextOptions options) || MaxTcpConnectionsPerEndpoint != cosmosOptions.MaxTcpConnectionsPerEndpoint || MaxRequestsPerTcpConnection != cosmosOptions.MaxRequestsPerTcpConnection || EnableContentResponseOnWrite != cosmosOptions.EnableContentResponseOnWrite + || Serializer != cosmosOptions.Serializer + || SerializationOptions != cosmosOptions.SerializationOptions )) { throw new InvalidOperationException( diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs index c60fec5a946..2932776b7d6 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs @@ -138,5 +138,21 @@ public interface ICosmosSingletonOptions : ISingletonOptions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// bool? EnableContentResponseOnWrite { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + CosmosSerializer? Serializer { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + CosmosSerializationOptions? SerializationOptions { get; } } } diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index 11db1638d02..fe7a0f1a959 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -290,6 +290,12 @@ public static string UpdateConflict([CanBeNull] object? itemId) public static string VisitChildrenMustBeOverridden => GetString("VisitChildrenMustBeOverridden"); + /// + /// SerializerOptions is not compatible with Serializer. Only one can be set. + /// + public static string SerializerOptionsConflictingSerializer + => GetString("SerializerOptionsConflictingSerializer"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index eaa7a3e2b8d..d271b340270 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -218,6 +218,9 @@ Reversing the ordering is not supported when limit or offset are already applied. + + SerializerOptions is not compatible with Serializer. Only one can be set. + The Cosmos database provider does not support transactions. diff --git a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs index bae924cd229..95578c9f3d0 100644 --- a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs @@ -45,7 +45,7 @@ public SingletonCosmosClientWrapper([NotNull] ICosmosSingletonOptions options) _endpoint = options.AccountEndpoint; _key = options.AccountKey; _connectionString = options.ConnectionString; - var configuration = new CosmosClientOptions { ApplicationName = _userAgent, Serializer = new JsonCosmosSerializer() }; + var configuration = new CosmosClientOptions { ApplicationName = _userAgent }; if (options.Region != null) { @@ -97,6 +97,16 @@ public SingletonCosmosClientWrapper([NotNull] ICosmosSingletonOptions options) configuration.MaxRequestsPerTcpConnection = options.MaxRequestsPerTcpConnection.Value; } + if (options.Serializer != null) + { + configuration.Serializer = options.Serializer; + } + + if (options.SerializationOptions != null) + { + configuration.SerializerOptions = options.SerializationOptions; + } + _options = configuration; } diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs index 91b65a9e005..93d0896cc42 100644 --- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs @@ -127,6 +127,34 @@ public async Task Should_throw_if_specified_connection_mode_is_wrong() }); } + [ConditionalFact] + public async Task Should_use_serialization_options() + { + var serializationOptions = new CosmosSerializationOptions + { + IgnoreNullValues = true + }; + + await using var testDatabase = CosmosTestStore.CreateInitialized(DatabaseName, o => + { + o.SerializationOptions(serializationOptions); + }); + var options = CreateOptions(testDatabase); + + var customer = new Customer { Id = 43 }; + + using var context = new CustomerContext(options); + context.Database.EnsureCreated(); + + context.Add(customer); + + context.SaveChanges(); + + var customerEntry = context.Entry(context.Find(customer.Id)); + var jsonProperty = customerEntry.Property("Name"); + Assert.Null(jsonProperty); + } + private DbContextOptions CreateOptions(CosmosTestStore testDatabase) => Fixture.AddOptions(testDatabase.AddProviderOptions(new DbContextOptionsBuilder())) .EnableDetailedErrors() diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs index d0f3606b7d6..6021b58fd3a 100644 --- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs +++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs @@ -5,6 +5,7 @@ using System.Net; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; using Xunit; @@ -44,6 +45,66 @@ public void Can_create_options_with_specified_region() Assert.Equal(regionName, extension.Region); } + [ConditionalFact] + public void Can_create_options_with_specified_serializer() + { + var serializer = new JsonCosmosSerializer(); + var options = new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + o => { o.Serializer(serializer); }); + + var extension = options + .Options.FindExtension(); + + Assert.Same(serializer, extension.Serializer); + } + + [ConditionalFact] + public void Can_create_options_with_specified_serialization_options() + { + var serializationOptions = new CosmosSerializationOptions{ IgnoreNullValues = true }; + var options = new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + o => { o.SerializationOptions(serializationOptions); }); + + var extension = options + .Options.FindExtension(); + + Assert.Same(serializationOptions, extension.SerializationOptions); + } + + [ConditionalFact] + public void Throws_if_specified_serializer_and_serialization_options() + { + var serializer = new JsonCosmosSerializer(); + var serializationOptions = new CosmosSerializationOptions { IgnoreNullValues = true }; + var options = Assert.Throws( + () => + new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + o => { o.Serializer(serializer).SerializationOptions(serializationOptions); })); + } + + [ConditionalFact] + public void Throws_if_specified_serialization_options_and_serializer() + { + var serializationOptions = new CosmosSerializationOptions { IgnoreNullValues = true }; + var serializer = new JsonCosmosSerializer(); + var options = Assert.Throws( + () => + new DbContextOptionsBuilder().UseCosmos( + "serviceEndPoint", + "authKeyOrResourceToken", + "databaseName", + o => { o.SerializationOptions(serializationOptions).Serializer(serializer); })); + } + [ConditionalFact] public void Can_create_options_with_wrong_region() {