Skip to content

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

@DenisBalan

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);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions