Building Production AI Agents in .NET
August 8, 2026 · PanaceaLogics Team

There is a large gap between an agent that demos well and an agent that runs unattended against your production systems. The demo needs a model and a prompt. Production needs authorization, idempotency, observability and a way to prove the thing still works after you change it.
This assumes you have already decided an agent is the right answer. If you have not, start with choosing your first use case and the three levels of adding AI to an existing app. What follows is the engineering once the decision is made.
Your tools are just your services
The central idea in .NET is unglamorous: an agent tool is a method you already have. You are not writing AI code so much as exposing existing business logic in a form a model can invoke.
[KernelFunction("get_overdue_invoices")]
[Description("Returns invoices past their due date for a customer.")]
public async Task<IReadOnlyList<InvoiceDto>> GetOverdueInvoicesAsync(
[Description("The customer's account number")] string accountNumber,
CancellationToken ct = default)
=> await _invoices.GetOverdueAsync(accountNumber, ct);
The description attributes are not documentation, they are the interface the model reasons about. A vague description is the single most common cause of an agent calling the wrong tool, and it is far cheaper to fix than a prompt.
Three rules that save a lot of pain:
Keep tools narrow. GetOverdueInvoices beats QueryInvoices(filter). A model given a flexible query API will construct filters you did not anticipate. A model given ten specific verbs behaves predictably.
Return typed DTOs, not entities. Never hand the model your EF entities. It will see navigation properties, internal flags and anything else you forgot was on there.
Make write tools idempotent and give them a key. The agent will retry. CreateTicket(idempotencyKey, ...) means a retry is safe; without it you get duplicate tickets in production and no obvious cause.

Authorization is the part people skip
This is where agent projects create genuine risk, and it is worth being blunt: the agent must never be the security boundary.
An agent asked “show me the CEO’s salary” should fail because the calling user lacks permission, not because the prompt told it not to. Prompts are guidance; they are not access control, and they can be talked around.
In practice that means flowing the user’s identity through to every tool call and letting your existing authorization run exactly as it would for a normal request:
public async Task<IReadOnlyList<InvoiceDto>> GetOverdueInvoicesAsync(string accountNumber, CancellationToken ct)
{
// Same check the controller would make. The agent gets no special path.
await _authz.AuthorizeOrThrowAsync(_user, Policy.ViewInvoices, accountNumber, ct);
return await _invoices.GetOverdueAsync(accountNumber, ct);
}
If the agent runs on a service principal with broad rights, you have built a privilege escalation route with a chat interface. Retrieval must be permission-trimmed at query time for the same reason.
State, and how much of it to keep
Agents need memory, and teams usually reach for too much of it.
- Conversation state is the current thread. Keep it, cap it, and summarise older turns rather than letting the context window fill with history nobody needs.
- Working state is what the agent has established during this task. Hold it explicitly in your own code rather than hoping it stays in the transcript.
- Long-term memory is durable facts about the user or account. This is genuinely useful and genuinely risky, because anything remembered is anything that can be leaked or become stale. Store it in your database with normal access control, not in a vector blob nobody governs.

The evaluation loop is the real deliverable
Without evaluation you cannot ship changes safely, because you cannot tell an improvement from a regression. Any prompt edit becomes a gamble.
Build a set of real inputs with expected outcomes, ideally 50 or more drawn from actual usage, and run it on every change. What to assert:
- Did it choose the right tool? This is the highest-signal check and the cheapest to write.
- Did it extract the right arguments? Wrong account number, wrong date range.
- Did it refuse when it should? Include cases that must escalate rather than answer.
- Is the answer grounded? Every claim traceable to a retrieved source.
Run this in CI. An agent without a regression suite is a system you can only change by feel.
Observability, because you will be asked
Log the full trace of every run: the input, retrieved context, each tool call with its arguments, the tokens consumed and the final output. When someone asks in six months why the system told a customer something, that log is the entire answer.
Attribute token cost per feature from day one. AI spend is the easiest line in a modern architecture to lose track of, and the conversation with finance goes much better when you can answer precisely.
Where a human stays in the loop
Reading, drafting and summarising can be automatic. Anything that writes to a record, sends a message or moves money should be reviewed until you have evidence it is safe, and the review should be a real decision rather than a rubber stamp.
The pattern that works: the agent prepares the action, a person approves it in one click, and the approval is logged with who and when. You get most of the time saving with none of the exposure, and you accumulate exactly the evidence you need to justify removing the checkpoint later.
We build agentic systems in .NET on Azure OpenAI and Semantic Kernel, wired into the services and permissions you already have. See our AI agents and copilots service, or get in touch.