Building a Modern Development Platform: Aspire for Local Development
- Introduction 🚀
- Prerequisites 📋
- Creating the Aspire Solution from Template 🏗️
- Running the Basic Aspire Dashboard 🎛️
- Adding a .NET Web API 🌐
- Adding a React Frontend ⚛️
- Running with Services Added 🚀
- Connecting React to the Weather API 🌤️
- Adding Cosmos DB Integration 🗄️
- Why Dev Containers Matter 🐳
- Aspire Roadmap & Future 🔮
- Resources & Community 🌍
- Conclusion 🎉
Introduction 🚀
In our tool selection post, we introduced Aspire as our solution for orchestrating local development environments. Today, we’re going to dive deep into Aspire by building a real-world application from scratch.
By the end of this post, you’ll have a complete understanding of how Aspire simplifies local development for distributed applications. We’ll build a full-stack application with:
- A .NET Web API backed by Azure Cosmos DB
- A React TypeScript frontend
- Aspire orchestration for running everything locally
- Built-in observability with the Aspire Dashboard
What is Aspire? Aspire is an opinionated, cloud-ready stack for building observable, production-ready distributed applications. It handles service orchestration, service discovery, and telemetry out of the box, making local development of microservices feel as simple as running a monolith.
The Problem Aspire Solves: Before Aspire, running a distributed application locally meant:
- Starting databases manually (Docker Compose, local SQL Server, etc.)
- Managing connection strings across multiple services
- Running each service individually in separate terminals
- Struggling to correlate logs and telemetry across services
- “Works on my machine” syndrome when environment setup varies by developer
Aspire eliminates all of this friction. Let’s see how.
GitHub Repository: All code from this tutorial is available at github.com/two4suited/blog-platform-aspire - created from the official Aspire Dev Container template
Prerequisites 📋
Before we begin, make sure you have:
- Visual Studio Code
- Dev Containers extension for VS Code
- Docker Desktop running on your machine
- Git for cloning the template
That’s it! The Dev Container will handle installing .NET 9 SDK, Aspire workload, Node.js, and all other dependencies automatically. No more “works on my machine” problems - the container is the machine.
Creating the Aspire Solution from Template 🏗️
Step 1: Create Repository from Aspire Dev Container Template
Instead of manually installing dependencies, we’ll use Microsoft’s official Aspire Dev Container template. This gives us a fully configured development environment in a container:
- Navigate to the Aspire Dev Container template repository
- Click “Use this template” → “Create a new repository”
- Name your repository
aspire-weather-app
- Make it public or private (your choice)
- Click “Create repository”
Step 2: Clone and Open in Dev Container
Clone your new repository locally:
git clone https://github.com/YOUR_USERNAME/aspire-weather-app
cd aspire-weather-app
code .
VS Code will detect the .devcontainer/devcontainer.json
file and prompt you to “Reopen in Container”. Click it!
The first time you do this, Docker will build the dev container. This takes a few minutes as it:
- Pulls the .NET 9 dev container image
- Installs the Aspire workload
- Configures Docker-in-Docker (for running Cosmos DB and other containers)
- Installs VS Code extensions (C# Dev Kit, Docker, GitHub Copilot)
- Sets up HTTPS certificates
Grab a coffee ☕ - subsequent opens will be much faster!
Step 3: Understanding the Dev Container Setup
The Aspire dev container template automatically configures everything you need for Aspire development:
Everything is Pre-Installed! 🎉
- .NET 9 SDK ✅
- Aspire workload ✅
- Aspire project templates ✅
- Aspire CLI ✅
- HTTPS certificates ✅
For complete dev container configuration details (including Node.js and Docker setup for full-stack applications), see the official Aspire Dev Containers documentation.
Step 4: Create Aspire Projects Using CLI Template Selection
We’ll use the Aspire CLI’s interactive experience to create our projects:
aspire new
The Aspire CLI will guide you through the setup:
- Template Selection Menu:
Select a project template: > Starter template AppHost and service defaults AppHost Service defaults Integration tests
-
Select “AppHost and service defaults” - this creates both projects we need in one step
-
Project Name: When prompted, enter
WeatherApp
- Output Location: When prompted for the output directory, enter
src
(this will create thesrc
directory for you)
This creates both essential components:
- AppHost - The orchestrator that manages all our services
- Service defaults - Shared configurations for telemetry, health checks, and resilience
This will create in the src/
directory:
WeatherApp.AppHost/
- Orchestration projectWeatherApp.ServiceDefaults/
- Shared configuration projectWeatherApp.sln
- Solution file tying them together
Why “AppHost and service defaults”? This template gives us:
- Clean foundation without sample projects (unlike Starter template)
- Both essential Aspire components in one step
- Proper project references already configured
- Ready to add our own API and frontend projects
Let’s explore the structure that was created:
cd src
ls -la
You’ll see:
WeatherApp.AppHost/ # Orchestration project
WeatherApp.ServiceDefaults/ # Shared configuration
WeatherApp.sln # Solution file
Step 5: Understanding the AppHost
Open src/WeatherApp.AppHost/AppHost.cs
. The AppHost template has a minimal setup:
var builder = DistributedApplication.CreateBuilder(args);
// Add your resources here
builder.Build().Run();
This is the heart of Aspire orchestration. Every service, database, and dependency will be defined here. As we add projects, we’ll use methods like:
builder.AddProject<>()
to add .NET projectsbuilder.AddNpmApp()
to add Node.js/React appsbuilder.AddAzureCosmosDB()
to add databases.WithReference()
to set up service-to-service communication
WeatherApp.ServiceDefaults contains extension methods that wire up:
- OpenTelemetry for distributed tracing
- Health check endpoints
- Service discovery
- Resilience patterns (retry, circuit breaker, etc.)
Running the Basic Aspire Dashboard 🎛️
Before we add any services, let’s see Aspire in action with just the basic setup:
aspire run
What aspire run
does:
- Automatically searches for and finds the AppHost project in the current directory and subdirectories
- Creates configuration in the
.aspire
folder withsettings.json
for project paths - Builds the AppHost project
- Starts the Aspire Dashboard (even with no services yet)
- Shows the foundation for our distributed application
The output will show something like:
Dashboard: https://localhost:17178/login?t=17f974bf68e390b0d4548af8d7e38b65
Logs: /home/vscode/.aspire/cli/logs/apphost-1295-2025-07-14-18-16-13.log
Certificate Warning: The first time you access the dashboard URL, you’ll see a certificate error in your browser. This is expected behavior in the dev container environment.
For complete details on handling certificate warnings, see the Aspire Dev Containers documentation.
Once you access the dashboard, you’ll see it’s ready and waiting for services! The dashboard shows:
- Resources - Currently empty, but ready for our services
- Console Logs - Dashboard and AppHost logs
- Structured Logs - Searchable logs with correlation IDs
- Traces - Ready for distributed tracing
- Metrics - Performance monitoring ready
Stop the Application: Press Ctrl+C in the terminal to stop Aspire when you’re ready to continue.
Now let’s add some services to see the real power of Aspire orchestration!
Adding a .NET Web API 🌐
The starter template includes sample projects, but let’s create our own clean weather API from scratch.
Step 6: Create the API Project
From the terminal in VS Code (inside the dev container):
# Navigate to the src directory where our projects are created
cd src
dotnet new webapi -n WeatherApp.Api
dotnet sln WeatherApp.sln add WeatherApp.Api/WeatherApp.Api.csproj
Step 7: Add Aspire Service Defaults
Add a project reference to the ServiceDefaults project in the API project:
cd WeatherApp.Api
dotnet add reference ../WeatherApp.ServiceDefaults/WeatherApp.ServiceDefaults.csproj
Update WeatherApp.Api/Program.cs
to wire up the service defaults:
Add ServiceDefaults after WebApplication.CreateBuilder(args)
:
builder.AddServiceDefaults();
This integrates your API with Aspire’s telemetry, health checks, service discovery, and resilience patterns from the ServiceDefaults project.
Step 8: Register the API in the AppHost
First, add a project reference from AppHost to the API:
dotnet add src/WeatherApp.AppHost/WeatherApp.AppHost.csproj reference src/WeatherApp.Api/WeatherApp.Api.csproj
Now update WeatherApp.AppHost/AppHost.cs
:
var builder = DistributedApplication.CreateBuilder(args);
// Add the API project
var api = builder.AddProject<Projects.WeatherApp_Api>("weatherapi");
builder.Build().Run();
Adding a React Frontend ⚛️
Step 9: Create the React App
The dev container already has Node.js installed, so we can create our React app with Vite:
# From the src directory
npm create vite@latest WeatherApp.Web -- --template react-ts
cd WeatherApp.Web
npm install
Step 10: Add the React App to Aspire
First, install the Aspire Community Toolkit for Node.js extensions:
dotnet add src/WeatherApp.AppHost/WeatherApp.AppHost.csproj package CommunityToolkit.Aspire.Hosting.NodeJS.Extensions
Now update WeatherApp.AppHost/AppHost.cs
to use the Vite integration:
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.WeatherApp_Api>("weatherapi");
// Add the React frontend using Vite integration
var frontend = builder.AddViteApp("frontend", "../WeatherApp.Web")
.WithNpmPackageInstallation()
.WithReference(api)
.WithEnvironment("BROWSER", "false")
.WithExternalHttpEndpoints();
builder.Build().Run();
Key features of this integration:
AddViteApp()
- Uses the official Vite integration from Aspire Community ToolkitWithNpmPackageInstallation()
- Automatically runsnpm install
before starting the dev serverWithReference(api)
- Makes the API URL available to the React app via environment variablesWithExternalHttpEndpoints()
- Exposes the Vite dev server for external access
Running with Services Added 🚀
Step 11: Launch the Complete Application
Now let’s run our application with all services configured:
aspire run
This time, Aspire will:
✅ Start the .NET Weather API
✅ Start the React dev server
✅ Launch the Aspire Dashboard
✅ Set up service discovery between all components
The dashboard now shows all your services running and their interconnections!
Connecting React to the Weather API 🌤️
Step 12: Configure Vite Proxy
Configure Vite to proxy API calls to the backend. Update WeatherApp.Web/vite.config.ts
:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
host: true,
proxy: {
'/api': {
target: process.env.services__weatherapi__https__0 ||
process.env.services__weatherapi__http__0,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
This configuration:
proxy: '/api'
- Intercepts all requests starting with/api
target
- Uses Aspire’s injected environment variables for the API URLchangeOrigin: true
- Changes the origin of the request to match the targetsecure: false
- Allows self-signed certificates in developmentrewrite
- Removes/api
prefix before forwarding to the backend
Step 13: Create the Weather Service
Create WeatherApp.Web/src/services/weatherService.ts
:
export interface WeatherForecast {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
location: string;
}
export const weatherService = {
async getForecast(): Promise<WeatherForecast[]> {
// Use the Vite proxy - requests to /api/* get forwarded to the backend
const response = await fetch('/api/weather');
if (!response.ok) {
throw new Error('Failed to fetch weather data');
}
return response.json();
}
};
Step 14: Update the React Component
Update WeatherApp.Web/src/App.tsx
to display the weather data:
import { useState, useEffect } from 'react'
import { weatherService } from './services/weatherService'
import type { WeatherForecast } from './services/weatherService'
import './App.css'
function App() {
const [weatherData, setWeatherData] = useState<WeatherForecast[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchWeatherData = async () => {
try {
setLoading(true)
setError(null)
const data = await weatherService.getForecast()
setWeatherData(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch weather data')
} finally {
setLoading(false)
}
}
fetchWeatherData()
}, [])
if (loading) {
return <div className="loading">Loading weather data...</div>
}
if (error) {
return (
<div className="error">
<h2>Error</h2>
<p>{error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
)
}
return (
<div className="weather-app">
<h1>Weather Forecast</h1>
<div className="weather-table-container">
<table className="weather-table">
<thead>
<tr>
<th>Date</th>
<th>Location</th>
<th>Temperature (°C)</th>
<th>Temperature (°F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{weatherData.map((forecast) => (
<tr key={forecast.id}>
<td>{new Date(forecast.date).toLocaleDateString()}</td>
<td>{forecast.location}</td>
<td>{forecast.temperatureC}°C</td>
<td>{forecast.temperatureF}°F</td>
<td>{forecast.summary}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
export default App
This component includes:
- Loading states - Shows loading spinner while fetching data
- Error handling - Displays errors with retry option
- TypeScript types - Fully typed with the WeatherForecast interface
- Clean table layout - Displays all weather data in an organized table
- Date formatting - Properly formats dates for display
Add the corresponding styles in WeatherApp.Web/src/App.css
:
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
}
.weather-app {
text-align: center;
}
.weather-app h1 {
color: #213547;
margin-bottom: 2rem;
}
.weather-table-container {
overflow-x: auto;
margin: 2rem 0;
}
.weather-table {
width: 100%;
border-collapse: collapse;
margin: 0 auto;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
.weather-table th {
background: #646cff;
color: white;
padding: 1rem;
text-align: left;
font-weight: 600;
}
.weather-table td {
padding: 1rem;
border-bottom: 1px solid #e5e5e5;
text-align: left;
}
.weather-table tbody tr:hover {
background: #f8f9fa;
}
.weather-table tbody tr:last-child td {
border-bottom: none;
}
.loading {
text-align: center;
padding: 2rem;
font-size: 1.2rem;
color: #646cff;
}
.error {
text-align: center;
padding: 2rem;
color: #d32f2f;
}
.error h2 {
margin-bottom: 1rem;
}
.error button {
background: #646cff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-top: 1rem;
}
.error button:hover {
background: #535bf2;
}
The CSS provides:
- Modern styling - Clean, professional appearance with Vite’s default color scheme
- Responsive design - Table scrolls horizontally on smaller screens
- Interactive elements - Hover effects and styled buttons
- Accessible design - Good contrast ratios and readable typography
- Loading/Error states - Styled feedback for different application states
Adding Cosmos DB Integration 🗄️
Now let’s back our API with Azure Cosmos DB for persistent storage. Thanks to the dev container’s Docker-in-Docker setup, we can run the Cosmos DB emulator right inside our development environment!
Step 15: Add Cosmos DB to AppHost
Aspire has built-in support for Azure Cosmos DB emulator. You can add it using either approach:
Add the Cosmos DB integration using the Aspire CLI:
aspire add cosmos
The Aspire CLI will show you available Cosmos DB integration packages and automatically install the correct one.
Update WeatherApp.AppHost/AppHost.cs
:
var builder = DistributedApplication.CreateBuilder(args);
#pragma warning disable ASPIRECOSMOSDB001
var cosmos = builder.AddAzureCosmosDB("cosmos").RunAsPreviewEmulator(
emulator =>
{
emulator.WithDataExplorer();
});
#pragma warning restore ASPIRECOSMOSDB001
var database = cosmos.AddCosmosDatabase("WeatherDB");
var container = database.AddContainer("WeatherData", "/id");
// Pass Cosmos DB container connection to the API
var api = builder.AddProject<Projects.WeatherApp_Api>("weatherapi")
.WithReference(container);
// Add the React frontend using Vite integration
var frontend = builder.AddViteApp("frontend", "../WeatherApp.Web")
.WithNpmPackageInstallation()
.WithReference(api)
.WithEnvironment("BROWSER", "false")
.WithExternalHttpEndpoints();
builder.Build().Run();
Step 16: Create the Data Layer Project
Let’s create a separate data project to handle Entity Framework and data access:
# From the src directory
cd src
dotnet new classlib -n WeatherApp.Data
dotnet sln WeatherApp.sln add WeatherApp.Data/WeatherApp.Data.csproj
Add the Entity Framework Cosmos package:
cd WeatherApp.Data
dotnet add package Aspire.Microsoft.EntityFrameworkCore.Cosmos
Create the weather entity in WeatherApp.Data/WeatherForecast.cs
:
namespace WeatherApp.Data;
public record WeatherForecast(Guid Id, DateOnly Date, int TemperatureC, string? Summary, string Location)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Create the Entity Framework context in WeatherApp.Data/WeatherContext.cs
:
using Microsoft.EntityFrameworkCore;
namespace WeatherApp.Data;
public class WeatherContext : DbContext
{
public WeatherContext(DbContextOptions<WeatherContext> options) : base(options)
{
}
public DbSet<WeatherForecast> WeatherForecasts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<WeatherForecast>(entity =>
{
entity.ToContainer("WeatherData");
entity.HasKey(w => w.Id);
entity.HasPartitionKey(w => w.Location);
});
}
}
Create a service to handle weather data operations in WeatherApp.Data/WeatherService.cs
:
using Microsoft.EntityFrameworkCore;
namespace WeatherApp.Data;
public interface IWeatherService
{
Task<List<WeatherForecast>> GetAllForecastsAsync();
Task<WeatherForecast> AddForecastAsync(WeatherForecast forecast);
}
public class WeatherService : IWeatherService
{
private readonly WeatherContext _context;
public WeatherService(WeatherContext context)
{
_context = context;
}
public async Task<List<WeatherForecast>> GetAllForecastsAsync()
{
return await _context.WeatherForecasts.ToListAsync();
}
public async Task<WeatherForecast> AddForecastAsync(WeatherForecast forecast)
{
_context.WeatherForecasts.Add(forecast);
await _context.SaveChangesAsync();
return forecast;
}
}
Add a service registration extension method in WeatherApp.Data/ServiceExtensions.cs
:
using Microsoft.Extensions.DependencyInjection;
namespace WeatherApp.Data;
public static class ServiceExtensions
{
public static IServiceCollection AddWeatherServices(this IServiceCollection services)
{
services.AddScoped<IWeatherService, WeatherService>();
return services;
}
}
Step 17: Configure the API to Use the Data Layer
Add a reference from the API to the Data project:
cd src/WeatherApp.Api
dotnet add reference ../WeatherApp.Data/WeatherApp.Data.csproj
Add the Entity Framework Cosmos package to the API:
cd src/WeatherApp.Api
dotnet add package Aspire.Microsoft.EntityFrameworkCore.Cosmos
Update Program.cs
:
using WeatherApp.Data;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// Add Entity Framework with Cosmos DB - connection is injected automatically by Aspire
builder.AddCosmosDbContext<WeatherContext>("WeatherData");
// Register weather services from the Data project
builder.Services.AddWeatherServices();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.MapDefaultEndpoints();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 18: Create Minimal API Endpoints
Replace the default WeatherForecast endpoint in WeatherApp.Api/Program.cs
with endpoints that use the weather service. Add this code before app.Run()
:
// Weather API endpoint
app.MapGet("/weatherforecast", async (IWeatherService weatherService) =>
{
var forecasts = await weatherService.GetAllForecastsAsync();
return Results.Ok(forecasts);
})
.WithName("GetWeatherForecast");
Remove the existing weather forecast endpoint and summaries array that was generated by the template, and replace it with the service-based endpoints above.
Step 19: Add a Data Seeding Worker Service
Let’s add a worker service to seed initial data and demonstrate Aspire’s ability to orchestrate background services. Create a new worker project:
# From the src directory
cd src
dotnet new worker -n WeatherApp.Seed
dotnet sln WeatherApp.sln add WeatherApp.Seed/WeatherApp.Seed.csproj
Add the necessary references to the seeder:
cd WeatherApp.Seed
dotnet add reference ../WeatherApp.ServiceDefaults/WeatherApp.ServiceDefaults.csproj
dotnet add reference ../WeatherApp.Data/WeatherApp.Data.csproj
dotnet add package Aspire.Microsoft.EntityFrameworkCore.Cosmos
Update WeatherApp.Seed/Program.cs
to integrate with Aspire and register the data services:
using WeatherApp.Seed;
using WeatherApp.Data;
var builder = Host.CreateApplicationBuilder(args);
// Add Aspire service defaults
builder.AddServiceDefaults();
// Add Entity Framework with Cosmos DB
builder.AddCosmosDbContext<WeatherContext>("WeatherData");
// Register weather services from the Data project
builder.Services.AddWeatherServices();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
Update the WeatherApp.Seed/Worker.cs
to use service scoping and application lifetime management:
using WeatherApp.Data;
namespace WeatherApp.Seed;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public Worker(ILogger<Worker> logger, IServiceScopeFactory serviceScopeFactory, IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_hostApplicationLifetime = hostApplicationLifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Weather Seeder starting at: {time}", DateTimeOffset.Now);
using var scope = _serviceScopeFactory.CreateScope();
var weatherService = scope.ServiceProvider.GetRequiredService<IWeatherService>();
// Check if data already exists
var existingForecasts = await weatherService.GetAllForecastsAsync();
if (existingForecasts.Any())
{
_logger.LogInformation("Weather data already exists. Skipping seed.");
_hostApplicationLifetime.StopApplication();
return;
}
// Seed fake weather data
_logger.LogInformation("Seeding weather data...");
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
var cities = new[]
{
"New York, USA", "London, UK", "Tokyo, Japan", "Sydney, Australia", "Paris, France",
"Berlin, Germany", "Toronto, Canada", "Mumbai, India", "São Paulo, Brazil", "Cairo, Egypt",
"Moscow, Russia", "Beijing, China", "Mexico City, Mexico", "Lagos, Nigeria", "Bangkok, Thailand",
"Dubai, UAE", "Singapore", "Buenos Aires, Argentina", "Stockholm, Sweden", "Amsterdam, Netherlands"
};
var forecasts = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
Guid.NewGuid(),
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)],
cities[Random.Shared.Next(cities.Length)]
))
.ToArray();
foreach (var forecast in forecasts)
{
await weatherService.AddForecastAsync(forecast);
_logger.LogInformation("Added forecast: {Date} - {Summary} - {TempC}°C",
forecast.Date, forecast.Summary, forecast.TemperatureC);
}
_logger.LogInformation("Weather Seeder completed seeding {Count} forecasts at: {time}",
forecasts.Length, DateTimeOffset.Now);
// Stop the application after seeding is complete
_hostApplicationLifetime.StopApplication();
}
}
Key Features of this Implementation:
- Service Scoping: Uses
IServiceScopeFactory
to properly resolve scoped services from a singleton worker - Application Lifetime Management: Injects
IHostApplicationLifetime
to stop the application after seeding completes - Realistic Data: Includes random cities from around the world and weather summaries
- Proper Entity Model: Uses
Guid
for the ID and includes location information - Self-Terminating: The worker stops the application after completing its task, perfect for one-time operations
- Idempotent: Checks if data already exists to avoid duplicate seeding
Why this pattern works well:
- The seeder runs once and exits, preventing resource waste
- In the Aspire Dashboard, you’ll see the seeder transition from “Not Started” → “Running” → “Exited”
- Perfect for initialization tasks that should complete and not run continuously
Now add the worker to the AppHost in WeatherApp.AppHost/AppHost.cs
:
var builder = DistributedApplication.CreateBuilder(args);
#pragma warning disable ASPIRECOSMOSDB001
var cosmos = builder.AddAzureCosmosDB("cosmos").RunAsPreviewEmulator(
emulator =>
{
emulator.WithDataExplorer();
});
#pragma warning restore ASPIRECOSMOSDB001
var database = cosmos.AddCosmosDatabase("WeatherDB");
var container = database.AddContainer("WeatherData", "/id");
// Pass Cosmos DB container connection to the API
var api = builder.AddProject<Projects.WeatherApp_Api>("weatherapi")
.WithReference(container);
// Add the data seeding worker service with explicit start
var seeder = builder.AddProject<Projects.WeatherApp_Seed>("weatherseeder")
.WithReference(container)
.WithExplicitStart();
// Add the React frontend using Vite integration
var frontend = builder.AddViteApp("frontend", "../WeatherApp.Web")
.WithNpmPackageInstallation()
.WithReference(api)
.WithEnvironment("BROWSER", "false")
.WithExternalHttpEndpoints();
builder.Build().Run();
Don’t forget to add the project reference from AppHost to the Seed project:
dotnet add src/WeatherApp.AppHost/WeatherApp.AppHost.csproj reference src/WeatherApp.Seed/WeatherApp.Seed.csproj
The WithExplicitStart()
configuration means:
- The seeder won’t start automatically when you run
aspire run
- It will appear in the Aspire Dashboard with a “Not Started” status
- You can manually start it from the dashboard by clicking the “Start” button
- This is perfect for seeding operations that you want to run on-demand rather than every time the application starts
Why Dev Containers Matter 🐳
Using the Aspire Dev Container template provides significant advantages:
Consistency Across Team
Every developer gets the exact same environment:
- ✅ Same .NET SDK version
- ✅ Same Node.js version
- ✅ Same Aspire workload version
- ✅ Same VS Code extensions
- ✅ Same Docker-in-Docker configuration
No more “works on my machine” - the container is the machine.
Zero Setup Time
New team members can go from zero to running the app in minutes:
- Clone the repo
- Open in VS Code
- Click “Reopen in Container”
- Press F5 to run
That’s it. No SDK installations, no workload installations, no environment configuration.
Isolation
The dev container doesn’t pollute your host machine:
- All .NET SDKs, Node.js versions, and tools are isolated
- Docker containers run inside the dev container (not on your host)
- You can work on multiple projects with different requirements simultaneously
Cloud-Ready
The same container configuration that works locally can be used in:
- GitHub Codespaces - Cloud-hosted dev environments
- CI/CD pipelines - Consistent build environments
- Team onboarding - Cloud-based development for new hires
Aspire Roadmap & Future 🔮
Aspire is under active development with an exciting roadmap ahead. The team maintains transparent communication about their plans through official GitHub discussions:
📍 Official Roadmap: Aspire 9 and Beyond Roadmap
📍 Latest Updates: Aspire December 2024 Update
Key Highlights from the Roadmap
🚀 Production Deployment Focus
- Azure Container Apps - First-class deployment support with simplified configuration
- Kubernetes Integration - Generate K8s manifests directly from AppHost definitions
- Azure Developer CLI (azd) - One-command deployment from local development to Azure
- Terraform/Bicep Integration - Infrastructure as Code generation from Aspire definitions
🔧 Enhanced Developer Experience
- Visual Studio Integration - Native Aspire project templates and debugging experience
- Performance Improvements - Faster startup times and reduced resource usage
- Enhanced Debugging - Better cross-service debugging and diagnostics
- Template Ecosystem - More starter templates for common scenarios
🌐 Expanded Integrations
- Cloud Provider Support - AWS and GCP resource integrations beyond Azure
- Database Ecosystem - PostgreSQL, MongoDB, SQL Server, and more
- Message Brokers - RabbitMQ, Apache Kafka, Azure Service Bus
- Observability Tools - Grafana, Prometheus, and other monitoring solutions
🎯 Community & Ecosystem
- Community Contributions - Growing ecosystem of community-built integrations
- Enterprise Features - Enhanced security, compliance, and governance features
- Documentation & Learning - Expanded tutorials, samples, and best practices
The team is actively shipping features across these areas, with regular preview releases and community feedback driving priorities.
Resources & Community 🌍
Official Documentation
- Aspire Documentation - Comprehensive guides and API reference
- Aspire CLI Overview - Complete CLI command reference
- Aspire Templates - All available project templates
- Aspire Samples - Real-world example applications
Community & Learning
- Aspire Discord Server - Active community and direct access to the team
- Aspireify.com - Community-driven Aspire resources and tutorials
- Aspire Fridays - Weekly video series from the Aspire team
GitHub & Contributing
- Aspire GitHub Repository - Source code, issues, and discussions
- Aspire Roadmap - Future plans and priorities
Conclusion 🎉
Aspire transforms local development for distributed applications. What used to require Docker Compose files, environment variables, and manual service management is now:
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddAzureCosmosDB("cosmosdb").RunAsEmulator().AddDatabase("weatherdb");
var api = builder.AddProject<Projects.WeatherApp_Api>("api").WithReference(db);
var frontend = builder.AddNpmApp("frontend", "../WeatherApp.Web").WithReference(api);
builder.Build().Run();
Five lines of code to orchestrate a full-stack application with observability built-in.
What We Built Today
🚀 Development Environment
- ✅ Complete dev container setup with zero manual configuration
- ✅ Aspire CLI and templates for enhanced developer experience
- ✅ Docker-in-Docker for seamless container orchestration
🏗️ Application Architecture
- ✅ Aspire solution with AppHost orchestration and ServiceDefaults
- ✅ .NET 9 Web API with minimal endpoints and Entity Framework Core
- ✅ React TypeScript frontend with Vite integration
- ✅ Azure Cosmos DB with local emulator for development
🔗 Service Integration
- ✅ Vite proxy configuration for seamless API communication
- ✅ Entity Framework Cosmos DB integration with proper partition keys
- ✅ Background worker service for data seeding with explicit start control
- ✅ Cross-service communication through environment variable injection
📊 Observability & Developer Experience
- ✅ Aspire Dashboard with real-time service monitoring
- ✅ Distributed tracing and structured logging
- ✅ Health checks and telemetry across all services
- ✅ One-command startup with
aspire run
Current Limitations & Future Enhancements 🔧
While our application demonstrates the core power of Aspire orchestration, there are some areas where we can further integrate with Aspire’s capabilities:
Frontend Integration Opportunities While our React application works perfectly with Aspire’s orchestration, there are additional integration opportunities we haven’t explored:
- Enhanced Observability: The frontend could send additional telemetry data to provide deeper insights into user interactions and client-side performance
- Health Check Integration: The React app could participate more fully in Aspire’s health monitoring system
- Advanced Configuration: More sophisticated configuration management for client-side settings
What We Have vs. What’s Possible Our current setup demonstrates the core value of Aspire—seamless service orchestration and communication. The frontend communicates with the backend through Aspire’s environment variable injection, and everything works smoothly in development.
Coming Soon In a future post, we’ll explore advanced frontend integration patterns and how to create an even more comprehensive observability story across the entire application stack.
Next Steps
In our next post, we’ll dive into TypeSpec for Contract-First API Development, where we’ll define API contracts that both our backend and frontend can consume with full type safety.
Try It Yourself
Clone the complete example:
git clone https://github.com/two4suited/blog-platform-aspire
cd blog-platform-aspire
aspire run
Have questions or feedback? Join the Aspire Discord or leave a comment below!
Coming Up Next: TypeSpec for Contract-First API Development 📋
More
Recent Posts
- » Building a Modern Development Platform: Aspire for Local Development
- » Building a Modern Development Platform: Tool Selection 🛠️
- » Building a Modern Development Platform: From Legacy .NET Framework to Cloud-Native 🚀
- » 9. Typescript Journey - Express API Docker
- » 8. Typescript Journey - Express API