From 105dc82c39e9d20eff931b438c54ff2315fa1df2 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Date: Mon, 10 Nov 2025 18:34:32 +0000
Subject: [PATCH] .NET: Feature foundry agent + user agent (#2058)
* Update unit tests
* Add user-agent protocol calls
* Update unit tests
* Update unit tests with http handler confirmation
* UT fix
* Fix xmldoc
* Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Address copilot feedback
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../AgentsClientExtensions.cs | 206 ++++++++-----
.../RequestOptionsExtensions.cs | 67 ++++
.../AgentsClientExtensionsTests.cs | 290 ++++++++++++++++--
3 files changed, 457 insertions(+), 106 deletions(-)
create mode 100644 dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs
index cbc14d90d1..6261cac4ef 100644
--- a/dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/AgentsClientExtensions.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.ClientModel;
+using System.ClientModel.Primitives;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.AzureAI;
using Microsoft.Extensions.AI;
@@ -20,7 +22,7 @@ public static class AgentClientExtensions
///
/// Retrieves an existing server side agent, wrapped as a using the provided .
///
- /// The to create the with. Cannot be .
+ /// The to create the with. Cannot be .
/// The name of the server side agent to create a for. Cannot be or whitespace.
/// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.
/// Provides a way to customize the creation of the underlying used by the agent.
@@ -28,12 +30,12 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent.
- /// Thrown when or is .
+ /// Thrown when or is .
/// Thrown when is empty or whitespace, or when the agent with the specified name was not found.
/// The agent with the specified name was not found.
/// When using prompt agent definitions with tools the parameter needs to be provided.
public static ChatClientAgent GetAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
IList? tools = null,
Func? clientFactory = null,
@@ -41,14 +43,13 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNullOrWhitespace(name);
- var agentRecord = AgentClient.GetAgent(name, cancellationToken).Value
- ?? throw new InvalidOperationException($"Agent with name '{name}' not found.");
+ AgentRecord agentRecord = GetAgentRecordByName(agentClient, name, cancellationToken);
return GetAIAgent(
- AgentClient,
+ agentClient,
agentRecord,
tools,
clientFactory,
@@ -60,7 +61,7 @@ public static class AgentClientExtensions
///
/// Asynchronously retrieves an existing server side agent, wrapped as a using the provided .
///
- /// The to create the with. Cannot be .
+ /// The to create the with. Cannot be .
/// The name of the server side agent to create a for. Cannot be or whitespace.
/// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.
/// Provides a way to customize the creation of the underlying used by the agent.
@@ -68,12 +69,12 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations based on the latest version of the named Azure AI Agent.
- /// Thrown when or is .
+ /// Thrown when or is .
/// Thrown when is empty or whitespace, or when the agent with the specified name was not found.
/// The agent with the specified name was not found.
/// When using prompt agent definitions with tools the parameter needs to be provided.
public static async Task GetAIAgentAsync(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
IList? tools = null,
Func? clientFactory = null,
@@ -81,14 +82,13 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNullOrWhitespace(name);
- var agentRecord = (await AgentClient.GetAgentAsync(name, cancellationToken).ConfigureAwait(false)).Value
- ?? throw new InvalidOperationException($"Agent with name '{name}' not found.");
+ AgentRecord agentRecord = await GetAgentRecordByNameAsync(agentClient, name, cancellationToken).ConfigureAwait(false);
return GetAIAgent(
- AgentClient,
+ agentClient,
agentRecord,
tools,
clientFactory,
@@ -100,7 +100,7 @@ public static class AgentClientExtensions
///
/// Gets a runnable agent instance from the provided agent record.
///
- /// The client used to interact with Azure AI Agents. Cannot be .
+ /// The client used to interact with Azure AI Agents. Cannot be .
/// The agent record to be converted. The latest version will be used. Cannot be .
/// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.
/// Provides a way to customize the creation of the underlying used by the agent.
@@ -110,7 +110,7 @@ public static class AgentClientExtensions
/// A instance that can be used to perform operations based on the latest version of the Azure AI Agent.
/// When using prompt agent definitions with tools the parameter needs to be provided.
public static ChatClientAgent GetAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
AgentRecord agentRecord,
IList? tools = null,
Func? clientFactory = null,
@@ -118,11 +118,11 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(agentRecord);
return GetAIAgent(
- AgentClient,
+ agentClient,
agentRecord.Versions.Latest,
tools,
clientFactory,
@@ -134,7 +134,7 @@ public static class AgentClientExtensions
///
/// Gets a runnable agent instance from a containing metadata about an Azure AI Agent.
///
- /// The client used to interact with Azure AI Agents. Cannot be .
+ /// The client used to interact with Azure AI Agents. Cannot be .
/// The agent version to be converted. Cannot be .
/// The tools to use when interacting with the agent. This is required when using prompt agent definitions with tools.
/// Provides a way to customize the creation of the underlying used by the agent.
@@ -142,10 +142,10 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// The to monitor for cancellation requests. The default is .
/// A instance that can be used to perform operations based on the provided version of the Azure AI Agent.
- /// Thrown when or is .
+ /// Thrown when or is .
/// When using prompt agent definitions with tools the parameter needs to be provided.
public static ChatClientAgent GetAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
AgentVersion agentVersion,
IList? tools = null,
Func? clientFactory = null,
@@ -153,13 +153,13 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(agentVersion);
ValidateUsingToolsParameter(agentVersion, tools);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
tools,
clientFactory,
@@ -171,23 +171,23 @@ public static class AgentClientExtensions
///
/// Creates a new Prompt AI Agent using the provided and options.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The options for creating the agent. Cannot be .
/// A factory function to customize the creation of the chat client used by the agent.
/// An optional for configuring the underlying OpenAI client.
/// An optional to use for resolving services required by the instances being invoked.
/// A to cancel the operation if needed.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when or is .
+ /// Thrown when or is .
public static ChatClientAgent GetAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
ChatClientAgentOptions options,
Func? clientFactory = null,
OpenAIClientOptions? openAIClientOptions = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(options);
if (string.IsNullOrWhiteSpace(options.Name))
@@ -195,15 +195,13 @@ public static class AgentClientExtensions
throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options));
}
- var agentRecord = AgentClient.GetAgent(options.Name, cancellationToken).Value
- ?? throw new InvalidOperationException($"Agent with name '{options.Name}' not found.");
-
+ AgentRecord agentRecord = GetAgentRecordByName(agentClient, options.Name, cancellationToken);
var agentVersion = agentRecord.Versions.Latest;
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
agentOptions,
clientFactory,
@@ -215,23 +213,23 @@ public static class AgentClientExtensions
///
/// Creates a new Prompt AI Agent using the provided and options.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The options for creating the agent. Cannot be .
/// A factory function to customize the creation of the chat client used by the agent.
/// An optional for configuring the underlying OpenAI client.
/// An optional to use for resolving services required by the instances being invoked.
/// A to cancel the operation if needed.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when or is .
+ /// Thrown when or is .
public static async Task GetAIAgentAsync(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
ChatClientAgentOptions options,
Func? clientFactory = null,
OpenAIClientOptions? openAIClientOptions = null,
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(options);
if (string.IsNullOrWhiteSpace(options.Name))
@@ -239,15 +237,13 @@ public static class AgentClientExtensions
throw new ArgumentException("Agent name must be provided in the options.Name property", nameof(options));
}
- var agentRecord = (await AgentClient.GetAgentAsync(options.Name, cancellationToken).ConfigureAwait(false)).Value
- ?? throw new InvalidOperationException($"Agent with name '{options.Name}' not found.");
-
+ AgentRecord agentRecord = await GetAgentRecordByNameAsync(agentClient, options.Name, cancellationToken).ConfigureAwait(false);
var agentVersion = agentRecord.Versions.Latest;
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, requireInvocableTools: true);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
agentOptions,
clientFactory,
@@ -259,7 +255,7 @@ public static class AgentClientExtensions
///
/// Creates a new Prompt AI agent using the specified configuration parameters.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The name for the agent.
/// The name of the model to use for the agent. Cannot be or whitespace.
/// The instructions that guide the agent's behavior. Cannot be or whitespace.
@@ -270,11 +266,11 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// A token to monitor for cancellation requests.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when , , or is .
+ /// Thrown when , , or is .
/// Thrown when or is empty or whitespace.
/// When using prompt agent definitions with tools the parameter needs to be provided.
public static ChatClientAgent CreateAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
string model,
string instructions,
@@ -285,13 +281,13 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNullOrWhitespace(name);
Throw.IfNullOrWhitespace(model);
Throw.IfNullOrWhitespace(instructions);
return CreateAIAgent(
- AgentClient,
+ agentClient,
name,
tools,
new AgentVersionCreationOptions(new PromptAgentDefinition(model) { Instructions = instructions }) { Description = description },
@@ -305,7 +301,7 @@ public static class AgentClientExtensions
///
/// Creates a new Prompt AI agent using the specified configuration parameters.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The name for the agent.
/// The name of the model to use for the agent. Cannot be or whitespace.
/// The instructions that guide the agent's behavior. Cannot be or whitespace.
@@ -316,11 +312,11 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// A token to monitor for cancellation requests.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when , , or is .
+ /// Thrown when , , or is .
/// Thrown when or is empty or whitespace.
/// When using prompt agent definitions with tools the parameter needs to be provided.
public static Task CreateAIAgentAsync(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
string model,
string instructions,
@@ -331,13 +327,13 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNullOrWhitespace(name);
Throw.IfNullOrWhitespace(model);
Throw.IfNullOrWhitespace(instructions);
return CreateAIAgentAsync(
- AgentClient,
+ agentClient,
name,
tools,
new AgentVersionCreationOptions(new PromptAgentDefinition(model) { Instructions = instructions }) { Description = description },
@@ -351,7 +347,7 @@ public static class AgentClientExtensions
///
/// Creates a new Prompt AI Agent using the provided and options.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The name of the model to use for the agent. Cannot be or whitespace.
/// The options for creating the agent. Cannot be .
/// A factory function to customize the creation of the chat client used by the agent.
@@ -359,10 +355,10 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// A to cancel the operation if needed.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when or is .
+ /// Thrown when or is .
/// Thrown when is empty or whitespace, or when the agent name is not provided in the options.
public static ChatClientAgent CreateAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string model,
ChatClientAgentOptions options,
Func? clientFactory = null,
@@ -370,7 +366,7 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(options);
Throw.IfNullOrWhitespace(model);
const bool RequireInvocableTools = true;
@@ -387,18 +383,18 @@ public static class AgentClientExtensions
ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools, RequireInvocableTools);
- AgentVersionCreationOptions? versionCreationOptions = new(agentDefinition);
+ AgentVersionCreationOptions? creationOptions = new(agentDefinition);
if (!string.IsNullOrWhiteSpace(options.Description))
{
- versionCreationOptions.Description = options.Description;
+ creationOptions.Description = options.Description;
}
- AgentVersion agentVersion = AgentClient.CreateAgentVersion(options.Name, versionCreationOptions, cancellationToken).Value;
+ AgentVersion agentVersion = CreateAgentVersionWithProtocol(agentClient, options.Name, creationOptions, cancellationToken);
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, RequireInvocableTools);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
agentOptions,
clientFactory,
@@ -410,7 +406,7 @@ public static class AgentClientExtensions
///
/// Creates a new Prompt AI Agent using the provided and options.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The name of the model to use for the agent. Cannot be or whitespace.
/// The options for creating the agent. Cannot be .
/// A factory function to customize the creation of the chat client used by the agent.
@@ -418,10 +414,10 @@ public static class AgentClientExtensions
/// An optional to use for resolving services required by the instances being invoked.
/// A to cancel the operation if needed.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when or is .
+ /// Thrown when or is .
/// Thrown when is empty or whitespace, or when the agent name is not provided in the options.
public static async Task CreateAIAgentAsync(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string model,
ChatClientAgentOptions options,
Func? clientFactory = null,
@@ -429,7 +425,7 @@ public static class AgentClientExtensions
IServiceProvider? services = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(options);
Throw.IfNullOrWhitespace(model);
const bool RequireInvocableTools = true;
@@ -446,18 +442,18 @@ public static class AgentClientExtensions
ApplyToolsToAgentDefinition(agentDefinition, options.ChatOptions?.Tools, RequireInvocableTools);
- AgentVersionCreationOptions? versionCreationOptions = new(agentDefinition);
+ AgentVersionCreationOptions? creationOptions = new(agentDefinition);
if (!string.IsNullOrWhiteSpace(options.Description))
{
- versionCreationOptions.Description = options.Description;
+ creationOptions.Description = options.Description;
}
- AgentVersion agentVersion = await AgentClient.CreateAgentVersionAsync(options.Name, versionCreationOptions, cancellationToken).ConfigureAwait(false);
+ AgentVersion agentVersion = await CreateAgentVersionWithProtocolAsync(agentClient, options.Name, creationOptions, cancellationToken).ConfigureAwait(false);
var agentOptions = CreateChatClientAgentOptions(agentVersion, options, RequireInvocableTools);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
agentOptions,
clientFactory,
@@ -469,34 +465,34 @@ public static class AgentClientExtensions
///
/// Creates a new AI agent using the specified agent definition and optional configuration parameters.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The name for the agent.
/// Settings that control the creation of the agent.
/// A factory function to customize the creation of the chat client used by the agent.
/// An optional for configuring the underlying OpenAI client.
/// A token to monitor for cancellation requests.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when or is .
+ /// Thrown when or is .
///
/// When using this extension method with a the tools are only declarative and not invocable.
/// Invocation of any in-process tools will need to be handled manually.
///
public static ChatClientAgent CreateAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
AgentVersionCreationOptions creationOptions,
Func? clientFactory = null,
OpenAIClientOptions? openAIClientOptions = null,
CancellationToken cancellationToken = default)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNullOrWhitespace(name);
Throw.IfNull(creationOptions);
var tools = (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
return CreateAIAgent(
- AgentClient,
+ agentClient,
name,
tools,
creationOptions,
@@ -511,20 +507,20 @@ public static class AgentClientExtensions
/// Asynchronously creates a new AI agent using the specified agent definition and optional configuration
/// parameters.
///
- /// The client used to manage and interact with AI agents. Cannot be .
+ /// The client used to manage and interact with AI agents. Cannot be .
/// The name for the agent.
/// Settings that control the creation of the agent.
/// A factory function to customize the creation of the chat client used by the agent.
/// An optional for configuring the underlying OpenAI client.
/// A token to monitor for cancellation requests.
/// A instance that can be used to perform operations on the newly created agent.
- /// Thrown when or is .
+ /// Thrown when or is .
///
/// When using this extension method with a the tools are only declarative and not invocable.
/// Invocation of any in-process tools will need to be handled manually.
///
public static Task CreateAIAgentAsync(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
AgentVersionCreationOptions creationOptions,
Func? clientFactory = null,
@@ -532,13 +528,13 @@ public static class AgentClientExtensions
CancellationToken cancellationToken = default)
{
Throw.IfNullOrWhitespace(name);
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(creationOptions);
var tools = (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
return CreateAIAgentAsync(
- AgentClient,
+ agentClient,
name,
tools,
creationOptions,
@@ -551,8 +547,48 @@ public static class AgentClientExtensions
#region Private
+ ///
+ /// Retrieves an agent record by name using the Protocol method with user-agent header.
+ ///
+ private static AgentRecord GetAgentRecordByName(AgentClient agentClient, string agentName, CancellationToken cancellationToken)
+ {
+ ClientResult protocolResponse = agentClient.GetAgent(agentName, cancellationToken.ToRequestOptions(false));
+ return ClientResult.FromOptionalValue((AgentRecord)protocolResponse, protocolResponse.GetRawResponse()).Value
+ ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
+ }
+
+ ///
+ /// Asynchronously retrieves an agent record by name using the Protocol method with user-agent header.
+ ///
+ private static async Task GetAgentRecordByNameAsync(AgentClient agentClient, string agentName, CancellationToken cancellationToken)
+ {
+ ClientResult protocolResponse = await agentClient.GetAgentAsync(agentName, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
+ return ClientResult.FromOptionalValue((AgentRecord)protocolResponse, protocolResponse.GetRawResponse()).Value
+ ?? throw new InvalidOperationException($"Agent with name '{agentName}' not found.");
+ }
+
+ ///
+ /// Creates an agent version using the Protocol method with user-agent header.
+ ///
+ private static AgentVersion CreateAgentVersionWithProtocol(AgentClient agentClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
+ {
+ using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions));
+ ClientResult protocolResponse = agentClient.CreateAgentVersion(agentName, protocolRequest, cancellationToken.ToRequestOptions(false));
+ return ClientResult.FromValue((AgentVersion)protocolResponse, protocolResponse.GetRawResponse()).Value;
+ }
+
+ ///
+ /// Asynchronously creates an agent version using the Protocol method with user-agent header.
+ ///
+ private static async Task CreateAgentVersionWithProtocolAsync(AgentClient agentClient, string agentName, AgentVersionCreationOptions creationOptions, CancellationToken cancellationToken)
+ {
+ using BinaryContent protocolRequest = BinaryContent.Create(ModelReaderWriter.Write(creationOptions));
+ ClientResult protocolResponse = await agentClient.CreateAgentVersionAsync(agentName, protocolRequest, cancellationToken.ToRequestOptions(false)).ConfigureAwait(false);
+ return ClientResult.FromValue((AgentVersion)protocolResponse, protocolResponse.GetRawResponse()).Value;
+ }
+
private static ChatClientAgent CreateAIAgent(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
IList? tools,
AgentVersionCreationOptions creationOptions,
@@ -562,7 +598,7 @@ public static class AgentClientExtensions
IServiceProvider? services,
CancellationToken cancellationToken)
{
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNullOrWhitespace(name);
Throw.IfNull(creationOptions);
@@ -570,10 +606,10 @@ public static class AgentClientExtensions
ApplyToolsToAgentDefinition(creationOptions.Definition, tools, requireInvocableTools);
- AgentVersion agentVersion = AgentClient.CreateAgentVersion(name, creationOptions, cancellationToken).Value;
+ AgentVersion agentVersion = CreateAgentVersionWithProtocol(agentClient, name, creationOptions, cancellationToken);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
tools,
clientFactory,
@@ -583,7 +619,7 @@ public static class AgentClientExtensions
}
private static async Task CreateAIAgentAsync(
- this AgentClient AgentClient,
+ this AgentClient agentClient,
string name,
IList? tools,
AgentVersionCreationOptions creationOptions,
@@ -594,17 +630,17 @@ public static class AgentClientExtensions
CancellationToken cancellationToken)
{
Throw.IfNullOrWhitespace(name);
- Throw.IfNull(AgentClient);
+ Throw.IfNull(agentClient);
Throw.IfNull(creationOptions);
tools ??= (creationOptions.Definition as PromptAgentDefinition)?.Tools.Select(t => t.AsAITool()).ToList();
ApplyToolsToAgentDefinition(creationOptions.Definition, tools, requireInvocableTools);
- AgentVersion agentVersion = await AgentClient.CreateAgentVersionAsync(name, creationOptions, cancellationToken).ConfigureAwait(false);
+ AgentVersion agentVersion = await CreateAgentVersionWithProtocolAsync(agentClient, name, creationOptions, cancellationToken).ConfigureAwait(false);
return CreateChatClientAgent(
- AgentClient,
+ agentClient,
agentVersion,
tools,
clientFactory,
@@ -615,7 +651,7 @@ public static class AgentClientExtensions
/// This method creates an with the specified ChatClientAgentOptions.
private static ChatClientAgent CreateChatClientAgent(
- AgentClient AgentClient,
+ AgentClient agentClient,
AgentVersion agentVersion,
ChatClientAgentOptions agentOptions,
Func? clientFactory,
@@ -623,7 +659,7 @@ public static class AgentClientExtensions
bool requireInvocableTools,
IServiceProvider? services)
{
- IChatClient chatClient = new AzureAIAgentChatClient(AgentClient, agentVersion, agentOptions.ChatOptions, openAIClientOptions);
+ IChatClient chatClient = new AzureAIAgentChatClient(agentClient, agentVersion, agentOptions.ChatOptions, openAIClientOptions);
if (clientFactory is not null)
{
diff --git a/dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs
new file mode 100644
index 0000000000..722d316330
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.AzureAI/RequestOptionsExtensions.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using System.Reflection;
+
+namespace Microsoft.Agents.AI;
+
+internal static class RequestOptionsExtensions
+{
+ /// Creates a configured for use with Foundry Agents.
+ public static RequestOptions ToRequestOptions(this CancellationToken cancellationToken, bool streaming)
+ {
+ RequestOptions requestOptions = new()
+ {
+ CancellationToken = cancellationToken,
+ BufferResponse = !streaming
+ };
+
+ requestOptions.AddPolicy(MeaiUserAgentPolicy.Instance, PipelinePosition.PerCall);
+
+ return requestOptions;
+ }
+
+ /// Provides a pipeline policy that adds a "MEAI/x.y.z" user-agent header.
+ private sealed class MeaiUserAgentPolicy : PipelinePolicy
+ {
+ public static MeaiUserAgentPolicy Instance { get; } = new MeaiUserAgentPolicy();
+
+ private static readonly string s_userAgentValue = CreateUserAgentValue();
+
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ AddUserAgentHeader(message);
+ ProcessNext(message, pipeline, currentIndex);
+ }
+
+ public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ AddUserAgentHeader(message);
+ return ProcessNextAsync(message, pipeline, currentIndex);
+ }
+
+ private static void AddUserAgentHeader(PipelineMessage message) =>
+ message.Request.Headers.Add("User-Agent", s_userAgentValue);
+
+ private static string CreateUserAgentValue()
+ {
+ const string Name = "MEAI";
+
+ if (typeof(MeaiUserAgentPolicy).Assembly.GetCustomAttribute()?.InformationalVersion is string version)
+ {
+ int pos = version.IndexOf('+');
+ if (pos >= 0)
+ {
+ version = version.Substring(0, pos);
+ }
+
+ if (version.Length > 0)
+ {
+ return $"{Name}/{version}";
+ }
+ }
+
+ return Name;
+ }
+ }
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs
index df2fba3f1c..e3afef7d8a 100644
--- a/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.AzureAI.UnitTests/AgentsClientExtensionsTests.cs
@@ -6,6 +6,9 @@ using System.ClientModel.Primitives;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -38,7 +41,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.GetAIAgent(agentRecord));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -116,7 +119,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.GetAIAgent(agentVersion));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -234,7 +237,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.GetAIAgent(options));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -329,7 +332,7 @@ public sealed class AgentClientExtensionsTests
var exception = await Assert.ThrowsAsync(() =>
client!.GetAIAgentAsync(options));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -383,7 +386,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.GetAIAgent("test-agent"));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -426,8 +429,11 @@ public sealed class AgentClientExtensionsTests
{
// Arrange
var mockClient = new Mock();
- mockClient.Setup(c => c.GetAgent(It.IsAny(), It.IsAny()))
- .Returns(ClientResult.FromOptionalValue((AgentRecord)null!, new MockPipelineResponse(200)));
+ mockClient.Setup(c => c.GetAgent(It.IsAny(), It.IsAny()))
+ .Returns(ClientResult.FromOptionalValue((AgentRecord)null!, new MockPipelineResponse(200, BinaryData.FromString("null"))));
+
+ mockClient.Setup(x => x.GetOpenAIClient(It.IsAny()))
+ .Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
// Act & Assert
var exception = Assert.Throws(() =>
@@ -453,7 +459,7 @@ public sealed class AgentClientExtensionsTests
var exception = await Assert.ThrowsAsync(() =>
client!.GetAIAgentAsync("test-agent"));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -480,8 +486,11 @@ public sealed class AgentClientExtensionsTests
{
// Arrange
var mockClient = new Mock();
- mockClient.Setup(c => c.GetAgentAsync(It.IsAny(), It.IsAny()))
- .ReturnsAsync(ClientResult.FromOptionalValue((AgentRecord)null!, new MockPipelineResponse(200)));
+ mockClient.Setup(c => c.GetAgentAsync(It.IsAny(), It.IsAny()))
+ .ReturnsAsync(ClientResult.FromOptionalValue((AgentRecord)null!, new MockPipelineResponse(200, BinaryData.FromString("null"))));
+
+ mockClient.Setup(x => x.GetOpenAIClient(It.IsAny()))
+ .Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
// Act & Assert
var exception = await Assert.ThrowsAsync(() =>
@@ -580,7 +589,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.CreateAIAgent("test-agent", "model", "instructions"));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -618,7 +627,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.CreateAIAgent("test-agent", options));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -673,7 +682,7 @@ public sealed class AgentClientExtensionsTests
var exception = Assert.Throws(() =>
client!.CreateAIAgent("model", options));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -847,7 +856,7 @@ public sealed class AgentClientExtensionsTests
var exception = await Assert.ThrowsAsync(() =>
client!.CreateAIAgentAsync("agent-name", options));
- Assert.Equal("AgentClient", exception.ParamName);
+ Assert.Equal("agentClient", exception.ParamName);
}
///
@@ -1672,6 +1681,176 @@ public sealed class AgentClientExtensionsTests
#endregion
+ #region User-Agent Header Tests
+
+ ///
+ /// Verify that GetAIAgent(string name) passes RequestOptions to the Protocol method.
+ ///
+ [Fact]
+ public void GetAIAgent_WithStringName_PassesRequestOptionsToProtocol()
+ {
+ // Arrange
+ RequestOptions? capturedRequestOptions = null;
+ var mockAgentClient = new Mock(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider());
+ mockAgentClient
+ .Setup(x => x.GetAgent(It.IsAny(), It.IsAny()))
+ .Callback((name, options) => capturedRequestOptions = options)
+ .Returns(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentTestJsonObject))));
+
+ mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny()))
+ .Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
+
+ // Act
+ var agent = mockAgentClient.Object.GetAIAgent("test-agent");
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(capturedRequestOptions);
+ }
+
+ ///
+ /// Verify that GetAIAgentAsync(string name) passes RequestOptions to the Protocol method.
+ ///
+ [Fact]
+ public async Task GetAIAgentAsync_WithStringName_PassesRequestOptionsToProtocolAsync()
+ {
+ // Arrange
+ RequestOptions? capturedRequestOptions = null;
+ var mockAgentClient = new Mock(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider());
+ mockAgentClient
+ .Setup(x => x.GetAgentAsync(It.IsAny(), It.IsAny()))
+ .Callback((name, options) => capturedRequestOptions = options)
+ .Returns(Task.FromResult(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentTestJsonObject)))));
+
+ mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny()))
+ .Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
+
+ // Act
+ var agent = await mockAgentClient.Object.GetAIAgentAsync("test-agent");
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(capturedRequestOptions);
+ }
+
+ ///
+ /// Verify that CreateAIAgent(string model, ChatClientAgentOptions options) passes RequestOptions to the Protocol method.
+ ///
+ [Fact]
+ public void CreateAIAgent_WithChatClientAgentOptions_PassesRequestOptionsToProtocol()
+ {
+ // Arrange
+ RequestOptions? capturedRequestOptions = null;
+ var mockAgentClient = new Mock(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider());
+ mockAgentClient
+ .Setup(x => x.CreateAgentVersion(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((name, content, options) => capturedRequestOptions = options)
+ .Returns(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentVersionTestJsonObject))));
+
+ mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny()))
+ .Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
+
+ var agentOptions = new ChatClientAgentOptions { Name = "test-agent" };
+
+ // Act
+ var agent = mockAgentClient.Object.CreateAIAgent("gpt-4", agentOptions);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(capturedRequestOptions);
+ }
+
+ ///
+ /// Verify that CreateAIAgentAsync(string model, ChatClientAgentOptions options) passes RequestOptions to the Protocol method.
+ ///
+ [Fact]
+ public async Task CreateAIAgentAsync_WithChatClientAgentOptions_PassesRequestOptionsToProtocolAsync()
+ {
+ // Arrange
+ RequestOptions? capturedRequestOptions = null;
+ var mockAgentClient = new Mock(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider());
+ mockAgentClient
+ .Setup(x => x.CreateAgentVersionAsync(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((name, content, options) => capturedRequestOptions = options)
+ .Returns(Task.FromResult(ClientResult.FromResponse(new MockPipelineResponse(200, BinaryData.FromString(AgentVersionTestJsonObject)))));
+
+ mockAgentClient.Setup(x => x.GetOpenAIClient(It.IsAny()))
+ .Returns(new OpenAIClient(new ApiKeyCredential("test-key")));
+
+ var agentOptions = new ChatClientAgentOptions { Name = "test-agent" };
+
+ // Act
+ var agent = await mockAgentClient.Object.CreateAIAgentAsync("gpt-4", agentOptions);
+
+ // Assert
+ Assert.NotNull(agent);
+ Assert.NotNull(capturedRequestOptions);
+ }
+
+ ///
+ /// Verifies that the user-agent header is added to both synchronous and asynchronous requests made by agent creation methods.
+ ///
+ [Fact]
+ public async Task CreateAIAgent_UserAgentHeaderAddedToRequestsAsync()
+ {
+ using var httpHandler = new HttpHandlerAssert(request =>
+ {
+ Assert.Equal("POST", request.Method.Method);
+ Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
+
+ return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentTestJsonObject, Encoding.UTF8, "application/json") };
+ });
+
+#pragma warning disable CA5399
+ using var httpClient = new HttpClient(httpHandler);
+#pragma warning restore CA5399
+
+ // Arrange
+ var agentClient = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
+
+ var agentOptions = new ChatClientAgentOptions { Name = "test-agent" };
+
+ // Act
+ var agent1 = agentClient.CreateAIAgent("test", agentOptions);
+ var agent2 = await agentClient.CreateAIAgentAsync("test", agentOptions);
+
+ // Assert
+ Assert.NotNull(agent1);
+ Assert.NotNull(agent2);
+ }
+
+ ///
+ /// Verifies that the user-agent header is added to both synchronous and asynchronous GetAIAgent requests.
+ ///
+ [Fact]
+ public async Task GetAIAgent_UserAgentHeaderAddedToRequestsAsync()
+ {
+ using var httpHandler = new HttpHandlerAssert(request =>
+ {
+ Assert.Equal("GET", request.Method.Method);
+ Assert.Contains("MEAI", request.Headers.UserAgent.ToString());
+
+ return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AgentTestJsonObject, Encoding.UTF8, "application/json") };
+ });
+
+#pragma warning disable CA5399
+ using var httpClient = new HttpClient(httpHandler);
+#pragma warning restore CA5399
+
+ // Arrange
+ var agentClient = new AgentClient(new Uri("https://test.openai.azure.com/"), new FakeAuthenticationTokenProvider(), new() { Transport = new HttpClientPipelineTransport(httpClient) });
+
+ // Act
+ var agent1 = agentClient.GetAIAgent("test");
+ var agent2 = await agentClient.GetAIAgentAsync("test");
+
+ // Assert
+ Assert.NotNull(agent1);
+ Assert.NotNull(agent2);
+ }
+
+ #endregion
+
#region Helper Methods
///
@@ -1792,21 +1971,41 @@ public sealed class AgentClientExtensionsTests
return new OpenAIClient(new ApiKeyCredential("test-key"), options);
}
+ public override ClientResult GetAgent(string agentName, RequestOptions options)
+ {
+ return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject))));
+ }
+
public override ClientResult GetAgent(string agentName, CancellationToken cancellationToken = default)
{
return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200));
}
+ public override Task GetAgentAsync(string agentName, RequestOptions options)
+ {
+ return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))));
+ }
+
public override Task> GetAgentAsync(string agentName, CancellationToken cancellationToken = default)
{
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentTestJsonObject)))!, new MockPipelineResponse(200)));
}
+ public override ClientResult CreateAgentVersion(string agentName, BinaryContent content, RequestOptions? options = null)
+ {
+ return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject))));
+ }
+
public override ClientResult CreateAgentVersion(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
return ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200));
}
+ public override Task CreateAgentVersionAsync(string agentName, BinaryContent content, RequestOptions? options = null)
+ {
+ return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200, BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))));
+ }
+
public override Task> CreateAgentVersionAsync(string agentName, AgentVersionCreationOptions? options = null, CancellationToken cancellationToken = default)
{
return Task.FromResult(ClientResult.FromValue(ModelReaderWriter.Read(BinaryData.FromString(this.ApplyResponseChanges(AgentVersionTestJsonObject)))!, new MockPipelineResponse(200)));
@@ -1889,10 +2088,14 @@ public sealed class AgentClientExtensionsTests
private sealed class MockPipelineResponse : PipelineResponse
{
private readonly int _status;
+ private readonly BinaryData _content;
+ private readonly MockPipelineResponseHeaders _headers;
- public MockPipelineResponse(int status)
+ public MockPipelineResponse(int status, BinaryData? content = null)
{
this._status = status;
+ this._content = content ?? BinaryData.Empty;
+ this._headers = new MockPipelineResponseHeaders();
}
public override int Status => this._status;
@@ -1905,9 +2108,9 @@ public sealed class AgentClientExtensionsTests
set { }
}
- public override BinaryData Content => BinaryData.Empty;
+ public override BinaryData Content => this._content;
- protected override PipelineResponseHeaders HeadersCore => new EmptyPipelineResponseHeaders();
+ protected override PipelineResponseHeaders HeadersCore => this._headers;
public override BinaryData BufferContent(CancellationToken cancellationToken = default) =>
throw new NotSupportedException("Buffering content is not supported for mock responses.");
@@ -1919,26 +2122,71 @@ public sealed class AgentClientExtensionsTests
{
}
- private sealed class EmptyPipelineResponseHeaders : PipelineResponseHeaders
+ private sealed class MockPipelineResponseHeaders : PipelineResponseHeaders
{
+ private readonly Dictionary _headers = new(StringComparer.OrdinalIgnoreCase)
+ {
+ { "Content-Type", "application/json" },
+ { "x-ms-request-id", "test-request-id" }
+ };
+
public override bool TryGetValue(string name, out string? value)
{
- value = null;
- return false;
+ return this._headers.TryGetValue(name, out value);
}
public override bool TryGetValues(string name, out IEnumerable? values)
{
+ if (this._headers.TryGetValue(name, out var value))
+ {
+ values = new[] { value };
+ return true;
+ }
+
values = null;
return false;
}
public override IEnumerator> GetEnumerator()
{
- yield break;
+ return this._headers.GetEnumerator();
}
}
}
+ private sealed class FakeAuthenticationTokenProvider : AuthenticationTokenProvider
+ {
+ public override GetTokenOptions? CreateTokenOptions(IReadOnlyDictionary properties)
+ {
+ return new GetTokenOptions(new Dictionary());
+ }
+
+ public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken)
+ {
+ return new AuthenticationToken("token-value", "token-type", DateTimeOffset.UtcNow.AddHours(1));
+ }
+
+ public override ValueTask GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken)
+ {
+ return new ValueTask(this.GetToken(options, cancellationToken));
+ }
+ }
+
#endregion
+
+ private sealed class HttpHandlerAssert(Func assertion) : HttpClientHandler
+ {
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var response = assertion(request);
+ return Task.FromResult(response);
+ }
+
+#if NET
+ protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ return assertion(request);
+ }
+#endif
+ }
}