Getting Started with .NET Aspire Local Dev
If you’ve ever stitched together 2–5 local services, a database, and a queue just to test one feature, you already know the pain. .NET Aspire makes that setup much easier.
You get one app host to orchestrate everything, built-in service discovery, health checks, and a dashboard that actually helps when things go sideways.
Let’s build a small distributed app and run it locally.
What we’re building
A simple setup with:
catalogapi(minimal API)webfrontend(minimal API callingcatalogapi)cache(Redis container)
Aspire will start and wire all of it from one place.
1) Create the starter solution
dotnet workload install aspire
dotnet new aspire-starter -n AspireGettingStarted
cd AspireGettingStarted
This gives you an AppHost project plus service projects.
2) Orchestrate services in AppHost
Open AppHost/Program.cs and wire resources/projects:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var catalogApi = builder.AddProject<Projects.CatalogApi>("catalogapi")
.WithReference(cache);
builder.AddProject<Projects.WebFrontend>("webfrontend")
.WithReference(catalogApi);
builder.Build().Run();
That’s the “local distributed environment” in one file.
3) Add a practical API endpoint with Redis caching
In CatalogApi, add Redis caching support:
dotnet add CatalogApi package Microsoft.Extensions.Caching.StackExchangeRedis
Then use it in CatalogApi/Program.cs:
using Microsoft.Extensions.Caching.Distributed;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("cache");
});
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapGet("/products/{id:int}", async (int id, IDistributedCache cache, CancellationToken ct) =>
{
var key = $"product:{id}";
var fromCache = await cache.GetStringAsync(key, ct);
if (fromCache is not null)
{
return Results.Ok(new { id, name = fromCache, source = "cache" });
}
var name = $"Product {id}";
await cache.SetStringAsync(
key,
name,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
},
ct);
return Results.Ok(new { id, name, source = "api" });
});
app.Run();
4) Call the API from another service
In WebFrontend/Program.cs:
using System.Net.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHttpClient("catalog", client =>
{
client.BaseAddress = new Uri("http://catalogapi");
});
var app = builder.Build();
app.MapDefaultEndpoints();
app.MapGet("/", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("catalog");
var product = await client.GetFromJsonAsync<ProductDto>("/products/1");
return Results.Ok(product);
});
app.Run();
public record ProductDto(int Id, string Name, string Source);
No hardcoded localhost ports between services. Aspire handles discovery.
5) Run everything
dotnet run --project AppHost
You should see Aspire launch your services and the dashboard. Hit webfrontend, then refresh a couple of times—you’ll see source flip from "api" to "cache".
Why this is a great starting point
For local distributed development, Aspire gives you a clean default:
- One command to run the whole app graph
- Consistent wiring between services/resources
- Better observability while developing
- Less “works on my machine” config drift
If you’re starting microservices (or even just “a couple of services”), this is the nicest on-ramp I’ve used in .NET.
