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