Generative AI for Beginners Generative AI Techniques Created: 26 Mar 2026 Updated: 26 Mar 2026

Function Calling with Microsoft.Extensions.AI

AI models are powerful text generators, but they cannot access live data, query databases, or perform precise calculations on their own. Function calling bridges this gap by letting the model request that your .NET code run a specific function, then use the result in its response.

How Function Calling Works

  1. You define regular C# methods and annotate them with [Description].
  2. You register those methods as tools in ChatOptions.Tools using AIFunctionFactory.Create.
  3. You enable automatic invocation with .UseFunctionInvocation() on the client builder.
  4. When the AI needs external information, it calls the appropriate function automatically.
  5. Your code runs the function and returns the result; the AI incorporates it into the final response.

Part 1 — Basic Function Call

Define a function with a [Description] attribute so the AI knows what it does:

[Description("Get a workout suggestion for a specific muscle group")]
public static string GetWorkoutSuggestion(
[Description("The muscle group to target, e.g. chest, back, legs, shoulders, arms, core")]
string muscleGroup)
{
var workouts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["chest"] = "Bench press 4x8, incline dumbbell press 3x10, cable flyes 3x12",
["legs"] = "Squat 4x8, Romanian deadlift 3x10, leg press 3x12, calf raises 4x15",
// ... more entries
};

return workouts.TryGetValue(muscleGroup, out var plan)
? $"Workout for {muscleGroup}: {plan}"
: $"No specific plan found for '{muscleGroup}'.";
}

Register the function and enable automatic invocation:

IChatClient chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation() // Enables automatic function calling
.Build();

var basicOptions = new ChatOptions
{
Tools = [AIFunctionFactory.Create(GymTools.GetWorkoutSuggestion)]
};

var response = await chatClient.GetResponseAsync(
"I want to train my legs today. What exercises should I do?",
basicOptions);

Console.WriteLine(response.Text);

The AI recognises it needs workout data, calls GetWorkoutSuggestion("legs"), receives the exercise plan, and formulates an answer around it.

Part 2 — Functions with Multiple Parameters

Functions can accept multiple parameters of different types. The AI infers each parameter from the user's message:

[Description("Calculate BMI (Body Mass Index) from weight and height")]
public static string CalculateBmi(
[Description("Body weight in kilograms")] double weightKg,
[Description("Height in centimeters")] double heightCm)
{
var heightM = heightCm / 100.0;
var bmi = weightKg / (heightM * heightM);

var category = bmi switch
{
< 18.5 => "Underweight",
< 25.0 => "Normal weight",
< 30.0 => "Overweight",
_ => "Obese"
};

return $"BMI: {bmi:F1} — Category: {category}";
}
var response = await chatClient.GetResponseAsync(
"I weigh 78 kilograms and I'm 175 cm tall. What's my BMI? Is that healthy?",
bmiOptions);

The AI extracts 78 and 175 from the sentence and passes them as weightKg and heightCm.

Part 3 — Parameter Descriptions and Default Values

Good [Description] attributes on parameters help the AI understand when and how to call the function. Default values mean the AI can omit optional parameters:

[Description("Search for exercises filtered by difficulty level")]
public static string SearchExercises(
[Description("Search query — exercise name, body part, or keywords")] string query,
[Description("Difficulty filter: 'beginner', 'intermediate', 'advanced', or 'all'")] string difficulty = "all",
[Description("Maximum number of results to return (1-10)")] int maxResults = 5)
{
// ... filter and return matching exercises
}
var response = await chatClient.GetResponseAsync(
"Show me beginner-friendly chest exercises.",
searchOptions);

The AI sets query = "chest", difficulty = "beginner", and uses the default maxResults = 5.

Part 4 — Multiple Functions

Register several functions in the same ChatOptions.Tools array. The AI chooses which function(s) to call based on the question:

var multiOptions = new ChatOptions
{
Tools =
[
AIFunctionFactory.Create(GymTools.GetWorkoutSuggestion),
AIFunctionFactory.Create(GymTools.CalculateBmi),
AIFunctionFactory.Create(GymTools.GetGymSchedule),
AIFunctionFactory.Create(GymTools.SearchExercises),
AIFunctionFactory.Create(GymTools.EstimateCaloriesBurned)
]
};
// AI picks GetGymSchedule
await chatClient.GetResponseAsync(
"What classes are available at the gym this Saturday?", multiOptions);

// AI picks EstimateCaloriesBurned
await chatClient.GetResponseAsync(
"If I run for 30 minutes and weigh 80 kg, how many calories will I burn?", multiOptions);

// AI may call BOTH CalculateBmi AND GetWorkoutSuggestion
await chatClient.GetResponseAsync(
"I weigh 90 kg and I'm 180 cm. Give me my BMI and suggest a good back workout.", multiOptions);

For the last query the AI calls two functions, combines the results, and delivers a single coherent answer.

Part 5 — Functions in Conversations

Function calling works seamlessly with conversation history. The AI remembers earlier context and decides when a function call is needed:

List<ChatMessage> history =
[
new(ChatRole.System,
"You are a friendly gym assistant. Keep answers concise and motivational.")
];

// Turn 1 — general question, no function needed
history.Add(new(ChatRole.User, "I'm new to the gym. Where should I start?"));
var turn1 = await chatClient.GetResponseAsync(history, multiOptions);
history.AddMessages(turn1);

// Turn 2 — triggers GetGymSchedule("Wednesday")
history.Add(new(ChatRole.User, "What group classes do you have on Wednesday?"));
var turn2 = await chatClient.GetResponseAsync(history, multiOptions);
history.AddMessages(turn2);

// Turn 3 — uses conversation context + triggers GetWorkoutSuggestion("shoulders")
history.Add(new(ChatRole.User, "I'd like a shoulder workout for after the Spin Class."));
var turn3 = await chatClient.GetResponseAsync(history, multiOptions);
history.AddMessages(turn3);

Part 6 — Function Calling with Streaming

Streaming and function calling work together. The stream pauses while a function executes, then resumes with the result incorporated:

await foreach (var update in chatClient.GetStreamingResponseAsync(
"How many calories does a 70 kg person burn doing 45 minutes of boxing? " +
"Also suggest a core workout to finish the session.",
multiOptions))
{
Console.Write(update.Text);
}

Part 7 — Graceful Error Handling

When a function receives unexpected input, return a helpful message instead of throwing an exception. The AI will relay the error gracefully:

[Description("Estimate calories burned for a given exercise session")]
public static string EstimateCaloriesBurned(
[Description("Type of exercise performed")] string exercise,
[Description("Duration in minutes")] int durationMinutes,
[Description("Body weight in kilograms")] double weightKg)
{
var metValues = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
["running"] = 9.8, ["cycling"] = 7.5, ["swimming"] = 8.0,
["walking"] = 3.8, ["yoga"] = 3.0, ["weightlifting"] = 6.0,
// ...
};

if (!metValues.TryGetValue(exercise, out var met))
return $"Unknown exercise '{exercise}'. Try: {string.Join(", ", metValues.Keys)}.";

var calories = met * weightKg * (durationMinutes / 60.0);
return $"Estimated calories burned: {calories:F0} kcal";
}

When the user asks about an exercise not in the dictionary (e.g. “trampolining”), the function returns a meaningful fallback and the AI still produces a helpful answer.

Best Practices

PracticeWhy
Write clear [Description] attributesThe AI uses these to decide when to call the function.
Keep functions focused (single responsibility)Smaller, well-named functions are easier for the AI to choose correctly.
Return strings with helpful error messagesThe AI can relay the error to the user instead of crashing.
Add [Description] to parameters tooHelps the AI map natural language to the right arguments.
Use default parameter valuesLets the AI omit optional parameters when the user doesn't specify them.

Summary

ConceptKey Takeaway
Function CallingLet the AI call your C# methods when it needs external data.
AIFunctionFactory.CreateWraps a .NET method as an AI-callable tool.
UseFunctionInvocation()Enables the client to execute functions automatically.
[Description]Tells the AI when and how to use the function.
Multiple FunctionsThe AI picks the right tool(s) based on the question.
Conversation HistoryFunctions work seamlessly across multi-turn chats.
Streaming + FunctionsThe stream pauses during function execution, then resumes.

Full Example

using System.ComponentModel;
using Microsoft.Extensions.AI;
using OpenAI;

namespace MicrosoftAgentFrameworkLesson.ConsoleApp;

// ── Tool functions (Fitness / Gym domain) ────────────────

public static class GymTools
{
[Description("Get a workout suggestion for a specific muscle group")]
public static string GetWorkoutSuggestion(
[Description("The muscle group to target, e.g. chest, back, legs, shoulders, arms, core")]
string muscleGroup)
{
var workouts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["chest"] = "Bench press 4x8, incline dumbbell press 3x10, cable flyes 3x12",
["back"] = "Deadlift 4x6, barbell row 4x8, lat pulldown 3x10, face pulls 3x15",
["legs"] = "Squat 4x8, Romanian deadlift 3x10, leg press 3x12, calf raises 4x15",
["shoulders"] = "Overhead press 4x8, lateral raises 3x12, rear delt flyes 3x15",
["arms"] = "Barbell curl 3x10, tricep dips 3x10, hammer curls 3x12, skull crushers 3x10",
["core"] = "Plank 3x60s, hanging leg raises 3x12, Russian twists 3x20, ab wheel rollouts 3x10"
};

return workouts.TryGetValue(muscleGroup, out var plan)
? $"Workout for {muscleGroup}: {plan}"
: $"No specific plan found for '{muscleGroup}'. Try: chest, back, legs, shoulders, arms, or core.";
}

[Description("Calculate BMI (Body Mass Index) from weight and height")]
public static string CalculateBmi(
[Description("Body weight in kilograms")] double weightKg,
[Description("Height in centimeters")] double heightCm)
{
if (weightKg <= 0 || heightCm <= 0)
return "Invalid input. Weight and height must be positive numbers.";

var heightM = heightCm / 100.0;
var bmi = weightKg / (heightM * heightM);

var category = bmi switch
{
< 18.5 => "Underweight",
< 25.0 => "Normal weight",
< 30.0 => "Overweight",
_ => "Obese"
};

return $"BMI: {bmi:F1} — Category: {category} (weight {weightKg} kg, height {heightCm} cm)";
}

[Description("Get the gym class schedule for a specific day of the week")]
public static string GetGymSchedule(
[Description("Day of the week, e.g. Monday, Tuesday")] string dayOfWeek)
{
var schedule = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Monday"] = "06:00 Spin Class | 08:00 Yoga | 12:00 HIIT | 18:00 Boxing",
["Tuesday"] = "07:00 Pilates | 09:00 Strength Training | 17:00 Zumba | 19:00 CrossFit",
["Wednesday"] = "06:00 Spin Class | 10:00 Aqua Aerobics | 16:00 Kickboxing | 18:00 Yoga",
["Thursday"] = "07:00 HIIT | 09:00 Body Pump | 17:00 Pilates | 19:00 Boxing",
["Friday"] = "06:00 Yoga | 08:00 Spin Class | 12:00 Zumba | 17:00 Open Gym",
["Saturday"] = "08:00 CrossFit | 10:00 Yoga | 14:00 Family Fitness",
["Sunday"] = "09:00 Gentle Yoga | 11:00 Open Gym"
};

return schedule.TryGetValue(dayOfWeek, out var classes)
? $"{dayOfWeek} schedule: {classes}"
: $"No schedule found for '{dayOfWeek}'. The gym is open Monday through Sunday.";
}

[Description("Search for exercises filtered by difficulty level")]
public static string SearchExercises(
[Description("Search query — exercise name, body part, or keywords")] string query,
[Description("Difficulty filter: 'beginner', 'intermediate', 'advanced', or 'all'")] string difficulty = "all",
[Description("Maximum number of results to return (1-10)")] int maxResults = 5)
{
// Simplified catalog
var exercises = new List<(string Name, string Difficulty, string BodyPart)>
{
("Push-ups", "beginner", "chest"),
("Bench Press", "intermediate", "chest"),
("Weighted Dips", "advanced", "chest"),
("Bodyweight Squat", "beginner", "legs"),
("Barbell Squat", "intermediate", "legs"),
("Pistol Squat", "advanced", "legs"),
("Plank", "beginner", "core"),
("Dragon Flag", "advanced", "core"),
("Pull-ups", "intermediate", "back"),
("Muscle-up", "advanced", "back"),
("Dumbbell Curl", "beginner", "arms"),
("Barbell Row", "intermediate", "back"),
("Burpees", "intermediate", "full body"),
("Jumping Jacks", "beginner", "full body"),
("Kettlebell Swing", "intermediate", "full body")
};

var results = exercises
.Where(e => e.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
e.BodyPart.Contains(query, StringComparison.OrdinalIgnoreCase))
.Where(e => difficulty == "all" ||
e.Difficulty.Equals(difficulty, StringComparison.OrdinalIgnoreCase))
.Take(maxResults)
.Select(e => $" • {e.Name} [{e.Difficulty}] — {e.BodyPart}")
.ToList();

return results.Count > 0
? $"Found {results.Count} exercise(s):\n{string.Join("\n", results)}"
: $"No exercises found for query '{query}' with difficulty '{difficulty}'.";
}

[Description("Estimate calories burned for a given exercise session")]
public static string EstimateCaloriesBurned(
[Description("Type of exercise performed")] string exercise,
[Description("Duration of the session in minutes")] int durationMinutes,
[Description("Body weight in kilograms")] double weightKg)
{
// Simplified MET-based estimation
var metValues = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
["running"] = 9.8,
["cycling"] = 7.5,
["swimming"] = 8.0,
["walking"] = 3.8,
["yoga"] = 3.0,
["weightlifting"] = 6.0,
["hiit"] = 10.0,
["boxing"] = 9.0,
["crossfit"] = 9.5,
["pilates"] = 4.0
};

if (!metValues.TryGetValue(exercise, out var met))
return $"Unknown exercise '{exercise}'. Try: {string.Join(", ", metValues.Keys)}.";

var calories = met * weightKg * (durationMinutes / 60.0);
return $"Estimated calories burned: {calories:F0} kcal ({exercise}, {durationMinutes} min, {weightKg} kg body weight)";
}
}

// ── Demo class ───────────────────────────────────────────

public static class FunctionCallingDemo
{
public static async Task RunAsync()
{
// 1. Retrieve API key
var apiKey = Environment.GetEnvironmentVariable("OPEN_AI_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
Console.WriteLine("Please set the OPEN_AI_KEY environment variable.");
return;
}

// 2. Create an IChatClient with function invocation enabled
IChatClient chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient()
.AsBuilder()
.UseFunctionInvocation() // Enables automatic function calling
.Build();

// ══════════════════════════════════════════════
// PART 1 — Basic Function Call
// ══════════════════════════════════════════════
Console.WriteLine("═══ Part 1: Basic Function Call ═══\n");

var basicOptions = new ChatOptions
{
Tools = [AIFunctionFactory.Create(GymTools.GetWorkoutSuggestion)]
};

var response = await chatClient.GetResponseAsync(
"I want to train my legs today. What exercises should I do?",
basicOptions);

Console.WriteLine(response.Text);

// ══════════════════════════════════════════════
// PART 2 — Functions with Multiple Parameters
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Part 2: Multi-Parameter Function ═══\n");

var bmiOptions = new ChatOptions
{
Tools = [AIFunctionFactory.Create(GymTools.CalculateBmi)]
};

response = await chatClient.GetResponseAsync(
"I weigh 78 kilograms and I'm 175 cm tall. What's my BMI? Is that healthy?",
bmiOptions);

Console.WriteLine(response.Text);

// ══════════════════════════════════════════════
// PART 3 — Parameter Descriptions & Defaults
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Part 3: Search with Filters ═══\n");

var searchOptions = new ChatOptions
{
Tools = [AIFunctionFactory.Create(GymTools.SearchExercises)]
};

response = await chatClient.GetResponseAsync(
"Show me beginner-friendly chest exercises.",
searchOptions);

Console.WriteLine(response.Text);

// ══════════════════════════════════════════════
// PART 4 — Multiple Functions
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Part 4: Multiple Functions ═══\n");

var multiOptions = new ChatOptions
{
Tools =
[
AIFunctionFactory.Create(GymTools.GetWorkoutSuggestion),
AIFunctionFactory.Create(GymTools.CalculateBmi),
AIFunctionFactory.Create(GymTools.GetGymSchedule),
AIFunctionFactory.Create(GymTools.SearchExercises),
AIFunctionFactory.Create(GymTools.EstimateCaloriesBurned)
]
};

// AI picks the right function automatically
response = await chatClient.GetResponseAsync(
"What classes are available at the gym this Saturday?",
multiOptions);
Console.WriteLine($"Schedule question → {response.Text}\n");

response = await chatClient.GetResponseAsync(
"If I run for 30 minutes and weigh 80 kg, how many calories will I burn?",
multiOptions);
Console.WriteLine($"Calorie question → {response.Text}\n");

// AI may call multiple functions for a complex query
response = await chatClient.GetResponseAsync(
"I weigh 90 kg and I'm 180 cm. Give me my BMI and suggest a good back workout.",
multiOptions);
Console.WriteLine($"Combined question → {response.Text}");

// ══════════════════════════════════════════════
// PART 5 — Functions in Conversations
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Part 5: Function Calling with Conversation History ═══\n");

List<ChatMessage> history =
[
new(ChatRole.System,
"You are a friendly gym assistant. Keep answers concise and motivational.")
];

// Turn 1 — general question (no function needed)
history.Add(new(ChatRole.User, "I'm new to the gym. Where should I start?"));
var turn1 = await chatClient.GetResponseAsync(history, multiOptions);
history.AddMessages(turn1);
Console.WriteLine($"User : I'm new to the gym. Where should I start?");
Console.WriteLine($"Coach: {turn1.Text}\n");

// Turn 2 — triggers GetGymSchedule
history.Add(new(ChatRole.User, "What group classes do you have on Wednesday?"));
var turn2 = await chatClient.GetResponseAsync(history, multiOptions);
history.AddMessages(turn2);
Console.WriteLine($"User : What group classes do you have on Wednesday?");
Console.WriteLine($"Coach: {turn2.Text}\n");

// Turn 3 — triggers GetWorkoutSuggestion (AI uses conversation context)
history.Add(new(ChatRole.User, "I'd like a shoulder workout for after the Spin Class."));
var turn3 = await chatClient.GetResponseAsync(history, multiOptions);
history.AddMessages(turn3);
Console.WriteLine($"User : I'd like a shoulder workout for after the Spin Class.");
Console.WriteLine($"Coach: {turn3.Text}");

// ══════════════════════════════════════════════
// PART 6 — Function Calling with Streaming
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Part 6: Function Calling + Streaming ═══\n");

await foreach (var update in chatClient.GetStreamingResponseAsync(
"How many calories does a 70 kg person burn doing 45 minutes of boxing? " +
"Also suggest a core workout to finish the session.",
multiOptions))
{
Console.Write(update.Text);
}

Console.WriteLine("\n");

// ══════════════════════════════════════════════
// PART 7 — Error Handling in Functions
// ══════════════════════════════════════════════
Console.WriteLine("═══ Part 7: Graceful Error Handling ═══\n");

response = await chatClient.GetResponseAsync(
"How many calories does a 75 kg person burn doing trampolining for 20 minutes?",
multiOptions);

Console.WriteLine(response.Text);
}
}
Share this lesson: