Skip to main content

Migrating the cart

Adding the Cosmos DB module

.NET Aspire has a Cosmos DB module. We will be using it.

Install it by running:

dotnet add package Aspire.Hosting.Azure.CosmosDB

Once we add that we can define the new Cosmos DB integration.

Simply add it by adding:

var cartDb = builder.AddAzureCosmosDB("cosmosdb")
.AddDatabase("cartdb");

and add a reference to the cart db on the API component:

builder.AddProject<Projects.Dometrain_Monolith_Api>("dometrain-api")
.WithReference(mainDb)
.WithReference(cartDb);

This might take a few minutes but in the end you will have a fully provisioned resource group, a key vault to store the connection string securely and the CosmosDB instance.

If you are using a Visual Studio account, please sign out.

If you haven't already, you will need to register KeyVault for your subscription.

az provider register -n Microsoft.KeyVault

Creating the container

Data in Cosmos DB is stored in "Containers". Not to be confused with Docker or Podman containers, you can think of these containers as tables in a traditional database.

Go to the Azure portal and create a new container with the following settings:

  • Existing Database: cartdb
  • Container id: carts
  • Partition key: /pk
  • Container throughput: Manual
  • Value: 400
  • Click ok

Adding Cosmos DB in the API project

This time we will add Cosmos DB using an Aspire specific package.

Aspire.Microsoft.Azure.Cosmos

We can now call the dependency injection registration method in the Program.cs:

builder.AddAzureCosmosClient("cosmosdb");

Replacing the db connection in the cart repository

All of our cart-related database interactions are contained within the ShoppingCartRepository.cs class. This means that to replace Postgres with Cosmos DB, all we need to do is remove the IDbConnectionFactory and replace it with the CosmosClient.

private readonly CosmosClient _cosmosClient;
private const string DatabaseId = "cartdb";
private const string ContainerId = "carts";

public ShoppingCartRepository(CosmosClient cosmosClient)
{
_cosmosClient = cosmosClient;
}

We can write the add to cart method with CosmosDB as follows:

public async Task<bool> AddCourseAsync(Guid studentId, Guid courseId)
{
var container = _cosmosClient.GetContainer(DatabaseId, ContainerId);
ShoppingCart cart;
try
{
var cartResponse = await container.ReadItemAsync<ShoppingCart>(studentId.ToString(), new PartitionKey(studentId.ToString()));
cart = cartResponse.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
cart = new ShoppingCart
{
StudentId = studentId,
CourseIds = []
};
}

if (!cart.CourseIds.Contains(courseId))
{
cart.CourseIds.Add(courseId);
}

var response = await container.UpsertItemAsync(cart);

return response.StatusCode is HttpStatusCode.OK or HttpStatusCode.Created;
}

One thing you need to note is that CosmosDB is using Newtonsoft.Json for its serialization. In order to fully control the Upsert operation we need to set the id property of the Cosmos DB object using the JsonProperty attribute. Be careful because it is case-sensitive.

public class ShoppingCart
{
[JsonProperty("pk")] //Needed for CosmosDB
[System.Text.Json.Serialization.JsonIgnore]
public string Pk => StudentId.ToString();

[JsonProperty("id")] //Needed for CosmosDB
public required Guid StudentId { get; set; }

public List<Guid> CourseIds { get; set; } = [];
}

Exercise: Migrate the remaining endpoints

The cart repository contains three more methods:

  • GetByIdAsync
  • RemoveItemAsync
  • ClearAsync

Write the implementation for these 3 methods using the CosmosClient

Solutions

GetByIdAsync
public async Task<ShoppingCart?> GetByIdAsync(Guid studentId)
{
var container = _cosmosClient.GetContainer(DatabaseId, ContainerId);
try
{
return await container.ReadItemAsync<ShoppingCart>(studentId.ToString(),
new PartitionKey(studentId.ToString()));
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
}
RemoveItemAsync
public async Task<bool> RemoveItemAsync(Guid studentId, Guid courseId)
{
var container = _cosmosClient.GetContainer(DatabaseId, ContainerId);
try
{
var cart = await GetByIdAsync(studentId);
if (cart is null)
{
return true;
}

cart.CourseIds.Remove(courseId);
var response = await container.UpsertItemAsync(cart);
return response.StatusCode is HttpStatusCode.OK or HttpStatusCode.Created;
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return true;
}
}
ClearAsync
public async Task<bool> ClearAsync(Guid studentId)
{
var container = _cosmosClient.GetContainer(DatabaseId, ContainerId);
try
{
await container.DeleteItemAsync<ShoppingCart>(studentId.ToString(), new PartitionKey(studentId.ToString()));
return true;
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return true;
}
}