Microsoft Agent Framework Middleware Created: 23 Feb 2026 Updated: 23 Feb 2026

Shared State

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?

  1. 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.
  2. Cross-middleware metrics — Accumulate timing, token counts, or call numbers across pipeline stages in one place.
  3. 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:

  1. CorrelationStampMiddleware (innermost) — Before calling the real agent, assigns a short correlation ID, increments the inquiry counter, and records the current timestamp in LoanRequestContext.
  2. 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

StepActive layerctx state
1AuditLogMiddleware — calls innerAgent.RunAsync()CorrelationId = "" (not yet set)
2CorrelationStampMiddleware — stamps ID, increments counterCorrelationId = "A3F8…", InquiryCount = 1
3Real agent runs and produces responseunchanged
4CorrelationStampMiddleware returns response up the chainunchanged
5AuditLogMiddleware resumes — reads CorrelationId, writes audit logCorrelationId = "A3F8…" ✓ available

Using a Strongly-Typed Context vs. Dictionary

ApproachProsCons
Strongly-typed class (LoanRequestContext)Compile-time safety, IntelliSense, no castingSlightly more boilerplate
Dictionary<string, object>Quick to set up, no extra classMagic 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 / MemberPackageRole
AIAgent.AsBuilder().Use().Build()Microsoft.Agents.AIRegister middleware on an agent
AgentRunMiddleware delegateMicrosoft.Agents.AITask<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


Share this lesson: