Microsoft Agent Framework
Microsoft.Extensions.AI
Created: 27 Feb 2026
Updated: 27 Feb 2026
IChatClient: Custom Middleware
DelegatingChatClient is a base class for building custom middleware that wraps another IChatClient. It forwards all calls to the inner client by default, so you only override the methods you need to augment. Common uses: logging, rate limiting, retries, auditing, and metrics.
Key Concepts
1. Deriving from DelegatingChatClient
Override GetResponseAsync (and optionally GetStreamingResponseAsync) to add behaviour before and after the inner call. Always call base.GetResponseAsync to pass the request through to the underlying client:
public sealed class AuditLoggingChatClient(IChatClient inner)
: DelegatingChatClient(inner)
{
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"REQUEST: {messages.Last().Text}");
ChatResponse response = await base.GetResponseAsync(messages, options, cancellationToken);
Console.WriteLine($"RESPONSE: {response.Text}");
return response;
}
}
2. Wrapping the Client
Pass the inner client to the constructor — no builder required for simple cases:
IChatClient rawClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
IChatClient client = new AuditLoggingChatClient(rawClient);
3. Accessing Response Metadata
ChatResponse.Usage exposes token counts from the API response:
Console.WriteLine($"Tokens used: {response.Usage?.TotalTokenCount ?? 0}");
Full Example
using Microsoft.Extensions.AI;
using OpenAI;
namespace MicrosoftAgentFrameworkLesson.ConsoleApp.ChatClient;
/// <summary>
/// Custom middleware built on DelegatingChatClient.
/// Logs every request and response, and counts total tokens used.
/// </summary>
public sealed class AuditLoggingChatClient(IChatClient inner) : DelegatingChatClient(inner)
{
private int _callNumber;
public override async Task<ChatResponse> GetResponseAsync(
IEnumerable<ChatMessage> messages,
ChatOptions? options = null,
CancellationToken cancellationToken = default)
{
int id = Interlocked.Increment(ref _callNumber);
string userText = messages.LastOrDefault(m => m.Role == ChatRole.User)?.Text ?? "(none)";
Console.WriteLine($" [LOG #{id}] ► {userText}");
ChatResponse response = await base.GetResponseAsync(messages, options, cancellationToken);
string preview = response.Text.Length > 70
? response.Text[..70] + "..."
: response.Text;
Console.WriteLine($" [LOG #{id}] ◄ {preview}");
Console.WriteLine($" [LOG #{id}] Tokens used: {response.Usage?.TotalTokenCount ?? 0}");
return response;
}
}
/// <summary>
/// Demonstrates DelegatingChatClient custom middleware.
/// Scenario: Customer questions answered with audit trail.
/// </summary>
public static class MiddlewareDemo
{
public static async Task RunAsync()
{
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY")
?? throw new InvalidOperationException("Set OPEN_AI_KEY environment variable.");
IChatClient rawClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
// Wrap with custom audit-logging middleware
IChatClient client = new AuditLoggingChatClient(rawClient);
Console.WriteLine("====== IChatClient — Custom Middleware (DelegatingChatClient) ======\n");
string[] questions =
[
"What is cloud computing?",
"Name two major cloud providers.",
"What is serverless architecture in one sentence?"
];
foreach (var q in questions)
{
await client.GetResponseAsync(q);
Console.WriteLine();
}
}
}