How to Build a .NET 10 Microservices Architecture Using .NET Aspire, Docker and C# (2026)

Aspire gives a .NET 10 solution a single place to describe which services run together, how they find each other, and which dependencies should be healthy before a service starts. In this lab, a storefront resolves a catalog API by name. The catalog uses a Redis container, and both applications report telemetry to the local Aspire dashboard.

What runs in the application?

AppHost starts Catalog, Storefront and Redis. Storefront resolves Catalog by service name.
AppHost starts Catalog, Storefront and Redis. Storefront resolves Catalog by service name.

The AppHost describes the application graph. Catalog and Storefront are two ASP.NET Core projects. Redis is a Docker-backed resource. Service Defaults is shared configuration for service discovery, HTTP resilience, health endpoints, and telemetry. Each service remains independently runnable if you supply equivalent endpoint configuration outside Aspire.

Set up the solution

Install .NET 10, the Aspire CLI or Aspire templates, and a supported Docker-compatible runtime. Create a project-based Aspire starter application. Keep its AppHost and Service Defaults projects, then create two ASP.NET Core empty projects named Catalog and Storefront with HTTP launch profiles. Remove the starter projects and corresponding AppHost references if you started with a sample front end. Add references from AppHost to both services and from each service to the generated Service Defaults project. Add the Aspire Redis hosting integration package to AppHost and the Aspire StackExchange Redis client integration to Catalog. Use compatible package versions from the same Aspire release line.

The AppHost SDK generates Projects.Catalog and Projects.Storefront from project references. If the names differ in your solution, use the generated names. Keep the generated Service Defaults extension methods, including the standard HTTP resilience and discovery registration. Aspire project resources · Aspire repository

Reproduce the project graph from the CLI

This example uses the Aspire 13.6.4 project templates and Redis integrations. Install the Aspire CLI with the official installation instructions. Recent project templates can depend on the CLI bundle for local orchestration. In a fresh directory, use the SDK and commands below. Run the two service templates with their generated launch profiles so the AppHost discovers the HTTP endpoints. Put the three code files from this guide in their respective projects as shown.

BASH
dotnet new install Aspire.ProjectTemplates::13.6.4
dotnet new aspire -n AspireShop
cd AspireShop
dotnet new web -n Catalog -f net10.0
dotnet new web -n Storefront -f net10.0
dotnet add Catalog reference AspireShop.ServiceDefaults
dotnet add Storefront reference AspireShop.ServiceDefaults
dotnet add AspireShop.AppHost reference Catalog Storefront
dotnet add AspireShop.AppHost package Aspire.Hosting.Redis --version 13.6.4
dotnet add Catalog package Aspire.StackExchange.Redis --version 13.6.4
dotnet build AspireShop.AppHost

Save the host as AspireShop.AppHost/AppHost.cs and the service files as Catalog/Program.cs and Storefront/Program.cs. Add both services to the solution if you want them visible alongside AppHost in Visual Studio. Start the stack with aspire run from the solution directory. In an environment without the CLI bundle, you can set AspireUseCliBundle to false temporarily in the AppHost project for a compile-only check. Restore the default for this lab’s run instructions. Use the dashboard link printed by the host to find Storefront’s actual port. Using a hardcoded port for another service would bypass the discovery feature this example demonstrates.

Define startup and service discovery

AppHost.cs
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var catalog = builder.AddProject<Projects.Catalog>("catalog")
    .WithReference(cache)
    .WaitFor(cache)
    .WithHttpHealthCheck("/ready");
builder.AddProject<Projects.Storefront>("storefront")
    .WithReference(catalog)
    .WaitFor(catalog)
    .WithExternalHttpEndpoints();
builder.Build().Run();

WithReference supplies connection or endpoint information. WaitFor is a startup dependency. Catalog registers a readiness route, and the AppHost uses it to decide when the storefront may start. A healthy startup does not guarantee the dependency will remain available. The storefront still needs runtime failure handling.

Implement Catalog and Storefront

Catalog.Program.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;
using StackExchange.Redis;

var b = WebApplication.CreateBuilder(args);
b.AddServiceDefaults();
b.AddRedisClient("cache");
b.Services.AddHealthChecks().AddCheck<CacheReady>("redis");
var app = b.Build();
app.MapDefaultEndpoints();
app.MapHealthChecks("/ready", new() { Predicate = _ => true });
app.MapGet("/products/{id:int}", async (int id, IConnectionMultiplexer redis) =>
{
    if (id != 42) return Results.NotFound();
    var db = redis.GetDatabase();
    var name = await db.StringGetAsync("demo:product:42");
    if (name.IsNull)
    {
        name = "USB-C cable";
        await db.StringSetAsync("demo:product:42", name, TimeSpan.FromMinutes(1));
    }
    return Results.Ok(new Product(id, name.ToString()));
});
app.Run();

sealed record Product(int Id, string Name);
sealed class CacheReady(IConnectionMultiplexer redis) : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            await redis.GetDatabase().PingAsync().WaitAsync(
                TimeSpan.FromSeconds(2), cancellationToken);
            return HealthCheckResult.Healthy();
        }
        catch (Exception e) when (e is RedisException or TimeoutException)
        {
            return HealthCheckResult.Unhealthy("Cache unavailable.");
        }
    }
}

The Redis entry is disposable data and has a one-minute expiration. A real catalog needs a durable source of truth and a decision about how to handle an unavailable cache. This lab intentionally includes Redis in readiness so you can observe the startup dependency.

Storefront.Program.cs
using System.Net;

var b = WebApplication.CreateBuilder(args);
b.AddServiceDefaults();
b.Services.AddHttpClient("catalog", c =>
    c.BaseAddress = new Uri("https+http://catalog"));
var app = b.Build();
app.MapDefaultEndpoints();
app.MapGet("/items/{id:int}", async Task<IResult> (
    int id, IHttpClientFactory clients, CancellationToken ct) =>
{
    try
    {
        using var response = await clients.CreateClient("catalog")
            .GetAsync($"/products/{id}", ct);
        if (response.StatusCode == HttpStatusCode.NotFound)
            return Results.NotFound();
        if (!response.IsSuccessStatusCode)
            return Results.StatusCode(503);
        var product = await response.Content.ReadFromJsonAsync<Product>(ct);
        return product is null ? Results.StatusCode(502) : Results.Ok(product);
    }
    catch (Exception e) when (e is HttpRequestException
        or Polly.Timeout.TimeoutRejectedException
        or Polly.CircuitBreaker.BrokenCircuitException)
    {
        return Results.StatusCode(503);
    }
});
app.Run();

sealed record Product(int Id, string Name);

The URI scheme tells service discovery to prefer HTTPS and allow an HTTP endpoint where configured. The name catalog matches the AppHost resource. The generated defaults add an HTTP resilience pipeline. Treat this call as a safe read. Review retry settings before reusing the same pipeline for write endpoints. ASP.NET PB HTTP resilience guide

Run the app and troubleshoot

Start Docker and run the AppHost. Follow the dashboard to the Storefront endpoint and request /items/42. Expect a JSON product with ID 42. Request /items/43 and expect 404. Inspect the catalog and storefront logs, then look for an HTTP trace linking the two requests. Stop Catalog and observe how the storefront handles a failed dependency. Use the dashboard to restart the service. These are expected results for the full stack.

Symptom First check
AppHost cannot start Redis Docker connection, image availability, local resources.
Storefront cannot resolve catalog AppHost resource name, WithReference, Service Defaults registration.
Catalog never becomes ready Redis connection reference, health endpoint path, application logs.
Service is healthy but calls fail Downstream route, scheme, authentication, deadline and network policy.

Separate liveness, readiness and runtime failure

A liveness probe answers whether restarting the process is likely to help. A readiness probe answers whether the instance should receive traffic. Redis failure can affect readiness if the current service depends on Redis for useful work. Including Redis in liveness can cause healthy application processes to restart together during a cache outage. Decide whether the cache is mandatory or optional, then align the probe and the service’s runtime behavior.

Inspect the dashboard’s resource events first if an application never starts. If the application starts and fails only when requested, compare a successful trace with a failing trace. A discovery error before the outbound call points to resource naming or endpoint configuration. A client span with a timeout points to network latency or downstream capacity. A downstream span with an exception points to application behavior. Once deployed, send traces to a persistent backend and avoid including customer identifiers in service names or high-cardinality tags.

Can I deploy the AppHost as-is?

Treat this graph as a description of the system. Choose an Aspire deployment integration or map the services to your target platform. Supply production secrets, DNS, health checks, ingress, and an external telemetry destination. Do not assume Docker-based local topology or the dashboard’s development access controls transfer to production.

Does Aspire replace Docker? Aspire describes and coordinates resources. Docker or a compatible engine runs the container in this lab.

Does discovery make a service public? A reference provides service endpoint information. Ingress and network exposure are deployment decisions.

Should every dependency delay startup? Reserve startup gates for dependencies a service actually needs to initialize. Plan how each service behaves when a dependency fails later.

References and related reading

Comments