From 289db0265ef13d7b1be73a897741a95186a42ed6 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Fri, 27 Mar 2026 12:26:40 +0100
Subject: [PATCH 01/29] Cosmos: Modernize JSON serialization - Update pipeline
---
.../Internal/CosmosSerializationUtilities.cs | 2 +-
.../Storage/Internal/CosmosClientWrapper.cs | 105 ++---
.../Storage/Internal/CosmosDatabaseWrapper.cs | 174 ++++----
.../CosmosJsonTimeOnlyReaderWriter.cs | 43 ++
.../CosmosJsonTimeSpanReaderWriter.cs | 43 ++
.../Internal/CosmosJsonVectorReaderWriter.cs | 122 ++++++
.../Internal/CosmosTimeOnlyTypeMapping.cs | 23 ++
.../Internal/CosmosTimeSpanTypeMapping.cs | 23 ++
.../Internal/CosmosTypeMappingSource.cs | 270 +++++++++++-
.../Internal/CosmosVectorTypeMapping.cs | 7 +-
.../Storage/Internal/ICosmosClientWrapper.cs | 5 +-
.../Update/Internal/DocumentSource.cs | 391 +++++-------------
.../Storage/Json/JsonValueReaderWriter.cs | 8 +-
.../CosmosComplexTypesTrackingTest.cs | 20 +-
.../CosmosTransactionalBatchTest.cs | 49 ++-
.../EmbeddedDocumentsTest.cs | 18 +-
.../EndToEndCosmosTest.cs | 109 +----
.../ReloadTest.cs | 6 -
.../Basic_cosmos_model/DataEntityType.cs | 14 +-
.../TestUtilities/CosmosTestStore.cs | 10 +-
20 files changed, 856 insertions(+), 586 deletions(-)
create mode 100644 src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeOnlyReaderWriter.cs
create mode 100644 src/EFCore.Cosmos/Storage/Internal/CosmosJsonTimeSpanReaderWriter.cs
create mode 100644 src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs
create mode 100644 src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
create mode 100644 src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
index 82841f88162..98a7b309083 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?
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index 6b195ab5a14..d9c1f582cf8 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -5,7 +5,6 @@
using System.Collections.ObjectModel;
using System.Net;
using System.Runtime.CompilerServices;
-using System.Text;
using Microsoft.Azure.Cosmos.Scripts;
using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
@@ -45,7 +44,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()
@@ -66,8 +64,7 @@ public CosmosClientWrapper(
ISingletonCosmosClientWrapper singletonWrapper,
IDbContextOptions dbContextOptions,
IExecutionStrategy executionStrategy,
- IDiagnosticsLogger commandLogger,
- IDiagnosticsLogger databaseLogger)
+ IDiagnosticsLogger commandLogger)
{
var options = dbContextOptions.FindExtension();
@@ -75,27 +72,9 @@ 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;
- }
-
///
/// 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
@@ -333,20 +312,20 @@ private static string GetPathFromRoot(IReadOnlyEntityType entityType)
///
public virtual Task CreateItemAsync(
string containerId,
- JToken document,
+ string documentId,
+ Stream 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, Stream 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;
@@ -369,7 +348,7 @@ private static async Task CreateItemOnceAsync(
}
using var response = await container.CreateItemStreamAsync(
- stream,
+ parameters.Document,
partitionKeyValue,
itemRequestOptions,
cancellationToken)
@@ -379,7 +358,7 @@ private static async Task CreateItemOnceAsync(
response.Diagnostics.GetClientElapsedTime(),
response.Headers.RequestCharge,
response.Headers.ActivityId,
- parameters.Document["id"]!.ToString(),
+ documentId,
containerId,
partitionKeyValue);
@@ -397,7 +376,7 @@ private static async Task CreateItemOnceAsync(
public virtual Task ReplaceItemAsync(
string collectionId,
string documentId,
- JObject document,
+ Stream document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default)
@@ -406,11 +385,9 @@ 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, Stream 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;
@@ -434,7 +411,7 @@ private static async Task ReplaceItemOnceAsync(
}
using var response = await container.ReplaceItemStreamAsync(
- stream,
+ parameters.Document,
parameters.ResourceId,
partitionKeyValue,
itemRequestOptions,
@@ -666,29 +643,65 @@ private static void ProcessResponse(string containerId, TransactionalBatchRespon
var entry = entries[i];
var response = batchResponse[i];
- ProcessResponse(entry.Entry, response.ETag, response.ResourceStream);
+ ProcessResponse(entry.Entry, response.ETag, response.ResourceStream); // Batches can not run pre or post triggers.
}
}
private static void ProcessResponse(IUpdateEntry entry, string eTag, Stream? content)
{
+ if (entry.EntityState == EntityState.Deleted)
+ {
+ return;
+ }
+
var etagProperty = entry.EntityType.GetETagProperty();
- if (etagProperty != null && entry.EntityState != EntityState.Deleted)
+ if (etagProperty != null)
{
entry.SetStoreGeneratedValue(etagProperty, eTag);
}
- var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
- if (jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate }
- && content != null)
+ // @TODO: If the entry is loaded from the database, has an etag and has no triggers, we know that nothing has changed in the meantime right?
+ // Could we optimize?
+ if (content != null && content.Length > 0)
{
- 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);
+ // @TODO: Entries without etag or with a pre- or post- trigger could have an updated document returned.
+ // We should consider processing the returned document in that case as well
+ // Invoke tracking shaper labmda?
+
+ // How did that work before? is appears to be only updaing the underlying jobject (removed now), but not the entry? Would setting the _jObject update the entry no right?
+ // But updating the underlying jobject would make it so that subsequent updates would not use old data, unless the user updated the property on the entity...
+ // Now that behaviour would be gone.
+
+ // @TODO: Ask what to do here?
+
+ // used to be this:
+ //var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
+ //if (jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate }
+ // && content != 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);
+ //}
+
+ // jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate } is always false by default..
+ // Can the user set this to true? Probably right.
+ // Interestingly, default for enableContentResponseOnRewrite is true, but jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate } is always false.
+ // So enableContentResponseOnRewrite default or true does nothing without modifying the jObjectProperty's value generated.
+ // Does this mean anyone was really using this?
+ // Implementing this to work without setting jobject property value generated (because it will be removed) would be a theoratical breaking change?
+ // Or a bug fix?
+
+ // No tests that don't use __jObject fail without this code
+ // @TODO: Add tests for this on main.
+
+ // Suggestion: Make the new default enableContentResponseOnRewrite false?
+ // People who have set jObjectProperty ValueGenerated can migrate by manually setting EnableContentResponseOnRewrite to true.
+ // Others will notice no changes this way.
}
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
index 8a6ae3fee28..3aeeb145872 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
@@ -459,7 +426,7 @@ 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 stream = updateEntry.Operation != CosmosCudOperation.Delete ? updateEntry.DocumentSource.Serialize(updateEntry.Entry) : null;
// With AutoTransactionBehavior.Always, AddToTransaction will always return true.
if (!AddToTransaction(transaction, updateEntry, stream))
@@ -499,24 +466,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 +520,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?
}
return documentSource;
@@ -625,7 +594,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/CosmosJsonVectorReaderWriter.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs
new file mode 100644
index 00000000000..60a55d75818
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore.Storage.Json;
+
+namespace Microsoft.EntityFrameworkCore.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 CosmosJsonVectorReaderWriter : JsonValueReaderWriter
+{
+ private static readonly PropertyInfo InstanceProperty = typeof(CosmosJsonVectorReaderWriter).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 CosmosJsonVectorReaderWriter Instance { 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 override object FromJson(ref Utf8JsonReaderManager manager, object? existingObject = null)
+ {
+ var tokenType = manager.CurrentReader.TokenType;
+ if (tokenType != JsonTokenType.StartArray)
+ {
+ throw new InvalidOperationException(
+ CoreStrings.JsonReaderInvalidTokenType(tokenType.ToString()));
+ }
+
+ var result = new List();
+
+ while (tokenType != JsonTokenType.EndArray)
+ {
+ manager.MoveNext();
+ tokenType = manager.CurrentReader.TokenType;
+
+ if (tokenType != JsonTokenType.Number || !manager.CurrentReader.TryGetInt32(out var intValue))
+ {
+ throw new InvalidOperationException(
+ CoreStrings.JsonReaderInvalidTokenType(tokenType.ToString()));
+ }
+
+ result.Add((byte)intValue);
+ }
+
+ return result.ToArray();
+ }
+
+ ///
+ /// 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 ToJson(Utf8JsonWriter writer, object value)
+ {
+ writer.WriteStartArray();
+
+ switch (value)
+ {
+ case IEnumerable bytes:
+ foreach (var item in bytes)
+ {
+ writer.WriteNumberValue(item);
+ }
+ break;
+ case ReadOnlyMemory rom:
+ foreach (var item in rom.Span)
+ {
+ writer.WriteNumberValue(item);
+ }
+ break;
+ case IEnumerable bytes:
+ foreach (var item in bytes)
+ {
+ writer.WriteNumberValue(item);
+ }
+ break;
+ case ReadOnlyMemory rom:
+ foreach (var item in rom.Span)
+ {
+ writer.WriteNumberValue(item);
+ }
+ break;
+ case IEnumerable bytes:
+ foreach (var item in bytes)
+ {
+ writer.WriteNumberValue(item);
+ }
+ break;
+ case ReadOnlyMemory rom:
+ foreach (var item in rom.Span)
+ {
+ writer.WriteNumberValue(item);
+ }
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+
+ writer.WriteEndArray();
+ }
+
+ ///
+ public override Expression ConstructorExpression
+ => Expression.Property(null, InstanceProperty);
+
+ ///
+ public override Type ValueType { get; } = typeof(byte[]);
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
new file mode 100644
index 00000000000..670456536e3
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
@@ -0,0 +1,23 @@
+// 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 CosmosTimeOnlyTypeMapping() : base(typeof(TimeOnly), null, null, null, CosmosJsonTimeOnlyReaderWriter.Instance)
+ {
+ }
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
new file mode 100644
index 00000000000..effc8c3a5ba
--- /dev/null
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
@@ -0,0 +1,23 @@
+// 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 CosmosTimeSpanTypeMapping() : base(typeof(TimeSpan), null, null, null, CosmosJsonTimeSpanReaderWriter.Instance)
+ {
+ }
+}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index 4464928a038..3781aa31fc4 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), new CosmosTimeOnlyTypeMapping() },
+ { typeof(TimeSpan), new CosmosTimeSpanTypeMapping() },
{
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
@@ -82,7 +85,10 @@ 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;
+ TryFindJsonCollectionMapping(elementMappingInfo, memoryType.MakeArrayType(), null, ref typeMapping, out var _, out var readerWriter);
+ return new CosmosTypeMapping(clrType, jsonValueReaderWriter: readerWriter)
.WithComposedConverter(
(ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(memoryType))!,
(ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(memoryType))!);
@@ -168,10 +174,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 +233,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 +240,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
{
@@ -233,14 +264,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)
+ {
+ 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("JsonValueReaderWriter infrastructure is not supported on Cosmos.");
+ ///
+ /// 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("JsonValueReaderWriter infrastructure is not supported on Cosmos.");
+
+ ///
+ /// 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
Task CreateItemAsync(
string containerId,
- JToken document,
+ string documentId,
+ Stream document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default);
@@ -59,7 +60,7 @@ Task CreateItemAsync(
Task ReplaceItemAsync(
string collectionId,
string documentId,
- JObject document,
+ Stream document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default);
diff --git a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
index 2f5f1cfdd6d..0432123be04 100644
--- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
+++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
@@ -2,12 +2,13 @@
// 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.Storage.Internal;
using Microsoft.EntityFrameworkCore.Update.Internal;
-using Newtonsoft.Json.Linq;
namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal;
@@ -23,10 +24,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 +33,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 +66,166 @@ 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 Stream 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);
+ stream.Position = 0;
+ return stream;
+ }
- 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 is IJsonConvertedValueReaderWriter { Converter.ConvertsNulls: 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/src/EFCore/Storage/Json/JsonValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
index f61f541659d..6e280e77a3e 100644
--- a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
+++ b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
@@ -15,9 +15,13 @@ namespace Microsoft.EntityFrameworkCore.Storage.Json;
public abstract class JsonValueReaderWriter
{
///
- /// Ensures the external types extend from the generic
+ /// 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.
///
- internal JsonValueReaderWriter()
+ [EntityFrameworkInternal]
+ public JsonValueReaderWriter()
{
}
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/CosmosTransactionalBatchTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
index 1831aff89ef..6aa986be2a8 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
@@ -281,6 +281,9 @@ public virtual async Task SaveChanges_entity_too_large_throws()
[ConditionalTheory, InlineData(true), InlineData(false)]
public virtual async Task SaveChanges_exactly_2_mib_does_not_split_and_one_byte_over_splits(bool oneByteOver)
{
+ //for (var i = 350; i > 0; i--)
+ //{
+
using var context = Fixture.CreateContext();
var customer1 = new Customer { Id = new string('x', 1023), PartitionKey = new string('x', 1023) };
@@ -292,8 +295,8 @@ public virtual async Task SaveChanges_exactly_2_mib_does_not_split_and_one_byte_
await context.SaveChangesAsync();
Fixture.ListLoggerFactory.Clear();
- customer1.Name = new string('x', 1044994);
- customer2.Name = new string('x', 1044994);
+ customer1.Name = new string('x', 1045148);
+ customer2.Name = new string('x', 1045148);
if (oneByteOver)
{
@@ -302,7 +305,7 @@ public virtual async Task SaveChanges_exactly_2_mib_does_not_split_and_one_byte_
await context.SaveChangesAsync();
using var assertContext = Fixture.CreateContext();
- Assert.Equal(2, (await context.Customers.ToListAsync()).Count);
+ Assert.Equal(2, (await assertContext.Customers.ToListAsync()).Count);
if (oneByteOver)
{
@@ -310,8 +313,14 @@ public virtual async Task SaveChanges_exactly_2_mib_does_not_split_and_one_byte_
}
else
{
+ //Debug.Assert(2 == Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch), $"Failed on iteration {i}");
+ //context.Remove(customer1);
+ //context.Remove(customer2);
+ //await context.SaveChangesAsync();
+ //Fixture.ListLoggerFactory.Clear();
Assert.Equal(1, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}
+ //}
}
[ConditionalFact]
@@ -439,13 +448,26 @@ public virtual async Task SaveChanges_transaction_behavior_always_update_entitie
await context.SaveChangesAsync();
- customer1.Name = new string('x', 1097582);
- customer2.Name = new string('x', 1097583);
+ customer1.Name = new string('x', 1097736);
+ customer2.Name = new string('x', 1097737);
if (oneByteOver)
{
customer1.Name += 'x';
customer2.Name += 'x';
+ //for (var i = 1; i <= 1000; i++)
+ //{
+ // customer1.Name += 'x';
+ // customer2.Name += 'x';
+ // try
+ // {
+ // await context.SaveChangesAsync();
+ // }
+ // catch (Exception ex)
+ // {
+ // throw new Exception($"Off by 2 * {i} bytes", ex);
+ // }
+ //}
await Assert.ThrowsAsync(() => context.SaveChangesAsync());
}
else
@@ -468,13 +490,26 @@ public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_up
await context.SaveChangesAsync();
- customer1.Name = new string('x', 1097581 + (1_024 - customer1.Id.Length) * 2);
- customer2.Name = new string('x', 1097581 + (1_024 - customer2.Id.Length) * 2);
+ customer1.Name = new string('x', 1097735 + (1_024 - customer1.Id.Length) * 2);
+ customer2.Name = new string('x', 1097735 + (1_024 - customer2.Id.Length) * 2);
if (oneByteOver)
{
customer1.Name += 'x';
customer2.Name += 'x';
+ //for (var i = 1; i <= 1000; i++)
+ //{
+ // customer1.Name += 'x';
+ // customer2.Name += 'x';
+ // try
+ // {
+ // await context.SaveChangesAsync();
+ // }
+ // catch (Exception ex)
+ // {
+ // throw new Exception($"Off by 2 * {i} bytes", ex);
+ // }
+ //}
await Assert.ThrowsAsync(() => context.SaveChangesAsync());
}
else
diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
index d552709df32..da40944ac15 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
@@ -162,8 +162,9 @@ await context.AddAsync(
await context.AddAsync(
new Person { Id = 3, Addresses = new List { existingAddress1Person3, existingAddress2Person3 } });
- await context.SaveChangesAsync();
+ var entrys = context.ChangeTracker.Entries().ToList();
+ await context.SaveChangesAsync();
var people = await context.Set().ToListAsync();
Assert.Empty(people[0].Addresses);
@@ -239,12 +240,8 @@ 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);
+ people[2].Addresses.First();
existingAddress1Person3 = people[2].Addresses.First();
existingAddress2Person3 = people[2].Addresses.Last();
@@ -269,7 +266,6 @@ await context.AddAsync(
{
existingAddress2Person3.Notes.Add(new Note { Content = "City note" });
}
-
await context.SaveChangesAsync();
await AssertState(context, useIds);
@@ -348,11 +344,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 0f81ad16f9a..31ee5399a0d 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs
@@ -177,92 +177,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)
{
@@ -760,6 +674,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(
@@ -1611,10 +1544,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/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/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
index 0d56c570a44..c2115d4e3ed 100644
--- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
@@ -233,15 +233,19 @@ private async Task CreateFromFile(DbContext context)
if (reader.TokenType == JsonToken.StartObject)
{
var document = serializer.Deserialize(reader)!;
-
- document["id"] = discriminatorInId == true
+ var documentId = discriminatorInId == true
? $"{entityName}|{document["id"]}"
: $"{document["id"]}";
+ document["id"] = documentId;
+
document["$type"] = entityName;
await cosmosClient.CreateItemAsync(
- containerName!, document, new FakeUpdateEntry(), new NullSessionTokenStorage()).ConfigureAwait(false);
+ containerName!, documentId,
+ new MemoryStream(Encoding.UTF8.GetBytes(document.ToString())),
+ new FakeUpdateEntry(),
+ new NullSessionTokenStorage()).ConfigureAwait(false);
}
else if (reader.TokenType == JsonToken.EndObject)
{
From e54b34a9449e7c54fae03ce3c4ad59dd5b40003f Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 13:22:58 +0200
Subject: [PATCH 02/29] Fix tests and add clone method
---
.../Internal/CosmosTimeOnlyTypeMapping.cs | 14 ++++++
.../Internal/CosmosTimeSpanTypeMapping.cs | 14 ++++++
.../JsonTypesCosmosTest.cs | 48 +++++++++++++++++++
.../Internal/CosmosTypeMappingSourceTest.cs | 8 ++--
.../JsonTypesTestBase.cs | 24 +++++-----
5 files changed, 92 insertions(+), 16 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
index 670456536e3..b1d8dc3e3d4 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
@@ -20,4 +20,18 @@ public class CosmosTimeOnlyTypeMapping : CosmosTypeMapping
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
index effc8c3a5ba..b8fe7ca8cc4 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
@@ -20,4 +20,18 @@ public class CosmosTimeSpanTypeMapping : CosmosTypeMapping
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/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
index 2b287e85924..41a88d76f15 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.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
From bd27697576f366374c827957fc316364ae5e4e4f Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 14:00:40 +0200
Subject: [PATCH 03/29] Vector fixes
---
.../CosmosPropertyBuilderExtensions.cs | 1 +
.../Internal/CosmosJsonVectorReaderWriter.cs | 122 ------------------
.../Internal/CosmosTypeMappingSource.cs | 36 +++++-
.../Internal/CosmosVectorTypeMapping.cs | 41 +-----
.../Storage/Json/JsonValueReaderWriter.cs | 8 +-
5 files changed, 36 insertions(+), 172 deletions(-)
delete mode 100644 src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs
diff --git a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
index ba3c2b815ee..b162344c66b 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs
deleted file mode 100644
index 60a55d75818..00000000000
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosJsonVectorReaderWriter.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text.Json;
-using Microsoft.EntityFrameworkCore.Storage.Json;
-
-namespace Microsoft.EntityFrameworkCore.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 CosmosJsonVectorReaderWriter : JsonValueReaderWriter
-{
- private static readonly PropertyInfo InstanceProperty = typeof(CosmosJsonVectorReaderWriter).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 CosmosJsonVectorReaderWriter Instance { 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 override object FromJson(ref Utf8JsonReaderManager manager, object? existingObject = null)
- {
- var tokenType = manager.CurrentReader.TokenType;
- if (tokenType != JsonTokenType.StartArray)
- {
- throw new InvalidOperationException(
- CoreStrings.JsonReaderInvalidTokenType(tokenType.ToString()));
- }
-
- var result = new List();
-
- while (tokenType != JsonTokenType.EndArray)
- {
- manager.MoveNext();
- tokenType = manager.CurrentReader.TokenType;
-
- if (tokenType != JsonTokenType.Number || !manager.CurrentReader.TryGetInt32(out var intValue))
- {
- throw new InvalidOperationException(
- CoreStrings.JsonReaderInvalidTokenType(tokenType.ToString()));
- }
-
- result.Add((byte)intValue);
- }
-
- return result.ToArray();
- }
-
- ///
- /// 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 ToJson(Utf8JsonWriter writer, object value)
- {
- writer.WriteStartArray();
-
- switch (value)
- {
- case IEnumerable bytes:
- foreach (var item in bytes)
- {
- writer.WriteNumberValue(item);
- }
- break;
- case ReadOnlyMemory rom:
- foreach (var item in rom.Span)
- {
- writer.WriteNumberValue(item);
- }
- break;
- case IEnumerable bytes:
- foreach (var item in bytes)
- {
- writer.WriteNumberValue(item);
- }
- break;
- case ReadOnlyMemory rom:
- foreach (var item in rom.Span)
- {
- writer.WriteNumberValue(item);
- }
- break;
- case IEnumerable bytes:
- foreach (var item in bytes)
- {
- writer.WriteNumberValue(item);
- }
- break;
- case ReadOnlyMemory rom:
- foreach (var item in rom.Span)
- {
- writer.WriteNumberValue(item);
- }
- break;
- default:
- throw new InvalidOperationException();
- }
-
- writer.WriteEndArray();
- }
-
- ///
- public override Expression ConstructorExpression
- => Expression.Property(null, InstanceProperty);
-
- ///
- public override Type ValueType { get; } = typeof(byte[]);
-}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index 3781aa31fc4..44f0e99897d 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -48,17 +48,39 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override CoreTypeMapping? FindMapping(IProperty property)
+ {
// 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
+ if (property.GetVectorDistanceFunction() is { } distanceFunction
+ && property.GetVectorDimensions() is { } dimensions)
{
- CosmosTypeMapping mapping
- when property.GetVectorDistanceFunction() is { } distanceFunction
- && property.GetVectorDimensions() is { } dimensions
- => new CosmosVectorTypeMapping(mapping, new CosmosVectorType(distanceFunction, dimensions)),
- var other => other
- };
+ CoreTypeMapping? elementMapping = null;
+
+ var collectionType = property.ClrType;
+ Type? elementType = null;
+ if (collectionType.IsGenericType
+ && collectionType.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>))
+ {
+ collectionType = collectionType.GetGenericArguments()[0].MakeArrayType();
+ elementType = collectionType.GetElementType();
+ }
+
+ var collectionReaderWriter = TryFindJsonCollectionMapping(new TypeMappingInfo(collectionType), collectionType, null, ref elementMapping, out var _, out var r) ? r : null;
+ CoreTypeMapping mapping = new CosmosVectorTypeMapping(property.ClrType, new CosmosVectorType(distanceFunction, dimensions), collectionReaderWriter);
+
+ if (elementType != null)
+ {
+ mapping = mapping.WithComposedConverter(
+ (ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(elementType))!,
+ (ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(elementType))!);
+ }
+
+ return mapping;
+ }
+
+ return base.FindMapping(property);
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
index 83b029e5d9b..7b8d57711dd 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
@@ -25,7 +25,7 @@ public class CosmosVectorTypeMapping : CosmosTypeMapping
// Note that this default is not valid because dimensions cannot be zero. But since there is no reasonable
// default dimensions size for a vector type, this is intentionally not valid rather than just being wrong.
// The fundamental problem here is that type mappings are "required" to have some default now.
- = new(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0));
+ = new(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0), null);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -33,42 +33,9 @@ public class CosmosVectorTypeMapping : CosmosTypeMapping
/// 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 CosmosVectorTypeMapping(
- Type clrType,
- CosmosVectorType vectorType,
- ValueComparer? comparer = null,
- ValueComparer? keyComparer = null,
- CoreTypeMapping? elementMapping = null)
- : this(
- new CoreTypeMappingParameters(
- clrType,
- converter: null,
- comparer,
- keyComparer,
- elementMapping: elementMapping,
- jsonValueReaderWriter: CosmosJsonVectorReaderWriter.Instance),
- vectorType)
- {
- }
-
- ///
- /// 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 CosmosVectorTypeMapping(CosmosTypeMapping mapping, CosmosVectorType vectorType)
- : this(
- new CoreTypeMappingParameters(
- mapping.ClrType,
- // This is a hack to allow both arrays and ROM types without different function overloads or type mappings.
- converter: mapping.Converter?.GetType() == typeof(BytesToStringConverter) ? null : mapping.Converter,
- mapping.Comparer,
- mapping.KeyComparer,
- elementMapping: mapping.ElementTypeMapping,
- jsonValueReaderWriter: CosmosJsonVectorReaderWriter.Instance),
- vectorType)
+ public CosmosVectorTypeMapping(Type clrType, CosmosVectorType vectorType, JsonValueReaderWriter? jsonValueReaderWriter) : base(clrType, jsonValueReaderWriter: jsonValueReaderWriter)
{
+ VectorType = vectorType;
}
///
@@ -77,7 +44,7 @@ public CosmosVectorTypeMapping(CosmosTypeMapping mapping, CosmosVectorType vecto
/// 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 CosmosVectorTypeMapping(CoreTypeMappingParameters parameters, CosmosVectorType vectorType)
+ private CosmosVectorTypeMapping(CoreTypeMappingParameters parameters, CosmosVectorType vectorType)
: base(parameters)
=> VectorType = vectorType;
diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
index 6e280e77a3e..f61f541659d 100644
--- a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
+++ b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
@@ -15,13 +15,9 @@ namespace Microsoft.EntityFrameworkCore.Storage.Json;
public abstract class JsonValueReaderWriter
{
///
- /// 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.
+ /// Ensures the external types extend from the generic
///
- [EntityFrameworkInternal]
- public JsonValueReaderWriter()
+ internal JsonValueReaderWriter()
{
}
From da9163d3f9c6d5cd4c6a4cbccf8d604c6c75c4e4 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 14:38:49 +0200
Subject: [PATCH 04/29] fix whitespace
---
test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
index 41a88d76f15..bac2f8c1056 100644
--- a/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/JsonTypesCosmosTest.cs
@@ -48,10 +48,10 @@ 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"]}""");
+ => 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]}""");
+ => 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.
From d69f953010ae4a8e562e45f54d013a21f7b111ec Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 18:05:05 +0200
Subject: [PATCH 05/29] Fix tests
---
.../Internal/CosmosTimeOnlyTypeMapping.cs | 8 +++
.../Internal/CosmosTimeSpanTypeMapping.cs | 8 +++
.../Internal/CosmosTypeMappingSource.cs | 4 +-
.../Baselines/BigModel/ManyTypesEntityType.cs | 68 ++-----------------
4 files changed, 24 insertions(+), 64 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
index b1d8dc3e3d4..77a0a4ffaaf 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeOnlyTypeMapping.cs
@@ -11,6 +11,14 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
///
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
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
index b8fe7ca8cc4..86bb1c7844f 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTimeSpanTypeMapping.cs
@@ -11,6 +11,14 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
///
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
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index 44f0e99897d..d9e5d3198fc 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -33,8 +33,8 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
=> _clrTypeMappings
= new Dictionary
{
- { typeof(TimeOnly), new CosmosTimeOnlyTypeMapping() },
- { typeof(TimeSpan), new CosmosTimeSpanTypeMapping() },
+ { typeof(TimeOnly), CosmosTimeOnlyTypeMapping.Default },
+ { typeof(TimeSpan), CosmosTimeSpanTypeMapping.Default },
{
typeof(JObject), new CosmosTypeMapping(
typeof(JObject), jsonValueReaderWriter: dependencies.JsonValueReaderWriterSource.FindReaderWriter(typeof(JObject)))
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",
From 530250623831aaa6dfe4d1ed5aa9d8dc9f7b4980 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 20:44:43 +0200
Subject: [PATCH 06/29] Clean
---
src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs | 1 -
.../Query/Internal/CosmosSerializationUtilities.cs | 2 +-
src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
index b162344c66b..ba3c2b815ee 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
-using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
index 98a7b309083..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 // @TODO: Can this be removed? Use document source instead?
+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/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
index 3aeeb145872..5ad66bcbfd6 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
@@ -520,7 +520,7 @@ public virtual DocumentSource GetDocumentSource(IEntityType entityType)
if (!_documentCollections.TryGetValue(entityType, out var documentSource))
{
_documentCollections.Add(
- entityType, documentSource = new DocumentSource(entityType)); // @TODO: Make this singleton?
+ entityType, documentSource = new DocumentSource(entityType)); // @TODO: Make this singleton #34567
}
return documentSource;
From b38b19cdfdd9dcc834e215235078771465bd6a14 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 21:53:55 +0200
Subject: [PATCH 07/29] Clean
---
.../Storage/Internal/CosmosClientWrapper.cs | 44 -------------------
1 file changed, 44 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index f3a195c780c..34c7da6039d 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -676,50 +676,6 @@ private static void ProcessWriteResponse(IUpdateEntry entry, string eTag, Stream
{
entry.SetStoreGeneratedValue(etagProperty, eTag);
}
-
- // @TODO: If the entry is loaded from the database, has an etag and has no triggers, we know that nothing has changed in the meantime right?
- // Could we optimize?
- if (content != null && content.Length > 0)
- {
- // @TODO: Entries without etag or with a pre- or post- trigger could have an updated document returned.
- // We should consider processing the returned document in that case as well
- // Invoke tracking shaper labmda?
-
- // How did that work before? is appears to be only updaing the underlying jobject (removed now), but not the entry? Would setting the _jObject update the entry no right?
- // But updating the underlying jobject would make it so that subsequent updates would not use old data, unless the user updated the property on the entity...
- // Now that behaviour would be gone.
-
- // @TODO: Ask what to do here?
-
- // used to be this:
- //var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
- //if (jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate }
- // && content != 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);
- //}
-
- // jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate } is always false by default..
- // Can the user set this to true? Probably right.
- // Interestingly, default for enableContentResponseOnRewrite is true, but jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate } is always false.
- // So enableContentResponseOnRewrite default or true does nothing without modifying the jObjectProperty's value generated.
- // Does this mean anyone was really using this?
- // Implementing this to work without setting jobject property value generated (because it will be removed) would be a theoratical breaking change?
- // Or a bug fix?
-
- // No tests that don't use __jObject fail without this code
- // @TODO: Add tests for this on main.
-
- // Suggestion: Make the new default enableContentResponseOnRewrite false?
- // People who have set jObjectProperty ValueGenerated can migrate by manually setting EnableContentResponseOnRewrite to true.
- // Others will notice no changes this way.
- }
}
///
From ab0a8f56e7e3175c404ba24aa0de97c47f76486f Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Mon, 30 Mar 2026 21:55:25 +0200
Subject: [PATCH 08/29] remove ContentResponseOnWriteEnabled
---
.../Internal/CosmosDbOptionExtension.cs | 27 --------------
.../Internal/CosmosSingletonOptions.cs | 8 -----
.../Internal/ICosmosSingletonOptions.cs | 8 -----
.../Storage/Internal/CosmosClientWrapper.cs | 14 ++++----
.../CosmosTransactionalBatchWrapper.cs | 15 ++++----
.../Storage/Internal/RequestOptionsHelper.cs | 36 +++----------------
.../CosmosDbContextOptionsExtensionsTests.cs | 1 -
7 files changed, 16 insertions(+), 93 deletions(-)
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
index e1656712408..7b994bb9e1a 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
@@ -33,7 +33,6 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension
private int? _gatewayModeMaxConnectionLimit;
private int? _maxTcpConnectionsPerEndpoint;
private int? _maxRequestsPerTcpConnection;
- private bool? _enableContentResponseOnWrite;
private DbContextOptionsExtensionInfo? _info;
private Func? _httpClientFactory;
private SessionTokenManagementMode _sessionTokenManagementMode = SessionTokenManagementMode.FullyAutomatic;
@@ -497,30 +496,6 @@ public virtual CosmosOptionsExtension WithMaxRequestsPerTcpConnection(int? reque
return clone;
}
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public virtual bool? EnableContentResponseOnWrite
- => _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 virtual CosmosOptionsExtension ContentResponseOnWriteEnabled(bool enabled)
- {
- var clone = Clone();
-
- clone._enableContentResponseOnWrite = enabled;
-
- return clone;
- }
-
///
/// A factory for creating the default , or if none has been
/// configured.
@@ -674,7 +649,6 @@ public override int GetServiceProviderHashCode()
hashCode.Add(Extension._region);
hashCode.Add(Extension._connectionMode);
hashCode.Add(Extension._limitToEndpoint);
- hashCode.Add(Extension._enableContentResponseOnWrite);
hashCode.Add(Extension._webProxy);
hashCode.Add(Extension._requestTimeout);
hashCode.Add(Extension._openTcpConnectionTimeout);
@@ -701,7 +675,6 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
&& Extension._region == otherInfo.Extension._region
&& Extension._connectionMode == otherInfo.Extension._connectionMode
&& Extension._limitToEndpoint == otherInfo.Extension._limitToEndpoint
- && Extension._enableContentResponseOnWrite == otherInfo.Extension._enableContentResponseOnWrite
&& Extension._webProxy == otherInfo.Extension._webProxy
&& Extension._requestTimeout == otherInfo.Extension._requestTimeout
&& Extension._openTcpConnectionTimeout == otherInfo.Extension._openTcpConnectionTimeout
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
index 5fa7ae5a2da..f1c6292c62a 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
@@ -135,14 +135,6 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions
///
public virtual int? MaxRequestsPerTcpConnection { get; private set; }
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public virtual bool? EnableContentResponseOnWrite { get; }
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs
index b2d043279fb..84f64d3ed56 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs
@@ -140,14 +140,6 @@ public interface ICosmosSingletonOptions : ISingletonOptions
///
int? MaxRequestsPerTcpConnection { 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.
- ///
- bool? EnableContentResponseOnWrite { get; }
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index 34c7da6039d..cbbc6286190 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -46,7 +46,6 @@ public class CosmosClientWrapper : ICosmosClientWrapper
private readonly string _databaseId;
private readonly IExecutionStrategy _executionStrategy;
private readonly IDiagnosticsLogger _commandLogger;
- private readonly bool? _enableContentResponseOnWrite;
static CosmosClientWrapper()
{
@@ -74,7 +73,6 @@ public CosmosClientWrapper(
_databaseId = options!.DatabaseName;
_executionStrategy = executionStrategy;
_commandLogger = commandLogger;
- _enableContentResponseOnWrite = options.EnableContentResponseOnWrite;
}
///
@@ -332,7 +330,7 @@ private static async Task CreateItemOnceAsync(
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);
@@ -395,7 +393,7 @@ private static async Task ReplaceItemOnceAsync(
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);
@@ -458,7 +456,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);
@@ -516,7 +514,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);
}
///
@@ -555,9 +553,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
{
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
index f2f9fead506..d9ad740b512 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
@@ -22,7 +22,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 +34,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;
}
///
@@ -77,7 +74,7 @@ public CosmosTransactionalBatchWrapper(
///
public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -104,7 +101,7 @@ public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
///
public bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -131,7 +128,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 +155,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)
diff --git a/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs b/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
index 7e4e20b46ba..cfd653a5c9f 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;
}
///
@@ -31,7 +30,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 virtual bool EnableContentResponseOnWrite { get; }
+ public virtual bool EnableContentResponseOnWrite { get; } = false;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -39,7 +38,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 +53,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/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
index b7c7b66580a..886932226b9 100644
--- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
+++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
@@ -64,7 +64,6 @@ 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));
- Test(o => o.ContentResponseOnWriteEnabled(), o => Assert.True(o.EnableContentResponseOnWrite));
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));
From 139fa7922a06e4df119e213b8ceaafb136b01ae4 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 09:36:51 +0200
Subject: [PATCH 09/29] Revert "remove ContentResponseOnWriteEnabled"
This reverts commit ab0a8f56e7e3175c404ba24aa0de97c47f76486f.
---
.../Internal/CosmosDbOptionExtension.cs | 27 ++++++++++++++
.../Internal/CosmosSingletonOptions.cs | 8 +++++
.../Internal/ICosmosSingletonOptions.cs | 8 +++++
.../Storage/Internal/CosmosClientWrapper.cs | 14 ++++----
.../CosmosTransactionalBatchWrapper.cs | 15 ++++----
.../Storage/Internal/RequestOptionsHelper.cs | 36 ++++++++++++++++---
.../CosmosDbContextOptionsExtensionsTests.cs | 1 +
7 files changed, 93 insertions(+), 16 deletions(-)
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
index 7b994bb9e1a..e1656712408 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosDbOptionExtension.cs
@@ -33,6 +33,7 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension
private int? _gatewayModeMaxConnectionLimit;
private int? _maxTcpConnectionsPerEndpoint;
private int? _maxRequestsPerTcpConnection;
+ private bool? _enableContentResponseOnWrite;
private DbContextOptionsExtensionInfo? _info;
private Func? _httpClientFactory;
private SessionTokenManagementMode _sessionTokenManagementMode = SessionTokenManagementMode.FullyAutomatic;
@@ -496,6 +497,30 @@ public virtual CosmosOptionsExtension WithMaxRequestsPerTcpConnection(int? reque
return clone;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual bool? EnableContentResponseOnWrite
+ => _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 virtual CosmosOptionsExtension ContentResponseOnWriteEnabled(bool enabled)
+ {
+ var clone = Clone();
+
+ clone._enableContentResponseOnWrite = enabled;
+
+ return clone;
+ }
+
///
/// A factory for creating the default , or if none has been
/// configured.
@@ -649,6 +674,7 @@ public override int GetServiceProviderHashCode()
hashCode.Add(Extension._region);
hashCode.Add(Extension._connectionMode);
hashCode.Add(Extension._limitToEndpoint);
+ hashCode.Add(Extension._enableContentResponseOnWrite);
hashCode.Add(Extension._webProxy);
hashCode.Add(Extension._requestTimeout);
hashCode.Add(Extension._openTcpConnectionTimeout);
@@ -675,6 +701,7 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
&& Extension._region == otherInfo.Extension._region
&& Extension._connectionMode == otherInfo.Extension._connectionMode
&& Extension._limitToEndpoint == otherInfo.Extension._limitToEndpoint
+ && Extension._enableContentResponseOnWrite == otherInfo.Extension._enableContentResponseOnWrite
&& Extension._webProxy == otherInfo.Extension._webProxy
&& Extension._requestTimeout == otherInfo.Extension._requestTimeout
&& Extension._openTcpConnectionTimeout == otherInfo.Extension._openTcpConnectionTimeout
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
index f1c6292c62a..5fa7ae5a2da 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosSingletonOptions.cs
@@ -135,6 +135,14 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions
///
public virtual int? MaxRequestsPerTcpConnection { get; private set; }
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual bool? EnableContentResponseOnWrite { get; }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs
index 84f64d3ed56..b2d043279fb 100644
--- a/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs
+++ b/src/EFCore.Cosmos/Infrastructure/Internal/ICosmosSingletonOptions.cs
@@ -140,6 +140,14 @@ public interface ICosmosSingletonOptions : ISingletonOptions
///
int? MaxRequestsPerTcpConnection { 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.
+ ///
+ bool? EnableContentResponseOnWrite { get; }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index cbbc6286190..34c7da6039d 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -46,6 +46,7 @@ public class CosmosClientWrapper : ICosmosClientWrapper
private readonly string _databaseId;
private readonly IExecutionStrategy _executionStrategy;
private readonly IDiagnosticsLogger _commandLogger;
+ private readonly bool? _enableContentResponseOnWrite;
static CosmosClientWrapper()
{
@@ -73,6 +74,7 @@ public CosmosClientWrapper(
_databaseId = options!.DatabaseName;
_executionStrategy = executionStrategy;
_commandLogger = commandLogger;
+ _enableContentResponseOnWrite = options.EnableContentResponseOnWrite;
}
///
@@ -330,7 +332,7 @@ private static async Task CreateItemOnceAsync(
var wrapper = parameters.Wrapper;
var sessionTokenStorage = parameters.SessionTokenStorage;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
- var itemRequestOptions = CreateItemRequestOptions(entry, sessionTokenStorage.GetSessionToken(containerId));
+ var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite, sessionTokenStorage.GetSessionToken(containerId));
var partitionKeyValue = ExtractPartitionKeyValue(entry);
var preTriggers = GetTriggers(entry, TriggerType.Pre, TriggerOperation.Create);
var postTriggers = GetTriggers(entry, TriggerType.Post, TriggerOperation.Create);
@@ -393,7 +395,7 @@ private static async Task ReplaceItemOnceAsync(
var wrapper = parameters.Wrapper;
var sessionTokenStorage = parameters.SessionTokenStorage;
var container = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
- var itemRequestOptions = CreateItemRequestOptions(entry, sessionTokenStorage.GetSessionToken(containerId));
+ var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite, sessionTokenStorage.GetSessionToken(containerId));
var partitionKeyValue = ExtractPartitionKeyValue(entry);
var preTriggers = GetTriggers(entry, TriggerType.Pre, TriggerOperation.Replace);
var postTriggers = GetTriggers(entry, TriggerType.Post, TriggerOperation.Replace);
@@ -456,7 +458,7 @@ private static async Task DeleteItemOnceAsync(
var sessionTokenStorage = parameters.SessionTokenStorage;
var items = wrapper.Client.GetDatabase(wrapper._databaseId).GetContainer(parameters.ContainerId);
- var itemRequestOptions = CreateItemRequestOptions(entry, sessionTokenStorage.GetSessionToken(containerId));
+ var itemRequestOptions = CreateItemRequestOptions(entry, wrapper._enableContentResponseOnWrite, sessionTokenStorage.GetSessionToken(containerId));
var partitionKeyValue = ExtractPartitionKeyValue(entry);
var preTriggers = GetTriggers(entry, TriggerType.Pre, TriggerOperation.Delete);
var postTriggers = GetTriggers(entry, TriggerType.Post, TriggerOperation.Delete);
@@ -514,7 +516,7 @@ public virtual ICosmosTransactionalBatchWrapper CreateTransactionalBatch(string
var batch = container.CreateTransactionalBatch(partitionKeyValue);
- return new CosmosTransactionalBatchWrapper(batch, containerId, partitionKeyValue, checkSize);
+ return new CosmosTransactionalBatchWrapper(batch, containerId, partitionKeyValue, checkSize, _enableContentResponseOnWrite);
}
///
@@ -553,9 +555,9 @@ private static async Task ExecuteTransactionalBa
return ProcessBatchResponse(batch.CollectionId, response, batch.Entries, sessionTokenStorage);
}
- private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, string? sessionToken)
+ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, bool? enableContentResponseOnWrite, string? sessionToken)
{
- var helper = RequestOptionsHelper.Create(entry);
+ var helper = RequestOptionsHelper.Create(entry, enableContentResponseOnWrite);
var itemRequestOptions = new ItemRequestOptions
{
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
index d9ad740b512..f2f9fead506 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
@@ -22,6 +22,7 @@ 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();
///
@@ -34,12 +35,14 @@ public CosmosTransactionalBatchWrapper(
TransactionalBatch transactionalBatch,
string collectionId,
PartitionKey partitionKeyValue,
- bool checkSize)
+ bool checkSize,
+ bool? enableContentResponseOnWrite)
{
_transactionalBatch = transactionalBatch;
_collectionId = collectionId;
_partitionKeyValue = partitionKeyValue;
_checkSize = checkSize;
+ _enableContentResponseOnWrite = enableContentResponseOnWrite;
}
///
@@ -74,7 +77,7 @@ public CosmosTransactionalBatchWrapper(
///
public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -101,7 +104,7 @@ public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
///
public bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -128,7 +131,7 @@ public bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEnt
///
public bool DeleteItem(string documentId, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -155,9 +158,9 @@ public bool DeleteItem(string documentId, IUpdateEntry updateEntry)
///
public TransactionalBatch GetTransactionalBatch() => _transactionalBatch;
- private TransactionalBatchItemRequestOptions? CreateItemRequestOptions(IUpdateEntry entry, out int size)
+ private TransactionalBatchItemRequestOptions? CreateItemRequestOptions(IUpdateEntry entry, bool? enableContentResponseOnWrite, out int size)
{
- var helper = RequestOptionsHelper.Create(entry);
+ var helper = RequestOptionsHelper.Create(entry, enableContentResponseOnWrite);
size = 0;
if (helper == null)
diff --git a/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs b/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
index cfd653a5c9f..7e4e20b46ba 100644
--- a/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/RequestOptionsHelper.cs
@@ -11,9 +11,10 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
///
public class RequestOptionsHelper
{
- private RequestOptionsHelper(string? ifMatchEtag)
+ private RequestOptionsHelper(string? ifMatchEtag, bool enableContentResponseOnWrite)
{
IfMatchEtag = ifMatchEtag;
+ EnableContentResponseOnWrite = enableContentResponseOnWrite;
}
///
@@ -30,7 +31,7 @@ private RequestOptionsHelper(string? ifMatchEtag)
/// 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; } = false;
+ public virtual bool EnableContentResponseOnWrite { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -38,7 +39,7 @@ private RequestOptionsHelper(string? ifMatchEtag)
/// 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)
+ public static RequestOptionsHelper? Create(IUpdateEntry entry, bool? enableContentResponseOnWrite)
{
var etagProperty = entry.EntityType.GetETagProperty();
if (etagProperty == null)
@@ -53,6 +54,33 @@ private RequestOptionsHelper(string? ifMatchEtag)
etag = converter.ConvertToProvider(etag);
}
- return new RequestOptionsHelper((string?)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);
}
}
diff --git a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
index 886932226b9..b7c7b66580a 100644
--- a/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
+++ b/test/EFCore.Cosmos.Tests/Extensions/CosmosDbContextOptionsExtensionsTests.cs
@@ -64,6 +64,7 @@ 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));
+ Test(o => o.ContentResponseOnWriteEnabled(), o => Assert.True(o.EnableContentResponseOnWrite));
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));
From 8bb031ed731e67979d156b6b85225ecb31adcc87 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 09:55:46 +0200
Subject: [PATCH 10/29] Obsolete EnableContentResponseOnWrite
---
.../CosmosDbContextOptionsBuilder.cs | 5 ++-
.../Internal/CosmosDbOptionExtension.cs | 1 +
.../Internal/CosmosSingletonOptions.cs | 4 +-
.../Storage/Internal/CosmosClientWrapper.cs | 15 +++----
.../CosmosTransactionalBatchWrapper.cs | 18 ++++----
.../Storage/Internal/RequestOptionsHelper.cs | 41 ++-----------------
.../Internal/SingletonCosmosClientWrapper.cs | 2 +
.../CosmosConcurrencyTest.cs | 4 ++
.../CosmosDbContextOptionsExtensionsTests.cs | 2 +
9 files changed, 31 insertions(+), 61 deletions(-)
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/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index 34c7da6039d..8d099a8606a 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -46,7 +46,6 @@ public class CosmosClientWrapper : ICosmosClientWrapper
private readonly string _databaseId;
private readonly IExecutionStrategy _executionStrategy;
private readonly IDiagnosticsLogger _commandLogger;
- private readonly bool? _enableContentResponseOnWrite;
static CosmosClientWrapper()
{
@@ -74,7 +73,6 @@ public CosmosClientWrapper(
_databaseId = options!.DatabaseName;
_executionStrategy = executionStrategy;
_commandLogger = commandLogger;
- _enableContentResponseOnWrite = options.EnableContentResponseOnWrite;
}
///
@@ -332,7 +330,7 @@ private static async Task CreateItemOnceAsync(
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);
@@ -395,7 +393,7 @@ private static async Task ReplaceItemOnceAsync(
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);
@@ -458,7 +456,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);
@@ -516,7 +514,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);
}
///
@@ -555,9 +553,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
{
@@ -567,7 +565,6 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, b
if (helper != null)
{
itemRequestOptions.IfMatchEtag = helper.IfMatchEtag;
- itemRequestOptions.EnableContentResponseOnWrite = helper.EnableContentResponseOnWrite;
}
return itemRequestOptions;
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
index f2f9fead506..e56fc7ece9c 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTransactionalBatchWrapper.cs
@@ -22,7 +22,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 +34,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;
}
///
@@ -77,7 +74,7 @@ public CosmosTransactionalBatchWrapper(
///
public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -104,7 +101,7 @@ public bool CreateItem(string id, Stream stream, IUpdateEntry updateEntry)
///
public bool ReplaceItem(string documentId, Stream stream, IUpdateEntry updateEntry)
{
- var itemRequestOptions = CreateItemRequestOptions(updateEntry, _enableContentResponseOnWrite, out var itemRequestOptionsLength);
+ var itemRequestOptions = CreateItemRequestOptions(updateEntry, out var itemRequestOptionsLength);
if (_checkSize)
{
@@ -131,7 +128,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 +155,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 +170,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/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..ac5c298ec9c 100644
--- a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs
@@ -93,6 +93,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/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index 3a576324a62..77e859a1485 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -59,7 +59,9 @@ public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentRespo
{
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
}
})
.Options;
@@ -119,7 +121,9 @@ public async Task Etag_is_updated_in_derived_entity_after_SaveChanges(bool? cont
{
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
}
})
.Options;
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));
From a1ad382d789eec0380e1ccd9b81d79224d2fead4 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 14:18:06 +0200
Subject: [PATCH 11/29] Simplify batch tests
---
.../CosmosConcurrencyTest.cs | 10 +-
.../CosmosTransactionalBatchTest.cs | 244 +++---------------
2 files changed, 40 insertions(+), 214 deletions(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index 77e859a1485..197acde4f9a 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -54,7 +54,7 @@ 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)
@@ -63,8 +63,7 @@ public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentRespo
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
#pragma warning restore CS0618 // Type or member is obsolete
}
- })
- .Options;
+ }))).Options;
var customer = new Customer
{
@@ -116,7 +115,7 @@ 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)
@@ -125,8 +124,7 @@ public async Task Etag_is_updated_in_derived_entity_after_SaveChanges(bool? cont
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
#pragma warning restore CS0618 // Type or member is obsolete
}
- })
- .Options;
+ }))).Options;
var customer = new PremiumCustomer
{
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
index 26cfa81bdb3..4d2b0209cfb 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net;
+using System.Runtime.CompilerServices;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Scripts;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
@@ -279,50 +280,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)
- {
- //for (var i = 350; i > 0; i--)
- //{
-
- 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', 1045148);
- customer2.Name = new string('x', 1045148);
-
- if (oneByteOver)
- {
- customer1.Name += 'x';
- }
-
- await context.SaveChangesAsync();
- using var assertContext = Fixture.CreateContext();
- Assert.Equal(2, (await assertContext.Customers.ToListAsync()).Count);
-
- if (oneByteOver)
- {
- Assert.Equal(2, Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
- }
- else
- {
- //Debug.Assert(2 == Fixture.ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch), $"Failed on iteration {i}");
- //context.Remove(customer1);
- //context.Remove(customer2);
- //await context.SaveChangesAsync();
- //Fixture.ListLoggerFactory.Clear();
- 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()
@@ -340,22 +297,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()
{
@@ -375,18 +316,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);
@@ -394,15 +336,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));
}
@@ -412,162 +358,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', 1097736);
- customer2.Name = new string('x', 1097737);
-
- if (oneByteOver)
- {
- customer1.Name += 'x';
- customer2.Name += 'x';
- //for (var i = 1; i <= 1000; i++)
- //{
- // customer1.Name += 'x';
- // customer2.Name += 'x';
- // try
- // {
- // await context.SaveChangesAsync();
- // }
- // catch (Exception ex)
- // {
- // throw new Exception($"Off by 2 * {i} bytes", ex);
- // }
- //}
- 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;
+ Fixture.ListLoggerFactory.Clear();
- 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) };
-
- 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', 1097735 + (1_024 - customer1.Id.Length) * 2);
- customer2.Name = new string('x', 1097735 + (1_024 - customer2.Id.Length) * 2);
-
- if (oneByteOver)
- {
- customer1.Name += 'x';
- customer2.Name += 'x';
- //for (var i = 1; i <= 1000; i++)
- //{
- // customer1.Name += 'x';
- // customer2.Name += 'x';
- // try
- // {
- // await context.SaveChangesAsync();
- // }
- // catch (Exception ex)
- // {
- // throw new Exception($"Off by 2 * {i} bytes", ex);
- // }
- //}
- 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));
}
}
From 1d432d53224294818f47482004a4174184b49425 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 14:18:42 +0200
Subject: [PATCH 12/29] Clean
---
test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
index 6093a7b6b59..2eb32d9017d 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
@@ -163,8 +163,6 @@ await context.AddAsync(
await context.AddAsync(
new Person { Id = 3, Addresses = new List { existingAddress1Person3, existingAddress2Person3 } });
- var entrys = context.ChangeTracker.Entries().ToList();
-
await context.SaveChangesAsync();
var people = await context.Set().ToListAsync();
From 057f7693bb7790ac99ca086076e5ecc9cf7ffbad Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 15:31:30 +0200
Subject: [PATCH 13/29] Set type mapping via CosmosPropertyBuilderExtensions
---
.../CosmosPropertyBuilderExtensions.cs | 3 ++
.../Internal/CosmosTypeMappingSource.cs | 42 ------------------
.../Internal/CosmosVectorTypeMapping.cs | 43 +++++++++++++++++--
3 files changed, 42 insertions(+), 46 deletions(-)
diff --git a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
index ba3c2b815ee..5a50e25d5ee 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
+using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
@@ -122,6 +123,7 @@ public static PropertyBuilder IsVectorProperty(
{
propertyBuilder.Metadata.SetVectorDistanceFunction(ValidateVectorDistanceFunction(distanceFunction));
propertyBuilder.Metadata.SetVectorDimensions(dimensions);
+ propertyBuilder.Metadata.SetTypeMapping(CosmosVectorTypeMapping.Create(propertyBuilder.Metadata.ClrType, new CosmosVectorType(distanceFunction, dimensions)));
return propertyBuilder;
}
@@ -176,6 +178,7 @@ public static PropertyBuilder IsVectorProperty(
propertyBuilder.Metadata.SetVectorDistanceFunction(ValidateVectorDistanceFunction(distanceFunction), fromDataAnnotation);
propertyBuilder.Metadata.SetVectorDimensions(dimensions, fromDataAnnotation);
+ propertyBuilder.Metadata.SetTypeMapping(CosmosVectorTypeMapping.Create(propertyBuilder.Metadata.ClrType, new CosmosVectorType(distanceFunction, dimensions)));
return propertyBuilder;
}
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index d9e5d3198fc..edf70fafa81 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -5,7 +5,6 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
-using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.EntityFrameworkCore.Storage.Json;
using Newtonsoft.Json.Linq;
@@ -41,47 +40,6 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
}
}.ToFrozenDictionary();
- ///
- /// 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 CoreTypeMapping? FindMapping(IProperty property)
- {
- // 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.
- if (property.GetVectorDistanceFunction() is { } distanceFunction
- && property.GetVectorDimensions() is { } dimensions)
- {
- CoreTypeMapping? elementMapping = null;
-
- var collectionType = property.ClrType;
- Type? elementType = null;
- if (collectionType.IsGenericType
- && collectionType.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>))
- {
- collectionType = collectionType.GetGenericArguments()[0].MakeArrayType();
- elementType = collectionType.GetElementType();
- }
-
- var collectionReaderWriter = TryFindJsonCollectionMapping(new TypeMappingInfo(collectionType), collectionType, null, ref elementMapping, out var _, out var r) ? r : null;
- CoreTypeMapping mapping = new CosmosVectorTypeMapping(property.ClrType, new CosmosVectorType(distanceFunction, dimensions), collectionReaderWriter);
-
- if (elementType != null)
- {
- mapping = mapping.WithComposedConverter(
- (ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(elementType))!,
- (ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(elementType))!);
- }
-
- return mapping;
- }
-
- return base.FindMapping(property);
- }
-
///
/// 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/CosmosVectorTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
index 7b8d57711dd..7c004f31ef4 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
@@ -25,7 +25,7 @@ public class CosmosVectorTypeMapping : CosmosTypeMapping
// Note that this default is not valid because dimensions cannot be zero. But since there is no reasonable
// default dimensions size for a vector type, this is intentionally not valid rather than just being wrong.
// The fundamental problem here is that type mappings are "required" to have some default now.
- = new(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0), null);
+ = Create(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -33,9 +33,44 @@ public class CosmosVectorTypeMapping : CosmosTypeMapping
/// 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 CosmosVectorTypeMapping(Type clrType, CosmosVectorType vectorType, JsonValueReaderWriter? jsonValueReaderWriter) : base(clrType, jsonValueReaderWriter: jsonValueReaderWriter)
+ public static CosmosVectorTypeMapping Create(Type clrType, CosmosVectorType vectorType)
{
- VectorType = vectorType;
+ var collectionType = clrType;
+ var isRom = clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>);
+ if (isRom)
+ {
+ collectionType = clrType.GetGenericArguments()[0].MakeArrayType();
+ }
+
+ var elementType = collectionType.GetElementType()!;
+
+ JsonValueReaderWriter? jsonValueReaderWriter = collectionType switch
+ {
+ Type t when t == typeof(byte[]) => new JsonCollectionOfStructsReaderWriter(JsonByteReaderWriter.Instance),
+ Type t when t == typeof(sbyte[]) => new JsonCollectionOfStructsReaderWriter(JsonSByteReaderWriter.Instance),
+ Type t when t == typeof(float[]) => new JsonCollectionOfStructsReaderWriter(JsonFloatReaderWriter.Instance),
+ _ => null
+ };
+
+ var parameters = new CoreTypeMappingParameters(
+ clrType,
+ null,
+ null,
+ null,
+ null,
+ jsonValueReaderWriter: jsonValueReaderWriter);
+
+ if (isRom)
+ {
+ parameters = parameters.WithComposedConverter(
+ (ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(elementType))!,
+ (ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(elementType))!,
+ null,
+ null,
+ null);
+ }
+
+ return new CosmosVectorTypeMapping(parameters, vectorType);
}
///
@@ -44,7 +79,7 @@ public CosmosVectorTypeMapping(Type clrType, CosmosVectorType vectorType, JsonVa
/// 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.
///
- private CosmosVectorTypeMapping(CoreTypeMappingParameters parameters, CosmosVectorType vectorType)
+ protected CosmosVectorTypeMapping(CoreTypeMappingParameters parameters, CosmosVectorType vectorType)
: base(parameters)
=> VectorType = vectorType;
From a411b55ade4bb4b3931f29f2cc7409447b0e0688 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 21:04:29 +0200
Subject: [PATCH 14/29] Ignore ManyServiceProvidersCreatedWarning
---
.../TestUtilities/CosmosTestStore.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
index 379a03d19c0..50bd8aea926 100644
--- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
@@ -86,6 +86,8 @@ public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuild
result.AddInterceptors(LinuxEmulatorSaveChangesInterceptor.Instance);
}
+ builder.ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
+
return result;
}
From 87ccd090b313893477f932e13b419de0fad83d25 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Tue, 31 Mar 2026 21:12:59 +0200
Subject: [PATCH 15/29] Clean
---
.../CosmosTransactionalBatchTest.cs | 1 -
.../TestUtilities/CosmosTestStore.cs | 1 +
2 files changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
index 4d2b0209cfb..9392722784e 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net;
-using System.Runtime.CompilerServices;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Scripts;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
index 50bd8aea926..ade073c9910 100644
--- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
@@ -169,6 +169,7 @@ protected override async Task InitializeAsync(Func createContext, Fun
{
return;
}
+
await base.InitializeAsync(createContext ?? (() => _storeContext), seed, clean).ConfigureAwait(false);
}
From 6133ccb6137b7694d92b4649dc5e941a17e11cb5 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 09:47:42 +0200
Subject: [PATCH 16/29] Move ManyServiceProvidersCreatedWarning to specific
test
---
test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs | 6 ++++--
.../TestUtilities/CosmosTestStore.cs | 2 --
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index 197acde4f9a..b6adaebde2b 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -63,7 +63,8 @@ public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentRespo
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
#pragma warning restore CS0618 // Type or member is obsolete
}
- }))).Options;
+ })
+ .ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)))).Options;
var customer = new Customer
{
@@ -124,7 +125,8 @@ public async Task Etag_is_updated_in_derived_entity_after_SaveChanges(bool? cont
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
#pragma warning restore CS0618 // Type or member is obsolete
}
- }))).Options;
+ })
+ .ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)))).Options;
var customer = new PremiumCustomer
{
diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
index ade073c9910..554256887ac 100644
--- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs
@@ -86,8 +86,6 @@ public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuild
result.AddInterceptors(LinuxEmulatorSaveChangesInterceptor.Instance);
}
- builder.ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
-
return result;
}
From 2e3f7c4bdc9fe2d4988987d916f55392eab96acd Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 09:52:36 +0200
Subject: [PATCH 17/29] Use HandlesNullWrites
---
.../Update/Internal/DocumentSource.cs | 3 +--
.../Storage/Json/JsonConvertedValueReaderWriter.cs | 14 +++++++++++++-
src/EFCore/Storage/Json/JsonValueReaderWriter.cs | 11 ++++++++++-
src/EFCore/Storage/Json/JsonValueReaderWriter`.cs | 11 +++++++++--
4 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
index 0432123be04..bb336243b84 100644
--- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
+++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
@@ -7,7 +7,6 @@
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
-using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.EntityFrameworkCore.Update.Internal;
namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal;
@@ -104,7 +103,7 @@ private void WriteJsonObject(
writer.WritePropertyName(jsonPropertyName);
var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter;
- if (propertyValue is not null || jsonValueReaderWriter is IJsonConvertedValueReaderWriter { Converter.ConvertsNulls: true })
+ if (propertyValue is not null || jsonValueReaderWriter?.HandlesNullWrites == true)
{
Check.DebugAssert(jsonValueReaderWriter is not null, $"Missing JsonValueReaderWriter for property: {property}");
jsonValueReaderWriter.ToJson(writer, propertyValue!);
diff --git a/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs
index 8c137b0e3df..74a36927a6e 100644
--- a/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs
+++ b/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs
@@ -32,13 +32,25 @@ public JsonConvertedValueReaderWriter(
_converter = converter;
}
+ ///
+ public override bool HandlesNullWrites => _converter.ConvertsNulls;
+
///
public override TModel FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> (TModel)_converter.ConvertFromProvider(_providerReaderWriter.FromJsonTyped(ref manager, existingObject))!;
///
public override void ToJsonTyped(Utf8JsonWriter writer, TModel value)
- => _providerReaderWriter.ToJson(writer, (TProvider)_converter.ConvertToProvider(value)!);
+ {
+ var convertedValue = _converter.ConvertToProvider(value);
+ if (convertedValue == null && !_providerReaderWriter.HandlesNullWrites)
+ {
+ writer.WriteNullValue();
+ return;
+ }
+
+ _providerReaderWriter.ToJson(writer, convertedValue);
+ }
JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter
=> _providerReaderWriter;
diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
index f61f541659d..94daf881dcb 100644
--- a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
+++ b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs
@@ -21,6 +21,15 @@ internal JsonValueReaderWriter()
{
}
+ ///
+ /// If , then the nulls will be passed to the writer's method. Otherwise null
+ /// values will always be written as .
+ ///
+ ///
+ /// The default is .
+ ///
+ public virtual bool HandlesNullWrites { get; } = false;
+
///
/// Reads the value from a UTF8 JSON stream or buffer.
///
@@ -48,7 +57,7 @@ internal JsonValueReaderWriter()
///
/// The into which the value should be written.
/// The value to write.
- public abstract void ToJson(Utf8JsonWriter writer, object value);
+ public abstract void ToJson(Utf8JsonWriter writer, object? value);
///
/// The type of the value being read/written.
diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs
index f6b67545f7e..1c9735fb8d2 100644
--- a/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs
+++ b/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs
@@ -15,8 +15,15 @@ public sealed override object FromJson(ref Utf8JsonReaderManager manager, object
=> FromJsonTyped(ref manager, existingObject)!;
///
- public sealed override void ToJson(Utf8JsonWriter writer, object value)
- => ToJsonTyped(writer, (TValue)value!);
+ public sealed override void ToJson(Utf8JsonWriter writer, object? value)
+ {
+ if (value == null && !HandlesNullWrites)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ ToJsonTyped(writer, (TValue)value!);
+ }
///
public sealed override Type ValueType
From 8bb011068b69dd0a0bd5d4bc5c1a3919a1025dfb Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 10:12:56 +0200
Subject: [PATCH 18/29] Add more Ignore(ManyServiceProvidersCreatedWarning) to
ConfigPatternsCosmosTest
---
.../EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
index dffdabadae9..6322a8e0765 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
@@ -187,7 +187,9 @@ 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)
+ .Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
From f0a20b213763f0d5ca5cd6efc76715eb4113da7c Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 13:06:45 +0200
Subject: [PATCH 19/29] Use separate SP
---
.../ConfigPatternsCosmosTest.cs | 3 +--
.../CosmosConcurrencyTest.cs | 14 ++++++++++----
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
index 6322a8e0765..890ff565da1 100644
--- a/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/ConfigPatternsCosmosTest.cs
@@ -188,8 +188,7 @@ public class CosmosFixture : ServiceProviderFixtureBase
{
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(w =>
- w.Ignore(CosmosEventId.NoPartitionKeyDefined)
- .Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
+ w.Ignore(CosmosEventId.NoPartitionKeyDefined));
protected override ITestStoreFactory TestStoreFactory
=> CosmosTestStoreFactory.Instance;
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index b6adaebde2b..8f875f00922 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -9,6 +9,10 @@ public class CosmosConcurrencyTest(CosmosConcurrencyTest.CosmosFixture fixture)
{
private const string DatabaseName = "CosmosConcurrencyTest";
+ protected IServiceProvider ServiceProvider { get; } = new ServiceCollection()
+ .AddEntityFrameworkCosmos()
+ .BuildServiceProvider();
+
protected CosmosFixture Fixture { get; } = fixture;
[ConditionalFact]
@@ -63,8 +67,9 @@ public async Task Etag_is_updated_in_entity_after_SaveChanges(bool? contentRespo
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
#pragma warning restore CS0618 // Type or member is obsolete
}
- })
- .ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)))).Options;
+ })))
+ .UseInternalServiceProvider(ServiceProvider)
+ .Options;
var customer = new Customer
{
@@ -125,8 +130,9 @@ public async Task Etag_is_updated_in_derived_entity_after_SaveChanges(bool? cont
o.ContentResponseOnWriteEnabled(contentResponseOnWriteEnabled.Value);
#pragma warning restore CS0618 // Type or member is obsolete
}
- })
- .ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)))).Options;
+ })))
+ .UseInternalServiceProvider(ServiceProvider)
+ .Options;
var customer = new PremiumCustomer
{
From cd57ea4798ae8c538fe816cfb020c534aff86ec6 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 17:04:10 +0200
Subject: [PATCH 20/29] Dispose SP
---
.../EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
index 8f875f00922..f69473226bf 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosConcurrencyTest.cs
@@ -5,11 +5,11 @@ 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 IServiceProvider ServiceProvider { get; } = new ServiceCollection()
+ protected ServiceProvider ServiceProvider { get; } = new ServiceCollection()
.AddEntityFrameworkCosmos()
.BuildServiceProvider();
@@ -248,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
From ab307c8edece0d93284eca8117ad98680affafdf Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 19:20:16 +0200
Subject: [PATCH 21/29] Add CosmosRetryTest
---
.../CosmosRetryStrategyTest.cs | 166 ++++++++++++++++++
1 file changed, 166 insertions(+)
create mode 100644 test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs
new file mode 100644
index 00000000000..71a30536167
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs
@@ -0,0 +1,166 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net;
+using Microsoft.Azure.Cosmos;
+
+namespace Microsoft.EntityFrameworkCore;
+
+#nullable disable
+
+[CosmosCondition(CosmosCondition.IsNotLinuxEmulator)]
+public class CosmosRetryTest(CosmosRetryTest.CosmosRetryFixture fixture)
+ : IClassFixture, IAsyncLifetime
+{
+ private const string DatabaseName = nameof(CosmosRetryTest);
+
+ protected CosmosRetryFixture Fixture { get; } = fixture;
+
+ [ConditionalTheory, InlineData(false), InlineData(true)]
+ public async Task Retry_for_create_stores_document(bool transactionalBatch)
+ {
+ var customer = new Customer { Id = 42, Name = "Theon" };
+ using (var context = CreateContext(transactionalBatch))
+ {
+ context.Add(customer);
+
+ try
+ {
+ Fixture.RequestHandler.Reset();
+ Fixture.RequestHandler.ShouldFailNextRequest = true;
+ await context.SaveChangesAsync();
+ }
+ catch (DbUpdateException ex) when (ex.InnerException is CosmosException { StatusCode: HttpStatusCode.Conflict })
+ {
+ // Ignored, because the request is actually executed and the error is only a mock,
+ // the document was already created and we get a conflict
+ }
+
+ Assert.Equal(2, Fixture.RequestHandler.RequestCount);
+ }
+
+ using (var context = CreateContext(transactionalBatch))
+ {
+ var customerFromStore = await context.Set().SingleAsync();
+ Assert.Equal(42, customerFromStore.Id);
+ Assert.Equal("Theon", customerFromStore.Name);
+ }
+ }
+
+ [ConditionalTheory, InlineData(false), InlineData(true)]
+ public async Task Retry_for_update_stores_document(bool transactionalBatch)
+ {
+ var customer = new Customer { Id = 42, Name = "Theon" };
+
+ using (var context = CreateContext(transactionalBatch))
+ {
+ context.Add(customer);
+ await context.SaveChangesAsync();
+ }
+
+ using (var context = CreateContext(transactionalBatch))
+ {
+ var customerFromStore = await context.Set().SingleAsync();
+ customerFromStore.Name = "Theon Greyjoy";
+
+ Fixture.RequestHandler.Reset();
+ Fixture.RequestHandler.ShouldFailNextRequest = true;
+ await context.SaveChangesAsync();
+ Assert.Equal(2, Fixture.RequestHandler.RequestCount);
+ }
+
+ using (var context = CreateContext(transactionalBatch))
+ {
+ var customerFromStore = await context.Set().SingleAsync();
+ Assert.Equal("Theon Greyjoy", customerFromStore.Name);
+ }
+ }
+
+ public class Customer
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ private RetryStrategyContext CreateContext(bool transactionalBatch)
+ {
+ var context = Fixture.CreateContext();
+ if (transactionalBatch)
+ {
+ context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
+ }
+ return context;
+ }
+
+ public async Task InitializeAsync()
+ {
+ Fixture.RequestHandler.Reset();
+ using var context = Fixture.CreateContext();
+ context.RemoveRange(await context.Customers.ToListAsync());
+ await context.SaveChangesAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ }
+
+ public class CosmosRetryFixture : SharedStoreFixtureBase
+ {
+ protected override string StoreName
+ => DatabaseName;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => CosmosTestStoreFactory.Instance;
+
+ public TooManyRequestsHandler RequestHandler { get; } = new TooManyRequestsHandler();
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder)
+ .UseCosmos(b => b.HttpClientFactory(() => new HttpClient(RequestHandler)));
+ }
+
+ public class RetryStrategyContext(DbContextOptions options) : PoolableDbContext(options)
+ {
+ public DbSet Customers { get; set; } = null!;
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ => builder.Entity(
+ b => b.HasPartitionKey(c => c.Id));
+ }
+
+ public class TooManyRequestsHandler : DelegatingHandler
+ {
+ public TooManyRequestsHandler()
+ : base(new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ })
+ {
+ }
+
+ public int RequestCount { get; private set; }
+
+ public bool ShouldFailNextRequest { get; set; }
+
+ public void Reset()
+ {
+ ShouldFailNextRequest = false;
+ RequestCount = 0;
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ RequestCount++;
+ var response = await base.SendAsync(request, cancellationToken);
+ if (ShouldFailNextRequest)
+ {
+ ShouldFailNextRequest = false;
+
+ response = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
+ response.Headers.Add("x-ms-retry-after-ms", "1");
+ }
+ return response;
+
+ }
+ }
+}
From d74bd6407e44aef7d614d99045fd6f17186842ec Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 19:27:09 +0200
Subject: [PATCH 22/29] Clean
---
test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
index 2eb32d9017d..ed2ca601c95 100644
--- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs
@@ -240,7 +240,6 @@ await context.AddAsync(
var existingFirstAddressEntry = context.Entry(people[2].Addresses.First());
Assert.Equal("First", people[2].Addresses.First().Street);
- people[2].Addresses.First();
existingAddress1Person3 = people[2].Addresses.First();
existingAddress2Person3 = people[2].Addresses.Last();
From a5f0443010d7bc4c092ba3cc68ce6da23fd26810 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 19:27:13 +0200
Subject: [PATCH 23/29] Add using
---
.../Storage/Internal/CosmosDatabaseWrapper.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
index 5ad66bcbfd6..e08d9bba257 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
@@ -467,19 +467,23 @@ private async Task SaveAsync(CosmosUpdateEntry updateEntry, CancellationTo
try
{
var id = updateEntry.DocumentSource.GetId(updateEntry.Entry.SharedIdentityEntry ?? updateEntry.Entry);
+ using var stream = updateEntry.Operation != CosmosCudOperation.Delete
+ ? updateEntry.DocumentSource.Serialize(updateEntry.Entry)
+ : null;
+
return updateEntry.Operation switch
{
CosmosCudOperation.Create => await _cosmosClient.CreateItemAsync(
updateEntry.CollectionId,
id,
- updateEntry.DocumentSource.Serialize(updateEntry.Entry),
+ stream!,
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
CosmosCudOperation.Update => await _cosmosClient.ReplaceItemAsync(
updateEntry.CollectionId,
id,
- updateEntry.DocumentSource.Serialize(updateEntry.Entry),
+ stream!,
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
From 18028cb5f162d052a82f96816fb85be795b2874a Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 2 Apr 2026 19:53:25 +0200
Subject: [PATCH 24/29] Clean
---
.../Storage/Internal/SingletonCosmosClientWrapper.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/SingletonCosmosClientWrapper.cs
index ac5c298ec9c..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;
From c2aea817e9d22b2adec37c6f4b44f583e2bce519 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Sat, 4 Apr 2026 11:08:48 +0200
Subject: [PATCH 25/29] Use ROM and new streams
---
.../Storage/Internal/CosmosClientWrapper.cs | 25 ++-
.../Storage/Internal/CosmosDatabaseWrapper.cs | 21 ++-
.../Internal/CosmosTransactionalBatchEntry.cs | 1 -
.../CosmosTransactionalBatchWrapper.cs | 23 ++-
.../Storage/Internal/ICosmosClientWrapper.cs | 4 +-
.../ICosmosTransactionalBatchWrapper.cs | 4 +-
.../Update/Internal/DocumentSource.cs | 5 +-
.../CosmosRetryStrategyTest.cs | 166 ------------------
8 files changed, 54 insertions(+), 195 deletions(-)
delete mode 100644 test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
index 8d099a8606a..31706f1bf2b 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
@@ -5,6 +5,7 @@
using System.Collections.ObjectModel;
using System.Net;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using Microsoft.Azure.Cosmos.Scripts;
using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
@@ -313,7 +314,7 @@ private static string GetPathFromRoot(IReadOnlyEntityType entityType)
public virtual Task CreateItemAsync(
string containerId,
string documentId,
- Stream document,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default)
@@ -321,7 +322,7 @@ public virtual Task CreateItemAsync(
private static async Task CreateItemOnceAsync(
DbContext _,
- (string ContainerId, string DocumentId, Stream Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
+ (string ContainerId, string DocumentId, ReadOnlyMemory Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
CancellationToken cancellationToken = default)
{
var containerId = parameters.ContainerId;
@@ -347,8 +348,14 @@ 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(
- parameters.Document,
+ stream,
partitionKeyValue,
itemRequestOptions,
cancellationToken)
@@ -376,7 +383,7 @@ private static async Task CreateItemOnceAsync(
public virtual Task ReplaceItemAsync(
string collectionId,
string documentId,
- Stream document,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default)
@@ -385,7 +392,7 @@ public virtual Task ReplaceItemAsync(
private static async Task ReplaceItemOnceAsync(
DbContext _,
- (string ContainerId, string ResourceId, Stream Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
+ (string ContainerId, string ResourceId, ReadOnlyMemory Document, IUpdateEntry Entry, ISessionTokenStorage SessionTokenStorage, CosmosClientWrapper Wrapper) parameters,
CancellationToken cancellationToken = default)
{
var containerId = parameters.ContainerId;
@@ -410,8 +417,14 @@ 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(
- parameters.Document,
+ stream,
parameters.ResourceId,
partitionKeyValue,
itemRequestOptions,
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
index e08d9bba257..4e7e95771f9 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
@@ -425,15 +425,14 @@ private IEnumerable CreateTransactions((Groupi
foreach (var updateEntry in batch.UpdateEntries)
{
- // Stream is disposed by Transaction.ExecuteAsync
- var stream = updateEntry.Operation != CosmosCudOperation.Delete ? updateEntry.DocumentSource.Serialize(updateEntry.Entry) : 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;
}
@@ -450,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(),
};
@@ -467,23 +466,23 @@ private async Task SaveAsync(CosmosUpdateEntry updateEntry, CancellationTo
try
{
var id = updateEntry.DocumentSource.GetId(updateEntry.Entry.SharedIdentityEntry ?? updateEntry.Entry);
- using var stream = updateEntry.Operation != CosmosCudOperation.Delete
+ var document = updateEntry.Operation != CosmosCudOperation.Delete
? updateEntry.DocumentSource.Serialize(updateEntry.Entry)
- : null;
+ : default;
return updateEntry.Operation switch
{
CosmosCudOperation.Create => await _cosmosClient.CreateItemAsync(
updateEntry.CollectionId,
id,
- stream!,
+ document,
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
CosmosCudOperation.Update => await _cosmosClient.ReplaceItemAsync(
updateEntry.CollectionId,
id,
- stream!,
+ document,
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
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 e56fc7ece9c..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;
@@ -72,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, out var itemRequestOptionsLength);
if (_checkSize)
{
- var size = stream.Length + itemRequestOptionsLength + OperationSerializationOverheadOverEstimateInBytes;
+ var size = document.Length + itemRequestOptionsLength + OperationSerializationOverheadOverEstimateInBytes;
if (_size + size > MaxSize && _size != 0)
{
@@ -87,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));
@@ -99,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, 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)
{
@@ -114,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));
diff --git a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
index 30f8b229734..a2bef3f6a36 100644
--- a/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/ICosmosClientWrapper.cs
@@ -46,7 +46,7 @@ public interface ICosmosClientWrapper
Task CreateItemAsync(
string containerId,
string documentId,
- Stream document,
+ ReadOnlyMemory document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default);
@@ -60,7 +60,7 @@ Task CreateItemAsync(
Task ReplaceItemAsync(
string collectionId,
string documentId,
- Stream 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/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
index bb336243b84..0172a1f1c32 100644
--- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
+++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs
@@ -65,7 +65,7 @@ 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 Stream Serialize(IUpdateEntry entry)
+ public virtual ReadOnlyMemory Serialize(IUpdateEntry entry)
{
var internalEntry = (IInternalEntry)entry;
var stream = new MemoryStream();
@@ -74,8 +74,7 @@ public virtual Stream Serialize(IUpdateEntry entry)
WriteJsonObject(writer, internalEntry, internalEntry.StructuralType, null);
}
- stream.Position = 0;
- return stream;
+ return new ReadOnlyMemory(stream.GetBuffer(), 0, (int)stream.Length);
}
private void WriteJsonObject(
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs
deleted file mode 100644
index 71a30536167..00000000000
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosRetryStrategyTest.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Net;
-using Microsoft.Azure.Cosmos;
-
-namespace Microsoft.EntityFrameworkCore;
-
-#nullable disable
-
-[CosmosCondition(CosmosCondition.IsNotLinuxEmulator)]
-public class CosmosRetryTest(CosmosRetryTest.CosmosRetryFixture fixture)
- : IClassFixture, IAsyncLifetime
-{
- private const string DatabaseName = nameof(CosmosRetryTest);
-
- protected CosmosRetryFixture Fixture { get; } = fixture;
-
- [ConditionalTheory, InlineData(false), InlineData(true)]
- public async Task Retry_for_create_stores_document(bool transactionalBatch)
- {
- var customer = new Customer { Id = 42, Name = "Theon" };
- using (var context = CreateContext(transactionalBatch))
- {
- context.Add(customer);
-
- try
- {
- Fixture.RequestHandler.Reset();
- Fixture.RequestHandler.ShouldFailNextRequest = true;
- await context.SaveChangesAsync();
- }
- catch (DbUpdateException ex) when (ex.InnerException is CosmosException { StatusCode: HttpStatusCode.Conflict })
- {
- // Ignored, because the request is actually executed and the error is only a mock,
- // the document was already created and we get a conflict
- }
-
- Assert.Equal(2, Fixture.RequestHandler.RequestCount);
- }
-
- using (var context = CreateContext(transactionalBatch))
- {
- var customerFromStore = await context.Set().SingleAsync();
- Assert.Equal(42, customerFromStore.Id);
- Assert.Equal("Theon", customerFromStore.Name);
- }
- }
-
- [ConditionalTheory, InlineData(false), InlineData(true)]
- public async Task Retry_for_update_stores_document(bool transactionalBatch)
- {
- var customer = new Customer { Id = 42, Name = "Theon" };
-
- using (var context = CreateContext(transactionalBatch))
- {
- context.Add(customer);
- await context.SaveChangesAsync();
- }
-
- using (var context = CreateContext(transactionalBatch))
- {
- var customerFromStore = await context.Set().SingleAsync();
- customerFromStore.Name = "Theon Greyjoy";
-
- Fixture.RequestHandler.Reset();
- Fixture.RequestHandler.ShouldFailNextRequest = true;
- await context.SaveChangesAsync();
- Assert.Equal(2, Fixture.RequestHandler.RequestCount);
- }
-
- using (var context = CreateContext(transactionalBatch))
- {
- var customerFromStore = await context.Set().SingleAsync();
- Assert.Equal("Theon Greyjoy", customerFromStore.Name);
- }
- }
-
- public class Customer
- {
- public int Id { get; set; }
- public string Name { get; set; }
- }
-
- private RetryStrategyContext CreateContext(bool transactionalBatch)
- {
- var context = Fixture.CreateContext();
- if (transactionalBatch)
- {
- context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;
- }
- return context;
- }
-
- public async Task InitializeAsync()
- {
- Fixture.RequestHandler.Reset();
- using var context = Fixture.CreateContext();
- context.RemoveRange(await context.Customers.ToListAsync());
- await context.SaveChangesAsync();
- }
-
- public async Task DisposeAsync()
- {
- }
-
- public class CosmosRetryFixture : SharedStoreFixtureBase
- {
- protected override string StoreName
- => DatabaseName;
-
- protected override ITestStoreFactory TestStoreFactory
- => CosmosTestStoreFactory.Instance;
-
- public TooManyRequestsHandler RequestHandler { get; } = new TooManyRequestsHandler();
-
- public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
- => base.AddOptions(builder)
- .UseCosmos(b => b.HttpClientFactory(() => new HttpClient(RequestHandler)));
- }
-
- public class RetryStrategyContext(DbContextOptions options) : PoolableDbContext(options)
- {
- public DbSet Customers { get; set; } = null!;
-
- protected override void OnModelCreating(ModelBuilder builder)
- => builder.Entity(
- b => b.HasPartitionKey(c => c.Id));
- }
-
- public class TooManyRequestsHandler : DelegatingHandler
- {
- public TooManyRequestsHandler()
- : base(new HttpClientHandler
- {
- ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
- })
- {
- }
-
- public int RequestCount { get; private set; }
-
- public bool ShouldFailNextRequest { get; set; }
-
- public void Reset()
- {
- ShouldFailNextRequest = false;
- RequestCount = 0;
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- RequestCount++;
- var response = await base.SendAsync(request, cancellationToken);
- if (ShouldFailNextRequest)
- {
- ShouldFailNextRequest = false;
-
- response = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
- response.Headers.Add("x-ms-retry-after-ms", "1");
- }
- return response;
-
- }
- }
-}
From 7c4ed82a58bd670a8fb286041c68927d039641c4 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Sat, 4 Apr 2026 11:26:28 +0200
Subject: [PATCH 26/29] Small exception message change
---
.../Storage/Internal/CosmosTypeMappingSource.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index edf70fafa81..eff864b06c6 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -235,7 +235,7 @@ public sealed class CosmosJsonStringKeyedDictionaryReaderWriter(JsonVa
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
@@ -301,7 +301,7 @@ public sealed class CosmosJsonStringKeyedDictionaryNullableValueReaderWriter> 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
@@ -367,7 +367,7 @@ public sealed class CosmosJsonStringKeyedDictionaryCollectionValueReaderWriter> 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
@@ -434,7 +434,7 @@ public sealed class CosmosJsonStringKeyedDictionaryReferenceCollectionValueReade
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
From b7b79f5bf110ecd0d85ca63893a3a716963de452 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Sat, 4 Apr 2026 12:00:38 +0200
Subject: [PATCH 27/29] Clean
---
.../Storage/Internal/CosmosDatabaseWrapper.cs | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
index 4e7e95771f9..5bb121ef0af 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs
@@ -466,23 +466,19 @@ private async Task SaveAsync(CosmosUpdateEntry updateEntry, CancellationTo
try
{
var id = updateEntry.DocumentSource.GetId(updateEntry.Entry.SharedIdentityEntry ?? updateEntry.Entry);
- var document = updateEntry.Operation != CosmosCudOperation.Delete
- ? updateEntry.DocumentSource.Serialize(updateEntry.Entry)
- : default;
-
return updateEntry.Operation switch
{
CosmosCudOperation.Create => await _cosmosClient.CreateItemAsync(
updateEntry.CollectionId,
id,
- document,
+ updateEntry.DocumentSource.Serialize(updateEntry.Entry),
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
CosmosCudOperation.Update => await _cosmosClient.ReplaceItemAsync(
updateEntry.CollectionId,
id,
- document,
+ updateEntry.DocumentSource.Serialize(updateEntry.Entry),
updateEntry.Entry,
SessionTokenStorage,
cancellationToken).ConfigureAwait(false),
From a061ffd842fb121bf48b108a2d7e54d99de239f7 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:06:47 +0200
Subject: [PATCH 28/29] Move TypeMapping spec to FindMapping
---
.../CosmosPropertyBuilderExtensions.cs | 3 ---
.../Storage/Internal/ByteArrayConverter.cs | 2 +-
.../Storage/Internal/CosmosTypeMappingSource.cs | 16 ++++++++++++++++
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
index 5a50e25d5ee..ba3c2b815ee 100644
--- a/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/CosmosPropertyBuilderExtensions.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
-using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore;
@@ -123,7 +122,6 @@ public static PropertyBuilder IsVectorProperty(
{
propertyBuilder.Metadata.SetVectorDistanceFunction(ValidateVectorDistanceFunction(distanceFunction));
propertyBuilder.Metadata.SetVectorDimensions(dimensions);
- propertyBuilder.Metadata.SetTypeMapping(CosmosVectorTypeMapping.Create(propertyBuilder.Metadata.ClrType, new CosmosVectorType(distanceFunction, dimensions)));
return propertyBuilder;
}
@@ -178,7 +176,6 @@ public static PropertyBuilder IsVectorProperty(
propertyBuilder.Metadata.SetVectorDistanceFunction(ValidateVectorDistanceFunction(distanceFunction), fromDataAnnotation);
propertyBuilder.Metadata.SetVectorDimensions(dimensions, fromDataAnnotation);
- propertyBuilder.Metadata.SetTypeMapping(CosmosVectorTypeMapping.Create(propertyBuilder.Metadata.ClrType, new CosmosVectorType(distanceFunction, dimensions)));
return propertyBuilder;
}
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/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index eff864b06c6..0a21af180f5 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -5,6 +5,7 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.ChangeTracking.Internal;
+using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.EntityFrameworkCore.Storage.Json;
using Newtonsoft.Json.Linq;
@@ -40,6 +41,21 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
}
}.ToFrozenDictionary();
+ ///
+ /// 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 CoreTypeMapping? FindMapping(IProperty property)
+ // 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.
+ => property.GetVectorDistanceFunction() is { } distanceFunction
+ && property.GetVectorDimensions() is { } dimensions
+ ? CosmosVectorTypeMapping.Create(property.ClrType, new CosmosVectorType(distanceFunction, dimensions))
+ : base.FindMapping(property);
+
///
/// 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
From 4d63968eb7b7697fd077966a85837f65e98b3028 Mon Sep 17 00:00:00 2001
From: JoasE <32096708+JoasE@users.noreply.github.com>
Date: Thu, 9 Apr 2026 09:47:24 +0200
Subject: [PATCH 29/29] Use TryFindJsonCollectionMapping
---
.../Internal/CosmosTypeMappingSource.cs | 37 ++++++++-
.../Internal/CosmosVectorTypeMapping.cs | 77 +++++++++----------
2 files changed, 72 insertions(+), 42 deletions(-)
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
index 0a21af180f5..5cd4eb78f47 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosTypeMappingSource.cs
@@ -53,9 +53,39 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
// directly.
=> property.GetVectorDistanceFunction() is { } distanceFunction
&& property.GetVectorDimensions() is { } dimensions
- ? CosmosVectorTypeMapping.Create(property.ClrType, new CosmosVectorType(distanceFunction, dimensions))
+ ? 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
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -83,8 +113,9 @@ public CosmosTypeMappingSource(TypeMappingSourceDependencies dependencies)
{
var elementMappingInfo = new TypeMappingInfo(memoryType);
CoreTypeMapping? typeMapping = null;
- TryFindJsonCollectionMapping(elementMappingInfo, memoryType.MakeArrayType(), null, ref typeMapping, out var _, out var readerWriter);
- return new CosmosTypeMapping(clrType, jsonValueReaderWriter: readerWriter)
+ 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))!);
diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
index 7c004f31ef4..031f73c3d49 100644
--- a/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
+++ b/src/EFCore.Cosmos/Storage/Internal/CosmosVectorTypeMapping.cs
@@ -25,7 +25,7 @@ public class CosmosVectorTypeMapping : CosmosTypeMapping
// Note that this default is not valid because dimensions cannot be zero. But since there is no reasonable
// default dimensions size for a vector type, this is intentionally not valid rather than just being wrong.
// The fundamental problem here is that type mappings are "required" to have some default now.
- = Create(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0));
+ = new(typeof(byte[]), new CosmosVectorType(DistanceFunction.Cosine, 0));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -33,44 +33,43 @@ public class CosmosVectorTypeMapping : CosmosTypeMapping
/// 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 CosmosVectorTypeMapping Create(Type clrType, CosmosVectorType vectorType)
+ public CosmosVectorTypeMapping(
+ Type clrType,
+ CosmosVectorType vectorType,
+ ValueComparer? comparer = null,
+ ValueComparer? keyComparer = null,
+ CoreTypeMapping? elementMapping = null,
+ JsonValueReaderWriter? jsonValueReaderWriter = null)
+ : this(
+ new CoreTypeMappingParameters(
+ clrType,
+ converter: null,
+ comparer,
+ keyComparer,
+ elementMapping: elementMapping,
+ jsonValueReaderWriter: jsonValueReaderWriter),
+ vectorType)
{
- var collectionType = clrType;
- var isRom = clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>);
- if (isRom)
- {
- collectionType = clrType.GetGenericArguments()[0].MakeArrayType();
- }
-
- var elementType = collectionType.GetElementType()!;
-
- JsonValueReaderWriter? jsonValueReaderWriter = collectionType switch
- {
- Type t when t == typeof(byte[]) => new JsonCollectionOfStructsReaderWriter(JsonByteReaderWriter.Instance),
- Type t when t == typeof(sbyte[]) => new JsonCollectionOfStructsReaderWriter(JsonSByteReaderWriter.Instance),
- Type t when t == typeof(float[]) => new JsonCollectionOfStructsReaderWriter(JsonFloatReaderWriter.Instance),
- _ => null
- };
-
- var parameters = new CoreTypeMappingParameters(
- clrType,
- null,
- null,
- null,
- null,
- jsonValueReaderWriter: jsonValueReaderWriter);
-
- if (isRom)
- {
- parameters = parameters.WithComposedConverter(
- (ValueConverter)Activator.CreateInstance(typeof(ReadOnlyMemoryConverter<>).MakeGenericType(elementType))!,
- (ValueComparer)Activator.CreateInstance(typeof(ReadOnlyMemoryComparer<>).MakeGenericType(elementType))!,
- null,
- null,
- null);
- }
+ }
- return new CosmosVectorTypeMapping(parameters, vectorType);
+ ///
+ /// 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 CosmosVectorTypeMapping(CosmosTypeMapping mapping, CosmosVectorType vectorType)
+ : this(
+ new CoreTypeMappingParameters(
+ mapping.ClrType,
+ // This is a hack to allow both arrays and ROM types without different function overloads or type mappings.
+ converter: mapping.Converter?.GetType() == typeof(BytesToStringConverter) ? null : mapping.Converter,
+ mapping.Comparer,
+ mapping.KeyComparer,
+ elementMapping: mapping.ElementTypeMapping,
+ jsonValueReaderWriter: mapping.JsonValueReaderWriter),
+ vectorType)
+ {
}
///
@@ -97,13 +96,13 @@ protected CosmosVectorTypeMapping(CoreTypeMappingParameters parameters, CosmosVe
/// 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 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);