# Console and worker apps

Console applications and background workers can participate in an Aspire application regardless of the language they're written in. Add them to your AppHost to manage their lifecycle alongside your other resources, provide connection strings and service endpoints, and view their console output in the dashboard.

## Adding a console app to the AppHost

Choose the resource type that matches how your app runs. Use an executable resource for a command-line program, a Python app resource for a script or module, or a JavaScript app resource for a Node.js app. For a .NET console app or Worker Service project, use a project resource. None of these apps needs to expose an HTTP endpoint to be orchestrated by Aspire.

<LearnMore>
  For more information on executable, Python, and JavaScript resources, see
  [Executable resources](/app-host/executable-resources/), [Python
  integration](/integrations/frameworks/python/), and [JavaScript
  integration](/integrations/frameworks/javascript/).
</LearnMore>

The following examples show how to add a .NET or Node.js console app, connect it to other resources, and run it as a background worker. The examples use three sets of tabs, which you choose independently:

- **Aspire CLI**, **apphost.cs (C# file-based app)**, and **PackageReference(*.csproj)** tabs select an installation method for Aspire integrations. If you're using a TypeScript AppHost, only the **Aspire CLI** tab is applicable.
- **.NET** and **Node.js** tabs select the language of the console app or executable that the AppHost orchestrates.
- **C#** and **TypeScript** tabs select the language of the AppHost itself.

For example, to orchestrate a Node.js console app from a C# AppHost, choose any installation method, then **Node.js**, and then **C#**. Your selections stay the same as you move through the page.

These examples assume an existing AppHost.

- **.NET:** The examples also assume a shared service defaults project. Create the named console, worker, and API projects before referencing them. In a C# AppHost, add project references so the corresponding `Projects.*` types are generated; in a TypeScript AppHost, use each project's actual path.
- **Node.js:** Create each app folder with a _package.json_ file before referencing it. The AppHost resolves the app folder path relative to the AppHost directory, and the script path relative to the app folder.

For the Redis examples, install the hosting integration in your AppHost. Running Redis also requires a supported [container runtime](/get-started/prerequisites/).

<InstallPackage packageName="Aspire.Hosting.Redis" />

<InstallPackage packageName="Aspire.Hosting.Redis" cliOnly />

Add the console app project to your AppHost:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

// Add a console app project to the AppHost
var processor = builder.AddProject<Projects.MyConsoleApp>("processor")
    .WithReference(cache);

builder.Build().Run();
```

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const cache = await builder.addRedis('cache');

// Add a console app project to the AppHost
await builder
  .addProject('processor', '../MyConsoleApp/MyConsoleApp.csproj')
  .withReference(cache);

await builder.build().run();
```

The `processor` project receives the `ConnectionStrings__cache` environment variable at runtime, just like any other project type.

**Note:** The console app project should target a .NET version supported by Aspire (.NET
  8 or later) and reference the service defaults project to benefit from
  telemetry and service discovery. For more information, see [Service
  defaults](/get-started/csharp-service-defaults/).

Install the JavaScript hosting integration in your AppHost. It provides `AddNodeApp` / `addNodeApp`, which runs a JavaScript file with Node.js:

<InstallPackage packageName="Aspire.Hosting.JavaScript" />

<InstallPackage packageName="Aspire.Hosting.JavaScript" cliOnly />

Then add the console app to your AppHost:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

// Add a Node.js console app to the AppHost
var processor = builder.AddNodeApp("processor", "../processor", "index.js")
    .WithReference(cache);

builder.Build().Run();
```

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const cache = await builder.addRedis('cache');

// Add a Node.js console app to the AppHost
await builder
  .addNodeApp('processor', '../processor', 'index.js')
  .withReference(cache);

await builder.build().run();
```

The console app uses the [ioredis](https://github.com/redis/ioredis) client. Aspire installs the dependencies listed in _package.json_ before starting the app:

```json title="processor/package.json"
{
  "name": "processor",
  "private": true,
  "type": "module",
  "dependencies": {
    "ioredis": "^5.4.0"
  }
}
```

```javascript title="processor/index.js"
import Redis from 'ioredis';

const redis = new Redis(process.env.CACHE_URI);

await redis.set('processor:last-run', new Date().toISOString());
console.log('Last run:', await redis.get('processor:last-run'));

await redis.quit();
```

The `processor` app receives the `CACHE_URI`, `CACHE_HOST`, `CACHE_PORT`, and `CACHE_PASSWORD` environment variables at runtime, along with `ConnectionStrings__cache`. For more information, see [Connect to Redis](/integrations/caching/redis/redis-connect/).

**Tip:** To start a Node.js app with a _package.json_ script (for example, `npm run
  start`) instead of a specific file, use `AddJavaScriptApp` /
  `addJavaScriptApp`. For more information, see [Add JavaScript
  application](/integrations/frameworks/javascript/#add-javascript-application).

## Service discovery in console apps

Console apps can call HTTP services that the AppHost manages. The following walkthrough assumes a `CatalogService` API project that exposes an HTTP endpoint at `/`, and a console app named `processor` that calls it.

Console apps using the .NET Generic Host can use the same service discovery infrastructure as web apps. This walkthrough uses a `MyConsoleApp` console project.

1.  From the console project directory, install the hosting package:

   <InstallDotNetPackage packageName="Microsoft.Extensions.Hosting" />

    Add a reference to your existing service defaults project in the console project's _MyConsoleApp.csproj_. Replace the example path with your project's actual path:

    ```xml title="MyConsoleApp.csproj"
    <ItemGroup>
      <ProjectReference Include="..\MyApp.ServiceDefaults\MyApp.ServiceDefaults.csproj" />
    </ItemGroup>
    ```

2.  Add a typed HTTP client to the console project:

   ```csharp title="CatalogClient.cs"
    using System.Net.Http;

    public sealed class CatalogClient(HttpClient httpClient)
    {
        public Task<string> GetAsync(CancellationToken cancellationToken = default)
            => httpClient.GetStringAsync("/", cancellationToken);
    }
    ```

3.  Configure the host in your console app's _Program.cs_. This example starts the host, calls the catalog API, and then stops:

   ```csharp title="Program.cs"
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;

    var builder = Host.CreateApplicationBuilder(args);

    // Add service defaults (telemetry, health checks, service discovery)
    builder.AddServiceDefaults();

    // Register your services
    builder.Services.AddHttpClient<CatalogClient>(
        client => client.BaseAddress = new Uri("https+http://catalog"));

    using var app = builder.Build();
    await app.StartAsync();

    var catalogClient = app.Services.GetRequiredService<CatalogClient>();
    Console.WriteLine(await catalogClient.GetAsync());

    await app.StopAsync();
    ```

4.  In the AppHost, reference the services this app depends on:

   ```csharp title="AppHost.cs"
    var builder = DistributedApplication.CreateBuilder(args);

    var catalog = builder.AddProject<Projects.CatalogService>("catalog");

    var processor = builder.AddProject<Projects.MyConsoleApp>("processor")
        .WithReference(catalog)
        .WaitFor(catalog);

    builder.Build().Run();
    ```

    ```typescript title="apphost.mts"
    import { createBuilder } from './.aspire/modules/aspire.mjs';

    const builder = await createBuilder();

    const catalog = await builder.addProject(
      'catalog',
      '../CatalogService/CatalogService.csproj'
    );

    const processor = await builder.addProject(
      'processor',
      '../MyConsoleApp/MyConsoleApp.csproj'
    );
    await processor.withReference(catalog);
    await processor.waitFor(catalog);

    await builder.build().run();
    ```

    The `WithReference(catalog)` call injects service discovery configuration so `processor` can resolve `https+http://catalog` to the running catalog service endpoint.

Node.js apps don't use the .NET service discovery resolver. Instead, they read the catalog service's endpoint URLs from environment variables that Aspire injects.

1.  Create a _processor_ folder next to your AppHost with a _package.json_ file. This example uses the built-in `fetch` API, so it has no dependencies:

   ```json title="processor/package.json"
    {
      "name": "processor",
      "private": true,
      "type": "module"
    }
    ```

2.  Add an _index.js_ file that calls the catalog API and then exits. It prefers the HTTPS endpoint and falls back to HTTP:

   ```javascript title="processor/index.js"
    const catalogUrl = process.env.CATALOG_HTTPS ?? process.env.CATALOG_HTTP;

    if (!catalogUrl) {
      throw new Error(
        'The catalog endpoint is not configured. Run the app through Aspire.'
      );
    }

    const response = await fetch(new URL('/', catalogUrl));
    console.log(await response.text());
    ```

3.  In the AppHost, reference the services this app depends on:

   ```csharp title="AppHost.cs"
    var builder = DistributedApplication.CreateBuilder(args);

    var catalog = builder.AddProject<Projects.CatalogService>("catalog");

    var processor = builder.AddNodeApp("processor", "../processor", "index.js")
        .WithReference(catalog)
        .WaitFor(catalog);

    builder.Build().Run();
    ```

    ```typescript title="apphost.mts"
    import { createBuilder } from './.aspire/modules/aspire.mjs';

    const builder = await createBuilder();

    const catalog = await builder.addProject(
      'catalog',
      '../CatalogService/CatalogService.csproj'
    );

    const processor = await builder.addNodeApp(
      'processor',
      '../processor',
      'index.js'
    );
    await processor.withReference(catalog);
    await processor.waitFor(catalog);

    await builder.build().run();
    ```

    The `withReference(catalog)` call injects the catalog service's endpoint URLs into `processor` as `CATALOG_HTTPS` and `CATALOG_HTTP`. Aspire also injects the same URLs in the `services__catalog__https__0` format. For more information, see [Service discovery](/fundamentals/service-discovery/).

To send traces, metrics, and logs from a Node.js app to the dashboard, configure the OpenTelemetry Node.js SDK in the app. For more information, see [Add telemetry configuration](/get-started/add-aspire-existing-app/#add-telemetry-configuration-optional).

## Background worker services

Background workers run for as long as the AppHost runs and stop when it shuts down. In .NET, Worker Service projects are the recommended pattern for long-running background tasks with Aspire. In Node.js, use a script that loops until it receives a shutdown signal.

### Creating a worker

Worker Service projects use `IHostedService` (or `BackgroundService`) and integrate naturally with the .NET Generic Host.

Create a `DataWorker` project with the Worker Service template, then add a reference to your shared service defaults project as shown in the console walkthrough:

```bash title=".NET CLI"
dotnet new worker --name DataWorker --output DataWorker
```

From the `DataWorker` directory, install the Redis **client** integration. This package supplies `AddRedisClient`; the AppHost's hosting package doesn't register a client in the worker:

<InstallDotNetPackage packageName="Aspire.StackExchange.Redis" />

Replace the generated worker class with _DataProcessor.cs_. Its `DoWorkAsync` method is a placeholder for your application's processing logic; registering the Redis client doesn't itself perform Redis operations.

```csharp title="DataProcessor.cs"
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class DataProcessor : BackgroundService
{
    private readonly ILogger<DataProcessor> _logger;

    public DataProcessor(ILogger<DataProcessor> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Processing data at: {Time}", DateTimeOffset.UtcNow);

            await DoWorkAsync(stoppingToken);

            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }

    private Task DoWorkAsync(CancellationToken stoppingToken)
    {
        // Replace with your background work.
        return Task.CompletedTask;
    }
}
```

```csharp title="Program.cs"
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.AddServiceDefaults();

// Register resource connections
builder.AddRedisClient("cache");

// Register the background worker
builder.Services.AddHostedService<DataProcessor>();

var app = builder.Build();
await app.RunAsync();
```

Create a _data-worker_ folder next to your AppHost with a _package.json_ file that includes the [ioredis](https://github.com/redis/ioredis) client:

```json title="data-worker/package.json"
{
  "name": "data-worker",
  "private": true,
  "type": "module",
  "dependencies": {
    "ioredis": "^5.4.0"
  }
}
```

Add a _worker.js_ file. The worker runs every 10 seconds until Aspire stops the process, then closes its Redis connection. Its `doWork` function is a placeholder for your application's processing logic.

```javascript title="data-worker/worker.js"
import { setTimeout as delay } from 'node:timers/promises';
import Redis from 'ioredis';

const redis = new Redis(process.env.CACHE_URI);

// Stop the loop when Aspire stops the process
const controller = new AbortController();
for (const signal of ['SIGINT', 'SIGTERM']) {
  process.once(signal, () => controller.abort());
}

async function doWork(redis, signal) {
  // Replace with your background work.
}

try {
  while (!controller.signal.aborted) {
    console.log(`Processing data at: ${new Date().toISOString()}`);

    await doWork(redis, controller.signal);

    await delay(10_000, undefined, { signal: controller.signal });
  }
} catch (error) {
  if (error.name !== 'AbortError') {
    throw error;
  }
} finally {
  await redis.quit();
}
```

### Adding to the AppHost

The following model also provides a PostgreSQL database. Install its hosting integration in the AppHost before using this example:

<InstallPackage packageName="Aspire.Hosting.PostgreSQL" />

<InstallPackage packageName="Aspire.Hosting.PostgreSQL" cliOnly />

The database reference supplies connection information, but the worker above doesn't yet use it. To add database operations, configure a client as described in [Connect to PostgreSQL](/integrations/databases/postgres/postgres-connect/).

Add the Worker Service project to your AppHost:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("app-db");

var worker = builder.AddProject<Projects.DataWorker>("data-worker")
    .WithReference(cache)
    .WithReference(db)
    .WaitFor(db);

builder.Build().Run();
```

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const cache = await builder.addRedis('cache');
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');

const worker = await builder.addProject(
  'data-worker',
  '../DataWorker/DataWorker.csproj'
);
await worker.withReference(cache);
await worker.withReference(db);
await worker.waitFor(db);

await builder.build().run();
```

Add the Node.js worker to your AppHost:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");
var db = builder.AddPostgres("postgres").AddDatabase("app-db");

var worker = builder.AddNodeApp("data-worker", "../data-worker", "worker.js")
    .WithReference(cache)
    .WithReference(db)
    .WaitFor(db);

builder.Build().Run();
```

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const cache = await builder.addRedis('cache');
const postgres = await builder.addPostgres('postgres');
const db = await postgres.addDatabase('app-db');

const worker = await builder.addNodeApp(
  'data-worker',
  '../data-worker',
  'worker.js'
);
await worker.withReference(cache);
await worker.withReference(db);
await worker.waitFor(db);

await builder.build().run();
```

The worker receives the database connection properties with an `APP_DB_` prefix, such as `APP_DB_URI`. Aspire replaces the dash in the resource name with an underscore in these variable names. For more information, see [Resource naming conventions](/app-host/resource-naming/).

<LearnMore>
  For more information on the Redis and PostgreSQL hosting integrations, see
  [Redis integration](/integrations/caching/redis/redis-get-started/) and
  [PostgreSQL
  integration](/integrations/databases/postgres/postgres-get-started/).
</LearnMore>

## Explicit start and console interaction

By default, all resources start automatically when the AppHost runs. For console apps that should only run on demand, use `WithExplicitStart`.

In your AppHost, add the console app project with `WithExplicitStart`:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var tool = builder.AddProject<Projects.MyInteractiveTool>("tool")
    .WithExplicitStart();

builder.Build().Run();
```

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const tool = await builder.addProject(
  'tool',
  '../MyInteractiveTool/MyInteractiveTool.csproj'
);
await tool.withExplicitStart();

await builder.build().run();
```

In your AppHost, add the Node.js app with `WithExplicitStart`:

```csharp title="AppHost.cs"
var builder = DistributedApplication.CreateBuilder(args);

var tool = builder.AddNodeApp("tool", "../tool", "index.js")
    .WithExplicitStart();

builder.Build().Run();
```

```typescript title="apphost.mts"
import { createBuilder } from './.aspire/modules/aspire.mjs';

const builder = await createBuilder();

const tool = await builder.addNodeApp('tool', '../tool', 'index.js');
await tool.withExplicitStart();

await builder.build().run();
```

With `WithExplicitStart`, the resource appears in the Aspire dashboard but doesn't start automatically. You can start it manually from the dashboard or programmatically.

**Caution:** Interactive console apps that read from stdin (for example,
  `Console.ReadLine()` in .NET or the `node:readline` module in Node.js) aren't
  fully supported in the Aspire dashboard's **Console logs** view. The dashboard
  captures and displays stdout/stderr, but the console logs page doesn't forward
  user input to the process's stdin.

Aspire builds that include the experimental `WithTerminal` API support interactive terminal user interfaces (TUIs) and shell-based experiences that need a live, bidirectional session, separately from the dashboard's console log viewer.

:::caution[Experimental API]
`WithTerminal` is reported under diagnostic ID `ASPIRETERMINAL001`. Using it in C# requires suppressing that diagnostic, and its API surface and behavior may change. This API might not be present in your installed release; use compatible hosting packages and a CLI build that includes it.
:::

## Viewing console output

Aspire captures stdout and stderr from all managed resources and displays them in the dashboard's **Console logs** page. Console apps output is fully visible, including colored text (ANSI escape codes are rendered).

<LearnMore>
  For more information, see [Console logs
  page](/dashboard/explore/#console-logs-page).
</LearnMore>

For structured logging in console apps, use `ILogger` instead of `Console.Write`. The following fragment assumes an existing logger and item identifier:

```csharp title="Program.cs"
// Prefer structured logging
logger.LogInformation("Processing item {ItemId}", itemId);

// Instead of
Console.WriteLine($"Processing item {itemId}");
```

With console logging and OpenTelemetry logging/export configured, such as through the service defaults setup above, logs appear in both the **Console logs** and **Structured logs** views of the dashboard. Calling `ILogger` alone doesn't export structured logs to the dashboard.

Output from `console.log` and `console.error` appears in the **Console logs** view. To also see logs in the **Structured logs** view, emit them through the OpenTelemetry Logs API. The following fragment assumes the OpenTelemetry Node.js SDK is configured with an OTLP log exporter, and an existing item identifier:

```javascript title="index.js"
import { logs, SeverityNumber } from '@opentelemetry/api-logs';

const logger = logs.getLogger('processor');

// Prefer structured logging
logger.emit({
  severityNumber: SeverityNumber.INFO,
  severityText: 'Information',
  body: 'Processing item',
  attributes: { itemId },
});

// Instead of
console.log(`Processing item ${itemId}`);
```

Logs emitted without a configured log exporter aren't sent to the dashboard. For more information on setting up the SDK, see [Add telemetry configuration](/get-started/add-aspire-existing-app/#add-telemetry-configuration-optional).

## Using connection strings without service defaults

Apps that don't use the service defaults project, such as a minimal .NET console app without a host or any Node.js app, can read connection information directly from environment variables.

For the configuration API example, install the environment variables configuration provider in your console project:

<InstallDotNetPackage packageName="Microsoft.Extensions.Configuration.EnvironmentVariables" />

```csharp title="Program.cs"
// For .NET apps, read from configuration
using Microsoft.Extensions.Configuration;

var config = new ConfigurationBuilder()
    .AddEnvironmentVariables()
    .Build();

var redisConnection = config.GetConnectionString("cache");
// Or equivalently:
var redisConnection2 = Environment.GetEnvironmentVariable("ConnectionStrings__cache");
```

**Tip:** When using configuration APIs directly, use `GetConnectionString("cache")`
  rather than reading `ConnectionStrings__cache` directly. The configuration
  system maps the double-underscore separator to the colon used by
  `IConfiguration`, making `ConnectionStrings:cache` and
  `ConnectionStrings__cache` equivalent. For more information, see [Resource
  naming conventions](/app-host/resource-naming/).

Read the values from `process.env`. Node.js doesn't map the `:` and `__` separators, so use the exact environment variable name:

```javascript title="index.js"
// Prefer the connection properties for the resource
const redisUri = process.env.CACHE_URI;

// The full connection string is also available:
const redisConnection = process.env.ConnectionStrings__cache;
```

For Redis, `ConnectionStrings__cache` uses the StackExchange.Redis connection string format rather than a URI, so pass `CACHE_URI` to Node.js clients such as ioredis. For the properties each resource provides, see that integration's connect page, such as [Connect to Redis](/integrations/caching/redis/redis-connect/).

## See also

- [Service defaults](/get-started/csharp-service-defaults/)
- [Service discovery](/fundamentals/service-discovery/)
- [Resource naming conventions](/app-host/resource-naming/)
- [Executable resources](/app-host/executable-resources/)
- [JavaScript integration](/integrations/frameworks/javascript/)
- [.NET Worker Service](https://learn.microsoft.com/dotnet/core/extensions/workers)