Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2853acc
WIP
JoasE Oct 19, 2025
e374631
Wip: merge session tokens
JoasE Oct 20, 2025
f295ce9
WIP..
JoasE Oct 20, 2025
ad8d930
Make Client responsible
JoasE Oct 20, 2025
3a3a510
add todo
JoasE Oct 20, 2025
691aa79
Cleanup and small improvements
JoasE Oct 20, 2025
c4a9977
Cleanup and small improvements
JoasE Oct 20, 2025
ac5ca96
Add option ManualSessionTokenManagementEnabled
JoasE Oct 21, 2025
6f84317
fix no etag sessiontoken match
JoasE Oct 21, 2025
64ab888
Add some testcases
JoasE Oct 21, 2025
dadc58b
Cleanup
JoasE Oct 24, 2025
57f1f37
Add todo
JoasE Oct 24, 2025
fc8a580
Fix empty tokens
JoasE Oct 24, 2025
364d0f4
Rename var
JoasE Oct 24, 2025
c9890e1
Remove some todos
JoasE Oct 24, 2025
255bbe2
Move to non-parsing session token management
JoasE Oct 24, 2025
87e0dfd
Revert "Add option ManualSessionTokenManagementEnabled"
JoasE Oct 24, 2025
df5809e
Cleanup
JoasE Oct 24, 2025
2c29285
Cleanup
JoasE Oct 24, 2025
77a81a3
Add tests for pr comment changes (pooling and query compilation vs ex…
JoasE Oct 25, 2025
81e137c
Clear session token storage on reset
JoasE Oct 25, 2025
0c62cee
Store session token storage in query context instead of compilation c…
JoasE Oct 25, 2025
5c80637
Add todo
JoasE Oct 25, 2025
1f8d6a9
Move to non-parsing but only composing session tokens
JoasE Oct 26, 2025
e729af3
Readd option ManualSessionTokenManagementEnabled
JoasE Oct 28, 2025
9974e8f
Move container checks to extension method and other small improvement…
JoasE Nov 1, 2025
017ffc7
Cleanup
JoasE Nov 1, 2025
0342b9c
Fix make public api virtual
JoasE Nov 1, 2025
8bd06b4
Cleanup & don't return null values in dictionary
JoasE Nov 1, 2025
34eeeea
WIP
JoasE Nov 6, 2025
e0b9ced
Improve tests and sessiontokenstorage
JoasE Nov 6, 2025
7f4105b
Use cosmos strings
JoasE Nov 6, 2025
20c2f1e
Add tests and fix cases
JoasE Nov 10, 2025
7427a65
Do not use automatic session tokens for manual mode if not provided.
JoasE Nov 10, 2025
8d46fed
Cleanup
JoasE Nov 10, 2025
9f4b366
Merge branch 'main' of https://github.com/JoasE/efcore into feature/#…
JoasE Nov 10, 2025
2ea28cb
Cleanup
JoasE Nov 10, 2025
96937a9
Remove debug only test
JoasE Nov 10, 2025
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
53 changes: 53 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosDatabaseFacadeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Storage;
using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;

// ReSharper disable once CheckNamespace
Expand All @@ -25,6 +26,58 @@ public static class CosmosDatabaseFacadeExtensions
public static CosmosClient GetCosmosClient(this DatabaseFacade databaseFacade)
=> GetService<ISingletonCosmosClientWrapper>(databaseFacade).Client;

/// <summary>
/// Gets the composite session token for the default container for this <see cref="DbContext" />.
/// </summary>
/// <remarks>Use this when using only 1 container in the same <see cref="DbContext"/></remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <returns>The session token dictionary.</returns>
public static string? GetSessionToken(this DatabaseFacade databaseFacade)
=> GetSessionTokenStorage(databaseFacade).GetSessionToken();

/// <summary>
/// Gets a dictionary that contains the composite session token per container for this <see cref="DbContext" />.
/// </summary>
/// <remarks>Use this when using multiple containers in the same <see cref="DbContext"/></remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <returns>The session token dictionary.</returns>
public static IReadOnlyDictionary<string, string?> GetSessionTokens(this DatabaseFacade databaseFacade)
=> GetSessionTokenStorage(databaseFacade);

/// <summary>
/// Appends the composite session token for the default container for this <see cref="DbContext" />.
/// </summary>
/// <remarks>Use this when using only 1 container in the same <see cref="DbContext"/></remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="sessionToken">The session token to append.</param>
public static void AppendSessionToken(this DatabaseFacade databaseFacade, string sessionToken)
=> GetSessionTokenStorage(databaseFacade).AppendSessionToken(sessionToken);

/// <summary>
/// Appends the composite session token per container for this <see cref="DbContext" />.
/// </summary>
/// <remarks>Use this when using multiple containers in the same <see cref="DbContext"/></remarks>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="sessionTokens">The session tokens to append per container.</param>
public static void AppendSessionTokens(this DatabaseFacade databaseFacade, IReadOnlyDictionary<string, string> sessionTokens)
=> GetSessionTokenStorage(databaseFacade).AppendSessionTokens(sessionTokens);

private static SessionTokenStorage GetSessionTokenStorage(DatabaseFacade databaseFacade)
{
var db = GetService<IDatabase>(databaseFacade);
if (db is not CosmosDatabaseWrapper dbWrapper)
{
throw new InvalidOperationException(CosmosStrings.CosmosNotInUse);
}

if (dbWrapper.SessionTokenStorage is not SessionTokenStorage sts)
{
throw new InvalidOperationException(CosmosStrings.EnableManualSessionTokenManagement);
}

return sts;
}

private static TService GetService<TService>(IInfrastructure<IServiceProvider> databaseFacade)
where TService : class
{
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Cosmos/Extensions/CosmosModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public static class CosmosModelExtensions
public static string? GetDefaultContainer(this IReadOnlyModel model)
=> (string?)model[CosmosAnnotationNames.ContainerName];

/// <summary>
/// Returns the all container names used in the model.
/// </summary>
/// <param name="model">The model.</param>
/// <returns>A set of the names of the containers used in the model.</returns>
public static HashSet<string> GetContainerNames(this IReadOnlyModel model)
=> (HashSet<string>)model[CosmosAnnotationNames.ContainerNames]!;

/// <summary>
/// Sets the default container name.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio
.TryAdd<LoggingDefinitions, CosmosLoggingDefinitions>()
.TryAdd<IDatabaseProvider, DatabaseProvider<CosmosOptionsExtension>>()
.TryAdd<IDatabase, CosmosDatabaseWrapper>()
.TryAdd<IResettableService, CosmosDatabaseWrapper>(sp => (CosmosDatabaseWrapper)sp.GetRequiredService<IDatabase>())
.TryAdd<IExecutionStrategyFactory, CosmosExecutionStrategyFactory>()
.TryAdd<IDbContextTransactionManager, CosmosTransactionManager>()
.TryAdd<IModelValidator, CosmosModelValidator>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ public virtual CosmosDbContextOptionsBuilder MaxRequestsPerTcpConnection(int req
public virtual CosmosDbContextOptionsBuilder ContentResponseOnWriteEnabled(bool enabled = true)
=> WithOption(e => e.ContentResponseOnWriteEnabled(Check.NotNull(enabled)));


/// <summary>
/// Sets the boolean to track and manage session tokens for requests made to Cosmos DB
/// and being able to access them via the <see cref="CosmosDatabaseFacadeExtensions.GetSessionTokens(DatabaseFacade)"/> and <see cref="CosmosDatabaseFacadeExtensions.AppendSessionTokens(DatabaseFacade, IReadOnlyDictionary{string, string})"/> methods.
/// This is only relevant when your application needs to manage session tokens manually.
/// For example: If you're using a round-robin load balancer that doesn't maintain session affinity between requests.
/// Enabling manual session token management can break session consistency when not handled properly.
/// See <see href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-manage-consistency?tabs=portal%2Cdotnetv2%2Capi-async#utilize-session-tokens">Utilize session tokens</see> for more details.
/// </summary>
/// <remarks>
/// 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="true" /> to track and manually manage session tokens in EF.</param>
public virtual CosmosDbContextOptionsBuilder ManualSessionTokenManagementEnabled(bool enabled = true)
=> WithOption(e => e.ManualSessionTokenManagementEnabled(enabled));

/// <summary>
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
/// does not modify options that are already in use elsewhere.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class CosmosOptionsExtension : IDbContextOptionsExtension
private bool? _enableContentResponseOnWrite;
private DbContextOptionsExtensionInfo? _info;
private Func<HttpClient>? _httpClientFactory;
private bool _enableManualSessionTokenManagement;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -73,6 +74,7 @@ protected CosmosOptionsExtension(CosmosOptionsExtension copyFrom)
_maxTcpConnectionsPerEndpoint = copyFrom._maxTcpConnectionsPerEndpoint;
_maxRequestsPerTcpConnection = copyFrom._maxRequestsPerTcpConnection;
_httpClientFactory = copyFrom._httpClientFactory;
_enableManualSessionTokenManagement = copyFrom._enableManualSessionTokenManagement;
}

/// <summary>
Expand Down Expand Up @@ -564,6 +566,30 @@ public virtual CosmosOptionsExtension WithHttpClientFactory(Func<HttpClient>? ht
return clone;
}

/// <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 virtual bool EnableManualSessionTokenManagement
=> _enableManualSessionTokenManagement;

/// <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 virtual CosmosOptionsExtension ManualSessionTokenManagementEnabled(bool enabled)
{
var clone = Clone();

clone._enableManualSessionTokenManagement = enabled;

return clone;
}

/// <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
Expand Down Expand Up @@ -632,6 +658,7 @@ public override int GetServiceProviderHashCode()
hashCode.Add(Extension._maxTcpConnectionsPerEndpoint);
hashCode.Add(Extension._maxRequestsPerTcpConnection);
hashCode.Add(Extension._httpClientFactory);
hashCode.Add(Extension._enableManualSessionTokenManagement);

_serviceProviderHash = hashCode.ToHashCode();
}
Expand All @@ -656,7 +683,8 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo
&& Extension._gatewayModeMaxConnectionLimit == otherInfo.Extension._gatewayModeMaxConnectionLimit
&& Extension._maxTcpConnectionsPerEndpoint == otherInfo.Extension._maxTcpConnectionsPerEndpoint
&& Extension._maxRequestsPerTcpConnection == otherInfo.Extension._maxRequestsPerTcpConnection
&& Extension._httpClientFactory == otherInfo.Extension._httpClientFactory;
&& Extension._httpClientFactory == otherInfo.Extension._httpClientFactory
&& Extension._enableManualSessionTokenManagement == otherInfo.Extension._enableManualSessionTokenManagement;

public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ public class CosmosSingletonOptions : ICosmosSingletonOptions
/// </summary>
public virtual Func<HttpClient>? HttpClientFactory { get; private set; }

/// <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 virtual bool EnableManualSessionTokenManagement { get; private set; }

/// <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
Expand Down Expand Up @@ -178,6 +186,7 @@ public virtual void Initialize(IDbContextOptions options)
MaxTcpConnectionsPerEndpoint = cosmosOptions.MaxTcpConnectionsPerEndpoint;
MaxRequestsPerTcpConnection = cosmosOptions.MaxRequestsPerTcpConnection;
HttpClientFactory = cosmosOptions.HttpClientFactory;
EnableManualSessionTokenManagement = cosmosOptions.EnableManualSessionTokenManagement;
}
}

Expand Down Expand Up @@ -208,6 +217,7 @@ public virtual void Validate(IDbContextOptions options)
|| MaxTcpConnectionsPerEndpoint != cosmosOptions.MaxTcpConnectionsPerEndpoint
|| MaxRequestsPerTcpConnection != cosmosOptions.MaxRequestsPerTcpConnection
|| HttpClientFactory != cosmosOptions.HttpClientFactory
|| EnableManualSessionTokenManagement != cosmosOptions.EnableManualSessionTokenManagement
))
{
throw new InvalidOperationException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,12 @@ public interface ICosmosSingletonOptions : ISingletonOptions
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
Func<HttpClient>? HttpClientFactory { get; }

/// <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>
bool EnableManualSessionTokenManagement { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,19 @@ protected override void ProcessModelAnnotations(
{
annotations.Remove(CosmosAnnotationNames.Throughput);
}

// @TODO: Is this the right place for this?
annotations.Add(CosmosAnnotationNames.ContainerNames, GetContainerNames(model));
}

private HashSet<string> GetContainerNames(IModel model)
=> model.GetEntityTypes()
.Where(et => et.FindPrimaryKey() != null)
.Select(et => et.GetContainer())
.Where(container => container != null)
.Distinct()!
.ToHashSet()!;

/// <summary>
/// Updates the entity type annotations that will be set on the read-only object.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ public static class CosmosAnnotationNames
/// </summary>
public const string ContainerName = Prefix + "ContainerName";

/// <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 const string ContainerNames = Prefix + "ContainerNames"; // @TODO: is this the right way?

/// <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
Expand Down
20 changes: 20 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@
<data name="ContainerContainingPropertyConflict" xml:space="preserve">
<value>The entity type '{entityType}' is mapped to the container '{container}' but it is also configured as being contained in property '{property}'.</value>
</data>
<data name="ContainerNameDoesNotExist" xml:space="preserve">
<value>The container with the name '{containerName}' does not exist.</value>
<comment>string</comment>
</data>
<data name="ContainerNotOnRoot" xml:space="preserve">
<value>An Azure Cosmos DB container name is defined on entity type '{entityType}', which inherits from '{baseEntityType}'. Container names must be defined on the root entity type of a hierarchy.</value>
</data>
Expand All @@ -171,6 +175,9 @@
<data name="ElementWithValueConverter" xml:space="preserve">
<value>The property '{propertyType} {structuralType}.{property}' has element type '{elementType}', which requires a value converter. Elements types requiring value converters are not currently supported with the Azure Cosmos DB database provider.</value>
</data>
<data name="EnableManualSessionTokenManagement" xml:space="preserve">
<value>Enable manual session token management using CosmosDbContextOptionsBuilder.ManualSessionTokenManagementEnabled to use this method.</value>
</data>
<data name="ETagNonStringStoreType" xml:space="preserve">
<value>The type of the etag property '{property}' on '{entityType}' is '{propertyType}'. All etag properties must be strings or have a string value converter.</value>
</data>
Expand Down Expand Up @@ -350,6 +357,9 @@
<data name="SaveChangesAutoTransactionBehaviorAlwaysTriggerAtomicity" xml:space="preserve">
<value>When using AutoTransactionBehavior.Always with the Cosmos DB provider, only 1 entity can be saved at a time when using pre- or post- triggers to ensure atomicity.</value>
</data>
<data name="SessionTokenCanNotBeWhiteSpace" xml:space="preserve">
<value>Session token can not be white space.</value>
</data>
<data name="SingleFirstOrDefaultNotSupportedOnNonNullableQueries" xml:space="preserve">
<value>SingleOrDefault and FirstOrDefault cannot be used Cosmos SQL does not allow Offset without Limit. Consider specifying a 'Take' operation on the query.</value>
</data>
Expand Down
Loading
Loading