Reusable extraction of the core orchestration technique using a C# source file as the Aspire DistributedApplication host (via SmallSharp), stable dev tunnels, port pinning, dependency ordering with WaitFor, a JavaScript frontend via AddJavaScriptApp, and a .slnx for one-click F5 in Visual Studio and �spire run.
No project-specific names, IDs, ports, or dependencies are included.
- Copy files preserving the api/ and web/ layout.
- Add the Aspire wrapper to your .slnx.
- dotnet restore
- aspire run or F5 the Aspire entry.
`csharp #:sdk Aspire.AppHost.Sdk@13.3.5 #:package Aspire.Hosting.DevTunnels #:package Aspire.Hosting.JavaScript #:package ThisAssembly.Project
var builder = DistributedApplication.CreateBuilder(args);
const int apiPort = 5000; const int webPort = 3000;
var emulator = builder.AddExecutable("emulator", "node", builder.AppHostDirectory, "-e", "setInterval(() => {}, 3600_000);");
var baseDir = AppContext.GetData("EntryPointFileDirectoryPath") as string ?? Path.GetFullPath(Path.Combine(ThisAssembly.Project.MSBuildProjectDirectory, ".."));
var api = builder.AddProject("api", Path.Combine(baseDir, "Api/Api.csproj")) .WithEndpoint("http", endpoint => { endpoint.Port = apiPort; endpoint.UriScheme = "http"; }) .WaitFor(emulator);
builder.AddDevTunnel("api-tunnel", tunnelId: "app-api") .WithReference(api.GetEndpoint("http"), allowAnonymous: true);
var web = builder.AddJavaScriptApp("web", Path.Combine(baseDir, "Web")) .WithHttpEndpoint(port: webPort, env: "PORT", name: "http") .WaitFor(api);
builder.AddDevTunnel("web-tunnel", tunnelId: "app-web") .WithReference(web.GetEndpoint("http"), allowAnonymous: true);
builder.Build().Run();
`
`xml Exe net10.0 true
`
`xml
`
`json { "appHost": { "path": "src/apphost.cs" }, "profiles": { "https": { "applicationUrl": "https://localhost:17101;http://localhost:15214", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21231", "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23181", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22092" } }, "http": { "applicationUrl": "http://localhost:15214", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development", "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19043", "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18106", "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20036", "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" } } }, "features": { "defaultWatchEnabled": "true" } }
`
`xml net10.0
`
`csharp using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenTelemetry; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. public static class ServiceDefaults { public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder { builder.ConfigureOpenTelemetry(); builder.AddDefaultHealthChecks(); builder.Services.AddServiceDiscovery(); builder.Services.ConfigureHttpClientDefaults(http => { http.AddStandardResilienceHandler(); http.AddServiceDiscovery(); }); return builder; }
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation();
})
.WithTracing(tracing =>
{
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation();
if (bool.TryParse(builder.Configuration["OTEL_CONSOLE"], out var console) && console)
tracing.AddConsoleExporter();
});
builder.AddOpenTelemetryExporters();
return builder;
}
static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]))
{
builder.Services.AddOpenTelemetry().UseOtlpExporter();
}
return builder;
}
public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
{
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
return builder;
}
public static WebApplication MapDefaultEndpoints(this WebApplication app)
{
if (app.Environment.IsDevelopment())
{
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
}
return app;
}
}
`
`csharp var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); var app = builder.Build(); app.MapDefaultEndpoints(); app.Run();
`
`json { "name": "web", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start" }, "dependencies": { "next": "^15", "react": "^19", "react-dom": "^19" }, "devDependencies": { "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", "typescript": "^5" } }
`
` s import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'export', trailingSlash: false }; export default nextConfig;
`
The file-based Aspire AppHost orchestrates local development for the API and frontend apps:
- Starts local development dependencies (via AddExecutable) and waits for them with WaitFor.
- Starts the API project (pinned port) and JS frontend together.
- Pins the API to a fixed port and the web app to a fixed port so the persistent dev tunnels remain valid across runs.
- Re-hosts the stable dev tunnels with anonymous access (�llowAnonymous: true).
- Waits for the API readiness before starting dependent apps (frontend).
Run it with:
shell aspire run
- WithEndpoint + explicit Port + UriScheme pins the HTTP listener for tunnel stability.
- AddJavaScriptApp("web", path).WithHttpEndpoint(port: NNNN, env: "PORT", name: "http") wires the PORT convention.
- AddDevTunnel(name, tunnelId: "stable-id") gives you a persistent public URL during dev.
- The �aseDir + ThisAssembly.Project + SmallSharp dance resolves the location of sibling projects correctly when the AppHost entrypoint is a loose .cs file.
- Include the Aspire wrapper .csproj (SmallSharp) in your .slnx for IDE launch.
- �spire.config.json next to the solution (or at repo root) tells Aspire where the file-based host lives and configures profiles/features.
- For production, the same API and web projects are deployed without the AppHost (the AppHost is dev-time only).