diff --git a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs
index 452dda4e45e..cc1a2abcdf0 100644
--- a/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs
+++ b/src/EFCore.Cosmos/Infrastructure/CosmosDbContextOptionsBuilder.cs
@@ -205,13 +205,14 @@ public virtual CosmosDbContextOptionsBuilder MaxRequestsPerTcpConnection(int req
/// This reduces networking and CPU load by not sending the resource back over the network and serializing it on the client.
///
///
+ /// The EntityFrameworkCore default is since 11.0.
/// See Using DbContextOptions, and
/// Accessing Azure Cosmos DB with EF Core for more information and examples.
///
/// to have null resource
+ [Obsolete("Enabling ContentResponseOnWrite currently has no benefit for EF Core.")]
public virtual CosmosDbContextOptionsBuilder ContentResponseOnWriteEnabled(bool enabled = true)
- => WithOption(e => e.ContentResponseOnWriteEnabled(Check.NotNull(enabled)));
-
+ => WithOption(e => e.ContentResponseOnWriteEnabled(enabled));
///
/// Sets the to use.
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
index e1656712408..2f0474bea3f 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
@@ -74,6 +74,7 @@ protected CosmosOptionsExtension(CosmosOptionsExtension copyFrom)
_gatewayModeMaxConnectionLimit = copyFrom._gatewayModeMaxConnectionLimit;
_maxTcpConnectionsPerEndpoint = copyFrom._maxTcpConnectionsPerEndpoint;
_maxRequestsPerTcpConnection = copyFrom._maxRequestsPerTcpConnection;
+ _enableContentResponseOnWrite = copyFrom._enableContentResponseOnWrite;
_httpClientFactory = copyFrom._httpClientFactory;
_sessionTokenManagementMode = copyFrom._sessionTokenManagementMode;
_enableBulkExecution = copyFrom._enableBulkExecution;
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
index 5fa7ae5a2da..a48b7c8ac55 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
@@ -141,7 +141,7 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual bool? EnableContentResponseOnWrite { get; }
+ public virtual bool? EnableContentResponseOnWrite { get; private set; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -185,6 +185,7 @@ public virtual void Initialize(IDbContextOptions options)
GatewayModeMaxConnectionLimit = cosmosOptions.GatewayModeMaxConnectionLimit;
MaxTcpConnectionsPerEndpoint = cosmosOptions.MaxTcpConnectionsPerEndpoint;
MaxRequestsPerTcpConnection = cosmosOptions.MaxRequestsPerTcpConnection;
+ EnableContentResponseOnWrite = cosmosOptions.EnableContentResponseOnWrite;
HttpClientFactory = cosmosOptions.HttpClientFactory;
EnableBulkExecution = cosmosOptions.EnableBulkExecution;
}
@@ -216,6 +217,7 @@ public virtual void Validate(IDbContextOptions options)
|| GatewayModeMaxConnectionLimit != cosmosOptions.GatewayModeMaxConnectionLimit
|| MaxTcpConnectionsPerEndpoint != cosmosOptions.MaxTcpConnectionsPerEndpoint
|| MaxRequestsPerTcpConnection != cosmosOptions.MaxRequestsPerTcpConnection
+ || EnableContentResponseOnWrite != cosmosOptions.EnableContentResponseOnWrite
|| HttpClientFactory != cosmosOptions.HttpClientFactory
|| EnableBulkExecution != cosmosOptions.EnableBulkExecution
))
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
index 82841f88162..4daf7b928b2 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
@@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
///
/// Inspired by RelationalJsonUtilities.
///
-public static class CosmosSerializationUtilities
+public static class CosmosSerializationUtilities // @TODO: Can this be removed? Use document source instead? #34567
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Cosmos/Storage/Internal/ByteArrayConverter.cs b/src/EFCore.Cosmos/Storage/Internal/ByteArrayConverter.cs
index fe63cd57d67..875d5cde061 100644
--- a/src/EFCore.Cosmos/Storage/Internal/ByteArrayConverter.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/ByteArrayConverter.cs
@@ -56,7 +56,7 @@ public override object ReadJson(
{
if (reader.TokenType != JsonToken.StartArray)
{
- throw new Exception(reader.TokenType.ToString());
+ throw new InvalidOperationException(CoreStrings.JsonReaderInvalidTokenType(reader.TokenType));
}
var byteList = new List();
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index d885e7dd89c..31706f1bf2b 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -5,7 +5,7 @@
using System.Collections.ObjectModel;
using System.Net;
using System.Runtime.CompilerServices;
-using System.Text;
+using System.Runtime.InteropServices;
using Microsoft.Azure.Cosmos.Scripts;
using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
@@ -47,8 +47,6 @@ public class CosmosClientWrapper : ICosmosClientWrapper
private readonly string _databaseId;
private readonly IExecutionStrategy _executionStrategy;
private readonly IDiagnosticsLogger _commandLogger;
- private readonly IDiagnosticsLogger _databaseLogger;
- private readonly bool? _enableContentResponseOnWrite;
static CosmosClientWrapper()
{
@@ -68,8 +66,7 @@ public CosmosClientWrapper(
ISingletonCosmosClientWrapper singletonWrapper,
IDbContextOptions dbContextOptions,
IExecutionStrategy executionStrategy,
- IDiagnosticsLogger commandLogger,
- IDiagnosticsLogger databaseLogger)
+ IDiagnosticsLogger commandLogger)
{
var options = dbContextOptions.FindExtension();
@@ -77,25 +74,6 @@ public CosmosClientWrapper(
_databaseId = options!.DatabaseName;
_executionStrategy = executionStrategy;
_commandLogger = commandLogger;
- _databaseLogger = databaseLogger;
- _enableContentResponseOnWrite = options.EnableContentResponseOnWrite;
- }
-
- ///
- /// 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 static Stream Serialize(JToken document)
- {
- var stream = new MemoryStream();
- using var writer = new StreamWriter(stream, new UTF8Encoding(), bufferSize: 1024, leaveOpen: true);
-
- using var jsonWriter = new JsonTextWriter(writer);
- CosmosClientWrapper.Serializer.Serialize(jsonWriter, document);
- jsonWriter.Flush();
- return stream;
}
///
@@ -335,25 +313,25 @@ private static string GetPathFromRoot(IReadOnlyEntityType entityType)
///
public virtual Task CreateItemAsync(
string containerId,
- JToken document,
+ string documentId,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default)
- => _executionStrategy.ExecuteAsync((containerId, document, updateEntry, sessionTokenStorage, this), CreateItemOnceAsync, null, cancellationToken);
+ => _executionStrategy.ExecuteAsync((containerId, documentId, document, updateEntry, sessionTokenStorage, this), CreateItemOnceAsync, null, cancellationToken);
private static async Task CreateItemOnceAsync(
DbContext _,
- (string ContainerId, JToken Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
+ (string ContainerId, string DocumentId, ReadOnlyMemory Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
CancellationToken cancellationToken = default)
{
- using var stream = Serialize(parameters.Document);
-
var containerId = parameters.ContainerId;
+ var documentId = parameters.DocumentId;
var entry = parameters.Entry;
var wrapper = parameters.Wrapper;
var sessionTokenStorage = parameters.SessionTokenStorage;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
- var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite, sessionTokenStorage.GetSessionToken(containerId));
+ var itemRequestOptions = CreateItemRequestOptions(entry, sessionTokenStorage.GetSessionToken(containerId));
var partitionKeyValue = ExtractPartitionKeyValue(entry);
var preTriggers = GetTriggers(entry, TriggerType.Pre, TriggerOperation.Create);
var postTriggers = GetTriggers(entry, TriggerType.Post, TriggerOperation.Create);
@@ -370,6 +348,12 @@ private static async Task CreateItemOnceAsync(
}
}
+ if (!MemoryMarshal.TryGetArray(parameters.Document, out var segment) || segment.Array == null)
+ {
+ throw new UnreachableException("ReadOnlyMemory should have an underlying array.");
+ }
+
+ using var stream = new MemoryStream(segment.Array, segment.Offset, segment.Count);
using var response = await container.CreateItemStreamAsync(
stream,
partitionKeyValue,
@@ -381,7 +365,7 @@ private static async Task CreateItemOnceAsync(
response.Diagnostics.GetClientElapsedTime(),
response.Headers.RequestCharge,
response.Headers.ActivityId,
- parameters.Document["id"]!.ToString(),
+ documentId,
containerId,
partitionKeyValue);
@@ -399,7 +383,7 @@ private static async Task CreateItemOnceAsync(
public virtual Task ReplaceItemAsync(
string collectionId,
string documentId,
- JObject document,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default)
@@ -408,17 +392,15 @@ public virtual Task ReplaceItemAsync(
private static async Task ReplaceItemOnceAsync(
DbContext _,
- (string ContainerId, string ResourceId, JObject Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
+ (string ContainerId, string ResourceId, ReadOnlyMemory Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
CancellationToken cancellationToken = default)
{
- using var stream = Serialize(parameters.Document);
-
var containerId = parameters.ContainerId;
var entry = parameters.Entry;
var wrapper = parameters.Wrapper;
var sessionTokenStorage = parameters.SessionTokenStorage;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
- var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite, sessionTokenStorage.GetSessionToken(containerId));
+ var itemRequestOptions = CreateItemRequestOptions(entry, sessionTokenStorage.GetSessionToken(containerId));
var partitionKeyValue = ExtractPartitionKeyValue(entry);
var preTriggers = GetTriggers(entry, TriggerType.Pre, TriggerOperation.Replace);
var postTriggers = GetTriggers(entry, TriggerType.Post, TriggerOperation.Replace);
@@ -435,6 +417,12 @@ private static async Task ReplaceItemOnceAsync(
}
}
+ if (!MemoryMarshal.TryGetArray(parameters.Document, out var segment) || segment.Array == null)
+ {
+ throw new UnreachableException("ReadOnlyMemory should have an underlying array.");
+ }
+
+ using var stream = new MemoryStream(segment.Array, segment.Offset, segment.Count);
using var response = await container.ReplaceItemStreamAsync(
stream,
parameters.ResourceId,
@@ -481,7 +469,7 @@ private static async Task DeleteItemOnceAsync(
var sessionTokenStorage = parameters.SessionTokenStorage;
var items = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
- var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite, sessionTokenStorage.GetSessionToken(containerId));
+ var itemRequestOptions = CreateItemRequestOptions(entry, sessionTokenStorage.GetSessionToken(containerId));
var partitionKeyValue = ExtractPartitionKeyValue(entry);
var preTriggers = GetTriggers(entry, TriggerType.Pre, TriggerOperation.Delete);
var postTriggers = GetTriggers(entry, TriggerType.Post, TriggerOperation.Delete);
@@ -539,7 +527,7 @@ public virtual ICosmosTransactionalBatchWrapper CreateTransactionalBatch(string
var batch = container.CreateTransactionalBatch(partitionKeyValue);
- return new CosmosTransactionalBatchWrapper(batch, containerId, partitionKeyValue, checkSize, _enableContentResponseOnWrite);
+ return new CosmosTransactionalBatchWrapper(batch, containerId, partitionKeyValue, checkSize);
}
///
@@ -578,9 +566,9 @@ private static async Task ExecuteTransactionalBa
return ProcessBatchResponse(batch.CollectionId, response, batch.Entries, sessionTokenStorage);
}
- private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, bool? enableContentResponseOnWrite, string? sessionToken)
+ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, string? sessionToken)
{
- var helper = RequestOptionsHelper.Create(entry, enableContentResponseOnWrite);
+ var helper = RequestOptionsHelper.Create(entry);
var itemRequestOptions = new ItemRequestOptions
{
@@ -590,7 +578,6 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, b
if (helper != null)
{
itemRequestOptions.IfMatchEtag = helper.IfMatchEtag;
- itemRequestOptions.EnableContentResponseOnWrite = helper.EnableContentResponseOnWrite;
}
return itemRequestOptions;
@@ -681,7 +668,7 @@ private static CosmosTransactionalBatchResult ProcessBatchResponse(string contai
var entry = entries[i];
var item = response[i];
- ProcessWriteResponse(entry.Entry, (string)item.ETag, (Stream)item.ResourceStream);
+ ProcessWriteResponse(entry.Entry, item.ETag, item.ResourceStream);
}
return CosmosTransactionalBatchResult.Success;
@@ -689,23 +676,15 @@ private static CosmosTransactionalBatchResult ProcessBatchResponse(string contai
private static void ProcessWriteResponse(IUpdateEntry entry, string eTag, Stream? content)
{
- var etagProperty = entry.EntityType.GetETagProperty();
- if (etagProperty != null && entry.EntityState != EntityState.Deleted)
+ if (entry.EntityState == EntityState.Deleted)
{
- entry.SetStoreGeneratedValue(etagProperty, eTag);
+ return;
}
- var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
- if (jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate }
- && content != null)
+ var etagProperty = entry.EntityType.GetETagProperty();
+ if (etagProperty != null)
{
- using var responseStream = content;
- using var reader = new StreamReader(responseStream);
- using var jsonReader = new JsonTextReader(reader);
-
- var createdDocument = Serializer.Deserialize(jsonReader);
-
- entry.SetStoreGeneratedValue(jObjectProperty, createdDocument);
+ entry.SetStoreGeneratedValue(etagProperty, eTag);
}
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
index 8a6ae3fee28..5bb121ef0af 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
@@ -9,7 +9,6 @@
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Update.Internal;
-using Newtonsoft.Json.Linq;
using Database = Microsoft.EntityFrameworkCore.Storage.Database;
namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
@@ -166,6 +165,17 @@ private SaveGroups CreateSaveGroups(IList entries)
var entry = entries[i];
Check.DebugAssert(!entry.EntityType.IsAbstract(), $"{entry.EntityType} is abstract");
+ if (entry.EntityState == EntityState.Modified)
+ {
+ // @TODO: Seems expensive. Can we move this to the change tracker?
+ // #14121 ?
+ if (entry.EntityType.GetFlattenedPropertiesInHierarchy().Where(entry.IsModified).All(prop => prop.GetJsonPropertyName() == "") &&
+ !entry.EntityType.GetFlattenedComplexProperties().Any(entry.IsModified))
+ {
+ continue;
+ }
+ }
+
if (!entry.EntityType.IsDocumentRoot())
{
var root = GetRootDocument((InternalEntityEntry)entry);
@@ -309,8 +319,6 @@ private SaveGroups CreateSaveGroups(IList entries)
return null;
}
- JObject? document = null;
-
if (entry.SharedIdentityEntry != null)
{
if (entry.EntityState == EntityState.Deleted)
@@ -324,123 +332,82 @@ private SaveGroups CreateSaveGroups(IList entries)
}
}
- switch (operation)
- {
- case CosmosCudOperation.Create:
- var primaryKey = entityType.FindPrimaryKey();
- if (primaryKey != null)
+ if (operation == CosmosCudOperation.Create)
+ {
+ var primaryKey = entityType.FindPrimaryKey();
+ if (primaryKey != null)
+ {
+ // The code below checks for primary key properties that are not configured for value generation but have not
+ // had a non-sentinel (effectively, non-CLR default) value set. For composite keys, we only check if at least
+ // one property has value generation or a value set, since it is normal to have non-value generated parts of composite
+ // keys where one part is the CLR default. However, on Cosmos, we exclude the partition key properties from this
+ // check to ensure that, even if partition key properties have been set, at least one other primary key property is
+ // also set.
+ var partitionPropertyNeedsValue = true;
+ var propertyNeedsValue = true;
+ var allPkPropertiesAreFk = true;
+ IProperty? firstNonPartitionKeyProperty = null;
+
+ var partitionKeyProperties = entityType.GetPartitionKeyProperties();
+ foreach (var property in primaryKey.Properties)
{
- // The code below checks for primary key properties that are not configured for value generation but have not
- // had a non-sentinel (effectively, non-CLR default) value set. For composite keys, we only check if at least
- // one property has value generation or a value set, since it is normal to have non-value generated parts of composite
- // keys where one part is the CLR default. However, on Cosmos, we exclude the partition key properties from this
- // check to ensure that, even if partition key properties have been set, at least one other primary key property is
- // also set.
- var partitionPropertyNeedsValue = true;
- var propertyNeedsValue = true;
- var allPkPropertiesAreFk = true;
- IProperty? firstNonPartitionKeyProperty = null;
-
- var partitionKeyProperties = entityType.GetPartitionKeyProperties();
- foreach (var property in primaryKey.Properties)
+ if (property.IsForeignKey())
{
- if (property.IsForeignKey())
- {
- // FK properties conceptually get their value from the associated principal key, which can be handled
- // automatically by the update pipeline in some cases, so exclude from this check.
- continue;
- }
+ // FK properties conceptually get their value from the associated principal key, which can be handled
+ // automatically by the update pipeline in some cases, so exclude from this check.
+ continue;
+ }
+
+ allPkPropertiesAreFk = false;
- allPkPropertiesAreFk = false;
+ var isPartitionKeyProperty = partitionKeyProperties.Contains(property);
+ if (!isPartitionKeyProperty)
+ {
+ firstNonPartitionKeyProperty = property;
+ }
- var isPartitionKeyProperty = partitionKeyProperties.Contains(property);
+ if (property.ValueGenerated != ValueGenerated.Never
+ || entry.HasExplicitValue(property))
+ {
if (!isPartitionKeyProperty)
{
- firstNonPartitionKeyProperty = property;
+ propertyNeedsValue = false;
+ break;
}
- if (property.ValueGenerated != ValueGenerated.Never
- || entry.HasExplicitValue(property))
- {
- if (!isPartitionKeyProperty)
- {
- propertyNeedsValue = false;
- break;
- }
-
- partitionPropertyNeedsValue = false;
- }
+ partitionPropertyNeedsValue = false;
}
+ }
- if (!allPkPropertiesAreFk)
+ if (!allPkPropertiesAreFk)
+ {
+ try
{
- try
+ if (firstNonPartitionKeyProperty != null
+ && propertyNeedsValue)
{
- if (firstNonPartitionKeyProperty != null
- && propertyNeedsValue)
- {
- // There were non-partition key properties, so only throw if it is one of these that is not set,
- // ignoring partition key properties.
- Dependencies.Logger.PrimaryKeyValueNotSet(firstNonPartitionKeyProperty!);
- }
- else if (firstNonPartitionKeyProperty == null
- && partitionPropertyNeedsValue)
- {
- // There were no non-partition key properties in the primary key, so in this case check if any of these is not set.
- Dependencies.Logger.PrimaryKeyValueNotSet(primaryKey.Properties[0]);
- }
+ // There were non-partition key properties, so only throw if it is one of these that is not set,
+ // ignoring partition key properties.
+ Dependencies.Logger.PrimaryKeyValueNotSet(firstNonPartitionKeyProperty!);
}
- catch (InvalidOperationException ex)
+ else if (firstNonPartitionKeyProperty == null
+ && partitionPropertyNeedsValue)
{
- throw WrapUpdateException(ex, [entry]);
+ // There were no non-partition key properties in the primary key, so in this case check if any of these is not set.
+ Dependencies.Logger.PrimaryKeyValueNotSet(primaryKey.Properties[0]);
}
}
- }
-
- document = documentSource.GetCurrentDocument(entry);
- if (document != null)
- {
- documentSource.UpdateDocument(document, entry);
- }
- else
- {
- document = documentSource.CreateDocument(entry);
- }
- break;
-
- case CosmosCudOperation.Update:
- document = documentSource.GetCurrentDocument(entry);
- if (document != null)
- {
- if (documentSource.UpdateDocument(document, entry) == null)
- {
- return null;
- }
- }
- else
- {
- document = documentSource.CreateDocument(entry);
-
- var propertyName = entityType.FindDiscriminatorProperty()?.GetJsonPropertyName();
- if (propertyName != null)
+ catch (InvalidOperationException ex)
{
- document[propertyName] =
- JToken.FromObject(entityType.GetDiscriminatorValue()!, CosmosClientWrapper.Serializer);
+ throw WrapUpdateException(ex, [entry]);
}
}
- break;
-
- case CosmosCudOperation.Delete:
- break;
-
- default:
- throw new UnreachableException();
+ }
}
return new CosmosUpdateEntry
{
CollectionId = collectionId,
- Document = document,
DocumentSource = documentSource,
Entry = entry,
Operation = operation.Value
@@ -458,15 +425,14 @@ private IEnumerable CreateTransactions((Groupi
foreach (var updateEntry in batch.UpdateEntries)
{
- // Stream is disposed by Transaction.ExecuteAsync
- var stream = updateEntry.Document != null ? CosmosClientWrapper.Serialize(updateEntry.Document) : null;
+ var document = updateEntry.Operation != CosmosCudOperation.Delete ? updateEntry.DocumentSource.Serialize(updateEntry.Entry) : default;
// With AutoTransactionBehavior.Always, AddToTransaction will always return true.
- if (!AddToTransaction(transaction, updateEntry, stream))
+ if (!AddToTransaction(transaction, updateEntry, document))
{
yield return transaction;
transaction = _cosmosClient.CreateTransactionalBatch(batch.Key.ContainerId, batch.Key.PartitionKeyValue, checkSize);
- AddToTransaction(transaction, updateEntry, stream);
+ AddToTransaction(transaction, updateEntry, document);
continue;
}
@@ -483,13 +449,13 @@ private IEnumerable CreateTransactions((Groupi
}
}
- private bool AddToTransaction(ICosmosTransactionalBatchWrapper transaction, CosmosUpdateEntry updateEntry, Stream? stream)
+ private bool AddToTransaction(ICosmosTransactionalBatchWrapper transaction, CosmosUpdateEntry updateEntry, ReadOnlyMemory document)
{
var id = updateEntry.DocumentSource.GetId(updateEntry.Entry.SharedIdentityEntry ?? updateEntry.Entry);
return updateEntry.Operation switch
{
- CosmosCudOperation.Create => transaction.CreateItem(id, stream!, updateEntry.Entry),
- CosmosCudOperation.Update => transaction.ReplaceItem(id, stream!, updateEntry.Entry),
+ CosmosCudOperation.Create => transaction.CreateItem(id, document, updateEntry.Entry),
+ CosmosCudOperation.Update => transaction.ReplaceItem(id, document, updateEntry.Entry),
CosmosCudOperation.Delete => transaction.DeleteItem(id, updateEntry.Entry),
_ => throw new UnreachableException(),
};
@@ -499,24 +465,26 @@ private async Task SaveAsync(CosmosUpdateEntry updateEntry, CancellationTo
{
try
{
+ var id = updateEntry.DocumentSource.GetId(updateEntry.Entry.SharedIdentityEntry ?? updateEntry.Entry);
return updateEntry.Operation switch
{
CosmosCudOperation.Create => await _cosmosClient.CreateItemAsync(
updateEntry.CollectionId,
- updateEntry.Document!,
+ id,
+ updateEntry.DocumentSource.Serialize(updateEntry.Entry),
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
CosmosCudOperation.Update => await _cosmosClient.ReplaceItemAsync(
updateEntry.CollectionId,
- updateEntry.DocumentSource.GetId(updateEntry.Entry.SharedIdentityEntry ?? updateEntry.Entry),
- updateEntry.Document!,
+ id,
+ updateEntry.DocumentSource.Serialize(updateEntry.Entry),
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
CosmosCudOperation.Delete => await _cosmosClient.DeleteItemAsync(
updateEntry.CollectionId,
- updateEntry.DocumentSource.GetId(updateEntry.Entry),
+ id,
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
@@ -551,7 +519,7 @@ public virtual DocumentSource GetDocumentSource(IEntityType entityType)
if (!_documentCollections.TryGetValue(entityType, out var documentSource))
{
_documentCollections.Add(
- entityType, documentSource = new DocumentSource(entityType, this));
+ entityType, documentSource = new DocumentSource(entityType)); // @TODO: Make this singleton #34567
}
return documentSource;
@@ -625,7 +593,6 @@ private sealed class CosmosUpdateEntry
public required CosmosCudOperation Operation { get; init; }
public required string CollectionId { get; init; }
public required DocumentSource DocumentSource { get; init; }
- public required JObject? Document { get; init; }
}
private sealed record Grouping(string ContainerId, PartitionKey PartitionKeyValue);
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeOnlyReaderWriter.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeOnlyReaderWriter.cs
new file mode 100644
index 00000000000..72b61f3845a
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeOnlyReaderWriter.cs
@@ -0,0 +1,43 @@
+// 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;
+using Microsoft.EntityFrameworkCore.Storage.Json;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public sealed class CosmosJsonTimeOnlyReaderWriter : JsonValueReaderWriter
+{
+ private static readonly PropertyInfo InstanceProperty = typeof(CosmosJsonTimeOnlyReaderWriter).GetProperty(nameof(Instance))!;
+
+ ///
+ /// 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 static CosmosJsonTimeOnlyReaderWriter Instance { get; } = new();
+
+ private CosmosJsonTimeOnlyReaderWriter()
+ {
+ }
+
+ ///
+ public override TimeOnly FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
+ => TimeOnly.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture);
+
+ ///
+ public override void ToJsonTyped(Utf8JsonWriter writer, TimeOnly value)
+ => writer.WriteStringValue(value.ToString("HH:mm:ss.FFFFFFF", CultureInfo.InvariantCulture));
+
+ ///
+ public override Expression ConstructorExpression
+ => Expression.Property(null, InstanceProperty);
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeSpanReaderWriter.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeSpanReaderWriter.cs
new file mode 100644
index 00000000000..9b2de9f7ef1
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeSpanReaderWriter.cs
@@ -0,0 +1,43 @@
+// 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;
+using Microsoft.EntityFrameworkCore.Storage.Json;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public sealed class CosmosJsonTimeSpanReaderWriter : JsonValueReaderWriter
+{
+ private static readonly PropertyInfo InstanceProperty = typeof(CosmosJsonTimeSpanReaderWriter).GetProperty(nameof(Instance))!;
+
+ ///
+ /// 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 static CosmosJsonTimeSpanReaderWriter Instance { get; } = new();
+
+ private CosmosJsonTimeSpanReaderWriter()
+ {
+ }
+
+ ///
+ public override TimeSpan FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
+ => TimeSpan.Parse(manager.CurrentReader.GetString()!, CultureInfo.InvariantCulture);
+
+ ///
+ public override void ToJsonTyped(Utf8JsonWriter writer, TimeSpan value)
+ => writer.WriteStringValue(value.ToString("c", CultureInfo.InvariantCulture));
+
+ ///
+ public override Expression ConstructorExpression
+ => Expression.Property(null, InstanceProperty);
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
new file mode 100644
index 00000000000..77a0a4ffaaf
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class CosmosTimeOnlyTypeMapping : CosmosTypeMapping
+{
+ ///
+ /// 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 static new CosmosTimeOnlyTypeMapping Default { get; } = new();
+
+ ///
+ /// 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 CosmosTimeOnlyTypeMapping() : base(typeof(TimeOnly), null, null, null, CosmosJsonTimeOnlyReaderWriter.Instance)
+ {
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected CosmosTimeOnlyTypeMapping(CoreTypeMappingParameters parameters) : base(parameters)
+ {
+ }
+
+ ///
+ protected override CoreTypeMapping Clone(CoreTypeMappingParameters parameters)
+ => new CosmosTimeOnlyTypeMapping(parameters);
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
new file mode 100644
index 00000000000..86bb1c7844f
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class CosmosTimeSpanTypeMapping : CosmosTypeMapping
+{
+ ///
+ /// 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 static new CosmosTimeSpanTypeMapping Default { get; } = new();
+
+ ///
+ /// 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 CosmosTimeSpanTypeMapping() : base(typeof(TimeSpan), null, null, null, CosmosJsonTimeSpanReaderWriter.Instance)
+ {
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected CosmosTimeSpanTypeMapping(CoreTypeMappingParameters parameters) : base(parameters)
+ {
+ }
+
+ ///
+ protected override CoreTypeMapping Clone(CoreTypeMappingParameters parameters)
+ => new CosmosTimeSpanTypeMapping(parameters);
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchEntry.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchEntry.cs
index 146f00de430..0fae2e3def4 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchEntry.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchEntry.cs
@@ -11,7 +11,6 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
///
public class CosmosTransactionalBatchEntry
{
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
index f2f9fead506..852f750b3a9 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
@@ -22,7 +23,6 @@ public class CosmosTransactionalBatchWrapper : ICosmosTransactionalBatchWrapper
private readonly string _collectionId;
private readonly PartitionKey _partitionKeyValue;
private readonly bool _checkSize;
- private readonly bool? _enableContentResponseOnWrite;
private readonly List _entries = new();
///
@@ -35,14 +35,12 @@ public CosmosTransactionalBatchWrapper(
TransactionalBatch transactionalBatch,
string collectionId,
PartitionKey partitionKeyValue,
- bool checkSize,
- bool? enableContentResponseOnWrite)
+ bool checkSize)
{
_transactionalBatch = transactionalBatch;
_collectionId = collectionId;
_partitionKeyValue = partitionKeyValue;
_checkSize = checkSize;
- _enableContentResponseOnWrite = enableContentResponseOnWrite;
}
///
@@ -75,13 +73,13 @@ public CosmosTransactionalBatchWrapper(
/// 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 bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
+ public bool CreateItem(string id, ReadOnlyMemory document, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
- var size = stream.Length + itemRequestOptionsLength + OperationSerializationOverheadOverEstimateInBytes;
+ var size = document.Length + itemRequestOptionsLength + OperationSerializationOverheadOverEstimateInBytes;
if (_size + size > MaxSize && _size != 0)
{
@@ -90,6 +88,13 @@ public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
_size += size;
}
+ if (!MemoryMarshal.TryGetArray(document, out var segment) || segment.Array == null)
+ {
+ throw new UnreachableException("ReadOnlyMemory should have an underlying array.");
+ }
+
+ // Stream is disposed by disposing the response of TransactionalBatch.ExecuteAsync in CosmosClientWrapper.
+ var stream = new MemoryStream(segment.Array, segment.Offset, segment.Count);
_transactionalBatch.CreateItemStream(stream, itemRequestOptions);
_entries.Add(new CosmosTransactionalBatchEntry(updateEntry, CosmosCudOperation.Create, id));
@@ -102,13 +107,13 @@ public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
/// 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 bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEntry)
+ public bool ReplaceItem(string documentId, ReadOnlyMemory document, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
- var size = stream.Length + itemRequestOptionsLength + OperationSerializationOverheadOverEstimateInBytes + Encoding.UTF8.GetByteCount(documentId);
+ var size = document.Length + itemRequestOptionsLength + OperationSerializationOverheadOverEstimateInBytes + Encoding.UTF8.GetByteCount(documentId);
if (_size + size > MaxSize && _size != 0)
{
@@ -117,6 +122,13 @@ public bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEnt
_size += size;
}
+ if (!MemoryMarshal.TryGetArray(document, out var segment) || segment.Array == null)
+ {
+ throw new UnreachableException("ReadOnlyMemory should have an underlying array.");
+ }
+
+ // Stream is disposed by disposing the response of TransactionalBatch.ExecuteAsync in CosmosClientWrapper.
+ var stream = new MemoryStream(segment.Array, segment.Offset, segment.Count);
_transactionalBatch.ReplaceItemStream(documentId, stream, itemRequestOptions);
_entries.Add(new CosmosTransactionalBatchEntry(updateEntry, CosmosCudOperation.Update, documentId));
@@ -131,7 +143,7 @@ public bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEnt
///
public bool DeleteItem(string documentId, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -158,9 +170,9 @@ public bool DeleteItem(string documentId, IUpdateEntry updateEntry)
///
public TransactionalBatch GetTransactionalBatch() => _transactionalBatch;
- private TransactionalBatchItemRequestOptions? CreateItemRequestOptions(IUpdateEntry entry, bool? enableContentResponseOnWrite, out int size)
+ private TransactionalBatchItemRequestOptions? CreateItemRequestOptions(IUpdateEntry entry, out int size)
{
- var helper = RequestOptionsHelper.Create(entry, enableContentResponseOnWrite);
+ var helper = RequestOptionsHelper.Create(entry);
size = 0;
if (helper == null)
@@ -173,7 +185,6 @@ public bool DeleteItem(string documentId, IUpdateEntry updateEntry)
size += helper.IfMatchEtag.Length;
}
- // EnableContentResponseOnWrite is a header so no request body size for that.
- return new TransactionalBatchItemRequestOptions { IfMatchEtag = helper.IfMatchEtag, EnableContentResponseOnWrite = helper.EnableContentResponseOnWrite };
+ return new TransactionalBatchItemRequestOptions { IfMatchEtag = helper.IfMatchEtag };
}
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index 4464928a038..5cd4eb78f47 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Frozen;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
@@ -19,7 +20,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
///
public class CosmosTypeMappingSource : TypeMappingSource
{
- private readonly Dictionary _clrTypeMappings;
+ private readonly FrozenDictionary _clrTypeMappings;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -32,11 +33,13 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
=> _clrTypeMappings
= new Dictionary
{
+ { typeof(TimeOnly), CosmosTimeOnlyTypeMapping.Default },
+ { typeof(TimeSpan), CosmosTimeSpanTypeMapping.Default },
{
typeof(JObject), new CosmosTypeMapping(
typeof(JObject), jsonValueReaderWriter: dependencies.JsonValueReaderWriterSource.FindReaderWriter(typeof(JObject)))
}
- };
+ }.ToFrozenDictionary();
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -48,14 +51,40 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
// A provider should typically not override this because using the property directly causes problems with Migrations where
// the property does not exist. However, since the Cosmos provider doesn't have Migrations, it should be okay to use the property
// directly.
- => base.FindMapping(property) switch
- {
- CosmosTypeMapping mapping
- when property.GetVectorDistanceFunction() is { } distanceFunction
+ => property.GetVectorDistanceFunction() is { } distanceFunction
&& property.GetVectorDimensions() is { } dimensions
- => new CosmosVectorTypeMapping(mapping, new CosmosVectorType(distanceFunction, dimensions)),
- var other => other
- };
+ ? CreateVectorTypeMapping(property, new CosmosVectorType(distanceFunction, dimensions))
+ : base.FindMapping(property);
+
+ private CosmosVectorTypeMapping? CreateVectorTypeMapping(IProperty property, CosmosVectorType cosmosVectorType)
+ {
+ var clrType = property.ClrType;
+ var collectionType = clrType;
+ var isRom = clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>);
+ if (isRom)
+ {
+ collectionType = clrType.GetGenericArguments()[0].MakeArrayType();
+ }
+
+ var sequenceType = collectionType.GetSequenceType();
+ var elementMappingInfo = new TypeMappingInfo(sequenceType);
+
+ CoreTypeMapping? _ = null;
+ if (!TryFindJsonCollectionMapping(elementMappingInfo, collectionType, null, ref _, out var _, out var readerWriter))
+ {
+ return null;
+ }
+
+ var typeMapping = new CosmosVectorTypeMapping(clrType, cosmosVectorType, jsonValueReaderWriter: readerWriter);
+ if (isRom)
+ {
+ typeMapping = typeMapping.WithComposedConverter(
+ (ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(sequenceType))!,
+ (ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(sequenceType))!);
+ }
+
+ return typeMapping;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -82,7 +111,11 @@ when property.GetVectorDistanceFunction() is { } distanceFunction
var memoryType = clrType.TryGetElementType(typeof(ReadOnlyMemory<>));
if (memoryType != null)
{
- return new CosmosTypeMapping(clrType)
+ var elementMappingInfo = new TypeMappingInfo(memoryType);
+ CoreTypeMapping? typeMapping = null;
+ return !TryFindJsonCollectionMapping(elementMappingInfo, memoryType.MakeArrayType(), null, ref typeMapping, out var _, out var readerWriter)
+ ? null
+ : new CosmosTypeMapping(clrType, jsonValueReaderWriter: readerWriter)
.WithComposedConverter(
(ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(memoryType))!,
(ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(memoryType))!);
@@ -168,10 +201,37 @@ when property.GetVectorDistanceFunction() is { } distanceFunction
if (jsonValueReaderWriter == null
&& elementMapping.JsonValueReaderWriter != null)
{
- jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance(
- typeof(PlaceholderJsonStringKeyedDictionaryReaderWriter<>)
- .MakeGenericType(elementMapping.JsonValueReaderWriter.ValueType),
- elementMapping.JsonValueReaderWriter);
+ if (elementType.IsNullableValueType())
+ {
+ jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance(
+ typeof(CosmosJsonStringKeyedDictionaryNullableValueReaderWriter<>)
+ .MakeGenericType(elementMapping.JsonValueReaderWriter.ValueType),
+ elementMapping.JsonValueReaderWriter);
+ }
+ else if (elementType != typeof(string) && elementType.TryGetElementType(typeof(IEnumerable<>)) is { } nestedElementType)
+ {
+ if (nestedElementType.IsClass)
+ {
+ jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance(
+ typeof(CosmosJsonStringKeyedDictionaryReferenceCollectionValueReaderWriter<,>)
+ .MakeGenericType(elementType, nestedElementType),
+ elementMapping.JsonValueReaderWriter);
+ }
+ else
+ {
+ jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance(
+ typeof(CosmosJsonStringKeyedDictionaryCollectionValueReaderWriter<,>)
+ .MakeGenericType(elementType, nestedElementType),
+ elementMapping.JsonValueReaderWriter);
+ }
+ }
+ else
+ {
+ jsonValueReaderWriter = (JsonValueReaderWriter?)Activator.CreateInstance(
+ typeof(CosmosJsonStringKeyedDictionaryReaderWriter<>)
+ .MakeGenericType(elementType),
+ elementMapping.JsonValueReaderWriter);
+ }
}
return new CosmosTypeMapping(
@@ -200,8 +260,6 @@ private static ValueComparer CreateStringDictionaryComparer(
#pragma warning restore EF1001 // Internal EF Core API usage.
}
- // This ensures that the element reader/writers are not null when using Cosmos dictionary type mappings, but
- // is never actually used because Cosmos does not (yet) read and write JSON using this mechanism.
///
/// 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
@@ -209,7 +267,7 @@ private static ValueComparer CreateStringDictionaryComparer(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
#pragma warning disable EF1001
- public sealed class PlaceholderJsonStringKeyedDictionaryReaderWriter(JsonValueReaderWriter elementReaderWriter)
+ public sealed class CosmosJsonStringKeyedDictionaryReaderWriter(JsonValueReaderWriter elementReaderWriter)
: JsonValueReaderWriter>>, ICompositeJsonValueReaderWriter
#pragma warning restore EF1001
{
@@ -224,7 +282,7 @@ public sealed class PlaceholderJsonStringKeyedDictionaryReaderWriter(J
public override IEnumerable> FromJsonTyped(
ref Utf8JsonReaderManager manager,
object? existingObject = null)
- => throw new NotImplementedException("JsonValueReaderWriter infrastructure is not supported on Cosmos.");
+ => throw new NotImplementedException("JsonValueReader infrastructure for Dictionary is not supported on Cosmos."); // @TODO: #34567
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -233,14 +291,229 @@ public override IEnumerable> FromJsonTyped(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override void ToJsonTyped(Utf8JsonWriter writer, IEnumerable> value)
- => throw new NotImplementedException("JsonValueReaderWriter infrastructure is not supported on Cosmos.");
+ {
+ writer.WriteStartObject();
+ foreach (var element in value)
+ {
+ writer.WritePropertyName(element.Key);
+ if (element.Value is not null)
+ {
+ _elementReaderWriter.ToJsonTyped(writer, element.Value);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+
+ writer.WriteEndObject();
+ }
+
+ JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter
+ => _elementReaderWriter;
+
+ private readonly ConstructorInfo _constructorInfo
+ = typeof(CosmosJsonStringKeyedDictionaryReaderWriter)
+ .GetConstructor([typeof(JsonValueReaderWriter)])!;
+
+ ///
+ public override Expression ConstructorExpression
+#pragma warning disable EF9100
+#pragma warning disable EF1001
+ => Expression.New(_constructorInfo, ((ICompositeJsonValueReaderWriter)this).InnerReaderWriter.ConstructorExpression);
+#pragma warning restore EF1001
+#pragma warning restore EF9100
+ }
+
+ ///
+ /// 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.
+ ///
+#pragma warning disable EF1001
+ public sealed class CosmosJsonStringKeyedDictionaryNullableValueReaderWriter(JsonValueReaderWriter elementReaderWriter)
+ : JsonValueReaderWriter>>, ICompositeJsonValueReaderWriter
+ where TElement : struct
+#pragma warning restore EF1001
+ {
+ private readonly JsonValueReaderWriter _elementReaderWriter = (JsonValueReaderWriter)elementReaderWriter;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override IEnumerable> FromJsonTyped(
+ ref Utf8JsonReaderManager manager,
+ object? existingObject = null)
+ => throw new NotImplementedException("JsonValueReader infrastructure for Dictionary is not supported on Cosmos."); // @TODO: #34567
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override void ToJsonTyped(Utf8JsonWriter writer, IEnumerable> value)
+ {
+ writer.WriteStartObject();
+ foreach (var element in value)
+ {
+ writer.WritePropertyName(element.Key);
+ if (element.Value.HasValue)
+ {
+ _elementReaderWriter.ToJsonTyped(writer, element.Value.Value);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+
+ writer.WriteEndObject();
+ }
+
+ JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter
+ => _elementReaderWriter;
+
+ private readonly ConstructorInfo _constructorInfo
+ = typeof(CosmosJsonStringKeyedDictionaryNullableValueReaderWriter)
+ .GetConstructor([typeof(JsonValueReaderWriter)])!;
+
+ ///
+ public override Expression ConstructorExpression
+#pragma warning disable EF9100
+#pragma warning disable EF1001
+ => Expression.New(_constructorInfo, ((ICompositeJsonValueReaderWriter)this).InnerReaderWriter.ConstructorExpression);
+#pragma warning restore EF1001
+#pragma warning restore EF9100
+ }
+
+ ///
+ /// 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.
+ ///
+#pragma warning disable EF1001
+ public sealed class CosmosJsonStringKeyedDictionaryCollectionValueReaderWriter(JsonValueReaderWriter elementReaderWriter)
+ : JsonValueReaderWriter>>, ICompositeJsonValueReaderWriter
+ where TConcreteCollection : IEnumerable
+#pragma warning restore EF1001
+ {
+ private readonly JsonValueReaderWriter> _elementReaderWriter = (JsonValueReaderWriter>)elementReaderWriter;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override IEnumerable> FromJsonTyped(
+ ref Utf8JsonReaderManager manager,
+ object? existingObject = null)
+ => throw new NotImplementedException("JsonValueReader infrastructure for Dictionary is not supported on Cosmos."); // @TODO: #34567
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override void ToJsonTyped(Utf8JsonWriter writer, IEnumerable> value)
+ {
+ writer.WriteStartObject();
+ foreach (var element in value)
+ {
+ writer.WritePropertyName(element.Key);
+ if (element.Value is not null)
+ {
+ _elementReaderWriter.ToJsonTyped(writer, element.Value);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+
+ writer.WriteEndObject();
+ }
+
+ JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter
+ => _elementReaderWriter;
+
+ private readonly ConstructorInfo _constructorInfo
+ = typeof(CosmosJsonStringKeyedDictionaryCollectionValueReaderWriter)
+ .GetConstructor([typeof(JsonValueReaderWriter)])!;
+
+ ///
+ public override Expression ConstructorExpression
+#pragma warning disable EF9100
+#pragma warning disable EF1001
+ => Expression.New(_constructorInfo, ((ICompositeJsonValueReaderWriter)this).InnerReaderWriter.ConstructorExpression);
+#pragma warning restore EF1001
+#pragma warning restore EF9100
+ }
+
+ ///
+ /// 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.
+ ///
+#pragma warning disable EF1001
+ public sealed class CosmosJsonStringKeyedDictionaryReferenceCollectionValueReaderWriter(JsonValueReaderWriter elementReaderWriter)
+ : JsonValueReaderWriter>>, ICompositeJsonValueReaderWriter
+ where TConcreteCollection : IEnumerable
+ where TElement : class
+#pragma warning restore EF1001
+ {
+ private readonly JsonValueReaderWriter
- public override CoreTypeMapping WithComposedConverter(
+ public override CosmosVectorTypeMapping WithComposedConverter(
ValueConverter? converter,
ValueComparer? comparer = null,
ValueComparer? keyComparer = null,
CoreTypeMapping? elementMapping = null,
JsonValueReaderWriter? jsonValueReaderWriter = null)
- => new CosmosVectorTypeMapping(
+ => new(
Parameters.WithComposedConverter(converter, comparer, keyComparer, elementMapping, jsonValueReaderWriter),
VectorType);
diff --git a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
index 7c401a3c977..a2bef3f6a36 100644
--- a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
@@ -45,7 +45,8 @@ public interface ICosmosClientWrapper
///
Task CreateItemAsync(
string containerId,
- JToken document,
+ string documentId,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default);
@@ -59,7 +60,7 @@ Task CreateItemAsync(
Task ReplaceItemAsync(
string collectionId,
string documentId,
- JObject document,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default);
diff --git a/src/EFCore.Cosmos/Storage/Internal/ICosmosTransactionalBatchWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/ICosmosTransactionalBatchWrapper.cs
index 50615843c52..abb49647956 100644
--- a/src/EFCore.Cosmos/Storage/Internal/ICosmosTransactionalBatchWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/ICosmosTransactionalBatchWrapper.cs
@@ -43,7 +43,7 @@ public interface ICosmosTransactionalBatchWrapper
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry);
+ bool CreateItem(string id, ReadOnlyMemory document, IUpdateEntry updateEntry);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -59,7 +59,7 @@ public interface ICosmosTransactionalBatchWrapper
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEntry);
+ bool ReplaceItem(string documentId, ReadOnlyMemory document, IUpdateEntry updateEntry);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs b/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
index 7e4e20b46ba..a204e2c33ab 100644
--- a/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
@@ -11,10 +11,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
///
public class RequestOptionsHelper
{
- private RequestOptionsHelper(string? ifMatchEtag, bool enableContentResponseOnWrite)
+ private RequestOptionsHelper(string? ifMatchEtag)
{
IfMatchEtag = ifMatchEtag;
- EnableContentResponseOnWrite = enableContentResponseOnWrite;
}
///
@@ -25,13 +24,6 @@ private RequestOptionsHelper(string? ifMatchEtag, bool enableContentResponseOnWr
///
public virtual string? IfMatchEtag { get; }
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public virtual bool EnableContentResponseOnWrite { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -39,7 +31,7 @@ private RequestOptionsHelper(string? ifMatchEtag, bool enableContentResponseOnWr
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public static RequestOptionsHelper? Create(IUpdateEntry entry, bool? enableContentResponseOnWrite)
+ public static RequestOptionsHelper? Create(IUpdateEntry entry)
{
var etagProperty = entry.EntityType.GetETagProperty();
if (etagProperty == null)
@@ -54,33 +46,6 @@ private RequestOptionsHelper(string? ifMatchEtag, bool enableContentResponseOnWr
etag = converter.ConvertToProvider(etag);
}
- bool enabledContentResponse;
- if (enableContentResponseOnWrite.HasValue)
- {
- enabledContentResponse = enableContentResponseOnWrite.Value;
- }
- else
- {
- switch (entry.EntityState)
- {
- case EntityState.Modified:
- {
- var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
- enabledContentResponse = (jObjectProperty?.ValueGenerated & ValueGenerated.OnUpdate) == ValueGenerated.OnUpdate;
- break;
- }
- case EntityState.Added:
- {
- var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
- enabledContentResponse = (jObjectProperty?.ValueGenerated & ValueGenerated.OnAdd) == ValueGenerated.OnAdd;
- break;
- }
- default:
- enabledContentResponse = false;
- break;
- }
- }
-
- return new RequestOptionsHelper((string?)etag, enabledContentResponse);
+ return new RequestOptionsHelper((string?)etag);
}
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs
index f52ed9ada70..88310a1ea49 100644
--- a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Azure.Core;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
@@ -93,6 +92,8 @@ public SingletonCosmosClientWrapper(ICosmosSingletonOptions options)
configuration.AllowBulkExecution = options.EnableBulkExecution.Value;
}
+ configuration.EnableContentResponseOnWrite = options.EnableContentResponseOnWrite == true;
+
_client = options switch
{
{ ConnectionString: not null and not "" } => new CosmosClient(options.ConnectionString, configuration),
diff --git a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
index 2f5f1cfdd6d..0172a1f1c32 100644
--- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
+++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
@@ -2,12 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
+using System.Text.Encodings.Web;
+using System.Text.Json;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
-using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
using Microsoft.EntityFrameworkCore.Update.Internal;
-using Newtonsoft.Json.Linq;
namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal;
@@ -23,10 +23,8 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal;
public class DocumentSource
{
private readonly string _containerId;
- private readonly CosmosDatabaseWrapper _database;
private readonly IEntityType _entityType;
private readonly IProperty? _idProperty;
- private readonly IProperty? _jObjectProperty;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -34,13 +32,11 @@ public class DocumentSource
/// 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 DocumentSource(IEntityType entityType, CosmosDatabaseWrapper database)
+ public DocumentSource(IEntityType entityType)
{
_containerId = entityType.GetContainer()!;
- _database = database;
_entityType = entityType;
_idProperty = entityType.GetProperties().FirstOrDefault(p => p.GetJsonPropertyName() == CosmosJsonIdConvention.IdPropertyJsonName);
- _jObjectProperty = entityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
}
///
@@ -69,332 +65,165 @@ public virtual string GetId(IUpdateEntry entry)
/// 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 JObject CreateDocument(IUpdateEntry entry)
- => CreateDocument(entry, null);
+ public virtual ReadOnlyMemory Serialize(IUpdateEntry entry)
+ {
+ var internalEntry = (IInternalEntry)entry;
+ var stream = new MemoryStream();
+ using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }))
+ {
+ WriteJsonObject(writer, internalEntry, internalEntry.StructuralType, 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 JObject CreateDocument(IUpdateEntry entry, int? ordinal)
- => CreateDocument((IInternalEntry)entry, entry.EntityType, ordinal);
+ return new ReadOnlyMemory(stream.GetBuffer(), 0, (int)stream.Length);
+ }
- private JObject CreateDocument(IInternalEntry entry, ITypeBase structuralType, int? ordinal)
+ private void WriteJsonObject(
+ Utf8JsonWriter writer,
+ IInternalEntry entry,
+ ITypeBase structuralType,
+ int? ordinal)
{
- var document = new JObject();
+ writer.WriteStartObject();
+
foreach (var property in structuralType.GetProperties())
{
- var storeName = property.GetJsonPropertyName();
- if (storeName.Length != 0)
- {
- document[storeName] = ConvertPropertyValue(property, entry);
- }
- else if (entry.HasTemporaryValue(property))
- {
- if (ordinal != null
- && property.IsOrdinalKeyProperty())
- {
- entry.SetStoreGeneratedValue(property, ordinal.Value);
- }
- }
- }
+ var jsonPropertyName = property.GetJsonPropertyName();
- if (structuralType is IEntityType entityType)
- {
- foreach (var embeddedNavigation in entityType.GetNavigations())
+ if (jsonPropertyName == "")
{
- var fk = embeddedNavigation.ForeignKey;
- if (!fk.IsOwnership
- || embeddedNavigation.IsOnDependent
- || fk.DeclaringEntityType.IsDocumentRoot())
+ if (property.IsKey() && property.IsOrdinalKeyProperty())
{
- continue;
- }
-
- var embeddedValue = entry.GetCurrentValue(embeddedNavigation);
- var embeddedPropertyName = fk.DeclaringEntityType.GetContainingPropertyName()!;
- if (embeddedValue == null)
- {
- document[embeddedPropertyName] = null;
- }
- else if (fk.IsUnique)
- {
- var dependentEntry = ((InternalEntityEntry)entry).StateManager.TryGetEntry(embeddedValue, fk.DeclaringEntityType)!;
- document[embeddedPropertyName] = _database.GetDocumentSource(dependentEntry.EntityType).CreateDocument(dependentEntry);
- }
- else
- {
- SetTemporaryOrdinals(entry, fk, embeddedValue);
-
- var stateManager = ((InternalEntityEntry)entry).StateManager;
-
- var embeddedOrdinal = 1;
- var array = new JArray();
- foreach (var dependent in (IEnumerable)embeddedValue)
- {
- var dependentEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!;
- array.Add(_database.GetDocumentSource(dependentEntry.EntityType).CreateDocument(dependentEntry, embeddedOrdinal));
- embeddedOrdinal++;
- }
-
- document[embeddedPropertyName] = array;
+ entry.SetStoreGeneratedValue(property, ordinal!.Value + 1, setModified: false);
}
+ continue;
}
- }
- foreach (var complexProperty in structuralType.GetComplexProperties())
- {
- var embeddedValue = entry.GetCurrentValue(complexProperty);
- var embeddedPropertyName = complexProperty.GetJsonPropertyName();
- if (embeddedValue == null)
- {
- document[embeddedPropertyName] = null;
- }
- else if (!complexProperty.IsCollection)
+ var propertyValue = entry.GetCurrentValue(property);
+ writer.WritePropertyName(jsonPropertyName);
+
+ var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter;
+ if (propertyValue is not null || jsonValueReaderWriter?.HandlesNullWrites == true)
{
- document[embeddedPropertyName] = CreateDocument(entry, complexProperty.ComplexType, null);
+ Check.DebugAssert(jsonValueReaderWriter is not null, $"Missing JsonValueReaderWriter for property: {property}");
+ jsonValueReaderWriter.ToJson(writer, propertyValue!);
}
else
{
- var internalEntry = (InternalEntryBase)entry;
-
- var embeddedOrdinal = 0;
- var array = new JArray();
- foreach (var dependent in (IEnumerable)embeddedValue)
- {
- var dependentEntry = internalEntry.GetComplexCollectionEntry(complexProperty, embeddedOrdinal);
- array.Add(CreateDocument(dependentEntry, complexProperty.ComplexType, null));
- embeddedOrdinal++;
- }
-
- document[embeddedPropertyName] = array;
+ writer.WriteNullValue();
}
}
- return document;
- }
-
- ///
- /// 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 JObject? UpdateDocument(JObject document, IUpdateEntry entry)
- => UpdateDocument(document, entry, 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 JObject? UpdateDocument(JObject document, IUpdateEntry entry, int? ordinal)
- => UpdateDocument(document, (IInternalEntry)entry, entry.EntityType, ordinal);
-
- private JObject? UpdateDocument(JObject document, IInternalEntry entry, ITypeBase structuralType, int? ordinal)
- {
- var anyPropertyUpdated = false;
- foreach (var property in structuralType.GetProperties())
+ foreach (var complexProperty in structuralType.GetComplexProperties())
{
- if (ordinal != null
- && entry.HasTemporaryValue(property)
- && property.IsOrdinalKeyProperty())
- {
- entry.SetStoreGeneratedValue(property, ordinal.Value);
- }
+ var jsonPropertyName = complexProperty.GetJsonPropertyName()!;
+ writer.WritePropertyName(jsonPropertyName);
- if (entry.EntityState == EntityState.Added
- || (entry is IUpdateEntry updateEntry && updateEntry.SharedIdentityEntry != null)
- || entry.IsModified(property))
- {
- var storeName = property.GetJsonPropertyName();
- if (storeName.Length != 0)
- {
- document[storeName] = ConvertPropertyValue(property, entry);
- anyPropertyUpdated = true;
- }
- }
+ WriteJsonStructuralPropertyValue(writer, entry, complexProperty, complexProperty.ComplexType, complexProperty.IsCollection);
}
if (structuralType is IEntityType entityType)
{
- foreach (var ownedNavigation in entityType.GetNavigations())
+ foreach (var navigation in entityType.GetNavigations())
{
- var fk = ownedNavigation.ForeignKey;
- if (!fk.IsOwnership
- || ownedNavigation.IsOnDependent
- || fk.DeclaringEntityType.IsDocumentRoot())
+ // skip back-references to the parent
+ var fk = navigation.ForeignKey;
+ if (!fk.IsOwnership || navigation.IsOnDependent)
{
continue;
}
- var embeddedDocumentSource = _database.GetDocumentSource(fk.DeclaringEntityType);
- var embeddedValue = entry.GetCurrentValue(ownedNavigation);
- var embeddedPropertyName = fk.DeclaringEntityType.GetContainingPropertyName()!;
- if (embeddedValue == null)
- {
- if (document[embeddedPropertyName] != null)
- {
- document[embeddedPropertyName] = null;
- anyPropertyUpdated = true;
- }
- }
- else if (fk.IsUnique)
- {
- var embeddedEntry = ((InternalEntityEntry)entry).StateManager.TryGetEntry(embeddedValue, fk.DeclaringEntityType)!;
-
- var embeddedDocument = embeddedDocumentSource.GetCurrentDocument(embeddedEntry);
- embeddedDocument = embeddedDocument != null
- ? embeddedDocumentSource.UpdateDocument(embeddedDocument, embeddedEntry, null)
- : embeddedDocumentSource.CreateDocument(embeddedEntry, null);
-
- if (embeddedDocument != null)
- {
- document[embeddedPropertyName] = embeddedDocument;
- anyPropertyUpdated = true;
- }
- }
- else
- {
- SetTemporaryOrdinals(entry, fk, embeddedValue);
-
- var stateManager = ((InternalEntityEntry)entry).StateManager;
+ var jsonPropertyName = navigation.TargetEntityType.GetContainingPropertyName();
- var embeddedOrdinal = 1;
- var array = new JArray();
- foreach (var dependent in (IEnumerable)embeddedValue)
- {
- var embeddedEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!;
+ Debug.Assert(jsonPropertyName != null, "Containing property name should not be null on owned navigation.");
- var embeddedDocument = embeddedDocumentSource.GetCurrentDocument(embeddedEntry);
- embeddedDocument = embeddedDocument != null
- ? embeddedDocumentSource.UpdateDocument(embeddedDocument, embeddedEntry, embeddedOrdinal) ?? embeddedDocument
- : embeddedDocumentSource.CreateDocument(embeddedEntry, embeddedOrdinal);
+ writer.WritePropertyName(jsonPropertyName);
- array.Add(embeddedDocument);
- embeddedOrdinal++;
- }
-
- document[embeddedPropertyName] = array;
- anyPropertyUpdated = true;
- }
+ WriteJsonStructuralPropertyValue(writer, entry, navigation, navigation.TargetEntityType, navigation.IsCollection);
}
}
- foreach (var complexProperty in structuralType.GetComplexProperties())
+ writer.WriteEndObject();
+ }
+
+ private void WriteJsonStructuralPropertyValue(
+ Utf8JsonWriter writer,
+ IInternalEntry parentEntry,
+ IPropertyBase property,
+ ITypeBase structuralType,
+ bool isCollection)
+ {
+ var value = parentEntry.GetCurrentValue(property);
+ if (isCollection)
+ {
+ WriteJsonArray(writer, parentEntry, property, structuralType, (IEnumerable?)value);
+ }
+ else
{
- var embeddedValue = entry.GetCurrentValue(complexProperty);
- var embeddedPropertyName = complexProperty.GetJsonPropertyName();
- if (embeddedValue == null)
+ if (value is null)
{
- if (document[embeddedPropertyName] != null)
- {
- document[embeddedPropertyName] = null;
- anyPropertyUpdated = true;
- }
+ writer.WriteNullValue();
+ return;
}
- else if (!complexProperty.IsCollection)
- {
- var embeddedDocument = document[embeddedPropertyName] as JObject;
- embeddedDocument = embeddedDocument != null
- ? UpdateDocument(embeddedDocument, entry, complexProperty.ComplexType, null)
- : CreateDocument(entry, complexProperty.ComplexType, null);
- if (embeddedDocument != null)
- {
- document[embeddedPropertyName] = embeddedDocument;
- anyPropertyUpdated = true;
- }
- }
- else
- {
- var embeddedCollection = document[embeddedPropertyName] as JArray;
- var embeddedOrdinal = 0;
- var array = new JArray();
- foreach (var dependent in (IEnumerable)embeddedValue)
- {
- var embeddedEntry = entry.GetComplexCollectionEntry(complexProperty, embeddedOrdinal);
-
- var embeddedDocument = embeddedEntry.OriginalOrdinal != -1 ? embeddedCollection?[embeddedEntry.OriginalOrdinal] as JObject : null;
- embeddedDocument = embeddedDocument != null
- ? UpdateDocument(embeddedDocument, embeddedEntry, complexProperty.ComplexType, null) ?? embeddedDocument
- : CreateDocument(embeddedEntry, complexProperty.ComplexType, null);
-
- array.Add(embeddedDocument);
- embeddedOrdinal++;
- }
+ var entry = structuralType is IComplexType
+ ? parentEntry
+ : ((InternalEntityEntry)parentEntry).StateManager.TryGetEntry(value!, (IEntityType)structuralType)!;
- document[embeddedPropertyName] = array;
- anyPropertyUpdated = true;
- }
+ WriteJsonObject(writer, entry, structuralType, null);
}
-
- return anyPropertyUpdated ? document : null;
}
- private static void SetTemporaryOrdinals(
- IInternalEntry entry,
- IForeignKey fk,
- object embeddedValue)
+ private void WriteJsonArray(
+ Utf8JsonWriter writer,
+ IInternalEntry parentEntry,
+ IPropertyBase property,
+ ITypeBase structuralType,
+ IEnumerable? value)
{
- var embeddedOrdinal = 1;
- var ordinalKeyProperty = FindOrdinalKeyProperty(fk.DeclaringEntityType);
- if (ordinalKeyProperty != null)
+ if (value is null)
{
- var stateManager = ((InternalEntityEntry)entry).StateManager;
- var shouldSetTemporaryKeys = false;
- foreach (var dependent in (IEnumerable)embeddedValue)
- {
- var embeddedEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!;
-
- if ((int)embeddedEntry.GetCurrentValue(ordinalKeyProperty)! != embeddedOrdinal
- && !embeddedEntry.HasTemporaryValue(ordinalKeyProperty))
- {
- shouldSetTemporaryKeys = true;
- break;
- }
-
- embeddedOrdinal++;
- }
+ writer.WriteNullValue();
+ return;
+ }
- if (shouldSetTemporaryKeys)
+ // When items in an owned entity collection are reordered, assigning ordinal key values
+ // sequentially can cause identity map conflicts - e.g. assigning ordinal 2 to a new
+ // entry while another tracked entry still holds ordinal 2.
+ // To avoid this, first assign temporary negative ordinals to move all entries out of
+ // the way, then let WriteJsonObject assign the correct final ordinals.
+ if (property is INavigation { TargetEntityType: var entityType })
+ {
+ var ordinalKeyProperty = entityType.FindPrimaryKey()?.Properties
+ .FirstOrDefault(p => p.IsOrdinalKeyProperty());
+ if (ordinalKeyProperty != null)
{
- var temporaryOrdinal = -1;
- foreach (var dependent in (IEnumerable)embeddedValue)
+ var stateManager = ((InternalEntityEntry)parentEntry).StateManager;
+ var tempOrdinal = -1;
+ foreach (var collectionElement in value)
{
- var embeddedEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!;
-
- embeddedEntry.SetTemporaryValue(ordinalKeyProperty, temporaryOrdinal, setModified: false);
-
- temporaryOrdinal--;
+ var tempEntry = stateManager.TryGetEntry(collectionElement, entityType);
+ tempEntry?.SetTemporaryValue(ordinalKeyProperty, tempOrdinal--, setModified: false);
}
}
}
- }
- private static IProperty? FindOrdinalKeyProperty(IEntityType entityType)
- => entityType.FindPrimaryKey()!.Properties.FirstOrDefault(p => p.GetJsonPropertyName().Length == 0 && p.IsOrdinalKeyProperty());
-
- ///
- /// 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 JObject? GetCurrentDocument(IUpdateEntry entry)
- => _jObjectProperty != null
- ? (JObject?)(entry.SharedIdentityEntry ?? entry).GetCurrentValue(_jObjectProperty)
- : null;
+ var i = 0;
+ writer.WriteStartArray();
+ foreach (var collectionElement in value)
+ {
+ var entry = structuralType is IComplexType complexType
+ ? (IInternalEntry)parentEntry.GetComplexCollectionEntry(complexType.ComplexProperty, i)
+ : ((InternalEntityEntry)parentEntry).StateManager.TryGetEntry(collectionElement, (IEntityType)structuralType)!;
+
+ WriteJsonObject(
+ writer,
+ entry,
+ structuralType,
+ ordinal: i++);
+ }
- private static JToken? ConvertPropertyValue(IProperty property, IInternalEntry entry)
- {
- var value = entry.GetCurrentProviderValue(property);
- return value == null
- ? null
- : (value as JToken) ?? JToken.FromObject(value, CosmosClientWrapper.Serializer);
+ writer.WriteEndArray();
+ return;
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
index dffdabadae9..890ff565da1 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
@@ -187,7 +187,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
public class CosmosFixture : ServiceProviderFixtureBase
{
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+ => base.AddOptions(builder).ConfigureWarnings(w =>
+ w.Ignore(CosmosEventId.NoPartitionKeyDefined));
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
index 46c51609e08..66d1b80a1b9 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Newtonsoft.Json.Linq;
using Xunit.Sdk;
namespace Microsoft.EntityFrameworkCore;
@@ -44,41 +43,36 @@ public async Task Can_change_complex_collection_element()
}
[ConditionalFact]
- public async Task Can_add_complex_collection_element()
+ public async Task Can_change_complex_collection_element_complex_collection()
{
await using var context = CreateContext();
var pub = CreatePubWithCollections(context);
await context.AddAsync(pub);
await context.SaveChangesAsync();
- pub.Activities.Add(new ActivityWithCollection { Name = "NewActivity" });
+ pub.Activities[0].Teams.Add(new Team { Name = "NewTeam" });
await context.SaveChangesAsync();
await using var assertContext = CreateContext();
var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id);
- Assert.Equivalent("NewActivity", dbPub.Activities.Last().Name);
- Assert.Equivalent(pub.Activities.Count, dbPub.Activities.Count);
+ Assert.Equal("NewTeam", dbPub.Activities[0].Teams.Last().Name);
}
[ConditionalFact]
- public async Task Can_add_and_dynamically_update_complex_collection_element()
+ public async Task Can_add_complex_collection_element()
{
await using var context = CreateContext();
var pub = CreatePubWithCollections(context);
await context.AddAsync(pub);
await context.SaveChangesAsync();
- var pubJObject = context.Entry(pub).Property("__jObject").CurrentValue;
- pubJObject["Activities"]![0]!["test"] = "test";
- pub.Activities.Insert(0, new ActivityWithCollection { Name = "NewActivity" });
-
+ pub.Activities.Add(new ActivityWithCollection { Name = "NewActivity" });
await context.SaveChangesAsync();
await using var assertContext = CreateContext();
var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id);
- var dbPubJObject = assertContext.Entry(dbPub).Property("__jObject").CurrentValue;
- Assert.Equal("test", dbPubJObject["Activities"]![1]!["test"]);
- Assert.Equal("NewActivity", dbPubJObject["Activities"]![0]!["Name"]);
+ Assert.Equivalent("NewActivity", dbPub.Activities.Last().Name);
+ Assert.Equivalent(pub.Activities.Count, dbPub.Activities.Count);
}
public override Task Can_save_null_second_level_complex_property_with_required_properties(bool async)
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index 3a576324a62..f69473226bf 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -5,10 +5,14 @@ namespace Microsoft.EntityFrameworkCore;
#nullable disable
-public class CosmosConcurrencyTest(CosmosConcurrencyTest.CosmosFixture fixture) : IClassFixture
+public class CosmosConcurrencyTest(CosmosConcurrencyTest.CosmosFixture fixture) : IClassFixture, IAsyncLifetime
{
private const string DatabaseName = "CosmosConcurrencyTest";
+ protected ServiceProvider ServiceProvider { get; } = new ServiceCollection()
+ .AddEntityFrameworkCosmos()
+ .BuildServiceProvider();
+
protected CosmosFixture Fixture { get; } = fixture;
[ConditionalFact]
@@ -54,14 +58,17 @@ public virtual Task Updating_then_updating_the_same_entity_results_in_DbUpdateCo
[ConditionalTheory, InlineData(null), InlineData(true), InlineData(false)]
public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentResponseOnWriteEnabled)
{
- var options = new DbContextOptionsBuilder(Fixture.CreateOptions())
+ var options = Fixture.TestStore.AddProviderOptions(Fixture.AddOptions(new DbContextOptionsBuilder()
.UseCosmos(o =>
{
if (contentResponseOnWriteEnabled != null)
{
+#pragma warning disable CS0618 // Type or member is obsolete
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
+#pragma warning restore CS0618 // Type or member is obsolete
}
- })
+ })))
+ .UseInternalServiceProvider(ServiceProvider)
.Options;
var customer = new Customer
@@ -114,14 +121,17 @@ public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentRespo
[ConditionalTheory, InlineData(null), InlineData(true), InlineData(false)]
public async Task Etag_is_updated_in_derived_entity_after_SaveChanges(bool? contentResponseOnWriteEnabled)
{
- var options = new DbContextOptionsBuilder(Fixture.CreateOptions())
+ var options = Fixture.TestStore.AddProviderOptions(Fixture.AddOptions(new DbContextOptionsBuilder()
.UseCosmos(o =>
{
if (contentResponseOnWriteEnabled != null)
{
+#pragma warning disable CS0618 // Type or member is obsolete
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
+#pragma warning restore CS0618 // Type or member is obsolete
}
- })
+ })))
+ .UseInternalServiceProvider(ServiceProvider)
.Options;
var customer = new PremiumCustomer
@@ -238,6 +248,9 @@ protected virtual ConcurrencyContext CreateContext()
protected virtual ConcurrencyContext CreateContext(DbContextOptions options)
=> new ConcurrencyContext(options);
+ public virtual Task InitializeAsync() => Task.CompletedTask;
+ public virtual async Task DisposeAsync() => await ServiceProvider.DisposeAsync();
+
public class CosmosFixture : SharedStoreFixtureBase
{
protected override string StoreName
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
index 08fc076cb21..9392722784e 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
@@ -279,41 +279,6 @@ public virtual async Task SaveChanges_entity_too_large_throws()
Assert.Equal(0, customersCount);
}
- [ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_exactly_2_mib_does_not_split_and_one_byte_over_splits(bool oneByteOver)
- {
- using var context = Fixture.CreateContext();
-
- var customer1 = new Customer { Id = new string('x', 1023), PartitionKey = new string('x', 1023) };
- var customer2 = new Customer { Id = new string('y', 1023), PartitionKey = new string('x', 1023) };
-
- context.Customers.Add(customer1);
- context.Customers.Add(customer2);
-
- await context.SaveChangesAsync();
- Fixture.ListLoggerFactory.Clear();
-
- customer1.Name = new string('x', 1044994);
- customer2.Name = new string('x', 1044994);
-
- if (oneByteOver)
- {
- customer1.Name += 'x';
- }
-
- await context.SaveChangesAsync();
- using var assertContext = Fixture.CreateContext();
- Assert.Equal(2, (await context.Customers.ToListAsync()).Count);
-
- if (oneByteOver)
- {
- Assert.Equal(2, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
- }
- else
- {
- Assert.Equal(1, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
- }
- }
[ConditionalFact]
public virtual async Task SaveChanges_too_large_entry_after_smaller_throws_after_saving_smaller()
@@ -331,22 +296,6 @@ public virtual async Task SaveChanges_too_large_entry_after_smaller_throws_after
Assert.Equal("1", (await assertContext.Customers.FirstAsync()).Id);
}
- [ConditionalFact]
- public virtual async Task SaveChanges_transaction_behavior_always_payload_exactly_2_mib()
- {
- using var context = Fixture.CreateContext();
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
-
- context.Customers.Add(new Customer { Id = "1", Name = new string('x', 1048291), PartitionKey = "1" });
- context.Customers.Add(new Customer { Id = "2", Name = new string('x', 1048291), PartitionKey = "1" });
-
- await context.SaveChangesAsync();
-
- using var assertContext = Fixture.CreateContext();
- var customersCount = await assertContext.Customers.CountAsync();
- Assert.Equal(2, customersCount);
- }
-
[ConditionalFact]
public virtual async Task SaveChanges_transaction_behavior_always_payload_larger_than_cosmos_limit_throws()
{
@@ -366,18 +315,19 @@ public virtual async Task SaveChanges_transaction_behavior_always_payload_larger
Assert.Equal(0, customersCount);
}
- private const int nameLengthToExceed2MiBWithSpecialCharIdOnUpdate = 1046358;
+ ///
+ /// How many bytes of data can be in a customer's properties to reach the max request size in a EF transactional batch request
+ ///
+ private const int MaxSerializedCustomerTransactionalBatchRequestSize = 2094389;
+ private const int MaxKeySize = 1023;
[ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_update_id_contains_special_chars_which_makes_request_larger_than_2_mib_splits_into_2_batches(bool isIdSpecialChar)
+ public virtual async Task SaveChanges_exactly_2_mib_does_not_split_and_one_byte_over_splits(bool oneByteOver)
{
using var context = Fixture.CreateContext();
- var id1 = isIdSpecialChar ? new string('€', 341) : new string('x', 341);
- var id2 = isIdSpecialChar ? new string('Ω', 341) : new string('y', 341);
-
- var customer1 = new Customer { Id = id1, PartitionKey = new string('€', 341) };
- var customer2 = new Customer { Id = id2, PartitionKey = new string('€', 341) };
+ var customer1 = new Customer { Id = new string('x', MaxKeySize), PartitionKey = new string('x', MaxKeySize) };
+ var customer2 = new Customer { Id = new string('y', MaxKeySize), PartitionKey = new string('x', MaxKeySize) };
context.Customers.Add(customer1);
context.Customers.Add(customer2);
@@ -385,15 +335,19 @@ public virtual async Task SaveChanges_update_id_contains_special_chars_which_mak
await context.SaveChangesAsync();
Fixture.ListLoggerFactory.Clear();
- customer1.Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate);
- customer2.Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate);
+ customer1.Name = new string('x', MaxSerializedCustomerTransactionalBatchRequestSize / 2 - 2 * MaxKeySize);
+ customer2.Name = new string('x', MaxSerializedCustomerTransactionalBatchRequestSize / 2 - 2 * MaxKeySize);
+
+ if (oneByteOver)
+ {
+ customer1.Name += 'x';
+ }
await context.SaveChangesAsync();
using var assertContext = Fixture.CreateContext();
- Assert.Equal(2, (await context.Customers.ToListAsync()).Count);
+ Assert.Equal(2, (await assertContext.Customers.ToListAsync()).Count);
- // The id being a special character should make the difference whether this fits in 1 batch.
- if (isIdSpecialChar)
+ if (oneByteOver)
{
Assert.Equal(2, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}
@@ -403,136 +357,44 @@ public virtual async Task SaveChanges_update_id_contains_special_chars_which_mak
}
}
+ private const int MaxSpecialCharsInId = MaxKeySize / 3;
+
[ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_create_id_contains_special_chars_which_would_make_request_larger_than_2_mib_on_update_does_not_split_into_2_batches_for_create(bool isIdSpecialChar)
+ public virtual async Task SaveChanges_update_id_contains_special_chars_which_makes_request_larger_than_2_mib_splits_into_2_batches(bool isIdSpecialChar)
{
- Fixture.ListLoggerFactory.Clear();
using var context = Fixture.CreateContext();
+ Fixture.ListLoggerFactory.Clear();
- var id1 = isIdSpecialChar ? new string('€', 341) : new string('x', 341);
- var id2 = isIdSpecialChar ? new string('Ω', 341) : new string('y', 341);
+ var id1 = isIdSpecialChar ? new string('€', MaxSpecialCharsInId) : new string('x', MaxSpecialCharsInId);
+ var id2 = isIdSpecialChar ? new string('Ω', MaxSpecialCharsInId) : new string('y', MaxSpecialCharsInId);
- var customer1 = new Customer { Id = id1, Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate), PartitionKey = new string('€', 341) };
- var customer2 = new Customer { Id = id2, Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate), PartitionKey = new string('€', 341) };
+ var customer1 = new Customer { Id = id1, Name = new string('x', MaxSerializedCustomerTransactionalBatchRequestSize / 2 - MaxKeySize - 1), PartitionKey = new string('€', MaxSpecialCharsInId) };
+ var customer2 = new Customer { Id = id2, Name = new string('x', MaxSerializedCustomerTransactionalBatchRequestSize / 2 - MaxKeySize - 1), PartitionKey = new string('€', MaxSpecialCharsInId) };
context.Customers.Add(customer1);
context.Customers.Add(customer2);
await context.SaveChangesAsync();
- using var assertContext = Fixture.CreateContext();
- Assert.Equal(2, (await context.Customers.ToListAsync()).Count);
-
- // The id being a special character should not make the difference whether this fits in 1 batch, as id is duplicated in the payload on create.
+ // The create doesn't duplicate the id in the payload, so it should fit in one batch even with special chars
Assert.Equal(1, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
- }
-
- [ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_transaction_behavior_always_update_entities_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over(bool oneByteOver)
- {
- using var context = Fixture.CreateContext();
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
-
- var customer1 = new Customer { Id = new string('x', 1_023), PartitionKey = new string('x', 1_023) };
- var customer2 = new Customer { Id = new string('y', 1_023), PartitionKey = new string('x', 1_023) };
- context.Customers.Add(customer1);
- context.Customers.Add(customer2);
-
- await context.SaveChangesAsync();
-
- customer1.Name = new string('x', 1097582);
- customer2.Name = new string('x', 1097583);
-
- if (oneByteOver)
- {
- customer1.Name += 'x';
- customer2.Name += 'x';
- await Assert.ThrowsAsync(() => context.SaveChangesAsync());
- }
- else
- {
- await context.SaveChangesAsync();
- }
- }
-
- [ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_update(bool oneByteOver)
- {
- using var context = Fixture.CreateContext();
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
-
- var customer1 = new Customer { Id = new string('x', 1), PartitionKey = new string('x', 1_023) };
- var customer2 = new Customer { Id = new string('y', 1_023), PartitionKey = new string('x', 1_023) };
+ Fixture.ListLoggerFactory.Clear();
- context.Customers.Add(customer1);
- context.Customers.Add(customer2);
+ context.Update(customer1);
+ context.Update(customer2);
await context.SaveChangesAsync();
+ using var assertContext = Fixture.CreateContext();
+ Assert.Equal(2, (await context.Customers.ToListAsync()).Count);
- customer1.Name = new string('x', 1097581 + (1_024 - customer1.Id.Length) * 2);
- customer2.Name = new string('x', 1097581 + (1_024 - customer2.Id.Length) * 2);
-
- if (oneByteOver)
- {
- customer1.Name += 'x';
- customer2.Name += 'x';
- await Assert.ThrowsAsync(() => context.SaveChangesAsync());
- }
- else
- {
- await context.SaveChangesAsync();
- }
- }
-
- [ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_transaction_behavior_always_create_entities_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over(bool oneByteOver)
- {
- using var context = Fixture.CreateContext();
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
-
- var customer1 = new Customer { Id = new string('x', 1_023), Name = new string('x', 1098841), PartitionKey = new string('x', 1_023) };
- var customer2 = new Customer { Id = new string('y', 1_023), Name = new string('x', 1098841), PartitionKey = new string('x', 1_023) };
- if (oneByteOver)
- {
- customer1.Name += 'x';
- customer2.Name += 'x';
- }
-
- context.Customers.Add(customer1);
- context.Customers.Add(customer2);
- if (oneByteOver)
- {
- await Assert.ThrowsAsync(() => context.SaveChangesAsync());
- }
- else
- {
- await context.SaveChangesAsync();
- }
- }
-
- [ConditionalTheory, InlineData(true), InlineData(false)]
- public virtual async Task SaveChanges_id_does_not_count_double_toward_request_size_on_create(bool oneByteOver)
- {
- using var context = Fixture.CreateContext();
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
-
- var customer1 = new Customer { Id = new string('x', 1), Name = new string('x', 1098841 + 1_022), PartitionKey = new string('x', 1_023) };
- var customer2 = new Customer { Id = new string('y', 1_023), Name = new string('x', 1098841), PartitionKey = new string('x', 1_023) };
- if (oneByteOver)
- {
- customer1.Name += 'x';
- customer2.Name += 'x';
- }
-
- context.Customers.Add(customer1);
- context.Customers.Add(customer2);
- if (oneByteOver)
+ // The id being a special character should make the difference whether this fits in 1 batch.
+ if (isIdSpecialChar)
{
- await Assert.ThrowsAsync(() => context.SaveChangesAsync());
+ Assert.Equal(2, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}
else
{
- await context.SaveChangesAsync();
+ Assert.Equal(1, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
index eabe49207ae..a20d4a3a28c 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
@@ -164,7 +164,6 @@ await context.AddAsync(
new Person { Id = 3, Addresses = new List { existingAddress1Person3, existingAddress2Person3 } });
await context.SaveChangesAsync();
-
var people = await context.Set().ToListAsync();
Assert.Empty(people[0].Addresses);
@@ -240,12 +239,7 @@ await context.AddAsync(
var existingFirstAddressEntry = context.Entry(people[2].Addresses.First());
- var addressJson = existingFirstAddressEntry.Property("__jObject").CurrentValue;
-
- Assert.Equal("First", addressJson[nameof(Address.Street)]);
- addressJson["unmappedId"] = 2;
-
- existingFirstAddressEntry.Property("__jObject").IsModified = true;
+ Assert.Equal("First", people[2].Addresses.First().Street);
existingAddress1Person3 = people[2].Addresses.First();
existingAddress2Person3 = people[2].Addresses.Last();
@@ -270,7 +264,6 @@ await context.AddAsync(
{
existingAddress2Person3.Notes.Add(new Note { Content = "City note" });
}
-
await context.SaveChangesAsync();
await AssertState(context, useIds);
@@ -349,11 +342,7 @@ async Task AssertState(EmbeddedTransportationContext context, bool useIds)
var existingAddressEntry = context.Entry(addresses[0]);
- var addressJson = existingAddressEntry.Property("__jObject").CurrentValue;
-
- Assert.Equal("First", addressJson[nameof(Address.Street)]);
- Assert.Equal(6, addressJson.Count);
- Assert.Equal(2, addressJson["unmappedId"]);
+ Assert.Equal("First", addresses[0].Street);
Assert.Equal("Another", addresses[1].Street);
Assert.Equal("City", addresses[1].City);
diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
index 1b35b3c9e4e..4be18117251 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
@@ -178,92 +178,6 @@ public async Task Can_add_update_delete_detached_entity_end_to_end(bool transact
}
}
- [ConditionalTheory, InlineData(false), InlineData(true)]
- public async Task Can_add_update_untracked_properties(bool transactionalBatch)
- {
- var contextFactory = await InitializeNonSharedTest(
- b => b.Entity(),
- shouldLogCategory: _ => true,
- onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.NoPartitionKeyDefined)));
-
- var customer = new Customer { Id = 42, Name = "Theon" };
-
- using (var context = CreateContext(contextFactory, transactionalBatch))
- {
- var entry = await context.AddAsync(customer);
-
- await context.SaveChangesAsync();
-
- var document = entry.Property("__jObject").CurrentValue;
- Assert.NotNull(document);
- Assert.Equal("Theon", document["Name"]);
-
- context.Remove(customer);
-
- await context.SaveChangesAsync();
-
- }
-
- using (var context = CreateContext(contextFactory, transactionalBatch))
- {
- Assert.Empty(await context.Set().ToListAsync());
-
- var entry = await context.AddAsync(customer);
-
- entry.Property("__jObject").CurrentValue = new JObject { ["key1"] = "value1" };
-
- await context.SaveChangesAsync();
-
- var document = entry.Property("__jObject").CurrentValue;
- Assert.NotNull(document);
- Assert.Equal("Theon", document["Name"]);
- Assert.Equal("value1", document["key1"]);
-
- document["key2"] = "value2";
- entry.State = EntityState.Modified;
- await context.SaveChangesAsync();
- }
-
- using (var context = CreateContext(contextFactory, transactionalBatch))
- {
- var customerFromStore = await context.Set().SingleAsync();
-
- Assert.Equal(42, customerFromStore.Id);
- Assert.Equal("Theon", customerFromStore.Name);
-
- var entry = context.Entry(customerFromStore);
- var document = entry.Property("__jObject").CurrentValue;
- Assert.Equal("value1", document["key1"]);
- Assert.Equal("value2", document["key2"]);
-
- document["key1"] = "value1.1";
- customerFromStore.Name = "Theon Greyjoy";
-
- await context.SaveChangesAsync();
- }
-
- using (var context = CreateContext(contextFactory, transactionalBatch))
- {
- var customerFromStore = await context.Set().SingleAsync();
-
- Assert.Equal("Theon Greyjoy", customerFromStore.Name);
-
- var entry = context.Entry(customerFromStore);
- var document = entry.Property("__jObject").CurrentValue;
- Assert.Equal("value1.1", document["key1"]);
- Assert.Equal("value2", document["key2"]);
-
- context.Remove(customerFromStore);
-
- await context.SaveChangesAsync();
- }
-
- using (var context = CreateContext(contextFactory, transactionalBatch))
- {
- Assert.Empty(await context.Set().ToListAsync());
- }
- }
-
[ConditionalTheory, InlineData(false), InlineData(true)]
public async Task Can_add_update_delete_end_to_end_with_Guid(bool transactionalBatch)
{
@@ -761,6 +675,25 @@ await Can_add_update_delete_with_collection(
{
{ "1", new Dictionary { { "value", 1 } } }, { "2", null }
});
+
+ await Can_add_update_delete_with_collection>>>>(
+ transactionalBatch,
+ new() {
+ { "2", new() { { "value", new() { { "1", ["1", "2"] } } } } },
+ { "1", new() { { "value", new() { { "2", ["3", "4"] } } } } }
+ },
+ c =>
+ {
+ c.Collection.Add("3", new() { { "value", new() { { "3", ["5", "6"] } } } });
+ c.Collection.Remove("1");
+ c.Collection["2"].Remove("value");
+ c.Collection["2"].Add("value2", new() { { "4", ["7", "8"] } });
+ },
+ new()
+ {
+ { "2", new() { { "value2", new() { { "4", ["7", "8"] } } } } },
+ { "3", new() { { "value", new() { { "3", ["5", "6"] } } } } }
+ });
}
private async Task Can_add_update_delete_with_collection(
@@ -1612,10 +1545,6 @@ public async Task Can_have_non_string_property_named_Discriminator(bool useDiscr
var entry = await context.AddAsync(new NonStringDiscriminator { Id = 1 });
await context.SaveChangesAsync();
- var document = entry.Property("__jObject").CurrentValue;
- Assert.NotNull(document);
- Assert.Equal("0", document["Discriminator"]);
-
var baseEntity = await context.Set().OrderBy(e => e.Id).FirstOrDefaultAsync();
Assert.NotNull(baseEntity);
diff --git a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
index 2b287e85924..bac2f8c1056 100644
--- a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
@@ -5,6 +5,54 @@ namespace Microsoft.EntityFrameworkCore;
public class JsonTypesCosmosTest(NonSharedFixture fixture) : JsonTypesTestBase(fixture)
{
+ public override Task Can_read_write_TimeSpan_JSON_values(string value, string json)
+ => base.Can_read_write_TimeSpan_JSON_values(
+ value, value switch
+ {
+ "-10675199.02:48:05.4775808" => """{"Prop":"-10675199.02:48:05.4775808"}""",
+ "10675199.02:48:05.4775807" => """{"Prop":"10675199.02:48:05.4775807"}""",
+ "00:00:00" => """{"Prop":"00:00:00"}""",
+ _ => json,
+ });
+
+ public override Task Can_read_write_TimeOnly_JSON_values(string value, string json)
+ => base.Can_read_write_TimeOnly_JSON_values(
+ value, value switch
+ {
+ "00:00:00.0000000" => """{"Prop":"00:00:00"}""",
+ _ => json,
+ });
+
+ public override Task Can_read_write_nullable_TimeOnly_JSON_values(string? value, string json)
+ => base.Can_read_write_nullable_TimeOnly_JSON_values(
+ value, value switch
+ {
+ "00:00:00.0000000" => """{"Prop":"00:00:00"}""",
+ _ => json,
+ });
+
+ public override Task Can_read_write_nullable_TimeSpan_JSON_values(string? value, string json)
+ => base.Can_read_write_nullable_TimeSpan_JSON_values(
+ value, value switch
+ {
+ "-10675199.02:48:05.4775808" => """{"Prop":"-10675199.02:48:05.4775808"}""",
+ "10675199.02:48:05.4775807" => """{"Prop":"10675199.02:48:05.4775807"}""",
+ "00:00:00" => """{"Prop":"00:00:00"}""",
+ _ => json,
+ });
+
+ public override Task Can_read_write_collection_of_TimeOnly_JSON_values(string _)
+ => base.Can_read_write_collection_of_TimeOnly_JSON_values("""{"Prop":["00:00:00","11:05:02.003004","23:59:59.9999999"]}""");
+
+ public override Task Can_read_write_collection_of_TimeSpan_JSON_values(string _)
+ => base.Can_read_write_collection_of_TimeSpan_JSON_values("""{"Prop":["-10675199.02:48:05.4775808","1.02:03:04.0050000","10675199.02:48:05.4775807"]}""");
+
+ public override Task Can_read_write_collection_of_nullable_TimeOnly_JSON_values(string _)
+ => base.Can_read_write_collection_of_nullable_TimeOnly_JSON_values("""{"Prop":[null,"00:00:00","11:05:02.003004","23:59:59.9999999"]}""");
+
+ public override Task Can_read_write_collection_of_nullable_TimeSpan_JSON_values(string _)
+ => base.Can_read_write_collection_of_nullable_TimeSpan_JSON_values("""{"Prop":["-10675199.02:48:05.4775808","1.02:03:04.0050000","10675199.02:48:05.4775807",null]}""");
+
public override Task Can_read_write_collection_of_Guid_converted_to_bytes_JSON_values(string expected)
// Cosmos provider cannot map collections of elements with converters. See Issue #34026.
=> Assert.ThrowsAsync(() => base.Can_read_write_collection_of_Guid_converted_to_bytes_JSON_values(
diff --git a/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs b/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs
index b54ff771e0d..2913707dfff 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ReloadTest.cs
@@ -31,9 +31,6 @@ public async Task Entity_reference_can_be_reloaded()
var entry = await context.AddAsync(new Item { Id = 1337, PartitionKey = "Foo" });
await context.SaveChangesAsync();
- var itemJson = entry.Property("__jObject").CurrentValue;
- itemJson["unmapped"] = 2;
-
await entry.ReloadAsync();
AssertSql(
@@ -52,9 +49,6 @@ FROM root c
WHERE (c["Id"] = @p)
OFFSET 0 LIMIT 1
""");
-
- itemJson = entry.Property("__jObject").CurrentValue;
- Assert.Null(itemJson["unmapped"]);
}
protected ReloadTestContext CreateContext()
diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
index d6f21078377..a5437a357ab 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/Basic_cosmos_model/DataEntityType.cs
@@ -193,7 +193,13 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
byte[] (byte[] source) => source.ToArray()),
converter: new ValueConverter, byte[]>(
byte[] (ReadOnlyMemory v) => ReadOnlyMemoryConverter.ToArray(v),
- ReadOnlyMemory (byte[] v) => ReadOnlyMemoryConverter.ToMemory(v)));
+ ReadOnlyMemory (byte[] v) => ReadOnlyMemoryConverter.ToMemory(v)),
+ jsonValueReaderWriter: new JsonConvertedValueReaderWriter, IEnumerable>(
+ new JsonCollectionOfStructsReaderWriter(
+ JsonByteReaderWriter.Instance),
+ new ValueConverter, byte[]>(
+ byte[] (ReadOnlyMemory v) => ReadOnlyMemoryConverter.ToArray(v),
+ ReadOnlyMemory (byte[] v) => ReadOnlyMemoryConverter.ToMemory(v))));
bytes.SetSentinelFromProviderValue(new byte[0]);
var list = runtimeEntityType.AddProperty(
@@ -226,7 +232,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
List> (List> v) => v),
clrType: typeof(List>),
jsonValueReaderWriter: new JsonCollectionOfReferencesReaderWriter>, Dictionary>(
- new CosmosTypeMappingSource.PlaceholderJsonStringKeyedDictionaryReaderWriter(
+ new CosmosTypeMappingSource.CosmosJsonStringKeyedDictionaryReaderWriter(
JsonInt32ReaderWriter.Instance)),
elementMapping: CosmosTypeMapping.Default.Clone(
comparer: new StringDictionaryComparer, int>(new ValueComparer(
@@ -242,7 +248,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (Dictionary v) => ((object)v).GetHashCode(),
Dictionary (Dictionary v) => v),
clrType: typeof(Dictionary),
- jsonValueReaderWriter: new CosmosTypeMappingSource.PlaceholderJsonStringKeyedDictionaryReaderWriter(
+ jsonValueReaderWriter: new CosmosTypeMappingSource.CosmosJsonStringKeyedDictionaryReaderWriter(
JsonInt32ReaderWriter.Instance)));
var listElementType = list.SetElementType(typeof(Dictionary),
nullable: true);
@@ -277,7 +283,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
int (Dictionary v) => ((object)v).GetHashCode(),
Dictionary (Dictionary v) => v),
clrType: typeof(Dictionary),
- jsonValueReaderWriter: new CosmosTypeMappingSource.PlaceholderJsonStringKeyedDictionaryReaderWriter(
+ jsonValueReaderWriter: new CosmosTypeMappingSource.CosmosJsonStringKeyedDictionaryReferenceCollectionValueReaderWriter(
new JsonCollectionOfReferencesReaderWriter(
JsonStringReaderWriter.Instance)));
diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/ManyTypesEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/ManyTypesEntityType.cs
index 6f3597f77de..d3f87c5bb7a 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/ManyTypesEntityType.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/ManyTypesEntityType.cs
@@ -6949,21 +6949,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
shadowIndex: -1,
relationshipIndex: -1,
storeGenerationIndex: -1);
- nullableTimeOnly.TypeMapping = CosmosTypeMapping.Default.Clone(
- comparer: new ValueComparer(
- bool (TimeOnly v1, TimeOnly v2) => v1.Equals(v2),
- int (TimeOnly v) => ((object)v).GetHashCode(),
- TimeOnly (TimeOnly v) => v),
- keyComparer: new ValueComparer(
- bool (TimeOnly v1, TimeOnly v2) => v1.Equals(v2),
- int (TimeOnly v) => ((object)v).GetHashCode(),
- TimeOnly (TimeOnly v) => v),
- providerValueComparer: new ValueComparer(
- bool (TimeOnly v1, TimeOnly v2) => v1.Equals(v2),
- int (TimeOnly v) => ((object)v).GetHashCode(),
- TimeOnly (TimeOnly v) => v),
- clrType: typeof(TimeOnly),
- jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance);
+ nullableTimeOnly.TypeMapping = CosmosTimeOnlyTypeMapping.Default;
nullableTimeOnly.SetComparer(new NullableValueComparer(nullableTimeOnly.TypeMapping.Comparer));
nullableTimeOnly.SetKeyComparer(new NullableValueComparer(nullableTimeOnly.TypeMapping.KeyComparer));
@@ -6999,21 +6985,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
shadowIndex: -1,
relationshipIndex: -1,
storeGenerationIndex: -1);
- nullableTimeSpan.TypeMapping = CosmosTypeMapping.Default.Clone(
- comparer: new ValueComparer(
- bool (TimeSpan v1, TimeSpan v2) => v1.Equals(v2),
- int (TimeSpan v) => ((object)v).GetHashCode(),
- TimeSpan (TimeSpan v) => v),
- keyComparer: new ValueComparer(
- bool (TimeSpan v1, TimeSpan v2) => v1.Equals(v2),
- int (TimeSpan v) => ((object)v).GetHashCode(),
- TimeSpan (TimeSpan v) => v),
- providerValueComparer: new ValueComparer(
- bool (TimeSpan v1, TimeSpan v2) => v1.Equals(v2),
- int (TimeSpan v) => ((object)v).GetHashCode(),
- TimeSpan (TimeSpan v) => v),
- clrType: typeof(TimeSpan),
- jsonValueReaderWriter: JsonTimeSpanReaderWriter.Instance);
+ nullableTimeSpan.TypeMapping = CosmosTimeSpanTypeMapping.Default;
nullableTimeSpan.SetComparer(new NullableValueComparer(nullableTimeSpan.TypeMapping.Comparer));
nullableTimeSpan.SetKeyComparer(new NullableValueComparer(nullableTimeSpan.TypeMapping.KeyComparer));
@@ -8595,7 +8567,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)v)) : v.ToString("o"))),
jsonValueReaderWriter: new JsonConvertedValueReaderWriter(
- JsonTimeOnlyReaderWriter.Instance,
+ CosmosJsonTimeOnlyReaderWriter.Instance,
new ValueConverter(
TimeOnly (string v) => TimeOnly.Parse(v, CultureInfo.InvariantCulture, DateTimeStyles.None),
string (TimeOnly v) => (v.Ticks % 10000000L == 0L ? string.Format(CultureInfo.InvariantCulture, "{0:HH\\:mm\\:ss}", ((object)v)) : v.ToString("o")))));
@@ -8649,7 +8621,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
TimeSpan (string v) => TimeSpan.Parse(v, CultureInfo.InvariantCulture),
string (TimeSpan v) => v.ToString("c")),
jsonValueReaderWriter: new JsonConvertedValueReaderWriter(
- JsonTimeSpanReaderWriter.Instance,
+ CosmosJsonTimeSpanReaderWriter.Instance,
new ValueConverter(
TimeSpan (string v) => TimeSpan.Parse(v, CultureInfo.InvariantCulture),
string (TimeSpan v) => v.ToString("c"))));
@@ -8740,21 +8712,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
shadowIndex: -1,
relationshipIndex: -1,
storeGenerationIndex: -1);
- timeOnly.TypeMapping = CosmosTypeMapping.Default.Clone(
- comparer: new ValueComparer(
- bool (TimeOnly v1, TimeOnly v2) => v1.Equals(v2),
- int (TimeOnly v) => ((object)v).GetHashCode(),
- TimeOnly (TimeOnly v) => v),
- keyComparer: new ValueComparer(
- bool (TimeOnly v1, TimeOnly v2) => v1.Equals(v2),
- int (TimeOnly v) => ((object)v).GetHashCode(),
- TimeOnly (TimeOnly v) => v),
- providerValueComparer: new ValueComparer(
- bool (TimeOnly v1, TimeOnly v2) => v1.Equals(v2),
- int (TimeOnly v) => ((object)v).GetHashCode(),
- TimeOnly (TimeOnly v) => v),
- clrType: typeof(TimeOnly),
- jsonValueReaderWriter: JsonTimeOnlyReaderWriter.Instance);
+ timeOnly.TypeMapping = CosmosTimeOnlyTypeMapping.Default;
var timeOnlyToStringConverterProperty = runtimeEntityType.AddProperty(
"TimeOnlyToStringConverterProperty",
@@ -8898,21 +8856,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas
shadowIndex: -1,
relationshipIndex: -1,
storeGenerationIndex: -1);
- timeSpan.TypeMapping = CosmosTypeMapping.Default.Clone(
- comparer: new ValueComparer(
- bool (TimeSpan v1, TimeSpan v2) => v1.Equals(v2),
- int (TimeSpan v) => ((object)v).GetHashCode(),
- TimeSpan (TimeSpan v) => v),
- keyComparer: new ValueComparer(
- bool (TimeSpan v1, TimeSpan v2) => v1.Equals(v2),
- int (TimeSpan v) => ((object)v).GetHashCode(),
- TimeSpan (TimeSpan v) => v),
- providerValueComparer: new ValueComparer(
- bool (TimeSpan v1, TimeSpan v2) => v1.Equals(v2),
- int (TimeSpan v) => ((object)v).GetHashCode(),
- TimeSpan (TimeSpan v) => v),
- clrType: typeof(TimeSpan),
- jsonValueReaderWriter: JsonTimeSpanReaderWriter.Instance);
+ timeSpan.TypeMapping = CosmosTimeSpanTypeMapping.Default;
var timeSpanToStringConverterProperty = runtimeEntityType.AddProperty(
"TimeSpanToStringConverterProperty",
diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
index b7c7b66580a..79e7571f4e1 100644
--- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
+++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
@@ -64,7 +64,9 @@ public void Can_create_options_with_valid_values()
Test(o => o.MaxRequestsPerTcpConnection(3), o => Assert.Equal(3, o.MaxRequestsPerTcpConnection));
Test(o => o.MaxTcpConnectionsPerEndpoint(3), o => Assert.Equal(3, o.MaxTcpConnectionsPerEndpoint));
Test(o => o.LimitToEndpoint(), o => Assert.True(o.LimitToEndpoint));
+#pragma warning disable CS0618 // Type or member is obsolete
Test(o => o.ContentResponseOnWriteEnabled(), o => Assert.True(o.EnableContentResponseOnWrite));
+#pragma warning restore CS0618 // Type or member is obsolete
Test(o => o.SessionTokenManagementMode(Cosmos.Infrastructure.SessionTokenManagementMode.EnforcedManual), o => Assert.Equal(Cosmos.Infrastructure.SessionTokenManagementMode.EnforcedManual, o.SessionTokenManagementMode));
Test(o => o.BulkExecutionEnabled(), o => Assert.True(o.EnableBulkExecution));
diff --git a/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs b/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs
index d879f8208ff..f695c6c3339 100644
--- a/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs
+++ b/test/EFCore.Cosmos.Tests/Storage/Internal/CosmosTypeMappingSourceTest.cs
@@ -68,7 +68,7 @@ public void Can_map_DateOnly()
[ConditionalFact]
public void Can_map_TimeOnly()
- => Can_map_scalar_by_clr_type(
+ => Can_map_scalar_by_clr_type(
new TimeOnly(20, 19, 12, 254), JTokenType.String, "\"20:19:12.254\"");
[ConditionalFact]
@@ -84,7 +84,7 @@ public void Can_map_DateTimeOffset()
[ConditionalFact]
public void Can_map_TimeSpan()
- => Can_map_scalar_by_clr_type(new TimeSpan(2, 3, 4, 5), JTokenType.TimeSpan, "\"2.03:04:05\"");
+ => Can_map_scalar_by_clr_type(new TimeSpan(2, 3, 4, 5), JTokenType.TimeSpan, "\"2.03:04:05\"");
[ConditionalFact]
public void Can_map_string()
@@ -148,7 +148,7 @@ public void Can_map_nullable_DateOnly()
[ConditionalFact]
public void Can_map_nullable_TimeOnly()
- => Can_map_scalar_by_clr_type(
+ => Can_map_scalar_by_clr_type(
new TimeOnly(20, 19, 12, 254), JTokenType.String, "\"20:19:12.254\"");
[ConditionalFact]
@@ -164,7 +164,7 @@ public void Can_map_nullable_DateTimeOffset()
[ConditionalFact]
public void Can_map_nullable_TimeSpan()
- => Can_map_scalar_by_clr_type(new TimeSpan(2, 3, 4, 5), JTokenType.TimeSpan, "\"2.03:04:05\"");
+ => Can_map_scalar_by_clr_type(new TimeSpan(2, 3, 4, 5), JTokenType.TimeSpan, "\"2.03:04:05\"");
[ConditionalFact]
public void Can_map_nullable_string()
diff --git a/test/EFCore.Specification.Tests/JsonTypesTestBase.cs b/test/EFCore.Specification.Tests/JsonTypesTestBase.cs
index f79a154024e..e93ea1ee233 100644
--- a/test/EFCore.Specification.Tests/JsonTypesTestBase.cs
+++ b/test/EFCore.Specification.Tests/JsonTypesTestBase.cs
@@ -1657,8 +1657,8 @@ protected class DateOnlyCollectionType
public IList DateOnly { get; set; } = null!;
}
- [ConditionalFact]
- public virtual Task Can_read_write_collection_of_TimeOnly_JSON_values()
+ [ConditionalTheory, InlineData("""{"Prop":["00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}""")]
+ public virtual Task Can_read_write_collection_of_TimeOnly_JSON_values(string expected)
=> Can_read_and_write_JSON_value>(
nameof(TimeOnlyCollectionType.TimeOnly),
[
@@ -1666,7 +1666,7 @@ public virtual Task Can_read_write_collection_of_TimeOnly_JSON_values()
new TimeOnly(11, 5, 2, 3, 4),
TimeOnly.MaxValue
],
- """{"Prop":["00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}""",
+ expected,
mappedCollection: true,
new List());
@@ -1712,8 +1712,8 @@ protected class DateTimeOffsetCollectionType
public IList DateTimeOffset { get; set; } = null!;
}
- [ConditionalFact]
- public virtual Task Can_read_write_collection_of_TimeSpan_JSON_values()
+ [ConditionalTheory, InlineData("""{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807"]}""")]
+ public virtual Task Can_read_write_collection_of_TimeSpan_JSON_values(string expected)
=> Can_read_and_write_JSON_value>(
nameof(TimeSpanCollectionType.TimeSpan),
[
@@ -1721,7 +1721,7 @@ public virtual Task Can_read_write_collection_of_TimeSpan_JSON_values()
new TimeSpan(1, 2, 3, 4, 5),
TimeSpan.MaxValue
],
- """{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807"]}""",
+ expected,
mappedCollection: true);
protected class TimeSpanCollectionType
@@ -2238,8 +2238,8 @@ protected class NullableDateOnlyCollectionType
public IList DateOnly { get; set; } = null!;
}
- [ConditionalFact]
- public virtual Task Can_read_write_collection_of_nullable_TimeOnly_JSON_values()
+ [ConditionalTheory, InlineData("""{"Prop":[null,"00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}""")]
+ public virtual Task Can_read_write_collection_of_nullable_TimeOnly_JSON_values(string expected)
=> Can_read_and_write_JSON_value>(
nameof(NullableTimeOnlyCollectionType.TimeOnly),
[
@@ -2248,7 +2248,7 @@ public virtual Task Can_read_write_collection_of_nullable_TimeOnly_JSON_values()
new TimeOnly(11, 5, 2, 3, 4),
TimeOnly.MaxValue
],
- """{"Prop":[null,"00:00:00.0000000","11:05:02.0030040","23:59:59.9999999"]}""",
+ expected,
mappedCollection: true);
protected class NullableTimeOnlyCollectionType
@@ -2295,8 +2295,8 @@ protected class NullableDateTimeOffsetCollectionType
public IReadOnlyList DateTimeOffset { get; set; } = null!;
}
- [ConditionalFact]
- public virtual Task Can_read_write_collection_of_nullable_TimeSpan_JSON_values()
+ [ConditionalTheory, InlineData("""{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807",null]}""")]
+ public virtual Task Can_read_write_collection_of_nullable_TimeSpan_JSON_values(string expected)
=> Can_read_and_write_JSON_value>(
nameof(NullableTimeSpanCollectionType.TimeSpan),
[
@@ -2305,7 +2305,7 @@ public virtual Task Can_read_write_collection_of_nullable_TimeSpan_JSON_values()
TimeSpan.MaxValue,
null
],
- """{"Prop":["-10675199:2:48:05.4775808","1:2:03:04.005","10675199:2:48:05.4775807",null]}""",
+ expected,
mappedCollection: true);
protected class NullableTimeSpanCollectionType