Azure Cosmos DB — Using EF Core with a NoSQL Database in a .NET Web API

Henrique Siebert Domareski
12 min readMar 12, 2024

--

Azure Cosmos DB is a globally distributed database service that offers high availability, global distribution, automatic scalability and support for multi-data models. Entity Framework Core is an ORM (object-relation mapping) that allows you to work with databases in a .NET application. In this article, I present how to use Entity Framework Core with a NoSQL database in Azure Cosmos DB, in a .NET Web API.

If this is your first contact with Cosmos DB, I have an introductory article that explains the basics of Azure Cosmos DB: Azure Cosmos DB — Getting Started and Creating a NoSQL Database.

Azure Cosmos DB Emulator

To develop applications using Cosmos DB, you can do it by using the Cosmos DB in Azure, or you can use the emulator. The Emulator emulates Cosmos DB service on your own environment, allowing you to develop and test applications without being charged. You can find more information about the emulator on this Microsoft Docs page.

Keep in mind that there are some differences between the emulator and the cloud service, the Data Explorer for example, only fully supports the SQL API.

Once installed, you can search for “Azure Cosmos DB Emulator” and open the emulator:

It might take some time to start, but once started, then you can access https://localhost:8081/_explorer/index.html, and you will see this page where you can find the connection details:

On the Emulator, you can also access the “Explorer” page where you have similar functionalities as the the Data Explorer in Azure Portal.

For this demo, I created three containers:

  • Products container, which has /Category as the Partition key.
  • Inventory container, which has /ProductId as the Partition key.
  • Suppliers container, which has /ProductId as the Partition key.

Presenting the Demo Project

Now that the database and the containers are created, let’s go to the .NET Web API. This project consists of a Web API that will store Products in Azure Cosmos DB.

This is the structure of the solution:

  • API layer, which contains the Controllers and project configurations such as Dependency Injection configuration.
  • Domain layer, which contains the Models classes, the Interfaces and the Service classes.
  • Infrastructure layer, which contains the DbContext and the Repository classes.

Domain layer

In this project, we need to define the Interfaces, create the Model classes and the Service classes. I will focus only on the relevant information regarding using EF Core, but you can find the complete solution on my GitHub.

Models Classes

In the Domain project, there are five Model classes, there are: Product, Dimensions, ShippingOption, Supplier and Inventory. Below you can find a UML diagram for these classes:

  • A Product has 1 or more ShippingOption
  • A Product has 1 or more Supplier
  • A Product has 1 Inventory
  • A Product has 1 Dimensions

For demonstration purposes, the Supplier and the Inventory are stored in different containers, this way we can explore how to work with single and multiple containers in Cosmos DB using EF Core. I created the Product class in a way that we can see examples of how to work with an entity that contains a primitive type property (Name, Category), a complex type property (Dimensions, Inventory), and a list property (ShippingOptions, Supplier).

Below you can see the Model classes:

public class Product
{
public Guid ProductId { get; set; }
public required string Name { get; set; }
public required string Category { get; set; }
public required Dimensions Dimensions { get; set; }
public List<ShippingOption> ShippingOptions { get; set; } = [];
public List<Supplier> Suppliers { get; set; } = [];
public Inventory Inventory { get; set; } = new Inventory();
}

public class Dimensions
{
public required string Height { get; set; }
public required string Width { get; set; }
public required string Depth { get; set; }
}

public class ShippingOption
{
public required string Method { get; set; }
public decimal Cost { get; set; }
}

public class Supplier
{
public Guid SupplierId { get; set; }
public Guid ProductId { get; set; }
public required string Name { get; set; }
public required string ContactEmail { get; set; }
}

public class Inventory
{
public Guid InventoryId { get; set; }
public Guid ProductId { get; set; }
public int StockQuantity { get; set; }
public DateTime LastRestocked { get; set; }
}

Service method

In the service class there is the implementation of the following methods:

public interface IProductService
{
Task<Product> Add(Product product);
Task<Product?> GetById(Guid productId);
Task<Product?> Update(Product product);
Task<bool> Delete(Guid productId);
}

As we do not have any business rules, these methods are only calling the repository methods, so I will not show the implementation of them, to avoid making the article too lengthy.

Infrastructure layer

In this project, we need to install the EF Core package, configure the DbContext class and implement the repository classes.

Installing and configuring EF Core for Cosmos DB

In the Infrastructure project, install EF Core for Cosmos. For that, install the package Microsoft.EntityFrameworkCore.Cosmos:

Create a class that inherits from DbContext class, which is a class that acts as a bridge between the domain entities and the database. This class is part of the EF Core API and provides a set of methods and properties for performing database operations.`In this class, you can add the DbSets and override the OnModelCreating.

These are the CosmosDbContext class with the DbSets, there are one DbSet for each container in Cosmos DB:

 public class CosmosDbContext : DbContext
{
public CosmosDbContext(DbContextOptions options) : base(options) { }

public DbSet<Product> Products { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
public DbSet<Inventory> Inventory { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ...
}
}

Still in this class, we need to configure the mapping between the domain entities and the database. For that, we can override the OnModelCreatin method, which is the method that is called when EF core is building the application’s models.

First, let’s set the autoscale throughput for the Cosmos DB containers. In Azure Cosmos DB, throughput refers to the amount of resources allocated to a container. Autoscale adjusts the throughput based on the workload:

modelBuilder.HasAutoscaleThroughput(1000);

For demonstration purposes, I’m going to use the HasDefaultContainer, that can be used to specify the default container name to be used for the entities that don’t explicitly specify a container. This can be used when you have a single container, or when you want to specify a default container. For this example, I’m setting the Products container as default:

modelBuilder.HasDefaultContainer("Products");

Now I’m configuring the Product entity:

modelBuilder.Entity<Product>()
.HasNoDiscriminator()
.HasPartitionKey(x => x.Category)
.HasKey(x => x.ProductId);
  • HasNoDiscriminator: The Discriminator is a property that helps EF Core to identify the type of entity stored in a container, this can be useful when you have multiple entities in the same container. In this example, we are specifying that this entity does not have a discriminator.
  • HasPartitionKey: This specifies that the partition key for the Products container, which is the Category property of the Product.
  • HasKey: This sets the primary key for the entity.

This is the configuration for the Supplier class:

modelBuilder.Entity<Supplier>()
.HasNoDiscriminator()
.ToContainer("Suppliers")
.HasPartitionKey(x => x.ProductId)
.HasKey(x => x.SupplierId);

This configuring contains one different when compared with the previous one, which is the ToContainer configuration. This is used to specify that for this entity, the data should be stored in the Suppliers container (if you do not specify this, it will use the default container).

And we have something similar for the Inventory:

modelBuilder.Entity<Inventory>()
.HasNoDiscriminator()
.ToContainer("Inventory")
.HasPartitionKey(x => x.ProductId)
.HasKey(x => x.InventoryId);

So this is an overview of the CosmosDbContext class with all the changes:

public class CosmosDbContext : DbContext
{
public CosmosDbContext(DbContextOptions options) : base(options) { }

public DbSet<Product> Products { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
public DbSet<Inventory> Inventory { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAutoscaleThroughput(1000);

modelBuilder.HasDefaultContainer("Products");

modelBuilder.Entity<Product>()
.HasNoDiscriminator()
.HasPartitionKey(x => x.Category)
.HasKey(x => x.ProductId);

modelBuilder.Entity<Supplier>()
.HasNoDiscriminator()
.ToContainer("Suppliers")
.HasPartitionKey(x => x.ProductId)
.HasKey(x => x.SupplierId);

modelBuilder.Entity<Inventory>()
.HasNoDiscriminator()
.ToContainer("Inventory")
.HasPartitionKey(x => x.ProductId)
.HasKey(x => x.InventoryId);
}
}

Repository Methods

In the repository class, is where we are going to implement the operations to the database. Important to mention that when working with EF Core and Cosmos DB, we need to always use async methods.

public interface IProductRepository
{
Task Add(Product product);
Task<Product?> GetById(Guid productId);
Task<Product?> Update(Product product);
Task<bool> Delete(Guid productId);
}

Add Operation

The method responsible for adding an entity into the database, is quite simple. As the configuration is already set in the CosmosDbContext class, we only need to call the Add method with the entity, and then call the SaveChangesAsync method.

public async Task Add(Product product)
{
_dbContext.Add(product);

await _dbContext.SaveChangesAsync();
}

Get Operation

To read an entity from the database, you can use the FindAsync method:

var product = await _dbContext
.Products
.FindAsync(productId);

In our example, the Product contains data that comes from different containers, and because of that, when using only this query with the FindAsync, the Inventory and the Suppliers will be empty. If you are familiarized with EF Core, you could think about using .Include, but unfortunately, this is not supported (at least not yet) by EF Core for Cosmos DB.

To retrieve data from another container, we need to use the Entry method, which provides access to change tracking information and operations for the entity, we can then access the information about the associated entity, including navigation properties and their current state:

var productEntry = _dbContext.Products.Entry(product);

Once we have the entry, we need to use the Reference method to indicate that we want to load a reference navigation property for an entity. This methods provides access to change tracking and loading information for a reference (i.e. non-collection) navigation property that associates this entity to another entity. Then we also use the LoadAsync to load the entity referenced by this navigation property.:

 await productEntry
.Reference(product => product.Inventory)
.LoadAsync();

For the Supplier, as Supplier is a List, we need to use the Collection method. This method provides access to change tracking and loading information for a collection navigation property that associates this entity to a collection of other entities:

await productEntry
.Collection(product => product.Suppliers)
.LoadAsync();

As we will need to re-use this code in other methods, I added this code to the private method LoadProductWithReferences, this method will return data from the Products, Inventory and Suppliers container. The GetById calls this method and return the product:

public async Task<Product?> GetById(Guid productId)
{
var product = await LoadProductWithReferences(productId);

return product;
}

private async Task<Product?> LoadProductWithReferences(Guid productId)
{
var product = await _dbContext
.Products
.FindAsync(productId);

if (product == null) return null;

var productEntry = _dbContext.Products.Entry(product);

// Include the Inventory (which comes from another container)
await productEntry
.Reference(product => product.Inventory)
.LoadAsync();

// Include the Suppliers (which come from another container)
await productEntry
.Collection(product => product.Suppliers)
.LoadAsync();

return product;
}

Update method

For the Update method, we first need to retrieve the entity from the database with its references (by calling the method LoadProductWithReferences), in order to also get the data that are stored in other containers. Once you retrieve the entity with its references, you can update the properties of the existing product with values from the product parameter, and then call the SaveChangesAsync to save the changes::

public async Task<Product?> Update(Product product)
{
var existingProduct = await LoadProductWithReferences(product.ProductId);

if (existingProduct == null) return null;

existingProduct.Name = product.Name;
existingProduct.Category = product.Category;
existingProduct.Dimensions = product.Dimensions;
existingProduct.ShippingOptions = product.ShippingOptions;
existingProduct.Suppliers = product.Suppliers;
existingProduct.Inventory = product.Inventory;

await _dbContext.SaveChangesAsync();

return product;
}

Delete operation

The delete method first loads the Product with all its references (by calling the method LoadProductWithReferences), and the reason is because we want to delete the data from the Product container but also want to delete the Product data that are stored in the Inventory and in the Suppliers container. For that, the product entity should be loaded from the database with the reference for these properties, otherwise when you call the Remove method, only the data in the Products container will be deleted.

After retrieving the product with the reference data, we can then call the Remove method and the SaveChangesAsync method to save the deletion:

public async Task<bool> Delete(Guid productId)
{
var product = await LoadProductWithReferences(productId);

if (product == null) return false;

_dbContext.Products.Remove(product);

await _dbContext.SaveChangesAsync();

return true;
}

Note that if you only load the Product with the FindAsync method, and do not load the reference data, when you call the Remove method, only the data in the Products container will be deleted.

API Layer

In the API project, there is the configuration for the Cosmos DB database, which can be found in the Program.cs:

builder.Services.AddDbContextFactory<CosmosDbContext>(optionsBuilder =>
optionsBuilder
.UseCosmos(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection"),
databaseName: "ApplicationDB",
cosmosOptionsAction: options =>
{
options.ConnectionMode(Microsoft.Azure.Cosmos.ConnectionMode.Direct);
options.MaxRequestsPerTcpConnection(16);
options.MaxTcpConnectionsPerEndpoint(32);
}));
  • The builder.Services.AddDbContextFactory, adds a DbContextFactory for the CosmosDbContext to the dependency injection container. The factory is configured through the optionsBuilder delegate.
  • The connectionString, which sets up the Cosmos DB provider for EF Core with the specified connection string. The connection string is retrieved from the configuration using the key “DefaultConnection”.
  • The databaseName, which specifies the name of the database within the Cosmos DB account. In this case, the database is named “ApplicationDB”.
  • The ConnectionMode, which in this example is set to “Direct”, which means that the client connects directly to the individual Cosmos DB gateway associated with the account.
  • The MaxRequestsPerTcpConnection, which sets the maximum number of requests per TCP connection to 16. This configuration parameter determines how many requests can be sent over a single TCP connection before a new connection is established.
  • The MaxTcpConnectionsPerEndpoint, which sets the maximum number of TCP connections per endpoint to 32. This configuration parameter limits the total number of concurrent TCP connections to the Cosmos DB service endpoint.

The DefaultConnection is coming from the appsettings.json file:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "[ADD-THE-CONNECTION-STRING]"
}
}

Remember to update this file with the connection string of your database.

In the API project, I created 4 endpoints that will perform the operations that were presented in the previous topic. These are the endpoints:

For demonstration purposes, I’m using the Model classes in the controller, in order to keep it simple. But it’s better to use a DTO or a ViewModel class instead of using the Model class.

Inside the solution, there is an .http file with some request examples for each endpoint. This is an example of a POST request to add a Product:

{
"productId": "df7f99f1-48d4-439a-9520-028d54bd287c",
"name": "Surface Pro 9",
"category": "Laptop",
"dimensions": {
"height": "0.879kg",
"width": "28.7cm",
"depth": "20.9cm"
},
"shippingOptions": [
{
"cost": 2.99,
"method": "Standard"
},
{
"cost": 4.99,
"method": "Express"
}
],
"suppliers": [
{
"supplierId": "31d287b1-bab3-45c6-a9c5-e49eddcc1384",
"name": "Supplier 1",
"contactEmail": "supplier1@demo.com"
},
{
"supplierId": "96e3d261-f032-46fa-bb8d-96c0ccf9abe3",
"name": "Supplier 2",
"contactEmail": "supplier2@demo.com"
},
{
"supplierId": "f62f4b72-9a4e-4220-9147-a928a34c6cb0",
"name": "Supplier 3",
"contactEmail": "supplier3@demo.com"
}
],
"inventory": {
"inventoryId": "45f01e3e-1a90-4db0-bfde-e00b7ed2ded5",
"stockQuantity": 100,
"lastRestocked": "2024-03-08T19:59:47.777Z"
}
}

As expected, this request will add one item to the Products container, three items in the Supplier container and one item in the Inventory container.

The item was added to the Products container:

It was also added one item to the Inventory container:

And also three items were added to the Suppliers container:

  • The PUT request, which is used to do the update, has a similar request body as the one used in the POST, so I will not add it here.
  • To retrieve the Product, a GET request can be sent, with the product id.
  • To delete a Product, send a DELETE request with the product id, and the Product, the Supplier and the Inventory items will be deleted.

Conclusion

Entity Framework Core for Azure Cosmos DB offers an efficient way to interact with a NoSQL database in a .NET Web API. In this article, we explored key concepts such as container configuration, partition keys, and how to work with single and multiple containers, as well as how to use Cosmos DB Emulator to avoid extra costs during the development.

This is the link for the project in GitHub: https://github.com/henriquesd/CosmosDbEfCoreDemo

If you like this demo, I kindly ask you to give a ⭐️ in the repository.

Thanks for reading!

--

--