---
title: Aspire Publish
description: Generate deployment pipelines and infrastructure configuration from your AppHost.
moved_from:
  - guides/development/aspire-publish.md
---

# Aspire Publish

Generate Azure DevOps pipeline YAML and infrastructure configuration from your AppHost using `aspire publish`.

---

## 📋 Overview

Instead of maintaining pipeline YAML files by hand, you can define your application's services, security, and infrastructure in C# inside the Aspire AppHost. Running `aspire publish --output-path ./` from your application root directory generates all the pipeline files, variables, and auth configuration your project needs.

### Why Aspire Publish?

| Benefit                | Description                                                       |
| ---------------------- | ----------------------------------------------------------------- |
| **Type safety**        | Compile-time validation prevents misconfigured pipeline variables |
| **Single source**      | AppHost is the single source of truth for project structure       |
| **Refactoring**        | Rename a permission or service and all generated files update     |
| **Discoverability**    | IntelliSense shows available services, options, and permissions   |
| **Consistency**        | All Forge projects follow the same generation patterns            |

---

## 🚀 Quick Start

### 1. Configure Your AppHost

```csharp
using Aspire.Hosting;
using SAIF.Platform.Aspire.Hosting;
using SAIF.Platform.Aspire.Hosting.Generation.Services;
using SAIF.Platform.Aspire.Hosting.Generation.Infrastructure.Security;

var builder = DistributedApplication.CreateBuilder(args);

// Environment setup
builder.AddSaifEnvironment()
    .WithApplicationName("MyApp")
    .WithBusinessDomain("it")
    .WithOwner("MyTeam");

// API service
var api = builder.AddApiService<Projects.MyApp_Api>("myapp");

// Frontend (optional)
var frontend = builder.AddFrontendService("frontend", "../MyApp.Frontend")
    .WithEnvironment("BACKEND_URL", api.GetEndpoint("http"));
api.WithReference(frontend);

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

### 2. Publish

From your application root directory:

```powershell
aspire publish --output-path ./
```

### 3. Review Generated Files

```text
.azdo/
├── azure-pipelines-api.yml
├── azure-pipelines-api-pr.yml
└── vars/
    ├── base.yml
    └── api.yml
```

### 4. Commit, Push, and Publish

`aspire publish` only writes files to disk — it does not create an Azure DevOps repository or register pipelines.

- **Existing project** (repository and pipelines already exist): commit the generated files and push. The existing pipelines pick up the updated YAML automatically.
- **New project** (no repository yet): commit the generated files, then run [`saif publish`](../../reference/cli-workflows.md#saif-publish) from the application root to create the Azure DevOps repository, push your code, and register the pipelines defined in `.azdo/`.

---

## 🏗️ Environment Setup

Every AppHost starts with `AddSaifEnvironment()`:

```csharp
builder.AddSaifEnvironment()
    .WithApplicationName("MyApp")
    .WithBusinessDomain("it")
    .WithOwner("Architecture");
```

### Optional Configuration

```csharp
builder.AddSaifEnvironment()
    .WithApplicationName("MyApp")
    .WithBusinessDomain("it")
    .WithOwner("Architecture")
    .WithTenant("corporate")
    .WithPipelineDefaults(o =>
    {
        o.DotNetVersion = "10.x";
        o.NodeVersion = "22.x";
        o.TemplateRef = "refs/heads/releases/v3";
        o.SecretsVariableGroupName = "it-api-exp-myapp"; // typically your project ID
    });
```

`SecretsVariableGroupName` is the name of the Azure DevOps variable group (Library) that holds this project's secrets. It must match an existing group exactly, or the pipeline won't find your secrets. It is **not created for you**. See [Create the Variable Group](../config/settings-and-secrets.md#create-the-variable-group) for how to create one; whatever name you give the group there is the value to set here (typically your project ID).

See [API Reference — Environment](#environment) for all available methods and defaults.

---

## 📦 Service Registration

### API Service

Registers a .NET API project for both local development (`aspire run`) and pipeline generation (`aspire publish`). API services are **standalone projects** with their own repository and pipeline.

```csharp
var api = builder.AddApiService<Projects.MyApp_Api>("myapp");
```

**Generated project ID:** `{domain}-api-{type}-{name}` (e.g., `it-api-exp-myapp`)

#### API Type Options

!!! note
    `ApiPipelineOptions`, `EventServicePipelineOptions`, and the other pipeline generation APIs are experimental and raise [SAIFPIPELINES001](../../reference/diagnostics/SAIFPIPELINES001.md). Follow that page to suppress the diagnostic.

```csharp
var api = builder.AddApiService<Projects.MyApp_Api>("myapp",
    new ApiPipelineOptions { Type = ApiType.Process });
```

| `ApiType`    | Short Code | Example Project ID      | Use Case                     |
| ------------ | ---------- | ----------------------- | ---------------------------- |
| `Experience` | `exp`      | `it-api-exp-myapp`      | BFF / frontend-facing APIs   |
| `Process`    | `proc`     | `it-api-proc-myapp`     | Business process APIs        |
| `System`     | `sys`      | `it-api-sys-myapp`      | System/infrastructure APIs   |
| `None`       | `none`     | `it-api-none-myapp`     | No API type classification   |

**Not sure which to pick?** `ApiType.Experience` is the default and the right choice if your API is called directly by a frontend or another team's client — most first projects fall into this category. Reach for `Process` only if this API's job is purely to orchestrate calls to other backend APIs with no direct caller-facing contract, and `System` only if it exists to integrate with one specific backend system (e.g., a single database or legacy system) rather than to serve a business capability.

The type is **baked into the project ID** (`{domain}-api-{type}-{name}`), which becomes the Azure DevOps repository name, pipeline names, and (by convention) the secrets variable group name. Changing it later means renaming all of those, not just a config value — pick based on the API's role, not on how confident you feel about the distinction.

!!! tip "Chose the wrong type?"
    There's no automated rename path. The simplest fix is to create a new project with the correct type via `saif new` and migrate your code over — trying to rename the existing repository, pipelines, and variable group individually is more error-prone than starting fresh.

#### OpenAPI Specification

```csharp
var api = builder.AddApiService<Projects.MyApp_Api>("myapp",
    new ApiPipelineOptions { OpenApiFileName = "openapi.v1.yaml" });
```

### Frontend Service

Registers a React/Vite frontend for local development and pipeline generation. The frontend can be used in two ways:

- **Alongside an API** — the frontend lives in the same repository as the API, sharing an AppHost (`.WithReference(api)`)
- **Standalone web project** — its own repository and AppHost with a dedicated web pipeline

```csharp
var frontend = builder.AddFrontendService("frontend", "../MyApp.Frontend");
```

**Generated project ID:** `{domain}-web-{name}` (e.g., `it-web-myapp`)

The second parameter is the path to the frontend project directory, relative to the AppHost.

### Event Service

Registers a virtual event service for pipeline generation only (no runnable project). Event services are **standalone projects** with their own repository, TypeSpec definitions, and pipeline.

```csharp
builder.AddEventService("events");
```

**Generated project ID:** `events-{name}` (e.g., `events-myapp`)

#### Event Service Options

```csharp
builder.AddEventService("events",
    new EventServicePipelineOptions { OpenApiFileName = "openapi.yaml" });
```

### Subscription Service

Registers an Azure Functions subscription project. Subscription services are a **feature paired with an API** — they live in the same repository and share an AppHost with their API project. Wraps the standard Aspire `AddAzureFunctionsProject<T>()` and adds pipeline annotations for discovery.

```csharp
var subscription = builder.AddSubscriptionService<Projects.MyApp_Subscriptions>("subscriptions")
    .WithReference(serviceBus)
    .WithReference(database);
```

**Generated project ID:** `{domain}-func-{appName}` (e.g., `it-func-myapp`)

---

## 🔒 Security Configuration

### App Roles and Business Roles

```csharp
builder.AddSecurity()
    .WithCorporateAssignment(new AppRoleAssignment(
        new BusinessRole("ClaimsAdjuster"),
        [new AppRole("Claims.Read"), new AppRole("Claims.Write")]))
    .WithExternalAssignment(new AppRoleAssignment(
        new BusinessRole("InjuredWorker"),
        [new AppRole("Claims.Submit")]));
```

See [Aspire Security Configuration](../identity/configuration/aspire-security-configuration.md) for the full security setup guide.

### Upstream API Dependencies

```csharp
builder.AddUpstreamApi("it-api-proc-orders")
    .WithScope(new Scope("Orders.Read"))
    .WithAppRole(new AppRole("Orders.App.Read"));
```

See [API Reference — Security](#security) for all available methods.

---

## 🗃️ Infrastructure Features

### Oracle Database

Declare an Oracle Database dependency to include the project ID variable group in generated API pipelines:

```csharp
api.WithOracle();
```

---

## 📂 Generated Files

The files generated depend on which services and features are registered in your AppHost.

### Pipeline Files

| Generated File                              | Condition                |
| ------------------------------------------- | ------------------------ |
| `.azdo/azure-pipelines-api.yml`             | API service registered   |
| `.azdo/azure-pipelines-api-pr.yml`          | API service registered   |
| `.azdo/azure-pipelines-web.yml`             | Frontend registered      |
| `.azdo/azure-pipelines-web-pr.yml`          | Frontend registered      |
| `.azdo/azure-pipelines-sub.yml`             | Subscription detected    |
| `.azdo/azure-pipelines-sub-pr.yml`          | Subscription detected    |
| `.azdo/azure-pipelines-auth.yml`            | Security registered      |
| `.azdo/azure-pipelines-auth-pr.yml`         | Security registered      |
| `.azdo/azure-pipelines-event-service.yml`   | Event service registered |
| `.azdo/azure-pipelines-event-service-pr.yml`| Event service registered |

### Variable Files

| Generated File                | Condition                |
| ----------------------------- | ------------------------ |
| `.azdo/vars/base.yml`        | Always                   |
| `.azdo/vars/api.yml`         | API service registered   |
| `.azdo/vars/web.yml`         | Frontend registered      |
| `.azdo/vars/sub.yml`         | Subscription detected    |
| `.azdo/vars/auth.yml`        | Security registered      |
| `.azdo/vars/event-service.yml`| Event service registered |

### Security Configuration Files

| Generated File                                  | Condition                          |
| ----------------------------------------------- | ---------------------------------- |
| `infra/api/config.yml`                          | Security registered                |
| `infra/auth/corp/config.yml`                    | Corporate assignments configured   |
| `infra/auth/ext/user/business-role-app-role.yml`| External assignments configured    |
| `infra/auth/ext/app/authorized-apps.yml`        | Upstream API with external scopes  |

---

## 🎯 Complete Example

This example shows all features working together:

```csharp
using Aspire.Hosting;
using SAIF.Platform.Aspire.Hosting;
using SAIF.Platform.Aspire.Hosting.Generation.Features;
using SAIF.Platform.Aspire.Hosting.Generation.Services;
using SAIF.Platform.Aspire.Hosting.Generation.Infrastructure.Security;

var builder = DistributedApplication.CreateBuilder(args);

// ── Environment ──────────────────────────────────────────────────────
builder.AddSaifEnvironment()
    .WithApplicationName("MyApp")
    .WithBusinessDomain("it")
    .WithOwner("Architecture");

// ── API ──────────────────────────────────────────────────────────────
var api = builder.AddApiService<Projects.MyApp_Api>("myapp");

// ── Security ─────────────────────────────────────────────────────────
builder.AddSecurity()
    .WithCorporateAssignment(new AppRoleAssignment(
        new BusinessRole("ClaimsAdjuster"),
        [new AppRole("Claims.Read"), new AppRole("Claims.Write")]))
    .WithExternalAssignment(new AppRoleAssignment(
        new BusinessRole("InjuredWorker"),
        [new AppRole("Claims.Submit")]));

builder.AddUpstreamApi("it-api-proc-orders")
    .WithScope(new Scope("Orders.Read"))
    .WithAppRole(new AppRole("Orders.App.Read"));

// ── Frontend ─────────────────────────────────────────────────────────
var frontend = builder.AddFrontendService("frontend", "../MyApp.Frontend")
    .WithEnvironment("BACKEND_URL", api.GetEndpoint("http"));
api.WithReference(frontend);

// ── Data ─────────────────────────────────────────────────────────────
#pragma warning disable ASPIRECOSMOSDB001
var cosmosdb = builder.AddAzureCosmosDB("cosmosdb")
    .RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent));
#pragma warning restore ASPIRECOSMOSDB001

var database = cosmosdb.AddCosmosDatabase("CosmosDbConnection", "it-api-exp-myapp");
database.AddContainer("Claims", "/PartitionKey");

api.WithReference(database);

// ── Service Bus + Subscription ───────────────────────────────────────
var serviceBus = builder.AddAzureServiceBus("sbnamespace")
    .RunAsEmulator(emulator => emulator.WithLifetime(ContainerLifetime.Persistent));

var subscription = builder.AddSubscriptionService<Projects.MyApp_Subscriptions>("subscriptions")
    .WithReference(serviceBus)
    .WithReference(database);

// ── Event Service ────────────────────────────────────────────────────
builder.AddEventService("events");

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

---

## 🔍 Aspire Dashboard

When running locally with `aspire run`, the Aspire dashboard shows all registered resources with their types and states:

| Icon         | Resource Type    | State      | Description                      |
| ------------ | ---------------- | ---------- | -------------------------------- |
| Pipeline     | Pipelines        | Configured | Pipeline generation registered   |
| Lock Closed  | Security         | Configured | Auth configuration registered    |
| Mail         | Event Service    | Configured | Event service pipeline ready     |
| Link         | Upstream API     | Configured | External API dependency declared |

Runnable resources (API, Frontend, Functions) appear with their standard Aspire states (Running, Starting, etc.).

---

## ✅ Verification

After publishing:

1. **Check generated files** — Verify pipeline YAML and variable files exist in `.azdo/`
2. **Compare with existing** — Review diffs if replacing hand-maintained pipelines
3. **Validate YAML** — Ensure variable references and template paths are correct
4. **Test PR pipeline** — Create a PR to validate the generated PR pipelines work

---

## 💡 Tips

### Migrate Existing Projects

To migrate a project from hand-maintained pipelines to Aspire publish:

1. Add `SAIF.Platform.Aspire.Hosting` to your AppHost project
2. Configure services and security in `AppHost.cs` (see examples above)
3. Run `aspire publish --output-path ./` from the application root directory to generate files
4. Review the diff — generated files write directly to `.azdo/` matching existing filenames and directory structure, overwriting in place
5. Commit and validate in a PR pipeline run

### When to Re-Publish

Re-run `aspire publish --output-path ./` whenever you:

- Add or remove a service (API, frontend, subscription, event service)
- Change security configuration (roles, permissions, upstream APIs)
- Update pipeline defaults (SDK versions, template ref)
- Change the application name or business domain

### Working Example

The Forge foundry includes a complete working example at `foundry/dotnet/aspire-publish/`. Use it as a reference for all supported features.

---

## 📖 API Reference

All extension methods are in the `SAIF.Platform.Aspire.Hosting` package. Methods extend either `IDistributedApplicationBuilder` (top-level registration) or `IResourceBuilder<T>` (fluent configuration).

### Environment

| Method | Parameters | Description |
| ------ | ---------- | ----------- |
| `AddSaifEnvironment()` | `string name = "saif"` | Registers the SAIF environment and enables pipeline generation. Call this first. |
| `.WithApplicationName()` | `string applicationName` | Sets the application name used in project ID computation and pipeline variables. **Required.** |
| `.WithBusinessDomain()` | `string domain` | Sets the business domain prefix (`it`, `claims`, `hr`, etc.). **Required.** |
| `.WithOwner()` | `string owner` | Sets the owning team name. **Required.** |
| `.WithTenant()` | `string tenant` | Overrides the deployment tenant. Default: `"corporate"`. |
| `.WithPipelineDefaults()` | `Action<PipelineDefaults> configure` | Overrides SDK versions, template ref, or secrets variable group. See [Pipeline Defaults](#pipelinedefaults). |

### Services

| Method | Parameters | Returns | Description |
| ------ | ---------- | ------- | ----------- |
| `AddApiService<TProject>()` | `string name`, `ApiPipelineOptions? options = null` | `IResourceBuilder<ProjectResource>` | Registers a .NET API project. Runs during `aspire run` and generates pipelines on publish. `TProject` must implement `IProjectMetadata`. |
| `AddFrontendService()` | `string name`, `string appDirectory`, `string runScriptName = "dev"` | `IResourceBuilder<JavaScriptAppResource>` | Registers a React/Vite frontend. `appDirectory` is relative to the AppHost. |
| `AddEventService()` | `string name`, `EventServicePipelineOptions? options = null` | `IResourceBuilder<EventServiceResource>` | Registers a virtual event service (pipeline generation only — no runnable project). |
| `AddSubscriptionService<TProject>()` | `string name` | `IResourceBuilder<AzureFunctionsProjectResource>` | Registers an Azure Functions subscription project. Wraps `AddAzureFunctionsProject<T>()` and adds pipeline annotations. `TProject` must implement `IProjectMetadata`. Requires an API service in the same AppHost. |

### Security

| Method | Parameters | Returns | Description |
| ------ | ---------- | ------- | ----------- |
| `AddSecurity()` | `string name = "security"` | `IResourceBuilder<SecurityResource>` | Registers security resource for auth config generation. |
| `.WithCorporateAssignment()` | `AppRoleAssignment assignment` | `IResourceBuilder<SecurityResource>` | Adds a corporate (Entra ID) business role → app role mapping. |
| `.WithExternalAssignment()` | `AppRoleAssignment assignment` | `IResourceBuilder<SecurityResource>` | Adds an external (Okta) business role → app role mapping. |
| `AddUpstreamApi()` | `string projectId` | `IResourceBuilder<UpstreamApiResource>` | Declares a dependency on an external API by its project ID. |
| `.WithScope()` | `Scope scope` | `IResourceBuilder<UpstreamApiResource>` | Adds a delegated permission (user context flows through). |
| `.WithAppRole()` | `AppRole appRole` | `IResourceBuilder<UpstreamApiResource>` | Adds an application permission (service-to-service). |
| `.WithAppRoleAssignment()` | `AppRoleAssignment assignment` | `IResourceBuilder<UpstreamApiResource>` | Adds all app roles from a business role assignment. |

### Infrastructure Features

| Method | Parameters | Returns | Description |
| ------ | ---------- | ------- | ----------- |
| `.WithOracle<T>()` | *(none)* | `IResourceBuilder<T>` | Declares Oracle Database dependency. Includes the project ID variable group in generated API pipelines. `T` must implement `IResource`. |

### Options Classes

#### `ApiPipelineOptions`

| Property | Type | Default | Description |
| -------- | ---- | ------- | ----------- |
| `Type` | `ApiType` | `Experience` | API classification: `Experience`, `Process`, `System`, or `None`. |
| `OpenApiFileName` | `string` | `"openapi.v1.yaml"` | OpenAPI specification filename. |

#### `EventServicePipelineOptions`

| Property | Type | Default | Description |
| -------- | ---- | ------- | ----------- |
| `OpenApiFileName` | `string` | `"openapi.yaml"` | OpenAPI specification filename. |

#### `PipelineDefaults`

| Property | Type | Default | Description |
| -------- | ---- | ------- | ----------- |
| `DotNetVersion` | `string` | `"10.x"` | .NET SDK version for pipeline agents. |
| `NodeVersion` | `string` | `"22.x"` | Node.js version for pipeline agents. |
| `TemplateRef` | `string` | `"refs/heads/releases/v3"` | Pipeline templates repository branch. |
| `SecretsVariableGroupName` | `string` | `""` | Azure DevOps variable group for secrets. |

### Security Model Types

All types are in `SAIF.Platform.Aspire.Hosting.Generation.Infrastructure.Security`.

| Type | Constructor | Description |
| ---- | ----------- | ----------- |
| `AppRole` | `(string Value, string? DisplayName = null, string? Description = null)` | An application permission identifier. |
| `Scope` | `(string Value, string? DisplayName = null, string? Description = null)` | An OAuth2 delegated permission. |
| `BusinessRole` | `(string Name)` | A named business role (Entra/Okta group). |
| `AppRoleAssignment` | `(BusinessRole Role, AppRole[] AppRoles)` | Maps a business role to one or more app roles. |

### Enums

#### `ApiType`

| Value | Short Code | Project ID Pattern |
| ----- | ---------- | ------------------ |
| `Experience` | `exp` | `{domain}-api-exp-{name}` |
| `Process` | `proc` | `{domain}-api-proc-{name}` |
| `System` | `sys` | `{domain}-api-sys-{name}` |
| `None` | `none` | `{domain}-api-none-{name}` |

---

## 📚 Related Documentation

- [Aspire Security Configuration](../identity/configuration/aspire-security-configuration.md): define auth configuration in C#
- [Event Service](../eventing/event-service.md): create event services with TypeSpec
- [Event Subscription](../eventing/event-subscription.md): add Service Bus subscriptions
- [Troubleshooting](../../troubleshoot/index.md): diagnosed pipeline and Aspire errors
