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:
3. Review Generated Files¶
.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 publishfrom 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():
builder.AddSaifEnvironment()
.WithApplicationName("MyApp")
.WithBusinessDomain("it")
.WithOwner("Architecture");
Optional Configuration¶
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 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 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.
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. Follow that page to suppress the diagnostic.
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.
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¶
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
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.
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:
📂 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 |
| 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:
- Check generated files — Verify pipeline YAML and variable files exist in
.azdo/ - Compare with existing — Review diffs if replacing hand-maintained pipelines
- Validate YAML — Ensure variable references and template paths are correct
- 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:
- Add
SAIF.Platform.Aspire.Hostingto your AppHost project - Configure services and security in
AppHost.cs(see examples above) - Run
aspire publish --output-path ./from the application root directory to generate files - Review the diff — generated files write directly to
.azdo/matching existing filenames and directory structure, overwriting in place - 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 |
|---|---|---|---|
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: define auth configuration in C#
- Event Service: create event services with TypeSpec
- Event Subscription: add Service Bus subscriptions
- Troubleshooting: diagnosed pipeline and Aspire errors