Skip to content

aspire run of a TS apphost runs even if there are compile errors #16645

@eerhardt

Description

@eerhardt

Drop the following TS apphost in https://github.com/dotnet/eshop:

// eShop Aspire TypeScript AppHost
// Translated from eShop.AppHost/Program.cs
// For more information, see: https://aspire.dev

import { createBuilder, ContainerLifetime, QueryParameterMatchMode } from './.modules/aspire.js';

const builder = await createBuilder();

await builder.addDockerComposeEnvironment("env");

// Infrastructure
const redis = await builder.addRedis("redis");
const rabbitMq = await builder.addRabbitMQ("eventbus")
    .withLifetime(ContainerLifetime.Persistent);
const postgres = await builder.addPostgres("postgres")
    .withImage("ankane/pgvector", { tag: "latest" })
    .withLifetime(ContainerLifetime.Persistent);

const catalogDb = await postgres.addDatabase("catalogdb");
const identityDb = await postgres.addDatabase("identitydb");
const orderDb = await postgres.addDatabase("orderingdb");
const webhooksDb = await postgres.addDatabase("webhooksdb");

// For test use only.
// Looks for an environment variable that forces the use of HTTP for all the endpoints.
// We are doing this for ease of running the Playwright tests in CI.
function shouldUseHttpForEndpoints(): boolean {
    const envValue = process.env["ESHOP_USE_HTTP_ENDPOINTS"];
    return envValue === "1";
}

const launchProfileName = shouldUseHttpForEndpoints() ? "http" : "https";

// Services
const identityApi = await builder.addProject("identity-api", "../Identity.API/Identity.API.csproj", launchProfileName)
    .withExternalHttpEndpoints()
    .withReference(identityDb)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

const identityEndpoint = await identityApi.getEndpoint(launchProfileName);

const basketApi = await builder.addCSharpApp("basket-api", "../Basket.API/Basket.API.csproj")
    .withReference(redis)
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withEnvironment("Identity__Url", identityEndpoint)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");
await redis.withParentRelationship(basketApi);

const catalogApi = await builder.addCSharpApp("catalog-api", "../Catalog.API/Catalog.API.csproj")
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withReference(catalogDb)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

const orderingApi = await builder.addCSharpApp("ordering-api", "../Ordering.API/Ordering.API.csproj")
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withReference(orderDb).waitFor(orderDb)
    .withHttpHealthCheck({ path: "/health" })
    .withEnvironment("Identity__Url", identityEndpoint)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

await builder.addCSharpApp("order-processor", "../OrderProcessor/OrderProcessor.csproj")
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withReference(orderDb)
    .waitFor(orderingApi) // wait for the orderingApi because it contains the EF migrations
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

await builder.addCSharpApp("payment-processor", "../PaymentProcessor/PaymentProcessor.csproj")
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

const webHooksApi = await builder.addCSharpApp("webhooks-api", "../Webhooks.API/Webhooks.API.csproj")
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withReference(webhooksDb)
    .withEnvironment("Identity__Url", identityEndpoint)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

// Reverse proxies
await builder.addYarp("mobile-bff")
    .withExternalHttpEndpoints()
    .withConfiguration(async (yarp) => {
        const catalogCluster = await yarp.addClusterFromResource(catalogApi);

        await yarp.addRoute("/catalog-api/api/catalog/items", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/by", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/{id}", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/by/{name}", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/withsemanticrelevance/{text}", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/withsemanticrelevance", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/type/{typeId}/brand/{brandId?}", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/type/all/brand/{brandId?}", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/catalogTypes", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/catalogBrands", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        await yarp.addRoute("/catalog-api/api/catalog/items/{id}/pic", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }])
            .withTransformPathRemovePrefix("/catalog-api");

        // Generic catalog catch-all route
        await yarp.addRoute("/api/catalog/{*any}", catalogCluster)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1", "2.0"], mode: QueryParameterMatchMode.Exact }]);

        // Ordering routes
        const orderingEndpoint = await orderingApi.getEndpoint("http");
        await yarp.addRoute("/api/orders/{*any}", orderingEndpoint)
            .withMatchRouteQueryParameter([{ name: "api-version", values: ["1.0", "1"], mode: QueryParameterMatchMode.Exact }]);

        // Identity routes
        const identityHttpEndpoint = await identityApi.getEndpoint("http");
        await yarp.addRoute("/identity/{*any}", identityHttpEndpoint)
            .withTransformPathRemovePrefix("/identity");
    });

// Apps
const webhooksClient = await builder.addProject("webhooksclient", "../WebhookClient/WebhookClient.csproj", { launchProfileOrOptions: launchProfileName })
    .withReference(webHooksApi)
    .withEnvironment("IdentityUrl", identityEndpoint)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

let webAppBuilder = builder.addProject("webapp", "../WebApp/WebApp.csproj", { launchProfileOrOptions: launchProfileName })
    .withExternalHttpEndpoints()
    .withUrlForEndpoint("http", async (url) => { url.displayText = "Online Store (http)"; });

if (launchProfileName === "https") {
    webAppBuilder = webAppBuilder.withUrlForEndpoint("https", async (url) => { url.displayText = "Online Store (https)"; });
}

const webApp = await webAppBuilder
    .withReference(basketApi)
    .withReference(catalogApi)
    .withReference(orderingApi)
    .withReference(rabbitMq).waitFor(rabbitMq)
    .withEnvironment("IdentityUrl", identityEndpoint)
    .withEnvironment("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "true");

// Set to true if you want to use OpenAI
const useOpenAI = false;
if (useOpenAI) {
    const openAI = await builder.addAzureOpenAI("openai");

    const chat = await openAI.addDeployment("chatModel", "gpt-4.1-mini", "2025-04-14")
        .withProperties(async (d) =>
        {
            d.deploymentName.set("gpt-4.1-mini");
            d.skuName.set("GlobalStandard");
            d.skuCapacity.set(50);
        });
    const textEmbedding = await openAI.addDeployment("textEmbeddingModel", "text-embedding-3-small", "1")
        .withProperties(async (d) =>
        {
            d.deploymentName.set("text-embedding-3-small");
            d.skuCapacity.set(20);
        });

    await catalogApi.withReference(textEmbedding);
    await webApp.withReference(chat);
}

// Ollama configuration (disabled by default)
// NOTE: CommunityToolkit Ollama integration is not yet available in the TypeScript SDK.
// Set to true if you want to use Ollama (requires manual integration)
const useOllama = false;
if (useOllama) {
    // TODO: addOllama is not available in the TypeScript SDK yet.
    // In C#: builder.AddOllama("ollama").WithDataVolume().WithGPUSupport().WithOpenWebUI()
    // const embeddings = ollama.AddModel("embedding", "all-minilm");
    // const chat = ollama.AddModel("chat", "llama3.1");
}

// Wire up the callback urls (self referencing)
const webAppEndpoint = await webApp.getEndpoint(launchProfileName);
const webhooksClientEndpoint = await webhooksClient.getEndpoint(launchProfileName);
await webApp.withEnvironment("CallBackUrl", webAppEndpoint);
await webhooksClient.withEnvironment("CallBackUrl", webhooksClientEndpoint);

// Identity has a reference to all of the apps for callback urls, this is a cyclic reference
const basketHttpEndpoint = await basketApi.getEndpoint("http");
const orderingHttpEndpoint = await orderingApi.getEndpoint("http");
const webHooksHttpEndpoint = await webHooksApi.getEndpoint("http");
await identityApi
    .withEnvironment("BasketApiClient", basketHttpEndpoint)
    .withEnvironment("OrderingApiClient", orderingHttpEndpoint)
    .withEnvironment("WebhooksApiClient", webHooksHttpEndpoint)
    .withEnvironment("WebhooksWebClient", webhooksClientEndpoint)
    .withEnvironment("WebAppClient", webAppEndpoint);

await builder.build().run();

Note that there is an error on line 35:

const launchProfileName = shouldUseHttpForEndpoints() ? "http" : "https";

// Services
const identityApi = await builder.addProject("identity-api", "../Identity.API/Identity.API.csproj", launchProfileName)

The call to addProject doesn't take a string on the last parameter, it takes an AddProjectOptions instance, so the line of code is supposed to be

const identityApi = await builder.addProject("identity-api", "../Identity.API/Identity.API.csproj", { launchProfileOrOptions: launchProfileName })

When you aspire run the incorrect apphost, it runs and the dashboard starts, but a bunch of services don't start, and there is no indication why:

Image

Looking at the console logs of each looks like:

Image

I would expect some sort of error indicating that there is a compile error in my apphost. Maybe we could put a banner at the top of the dashboard saying the apphost doesn't compile.

cc @davidfowl

Metadata

Metadata

Assignees

Labels

area-app-testingIssues pertaining to the APIs in Aspire.Hosting.Testing

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions