Table of Contents

Introduction

Hey everyone!

You can find all the C# code samples here: MicrosoftAgentFramework GitHub Repo

Welcome to my Microsoft Agent Framework tutorial, which marks the official start of a new series dedicated to building C# AI agents within the .NET ecosystem. Over the course of this multi-part guide, we will explore everything from basic setup to advanced production-grade patterns.

Before learning about the framework, you may also want to take a look at my other posts, which will supplement this knowledge, especially in terms of Retrieval Augmented Generation (RAG) techniques in Azure, with a specific focus on Azure AI Search. Grounding your agents with proper data is critical, and those guides will give you the foundational cloud architecture context you need:

In this introductory master post, we are going to look at 10 practical, runnable code snippets that show you how to build AI agents in .NET step by step. We will cover everything from simple text responses to structured JSON outputs, state management, tools and observability.

What is the Microsoft Agent Framework?

To understand where the Microsoft Agent Framework (MAF) fits, we need to look at the current .NET AI landscape. Until recently, if you wanted to build C# AI agents, you generally had to choose between two distinct worlds:

  • Semantic Kernel: A heavy, enterprise-grade orchestration SDK that relies closely on plugins, semantic functions, and strict planner patterns.
  • AutoGen: A highly dynamic, multi-agent framework built around an asynchronous actor model, optimized for complex agent-to-agent conversations.

The Microsoft Agent Framework serves as the middle ground. It is a highly lightweight, composable ecosystem designed specifically to help you build AI agents in .NET (or Python) without the heavy boilerplate or rigid architectural constraints of larger SDKs.

The Architecture: Open and Model-Agnostic

The most critical architectural detail to understand about this framework is that it is built directly on top of the standardized Microsoft.Extensions.AI abstractions (specifically the IChatClient interface).

Because it sits on top of IChatClient, the framework is completely model-agnostic. Even though my examples utilize an Azure OpenAI agent C# setup, you are absolutely not locked into the Microsoft or OpenAI ecosystem. You can seamlessly swap the underlying backend execution engine to run completely different models, including:

  • OpenAI / Azure OpenAI (via AzureOpenAIClient)
  • Anthropic Claude (via community or official IChatClient providers)
  • Ollama (for running local open-source models like Llama 3 or Phi-3 on your own workstation)
  • Groq, Mistral, or Hugging Face endpoints

By standardizing on this interface, the framework treats the underlying Large Language Model (LLM) simply as a pluggable execution resource, allowing your agent configuration, session tracking, and RAG workflows to remain identical no matter where your inference runs.

Core Foundational Building Blocks (Examples 1-4)

Example 1: Hello World Agent

using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI.Chat;

namespace _01_Basics
{
    /// <summary>
    /// Microsoft Agent Framework Tutorial, Example: Hello World Agent
    /// </summary>
    public class HelloWorldExample
    {
        private readonly AIAgent _mafAgent;

        public HelloWorldExample()
        {
            var openAiClient = new AzureOpenAIClient(
                new Uri(Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CLIENT_URI")!),
                new DefaultAzureCredential());

            var chatClient = openAiClient
                .GetChatClient(deploymentName: Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CHAT_CLIENT_DEPLOYMENT_NAME")!);

            _mafAgent = chatClient
                .AsAIAgent(
                    instructions: "Always respond with 'Hello world'",
                    name: "The Hello World Agent",
                    description: "This is a very simple agent");
        }

        public async Task RunAsync()
        {
            var agentResponse = await _mafAgent.RunAsync(message: "Hello");
            Console.WriteLine(agentResponse);

            Console.WriteLine($"Input Tokens: {agentResponse.Usage!.InputTokenCount}, Output Tokens: {agentResponse.Usage.OutputTokenCount}");

            await foreach (var agentResponseUpdate in _mafAgent.RunStreamingAsync("Hello again"))
            {
                Console.Write(agentResponseUpdate);
            }
        }
    }
}

This example shows how to bootstrap the framework and build a functional AI agent with minimal code. Instead of manually managing raw chat completion loops, the .AsAIAgent() extension method encapsulates the identity, description, and system instructions directly on top of the standard IChatClient.

Key takeaways:

  • Production-Grade Authentication: We use DefaultAzureCredential() to avoid hardcoded API keys. When deploying within the Azure ecosystem (like Microsoft Foundry), make sure your managed identity is assigned the Foundry User RBAC role to allow the framework to execute inference).
  • Token Tracking: The agentResponse object provides direct access to token usage metadata via agentResponse.Usage. This is vital for monitoring application costs and logging telemetry.
  • Execution Modes (Standard vs. Streaming):
    • RunAsync (Standard): Fetches the complete payload at once. This one-time response pattern is ideal for background processes, data transformation, logging, or integration tasks where the final result is needed all at once.
    • RunStreamingAsync (Streaming): Streams tokens back as an async enumerable in real time. This mode is optimal for user-facing workflows, such as chat UIs, where reducing the time-to-first-token (TTFT) is critical for user experience.

Example 2: Structured Output Agent

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Structured Output Agent
/// </summary>
public class StructuredOutputExample
{
    private readonly AIAgent _mafAgent;

    // ctor omitted for brevity

    public async Task RunAsync()
    {
        var response = await _mafAgent.RunAsync<MovieReview>(
            "I just watched Inception. It was mind-blowing! Definitely a 5 out of 5 stars.");

        Console.WriteLine($"Movie: {response.Result!.Title}");
        Console.WriteLine($"Sentiment: {response.Result.Sentiment}");
        Console.WriteLine($"Stars: {response.Result.Stars}");
    }
}

public class MovieReview
{
    public string Title { get; set; } = string.Empty;
    public string Sentiment { get; set; } = string.Empty;
    public int Stars { get; set; }
}

Result:

Movie: Inception
Sentiment: positive
Stars: 5

When building text classification pipelines or backend automated processes, raw string responses from an LLM are notoriously fragile to parse. The framework solves this by supporting strongly-typed generic execution out of the box, mapping responses directly into your C# objects.

Key takeaways:

  • Automated Object Mapping: Passing your class directly into the method (_mafAgent.RunAsync<MovieReview>(...)) forces the agent to handle schema generation and serialization on its own.
  • Model Dependency (The JSON Schema Gotcha): Under the hood, this feature relies entirely on the capabilities of your underlying LLM. For OpenAI and Azure OpenAI models, the framework translates your C# class into an explicit json_schema property inside the API payload. Ensure you are targeting models that natively support structured outputs to guarantee reliable, deterministic JSON format compliance.

Example 3: Deep Dive into Message Types

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Deep Dive into Message Types
/// </summary>
public class MessageTypesExample
{
    private readonly AIAgent _mafAgent;

    // ctor omitted for brevity

    public async Task RunAsync()
    {
        // 1. Plain string message (simplest form)
        Console.WriteLine("=== String Message ===");
        Console.WriteLine(await _mafAgent.RunAsync("What is 2 + 2?"));

        // 2. ChatMessage with TextContent
        Console.WriteLine("\n=== ChatMessage (TextContent) ===");
        var textMessage = new ChatMessage(ChatRole.User, "What is the capital of Poland?");
        Console.WriteLine(await _mafAgent.RunAsync(textMessage));

        // 3. ChatMessage with multiple content parts (TextContent + DataContent for inline image)
        Console.WriteLine("\n=== ChatMessage (TextContent + DataContent) ===");

        var multipartMessage = new ChatMessage(ChatRole.User,
        [
            new TextContent("Describe what you see in this tiny image."),
            await DataContent.LoadFromAsync(Path.Combine(AppContext.BaseDirectory, "astronaut.jpg"))
        ]);

        Console.WriteLine(await _mafAgent.RunAsync(multipartMessage));

        // 4. ChatMessage with multiple content parts (TextContent + UriContent for hosted web image)
        Console.WriteLine("\n=== ChatMessage (TextContent + UriContent) ===");
        var urlMultipartMessage = new ChatMessage(ChatRole.User,
        [
            new TextContent("What is in this picture?"),
            new UriContent(new Uri("https://sadeployedinazuremaf.blob.core.windows.net/samples/apollo_11.png?useSasTokenHere"))
        ]);
        Console.WriteLine(await _mafAgent.RunAsync(urlMultipartMessage));

        var functionCallContent = new FunctionCallContent(callId: Guid.NewGuid().ToString(), name: "Name", arguments: new Dictionary<string, object?>());
        var functionCallResult = new FunctionResultContent(callId: Guid.NewGuid().ToString(), result: "Result");
    }
}

The astronaut.jpg image used in this example:

A picture showing an astrounaut on the Moon.

Result:

=== String Message ===
2 + 2 = 4.

=== ChatMessage (TextContent) ===
The capital of Poland is **Warsaw**.

=== ChatMessage (TextContent + DataContent) ===
An astronaut in a white spacesuit is standing on the moon's gray, dusty surface. The dark sky is in the background, and the astronaut's shadow stretches across the ground.

=== ChatMessage (TextContent + UriContent) ===
A rocket is launching from a launch pad, with smoke and flames billowing around it. It looks like a Saturn V-style spacecraft lifting off.

Interacting with modern LLMs requires handling multiple data shapes, from basic queries to multi-modal media. The Microsoft Agent Framework standardizes this by leveraging the core messaging primitives found in Microsoft.Extensions.AI.

Key takeaways:

  • Unified Parameter Normalization: While the API allows you to pass a plain C# string for convenience, this is purely syntactic sugar. Under the hood, the framework automatically normalizes that raw string, wrapping it into a formal ChatMessage object configured with ChatRole.User and a TextContent block before execution.
  • Explicit Abstractions: When your architecture requires granular control over conversation boundaries, you can bypass the convenience wrappers and pass explicit ChatMessage objects directly. This allows you to construct precise payloads defining custom roles and targeted system behaviors.
  • Streamlined Vision Processing: Sending images to a multi-modal model (like GPT-4o) is remarkably straightforward. Using DataContent.LoadFromAsync(), the framework manages file streaming, mime-type identification, and Base64 extraction entirely behind the scenes, allowing you to attach local media assets directly to your agent’s execution array in a single step.
  • Bandwidth-Efficient Remote Referencing: For assets already hosted in the cloud, such as images secured via SAS tokens in Azure Blob Storage you can leverage UriContent. Instead of downloading raw bytes and managing local file streams, you can pass the URI directly.

Example 4: Orchestrating Conversation Flows with Chat Roles

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Orchestrating Conversation Flows with Chat Roles
/// </summary>
public class ChatRolesExample
{
    private readonly AIAgent _mafAgent;

    // ctor omitted for brevity

    public async Task RunAsync()
    {
        // 1. User role — standard end-user message
        Console.WriteLine("=== User Role ===");
        var userMessage = new ChatMessage(ChatRole.User, "What is the speed of light?");
        Console.WriteLine(await _mafAgent.RunAsync(userMessage));

        // 2. Assistant role — inject a prior assistant turn to steer conversation context
        Console.WriteLine("\n=== Assistant Role (injected prior turn) ===");
        var priorUserMessage = new ChatMessage(ChatRole.User, "What programming language should I learn first?");
        var priorAssistantMessage = new ChatMessage(ChatRole.Assistant, "I recommend Python because it has a simple syntax and a large ecosystem.");
        var followUpMessage = new ChatMessage(ChatRole.User, "What are some good beginner Python projects?");

        Console.WriteLine(await _mafAgent.RunAsync([priorUserMessage, priorAssistantMessage, followUpMessage]));

        // 3. System role — override agent behaviour at runtime with an additional system message
        Console.WriteLine("\n=== System Role (runtime persona override) ===");
        var systemOverride = new ChatMessage(ChatRole.System, "From now on respond only using uppercase.");
        var rhymeRequest = new ChatMessage(ChatRole.User, "Say a joke.");

        Console.WriteLine(await _mafAgent.RunAsync([systemOverride, rhymeRequest]));
    }
}

LLMs rely on structured roles to distinguish between user inputs, model replies, and system instructions. The framework models this using the ChatRole enum, allowing you to manipulate the chat history payload directly before executing inference.

Key takeaways:

  • Context Injection (ChatRole.Assistant): You can pre-seed the execution window by passing an array of historical user and assistant messages. This is highly useful for automated unit testing or setting up few-shot examples to guide the model’s tone.
  • Runtime Overrides (ChatRole.System): Although foundational instructions are defined during agent initialization, you can pass an explicit system message inside your execution array to dynamically alter or layer personas at runtime on the fly.

State, Session, and Memory Management (Examples 5-7)

Example 5: Extending Capabilities with Tools (Function Calling)

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Extending Capabilities with Tools (Function Calling)
/// </summary>
public class ToolsExample
{
    private readonly AIAgent _mafAgent;

    public ToolsExample()
    {
        var openAiClient = new AzureOpenAIClient(
            new Uri(Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CLIENT_URI")!),
            new DefaultAzureCredential());


        _mafAgent = openAiClient
            .GetChatClient(deploymentName: Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CHAT_CLIENT_DEPLOYMENT_NAME")!)
            .AsAIAgent(
                instructions: """
                    You are a brief, helpful travel assistant. 
                    Use the provided tools to check weather, convert currency, and log expenses when requested. 
                    For multi-step requests, chain the necessary tools sequentially. 
                    Keep responses friendly, accurate, and concise.
                """,
                name: "Travel Companion Agent",
                description: "A smart, helpful travel assistant that assists users with weather checks, currency conversions, and expense logging during their trips.",
                tools: [
                    AIFunctionFactory.Create(GetWeather),
                    AIFunctionFactory.Create(ConvertToUsd),
                    AIFunctionFactory.Create(LogExpense)
                ]);
    }

    public async Task RunAsync()
    {           
        Console.WriteLine(await _mafAgent.RunAsync("Hey, I'm heading out for a walk in Tokyo right now. Do I need to bring a coat or an umbrella?"));
        Console.WriteLine("---\n");

        Console.WriteLine(await _mafAgent.RunAsync("I'm looking at a souvenir online that costs 4500 Japanese Yen. How much is that going to cost me in US dollars?"));
        Console.WriteLine("---\n");

        Console.WriteLine(await _mafAgent.RunAsync("Please add a 15.50 charge for a museum ticket to my travel expense tracker."));
        Console.WriteLine("---\n");

        Console.WriteLine(await _mafAgent.RunAsync("""
            I am currently in London and want to go see a street performance, but only if it's not raining. 
            If the weather is clear, I plan to buy a ticket that costs 25 GBP. 
            Check the weather for me first. 
            If it's good, figure out how much that ticket costs in USD and log it to my budget as 'Street Concert'.
            """));
    }

    [Description("Gets the current weather forecast for a specific city to help the user pack or plan.")]
    private string GetWeather([Description("The name of the city, e.g., 'London', 'Paris'")] string city)
    {
        // Mock implementation for tutorial purposes
        var random = new Random();
        string[] conditions = { "Sunny", "Rainy", "Cloudy", "Windy" };
        string condition = conditions[random.Next(conditions.Length)];
        int temperature = random.Next(15, 28);

        return $"The current weather in {city} is {condition} and {temperature}°C.";
    }

    [Description("Converts an amount from a foreign currency to US Dollars (USD).")]
    private string ConvertToUsd(
        [Description("The amount of money to convert")] double amount,
        [Description("The 3-letter currency code being converted from, e.g., 'EUR', 'GBP'")] string fromCurrency)
    {
        // Mock static exchange rates for simplicity
        double rate = fromCurrency.ToUpper() switch
        {
            "EUR" => 1.10,
            "GBP" => 1.30,
            "JPY" => 0.0065,
            _ => 1.00
        };

        double convertedAmount = Math.Round(amount * rate, 2);
        return $"{amount} {fromCurrency.ToUpper()} is approximately {convertedAmount} USD (Rate: {rate}).";
    }

    [Description("Logs a travel expense to the user's budget spreadsheet or database.")]
    private string LogExpense(
        [Description("The item or service purchased, e.g., 'Umbrella', 'Dinner'")] string item,
        [Description("The cost of the item in USD")] double amountInUsd)
    {
        // Mock action: In a real app, this would write to a database, API, or CSV file

        return $"Successfully logged '{item}' costing ${amountInUsd} to your travel budget.";
    }
}

Result:

Tokyo is currently cloudy and 21°C, so you probably don't need a coat. I'd skip the umbrella unless the forecast looks rainy later.
---

4500 JPY is approximately **$29.25 USD**.
---

Done - I logged a $15.50 museum ticket expense to your travel tracker.
---

London is sunny and 18°C, so it looks good for the street performance.

I converted the 25 GBP ticket to about $32.50 USD and logged it as **Street Concert**.

An agent truly shifts from a text generator to an autonomous assistant when you grant it the ability to interact with external business logic. The Microsoft Agent Framework handles native function calling (tools) seamlessly by generating JSON schemas directly from your C# code structure.

Key takeaways:

  • Automatic Tool Schema Discovery: By using AIFunctionFactory.Create(MethodName), the framework reflects over your parameters and reads standard [Description] attributes. It builds the entire JSON description payload for the model under the hood, removing the need for manual JSON schema declarations.
  • Autonomous Tool Chaining: Look at the final multi-step query for London. MAF handles the entire reasoning loop natively. If the weather is clear, it automatically parses the output of GetWeather, sends a call to ConvertToUsd, captures that value, and feeds it into LogExpense sequentially within a single RunAsync call.
  • Local C# Execution: Unlike server-side configurations, these tools run locally inside your C# application runtime. The LLM only determines which function to call and with what arguments; your native code executes the logic securely on your own server or container.

Example 6: Multi-Turn Conversation State with Agent Sessions

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Multi-Turn Conversation State with Agent Sessions
/// </summary>
public class AgentSessionExample
{
    private readonly AIAgent _mafAgent;

    // ctor omitted for brevity

    public async Task RunAsync()
    {
        var initialPrompt = "My name is Michal and I help people to build future-ready AI solutions deployed in Azure.";
        var followUpQuestion = "What is my name and what do I do?";

        Console.WriteLine("Without SESSION");
        await CallAgentAndLogTokensUsage(initialPrompt);
        await CallAgentAndLogTokensUsage(followUpQuestion);

        Console.WriteLine("\n---");

        var agentSession = await _mafAgent.CreateSessionAsync();
        await CallAgentAndLogTokensUsage(initialPrompt, agentSession);
        await CallAgentAndLogTokensUsage(followUpQuestion, agentSession);
    }

    private async Task CallAgentAndLogTokensUsage(string message, AgentSession? agentSession = null)
    {
        var agentResponse = await _mafAgent.RunAsync(message, agentSession);

        Console.WriteLine(agentResponse.Text);
        Console.WriteLine($"Input Tokens: {agentResponse.Usage!.InputTokenCount}, Output Tokens: {agentResponse.Usage.OutputTokenCount}\n\n");
    }
}

Result:

Without SESSION
Nice to meet you, Michal.
Input Tokens: 45, Output Tokens: 12

I don't know your name or occupation.
Input Tokens: 36, Output Tokens: 13

---
Nice to meet you, Michal-future-ready Azure AI sounds great.
Input Tokens: 45, Output Tokens: 19

Your name is Michal, and you build future-ready Azure AI solutions.
Input Tokens: 83, Output Tokens: 19

LLMs are completely stateless by default. If you don’t explicitly track and send the preceding chat history with every new request, the model will instantly forget any context shared in earlier turns. The Microsoft Agent Framework manages this seamlessly using the AgentSession abstraction.

Key takeaways:

  • Stateless vs. Stateful Boundaries: In the first execution block, the agent runs in isolation. It answers the initial introduction but completely fails to answer the follow-up question because the context was not retained.
  • The Token Footprint Proof: Look closely at the token logging results inside the stateful session blocks. The first call consumes 45 input tokens. The second follow-up call jumps to 83 input tokens. This proves that MAF is automatically capturing the prior user prompt, the model’s first reply, and appending them to the chat payload before sending the query.
  • Pluggable Architecture: By default, CreateSessionAsync() tracks this history in memory. However, for distributed cloud microservices or multi-instance environments, the underlying state provider can be swapped out to persist these session logs inside external distributed stores like Azure Cosmos DB (I will cover that in details in a dedicated blog post so stay tuned!).

Example 7: Persistent Memory and Session Serialization

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Persistent Memory and Session Serialization
/// </summary>
public class MemoryExample
{
    private readonly AIAgent _mafAgent;

    // ctor omitted for brevity

    public async Task RunAsync()
    {
        var agentSession = await _mafAgent.CreateSessionAsync();
        Console.WriteLine(await _mafAgent.RunAsync("My name is Michal and I help people to build future-ready AI solutions deployed in Azure.", agentSession));
        Console.WriteLine(await _mafAgent.RunAsync("What is my name and what do I do?", agentSession));

        var serializedSession = await _mafAgent.SerializeSessionAsync(agentSession);
        Console.WriteLine();
        Console.WriteLine(serializedSession);

        Console.WriteLine("-----\n Persist session to database");
        Console.WriteLine("-----\n Read session from database and continue \n-----");

        var deserializedSession = await _mafAgent.DeserializeSessionAsync(serializedSession);
        Console.WriteLine(await _mafAgent.RunAsync("Can you repeat please?", deserializedSession));
    }
}

In a production cloud environment, keeping chat state purely in-memory is a major anti-pattern. If your backend container restarts, your users lose their entire conversation history. This step of our Microsoft Agent Framework tutorial addresses state persistence directly.

The Microsoft Agent Framework .NET ecosystem provides native session serialization methods to solve this challenge out of the box.

Key takeaways:

  • Native State Externalization: By calling SerializeSessionAsync, you can extract the full conversation history into a standard string format. This string can be easily stored in external databases.
  • Preserved Context: Even after destroying the original session instance, the reloaded deserializedSession retains perfect memory of the user’s name and intent, delivering a cohesive user experience through your Azure OpenAI agent C# service.

Extending Agent Capabilities (Examples 8-10)

Example 8: Enterprise Observability and OpenTelemetry Tracing

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Enterprise Observability and OpenTelemetry Tracing
/// </summary>
public class ObservabilityExample
{
    public async Task RunAsync()
    {
        var openAiClient = new AzureOpenAIClient(
            new Uri(Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CLIENT_URI")!),
            new DefaultAzureCredential());

        string sourceName = Guid.NewGuid().ToString("N");
        var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder()
            .AddSource(sourceName)
            .AddConsoleExporter();

        using var tracerProvider = tracerProviderBuilder.Build();

        var mafAgent = openAiClient
            .GetChatClient(deploymentName: Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CHAT_CLIENT_DEPLOYMENT_NAME")!)
            .AsAIAgent(
                instructions: "You are a helpful assistant that gives concise answers.",
                name: "Agent with OpenTelemetry")
            .AsBuilder()
            .UseOpenTelemetry(sourceName)
            .Build();

        Console.WriteLine(await mafAgent.RunAsync("Hello, how are you?"));
    }
}

Result:

Activity.TraceId:            fbe26e0d0707afeae871e2012fea57f3
Activity.SpanId:             0e24f66636c594da
Activity.TraceFlags:         Recorded
Activity.ParentSpanId:       a3f5aab1e7863348
Activity.DisplayName:        chat gpt-5.4-mini
Activity.Kind:               Client
Activity.StartTime:          2026-05-19T18:27:23.0082889Z
Activity.Duration:           00:00:10.5893301
Activity.Tags:
    gen_ai.operation.name: chat
    gen_ai.request.model: gpt-5.4-mini
    gen_ai.provider.name: openai
	
... trimmed for brevity

Moving C# AI agents from a local hobby project into an enterprise production environment requires absolute visibility into execution paths, token usage, and latency. This section of our Microsoft Agent Framework tutorial demonstrates how to capture deep system diagnostics natively.

The Microsoft Agent Framework .NET ecosystem integrates directly with industry-standard diagnostics to make profiling straightforward.

Key takeaways:

  • The Fluent Builder Pattern: Instead of executing a basic agent directly, you can append the .AsBuilder() method. This unlocks a configuration workflow that lets you layer complex infrastructure behaviors—like telemetry, safety filters, and policies—onto your agent before calling .Build().
  • Standardized OpenTelemetry Hook: By passing your diagnostic source name into .UseOpenTelemetry(), the framework automatically registers spans for every interaction. It tracks underlying execution details, including duration, tool invocation paths, and inference model responses.
  • Cloud-Ready Cloud Architecture: Because this uses standard OpenTelemetry abstractions, you aren’t limited to console logs. You can route these telemetry traces straight into distributed APM platforms like Azure Application Insights, Prometheus, or Grafana

Example 9: Local Grounded Context and Simple RAG

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Local Grounded Context and Simple RAG
/// </summary>
public class SimpleRagExample
{
    private readonly AIAgent _mafAgent;

    public SimpleRagExample()
    {
        var openAiClient = new AzureOpenAIClient(
            new Uri(Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CLIENT_URI")!),
            new DefaultAzureCredential());

        var agentOptions = new ChatClientAgentOptions
        {
            ChatOptions = new()
            {
                Instructions = "You are a helpful support specialist. Answer questions using ONLY the provided context and cite the source document name."
            },
            AIContextProviders = [new TextSearchProvider(SearchMethodAsync, new TextSearchProviderOptions
            {
                SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke
            })]
        };

        _mafAgent = openAiClient
            .GetChatClient(deploymentName: Environment.GetEnvironmentVariable("AZURE_OPEN_AI_CHAT_CLIENT_DEPLOYMENT_NAME")!)
            .AsAIAgent(agentOptions);
    }

    public async Task RunAsync()
    {
        // MAF automatically calls SearchMethodAsync under the hood before running the LLM
        Console.WriteLine(await _mafAgent.RunAsync("Can I work remotely on Mondays in 2026?"));
    }

    private Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchMethodAsync(
        string query,
        CancellationToken cancellationToken)
    {
        List<TextSearchProvider.TextSearchResult> results = [];

        // Simple keyword-based matching simulating a basic database row lookup
        if (query.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
            query.Contains("work", StringComparison.OrdinalIgnoreCase))
        {
            results.Add(new()
            {
                SourceName = "Corporate Remote Work Policy 2026",                    
                SourceLink = "https://internal.company.com/policies/remote-2026",
                Text = "The 2026 remote work policy allows employees to work from anywhere within Poland for up to 3 days per week. Mondays are mandatory office days."
            });
        }

        if (query.Contains("bike", StringComparison.OrdinalIgnoreCase) ||
            query.Contains("benefit", StringComparison.OrdinalIgnoreCase))
        {
            results.Add(new()
            {
                SourceName = "Employee Benefits Guide",
                SourceLink = "https://internal.company.com/benefits/bike",
                Text = "The corporate bicycle benefit covers up to 2000 PLN for purchasing or maintaining cross and mountain bikes."
            });
        }

        return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>(results);
    }
}

Result:

No - under the **Corporate Remote Work Policy 2026**, **Mondays are mandatory office days**, so you cannot work remotely on Mondays. The policy does allow working from anywhere within Poland for up to 3 days per week on other days.

**Source:** Corporate Remote Work Policy 2026 - https://internal.company.com/policies/remote-2026

This example shows how the framework handles Retrieval-Augmented Generation (RAG) loops natively without forcing you to manually orchestrate the text splitting, fetching, and prompt engineering phases.

Key takeaways:

  • Automated Context Injection: By registering a TextSearchProvider inside AIContextProviders and setting the behavior to BeforeAIInvoke, the framework automatically intercepts the user query. It calls your custom retrieval method before the LLM runs, completely removing manual string-concatenation boilerplate from your code.
  • Decoupled Data Fetching: The local SearchMethodAsync acts as an isolated data gateway. While this tutorial simulates a basic local keyword match, the clean interface allows you to easily drop in a production database call, an internal repository, or an Azure AI Search index without altering your core agent logic.
  • Strict Grounding Boundaries: Under the hood, the framework packages the returned chunks directly into the message history payload. When combined with strict system instructions (“use ONLY the provided context”), this pattern creates a reliable boundary that forces the model to stick to your data.

Example 10: Structured Processing with Workflows

/// <summary>
/// Microsoft Agent Framework Tutorial, Example: Structured Processing with Workflows
/// </summary>
public class WorkflowsExample
{
    public async Task RunAsync()
    {
        Func<string, string> trimFunc = s => string.Join(' ', s.Split(' ', StringSplitOptions.RemoveEmptyEntries));
        var trim = trimFunc.BindAsExecutor("TrimExtraWhitespaceExecutor");

        var wordCount = new WordCountExecutor();

        var builder = new WorkflowBuilder(trim);
        builder.AddEdge(trim, wordCount).WithOutputFrom(wordCount);
        var workflow = builder.Build();

        await using var run = await InProcessExecution.RunAsync(workflow, "  Deployed   in    Azure   ");
        foreach (var completedEvent in run.NewEvents.OfType<ExecutorCompletedEvent>())
        {
            Console.WriteLine($"Executor [{completedEvent.ExecutorId}] > Result: {completedEvent.Data}");
        }
    }

    private class WordCountExecutor() : Executor<string, string>("WordCountExecutor")
    {
        public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
        {
            int count = message.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
            return ValueTask.FromResult($"'{message}' contains {count} word(s).");
        }
    }
}

Result:

Executor [TrimExtraWhitespaceExecutor] > Result: Deployed in Azure
Executor [WordCountExecutor] > Result: 'Deployed in Azure' contains 3 word(s).

While agents excel at open-ended reasoning, certain production tasks demand deterministic, structured execution loops. The framework provides a dedicated event-driven directed graph engine to build strict multi-step pipelines where reliability and sequential execution are non-negotiable.

Key takeaways:

  • Flexible Node Creation: The framework allows you to build execution steps (Executors) in two ways. For simple data transformations, you can instantly turn a standard C# delegate or lambda into a node using BindAsExecutor. For heavier business logic, you can derive directly from the Executor<TIn, TOut> base class.
  • Explicit Graph Topologies: Using WorkflowBuilder, you define precise paths by adding edges between nodes (builder.AddEdge(trim, wordCount)). This allows you to chain preprocessing, agent evaluation, validation, and post-processing steps together in code, ensuring data flows correctly from one segment to the next.

Summary

I hope this Microsoft Agent Framework tutorial helped you to realize what capabilities are available within this lightweight ecosystem and how easily it handles core agent orchestration. Many of these topics will be discussed in detail in future posts, especially regarding their integration with various Azure services.

Thanks for reading and see you in the next post!

Categorized in:

Agents,