DEV Community

Cover image for Building Your First Agent with Microsoft Foundry Agent Service from .NET
Morten Christensen
Morten Christensen

Posted on

Building Your First Agent with Microsoft Foundry Agent Service from .NET

For this post I wanted to share how I approached building my first agent in Microsoft Foundry Agent Service with C#. Every resource you actually need, every CLI command that provisions it, and the SDK code explained rather than assumed. If you've found a cleaner setup, or disagree with any of the trade-offs I mention here, I'd genuinely like to hear it.

I'm writing this because the official quickstart is Python-first and hides the Azure setup behind azd scaffolding. It doesn't tell you which resources you actually need, what the RBAC role assignments are, or why the SDK now ships four NuGet packages and which one will silently break your build. The C# SDK stabilised at 2.0.0 in mid-2026, the product was renamed from Azure AI Foundry at the same time, and the RBAC role names changed too. Hit the documentation cold and three separate things look broken at once. None of them are.

First, the naming situation

The service was called Azure AI Foundry. It's now called Microsoft Foundry. The RBAC roles went with it: the role you assign to give a developer project access is now Foundry User (previously Azure AI User). The role IDs (the GUIDs) are unchanged, which matters when you script the assignment. Portal screens, error messages, and a lot of existing tutorials still use the old names. You're not confused; the rollout just isn't finished yet.

What you need in Azure

Four things:

  1. A Foundry resource (top-level Azure resource, kind AIServices)
  2. A project inside that resource
  3. A deployed model
  4. A role assignment so your identity can call the API

That's it. No Storage account, no Search index, nothing else for the basic prompt-agent path. Here's a setup script:

#!/usr/bin/env bash
# setup.sh — Provision all Azure resources needed for the Foundry Agent Service examples.
# Run from any directory. Requires the Azure CLI (az) to be installed and logged in.
#
# Usage:
#   chmod +x setup.sh
#   ./setup.sh
#
# The final line prints the PROJECT_ENDPOINT you should export before running the C# examples.

set -euo pipefail

RESOURCE_GROUP="foundry-agent-rg"
LOCATION="eastus2"
RESOURCE_NAME="foundry-agent-demo"   # Must be globally unique — change this to something specific to your org
PROJECT_NAME="first-agent-project"
DEPLOYMENT_NAME="gpt-5.4-mini"
MODEL_NAME="gpt-5.4-mini"
MODEL_VERSION="2026-03-17"           # Verified GA version for gpt-5.4-mini as of July 2026
SKU_NAME="GlobalStandard"
SKU_CAPACITY=10

echo "=== Step 1: Create resource group ==="
az group create \
  --name "$RESOURCE_GROUP" \
  --location "$LOCATION"

echo ""
echo "=== Step 2: Create Foundry resource ==="
echo "Note: This can take 1–2 minutes. --allow-project-management cannot be changed after creation."
az cognitiveservices account create \
  --name "$RESOURCE_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --kind AIServices \
  --sku S0 \
  --location "$LOCATION" \
  --custom-domain "$RESOURCE_NAME" \
  --allow-project-management \
  --yes

echo ""
echo "=== Step 3: Create project ==="
az cognitiveservices account project create \
  --name "$RESOURCE_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --project-name "$PROJECT_NAME"

echo ""
echo "=== Step 4: Deploy model ==="
az cognitiveservices account deployment create \
  --name "$RESOURCE_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --deployment-name "$DEPLOYMENT_NAME" \
  --model-name "$MODEL_NAME" \
  --model-format "OpenAI" \
  --model-version "$MODEL_VERSION" \
  --sku-name "$SKU_NAME" \
  --sku-capacity "$SKU_CAPACITY"

echo ""
echo "=== Step 5: Get project resource ID for RBAC ==="
PROJECT_ID=$(az cognitiveservices account project show \
  --name "$RESOURCE_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --project-name "$PROJECT_NAME" \
  --query id -o tsv)
echo "Project resource ID: $PROJECT_ID"

echo ""
echo "=== Step 6: Get current user ==="
CURRENT_USER=$(az account show --query user.name -o tsv)
echo "Current user: $CURRENT_USER"

echo ""
echo "=== Step 7: Assign Foundry User role on project ==="
# Using the stable GUID for the Foundry User role (previously 'Azure AI User').
# The display name was renamed; the GUID is unchanged.
az role assignment create \
  --role "53ca6127-db72-4b80-b1b0-d745d6d5456d" \
  --assignee "$CURRENT_USER" \
  --scope "$PROJECT_ID"

echo ""
echo "=== Done ==="
echo ""
echo "Project endpoint URL:"
echo "https://${RESOURCE_NAME}.services.ai.azure.com/api/projects/${PROJECT_NAME}"
echo ""
echo "Set this in your shell before running the C# examples:"
echo "export PROJECT_ENDPOINT=\"https://${RESOURCE_NAME}.services.ai.azure.com/api/projects/${PROJECT_NAME}\""
echo "export AGENT_NAME=\"first-agent\""
Enter fullscreen mode Exit fullscreen mode

A couple of things worth calling out. The --custom-domain value becomes part of your project endpoint URL and must be globally unique, so pick something specific to your organisation, not just foundry-agent-demo. The --allow-project-management flag cannot be changed after the resource is created, so don't skip it.

Use the role definition GUID for the assignment rather than the display name. The display name was just renamed and, depending on your tenant, it may resolve incorrectly during the rollout period. The GUID is stable.

Deploy into East US 2 if you have a choice. The model router and every standard built-in tool are available there. Some regions are missing tools, and the tool availability matrix is worth checking before you commit if you plan to use code interpreter or file search.

The project endpoint you'll use in code looks like https://<resource-name>.services.ai.azure.com/api/projects/<project-name>. Copy it from the Foundry portal once provisioning finishes, or construct it from the names you used in the script.

Prompt agents or hosted agents?

Before writing any code, it's worth being clear about what you're actually building. Foundry Agent Service has two meaningfully different modes.

Prompt agents are defined declaratively: model, instructions, tools. Foundry runs them for you. No application code to maintain, no compute to pay for. You send a request to the Responses API and get an answer back. For internal tools, question-answering, pipelines without custom orchestration logic: this is where I'd start.

Hosted agents are code you write yourself (Agent Framework, LangGraph, the OpenAI Agents SDK, or anything else), packaged as a container and run on Foundry-managed compute. You get a managed endpoint, autoscaling, and a dedicated Entra identity. You also pay for container compute on top of inference. This is the right choice when you need custom business logic embedded in the agent, not just configuration.

This article covers prompt agents. Hosted agents from .NET use the Microsoft Agent Framework (a separate open-source project) and are different enough to deserve their own treatment. I'll cover that in the next post in this series.

The packages — and the one to avoid

The C# SDK for Foundry requires four packages:

<Project Sdk="Microsoft.NET.Sdk">

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

  <ItemGroup>
    <!-- Azure AI Projects: thin client over the Foundry project API.
         Provides AIProjectClient and access to AgentAdministrationClient. -->
    <PackageReference Include="Azure.AI.Projects" Version="2.0.1" />

    <!-- Azure AI Projects Agents: agent administration surface.
         Provides DeclarativeAgentDefinition, ProjectsAgentVersion,
         CreateAgentVersion, DeleteAgentVersion, and the tool type hierarchy. -->
    <PackageReference Include="Azure.AI.Projects.Agents" Version="2.0.0" />

    <!-- Azure AI Extensions OpenAI: invocation surface.
         Provides ProjectResponsesClient, ProjectConversationsClient,
         ProjectConversation, and GetProjectResponsesClientForAgent. -->
    <PackageReference Include="Azure.AI.Extensions.OpenAI" Version="2.0.0" />

    <!-- Azure Identity: DefaultAzureCredential (resolves via az login locally,
         managed identity in production — same code path either way). -->
    <PackageReference Include="Azure.Identity" Version="1.21.0" />

    <!--
      *** DO NOT ADD ***
      Azure.AI.Projects.OpenAI (preview) defines the same types as
      Azure.AI.Extensions.OpenAI in different namespaces. Installing both
      causes ambiguous reference compile errors that are difficult to diagnose.
      Use only Azure.AI.Extensions.OpenAI.
    -->
  </ItemGroup>

</Project>
Enter fullscreen mode Exit fullscreen mode

Azure.AI.Projects is the thin client over the Foundry project API. Azure.AI.Projects.Agents adds the agent administration surface: creating, versioning, and deleting definitions. Azure.AI.Extensions.OpenAI is what you use to invoke the agent; it provides ProjectResponsesClient, which calls the Responses API on your project endpoint. Azure.Identity covers credentials.

The one to avoid is Azure.AI.Projects.OpenAI. It's a preview package that defines the same types in a different namespace. Install it alongside Azure.AI.Extensions.OpenAI and you get ambiguous reference compile errors that take a while to trace back to the source. The comment in the project file says the same thing. Just don't add it.

Creating your first agent

Here's a complete console application. It creates a prompt agent, asks a question, then asks a follow-up that only makes sense if the model remembers the first answer:

using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using OpenAI.Responses;
using System.ClientModel;

// Configuration — read from environment variables.
string projectEndpoint = Environment.GetEnvironmentVariable("PROJECT_ENDPOINT")
    ?? throw new InvalidOperationException("PROJECT_ENDPOINT environment variable is not set.");

string agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "first-agent";

// 1. Create the project client.
AIProjectClient projectClient = new(
    endpoint: new Uri(projectEndpoint),
    tokenProvider: new DefaultAzureCredential());

// 2. Define the agent declaratively: model choice + system instructions.
DeclarativeAgentDefinition agentDefinition = new("gpt-5.4-mini")
{
    Instructions = "You are a helpful assistant."
};

// 3. Register the agent version.
ClientResult<ProjectsAgentVersion> agentVersionResult =
    projectClient.AgentAdministrationClient.CreateAgentVersion(
        agentName,
        options: new ProjectsAgentVersionCreationOptions(agentDefinition));

ProjectsAgentVersion agentVersion = agentVersionResult.Value;
Console.WriteLine($"Agent registered: name={agentVersion.Name}, version={agentVersion.Version}");

// 4. Create a conversation.
ClientResult<ProjectConversation> conversationResult =
    projectClient.ProjectOpenAIClient
        .GetProjectConversationsClient()
        .CreateProjectConversation(options: null);

ProjectConversation conversation = conversationResult.Value;
Console.WriteLine($"Conversation created: id={conversation.Id}");

// 5. Get a responses client bound to this agent and conversation.
ProjectResponsesClient responsesClient =
    projectClient.ProjectOpenAIClient
        .GetProjectResponsesClientForAgent(
            defaultAgent: new AgentReference(agentVersion.Name),
            defaultConversationId: conversation.Id);

// 6. First turn.
ClientResult<ResponseResult> turn1Result =
    responsesClient.CreateResponse(userInputText: "What is the capital of Denmark?",
                                   previousResponseId: null);

ResponseResult turn1 = turn1Result.Value;
Console.WriteLine($"[Turn 1]: {turn1.GetOutputText()}");

// 7. Second turn — the model knows from conversation history what "that city" refers to.
ClientResult<ResponseResult> turn2Result =
    responsesClient.CreateResponse(userInputText: "And what is the population of that city?",
                                   previousResponseId: null);

ResponseResult turn2 = turn2Result.Value;
Console.WriteLine($"[Turn 2]: {turn2.GetOutputText()}");

// Note: agent version is intentionally not deleted here.
// example-04 creates its own agent; cleanup instructions are in README.md.
Enter fullscreen mode Exit fullscreen mode

A few things to walk through.

AIProjectClient takes the project endpoint URI and a TokenCredential. Locally, DefaultAzureCredential resolves via az login. In production it picks up a managed identity without any code change.

ClientResult<T> shows up on every method that returns something from the service. It's in the System.ClientModel package, which is pulled in transitively, but you still need using System.ClientModel; in your file. Call .Value to get the actual object.

DeclarativeAgentDefinition is where model choice and system instructions live. It goes into CreateAgentVersion, which registers a versioned definition in Foundry. Each call creates a new immutable version. The method returns ClientResult<ProjectsAgentVersion>; call .Value to get the ProjectsAgentVersion, which carries the stable name and version string you'll reference at invocation time.

ProjectConversation is what carries history across turns. Pass the conversation ID to GetProjectResponsesClientForAgent alongside new AgentReference(agentVersion.Name), and each CreateResponse call on that client automatically includes prior context. One detail: CreateResponse always requires a previousResponseId argument, even if you're passing null. The second question in the example knows what "that city" refers to because of the conversation binding, not because you're stitching history together yourself. Honestly this is the bit that surprised me most. Less work than I expected.

Adding web search

The web search tool grounds the agent's responses in real-time public web data. It runs Bing Search under the hood. Two things to know before you enable it: there's an additional per-query cost, and the Microsoft Data Protection Addendum does not apply to data sent to Bing. If you're in a regulated environment, check the Grounding with Bing terms first. For internal tools or prototypes, it's fine.

The change to the agent definition is small:

using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using OpenAI.Responses;
using System.ClientModel;

// Configuration — read from environment variables.
string projectEndpoint = Environment.GetEnvironmentVariable("PROJECT_ENDPOINT")
    ?? throw new InvalidOperationException("PROJECT_ENDPOINT environment variable is not set.");

string agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "web-search-agent";

// 1. Create the project client.
AIProjectClient projectClient = new(
    endpoint: new Uri(projectEndpoint),
    tokenProvider: new DefaultAzureCredential());

// 2. Define the agent with the web search tool.
DeclarativeAgentDefinition agentDefinition = new("gpt-5.4-mini")
{
    Instructions = "You are a helpful assistant that can search the web."
};

agentDefinition.Tools.Add(
    ResponseTool.CreateWebSearchPreviewTool(
        userLocation: WebSearchToolLocation.CreateApproximateLocation(
            country: "DK",
            region: "Capital Region",
            city: "Copenhagen",
            timezone: null)));

// 3. Register the agent version.
ClientResult<ProjectsAgentVersion> agentVersionResult =
    projectClient.AgentAdministrationClient.CreateAgentVersion(
        agentName,
        options: new ProjectsAgentVersionCreationOptions(agentDefinition));

ProjectsAgentVersion agentVersion = agentVersionResult.Value;
Console.WriteLine($"Agent registered: name={agentVersion.Name}, version={agentVersion.Version}");

// 4. Create a conversation.
ClientResult<ProjectConversation> conversationResult =
    projectClient.ProjectOpenAIClient
        .GetProjectConversationsClient()
        .CreateProjectConversation(options: null);

ProjectConversation conversation = conversationResult.Value;

// 5. Get a responses client bound to this agent and conversation.
ProjectResponsesClient responsesClient =
    projectClient.ProjectOpenAIClient
        .GetProjectResponsesClientForAgent(
            defaultAgent: new AgentReference(agentVersion.Name),
            defaultConversationId: conversation.Id);

// 6. Call the agent with a web search query.
ClientResult<ResponseResult> responseResult =
    responsesClient.CreateResponse(
        userInputText: "What is in the tech news today?",
        previousResponseId: null);

ResponseResult response = responseResult.Value;

// 7. Print the text response.
Console.WriteLine($"\nResponse: {response.GetOutputText()}");

// 8. Extract URL citations from the response output items.
Console.WriteLine("\nCitations:");
bool foundCitation = false;

foreach (ResponseItem item in response.OutputItems)
{
    if (item is MessageResponseItem messageItem)
    {
        foreach (ResponseContentPart part in messageItem.Content)
        {
            // OutputTextAnnotations is only populated on output_text parts.
            if (part.Kind == ResponseContentPartKind.OutputText)
            {
                foreach (ResponseMessageAnnotation annotation in part.OutputTextAnnotations)
                {
                    if (annotation is UriCitationMessageAnnotation citation)
                    {
                        Console.WriteLine($"  Citation: {citation.Title}{citation.Uri}");
                        foundCitation = true;
                    }
                }
            }
        }
    }
}

if (!foundCitation)
{
    Console.WriteLine("  (no URL citations returned — web search may not have been triggered)");
}

// 9. Clean up: delete the agent version.
// DeleteAgentVersion takes agentName and agentVersion (the version string, e.g. "1").
projectClient.AgentAdministrationClient.DeleteAgentVersion(
    agentName: agentVersion.Name,
    agentVersion: agentVersion.Version);

Console.WriteLine($"\nCleaned up: agent version {agentVersion.Name}:{agentVersion.Version} deleted.");
Enter fullscreen mode Exit fullscreen mode

The invocation is identical to the basic example. CreateResponse returns ClientResult<ResponseResult>; call .Value.GetOutputText() for the text. The difference shows up in what's attached to that response: the output items contain UriCitationMessageAnnotation entries for the sources the model cited.

Two things to watch for if you copy this. First: the correct factory is ResponseTool.CreateWebSearchPreviewTool, not CreateWebSearchTool. Both exist in OpenAI 2.9.x. CreateWebSearchTool serialises to "type": "web_search", which the Foundry Responses API currently rejects with a 400. CreateWebSearchPreviewTool serialises to "web_search_preview", which Foundry accepts. Second: DeclarativeAgentDefinition.Tools is a read-only property. You can't set it in an object initialiser. Call .Add() after construction, as the code above does.

The userLocation hint is optional but worth setting if your agent answers questions for users in a specific region. Bing uses it to bias results. Omitting it still works.

Web search is in preview. It's available in East US 2 and most major regions, but check the tool support matrix for your target region before you depend on it in production.

Cost and a few regional notes

For prompt agents you pay for inference tokens. When web search is enabled, Bing Search usage is on top of that. With gpt-5.4-mini Global Standard and developer-level traffic, a full day of testing runs well under a dollar. No premium quota tier required.

Tool availability varies by region. Code interpreter is unavailable in South Central US and Spain Central as of this writing. East US 2 has the broadest coverage. Check the tool support matrix before you settle on a deployment region; it's easier to pick the right one up front than to migrate a resource after the fact.

The DeclarativeAgentDefinition you built here maps directly to what you'd put in a CI/CD pipeline. Create the agent version during deployment, update the definition when instructions change, roll back by version ID when something breaks. That versioning model is where prompt agents become production-usable rather than just a demo.

Hosted agents from .NET, and what the Microsoft Agent Framework brings to that picture, is where this series goes next.

Code Examples

You can find the examples and a markdown version of this article on my github. On github you'll find that the article has a few more notes in the code, which I intentionally left out here as its a bit verbose.
Code examples for this article

Top comments (0)