Skip to content

Lack of Auto-Increment Support in Cosmos DB with EF Core #1105

Open

Description

@DenisBalan

// might want to mark this issue as bug, to be fixed in the long run

Issue
Cosmos DB does not support auto-incrementing fields like SQL's IDENTITY. When using Entity Framework Core with Cosmos DB, there is no built-in way to generate a unique, sequential global number for each entity. This makes it difficult to assign a reliable GlobalSequenceNumber across all documents.

For ex for PostgreSQL schema looks like GlobalSequenceNumber bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,

Workaround
A custom EF Core interceptor is used to set the GlobalSequenceNumber for new entities. The current workaround uses UTC ticks as a unique value, but this is not a true auto-increment and can lead to collisions under high load. The recommended robust solution is to implement a counter document with optimistic concurrency, similar to other event stores to guarantee sequential numbers in Cosmos DB.

Also not sure why for efcore GlobalSequenceNumber property is init-Only

Image

Ugly but working fix (with reflection for init prop)

services.AddDbContext<MyOwnDbContext>((provider, options) =>
    options.UseCosmos(
            "...",
            databaseName: "mydb-dev",
    optionsx =>
    {
        optionsx.ConnectionMode(ConnectionMode.Gateway);
    })
    .AddInterceptors(provider.GetRequiredService<EntityModificationInterceptor>())
);
public class EntityModificationInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        var context = eventData.Context;
        if (context == null) return result;

        var data = context.ChangeTracker.Entries().ToList();

        foreach (var entry in data)
        {
            if (entry.CurrentValues.Properties.Any(p => p.Name == nameof(EventEntity.GlobalSequenceNumber)))
            {

                var currentValue = entry.CurrentValues[nameof(EventEntity.GlobalSequenceNumber)];
                var tsValue = DateTime.UtcNow.Ticks;

                long globalSequence = currentValue is long l ? l : 0;

                if (globalSequence == 0)
                {
                    if (entry.Entity is EventEntity entity)
                    {
                        var prop = entry.Entity.GetType().GetProperty(nameof(EventEntity.GlobalSequenceNumber));
                        if (prop != null)
                        {
                            prop.SetValue(entry.Entity, tsValue);
                        }

                    }
                    entry.Property(nameof(EventEntity.GlobalSequenceNumber)).CurrentValue = tsValue;
                    var typeName = entry.Entity.GetType().Name;
                    entry.Property("__id").CurrentValue = $"{typeName}|{tsValue}";
                }
            }
        }

        return result;
    }

    public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        SavingChanges(eventData, result);
        return await base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}

Activity

  1. github-actions commented on Sep 3, 2025

    @github-actions

    Hello there!

    We hope you are doing well. We noticed that this issue has not seen any activity in the past 90 days.
    We consider this issue to be stale and will be closing it within the next seven days.

    If you still require assistance with this issue, please feel free to reopen it or create a new issue.

    Thank you for your understanding and cooperation.

    Best regards,
    EventFlow

  2. github-actions commented on Sep 10, 2025

    @github-actions

    Hello there!

    This issue has been closed due to inactivity for seven days. If you believe this issue still
    needs attention, please feel free to open a new issue or comment on this one to request its
    reopening.

    Thank you for your contribution to this repository.

    Best regards,
    EventFlow

  3. reopened this on Sep 10, 2025
  4. rasmus commented on Oct 26, 2025

    @rasmus
    Member

    .analyze

  5. github-actions commented on Oct 26, 2025

    @github-actions

    Overview

    • EF integration assumes the backing database can assign an ever-increasing GlobalSequenceNumber when committing events, but Cosmos DB lacks any identity/auto-increment primitive. As a result, every inserted event keeps the default 0 key, so the second write fails and LoadAllCommittedEvents cannot advance.

    Reproduction

    • Configure a DbContext with options.UseCosmos(...) and call modelBuilder.AddEventFlowEvents() (Cosmos container is created with GlobalSequenceNumber as the primary key).
    • Bootstrap the EventFlow EF event store and emit two events for the same aggregate.
    • The second SaveChanges() throws because the new event still carries GlobalSequenceNumber = 0, producing a duplicate key for the document id.

    Root Cause

    • CommitEventsAsync materializes EventEntity instances without assigning any global position and relies on the database to generate it (Source/EventFlow.EntityFramework/EventStores/EntityFrameworkEventPersistence.cs:62-71,83).
    • The model simply marks GlobalSequenceNumber as the key, with no generation strategy (Source/EventFlow.EntityFramework/Extensions/ModelBuilderExtensions.cs:33-41); relational providers implicitly turn this into an identity column, but Cosmos does not.
    • Because the property is init-only (Source/EventFlow.EntityFramework/EventStores/EventEntity.cs:28-36), downstream code cannot easily substitute its own generator without resorting to reflection, as shown in the issue.

    Assessment

    • Today’s EF implementation effectively supports only stores that can guarantee a monotonic, gapless, cluster-wide numeric key (SQL Server, PostgreSQL, SQLite, etc.). Cosmos DB violates that assumption, so the provider currently cannot deliver the guarantees needed by LoadAllCommittedEvents (global ordering, resumable GlobalPosition tokens).
    • Treating this as a bug makes sense: the package advertises EF Core support, yet silently depends on relational behavior.

    Options for a Fix

    1. Introduce an abstraction for global-sequence generation in the EF layer (e.g., IGlobalSequenceNumberGenerator), default it to identity columns, and supply a Cosmos implementation that stores the counter in a separate document and increments it using optimistic concurrency or TransactionalBatch.
    2. Provide a Cosmos-specific event persistence (parallel to the SQL implementations) that bypasses EF identity expectations and uses a counter container plus leasing to generate global numbers.
    3. At minimum, loosen EventEntity.GlobalSequenceNumber to get; set; and document an officially supported interception hook, so users can inject a safe generator without breaking immutability via reflection.

    Workarounds

    • The UTC-ticks interceptor from the issue unblocks writes, but it sacrifices ordering guarantees under high load, can collide across pods, and still requires reflection to bypass init. It should be documented as “unsafe/temporary only.”

    Next Steps

    • Decide whether the EF package intends to support Cosmos DB. If yes, prioritise designing the counter-based global sequence abstraction and tests that verify sequential numbering under concurrent inserts. If not, clearly document the limitation to avoid future surprises.

    Your friendly neighborhood AI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions