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 thePkproperty 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
CosmosClientfrom 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:
- Every document needs an
idproperty - This is the unique identifier within a partition - Every document should have a partition key value - This determines which partition the document lives in
- 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
StudentIdas both theidand 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 IDRemoveItemAsync- Remove a specific course from a student's cartClearAsync- 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
CosmosExceptionwithStatusCode == HttpStatusCode.NotFoundfor 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;
}
}