Skip to content
Open
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
4 changes: 3 additions & 1 deletion dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowEval/Evaluation_WorkflowEval.csproj" />
<Project Path="samples/03-workflows/Evaluation/Evaluation_WorkflowExpectedOutputs/Evaluation_WorkflowExpectedOutputs.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/" />
<Folder Name="/Samples/04-hosting/">
<Project Path="samples/04-hosting/ContainerWorkflow/ContainerWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/af-hosting/">
<File Path="samples/04-hosting/af-hosting/README.md" />
</Folder>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions dotnet/samples/04-hosting/ContainerWorkflow/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish dotnet/samples/04-hosting/ContainerWorkflow/ContainerWorkflow.csproj \
-c Release -f net10.0 -o /app/publish --tl:off

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
ENV ASPNETCORE_HTTP_PORTS=8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "ContainerWorkflow.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
**
!CODE_OF_CONDUCT.md
!.editorconfig
!dotnet/
!dotnet/**
**/bin/
**/obj/
**/.env
**/.env.*
**/local-feed/
**/appsettings.Development.json
**/secrets.json
**/*.pfx
**/*.pem
**/*.key
67 changes: 67 additions & 0 deletions dotnet/samples/04-hosting/ContainerWorkflow/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft. All rights reserved.

// Host a deterministic expense-routing workflow in ASP.NET Core, locally or in a container.
using Microsoft.Agents.AI.Workflows;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

app.MapGet("/health", () => Results.Ok());
app.MapPost("/expenses", async (ExpenseRequest expense, CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(expense.Id) || expense.Id.Length > 128 || expense.Amount <= 0)
{
return Results.BadRequest(new { error = "Supply an id of 1-128 characters and an amount greater than zero." });
}

// Each request owns its executors and workflow state, so concurrent requests remain isolated.
NormalizeExpenseExecutor normalize = new();
RouteExpenseExecutor route = new();
Workflow workflow = new WorkflowBuilder(normalize).AddEdge(normalize, route).WithOutputFrom(route).Build();

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, expense, cancellationToken: cancellationToken);
ExpenseDecision? decision = null;
await foreach (WorkflowEvent evt in run.WatchStreamAsync(cancellationToken))
{
if (evt is WorkflowErrorEvent or ExecutorFailedEvent)
{
return Results.Problem("The expense workflow failed.");
}

if (evt is WorkflowOutputEvent { Data: ExpenseDecision output })
{
decision = output;
}
}

// Disposing the run also stops execution when request cancellation ends stream consumption.
return decision is null ? Results.Problem("The workflow produced no decision.") : Results.Ok(decision);
});

await app.RunAsync();

internal readonly record struct ExpenseRequest(string? Id, decimal Amount);

internal sealed record ExpenseDecision(string Id, decimal Amount, string Route);

internal sealed class NormalizeExpenseExecutor() : Executor<ExpenseRequest, ExpenseRequest>("NormalizeExpense")
{
/// <inheritdoc/>
public override ValueTask<ExpenseRequest> HandleAsync(ExpenseRequest message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
return ValueTask.FromResult(message with { Id = message.Id!.Trim() });
}
}

internal sealed class RouteExpenseExecutor() : Executor<ExpenseRequest, ExpenseDecision>("RouteExpense")
{
/// <inheritdoc/>
public override ValueTask<ExpenseDecision> HandleAsync(ExpenseRequest message, IWorkflowContext context, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
// ponytail: this sample routes expenses; it does not approve payments or call a business system.
return ValueTask.FromResult(new ExpenseDecision(message.Id!, message.Amount,
message.Amount > 100 ? "manager_review" : "standard_review"));
}
}
76 changes: 76 additions & 0 deletions dotnet/samples/04-hosting/ContainerWorkflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Host a workflow in a container

This sample hosts a pro-code workflow in ASP.NET Core and packages it as a non-root
Linux container. It routes an expense through two executors: normalize the expense
ID, then select standard review (amount <= 100) or manager review (amount > 100).
The rules are deterministic so you can verify the hosting setup without a model,
cloud account, credentials, or external business system.

Each HTTP request builds its own workflow and executors. Cancelling a request ends
event consumption; disposing its `StreamingRun` stops the underlying execution.
An executor failure produces an HTTP 500 response rather than a successful result.

## Run locally

Install the .NET SDK specified in `dotnet/global.json`. From the repository root:

```sh
dotnet run --project dotnet/samples/04-hosting/ContainerWorkflow -f net10.0 -- --urls http://localhost:8080
```

In another terminal:

```sh
curl http://localhost:8080/health
curl -H "Content-Type: application/json" -d '{"id":"EXP-001","amount":150}' http://localhost:8080/expenses
```

Expected response:

```json
{"id":"EXP-001","amount":150,"route":"manager_review"}
```

For PowerShell, use:

```powershell
Invoke-RestMethod http://localhost:8080/expenses -Method Post -ContentType application/json -Body '{"id":"EXP-001","amount":150}'
```

Amounts of 100 or less select `standard_review`. A missing/blank ID, an ID longer
than 128 characters, or a non-positive amount returns HTTP 400. Malformed JSON is
also rejected by ASP.NET Core.

## Build and run the container

With Docker running in Linux-container mode, run these commands from the repository
root. The build uses project references to compile this checkout of the framework.
The Dockerfile-specific ignore file limits the build context to .NET sources and
excludes local build output and common credential files.

```sh
docker build -f dotnet/samples/04-hosting/ContainerWorkflow/Dockerfile -t agent-framework-workflow .
docker run --rm --name agent-framework-workflow -p 127.0.0.1:8080:8080 agent-framework-workflow
```

Use the same requests above to verify the container. Stop it with Ctrl+C, or from
another terminal:

```sh
docker stop agent-framework-workflow
```

## Hosting boundaries

This is an in-process, request/response deployment example. It does not persist
workflow state across process restarts, execute payments, or provide a human
approval UI. The expense ID is returned for correlation; it is not an idempotency
key. Repeating a request executes a new workflow.

The endpoint is unauthenticated for local testing. Add application authentication
and authorization before exposing it to other users. No inbound credentials or
request bodies are deliberately logged by the sample.

For cloud-managed hosting, see [Foundry hosted agents](../FoundryHostedAgents).
For durable agents and external scheduling, see the
[Durable Agent Framework extension](https://github.com/microsoft/agent-framework-durable-extension).
7 changes: 7 additions & 0 deletions dotnet/samples/04-hosting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Hosting samples

| Sample | Demonstrates |
| --- | --- |
| [Container workflow](./ContainerWorkflow) | Host a pro-code workflow with ASP.NET Core and run it in a Docker container |
| [Foundry hosted agents](./FoundryHostedAgents) | Deploy and invoke agents with Microsoft Foundry |
| [A2A](./A2A) | Host agents using the Agent-to-Agent protocol |
Loading