# 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
```

---

## 🏗️ Environment Setup

Every AppHost starts with `AddSaifEnvironment()`:

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

### Optional Configuration

```csharp
builder.AddSaifEnvironment()
    .WithApplicationName("Emmjoh")
    .WithBusinessDomain("it")
    .WithOwner("Architecture")
    .WithTenant("corporate")
    .WithPipelineDefaults(o =>
    {
        o.DotNetVersion = "10.x";
        o.NodeVersion = "22.x";
        o.TemplateRef = "refs/heads/releases/v3";
        o.SecretsVariableGroupName = "my-secrets";
    });
```

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

```csharp
var api = builder.AddApiService<Projects.MyApp_Api>("myapp",
    new ApiPipelineOptions { ApiType = 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   |

#### 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](../security/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 |
| -------- | ---- | ------- | ----------- |
| `ApiType` | `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](../security/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
- [Pipeline Troubleshooting](pipeline-troubleshooting.md) — Resolve pipeline errors
- [Aspire Troubleshooting](aspire-troubleshooting.md) — Debug Aspire startup issues
