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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions