Azure OpenAI gives .NET developers production-grade AI with enterprise controls. This guide shows how to use the Azure.AI.OpenAI client library for:

  • Chat/completions
  • Streaming
  • Function calling (tools)
  • Structured outputs (JSON Schema)
  • Assistants via the Microsoft Agent Framework

All samples target .NET 8/9 and work in console apps, APIs, and Blazor.

Prerequisites

  • Azure subscription + Azure OpenAI resource
  • A chat model deployment (for example: gpt-4o or gpt-4o-mini)
  • NuGet packages:
 dotnet add package Azure.AI.OpenAI
 dotnet add package Azure.Identity
 # For Assistants/Agents (optional)
 dotnet add package Microsoft.Agents.AI.OpenAI --prerelease

Recommended environment variables:

  • AZURE_OPENAI_ENDPOINT = https://.openai.azure.com
  • AZURE_OPENAI_KEY = (only for key auth)

Create the client

using System;
using Azure;
using Azure.AI.OpenAI;
using Azure.Identity;

// Using API key
var endpoint = new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!);
var key = new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!);
var aoai = new AzureOpenAIClient(endpoint, key);

// Or, using Microsoft Entra ID (Managed Identity, Visual Studio/CLI, etc.)
// var aoai = new AzureOpenAIClient(endpoint, new DefaultAzureCredential());

// Use your deployment name (NOT the base model name)
var chat = aoai.GetChatClient("gpt-4o-mini"); // e.g., "my-gpt4o-deployment"

Notes

  • In Azure, pass the deployment name you created in the Azure AI Foundry portal.
  • For production, prefer Microsoft Entra ID over key-based auth.

1) Chat/Completions (basic)

using OpenAI.Chat;

var result = await chat.CompleteChatAsync([
    new SystemChatMessage("You are a concise, helpful assistant."),
    new UserChatMessage("Explain Dependency Injection in one sentence.")
]);

// Result content can have multiple parts (text, tool-calls, etc.)
foreach (var part in result.Value.Content)
{
    if (part is TextContent text)
    {
        Console.WriteLine(text.Text);
    }
}

2) Streaming

Streaming delivers tokens incrementally " great for responsive UIs (e.g., Blazor Server/Wasm).

await foreach (var update in chat.CompleteChatStreamingAsync([
    new SystemChatMessage("You stream answers word-by-word."),
    new UserChatMessage("Write a 1-2 sentence summary of CQRS.")
]))
{
    if (update is StreamingChatCompletionUpdate u)
    {
        foreach (var part in u.ContentUpdate)
        {
            if (part is TextContent text)
            {
                Console.Write(text.Text); // append incrementally
            }
        }
    }
}
Console.WriteLine();

Blazor tip: push streamed chunks into a StringBuilder, call StateHasChanged() (throttled), and render.

3) Function Calling (Tools)

Let the model call your code via tool definitions. Typical loop:

  1. Send user/system messages + tool definitions
  2. If the model returns tool calls, execute them in .NET
  3. Send the tool results back to the model to get the final answer
  • Define a function tool with parameters via JSON Schema
using System.Text.Json;
using OpenAI.Chat;

var getWeather = ChatTool.CreateFunctionTool(
    name: "get_weather",
    description: "Get current weather for a city",
    // Minimal JSON schema for parameters
    parameters: BinaryData.FromString("""
    {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "location": { "type": "string", "description": "City name" },
        "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
      },
      "required": ["location", "unit"]
    }
    """));
  • Ask the model; allow auto tool choice
var first = await chat.CompleteChatAsync(
    messages: [ new UserChatMessage("What's the weather in Paris in celsius?") ],
    options: new ChatCompletionOptions
    {
        Tools = { getWeather },
        ToolChoice = ChatToolChoice.Auto
    });
  • If the model decided to call a tool, execute it
var followUpMessages = new List<ChatMessage>();
foreach (var part in first.Value.Content)
{
    if (part is FunctionCallContent call)
    {
        // Parse arguments
        var args = JsonDocument.Parse(call.Arguments);
        var location = args.RootElement.GetProperty("location").GetString();
        var unit = args.RootElement.GetProperty("unit").GetString();

        // Your .NET function
        var weather = await GetWeatherAsync(location!, unit!);

        // 4) Send tool result back using a tool message tied to the call id
        followUpMessages.Add(new ToolChatMessage(call.Id, JsonSerializer.Serialize(weather)));
    }
}
  • Ask the model to produce the final natural language answer
var final = await chat.CompleteChatAsync([
    new UserChatMessage("What's the weather in Paris in celsius?"),
    ..followUpMessages
]);

foreach (var part in final.Value.Content)
{
    if (part is TextContent text)
    {
        Console.WriteLine(text.Text);
    }
}

// Example app function
static Task<object> GetWeatherAsync(string location, string unit)
{
    // Replace with a real API call
    var tempC = 18.4;
    var tempF = (tempC * 9 / 5) + 32;
    return Task.FromResult<object>(new
    {
        location,
        unit,
        temperature = unit == "fahrenheit" ? tempF : tempC,
        condition = "Cloudy"
    });
}

Tips

  • Keep tool schemas strict and small; validate inputs server-side.
  • Return machine-friendly JSON; the model will translate for the user.

4) Structured Outputs (JSON Schema)

Use structured outputs to force the model to return JSON that matches a schema " ideal for parsing, validation, and multi-step workflows.

using OpenAI.Chat;

var issueSchema = """
{
  "name": "GitHubIssue",
  "schema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "title": { "type": "string" },
      "labels": {
        "type": "array",
        "items": { "type": "string" }
      },
      "priority": { "type": "string", "enum": ["low","medium","high"] },
      "assignee": { "type": ["string","null"] }
    },
    "required": ["title","labels","priority","assignee"]
  },
  "strict": true
}
""";

var response = await chat.CompleteChatAsync(
    messages: [
        new SystemChatMessage("Extract a GitHub issue object from the user's description."),
        new UserChatMessage("Bug: Search page throws 500 when query is empty. Probably needs null-check. Assign to Alice. Labels: bug, backend. Priority high.")
    ],
    options: new ChatCompletionOptions
    {
        ResponseFormat = ChatResponseFormat.CreateJsonSchema(BinaryData.FromString(issueSchema))
    });

// Guaranteed to match the schema (strict mode) " safe to parse
var json = response.Value.ToJson(); // or iterate parts and read text
Console.WriteLine(json);

Important

  • Structured outputs require models/APIs that support json_schema with strict: true.
  • Some features (e.g., Assistants/Agents, certain audio models) may not support structured outputs.
  • Ensure each object has additionalProperties: false and every field is listed in required.

5) Assistants with the Microsoft Agent Framework (ChatCompletion Agents)

If you prefer a higher-level agent runtime (tools, state, and streaming built-in), use the Microsoft Agent Framework over Chat Completions.

using System;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using OpenAI.Chat;

var aoai = new AzureOpenAIClient(new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!), new DefaultAzureCredential());
var chat = aoai.GetChatClient("gpt-4o-mini");

AIAgent agent = chat.CreateAIAgent(
    instructions: "You are a helpful engineering assistant.",
    name: "EngAssistant");

// Simple run
Console.WriteLine(await agent.RunAsync("Give me 3 tips for API resiliency."));

// Streaming
await foreach (var chunk in agent.RunStreamingAsync("Explain circuit breakers in 2 sentences."))
{
    Console.Write(chunk);
}

Notes

  • You can register function tools with the agent and handle tool invocations similarly.
  • Prefer Agents for multi-step problems where orchestration/state helps.

Choosing between features

  • Chat/completions: flexible, low-level control; you own orchestration
  • Streaming: best UX for interactive apps (e.g., Blazor)
  • Function calling: model calls your APIs; great for retrieval, actions, and tool-use
  • Structured outputs: enforce JSON shape for robust parsing and workflows
  • Assistants/Agents: higher-level runtime when you want built-in planning and streaming

Production tips

  • Prefer Entra ID auth; scope credentials via DefaultAzureCredential
  • Log prompts, tool calls, and token usage (exclude secrets/PII)
  • Timeouts, retries, and circuit breakers around network calls
  • Validate and bound inputs to tools; sanitize outputs
  • Version your prompts and schemas; add unit tests for prompts and tools
  • Cache non-personal results where appropriate (e.g., Azure Cache for Redis)

References