Search code examples
.netazureazure-cosmosdb.net-core-3.1azure-cosmosdb-mongoapi

Duplicate key issue in Azure Cosmos DB using MongoDB API in .Net Core 3.1


Preface

I have tried several solutions which appear identical to one another. I have an issue in regards to inserting new documents into my Azure Cosmos DB. The error I am getting is a duplicate key error. What I am trying to accomplish is having Azure Cosmos DB generate the key on its own for me, instead of myself having to explicitly create the key in code.

The main concern I have with generating the key in my .Net logic is the chance of my .Net application creating duplicate keys due to it briefly going down at times which could reset its internal server clock. I do not believe this could happen in Azure, so I am hoping to take advantage of this by having it continue to generate the keys when an insert occurs.

Simple Code Sample

I am using an interface model to store my object in the Azure Database.

interface IExample
{
    ObjectId Id { get; set; }
}

I also have a concrete class designed implementing this interface.

public class Example : IExample
{
    public ObjectId Id { get; set; }
}

Examples with Attempted Data Annotations

I have tried using the following attributes and their combinations in both the interface and class above for the Id field.

interface IExample
{
    [BsonId]
    ObjectId Id { get; set; }
}
public class Example : IExample
{
    [BsonId]
    public ObjectId Id { get; set; }
}

interface IExample
{
    [DatabaseGenerated(DatabaseGeneratedOption.Calculated)]
    ObjectId Id { get; set; }
}
public class Example : IExample
{
    [DatabaseGenerated(DatabaseGeneratedOption.Calculated)]
    public ObjectId Id { get; set; }
}

interface IExample
{
    [BsonId, DatabaseGenerated(DatabaseGeneratedOption.Calculated)]
    ObjectId Id { get; set; }
}
public class Example : IExample
{
    [BsonId, DatabaseGenerated(DatabaseGeneratedOption.Calculated)]
    public ObjectId Id { get; set; }
}

None of these combinations appear to be telling the CosmosDB that Id should be generated by itself when I have my collection object insert the model.

Repository Example

The following is currently what I am working with when I insert a document into my collection in my repository file.

private ICosmosExampleDBContext _dbContext { get; set; }
private readonly IMongoCollection<IExample> _collection;
public ExampleRepository(ICosmosExampleDBContext dbContext, IOptions<ExampleOptions> options)
{
    this._dbContext = dbContext;
    this._collection = this._dbContext.GetCollection<IExample>(options.Value.Collection);
}
public void CreateExample(IExample example)
{
    try
    {
        // A duplicate key error is created if I don't explicitly create the key here.
        // E11000 is encountered without this, even with above data annotations.
        example.Id = MongoDB.Bson.ObjectId.GenerateNewId();
        this._collection.InsertOne(example);
    }
    catch(Exception e)
    {
        throw e;
    }
}

I guess my follow up questions for this over-arching question is:

  • Is it wrong for me to want to have the Cosmos DB generate my key?
  • Should I keep explicitly creating the key instead of wanting CosmosDB to generate it for me?

The error I am getting is E11000 without the explicit Object ID creation.

Edit: Updated code for attempted data annotations, to show implementation of IExample.


Solution

  • After Further Development and Research

    After working on the solution a bit more, and getting my APIs working for POST, PATCH, and GET, I noticed that it is hard working with an ObjectId property. I also found a post stating that it is recommended to use a string over an ObjectId type for the property.

    JSON.NET cast error when serializing Mongo ObjectId 3rd part ZOXEXIVO's answer

    Using a string Id field with the [BsonId, BsonRepresentation(BsonType.ObjectId)] data annotation allows the CosmosDB to self generate the keys on entry. I updated my data models to use the following for their Id property.

    Example Code

    public interface IExample
    {
        public string Id { get; set; }
    }
    public class Example : IExample
    {
        [BsonId, BsonRepresentation(BsonType.ObjectId)]
        public string Id { get; set; }
    }
    

    Repository

    I also updated my repository to use the concrete class, Example, over the interface, IExample. This allowed me to register my data type as a Bson Document in my Startup.cs.

    private ICosmosExampleDBContext _dbContext { get; set; }
    private readonly IMongoCollection<Example> _collection;
    public ExampleRepository(ICosmosExampleDBContext dbContext, IOptions<ExampleOptions> options)
    {
        this._dbContext = dbContext;
        this._collection = this._dbContext.GetCollection<Example>(options.Value.Collection);
    }
    public void CreateExample(Example example)
    {
        try
        {
            // Since Id is now a string, when it is empty, it gets populated for me.
            // example.Id = MongoDB.Bson.ObjectId.GenerateNewId();
            this._collection.InsertOne(example);
        }
        catch(Exception e)
        {
            throw e;
        }
    }
    

    Summary

    Overall, the result of working on resolving issues relating to my methods led me to learn about using a string Id property in my Class. Using a string Id property with the BsonId and BsonRepresentation(BsonType.ObjectId) data annotations allows my CosmosDb to generate unique keys for my solution on posts. For data models in the future, I would recommend this solution to fellow developers, since, it also cleans up returning these data models on GET.