Function calling is how you let an OpenAI model do things instead of just talking. You describe some functions, the model decides when one is needed, and your C# code runs it and hands back the result. It’s the mechanism behind every tool-using assistant. This guide walks through it end to end in .NET.
If you’re building full agents, the Microsoft Agent Framework wraps this loop for you — but understanding the raw mechanism is worth it.
The idea in one sentence
You send the model your message plus a list of function definitions (name, description, parameter schema). The model either answers directly, or replies “call getWeather with {city: 'Chennai'}.” You run that function, send the result back, and the model uses it to form its final answer.
Step 1: Define a function the model can call
Describe the function so the model knows what it does and what arguments it needs — the description quality directly affects whether the model calls it correctly:
using OpenAI.Chat;
ChatTool getWeatherTool = ChatTool.CreateFunctionTool(
functionName: "get_weather",
functionDescription: "Gets the current weather for a city.",
functionParameters: BinaryData.FromString("""
{
"type": "object",
"properties": {
"city": { "type": "string", "description": "The city name" }
},
"required": ["city"]
}
"""));
Step 2: Send the request with the tool attached
var client = new ChatClient("gpt-4o-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));
var options = new ChatCompletionOptions();
options.Tools.Add(getWeatherTool);
List<ChatMessage> messages = [new UserChatMessage("What's the weather in Chennai?")];
ChatCompletion completion = await client.CompleteChatAsync(messages, options);
Step 3: Handle the model’s tool call
If the model decided to call your function, completion.ToolCalls is populated. Run the real function, then send the result back so the model can finish:
if (completion.FinishReason == ChatFinishReason.ToolCalls)
{
messages.Add(new AssistantChatMessage(completion)); // record the model's request
foreach (var call in completion.ToolCalls)
{
if (call.FunctionName == "get_weather")
{
var args = JsonDocument.Parse(call.FunctionArguments);
string city = args.RootElement.GetProperty("city").GetString()!;
string result = GetWeather(city); // your real C# method
messages.Add(new ToolChatMessage(call.Id, result));
}
}
// Ask again — now the model has the tool result and can answer
completion = await client.CompleteChatAsync(messages, options);
}
Console.WriteLine(completion.Content[0].Text);
That’s the whole loop: request → model asks for a tool → you run it → send the result → model answers. For multiple or chained calls, you repeat until FinishReason is no longer ToolCalls.
Validate the arguments
The model’s arguments are model-generated, so treat them as untrusted input: validate types and ranges before doing anything with side effects. A model asking to call delete_account doesn’t mean your code should skip its own authorization checks — see securing AI agents.
Function calling vs. just asking for JSON
Function calling is also a reliable way to get structured output: the parameter schema is a JSON schema the model conforms to. If all you need is structured data (not an action), that’s a valid use of the same mechanism.
Note: OpenAI SDK type names (
ChatTool,ToolChatMessage) shift between versions. Verify against the OpenAI .NET SDK; the loop — define, send, handle tool call, return result, repeat — is stable, and it’s what higher-level frameworks automate.
Takeaway
Function calling turns an OpenAI model into something that can act: define functions with clear descriptions and schemas, attach them to the request, and when the model asks, run the real C# method and hand back the result. Validate the arguments, loop until the model stops calling tools, and you have the core of every AI assistant — the same loop the Agent Framework wraps so you don’t write it by hand.
Have a correction or a topic you want covered? Email mani.bc72@gmail.com.