Wednesday, September 9, 2026

Build an AI Agent with C# and .NET 8

In the previous article, we learned what an AI Agent is, how the agent loop works, and how agents can use tools to accomplish tasks.

Now let's build a simple AI Agent using C# and .NET 8.

The goal of this tutorial is not to build a complicated autonomous system. Instead, we will create a simple agent that demonstrates the core concepts:

  • LLM integration
  • Tool calling
  • Agent decision-making
  • Tool execution
  • Observations
  • Agent loop

Once you understand this basic architecture, you can extend it with RAG, MCP, databases, APIs, memory, and multiple tools.


What We Are Building

We will create a simple customer-support AI agent.

The user will be able to ask something like:

"What is the status of order 1001?"

The agent will decide that it needs to call an order lookup tool.

User
 ↓
AI Agent
 ↓
LLM
 ↓
Choose Tool
 ↓
get_order_status()
 ↓
Order System
 ↓
Tool Result
 ↓
LLM
 ↓
Final Answer

Prerequisites

Before starting, make sure you have:

  • .NET 8 SDK
  • Visual Studio 2022 or Visual Studio Code
  • Basic C# knowledge
  • Basic understanding of APIs and JSON
  • Access to an LLM API or a locally running compatible model

You can also use a local LLM if your model and client support tool/function calling.


Step 1: Create a .NET 8 Console Application

Open a terminal and create a new console application:

dotnet new console -n SimpleAiAgent
cd SimpleAiAgent

Open the project in Visual Studio or Visual Studio Code.


Step 2: Install the OpenAI .NET SDK

For this example, we will use the official OpenAI .NET client.

Install the NuGet package:

dotnet add package OpenAI

The same general architecture can also be adapted to other LLM providers that support compatible tool/function calling.


Step 3: Configure the API Key

For a quick local test, you can configure your API key as an environment variable.

On Windows:

setx OPENAI_API_KEY "YOUR_API_KEY"

Restart your terminal after setting the environment variable.

Security Note: Never commit API keys directly into source control. Use environment variables, user secrets, managed identity, or a secure secret-management solution for production applications.

Step 4: Create an Order Service

Let's create a simple C# service that represents an external business system.

Create a class named OrderService.cs:

public class OrderService
{
    public string GetOrderStatus(int orderId)
    {
        if (orderId == 1001)
        {
            return "Order 1001 is shipped and is expected to arrive tomorrow.";
        }

        if (orderId == 1002)
        {
            return "Order 1002 is being processed.";
        }

        return $"Order {orderId} was not found.";
    }
}

In a real application, this method could call:

  • A REST API
  • SQL Server
  • An ERP system
  • An order-management system
  • A microservice

The AI agent does not need to know how the data is retrieved. It only needs a well-defined tool.


Step 5: Define the Agent Tool

We need to expose the order lookup operation to the LLM.

The tool definition tells the model:

  • What the tool is called
  • What the tool does
  • What parameters it accepts

A conceptual tool definition looks like:

Tool Name:
get_order_status

Description:
Gets the current status of a customer order.

Parameter:
orderId - The order number to look up.

The description is important because the model uses it to determine when the tool should be used.


Step 6: Create the Tool Definition in C#

Using the OpenAI .NET SDK, we can describe the function to the model.

using OpenAI.Chat;

var getOrderStatusTool = ChatTool.CreateFunctionTool(
    functionName: "get_order_status",
    functionDescription: "Gets the current status of a customer order.",
    functionParameters: BinaryData.FromString("""
    {
        "type": "object",
        "properties": {
            "orderId": {
                "type": "integer",
                "description": "The order number to look up."
            }
        },
        "required": ["orderId"]
    }
    """)
);

The JSON schema tells the model what arguments the function expects.


Step 7: Create the Chat Client

Now we can create the chat client.

using OpenAI.Chat;

string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
    ?? throw new InvalidOperationException("OPENAI_API_KEY is not configured.");

ChatClient client = new(
    model: "gpt-4o-mini",
    apiKey: apiKey
);
Note: Model names and availability can change over time. Use a model that is currently available to your account and supports the capabilities required by your application.

Step 8: Send the User Request

Let's start with a simple request:

string userMessage =
    "What is the status of order 1001?";

We now send the message to the LLM along with the tool definition.

List messages =
[
    new UserChatMessage(userMessage)
];

ChatCompletionOptions options = new()
{
    Tools = { getOrderStatusTool }
};

ChatCompletion completion =
    client.CompleteChat(messages, options);

Step 9: Detect a Tool Call

The model may determine that it needs to use the get_order_status tool.

We can inspect the response:

if (completion.FinishReason == ChatFinishReason.ToolCalls)
{
    Console.WriteLine("The agent requested a tool.");
}

The model can return a tool call containing the function name and arguments.

Conceptually, the response may look like:

Function:
get_order_status

Arguments:
{
    "orderId": 1001
}

Step 10: Execute the Tool

Now our C# application needs to execute the requested function.

OrderService orderService = new();

string result =
    orderService.GetOrderStatus(1001);

Console.WriteLine(result);

The output will be:

Order 1001 is shipped and is expected to arrive tomorrow.

This is an important concept:

The LLM does not directly execute your C# method. The application receives the tool request, validates it, executes the appropriate code, and sends the result back to the model.

Step 11: Send the Tool Result Back to the LLM

After executing the tool, the application sends the result back to the model.

messages.Add(new AssistantChatMessage(completion));

messages.Add(
    new ToolChatMessage(
        toolCallId,
        result
    )
);

completion = client.CompleteChat(messages, options);

The LLM now has the information returned by the tool.


Step 12: Generate the Final Answer

The model can now generate a natural-language response.

Console.WriteLine(completion.Content[0].Text);

Example:

Order 1001 has been shipped and is expected
to arrive tomorrow.

The complete flow is:

User
 ↓
"What is the status of order 1001?"
 ↓
LLM
 ↓
Tool Call
 ↓
get_order_status(1001)
 ↓
C# OrderService
 ↓
Order System
 ↓
Tool Result
 ↓
LLM
 ↓
Final Answer

Step 13: Build the Agent Loop

A real agent may need to perform more than one action.

For example:

User:
"Find order 1001 and tell me whether
it is likely to arrive tomorrow."

Agent:

1. Get order status
2. Get shipment information
3. Check delivery estimate
4. Analyze results
5. Generate answer

Therefore, we need a loop.

while (true)
{
    Send messages to LLM

    if (LLM requests a tool)
    {
        Execute tool
        Add tool result to messages
        Continue
    }

    Return final response
    break;
}

This is the foundation of an agent loop.


Complete Simplified Agent Example

The following example demonstrates the overall architecture:

using OpenAI.Chat;

string apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
    ?? throw new InvalidOperationException("OPENAI_API_KEY is not configured.");

ChatClient client = new(
    model: "gpt-4o-mini",
    apiKey: apiKey
);

OrderService orderService = new();

var tool = ChatTool.CreateFunctionTool(
    functionName: "get_order_status",
    functionDescription: "Gets the current status of a customer order.",
    functionParameters: BinaryData.FromString("""
    {
        "type": "object",
        "properties": {
            "orderId": {
                "type": "integer",
                "description": "The order number."
            }
        },
        "required": ["orderId"]
    }
    """)
);

List messages =
[
    new UserChatMessage(
        "What is the status of order 1001?"
    )
];

ChatCompletionOptions options = new()
{
    Tools = { tool }
};

while (true)
{
    ChatCompletion completion =
        client.CompleteChat(messages, options);

    if (completion.FinishReason == ChatFinishReason.ToolCalls)
    {
        messages.Add(new AssistantChatMessage(completion));

        foreach (ChatToolCall toolCall in completion.ToolCalls)
        {
            if (toolCall.FunctionName == "get_order_status")
            {
                int orderId =
                    int.Parse(toolCall.FunctionArguments
                        .ToString()
                        .Replace("{\"orderId\":", "")
                        .Replace("}", ""));

                string result =
                    orderService.GetOrderStatus(orderId);

                messages.Add(
                    new ToolChatMessage(
                        toolCall.Id,
                        result
                    )
                );
            }
        }

        continue;
    }

    Console.WriteLine(completion.Content[0].Text);
    break;
}
Important: The JSON argument parsing above is intentionally simplified for demonstration. Production applications should deserialize tool arguments using System.Text.Json, validate all inputs, and handle malformed requests safely.

Production-Ready Argument Parsing

Instead of manually parsing JSON strings, use System.Text.Json.

using System.Text.Json;

public class OrderRequest
{
    public int OrderId { get; set; }
}

OrderRequest request =
    JsonSerializer.Deserialize<OrderRequest>(
        toolCall.FunctionArguments.ToString()
    )!;

Then execute:

string result =
    orderService.GetOrderStatus(request.OrderId);

This is safer and easier to maintain.


What Makes This an AI Agent?

You might ask:

"Isn't this just function calling?"

Function calling is one of the building blocks.

The agent behavior comes from combining the model with a loop that can:

  • Understand the user's goal
  • Choose a tool
  • Execute the tool
  • Observe the result
  • Decide whether another action is required
  • Return the final result

In simplified form:

LLM
 ↓
Decision
 ↓
Tool
 ↓
Observation
 ↓
LLM
 ↓
Decision
 ↓
Tool
 ↓
Observation
 ↓
Final Answer

Adding Multiple Tools

An agent becomes more useful when it has multiple tools.

For example:

get_order_status()
get_customer()
check_inventory()
get_shipment()
search_knowledge_base()
send_email()

The model can select the appropriate tool based on the user's request.

For example:

User:
"Is Product 123 available?"

        ↓

Agent

        ↓

check_inventory(123)

Another request might result in:

User:
"Where is my order?"

        ↓

Agent

        ↓

get_order_status(1001)

Tool Dispatcher Pattern

When you have many tools, it is useful to create a central dispatcher.

switch (toolCall.FunctionName)
{
    case "get_order_status":
        return GetOrderStatus(toolCall);

    case "get_customer":
        return GetCustomer(toolCall);

    case "check_inventory":
        return CheckInventory(toolCall);

    default:
        throw new InvalidOperationException(
            $"Unknown tool: {toolCall.FunctionName}");
}

This keeps the agent loop separate from the business logic.


Recommended .NET Architecture

For a production application, avoid putting everything inside Program.cs.

A cleaner structure could be:

SimpleAiAgent/
│
├── Agents/
│   ├── AiAgent.cs
│   └── AgentOptions.cs
│
├── Tools/
│   ├── IAgentTool.cs
│   ├── OrderTool.cs
│   ├── CustomerTool.cs
│   └── InventoryTool.cs
│
├── Services/
│   ├── OrderService.cs
│   └── CustomerService.cs
│
├── Models/
│   ├── Order.cs
│   └── Customer.cs
│
├── Program.cs
└── appsettings.json

This separation makes the application easier to test and maintain.


Adding an Interface for Tools

You can define a common interface:

public interface IAgentTool
{
    string Name { get; }

    string Description { get; }

    Task<string> ExecuteAsync(
        string arguments);
}

Each tool can implement this interface.

public class OrderTool : IAgentTool
{
    public string Name => "get_order_status";

    public string Description =>
        "Gets the current status of a customer order.";

    public Task<string> ExecuteAsync(string arguments)
    {
        // Validate arguments
        // Call order service
        // Return result

        return Task.FromResult(
            "Order is shipped."
        );
    }
}

Adding RAG to the Agent

We can extend the agent by adding a RAG tool.

search_company_documents()

Now the agent can answer questions using company documentation.

User
 ↓
Agent
 ↓
LLM
 ↓
Need company information?
 ↓
RAG Tool
 ↓
Vector Database
 ↓
Relevant Documents
 ↓
LLM
 ↓
Answer

This is a powerful combination of AI agents and RAG.


Adding MCP

MCP can also be incorporated into an agent architecture.

AI Agent
    ↓
MCP Client
    ↓
MCP Server
    ↓
Tools
    ↓
External Systems

Instead of implementing every integration directly inside the agent application, MCP can provide a standardized way for compatible AI applications to discover and use external capabilities.


Adding Memory

An agent can also maintain conversation or task state.

Conversation

User:
"My order is 1001."

Agent:
"Order 1001 is shipped."

User:
"When will it arrive?"

Agent:
"Order 1001 is expected tomorrow."

The agent needs the previous context to understand what the user means by it.

For longer-lived applications, memory can be stored in databases or other persistent storage systems.


Adding Guardrails

Production agents should not be allowed to execute arbitrary operations without controls.

Consider a tool such as:

delete_customer()
refund_payment()
cancel_order()

These operations can have real business consequences.

Therefore, implement:

  • Authentication
  • Authorization
  • Input validation
  • Tool allowlists
  • Rate limiting
  • Timeouts
  • Maximum agent steps
  • Audit logging
  • Human approval for sensitive operations

Maximum Agent Steps

An agent should not be allowed to loop forever.

For example:

const int maxSteps = 10;

for (int step = 0; step < maxSteps; step++)
{
    // Ask LLM
    // Execute tools
    // Continue or finish
}

If the maximum number of steps is reached, stop the agent and return an appropriate message.


Error Handling

Tools can fail.

For example:

Database unavailable
API timeout
Invalid order ID
Authorization failure
Network failure

Your tool layer should handle these errors gracefully.

try
{
    var result = await orderService.GetOrderStatusAsync(orderId);

    return result;
}
catch (Exception ex)
{
    // Log exception
    // Return safe tool result
}

The agent can then decide whether it should retry, use another tool, or report the problem.


Logging an AI Agent

Agent applications should have detailed logging.

Useful information includes:

  • User request
  • Agent execution ID
  • Tool selected
  • Tool arguments
  • Tool execution time
  • Tool result status
  • LLM response time
  • Number of agent steps
  • Errors

Be careful not to log sensitive information such as passwords, API keys, payment information, or unnecessary personal data.


Simple Agent Architecture for .NET

                   User
                     ↓
                ASP.NET Core
                     ↓
                 AI Agent
                     ↓
                    LLM
                     ↓
              Tool Dispatcher
                     ↓
        ┌────────────┼────────────┐
        ↓            ↓            ↓
     Order API    Database      RAG
        ↓            ↓            ↓
      Data         Data       Documents
        └────────────┼────────────┘
                     ↓
                 Tool Result
                     ↓
                    LLM
                     ↓
                 Final Answer

Agent vs Normal API

It is important to understand when an agent is actually necessary.

If your application simply needs:

GET /orders/1001

you probably do not need an AI agent.

A normal API is simpler and more predictable.

An agent becomes more useful when the request is open-ended:

"Investigate why order 1001 is delayed
and tell me what action we should take."

The agent may need to combine several operations:

Get Order
   ↓
Check Payment
   ↓
Check Shipment
   ↓
Search Documentation
   ↓
Analyze
   ↓
Recommend Action

Common Mistakes When Building AI Agents

1. Giving the Agent Too Many Tools

Providing hundreds of tools can make tool selection more difficult and increase complexity.

2. Poor Tool Descriptions

Tool descriptions should clearly explain what the tool does and when it should be used.

3. No Validation

Never trust tool arguments simply because they came from an LLM.

4. No Step Limit

Always protect the application from endless agent loops.

5. No Authorization

Agents should operate with only the permissions they actually need.

6. Using an Agent for Everything

Simple deterministic operations are often better implemented using normal application logic.


Agentic Application Design Principle

A useful principle is:

Use AI for decisions that benefit from flexibility, and use deterministic code for operations that require predictability.

For example:

AI:
"What should I do next?"

Code:
"How do I execute this operation safely?"

The AI can select the tool, while your application remains responsible for validation, security, business rules, and actual execution.


Complete Flow

Our simple AI agent can now be represented as:

                User
                  ↓
          "Check order 1001"
                  ↓
              AI Agent
                  ↓
                 LLM
                  ↓
          Choose Tool
                  ↓
        get_order_status()
                  ↓
          Validate Input
                  ↓
          Execute C# Code
                  ↓
          Order Service
                  ↓
          Business System
                  ↓
            Tool Result
                  ↓
                 LLM
                  ↓
            Final Answer

Key Takeaways

  • An AI agent can be built using an LLM, tools, application logic, and an execution loop.
  • Function calling is an important building block for tool-based agents.
  • The LLM decides which tool may be useful, but the application executes the tool.
  • Tool arguments must be validated before execution.
  • Multiple tools allow agents to perform more complex tasks.
  • RAG can provide an agent with access to external knowledge.
  • MCP can provide a standardized way to connect compatible AI applications with external capabilities.
  • Memory can maintain useful conversation or task state.
  • Production agents need security, authorization, logging, error handling, and step limits.
  • Not every application needs an AI agent.
  • Deterministic code should continue to handle critical business rules and operations.

Conclusion

Building an AI agent does not require a huge framework to understand the fundamentals.

At its core, an agent can be thought of as:

Goal
 ↓
LLM
 ↓
Choose Tool
 ↓
Execute Tool
 ↓
Observe Result
 ↓
LLM
 ↓
Next Action
 ↓
Repeat
 ↓
Final Answer

With C# and .NET 8, you can build this architecture using familiar concepts such as classes, interfaces, dependency injection, APIs, services, logging, and asynchronous programming.

Once the basic agent works, you can gradually add RAG, MCP, memory, additional tools, authentication, observability, and human approval.

The important thing is to start with a small, well-defined agent and expand its capabilities only when the use case requires them.

Next: AI Agent Memory Explained: Short-Term vs Long-Term Memory — Learn how AI agents maintain conversation context, task state, and persistent knowledge.

No comments:

Post a Comment