Skip to content

.NET Aspire Integration

.NET Aspire is a cloud-ready stack for building and running observable, distributed .NET applications. Cargonerds uses it as the single entry point for running the whole system locally (one dotnet run brings up SQL Server, Redis, RabbitMQ, the .NET services and the Node frontends) and as the publishing model for Azure (azd reads the Aspire manifest to provision Container Apps / App Service plus the backing Azure resources).

ABP ships first-class guidance for this layout; see ABP's .NET Aspire integration for the framework's take on orchestrating an ABP solution with Aspire.

Version pinning

The Aspire package version is pinned by AspireVersion in common.props (currently 13.2.2) and flows through Directory.Packages.props. The AppHost MSBuild SDK is pinned separately in src/Cargonerds.AppHost/Cargonerds.AppHost.csproj (<Sdk Name="Aspire.AppHost.Sdk" Version="13.1.0" />). All projects target net10.0.

Two projects own the integration:

Project Role Marker
src/Cargonerds.AppHost The distributed-application host / orchestrator. Declares every resource and wires their dependencies, endpoints, env vars, health checks and start order. <IsAspireHost>true</IsAspireHost>, OutputType=Exe
src/Cargonerds.ServiceDefaults The shared cross-cutting host setup referenced by every service. AddServiceDefaults() adds OpenTelemetry, health checks, HTTP resilience, service discovery and the multi-layer config pipeline. <IsAspireSharedProject>true</IsAspireSharedProject>

A heavily-used helper package is Nextended.Aspire (10.1.0, pinned in Directory.Packages.props). Several extension methods used in the AppHost (EnsureDockerRunningIfLocalDebug, WithExplicitStart/WithExplicitStartIf, GetFirstExistingEndpoint, AddJavaScriptApp's NPM-workspace discovery) come from there rather than from the Aspire SDK itself.

flowchart TD
    AppHost["Cargonerds.AppHost<br/>(Program.cs)"]

    subgraph infra["Infrastructure resources"]
        DB[("sqlserver / db<br/>spark-db :14330")]
        REDIS[("redis<br/>spark-redis")]
        MQ[("messaging<br/>spark-rabbitmq")]
    end

    subgraph svc[".NET service projects"]
        MIG["db-migrator"]
        AUTH["auth :44345"]
        API["api :44354"]
        ADMIN["admin (Blazor) :44381"]
    end

    subgraph fe["Frontends (auto-discovered)"]
        RT["realtime :4200"]
    end

    DOCS["documentation<br/>(MkDocs, explicit start)"]

    AppHost --> infra
    AppHost --> svc
    AppHost --> fe
    AppHost --> DOCS

    MIG -- WaitForCompletion --> AUTH
    MIG -- WaitForCompletion --> API
    AUTH -- WaitFor --> API
    API -- WaitFor --> ADMIN
    API -- WaitFor --> RT

    DB --> MIG
    DB --> AUTH
    DB --> API
    REDIS --> AUTH
    REDIS --> API
    MQ --> AUTH
    MQ --> API

The AppHost

src/Cargonerds.AppHost/Program.cs is a top-level program that builds the entire resource graph and ends with:

builder.Build().EnsureDockerRunningIfLocalDebug().Run();

EnsureDockerRunningIfLocalDebug() (from Nextended.Aspire) fails fast with a clear message if Docker Desktop is not running during a local debug session.

Resource names are centralised in CargonerdsConsts.Aspire.Service (src/Cargonerds.Domain.Shared/CargonerdsConsts.cs) — a nested class hierarchy whose instances each carry a Name:

Field Name
UiLib @cargonerds/ui-library
Auth auth
Public web
Admin admin
Api api
Redis redis
Messaging messaging
Migrator db-migrator
Documentation documentation

CargonerdsConsts.Aspire.Directories holds AppHost-relative paths (SolutionRoot = "../../", Frontends = ../../frontend, Documentation = ../../docs), and CargonerdsConsts also defines InternalServicePassword = "UnsecurePassword" used as the static password for internal-only services.

Run mode vs publish mode

The single Program.cs dual-targets two modes, branching on builder.ExecutionContext.IsRunMode / IsPublishMode:

  • Run mode — local dotnet run / F5. Real containers, fixed local ports, the spark-DB-vs-container decision, dev speed-up toggles.
  • Publish modeazd / manifest generation for Azure Container Apps / App Service. SQL/Redis/RabbitMQ resolve to their Azure equivalents and service URLs are computed from DEPLOYMENT_DOMAIN.

The local helper IfRunMode(...) in Extensions/ResourceBuilderExtensions.cs is just builder.If(ExecutionContext.IsRunMode, action) and is used throughout to gate run-mode-only wiring.

Parameters and secrets

Two Aspire parameters are declared up front:

var unsecurePassword = builder.AddParameter(
    nameof(CargonerdsConsts.InternalServicePassword),
    CargonerdsConsts.InternalServicePassword);   // Redis + RabbitMQ
var dbPassword = builder.AddParameter("dbPassword", "securePassword-2026!");  // SQL container

What it provisions

Database — branches on SparkDbRunModeConfiguration (see below):

  • If a spark-DB run-mode config is present (a local AppHost pointed at an Azure DB), the Default connection string is supplied literally via builder.AddConnectionString("Default", …).
  • Otherwise an Azure SQL Server is declared and run as a container locally:
builder.AddAzureSqlServer("sqlserver")
    .RunAsContainer(s => s
        .WithEndpoint("tcp", e => { e.Port = 14330; e.IsProxied = false; })
        .WithDataVolume("spark-db")
        .WithContainerName("spark-db")
        .WithLifetime(ContainerLifetime.Persistent)
        .WithPassword(dbPassword))
    .AddDatabase("db")
    .WithDefaultAzureSku();

The container uses a fixed, un-proxied TCP endpoint on port 14330, a persistent data volume and a persistent lifetime, and is exposed to services as the Default connection string.

Redis — Azure Redis run as a container, bundled with RedisInsight and Redis Commander for inspection, a persistent spark-redis data volume and the shared internal password:

#pragma warning disable CS0618 // Switching to AzureManagedRedis increases costs
builder.AddAzureRedis(CargonerdsConsts.Aspire.Service.Redis.Name)
    .WithAccessKeyAuthentication()
    .RunAsContainer(o => o
        .WithPassword(unsecurePassword)
        .WithRedisInsight(...).WithRedisCommander(...)
        .WithDataVolume(name: "spark-redis")
        .WithContainerName("spark-redis")
        .WithLifetime(ContainerLifetime.Persistent));
#pragma warning restore CS0618

AddAzureRedis is obsolete on purpose

The call is wrapped in #pragma warning disable CS0618 with an explicit comment ("Switching to AzureManagedRedis increases costs"). Do not "fix" the warning by switching to AddAzureManagedRedis.

RabbitMQ — the messaging resource, management plugin enabled, persistent, container-named only in run mode:

builder.AddRabbitMQ(CargonerdsConsts.Aspire.Service.Messaging.Name, password: unsecurePassword)
    .WithManagementPlugin()
    .WithExternalHttpEndpoints()
    .WithLifetime(ContainerLifetime.Persistent)
    .IfRunMode(c => c.WithContainerName("spark-rabbitmq"))
    .PublishAsContainer();

.NET service projectsdb-migrator, auth, api and admin (Blazor), each registered with the local AddProjectWithDefaults helper:

var migrator      = builder.AddProjectWithDefaults<Cargonerds_DbMigrator>(CargonerdsConsts.Aspire.Service.Migrator);
var authServer    = builder.AddProjectWithDefaults<Cargonerds_AuthServer>(CargonerdsConsts.Aspire.Service.Auth);
var apiHost       = builder.AddProjectWithDefaults<Cargonerds_HttpApi_Host>(CargonerdsConsts.Aspire.Service.Api);
var adminFrontend = builder.AddProjectWithDefaults<Cargonerds_Blazor>(CargonerdsConsts.Aspire.Service.Admin);

AddProjectWithDefaults is the project equivalent of "give me an Aspire project that knows it is running under the AppHost": it calls AddProject<TProject>(service.Name, launchProfile) and always sets USE_ASPIRE_CONFIG=true so the child loads its appsettings.aspire.json (see Cross-service wiring). The launch profile defaults to the project file name. (Cargonerds_* are the Aspire source-generated Projects.* metadata types.)

public static IResourceBuilder<ProjectResource> AddProjectWithDefaults<TProject>(
    this IDistributedApplicationBuilder builder,
    CargonerdsConsts.Aspire.Service service,
    string? launchProfileName = null)
    where TProject : IProjectMetadata, new() =>
    builder
        .AddProject<TProject>(service.Name, launchProfileName ?? ProjectName<TProject>())
        .WithEnvironment(EnvVars.UseAspireConfig, "true");

Frontends — every subfolder of frontend/ that contains a Dockerfile is auto-discovered by AddAllFrontends() (→ AddAllNpmAppsInPath) and registered as a JavaScript app (currently only realtime). For each app the AppHost generates the NPM-workspaces package.json, picks the start script (first script whose name contains aspire or start, else start), and wires:

builder.AddJavaScriptApp(appName, app, startScript)
    .WithEnvironment("BROWSER", "none")
    .WithHttpEndpoint(port: fixedPort, env: "PORT", name: "http")   // realtime pinned to 4200 in run mode
    .WithGenerateProxyCommandCommand()                              // dashboard "Generate Proxies" command
    .WithExternalHttpEndpoints()
    .WithHttpHealthCheck("/", 200)
    .WithOtlpExporter()
    .PublishAsDockerFile(...)                                       // ui-library workspace ⇒ build context = frontend/
    .IfRunMode(b => b.WithEnvironment("NODE_TLS_REJECT_UNAUTHORIZED", "0"));

appName is normalised by EscapeProjectname (strip a leading cargonerds-, lowercase, dash-separate). In publish mode, frontends in IgnoredOnDeploy (["angular"]) are skipped. Back in Program.cs each frontend additionally gets WaitFor(apiHost), WithNextAuth(apiHost, whiteLabelOptions, authServer) and WithExplicitStartIf(explicitFrontendStart). See Realtime frontend for the NextAuth env-var contract.

Documentation — this MkDocs site, built from docs/Dockerfile and started on demand:

builder.AddDockerfile(CargonerdsConsts.Aspire.Service.Documentation.Name,
                      CargonerdsConsts.Aspire.Directories.Documentation, "Dockerfile")
    .WithLifetime(ContainerLifetime.Persistent)
    .WithHttpEndpoint(port: builder.ExecutionContext.IsPublishMode ? 80 : 8001, targetPort: 8000, name: "http")
    .WithExternalHttpEndpoints()
    .WithExplicitStart();   // must be started manually from the dashboard

Fixed local ports

Run-mode ports are hard-coded in src/Cargonerds.AppHost/RunModeServicePorts.cs:

Service Port Scheme
Auth 44345 HTTPS
Api 44354 HTTPS
Admin (Blazor) 44381 HTTPS
Realtime 4200 HTTP
Db (DbPort) 53938

Fixed ports collide if already bound

RunModeServicePorts hard-codes 44345 / 44354 / 44381 / 4200, and the SQL container's TCP 14330 is un-proxied (IsProxied = false). A stray local SQL Server on 14330, or anything already bound to those HTTPS ports, will prevent the AppHost from starting cleanly.

How services are wired

Start order and health gates

Each project's WaitFor / WaitForCompletion calls are conditioned by two dev-speed-up env vars, read once at the top of Program.cs:

  • SKIP_WAITING_IN_BACKEND (EnvVars.SkipWaitingInBackend) → skip backend health/completion waits.
  • EXPLICIT_FRONTEND_START (EnvVars.ExplicitFrontendStart) → require a manual start of the frontends in the dashboard.

The wiring per resource:

  • migratorWaitFor(db), WithReference(db, "Default"); if the spark-DB config requested validation-only it sets VALIDATE_MIGRATIONS_ONLY=true.
  • adminFrontend (Blazor) — run-mode HTTPS on 44381, external endpoints, WithHttpHealthCheck("/", 200), WaitForIf(!skip, authServer, apiHost), explicit-start gate, WithWhiteLabelSettingsPath().
  • authServer — run-mode HTTPS 44345, external, WaitFor(redis, rabbitmq, db), white-label path, WaitForCompletionIf(!skip, migrator), WithReference(db, "Default").
  • apiHost — run-mode HTTPS 44354 and (unless skipping) an HTTP health check against the ABP endpoint /api/abp/application-configuration; external; WaitForIf(!skip, authServer); WaitFor(redis, rabbitmq, db); WithHealthStatusCheck(); WithWhiteLabelSettingsPath(); WaitForCompletionIf(!skip, migrator); WithReference(db, "Default"); and PassConfigurationValue(SPARK_ENVIRONMENT) to forward the host's SPARK_ENVIRONMENT to the child.

The local WaitFor(params IResourceBuilder<IResource>?[]) helper is null-tolerant (resources.WhereNotNull().Aggregate(...)), so passing an optional/absent resource is safe.

WithHealthStatusCheck() adds the API's /health-status HTTP check (200), sets App__HealthCheckUrl, and registers dashboard URLs including /health-ui:

builder.WithHttpHealthCheck("/health-status", 200);
builder.WithEnvironment("App__HealthCheckUrl", "/health-status");
// TODO: Enable Health UI but following code is only working locally.
builder
    .WithUrlForEndpoint("https", url => url.DisplayText = $"{builder.Resource.Name.ToPascalCase()}-Home")
    .WithUrlForEndpoint("https", _ => new() { Url = "/health-ui", DisplayText = "Health UI" });

Health UI is local-only (TODO)

The Health-UI URL wiring in WithHealthStatusCheck carries a code comment that it only works locally; treat it as a dashboard convenience, not a deployed endpoint.

Cross-service wiring (service discovery)

After all resources are declared, Program.cs fans out references so every service can resolve the others:

var projects = new List<IResourceBuilder<ProjectResource>> { authServer, apiHost, adminFrontend };
IResourceBuilder<IResourceWithEndpoints>[] servicesWithEndpoints = [.. frontends];
IResourceBuilder<IResourceWithConnectionString>[] servicesWithConnectionString = [rabbitmq, redis];

foreach (var project in projects.Append(migrator))
{
    project.WithServiceReference(projects);              // project → project
    project.WithServiceReference(servicesWithEndpoints); // project → frontends
    foreach (var service in servicesWithConnectionString)
        project.WithReference(service);                 // rabbitmq + redis connection strings
}

WithServiceReference is the producer side of service discovery. It sets an env var services__{targetName}__https__0 whose value is mode-dependent:

var configPath = $"services__{target.Resource.Name}__https__0";
return builder.ApplicationBuilder.ExecutionContext.IsPublishMode
    ? builder.WithEnvironment(configPath, GetDeploymentDomain(target))          // https://{name}.{DEPLOYMENT_DOMAIN}
    : builder.WithEnvironment(configPath, target.GetFirstExistingEndpoint());   // resolved local endpoint

The consumer side lives in each child's committed appsettings.aspire.json (these files are static, not generated). For example src/Cargonerds.HttpApi.Host/appsettings.aspire.json:

{
  "App": { "SelfUrl": "{api}", "AllowedCorsOrigins": "{realtime}", "CorsOrigins": "{realtime},{admin}", "Realtime": "{realtime}" },
  "AuthServer": { "Authority": "{auth}", "MetaAddress": "{auth}", "WellKnownConfigAddress": "{auth}/.well-known/openid-configuration" },
  "Redis": { "Configuration": "{redis}" },
  "RabbitMQ": { "Connections": { "Default": { "HostName": "{messaging}", "Override": true } } }
}

At child startup the custom ServiceDiscoveryConfigurationProvider (registered by AddServiceDiscoveryConfiguration()) walks every config value, and for each {name} token resolves the first of:

  1. services:{name}:https:0
  2. services:{name}:http:0
  3. ConnectionStrings:{name}

If none exist it throws an InvalidOperationException that lists the available services — good DX, but a hard failure. The token regex is \{([\w-]+)\}.

Service-discovery tokens fail fast at child startup

A {name} token in any appsettings.aspire.json value that has no matching services:* endpoint or ConnectionStrings:* entry will crash the child on boot — not just disable a single feature. If you add a token, remember to wire the corresponding resource in the AppHost.

Note the AppHost emits services__{name}__https__0 (double-underscore env-var form) while the provider reads services:{name}:https:0 — these are the same key in .NET configuration, just the env-var vs colon spellings.

Frontend env wiring (WithNextAuth)

Extensions/NodeAppExtensions.cs injects the NextAuth contract into each JavaScriptAppResource:

return builder
    .WithSelfEnvironmentHttpsEndpoint("APP_URL")
    .WithEnvironmentHttpsEndpoint("API_URL", api)
    .WithEnvironmentHttpsEndpoint("AuthServer__Authority", authServer)
    .WithEnvironment("AUTH_CLIENT_ID", builder.Resource.Name)
    .WithEnvironment("MAPTILER_API_KEY", mapTilerApiKey);

So OIDC and API calls from the React app resolve to the orchestrated endpoints. AUTH_CLIENT_ID is literally the resource name, and MAPTILER_API_KEY is read from WhiteLabelingOptions.ExternalApis.MapTiler.ApiKey. WithEnvironmentHttpsEndpoint itself is mode-aware: publish mode emits https://{name}.{DEPLOYMENT_DOMAIN}, run mode resolves the live endpoint via GetFirstExistingEndpoint().

Service defaults

Cargonerds.ServiceDefaults exposes AddServiceDefaults() (src/Cargonerds.ServiceDefaults/HostApplicationBuilderExtensions.cs, namespaced Microsoft.Extensions.Hosting so it is discoverable without an extra using). Every host calls it as the first builder step:

public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder)
    where TBuilder : IHostApplicationBuilder
{
    builder.AddExtraConfigFiles();              // multi-layer config pipeline (see below)
    builder.AddServiceDiscoveryConfiguration(); // registers ServiceDiscoveryConfigurationSource

    builder.ConfigureOpenTelemetry();

    builder.Services.AddServiceDiscovery();

    builder.Services.ConfigureHttpClientDefaults(http =>
    {
        http.AddStandardResilienceHandler();    // retries, circuit breaker, timeouts
        http.AddServiceDiscovery();             // resolve service names in HttpClients
    });

    return builder;
}

It bundles four cross-cutting concerns:

Concern What AddServiceDefaults does
Service discovery AddServiceDiscovery() for the DI container + the custom config source that resolves {name} tokens (above).
HTTP resilience ConfigureHttpClientDefaults adds the standard resilience handler (retry, circuit breaker, timeouts) to all HttpClients, plus service discovery so logical names resolve.
Health checks MapDefaultEndpoints maps /health and /alive (see below).
OpenTelemetry logging, metrics and traces with an OTLP exporter (see below).

OpenTelemetry

ConfigureOpenTelemetry enables all three signals:

  • LoggingAddOpenTelemetry with IncludeFormattedMessage + IncludeScopes.
  • Metrics — ASP.NET Core + HttpClient + Runtime instrumentation.
  • Tracing — source = builder.Environment.ApplicationName, ASP.NET Core + HttpClient instrumentation (gRPC instrumentation is present but commented out).

Exporters (AddOpenTelemetryExporters) are conditional and can both be active:

if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]))
    builder.Services.AddOpenTelemetry().UseOtlpExporter();

var aiConn = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
if (!string.IsNullOrEmpty(aiConn))
    builder.Services.AddOpenTelemetry().UseAzureMonitor(o => o.ConnectionString = aiConn);

The AppHost injects OTEL_EXPORTER_OTLP_ENDPOINT into each resource, which is what makes the dashboard's trace and metric views light up locally; APPLICATIONINSIGHTS_CONNECTION_STRING adds Azure Monitor in the cloud.

Health endpoints

MapDefaultEndpoints (called explicitly, e.g. in Cargonerds.AuthServer/Program.cs) maps the generic endpoints only in Development, with a security note in the code:

if (app.Environment.IsDevelopment())
{
    app.MapHealthChecks("/health");                                                   // all checks
    app.MapHealthChecks("/alive", new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") });
}

These are distinct from the API's custom /health-status and /health-ui endpoints wired by WithHealthStatusCheck in the AppHost.

The config pipeline (AddExtraConfigFiles)

AddServiceDefaults also layers Cargonerds' bespoke configuration files under ABP's config system (because hosts call AddServiceDefaults() before the ABP module bootstrap). In order, each file optional:

  1. appsettings.aspire.json — only when USE_ASPIRE_CONFIG=true.
  2. appsettings.azure[.<part>...].json — when AZURE_ENVIRONMENT is set, via AddAzureJsonFiles, which expands the dotted environment name cumulatively and appends a ConfigurationParameterReplacementSource ({token} substitution from the ConfigurationParameters section).
  3. WHITE_LABEL_SETTINGS_PATH (default appsettings.rohlig.json).
  4. appsettings.spark.{env}.json + appsettings.spark.{env}.user.json (reload-on-change), where env = SparkEnvironment.FromName(SPARK_ENVIRONMENT). SparkEnvironment is also registered as a DI singleton.

This pipeline — the two token engines, USE_ASPIRE_CONFIG, AZURE_ENVIRONMENT vs SPARK_ENVIRONMENT, and the ConfigurationParameters replacement — is documented end-to-end in Configuration reference and Deployment configuration.

Environment variables the AppHost reads and forwards

src/Cargonerds.AppHost/EnvVars.cs is the single source of truth for env-var names and typed accessors:

Variable Read by Purpose
USE_ASPIRE_CONFIG child services Load appsettings.aspire.json. Set automatically by AddProjectWithDefaults. Mandatory under the AppHost.
SPARK_ENVIRONMENT api host (forwarded) Selects the Hub DB / blob storage / service bus (the SPARK data source).
VALIDATE_MIGRATIONS_ONLY migrator Validate migrations without applying them; fails if open migrations exist.
WHITE_LABEL_SETTINGS_PATH child services Absolute path to appsettings.rohlig.json (run mode only).
APPHOST_AZURE_ENVIRONMENT AppHost Selects which Azure settings the AppHost loads for spark-DB run mode.
APPHOST_AZURE_DATABASE_COPY_SUFFIX AppHost Appended to spark-db-name so a local AppHost runs against a copy of an Azure DB.
SKIP_WAITING_IN_BACKEND AppHost Skip backend health/completion waits to speed up local starts.
EXPLICIT_FRONTEND_START AppHost Frontends require manual start in the dashboard.
DEPLOYMENT_DOMAIN AppHost (publish) Public domain used to compute service URLs; throws if missing in publish mode.

USE_ASPIRE_CONFIG is mandatory for orchestrated services

Per the EnvVars.UseAspireConfig doc comment: "If not set the default ABP configuration will be used which is not compatible with the Aspire AppHost." The AppHost always sets it for the four AddProjectWithDefaults projects; if you run a host standalone (IIS Express / dotnet run outside the AppHost) you must set it yourself or you'll get the non-Aspire config.

AZURE_ENVIRONMENT (child) vs APPHOST_AZURE_ENVIRONMENT (host)

These are different keys for different layers. The host key selects which Azure settings the AppHost loads for spark-DB run mode; the child key selects which appsettings.azure.* the service loads.

Running against an Azure spark DB

SparkDbRunModeConfiguration.Create lets a local AppHost point at a real Azure spark DB (or a renamed copy) instead of spinning up a SQL container. It runs only in run mode and only when APPHOST_AZURE_ENVIRONMENT is set. It loads appsettings.azure[.parts].json from the ServiceDefaults folder, optionally renames the DB by appending APPHOST_AZURE_DATABASE_COPY_SUFFIX to ConfigurationParameters:spark-db-name, then resolves the Default connection string through the ConfigurationParameters replacement.

Guard rails (verified in source):

  • prod.* environments require the copy-suffix — you cannot accidentally run local code against the real prod DB.
  • Missing spark-db-name (when a suffix is used) or a missing resolved Default connection string throws.
  • ValidateMigrationsOnly is set to true exactly when no copy-suffix is used — i.e. you're pointed at a shared DB, so the migrator must only validate, never apply.
return new SparkDbRunModeConfiguration(
    defaultConnectionString,
    string.IsNullOrWhiteSpace(azureDatabaseCopySuffix));   // ValidateMigrationsOnly

The dashboard

Running the AppHost launches the Aspire dashboard automatically; its URL is printed to the console (the port is assigned by Aspire, not fixed). From it you can:

  • View the health/state of every resource and its dependency graph.
  • Read live logs and structured traces and metrics (powered by the OTLP exporter that AddServiceDefaults wires up).
  • Inspect the environment variables and endpoints passed to each resource.
  • Manually start on-demand resources such as documentation, and (if EXPLICIT_FRONTEND_START) the frontends.
  • Run the per-frontend "Generate Proxies" command, which executes ABP's abp generate-proxy -t ng against the frontend/ directory (Helper/CommandHelper.cs, surfaced via WithGenerateProxyCommandCommand).

Running with Aspire

dotnet run --project src/Cargonerds.AppHost

Docker Desktop must be running (EnsureDockerRunningIfLocalDebug()). See Running locally and Debugging for the local workflow, the dev speed-up toggles and the "Generate Proxies" command.

The manifest and deployment

Aspire also drives publishing. In publish mode the AppHost emits a manifest that azd consumes: SQL Server, Redis and RabbitMQ are declared with AddAzureSqlServer / AddAzureRedis / AddRabbitMQ(...).PublishAsContainer(), so they provision Azure equivalents (or containers); service URLs are computed from DEPLOYMENT_DOMAIN; and frontends listed in IgnoredOnDeploy (angular) are omitted. The full flow — azure.yaml, Container Apps vs App Service, the spark-DB-vs-container decision and CI/CD — is covered in:

Local ↔ Azure mapping

Resource Run mode (local) Publish mode (Azure)
SQL Server spark-db container, TCP 14330 (un-proxied), persistent volume — or an Azure spark DB via SparkDbRunModeConfiguration Azure SQL (AddAzureSqlServer + WithDefaultAzureSku)
Redis spark-redis container + RedisInsight + Redis Commander Azure Cache for Redis (AddAzureRedis, access-key auth)
RabbitMQ spark-rabbitmq container + management plugin Container (PublishAsContainer)
Service URLs (services__*, API_URL, …) GetFirstExistingEndpoint() (live local endpoint) https://{resourceName}.{DEPLOYMENT_DOMAIN}
Frontends all frontend/* with a Dockerfile same, minus IgnoredOnDeploy (angular)
Documentation :8001 → target 8000, explicit start :80 → target 8000
OTel exporter OTLP → dashboard Azure Monitor (APPLICATIONINSIGHTS_CONNECTION_STRING) + OTLP if configured

ABP integration notes

  • Bootstrap order. Every host (HttpApi.Host, AuthServer, Blazor) calls builder.AddServiceDefaults() before the ABP module bootstrap (await builder.AddApplicationAsync<…Module>() / app.InitializeApplicationAsync()), alongside ABP's UseAutofac() and AddAppSettingsSecretsJson(). Aspire config and OpenTelemetry are therefore layered in under ABP's configuration system. See ABP modularity and dependency injection.
  • Readiness via ABP. The API's run-mode readiness probe hits ABP's /api/abp/application-configuration endpoint.
  • Auto-generated client proxies. The dashboard "Generate Proxies" command runs ABP's dynamic proxy generation / abp generate-proxy -t ng for the React frontend.
  • ABP DbMigrator orchestration. Cargonerds.DbMigrator is an ABP migrator hosted as an Aspire project whose completion gates the API and Auth servers (see Migrations).
  • Redis distributed cache. Cargonerds.HttpApi.Host calls builder.AddRedisClient("redis") immediately after AddServiceDefaults(), feeding ABP's distributed cache (StackExchange.Redis); the {redis} token in appsettings.aspire.json supplies the connection string. RabbitMQ similarly backs ABP's distributed event bus. See Caching and Messaging.
  • OpenIddict / auth. The AppHost compile-links OpenIddictAppConfiguration.cs and references Volo.Abp.OpenIddict.Pro.Domain.Shared; the React frontend's NextAuth is pointed at the ABP OpenIddict auth server authority via AuthServer__Authority + AUTH_CLIENT_ID.

DbMigrator does NOT use AddServiceDefaults()

Cargonerds.DbMigrator/Program.cs manually re-implements the config layering (USE_ASPIRE_CONFIGappsettings.aspire.json, AddAzureJsonFiles(env, "azure.migrator") with an extra azure.migrator prefix for DB-admin credentials, and the service-discovery source). As a result it has no OpenTelemetry / resilience / health checks from ServiceDefaults, and its config pipeline can drift from the shared one. It is a generic Host worker unless run with --enable-api (then a slim WebApplication exposing a token-guarded /migration/status); it also honours --disable-redis.

Gotchas

Committed secrets

src/Cargonerds.ServiceDefaults/appsettings.azure.*.json contain real SQL / Redis / RabbitMQ credentials checked into the repo (e.g. a live SQL password and a redis.cache.windows.net access key in appsettings.azure.dev.json). These are copied to build output and to publish. Treat this as a real secret-exposure risk; any rotated values must not be re-committed. (The same concern applies to the spark and white-label files — see Configuration reference.)

  • Three sources of extension methods. Helpers used in Program.cs come from three places: the local AppHost/Extensions/ResourceBuilderExtensions.cs (IfRunMode, WaitFor, WithHealthStatusCheck, WithServiceReference, …); Nextended.Aspire (EnsureDockerRunningIfLocalDebug, WithExplicitStart/WithExplicitStartIf, GetFirstExistingEndpoint, AddJavaScriptApp); and Nextended.Core.Extensions (If/Apply). The rest (RunAsContainer, PublishAsContainer, WithDefaultAzureSku) are Aspire built-ins. When tracing a method, check all three.
  • Filename typo. The AppHost has ConfigruationExtensions.cs (sic — the misspelling is in the filename); search by that exact spelling.
  • RedisResourceExtensions.ConfigureRedis is effectively dead. It targets a plain RedisResource, but Program.cs uses AddAzureRedis(...).RunAsContainer(...) inline and never calls it.
  • appsettings.aspire.json files are committed per service (HttpApi.Host, AuthServer, Blazor, Web.Public, DbMigrator). They are the static consumer half of service discovery — not generated by the AppHost.