Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
289db02
Cosmos: Modernize JSON serialization - Update pipeline
JoasE Mar 27, 2026
e637ecc
Merge branch 'main' of https://github.com/dotnet/efcore into feature/…
JoasE Mar 27, 2026
e54b34a
Fix tests and add clone method
JoasE Mar 30, 2026
bd27697
Vector fixes
JoasE Mar 30, 2026
e5a4b9a
Merge branch 'main' of https://github.com/dotnet/efcore into feature/…
JoasE Mar 30, 2026
da9163d
fix whitespace
JoasE Mar 30, 2026
d69f953
Fix tests
JoasE Mar 30, 2026
5302506
Clean
JoasE Mar 30, 2026
72d2272
Merge branch 'main' of https://github.com/dotnet/efcore into feature/…
JoasE Mar 30, 2026
b38b19c
Clean
JoasE Mar 30, 2026
ab0a8f5
remove ContentResponseOnWriteEnabled
JoasE Mar 30, 2026
139fa79
Revert "remove ContentResponseOnWriteEnabled"
JoasE Mar 31, 2026
8bb031e
Obsolete EnableContentResponseOnWrite
JoasE Mar 31, 2026
a1ad382
Simplify batch tests
JoasE Mar 31, 2026
1d432d5
Clean
JoasE Mar 31, 2026
057f769
Set type mapping via CosmosPropertyBuilderExtensions
JoasE Mar 31, 2026
a411b55
Ignore ManyServiceProvidersCreatedWarning
JoasE Mar 31, 2026
87ccd09
Clean
JoasE Mar 31, 2026
6133ccb
Move ManyServiceProvidersCreatedWarning to specific test
JoasE Apr 2, 2026
2e3f7c4
Use HandlesNullWrites
JoasE Apr 2, 2026
8bb0110
Add more Ignore(ManyServiceProvidersCreatedWarning) to ConfigPatterns…
JoasE Apr 2, 2026
f0a20b2
Use separate SP
JoasE Apr 2, 2026
cd57ea4
Dispose SP
JoasE Apr 2, 2026
ab307c8
Add CosmosRetryTest
JoasE Apr 2, 2026
d74bd64
Clean
JoasE Apr 2, 2026
a5f0443
Add using
JoasE Apr 2, 2026
18028cb
Clean
JoasE Apr 2, 2026
5e41d53
Merge branch 'main' of https://github.com/dotnet/efcore into feature/…
JoasE Apr 4, 2026
c2aea81
Use ROM and new streams
JoasE Apr 4, 2026
7c4ed82
Small exception message change
JoasE Apr 4, 2026
b7b79f5
Clean
JoasE Apr 4, 2026
a061ffd
Move TypeMapping spec to FindMapping
JoasE Apr 9, 2026
4d63968
Use TryFindJsonCollectionMapping
JoasE Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
Comment thread
AndriySvyryd marked this conversation as resolved.
Outdated

return propertyBuilder;
}
Expand Down Expand Up @@ -176,6 +178,7 @@ public static PropertyBuilder<TProperty> IsVectorProperty<TProperty>(

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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
/// <remarks>
/// The EntityFrameworkCore default is <see langword="false" /> since 11.0.
/// See <see href="https://aka.ms/efcore-docs-dbcontext-options">Using DbContextOptions</see>, and
/// <see href="https://aka.ms/efcore-docs-cosmos">Accessing Azure Cosmos DB with EF Core</see> for more information and examples.
/// </remarks>
/// <param name="enabled"><see langword="false" /> to have null resource</param>
[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));

/// <summary>
/// Sets the <see cref="Cosmos.Infrastructure.SessionTokenManagementMode"/> to use.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public virtual bool? EnableContentResponseOnWrite { get; }
public virtual bool? EnableContentResponseOnWrite { get; private set; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
/// <remarks>
/// Inspired by RelationalJsonUtilities.
/// </remarks>
public static class CosmosSerializationUtilities
public static class CosmosSerializationUtilities // @TODO: Can this be removed? Use document source instead? #34567
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you should be able to remove this

{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
80 changes: 23 additions & 57 deletions src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -47,8 +46,6 @@ public class CosmosClientWrapper : ICosmosClientWrapper
private readonly string _databaseId;
private readonly IExecutionStrategy _executionStrategy;
private readonly IDiagnosticsLogger<DbLoggerCategory.Database.Command> _commandLogger;
private readonly IDiagnosticsLogger<DbLoggerCategory.Database> _databaseLogger;
private readonly bool? _enableContentResponseOnWrite;

static CosmosClientWrapper()
{
Expand All @@ -68,34 +65,14 @@ public CosmosClientWrapper(
ISingletonCosmosClientWrapper singletonWrapper,
IDbContextOptions dbContextOptions,
IExecutionStrategy executionStrategy,
IDiagnosticsLogger<DbLoggerCategory.Database.Command> commandLogger,
IDiagnosticsLogger<DbLoggerCategory.Database> databaseLogger)
IDiagnosticsLogger<DbLoggerCategory.Database.Command> commandLogger)
{
var options = dbContextOptions.FindExtension<CosmosOptionsExtension>();

_singletonWrapper = singletonWrapper;
_databaseId = options!.DatabaseName;
_executionStrategy = executionStrategy;
_commandLogger = commandLogger;
_databaseLogger = databaseLogger;
_enableContentResponseOnWrite = options.EnableContentResponseOnWrite;
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>
Expand Down Expand Up @@ -335,25 +312,25 @@ private static string GetPathFromRoot(IReadOnlyEntityType entityType)
/// </summary>
public virtual Task<bool> 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);

Comment thread
JoasE marked this conversation as resolved.
private static async Task<bool> 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;
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);
Expand All @@ -371,7 +348,7 @@ private static async Task<bool> CreateItemOnceAsync(
}

using var response = await container.CreateItemStreamAsync(
stream,
parameters.Document,
partitionKeyValue,
itemRequestOptions,
cancellationToken)
Expand All @@ -381,7 +358,7 @@ private static async Task<bool> CreateItemOnceAsync(
response.Diagnostics.GetClientElapsedTime(),
response.Headers.RequestCharge,
response.Headers.ActivityId,
parameters.Document["id"]!.ToString(),
documentId,
containerId,
partitionKeyValue);

Expand All @@ -399,7 +376,7 @@ private static async Task<bool> CreateItemOnceAsync(
public virtual Task<bool> ReplaceItemAsync(
string collectionId,
string documentId,
JObject document,
Stream document,
IUpdateEntry updateEntry,
ISessionTokenStorage sessionTokenStorage,
CancellationToken cancellationToken = default)
Expand All @@ -408,17 +385,15 @@ public virtual Task<bool> ReplaceItemAsync(

private static async Task<bool> 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;
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);
Expand All @@ -436,7 +411,7 @@ private static async Task<bool> ReplaceItemOnceAsync(
}

using var response = await container.ReplaceItemStreamAsync(
stream,
parameters.Document,
parameters.ResourceId,
partitionKeyValue,
itemRequestOptions,
Expand Down Expand Up @@ -481,7 +456,7 @@ private static async Task<bool> 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);
Expand Down Expand Up @@ -539,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);
}

/// <summary>
Expand Down Expand Up @@ -578,9 +553,9 @@ private static async Task<CosmosTransactionalBatchResult> 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
{
Expand All @@ -590,7 +565,6 @@ private static ItemRequestOptions CreateItemRequestOptions(IUpdateEntry entry, b
if (helper != null)
{
itemRequestOptions.IfMatchEtag = helper.IfMatchEtag;
itemRequestOptions.EnableContentResponseOnWrite = helper.EnableContentResponseOnWrite;
}

return itemRequestOptions;
Expand Down Expand Up @@ -681,31 +655,23 @@ private static CosmosTransactionalBatchResult ProcessBatchResponse(string contai
var entry = entries[i];
var item = response[i];

ProcessWriteResponse(entry.Entry, (string)item.ETag, (Stream)item.ResourceStream);
ProcessWriteResponse(entry.Entry, item.ETag, item.ResourceStream);
}

return CosmosTransactionalBatchResult.Success;
}

private static void ProcessWriteResponse(IUpdateEntry entry, string eTag, Stream? content)
{
var etagProperty = entry.EntityType.GetETagProperty();
if (etagProperty != null && entry.EntityState != EntityState.Deleted)
if (entry.EntityState == EntityState.Deleted)
{
entry.SetStoreGeneratedValue(etagProperty, eTag);
return;
}

var jObjectProperty = entry.EntityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName);
if (jObjectProperty is { ValueGenerated: ValueGenerated.OnAddOrUpdate }
&& content != null)
var etagProperty = entry.EntityType.GetETagProperty();
if (etagProperty != null)
{
using var responseStream = content;
using var reader = new StreamReader(responseStream);
using var jsonReader = new JsonTextReader(reader);

var createdDocument = Serializer.Deserialize<JObject>(jsonReader);

entry.SetStoreGeneratedValue(jObjectProperty, createdDocument);
entry.SetStoreGeneratedValue(etagProperty, eTag);
}
}

Expand Down
Loading
Loading