Sitemap

.NET Aspire: A Real-World Implementation — Transforming Microservices Development

6 min readJun 11, 2025

--

How we modernized our enterprise microservices architecture and achieved seamless local development, enhanced observability, and simplified orchestration

Introduction

Managing complex microservices architectures has become one of the most challenging aspects of modern software development. After struggling with the typical pain points — complex local setups, configuration drift, poor observability, and dependency management nightmares — our team decided to implement .NET Aspire across our entire distributed system.

The results exceeded our expectations. What used to take new developers hours to set up now takes minutes. Debugging distributed issues went from detective work to straightforward troubleshooting. Most importantly, our development experience went from frustrating to genuinely enjoyable.

This article shares our journey implementing .NET Aspire in a production microservices system, the challenges we overcame, and the patterns we discovered along the way.

What is .NET Aspire?

.NET Aspire is Microsoft’s opinionated, cloud-ready stack for building observable, production-ready distributed applications. It provides:

What is .NET Aspire?

.NET Aspire isn’t just another framework — it’s a complete paradigm shift in how we think about distributed application development. Microsoft has created something truly revolutionary:

  • Orchestration: Simplified management of service startup, discovery, and connectivity
  • Components: Pre-built integrations with popular services (databases, message brokers, caching)
  • Observability: Built-in telemetry, logging, and health monitoring
  • Tooling: Rich developer experience with Visual Studio and VS Code integration

The Challenge: Our Pre-Aspire Reality

Our distributed system consisted of six interconnected microservices:

  • Order Service: Core business logic orchestration
  • Integration Service: External system integration layer
  • User Service: Customer intelligence and relationship management
  • Payment Service: Enterprise system connectivity
  • Analytics Service: Analytics and recommendation engine
  • File Service: Document and content distribution service

The Pain Points We Faced:

  1. Complex Local Setup: New developers needed 2–3 hours just to get their environment running
  2. Configuration Drift: Inconsistent environment configurations between development, testing, and production
  3. Poor Observability: Scattered logs and metrics made debugging distributed issues extremely challenging
  4. Manual Service Discovery: Hardcoded URLs and manual endpoint management
  5. Dependency Chaos: Services failed to start in the correct order, causing cascading failures

The Solution: Implementing .NET Aspire

Step 1: Creating the AppHost Project

The AppHost is the orchestration center for your entire distributed system — a console application that manages service startup, configuration, and dependencies.

<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.1" />
  <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.Azure.Storage" />
<PackageReference Include="Aspire.Hosting.RabbitMQ" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
</ItemGroup>
</Project>

Step 2: Infrastructure as Code with Aspire

One of Aspire’s most powerful features is defining infrastructure declaratively within your application configuration:

public class Program
{
public static void Main(string[] args)
{
var builder = DistributedApplication.CreateBuilder(args);
        // SQL Server with persistent data
var sqlServer = builder.AddSqlServer("sqlserver")
.WithLifetime(ContainerLifetime.Persistent)
.WithHostPort(1435)
.WithDataVolume();
// Create databases for each service domain
var orderDatabase = sqlServer.AddDatabase("OrderDatabase");
var orderEventDatabase = sqlServer.AddDatabase("OrderEventDatabase");
var userDatabase = sqlServer.AddDatabase("UserDatabase");
var userEventDatabase = sqlServer.AddDatabase("UserEventDatabase");
// Message broker setup
var rabbitMqUsername = builder.AddParameter("rabbitmq-username", "guest");
var rabbitMqPassword = builder.AddParameter("rabbitmq-password", "guest", secret: true);
var rabbitMq = builder.AddRabbitMQ("message-broker", rabbitMqUsername, rabbitMqPassword)
.WithEndpoint(5673, 5672, name: "amqp")
.WithEndpoint(15672, 15672, name: "management");
// Azure Storage emulator for local development
var azureStorage = builder.AddAzureStorage("storage")
.RunAsEmulator()
.AddBlobs("blobs");
builder.Build().Run();
}
}

Step 3: Service Registration and Dependencies

Each microservice is registered with its dependencies and environment configuration:

// Order Service - Core orchestration service
var orderService = builder.AddProject<Projects.Order_Service>("order-service")
.WithReference(orderDatabase)
.WithReference(orderEventDatabase)
.WithReference(rabbitMq)
.WithExternalHttpEndpoints();
// Configure environment variables
orderService.WithEnvironment("ConnectionStrings__Database", GetDatabaseConnectionString("OrderDatabase"))
.WithEnvironment("ConnectionStrings__EventDatabase", GetDatabaseConnectionString("OrderEventDatabase"))
.WithEnvironment("MessageBroker__Host", "amqp://localhost:5673")
.WithEnvironment("MessageBroker__Username", "guest")
.WithEnvironment("MessageBroker__Password", "guest")
.WaitFor(sqlServer)
.WaitFor(rabbitMq);
// User Service with service dependencies
var userService = builder.AddProject<Projects.User_Service>("user-service")
.WithReference(userDatabase)
.WithReference(userEventDatabase)
.WithReference(rabbitMq)
.WithReference(orderService) // Service-to-service reference
.WithReference(integrationService);
userService.WithEnvironment("ConnectionStrings__Database", GetDatabaseConnectionString("UserDatabase"))
.WithEnvironment("ConnectionStrings__EventDatabase", GetDatabaseConnectionString("UserEventDatabase"))
.WithEnvironment("ExternalApi__ApiKey", Environment.GetEnvironmentVariable("ExternalApiKey"))
.WithEnvironment("ExternalApi__BaseUrl", Environment.GetEnvironmentVariable("ExternalApiUrl"))
.WaitFor(sqlServer)
.WaitFor(rabbitMq)
.WaitFor(orderService);

Step 4: ServiceDefaults for Consistency

Create a shared ServiceDefaults project to ensure consistent configuration across all services:

public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.AddBasicServiceDefaults();
        // Add service discovery
builder.Services.AddServiceDiscovery();
// Configure HTTP clients with resilience and service discovery
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
return builder;
}
public static IHostApplicationBuilder AddBasicServiceDefaults(this IHostApplicationBuilder builder)
{
// Health checks
builder.AddDefaultHealthChecks();
// OpenTelemetry for observability
builder.ConfigureOpenTelemetry();
return builder;
}
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation();
});
return builder;
}
}

Step 5: Service Integration

The beauty of Aspire is that existing services require minimal changes to integrate:

// Program.cs in each microservice
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
        // Add Aspire service defaults
builder.AddServiceDefaults();
// Your existing service configuration
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Database")));
builder.Services.AddMassTransit(x =>
{
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("MessageBroker"));
});
});
var app = builder.Build(); // Map Aspire default endpoints (health checks, metrics)
app.MapDefaultEndpoints();
// Your existing middleware and endpoints
app.UseRouting();
app.MapControllers();
await app.RunAsync();
}
}

Step 6: Environment Management and Secrets

Aspire provides elegant solutions for managing different environments and secrets:

// Environment-specific configuration
public static void ConfigureEnvironmentVariables(IResourceBuilder<ProjectResource> project)
{
project.WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development")
.WithEnvironment("IsLocalDevelopment", "true");
}
// Secrets management using parameters
var apiKey = builder.AddParameter("external-api-key", secret: true);
var databasePassword = builder.AddParameter("database-password", secret: true);
// Use secrets in service configuration
identityNexus.WithEnvironment("ExternalApi__ApiKey", apiKey)
.WithEnvironment("ConnectionStrings__Database",
$"Server=localhost,1435;Database=IdentityDB;Password={databasePassword};");

Step 7: Advanced Configuration Patterns

Reusable Configuration Helpers:

// Helper function for database connection strings
string GetDatabaseConnectionString(string databaseName) =>
$"Server=localhost,1435;Database={databaseName};User Id=sa;Password={builder.Configuration["Parameters:sqlserver-password"]};Encrypt=False;TrustServerCertificate=true;";
// Common environment configuration
Action<IResourceBuilder<ProjectResource>> ConfigureCommonEnvironments = project =>
project.WithEnvironment("IsDevelopment", "true")
.WithEnvironment("IsLocalDevelopment", "true")
.WithEnvironment("MessageBroker__Host", "amqp://localhost:5673")
.WithEnvironment("MessageBroker__Username", "guest")
.WithEnvironment("MessageBroker__Password", "guest");
// Apply configuration to services
ConfigureCommonEnvironments(velocityEngine);
ConfigureCommonEnvironments(identityNexus);

Step 8: Cross-Service Correlation Tracking

Implement correlation ID tracking to trace requests across your distributed system:

public class CorrelationMiddleware
{
private readonly RequestDelegate _next;
    public CorrelationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = GetOrCreateCorrelationId(context);
// Add to response headers
context.Response.OnStarting(() =>
{
context.Response.Headers.TryAdd("X-Correlation-ID", correlationId);
return Task.CompletedTask;
});
// Add to logging scope
using var scope = context.RequestServices
.GetRequiredService<ILogger<CorrelationMiddleware>>()
.BeginScope(new { CorrelationId = correlationId });
await _next(context);
}
private string GetOrCreateCorrelationId(HttpContext context)
{
if (context.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
return correlationId!;
return Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString();
}
}

Testing with Aspire

Aspire provides excellent testing capabilities through Aspire.Hosting.Testing:

public class IntegrationTests
{
[Test]
public async Task IdentityNexus_ReturnsCustomerData_Successfully()
{
// Arrange
var appHost = await DistributedApplicationTestingBuilder
.CreateAsync<Program>();
        await using var app = await appHost.BuildAsync();
await app.StartAsync();
// Act
var httpClient = app.CreateHttpClient("identity-nexus");
await app.ResourceNotifications.WaitForResourceHealthyAsync("identity-nexus");
var response = await httpClient.GetAsync("/api/customers/premium-user-123"); // Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}
}

Results and Benefits

The transformation results were significant across multiple dimensions:

Developer Experience

  • Setup Time: Reduced from 2–3 hours to 5 minutes for new developers
  • Configuration Errors: Eliminated environment-specific configuration issues
  • Service Dependencies: Automatic service startup ordering and dependency management

Observability

  • Unified Logging: Centralized logs with correlation IDs across all services
  • Distributed Tracing: Complete request flow visibility across service boundaries
  • Health Monitoring: Real-time service health status and automatic recovery

Infrastructure Management

  • Environment Consistency: Identical development environments across all machines
  • Resource Management: Automatic container lifecycle management
  • Service Discovery: Eliminated hardcoded URLs and manual endpoint management

Performance and Reliability

  • Circuit Breakers: Built-in resilience patterns for external service calls
  • Load Balancing: Automatic load distribution across service instances
  • Health Checks: Proactive service health monitoring and alerting

Best Practices and Lessons Learned

1. Start Small, Scale Gradually

Begin with a subset of services and gradually migrate your entire system. This approach allows you to learn Aspire patterns without overwhelming complexity.

2. Leverage ServiceDefaults Effectively

Create comprehensive ServiceDefaults to ensure consistency across all services. Include logging, health checks, and telemetry configuration.

3. Environment Configuration Strategy

Use Aspire parameters for secrets and environment-specific values. Create helper functions for reusable configuration patterns.

4. Service Dependencies Management

Carefully design service dependencies using WithReference() and WaitFor() to ensure proper startup ordering and avoid circular dependencies.

5. Testing Strategy

Implement integration tests using Aspire.Hosting.Testing to validate service interactions and configuration correctness.

Common Pitfalls to Avoid

1. Over-Configuring Services

Avoid adding unnecessary environment variables. Aspire handles many configurations automatically through service references.

2. Ignoring Container Lifetime

Choose appropriate container lifetimes (Session vs Persistent) based on your data persistence requirements.

3. Complex Service Graphs

Keep service dependency graphs simple. Consider using event-driven patterns for complex inter-service communication.

4. Missing Health Checks

Always implement proper health checks in your services. Aspire relies on these for orchestration decisions.

Future Considerations

As .NET Aspire continues to evolve, consider these upcoming features and patterns:

  • Azure Integration: Enhanced cloud deployment capabilities
  • Kubernetes Support: Native Kubernetes manifest generation
  • Advanced Telemetry: More sophisticated observability features
  • Service Mesh Integration: Integration with service mesh technologies

Conclusion

.NET Aspire has fundamentally transformed our approach to microservices development. The combination of simplified orchestration, enhanced observability, and improved developer experience has made our team significantly more productive while reducing operational complexity.

The investment in migrating to Aspire paid dividends almost immediately. New team members can now be productive on day one, debugging distributed issues is no longer a nightmare, and our local development environment perfectly mirrors our production setup.

If you’re working with .NET microservices and struggling with similar challenges, I highly recommend exploring .NET Aspire. Start with a pilot project, learn the patterns, and gradually expand your implementation. The developer experience improvements alone make the investment worthwhile.

Additional Resources

Have you implemented .NET Aspire in your projects? Share your experiences and lessons learned in the comments below. I’d love to hear about your journey and any unique challenges you’ve overcome.

--

--

No responses yet