Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ on:
paths:
- "dotnet-ai/provider-agnostic-chat-gateway/**"
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
- "dotnet-aspire/orchestrate-distributed-system/**"
- ".github/workflows/build-samples.yml"

pull_request:
paths:
- "dotnet-ai/provider-agnostic-chat-gateway/**"
- "dotnet-ai/hybrid-search-ef-core-pgvector/**"
- "dotnet-aspire/orchestrate-distributed-system/**"
- ".github/workflows/build-samples.yml"

workflow_dispatch:
Expand Down Expand Up @@ -76,3 +78,28 @@ jobs:
--project dotnet-ai/hybrid-search-ef-core-pgvector/HybridSearchMinimal.csproj \
--configuration Release \
--no-build

build-aspire-orchestration:
name: Build Aspire orchestration sample
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Restore
run: |
dotnet restore \
dotnet-aspire/orchestrate-distributed-system/AspireOrchestrationMinimal.slnx

- name: Build
run: |
dotnet build \
dotnet-aspire/orchestrate-distributed-system/AspireOrchestrationMinimal.slnx \
--configuration Release \
--no-restore
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| --- | --- | --- |
| [`dotnet-ai/provider-agnostic-chat-gateway`](dotnet-ai/provider-agnostic-chat-gateway/) | One HTTP chat endpoint using Ollama and optional OpenAI providers through `Microsoft.Extensions.AI.IChatClient` | [Build a switchable multi-provider AI gateway with IChatClient](https://www.dotnet-guide.com/tutorials/dotnet-ai/provider-agnostic-chat-gateway/) |
| [`dotnet-ai/hybrid-search-ef-core-pgvector`](dotnet-ai/hybrid-search-ef-core-pgvector/) | Minimal Reciprocal Rank Fusion demo combining pre-ranked keyword and vector results | [Hybrid Search in .NET with EF Core 10 and pgvector](https://www.dotnet-guide.com/tutorials/dotnet-ai/hybrid-search-ef-core-pgvector/) |
| [`dotnet-aspire/orchestrate-distributed-system`](dotnet-aspire/orchestrate-distributed-system/) | Minimal Aspire AppHost coordinating a web project and API with service discovery and startup ordering | [Aspire in .NET: Orchestrate, Run, and Deploy a Distributed System from One App Host](https://www.dotnet-guide.com/tutorials/dotnet-aspire/orchestrate-distributed-system/) |

## Companion articles

Expand Down Expand Up @@ -54,6 +55,19 @@ tutorials/
| |-- HybridSearchMinimal.csproj
| |-- Program.cs
| `-- README.md
|-- dotnet-aspire/
| `-- orchestrate-distributed-system/
| |-- AspireOrchestrationMinimal.slnx
| |-- README.md
| |-- AspireOrchestrationMinimal.AppHost/
| | |-- AppHost.cs
| | `-- AspireOrchestrationMinimal.AppHost.csproj
| |-- AspireOrchestrationMinimal.Api/
| | |-- Program.cs
| | `-- AspireOrchestrationMinimal.Api.csproj
| `-- AspireOrchestrationMinimal.Web/
| |-- Program.cs
| `-- AspireOrchestrationMinimal.Web.csproj
|-- .github/
| `-- workflows/
| `-- build-samples.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "AspireOrchestrationMinimal.Api is running.");

app.MapGet("/message", () => Results.Ok(new
{
message = "Hello from the Aspire-managed API",
service = "api"
}));

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
var builder = DistributedApplication.CreateBuilder(args);

var api = builder
.AddProject<Projects.AspireOrchestrationMinimal_Api>("api");

builder
.AddProject<Projects.AspireOrchestrationMinimal_Web>("web")
.WithReference(api)
.WaitFor(api)
.WithExternalHttpEndpoints();

builder.Build().Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="13.4.6" />
<ProjectReference Include="..\AspireOrchestrationMinimal.Api\AspireOrchestrationMinimal.Api.csproj" IsAspireProjectResource="true" />
<ProjectReference Include="..\AspireOrchestrationMinimal.Web\AspireOrchestrationMinimal.Web.csproj" IsAspireProjectResource="true" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.8.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Microsoft.Extensions.ServiceDiscovery;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddServiceDiscovery();

builder.Services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri("https+http://api");
})
.AddServiceDiscovery();

var app = builder.Build();

app.MapGet("/", async (IHttpClientFactory httpClientFactory) =>
{
var client = httpClientFactory.CreateClient("api");

string apiMessage;

try
{
apiMessage = await client.GetStringAsync("/message");
}
catch (Exception ex)
{
apiMessage = $"API unavailable: {ex.Message}";
}

var html = $"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Aspire orchestration sample</title>
</head>
<body>
<h1>Aspire orchestration sample</h1>
<p>Web resource: running</p>
<p>API response: {apiMessage}</p>
</body>
</html>
""";

return Results.Content(html, "text/html");
});

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Solution>
<Project Path="AspireOrchestrationMinimal.AppHost/AspireOrchestrationMinimal.AppHost.csproj" />
<Project Path="AspireOrchestrationMinimal.Api/AspireOrchestrationMinimal.Api.csproj" />
<Project Path="AspireOrchestrationMinimal.Web/AspireOrchestrationMinimal.Web.csproj" />
</Solution>
167 changes: 167 additions & 0 deletions dotnet-aspire/orchestrate-distributed-system/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Aspire AppHost Orchestration &mdash; Minimal Sample

A minimal .NET 10 Aspire sample demonstrating how one AppHost starts two .NET
projects, connects them with a logical reference, and enables the web project
to call the API through service discovery without a fixed localhost port.

## Full tutorial

[Aspire in .NET: Orchestrate, Run, and Deploy a Distributed System from One App Host](https://www.dotnet-guide.com/tutorials/dotnet-aspire/orchestrate-distributed-system/)

## What this sample demonstrates

* one Aspire AppHost;
* one API project;
* one web project;
* service discovery through `https+http://api`;
* project references and `WithReference(api)`;
* startup ordering with `WaitFor(api)`;
* an externally reachable web endpoint through `WithExternalHttpEndpoints`;
* dashboard visibility of both resources;
* web-to-API communication resolved by the AppHost.

## Architecture

```text
AppHost
├── api ── GET /message
└── web ── GET / (calls api through service discovery)
└── WithReference(api)
└── WaitFor(api)
```

## File structure

```text
orchestrate-distributed-system/
├── AspireOrchestrationMinimal.slnx
├── README.md
├── AspireOrchestrationMinimal.AppHost/
│ ├── AppHost.cs
│ └── AspireOrchestrationMinimal.AppHost.csproj
├── AspireOrchestrationMinimal.Api/
│ ├── Program.cs
│ └── AspireOrchestrationMinimal.Api.csproj
└── AspireOrchestrationMinimal.Web/
├── Program.cs
└── AspireOrchestrationMinimal.Web.csproj
```

## Prerequisites

* .NET 10 SDK
* HTTPS development certificate trusted (run `dotnet dev-certs https --trust`)

No Docker, Podman, PostgreSQL, Redis, API keys, or cloud credentials are required.

## Run the sample

Open a terminal in this sample folder and run:

```powershell
dotnet restore
dotnet build --configuration Release
dotnet run --project AspireOrchestrationMinimal.AppHost
```

The AppHost starts both projects. The Aspire dashboard opens automatically.

## Verify the sample

### Dashboard

Confirm that the Aspire dashboard shows:

* `api` &mdash; started successfully
* `web` &mdash; started successfully, waited for `api`
* both endpoints are visible

### Web endpoint

Open the web endpoint shown in the dashboard. The page displays:

```text
Aspire orchestration sample

Web resource: running
API response: {"message":"Hello from the Aspire-managed API","service":"api"}
```

### API endpoint

```text
GET / → AspireOrchestrationMinimal.Api is running.
GET /message → {"message":"Hello from the Aspire-managed API","service":"api"}
```

## Service discovery

The web project uses:

```csharp
client.BaseAddress = new Uri("https+http://api");
```

The resource name `api` matches the AppHost declaration:

```csharp
var api = builder.AddProject<Projects.AspireOrchestrationMinimal_Api>("api");
```

Aspire resolves the logical name at runtime. The web project contains **no**
fixed localhost port such as `http://localhost:5001`.

## `WithReference` versus `WaitFor`

| Method | Purpose |
| --- | --- |
| `WithReference(api)` | Supplies service-discovery and configuration information to the referencing project |
| `WaitFor(api)` | Controls startup ordering — the web project does not start until the API is ready |

They solve different problems. This sample uses both.

## Important boundary

This repository sample intentionally does **not** reproduce the complete tutorial.

The full DOTNET GUIDE tutorial additionally contains:

* PostgreSQL and Redis integration
* ServiceDefaults project
* EF Core and Npgsql
* database migrations
* caching
* custom health-check and readiness packages
* `Aspire.Hosting.Testing`
* integration-test projects
* OpenTelemetry customization
* generative-AI telemetry
* publishing (`aspire publish`)
* deployment (`aspire deploy`)
* Azure deployment guidance
* Kubernetes and Helm
* Docker-oriented publishing
* production secrets, ingress, scaling, and networking
* complete release-management and production-hardening guidance

## Verification

| Item | Detail |
| --- | --- |
| Target framework | .NET 10 (`net10.0`) |
| SDK | .NET SDK 10.0.302 |
| Aspire version | `Aspire.Hosting.AppHost` 13.4.6 |
| Service discovery | `Microsoft.Extensions.ServiceDiscovery` 10.8.0 |
| External services required | None |
| Containers required | None |
| API keys required | None |
| Local restore | Verified |
| Release build | Verified |
| Last reviewed | 2026-07-26 |

## License

Repository-owned sample code is available under the [MIT License](../../LICENSE).

Third-party Aspire and .NET packages remain subject to their own licenses.