Shared state allows middleware components to communicate and exchange data during the processing of a single agent request. No framework-specific infrastructure is required — a plain C# object captured by closure is all it takes. Every middleware method that closes over the same object instance can read and write to it throughout the pipeline.
Why Shared State?
- Correlation tracking — Stamp a unique request ID early in the pipeline; downstream middleware and logging use the same ID without passing it as a parameter.
- Cross-middleware metrics — Accumulate timing, token counts, or call numbers across pipeline stages in one place.
- Compliance audit trails — One middleware writes the context; another reads it after the agent responds to emit a structured audit record.
The Closure Pattern
Declare a shared object in the same scope as both local middleware functions:
sealed class LoanRequestContext
{
public string CorrelationId { get; set; } = string.Empty;
public int InquiryCount { get; set; }
public DateTimeOffset LastInquiryAt { get; set; }
}
var ctx = new LoanRequestContext(); // one instance, shared by both middleware below
async Task<AgentResponse> CorrelationStampMiddleware(...)
{
// Writes to shared state BEFORE the agent runs.
ctx.CorrelationId = Guid.NewGuid().ToString("N")[..12].ToUpper();
ctx.InquiryCount++;
ctx.LastInquiryAt = DateTimeOffset.UtcNow;
return await innerAgent.RunAsync(messages, session, options, ct);
}
async Task<AgentResponse> AuditLogMiddleware(...)
{
var response = await innerAgent.RunAsync(messages, session, options, ct);
// Reads from shared state AFTER the agent responds.
Console.WriteLine($"[AuditLog] ID={ctx.CorrelationId} at={ctx.LastInquiryAt:O} status=OK");
return response;
}
Both local functions close over ctx. No dependency injection or static fields are needed.
The Demo Scenario: Bank Loan Inquiry Agent
A bank wraps its loan-advisor agent with two middleware layers:
- CorrelationStampMiddleware (innermost) — Before calling the real agent, assigns a short correlation ID, increments the inquiry counter, and records the current timestamp in
LoanRequestContext. - AuditLogMiddleware (outermost) — After the agent responds, reads the correlation ID written by the inner layer and emits a compliance audit entry.
Registering the Middleware
AIAgent loanAgent = baseAgent
.AsBuilder()
.Use(runFunc: CorrelationStampMiddleware, runStreamingFunc: null) // innermost
.Use(runFunc: AuditLogMiddleware, runStreamingFunc: null) // outermost
.Build();
The last .Use() call wraps outermost and runs first. AuditLogMiddleware enters its await innerAgent.RunAsync() which reaches CorrelationStampMiddleware, which stamps the context before the real agent executes. When AuditLogMiddleware resumes after the inner await, ctx.CorrelationId is already set.
Pipeline Execution Flow
| Step | Active layer | ctx state |
|---|
| 1 | AuditLogMiddleware — calls innerAgent.RunAsync() | CorrelationId = "" (not yet set) |
| 2 | CorrelationStampMiddleware — stamps ID, increments counter | CorrelationId = "A3F8…", InquiryCount = 1 |
| 3 | Real agent runs and produces response | unchanged |
| 4 | CorrelationStampMiddleware returns response up the chain | unchanged |
| 5 | AuditLogMiddleware resumes — reads CorrelationId, writes audit log | CorrelationId = "A3F8…" ✓ available |
Using a Strongly-Typed Context vs. Dictionary
| Approach | Pros | Cons |
|---|
Strongly-typed class (LoanRequestContext) | Compile-time safety, IntelliSense, no casting | Slightly more boilerplate |
Dictionary<string, object> | Quick to set up, no extra class | Magic strings, runtime cast errors |
Prefer strongly-typed context classes for production code.
Full Example
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
namespace MicrosoftAgentFrameworkLesson.ConsoleApp.Lesson9;
public static class Demo1_SharedState
{
sealed class LoanRequestContext
{
public string CorrelationId { get; set; } = string.Empty;
public int InquiryCount { get; set; }
public DateTimeOffset LastInquiryAt { get; set; }
}
public static async Task RunAsync(string apiKey)
{
Console.WriteLine("=== Demo: Shared State — Loan Inquiry Agent ===");
Console.WriteLine();
AIAgent baseAgent = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsAIAgent(
instructions: "You are a bank loan advisor. Answer questions about mortgage and personal loan eligibility criteria concisely.",
name: "LoanAdvisor");
// Shared state — both middleware methods close over this instance.
var ctx = new LoanRequestContext();
// Stamps a unique correlation ID and records the inquiry timestamp
// BEFORE the agent runs.
async Task<AgentResponse> CorrelationStampMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken ct)
{
ctx.CorrelationId = Guid.NewGuid().ToString("N")[..12].ToUpper();
ctx.InquiryCount++;
ctx.LastInquiryAt = DateTimeOffset.UtcNow;
Console.WriteLine($" [Correlation] ID={ctx.CorrelationId} inquiry=#{ctx.InquiryCount}");
return await innerAgent.RunAsync(messages, session, options, ct)
.ConfigureAwait(false);
}
// Reads the correlation ID written by CorrelationStampMiddleware and
// emits a compliance audit entry AFTER the agent responds.
async Task<AgentResponse> AuditLogMiddleware(
IEnumerable<ChatMessage> messages,
AgentSession? session,
AgentRunOptions? options,
AIAgent innerAgent,
CancellationToken ct)
{
AgentResponse response = await innerAgent
.RunAsync(messages, session, options, ct)
.ConfigureAwait(false);
Console.WriteLine($" [AuditLog] ID={ctx.CorrelationId} at={ctx.LastInquiryAt:O} status=OK");
return response;
}
// CorrelationStampMiddleware is innermost; AuditLogMiddleware is outermost.
AIAgent loanAgent = baseAgent
.AsBuilder()
.Use(runFunc: CorrelationStampMiddleware, runStreamingFunc: null)
.Use(runFunc: AuditLogMiddleware, runStreamingFunc: null)
.Build();
Console.WriteLine("Customer: What is the minimum credit score for a personal loan?");
AgentResponse r1 = await loanAgent.RunAsync(
"What is the minimum credit score for a personal loan?");
Console.WriteLine($"Advisor: {r1.Text}");
Console.WriteLine();
Console.WriteLine("Customer: What documents are required for a mortgage application?");
AgentResponse r2 = await loanAgent.RunAsync(
"What documents are required for a mortgage application?");
Console.WriteLine($"Advisor: {r2.Text}");
Console.WriteLine();
Console.WriteLine($"Session summary: {ctx.InquiryCount} inquiries processed.");
}
}
Key Types
| Type / Member | Package | Role |
|---|
AIAgent.AsBuilder().Use().Build() | Microsoft.Agents.AI | Register middleware on an agent |
AgentRunMiddleware delegate | Microsoft.Agents.AI | Task<AgentResponse>(messages, session, options, innerAgent, ct) |
| Any plain C# object (closure) | — | Shared state container; no framework type required |
Required Packages
<PackageReference Include="Microsoft.Agents.AI" Version="1.0.0-rc1" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.3.0" />
Reference
Microsoft Agent Framework – Middleware: Shared State
Link Interceptor Active