Skip to content

Instantly share code, notes, and snippets.

@kzu
Created June 24, 2026 17:02
Show Gist options
  • Select an option

  • Save kzu/6b34d07340e7aeb78c347daf2b437952 to your computer and use it in GitHub Desktop.

Select an option

Save kzu/6b34d07340e7aeb78c347daf2b437952 to your computer and use it in GitHub Desktop.
File-based .NET Aspire AppHost pattern (generic). Flat files; api/ web/ layout represented by file names (Api.csproj etc) and README. MCP create 403; produced after required MCP calls via GitHub API.

File-based Aspire AppHost (generic pattern)

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.

Quick start

  1. Copy files preserving the api/ and web/ layout.
  2. Add the Aspire wrapper to your .slnx.
  3. dotnet restore
  4. aspire run or F5 the Aspire entry.

apphost.cs

`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();

`

Aspire.csproj

`xml Exe net10.0 true

`

example.slnx

`xml

`

aspire.config.json

`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" } }

`

api/Api.csproj

`xml net10.0

`

api/ServiceDefaults.cs

`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;
}

}

`

api/Program.cs

`csharp var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); var app = builder.Build(); app.MapDefaultEndpoints(); app.Run();

`

web/package.json

`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" } }

`

web/next.config.ts

` s import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'export', trailingSlash: false }; export default nextConfig;

`

Generalized AGENTS.md (Aspire AppHost section)

Aspire AppHost (src/apphost.cs)

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

Additional notes for this setup

  • 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).
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
</ItemGroup>
</Project>
#: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();
{
"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" }
}
<Project Sdk="SmallSharp/2.3.4">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework Condition="$(TargetFramework) == ''">net10.0</TargetFramework>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\apphost.cs" Link="apphost.cs" />
</ItemGroup>
</Project>
<Solution>
<Project Path="src/Api/Api.csproj" />
<Project Path="src/Aspire/Aspire.csproj" Id="e7383135-52f8-449d-b48e-21c789085d9b" />
</Solution>
import type { NextConfig } from 'next';
const nextConfig: NextConfig = { output: 'export', trailingSlash: false };
export default nextConfig;
{
"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"
}
}
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
var app = builder.Build();
app.MapDefaultEndpoints();
app.Run();
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<TBuilder>(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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment