Azure Cosmos DB — Using EF Core with a NoSQL Database in a .NET Web API
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, theInterfaces
and theService
classes. - Infrastructure layer, which contains the
DbContext
and theRepository
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 moreShippingOption
- A
Product
has 1 or moreSupplier
- A
Product
has 1Inventory
- A
Product
has 1Dimensions
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 theCategory
property of theProduct
. - 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 theoptionsBuilder
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 thePOST
, 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 theProduct
, theSupplier
and theInventory
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!