EF Core Azure Cosmos DB Provider

Note

This provider is new in EF Core 3.0.

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.

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.

Like 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 securily

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 quering data follows the normal EF pattern:

var londonOrder = new Order
{
    Id = 1,
    ShippingAddress = new StreetAddress { City = "London", Street = "221 B Baker St" }
};

using (var context = new OrderContext())
{
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    context.Add(londonOrder);

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

Important

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

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.

Embedded Entities

For Cosmos 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,
    "Discriminator": "Order",
    "TrackingNumber": null,
    "id": "Order|1",
    "Address": {
        "ShipsToCity": "London",
        "Discriminator": "StreetAddress",
        "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 ICollection<StreetAddress> ShippingCenters { get; set; }
}

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

using (var context = new OrderContext())
{
    context.Add(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" }
        }
    });

    await context.SaveChangesAsync();
}

They will be persisted in this way:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Discriminator": "StreetAddress",
            "Street": "500 S 48th Street"
        },
        {
            "City": "Anaheim",
            "Discriminator": "StreetAddress",
            "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})");
}

Tip

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

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 orderEntry = context.Add(londonOrder);
    orderEntry.State = EntityState.Unchanged;

    londonOrder.ShippingAddress.Street = "3 Abbey Road";

    await context.SaveChangesAsync();
}

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

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

This is the resulting JSON:

{
    "Id": 1,
    "Discriminator": "Order",
    "TrackingNumber": null,
    "id": "Order|1",
    "Address": {
        "ShipsToCity": "London",
        "Discriminator": "StreetAddress",
        "ShipsToStreet": "3 Abbey Road"
    },
    "_rid": "6QEKAM+BOOABAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-8f7ac48f01d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163739
}