Skip to content

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

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:

aspire publish --output-path ./

3. Review Generated Files

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

🏗️ Environment Setup

Every AppHost starts with AddSaifEnvironment():

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

Optional Configuration

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 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.

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

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

API Type Options

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

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
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.

builder.AddEventService("events");

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

Event Service Options

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.

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

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 for the full security setup guide.

Upstream API Dependencies

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

See API Reference — 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:

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:

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.

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}