Skip to main content

Command Palette

Search for a command to run...

AI Agents in ASP.NET Core with Microsoft Agent Framework: When to Use Them and How

Updated
13 min readView as Markdown
AI Agents in ASP.NET Core with Microsoft Agent Framework: When to Use Them and How

Since Microsoft Agent Framework reached 1.0 GA in April 2026, "should we use an agent for this?" has become one of the most common design questions I get from .NET teams. Using Microsoft Agent Framework in ASP.NET Core is now a genuinely production-ready option, not a research toy, and it collapses the old choice between AutoGen's simple abstractions and Semantic Kernel's enterprise plumbing into one supported library. But the framework being ready does not mean an agent is the right shape for your endpoint. In production I have shipped both agent-backed and plain tool-calling endpoints, and the ones that caused 2 a.m. pages were almost always the agents I reached for too early.

This article is the decision I run through before wiring an agent into an API: what an agent is, when the reason-act-observe loop earns its cost, when a simpler call is the better choice, and how the core pieces fit together in ASP.NET Core. The patterns here are deliberately conceptual - the full annotated support-triage agent, with retries, guardrails, and a test suite, lives on Patreon so you can clone it and adapt it to your own domain rather than rebuild it from scratch.

Getting an agent right in a real API means handling tool safety, conversation state, and cost at the same time, not one at a time. That combination is exactly what Chapter 12 of the AI-Powered .NET APIs course builds end to end - a support triage agent wired into one running ASP.NET Core API, so the context around every decision is always in front of you.

AI-Powered .NET APIs

What an AI Agent Actually Is (And What It Is Not)

An agent is a loop, not a feature. A plain LLM call takes a prompt and returns a completion in one shot. An agent takes a goal, decides which tools to call to make progress, observes the results, and decides again - reason, act, observe - until it believes the goal is met. That loop is the entire value proposition and also the entire risk. It lets the model handle a task whose steps you cannot fully script in advance, and it hands the model the keys to decide how many times to call your code.

Microsoft Agent Framework gives you this loop as a first-class type, and the official Microsoft Agent Framework documentation is the canonical reference for the API surface. It sits directly on top of Microsoft.Extensions.AI, so an agent is really a thin, stateful wrapper over the same IChatClient you already use for chat and structured outputs. If you have read MEAI vs Semantic Kernel vs Agent Framework in .NET, this is the concrete "what to build with" that follows that "what to choose" decision.

What an agent is not: it is not required for tool calling. A single IChatClient call can already invoke one tool and return. It is not a workflow engine for steps you fully control - a switch statement is cheaper and more debuggable. And it is not a way to avoid validating model output. The loop makes the model more capable, not more trustworthy.

When Does an AI Agent Make Sense in ASP.NET Core?

Reach for an agent when the number and order of steps genuinely depend on the input. A support request that might need an order lookup, then a refund-policy check, then a ticket creation - in an order that varies per message - is a real agent case. The model deciding the path is doing work you would otherwise write as brittle branching logic that never covers every combination.

Concretely, an agent earns its place when several of these are true:

  • The task needs more than one tool call, and which tools depend on intermediate results.

  • The sequence varies per request, so you cannot hard-code the flow.

  • The task benefits from multi-turn context - the user clarifies, the agent adjusts.

  • A wrong path is recoverable and cheap, not a destructive one-way action.

These are the same signals that make traditional code hard to write. That is the tell: an agent is worth its overhead precisely when the alternative is a tangle of conditional orchestration you would dread maintaining.

When an Agent Is Overkill

Most endpoints that teams label "agentic" are not. If your flow is "extract fields from this text and return JSON," that is a structured-output call, not an agent - the difference is covered in Structured Outputs vs Tool Calling in .NET APIs. If it is "call exactly this one function and summarise the result," that is single-turn tool calling. If it is "run these three steps in this fixed order," that is a method with three awaits.

The trade-off that bit us first was cost and latency. Every loop iteration is another round trip, and the model decides how many to run. An endpoint I expected to make one model call was making four on ambiguous inputs, quadrupling latency and token spend before we capped it. When the flow is deterministic, an agent buys nothing but variance - use the loop only when the variance is the point.

The Core Building Blocks: Agent, Thread, and Tools

Three types carry almost everything. An AIAgent is the configured loop - instructions plus the tools it may call. An AgentThread is the conversation state. And tools are ordinary C# methods surfaced through AIFunctionFactory.

Creating an agent is a single expression built on the chat client you already know. The provider is a one-line detail, exactly as with plain IChatClient - swap OpenAIClient for Azure OpenAI, GitHub Models, or a local Ollama model without touching anything downstream.

// Microsoft.Agents.AI 1.x (GA April 2026) on .NET 10, built on Microsoft.Extensions.AI 10.x
AIAgent agent = new OpenAIClient(apiKey)
    .GetChatClient("gpt-4o-mini")
    .AsIChatClient()
    .CreateAIAgent(
        instructions: "You are a support triage assistant. Be concise and use tools for facts.",
        name: "TriageAgent");

string reply = (await agent.RunAsync("Where is my order 4821?")).Text;

Tools are where the agent stops guessing and starts acting on your data. You expose a plain method, describe it, and register it - the framework handles the function-calling protocol and automatic invocation.

[Description("Looks up the delivery status for a given order id.")]
static async Task<string> GetOrderStatus(int orderId) =>
    await orderService.GetStatusAsync(orderId);

AIAgent agent = chatClient.CreateAIAgent(
    instructions: "Help customers with order questions. Use tools for any factual claim.",
    name: "TriageAgent",
    tools: [AIFunctionFactory.Create(GetOrderStatus)]);

The [Description] attribute is not decoration - it is how the model learns when to call the tool, so treat it as part of the prompt. This tool-registration mechanism is the same AIFunctionFactory used across Microsoft.Extensions.AI; the agent simply lets the model call it repeatedly inside its loop. The framework is open source, and the runnable .NET samples on GitHub are worth reading before you commit to a design.

Wiring an Agent Into an ASP.NET Core Endpoint

The official quickstarts build console apps. The interesting problems start when the agent lives behind an HTTP endpoint. Register the agent once as a singleton - it is configuration, not per-request state - and inject it into a minimal API handler.

builder.Services.AddChatClient(
    new OpenAIClient(apiKey).GetChatClient("gpt-4o-mini").AsIChatClient());

builder.Services.AddSingleton(sp =>
    sp.GetRequiredService<IChatClient>().CreateAIAgent(
        instructions: "You are a support triage assistant.",
        name: "TriageAgent",
        tools: [AIFunctionFactory.Create(GetOrderStatus)]));
app.MapPost("/triage", async (TriageRequest req, AIAgent agent, CancellationToken ct) =>
{
    AgentThread thread = agent.GetNewThread();
    var result = await agent.RunAsync(req.Message, thread, cancellationToken: ct);
    return Results.Ok(new { answer = result.Text });
});

Keep the handler thin, as with any model call: accept a message, run the agent, return the answer. Everything hard - timeouts, retries, cost caps, telemetry - belongs in the pipeline around this, not inside the handler. And always pass the CancellationToken through: an agent loop can run several model calls, and a disconnected client should not keep spending tokens.

Managing Conversation State Across HTTP Requests

Here is the design decision that separates a demo from a shippable endpoint, and the one the console tutorials never have to face. An AgentThread holds the conversation, but HTTP is stateless. A fresh GetNewThread() per request gives every call a blank memory, which is correct for one-shot triage and wrong for a multi-turn conversation.

AgentThread thread = agent.GetNewThread();
await agent.RunAsync("My order is late.", thread);
await agent.RunAsync("What did I just ask about?", thread); // this turn remembers the first

If you need continuity across requests, you own that state - the framework will not do it for you. The pattern that survived production for us was to key threads by a conversation id the client sends back, persist the thread's history in your own store between turns, and rehydrate it on the next request. Do not hold live threads in a static dictionary in memory: it does not survive a restart, it does not scale past one instance, and it is an unbounded memory leak waiting for traffic. Treat thread history as durable data you store, cap, and trim - the same discipline you would apply to any session.

Tool Safety: Treat Every Agent Action as Untrusted

An agent decides which tools to call, which means an attacker who can influence the input can influence your tool calls. This is not hypothetical - it is prompt injection with a blast radius, because now the model does not just produce text, it triggers code. The guardrails in Preventing Prompt Injection in ASP.NET Core AI APIs apply with more force here than in a plain chat endpoint.

Three rules I do not ship an agent without. First, least privilege for tools: expose only the functions this agent genuinely needs, and give each one the narrowest scope. A read-only order-status tool is safe to hand the model; a "delete customer" tool is not. Second, validate tool arguments as untrusted input: the model chose them, so bound the order id, check ownership, reject anything outside the caller's permission - never assume the model passed a sane value. Third, require human confirmation for destructive or irreversible actions: an agent can draft a refund, but a person approves the money movement. The model gets to reason; it does not get privileges.

The Trade-offs I Weigh Before Shipping an Agent

An agent trades determinism for capability, and that trade is not free. These are the costs I price in before choosing the loop over a plain call:

  • Cost and latency scale with iterations. The model decides how many round trips to make, so a single request can cost several model calls. Cap the maximum iterations and set aggressive timeouts before launch.

  • Debuggability drops. A fixed sequence is trivial to trace; an agent's path changes per input. You will want per-run traces of every tool call and decision, which is why observability is not optional for agent endpoints.

  • Non-determinism is the default. The same input can take different paths. Your tests assert on outcomes and tool-call safety, not on an exact script.

  • State is your responsibility. As above, conversation continuity across requests is code you own.

Get these right and an agent handles genuinely open-ended work that branching logic never could. Get them wrong - reach for an agent when a switch would do - and you have bought variance, cost, and a harder page at 2 a.m. The framework is production-ready; the judgment about when to use it is still yours.

What to Do Next

You now have the decision: use an agent when the steps and their order genuinely depend on the input, and use a plain tool-calling or structured-output call when they do not. When one does fit, the shape is small - CreateAIAgent with clear instructions and least-privilege tools, an AgentThread you persist yourself, and a thin endpoint that passes the cancellation token. A natural next step is exposing those capabilities over the Model Context Protocol so any AI client can use them, which I walk through in How to Build an MCP Server in ASP.NET Core.

The core lesson from production: the agent loop is the easy part. The durable engineering is tool safety, state ownership, and cost control - the work that turns a clever demo into an endpoint you can trust with real traffic.

Frequently Asked Questions

When should I use an AI agent instead of a single LLM call in ASP.NET Core?

Use an agent only when the task needs more than one tool call and the sequence of those calls depends on intermediate results, so you cannot script the flow in advance. If the endpoint runs a fixed set of steps, extracts structured data, or calls exactly one tool, a plain IChatClient call is cheaper, faster, and far easier to debug. The reason-act-observe loop earns its cost when the variance in the task is the whole point; otherwise it just adds latency and token spend.

What NuGet packages do I need for Microsoft Agent Framework in .NET?

You need Microsoft.Agents.AI (1.x, GA April 2026) for the AIAgent and AgentThread types, plus the provider integration for your model - for example the OpenAI or Azure OpenAI client. Because the framework builds on Microsoft.Extensions.AI 10.x, you reuse the same IChatClient abstraction, so switching between OpenAI, Azure OpenAI, GitHub Models, or a local Ollama model is a one-line registration change. Target .NET 10 for the current tooling.

How do I keep conversation history across HTTP requests with an agent?

An AgentThread holds history in memory for the life of that object, but HTTP is stateless, so you must persist the thread yourself between requests. Key each conversation by an id the client sends back, store the thread's message history in your own database or cache after each turn, and rehydrate it before the next RunAsync. Never keep live threads in a static in-memory dictionary - it does not survive restarts, does not scale across instances, and grows unbounded.

Is it safe to let an AI agent call my services as tools?

Only with guardrails. The model chooses which tools to call and with what arguments, so treat every tool invocation as untrusted input: expose only the functions the agent truly needs, give each the narrowest scope, validate all arguments against the caller's permissions, and require explicit human confirmation for anything destructive or irreversible. An agent should be able to read order status freely but never move money or delete data without a person in the loop.

Do I need an agent to do tool calling in .NET?

No. A single Microsoft.Extensions.AI IChatClient call can already invoke a tool and return its result in one turn. You only need an agent when the model must call multiple tools, observe their outputs, and decide the next call dynamically. If one function invocation answers the request, skip the agent loop entirely and make the direct call - it is the simpler and more predictable design.


About the Author

I'm Celin Daniel, Co-founder of Coding Droplets. I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.

More from this blog

C

Coding Droplets

282 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.