As we move from development to production, deployment complexity often becomes the biggest bottleneck. For Vyshyvanka, we chose .NET Aspire as our development orchestration framework and as the foundation for production-ready deployments. Today, we look at how Aspire simplifies running a multi-service workflow engine.
Why Aspire?
Traditional development setup means manually configuring your API, your Blazor frontend, your database, and ensuring they can discover each other. If one service's port changes, everything breaks. Aspire solves this by treating the entire application as a single, orchestrated 'app model'. This app model describes how your services relate to each other — what resources they need, what ports they share, and how they discover each other via service discovery.
Our AppHost
The Vyshyvanka.AppHost project defines our application topology in C#:
var builder = DistributedApplication.CreateBuilder(args);
// Check if PostgreSQL mode is enabled
var usePostgres = builder.Configuration["UsePostgres"]?
.Equals("true", StringComparison.OrdinalIgnoreCase) == true;
IResourceBuilder<ProjectResource> api;
if (usePostgres)
{
// Production: PostgreSQL with persistent volume
var postgres = builder.AddPostgres("postgres")
.WithDataVolume("vyshyvanka-postgres-data");
var database = postgres.AddDatabase("vyshyvankadb");
api = builder.AddProject<Projects.Vyshyvanka_Api>("api")
.WithReference(database)
.WaitFor(database);
}
else
{
// Development: SQLite (no external dependencies)
api = builder.AddProject<Projects.Vyshyvanka_Api>("api");
}
// Designer discovers API via service discovery
builder.AddProject<Projects.Vyshyvanka_Designer>("designer")
.WithReference(api)
.WaitFor(api);
builder.Build().Run();
This is actual production code from our repository. A few things to note:
- Conditional infrastructure: The same AppHost supports both SQLite (for quick local dev) and PostgreSQL (for production-like environments). A single environment variable flips the switch.
- Service discovery: The Designer automatically discovers the API endpoint — no hard-coded URLs in configuration files.
-
Dependency ordering:
WaitForensures the API doesn't start until the database is ready, and the Designer doesn't start until the API is accepting requests.
Development Experience
Running the full stack locally is a single command:
dotnet run --project src/Vyshyvanka.AppHost
This starts:
- The PostgreSQL container (if configured) with a persistent data volume
- The API service with proper connection strings injected
- The Blazor WebAssembly Designer with API endpoint configured
The Aspire dashboard gives you a unified view of all services, their logs, and distributed traces — all without any additional configuration.
Service Defaults
The Vyshyvanka.ServiceDefaults project provides shared configuration that every service gets automatically:
- Health checks — standardized readiness and liveness probes
- OpenTelemetry — distributed tracing and metrics collection
- Resilience — default HTTP client resilience policies
- Service discovery — automatic endpoint resolution
Each service opts in with a single line: builder.AddServiceDefaults().
Containerization
Aspire is built on a container-first mindset. In development, it manages Docker containers for infrastructure (PostgreSQL). For production deployment, the same application model can generate deployment manifests for container orchestrators.
Because our application is already decomposed into well-defined services with explicit dependencies, the transition from local development to containerized production is straightforward:
- The API and Designer are standard .NET web applications — they containerize with the default .NET SDK container support.
- Database connection strings are injected via environment variables, whether running locally or in a container.
- Service discovery works the same way in both environments.
Environment-Specific Configuration
We handle the dev/prod split cleanly through the AppHost configuration:
| Environment | Database | Auth Provider | Credential Storage |
|---|---|---|---|
| Development | SQLite (file) | Built-in (seeded users) | Built-in (AES-256) |
| Staging | PostgreSQL (container) | Keycloak/Authentik | Built-in (AES-256) |
| Production | PostgreSQL (managed) | OIDC provider | Vault/OpenBao |
The application code is identical across all environments. Only the infrastructure wiring changes.
Observability Built In
One of the biggest wins with Aspire is built-in observability. Through ServiceDefaults, every HTTP request, database query, and cross-service call is automatically traced with OpenTelemetry. When a workflow fails in production, you can trace the entire request path through the system using standard observability tools (Jaeger, Zipkin, Application Insights, etc.).
Deployment Consistency
The 'it works on my machine' problem is eliminated. With Aspire, your infrastructure is defined as code alongside your logic. The topology you test locally is the same topology that runs in production. You don't manage shell scripts to wire up services — you manage the C# model that defines your application architecture.
By using Aspire as our orchestration layer, we have drastically reduced the time it takes to go from a local feature to a testable deployment.
In the next part, we will discuss Part 14: Community and Ecosystem - Contributing to Vyshyvanka. Stay tuned!
Check out the project source code here: https://github.com/homolibere/Vyshyvanka
Top comments (0)