Skip to main content

Migrating the cart

Adding the Cosmos DB Integration

Aspire provides a Cosmos DB hosting integration that supports both local development with the emulator and deployment to Azure Cosmos DB.

First, install the Cosmos DB hosting package to your AppHost project:

dotnet add package Aspire.Hosting.Azure.CosmosDB

Local Development with Cosmos DB Emulator (Preview)

For local development, Aspire can use the Azure Cosmos DB Linux Emulator (Preview) running in a container. This gives you a full Cosmos DB experience without needing an Azure subscription.

The preview emulator is:

  • Linux-based: Works on a wide variety of processors and operating systems
  • Modern: Next generation of the Cosmos DB emulator
  • Cross-platform: Better compatibility than the legacy Windows emulator
  • Container-based: Runs as a Docker container with no manual setup

Enabling the Preview Feature

Since the preview emulator is an experimental feature, you need to suppress the ASPIRECOSMOSDB001 diagnostic. Add this to your AppHost project file (.csproj):

<PropertyGroup>
<NoWarn>$(NoWarn);ASPIRECOSMOSDB001</NoWarn>
</PropertyGroup>

Setting Up the Preview Emulator

Add the Cosmos DB preview emulator to your AppHost's Program.cs:

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

The .RunAsPreviewEmulator() method tells Aspire to:

  • Start the preview Cosmos DB emulator container automatically
  • Configure connection strings for local development
  • Handle certificate trust for HTTPS connections
  • Use the modern Linux-based emulator image

Optional: Enable Data Explorer

The preview emulator supports a Data Explorer web UI where you can view and manage your Cosmos DB data visually. To enable it:

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

When you run your AppHost, the Data Explorer will be accessible via the Aspire Dashboard. This is incredibly useful for:

  • Viewing documents in your containers
  • Running queries interactively
  • Debugging data issues
  • Understanding how your data is stored

Adding the Container Declaration

Now we can define the "carts" container directly in code. We'll assign it to a variable since we'll reference this specific container later:

var carts = builder.AddAzureCosmosDB("cosmosdb")
.RunAsPreviewEmulator()
.WithDataExplorer() // Optional: adds web UI for viewing data
.AddDatabase("cartdb")
.AddContainer("carts", partitionKeyPath: "/pk");

The .AddContainer() method:

  • Creates the container automatically when the database initializes
  • Sets the partition key to /pk (critical for performance)
  • Eliminates manual container creation steps

Connecting to the API

Add a reference to the carts container in your API project registration:

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

Notice we reference the carts container (not the database or account) and use .WaitFor(carts) to ensure the API doesn't start until the Cosmos DB container is ready.

Complete AppHost Configuration

Here's what your complete Cosmos DB setup looks like:

var carts = builder.AddAzureCosmosDB("cosmosdb")
.RunAsPreviewEmulator()
.WithDataExplorer() // Optional but recommended for development
.AddDatabase("cartdb")
.AddContainer("carts", partitionKeyPath: "/pk");

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

Remember: Add <NoWarn>$(NoWarn);ASPIRECOSMOSDB001</NoWarn> to your AppHost .csproj file to suppress the preview warning.

Deploying to Azure Cosmos DB

When you're ready to deploy to Azure (after completing the Azure setup from the previous section), simply remove the .RunAsPreviewEmulator() and .WithDataExplorer() calls:

var carts = builder.AddAzureCosmosDB("cosmosdb")
.AddDatabase("cartdb")
.AddContainer("carts", partitionKeyPath: "/pk");

Aspire will automatically:

  • Provision a new Azure Cosmos DB account in your resource group
  • Create a Key Vault to securely store connection strings
  • Create the database and container with the specified configuration
  • Configure your services with the connection string

Note: The .RunAsPreviewEmulator() and .WithDataExplorer() methods are only for local development. They are automatically ignored when deploying to Azure.

Understanding Cosmos DB Containers

Data in Cosmos DB is stored in containers. Despite the name, these are not Docker containers—think of them as tables in a traditional database.

Key concepts:

  • Database: A logical grouping of containers (like a database in SQL)
  • Container: Where documents (JSON objects) are stored (like a table)
  • Partition key: The property used to distribute data across partitions for scale
  • Throughput: Request Units (RU/s) allocated for performance

For the carts container:

  • Container ID: carts
  • Partition key: /pk (maps to the Pk property in our ShoppingCart model)
  • Throughput: Managed by Azure in serverless mode, or set manually in provisioned mode

Adding Cosmos DB Client to the API Project

Now we need to configure the API project to connect to Cosmos DB.

Install the Aspire Cosmos DB client package to your API project:

dotnet add package Aspire.Microsoft.Azure.Cosmos

This package provides an integration that:

  • Automatically configures the CosmosClient from connection strings
  • Includes health checks for Cosmos DB
  • Adds telemetry and logging for database operations
  • Works seamlessly with both the emulator and Azure Cosmos DB

Register the Cosmos DB client in your API's Program.cs:

builder.AddAzureCosmosClient("carts");

The connection name "carts" references the container resource we defined in the AppHost. Aspire best practice is to reference the lowest-level resource you need—in this case, the specific container rather than the database or account level. This ensures the connection is scoped correctly and provides the most specific configuration.

Implementing the Cart Repository with Cosmos DB

All of our cart-related database interactions are contained within the ShoppingCartRepository.cs class. Since we're migrating from PostgreSQL to Cosmos DB, we need to update this class to use the CosmosClient instead of IDbConnectionFactory.

Update the Repository Constructor

Replace the PostgreSQL dependency injection with Cosmos DB:

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

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

The CosmosClient is automatically injected by Aspire's dependency injection configuration.

Implementing AddCourseAsync

Here's how to implement the add to cart method with Cosmos DB:

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

Understanding the Cosmos DB Document Model

Cosmos DB has specific requirements for documents:

  1. Every document needs an id property - This is the unique identifier within a partition
  2. Every document should have a partition key value - This determines which partition the document lives in
  3. Cosmos DB uses Newtonsoft.Json for serialization (not System.Text.Json by default)

Update your ShoppingCart model to work with Cosmos DB:

public class ShoppingCart
{
// Partition key property - must match the partition key path ("/pk")
[JsonProperty("pk")]
[System.Text.Json.Serialization.JsonIgnore] // Hide from System.Text.Json serialization
public string Pk => StudentId.ToString();

// Document ID - required by Cosmos DB (case-sensitive!)
[JsonProperty("id")]
public required Guid StudentId { get; set; }

// Your business data
public List<Guid> CourseIds { get; set; } = [];
}

Key points:

  • [JsonProperty("pk")] maps to the partition key path we defined (/pk)
  • [JsonProperty("id")] provides the unique document identifier (case-sensitive!)
  • We use StudentId as both the id and partition key value since each student has one cart
  • [System.Text.Json.Serialization.JsonIgnore] prevents conflicts if you use System.Text.Json elsewhere

Exercise: Implementing the Remaining Methods

The cart repository contains three more methods that need to be migrated from PostgreSQL to Cosmos DB:

  • GetByIdAsync - Retrieve a student's cart by their ID
  • RemoveItemAsync - Remove a specific course from a student's cart
  • ClearAsync - Delete a student's entire cart

Try implementing these methods yourself using the patterns from AddCourseAsync before looking at the solutions.

Hints

  • Use container.ReadItemAsync<T>() to read a single document by ID and partition key
  • Use container.UpsertItemAsync() to update an existing document
  • Use container.DeleteItemAsync<T>() to delete a document
  • Handle CosmosException with StatusCode == HttpStatusCode.NotFound for missing items
  • Remember that the partition key value is the same as the student ID

Solutions

Try implementing these yourself first! Click to reveal the solutions when you're ready.

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