Microsoft Agent Framework Microsoft.Extensions.AI Created: 28 Feb 2026 Updated: 28 Feb 2026

Accessing Data in AI Functions

When you create AI functions with AIFunctionFactory.Create, the delegate often needs data that the AI model cannot or should not provide — a database connection, a cached lookup table, or per-request ambient context injected by your application. Microsoft.Extensions.AI offers three main techniques to pass this data into function delegates.

Key Concepts

1. Closure

The simplest technique: capture any variable from the enclosing scope directly in the delegate. The data is baked into the AIFunction at creation time and is always available when the function runs:

var packageStatuses = new Dictionary<string, string>
{
["PKG-1001"] = "In transit — Warsaw hub",
["PKG-1002"] = "Out for delivery",
};

AIFunction getPackageStatus = AIFunctionFactory.Create(
(string trackingId) =>
packageStatuses.TryGetValue(trackingId, out var status)
? status
: "Tracking ID not found.",
"GetPackageStatus",
"Returns the current status of a package by its tracking ID");

2. AIFunctionArguments — Manual Invocation

Declare the delegate parameter as AIFunctionArguments instead of typed parameters. The whole named-argument dictionary is bound to it. Call AIFunction.InvokeAsync(AIFunctionArguments) directly — no AI model involved:

AIFunction estimateDelivery = AIFunctionFactory.Create(
(AIFunctionArguments args) =>
{
string origin = args.TryGetValue("origin", out var o) ? o!.ToString()! : "?";
string destination = args.TryGetValue("destination", out var d) ? d!.ToString()! : "?";
// ...
return $"{origin} → {destination}: estimated {days} business day(s)";
});

var result = await estimateDelivery.InvokeAsync(new AIFunctionArguments
{
{ "origin", "Berlin" },
{ "destination", "Lisbon" },
{ "weightKg", "12" }
});

AIFunctionArguments also carries a Context dictionary (IDictionary<object, object>) and a Services (IServiceProvider) for dependency injection — both accessible the same way.

3. FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties

When the AI model invokes a function through FunctionInvokingChatClient (added by .UseFunctionInvocation()), a static FunctionInvokingChatClient.CurrentContext is populated for the duration of that call. Inside the delegate you can read CurrentContext.Options.AdditionalProperties — a dictionary your application filled in on ChatOptions before the request. The AI model never sees these values:

AIFunction getCarrierPolicy = AIFunctionFactory.Create(
() =>
{
var props = FunctionInvokingChatClient.CurrentContext.Options!.AdditionalProperties;
string carrier = props != null && props.TryGetValue("carrierCode", out var c)
? c!.ToString()! : "UNKNOWN";
return carrier switch
{
"DHL" => "DHL: free returns within 30 days...",
_ => "No policy found."
};
},
"GetCarrierPolicy",
"Returns the delivery and return policy for the active carrier");

var options = new ChatOptions
{
Tools = [getCarrierPolicy],
AdditionalProperties = new AdditionalPropertiesDictionary
{
["carrierCode"] = "DHL"
}
};

Full Example

using Microsoft.Extensions.AI;
using OpenAI;

namespace MicrosoftAgentFrameworkLesson.ConsoleApp.FunctionCalling;

/// <summary>
/// Demonstrates three techniques for passing contextual data into AIFunction delegates:
/// 1. Closure — external data captured in the delegate's enclosing scope
/// 2. AIFunctionArguments — reading named args during direct manual invocation
/// 3. CurrentContext.Options.AdditionalProperties — per-request ambient data injected
/// via ChatOptions, invisible to the AI model
/// Scenario: Logistics package tracking assistant.
/// </summary>
public static class AccessDataInFunctionsDemo
{
public static async Task RunAsync()
{
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY")
?? throw new InvalidOperationException("Set OPEN_AI_KEY environment variable.");

Console.WriteLine("====== Accessing Data in AI Functions — Logistics Tracker ======\n");

// -----------------------------------------------------------------------
// Demo 1: Closure
// The packageStatuses dictionary is captured from the enclosing scope.
// The AI model calls GetPackageStatus(trackingId) and gets live data.
// -----------------------------------------------------------------------
var packageStatuses = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["PKG-1001"] = "In transit — Warsaw hub",
["PKG-1002"] = "Out for delivery",
["PKG-1003"] = "Delivered — signed by J. Nowak",
["PKG-1004"] = "Awaiting customs clearance",
};

AIFunction getPackageStatus = AIFunctionFactory.Create(
(string trackingId) =>
packageStatuses.TryGetValue(trackingId, out var status)
? status
: "Tracking ID not found.",
"GetPackageStatus",
"Returns the current status of a package by its tracking ID");

Console.WriteLine("--- Demo 1: Closure ---");
IChatClient aiClient = new ChatClientBuilder(
new OpenAIClient(apiKey).GetChatClient("gpt-4o-mini").AsIChatClient())
.UseFunctionInvocation()
.Build();

ChatResponse r1 = await aiClient.GetResponseAsync(
"What is the status of package PKG-1002?",
new ChatOptions { Tools = [getPackageStatus] });
Console.WriteLine($"Assistant: {r1.Text}");
Console.WriteLine();

// -----------------------------------------------------------------------
// Demo 2: AIFunctionArguments — direct manual invocation
// The delegate signature accepts AIFunctionArguments instead of typed params.
// Caller passes a named-argument dictionary to AIFunction.InvokeAsync().
// -----------------------------------------------------------------------
AIFunction estimateDelivery = AIFunctionFactory.Create(
(AIFunctionArguments args) =>
{
string origin = args.TryGetValue("origin", out var o) ? o!.ToString()! : "?";
string destination = args.TryGetValue("destination", out var d) ? d!.ToString()! : "?";
string weightKg = args.TryGetValue("weightKg", out var w) ? w!.ToString()! : "1";

double kg = double.TryParse(weightKg, out var n) ? n : 1.0;
int days = kg <= 5 ? 2 : kg <= 20 ? 4 : 7;

return $"{origin} → {destination}: estimated {days} business day(s) for {kg:N1} kg";
});

Console.WriteLine("--- Demo 2: AIFunctionArguments (manual invocation) ---");
var manualResult = await estimateDelivery.InvokeAsync(new AIFunctionArguments
{
{ "origin", "Berlin" },
{ "destination", "Lisbon" },
{ "weightKg", "12" }
});
Console.WriteLine($"EstimateDelivery result: {manualResult}");
Console.WriteLine();

// -----------------------------------------------------------------------
// Demo 3: FunctionInvokingChatClient.CurrentContext.Options.AdditionalProperties
// The carrier code is injected per-request via ChatOptions.AdditionalProperties.
// The AI model never sees this value — it just asks for the carrier policy.
// -----------------------------------------------------------------------
AIFunction getCarrierPolicy = AIFunctionFactory.Create(
() =>
{
var props = FunctionInvokingChatClient.CurrentContext?.Options?.AdditionalProperties;
string carrier = props != null && props.TryGetValue("carrierCode", out var c)
? c!.ToString()!
: "UNKNOWN";

return carrier switch
{
"DHL" => "DHL: free returns within 30 days, signature required above €150.",
"UPS" => "UPS: next-day guarantee, Saturday delivery available.",
"FEDEX" => "FedEx: temperature-sensitive shipments accepted, declared value mandatory.",
_ => "No policy found for this carrier."
};
},
"GetCarrierPolicy",
"Returns the delivery and return policy for the active carrier");

var optionsWithContext = new ChatOptions
{
Tools = [getCarrierPolicy],
AdditionalProperties = new AdditionalPropertiesDictionary
{
["carrierCode"] = "DHL"
}
};

Console.WriteLine("--- Demo 3: AdditionalProperties via FunctionInvokingChatClient.CurrentContext ---");
ChatResponse r3 = await aiClient.GetResponseAsync(
"What is the carrier policy for my shipment?",
optionsWithContext);
Console.WriteLine($"Assistant: {r3.Text}");
Console.WriteLine();
}
}
Share this lesson: