EF Core Azure Cosmos DB Provider

This database provider allows Entity Framework Core to be used with Azure Cosmos DB. The provider is maintained as part of the Entity Framework Core Project.

It is strongly recommended to familiarize yourself with the Azure Cosmos DB documentation before reading this section.

Note

This provider only works with Azure Cosmos DB for NoSQL.

Install

Install the Microsoft.EntityFrameworkCore.Cosmos NuGet package.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Get started

Tip

You can view this article's sample on GitHub.

As for other providers the first step is to call UseCosmos:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OrdersDB");

Warning

The endpoint and key are hardcoded here for simplicity, but in a production app these should be stored securely. See Connecting and authenticating for different ways to connect to Azure Cosmos DB.

In this example Order is a simple entity with a reference to the owned type StreetAddress.

public class Order
{
    public int Id { get; set; }
    public int? TrackingNumber { get; set; }
    public string PartitionKey { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}

Saving and querying data follows the normal EF pattern:

using (var context = new OrderContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(
        new Order
        {
            Id = 1, ShippingAddress = new StreetAddress { City = "London", Street = "221 B Baker St" }, PartitionKey = "1"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.FirstAsync();
    Console.WriteLine($"First order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

Important

Calling EnsureCreatedAsync is necessary to create the required containers and insert the seed data if present in the model. However EnsureCreatedAsync should only be called during deployment, not normal operation, as it may cause performance issues.

Connecting and authenticating

The Azure Cosmos DB provider for EF Core has multiple overloads of the UseCosmos method. These overloads support the different ways that a connection can be made to the database, and the different ways of ensuring that the connection is secure.

Important

Make sure to understand Secure access to data in Azure Cosmos DB to understand the security implications and best practices for using each overload of the UseCosmos method.

Connection Mechanism UseCosmos Overload More information
Account endpoint and key UseCosmos<DbContext>(accountEndpoint, accountKey, databaseName) Primary/secondary keys
Account endpoint and token UseCosmos<DbContext>(accountEndpoint, tokenCredential, databaseName) Resource tokens
Connection string UseCosmos<DbContext>(connectionString, databaseName) Work with account keys and connection strings

Queries

LINQ queries

EF Core LINQ queries can be executed against Azure Cosmos DB in the same way as for other database providers. For example:

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

Note

The Azure Cosmos DB provider does not translate the same set of LINQ queries as other providers. See Limitations for more information.

SQL queries

Queries can also be written directly in SQL. For example:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .ToListAsync();

This query results in the following query execution:

SELECT c
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c

Just like for relational FromSql queries, the hand written SQL can be further composed using LINQ operators. For example:

var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
        @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
    .Where(e => e.InsertedOn <= DateTime.UtcNow)
    .Select(e => e.Angle1).Distinct()
    .ToListAsync();

This combination of SQL and LINQ is translated to:

SELECT DISTINCT c["Angle1"]
FROM (
    SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c
WHERE (c["InsertedOn"] <= GetCurrentDateTime())

Azure Cosmos DB options

It is also possible to configure the Azure Cosmos DB provider with a single connection string and to specify other options to customize the connection:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OptionsDB",
        options =>
        {
            options.ConnectionMode(ConnectionMode.Gateway);
            options.WebProxy(new WebProxy());
            options.LimitToEndpoint();
            options.Region(Regions.AustraliaCentral);
            options.GatewayModeMaxConnectionLimit(32);
            options.MaxRequestsPerTcpConnection(8);
            options.MaxTcpConnectionsPerEndpoint(16);
            options.IdleTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.OpenTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.RequestTimeout(TimeSpan.FromMinutes(1));
        });

Tip

The code above shows possible options. It is not intended that these will all be used at the same time! See the Azure Cosmos DB Options documentation for a detailed description of the effect of each option mentioned above.

Cosmos-specific model customization

By default all entity types are mapped to the same container, named after the derived context ("OrderContext" in this case). To change the default container name use HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

To map an entity type to a different container use ToContainer:

modelBuilder.Entity<Order>()
    .ToContainer("Orders");

To identify the entity type that a given item represent EF Core adds a discriminator value even if there are no derived entity types. The name and value of the discriminator can be changed.

If no other entity type will ever be stored in the same container the discriminator can be removed by calling HasNoDiscriminator:

modelBuilder.Entity<Order>()
    .HasNoDiscriminator();

Partition keys

By default, EF Core will create containers with the partition key set to "__partitionKey" without supplying any value for it when inserting items. But to fully leverage the performance capabilities of Azure Cosmos DB, a carefully selected partition key should be used. It can be configured by calling HasPartitionKey:

modelBuilder.Entity<Order>()
    .HasPartitionKey(o => o.PartitionKey);

Note

The partition key property can be of any type as long as it is converted to string.

Once configured the partition key property should always have a non-null value. A query can be made single-partition by adding a WithPartitionKey call.

using (var context = new OrderContext())
{
    context.Add(
        new Order
        {
            Id = 2, ShippingAddress = new StreetAddress { City = "New York", Street = "11 Wall Street" }, PartitionKey = "2"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.WithPartitionKey("2").LastAsync();
    Console.WriteLine($"Last order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

It is generally recommended to add the partition key to the primary key as that best reflects the server semantics and allows some optimizations, for example in FindAsync.

Provisioned throughput

If you use EF Core to create the Azure Cosmos DB database or containers you can configure provisioned throughput for the database by calling CosmosModelBuilderExtensions.HasAutoscaleThroughput or CosmosModelBuilderExtensions.HasManualThroughput. For example:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

To configure provisioned throughput for a container call CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput or CosmosEntityTypeBuilderExtensions.HasManualThroughput. For example:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Time-to-live

Entity types in the Azure Cosmos DB model can now be configured with a default time-to-live. For example:

modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);

Or, for the analytical store:

modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);

Time-to-live for individual entities can be set using a property mapped to "ttl" in the JSON document. For example:

modelBuilder.Entity<Village>()
    .HasDefaultTimeToLive(3600)
    .Property(e => e.TimeToLive)
    .ToJsonProperty("ttl");

Note

A default time-to-live must configured on the entity type for the "ttl" to have any effect. See Time to Live (TTL) in Azure Cosmos DB for more information.

The time-to-live property is then set before the entity is saved. For example:

var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();

The time-to-live property can be a shadow property to avoid polluting the domain entity with database concerns. For example:

modelBuilder.Entity<Hamlet>()
    .HasDefaultTimeToLive(3600)
    .Property<int>("TimeToLive")
    .ToJsonProperty("ttl");

The shadow time-to-live property is then set by accessing the tracked entity. For example:

var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();

Embedded entities

Note

Related entity types are configured as owned by default. To prevent this for a specific entity type call ModelBuilder.Entity.

For Azure Cosmos DB, owned entities are embedded in the same item as the owner. To change a property name use ToJsonProperty:

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.ToJsonProperty("Address");
        sa.Property(p => p.Street).ToJsonProperty("ShipsToStreet");
        sa.Property(p => p.City).ToJsonProperty("ShipsToCity");
    });

With this configuration the order from the example above is stored like this:

{
    "Id": 1,
    "PartitionKey": "1",
    "TrackingNumber": null,
    "id": "1",
    "Address": {
        "ShipsToCity": "London",
        "ShipsToStreet": "221 B Baker St"
    },
    "_rid": "6QEKAM+BOOABAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-692e763901d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163674
}

Collections of owned entities are embedded as well. For the next example we'll use the Distributor class with a collection of StreetAddress:

public class Distributor
{
    public int Id { get; set; }
    public string ETag { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

The owned entities don't need to provide explicit key values to be stored:

var distributor = new Distributor
{
    Id = 1,
    ShippingCenters = new HashSet<StreetAddress>
    {
        new StreetAddress { City = "Phoenix", Street = "500 S 48th Street" },
        new StreetAddress { City = "Anaheim", Street = "5650 Dolly Ave" }
    }
};

using (var context = new OrderContext())
{
    context.Add(distributor);

    await context.SaveChangesAsync();
}

They will be persisted in this way:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        },
        {
            "City": "Anaheim",
            "Street": "5650 Dolly Ave"
        }
    ],
    "_rid": "6QEKANzISj0BAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKANzISj0=/docs/6QEKANzISj0BAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-7b2b439701d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163705
}

Internally EF Core always needs to have unique key values for all tracked entities. The primary key created by default for collections of owned types consists of the foreign key properties pointing to the owner and an int property corresponding to the index in the JSON array. To retrieve these values entry API could be used:

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers: {firstDistributor.ShippingCenters.Count}");

    var addressEntry = context.Entry(firstDistributor.ShippingCenters.First());
    var addressPKProperties = addressEntry.Metadata.FindPrimaryKey().Properties;

    Console.WriteLine(
        $"First shipping center PK: ({addressEntry.Property(addressPKProperties[0].Name).CurrentValue}, {addressEntry.Property(addressPKProperties[1].Name).CurrentValue})");
    Console.WriteLine();
}

Tip

When necessary the default primary key for the owned entity types can be changed, but then key values should be provided explicitly.

Collections of primitive types

Collections of supported primitive types, such as string and int, are discovered and mapped automatically. Supported collections are all types that implement IReadOnlyList<T> or IReadOnlyDictionary<TKey,TValue>. For example, consider this entity type:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Both the list and the dictionary can be populated and inserted into the database in the normal way:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

This results in the following JSON document:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

These collections can then be updated, again in the normal way:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

Limitations:

  • Only dictionaries with string keys are supported
  • Querying into the contents of primitive collections is not currently supported. Vote for #16926, #25700, and #25701 if these features are important to you.

Working with disconnected entities

Every item needs to have an id value that is unique for the given partition key. By default EF Core generates the value by concatenating the discriminator and the primary key values, using '|' as a delimiter. The key values are only generated when an entity enters the Added state. This might pose a problem when attaching entities if they don't have an id property on the .NET type to store the value.

To work around this limitation one could create and set the id value manually or mark the entity as added first, then changing it to the desired state:

using (var context = new OrderContext())
{
    var distributorEntry = context.Add(distributor);
    distributorEntry.State = EntityState.Unchanged;

    distributor.ShippingCenters.Remove(distributor.ShippingCenters.Last());

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers is now: {firstDistributor.ShippingCenters.Count}");

    var distributorEntry = context.Entry(firstDistributor);
    var idProperty = distributorEntry.Property<string>("__id");
    Console.WriteLine($"The distributor 'id' is: {idProperty.CurrentValue}");
}

This is the resulting JSON:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        }
    ],
    "_rid": "JBwtAN8oNYEBAAAAAAAAAA==",
    "_self": "dbs/JBwtAA==/colls/JBwtAN8oNYE=/docs/JBwtAN8oNYEBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9377-d7a1ae7c01d5\"",
    "_attachments": "attachments/",
    "_ts": 1572917100
}

Optimistic concurrency with eTags

To configure an entity type to use optimistic concurrency call UseETagConcurrency. This call will create an _etag property in shadow state and set it as the concurrency token.

modelBuilder.Entity<Order>()
    .UseETagConcurrency();

To make it easier to resolve concurrency errors you can map the eTag to a CLR property using IsETagConcurrency.

modelBuilder.Entity<Distributor>()
    .Property(d => d.ETag)
    .IsETagConcurrency();