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

Streaming and Structured Output with Microsoft.Extensions.AI

In this lesson you will learn two essential techniques for building responsive, production-ready AI applications in .NET: streaming (displaying tokens as they arrive) and structured output (getting strongly-typed objects instead of plain text).

Why Streaming Matters

With GetResponseAsync the user stares at a blank screen until the entire response is ready. For long answers this feels sluggish. Streaming lets you display each token the moment it arrives — exactly like ChatGPT or Copilot.

RegularStreaming
GetResponseAsyncGetStreamingResponseAsync
Returns ChatResponseReturns IAsyncEnumerable<ChatResponseUpdate>
Wait for full responseProcess each token as it arrives
Simpler codeBetter user experience

Part 1 — Streaming Responses

Basic Streaming Pattern

Replace GetResponseAsync with GetStreamingResponseAsync and iterate with await foreach:

await foreach (ChatResponseUpdate update in
chatClient.GetStreamingResponseAsync(
"Create a 3-day travel itinerary for someone visiting Kyoto, Japan for the first time."))
{
Console.Write(update.Text);
}

Each ChatResponseUpdate typically contains a single word or token. Writing it immediately gives the user real-time feedback.

Streaming with Conversation History

You can combine streaming with a message history list. Collect all updates, then call AddMessages so the next turn includes the full context:

List<ChatMessage> travelChat = [
new(ChatRole.System,
"You are a seasoned travel guide. Give brief, practical advice.")
];

travelChat.Add(new(ChatRole.User, "What is the best way to get around Istanbul on a budget?"));

List<ChatResponseUpdate> updates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(travelChat))
{
Console.Write(update.Text);
updates.Add(update);
}

// Merge streamed tokens into history
travelChat.AddMessages(updates);

Converting Updates to a Full Response

Sometimes you need the complete ChatResponse object after streaming. Use ToChatResponse:

List<ChatResponseUpdate> allUpdates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(
"Name five must-try street foods in Bangkok, Thailand."))
{
Console.Write(update.Text);
allUpdates.Add(update);
}

ChatResponse fullResponse = allUpdates.ToChatResponse();
Console.WriteLine($"Collected {fullResponse.Messages.Count} message(s) from the stream.");

Part 2 — Structured Output

Plain text is flexible, but sometimes your code needs data in a specific shape — enums, records, or lists. Microsoft.Extensions.AI provides GetResponseAsync<T> which instructs the model to return JSON matching your type and deserializes it automatically.

Enum Classification

Step 1 — Define the type:

public enum TripType
{
Beach,
Mountain,
City,
Cultural,
Adventure
}

Step 2 — Request a typed response:

string[] destinations = [
"Bali — famous for its beaches, surf spots and ocean sunsets.",
"Zurich — a walkable European city with museums, trams and lakeside cafés.",
"Machu Picchu — ancient Inca ruins perched high in the Andes mountains.",
"Marrakech — vibrant souks, riads, and centuries of Moroccan heritage.",
"Queenstown — bungee jumping, skydiving and jet-boat rides in New Zealand."
];

foreach (var dest in destinations)
{
var result = await chatClient.GetResponseAsync<TripType>(
$"Classify this destination into a trip type: {dest}");
Console.WriteLine($"{dest.Split('—')[0].Trim()} → {result.Result}");
}

Expected output:

Bali → Beach
Zurich → City
Machu Picchu → Mountain
Marrakech → Cultural
Queenstown → Adventure

Complex Structured Output with Records

For richer data define a record:

public record DestinationProfile(
string DestinationName,
TripType Category,
string BestSeason,
int EstimatedDailyCostUsd,
string[] TopAttractions
);
var profileResult = await chatClient.GetResponseAsync<DestinationProfile>(
"Create a travel profile for Reykjavik, Iceland.");

var profile = profileResult.Result;
Console.WriteLine($"Destination : {profile.DestinationName}");
Console.WriteLine($"Category : {profile.Category}");
Console.WriteLine($"Best Season : {profile.BestSeason}");
Console.WriteLine($"Daily Cost : ${profile.EstimatedDailyCostUsd}");
Console.WriteLine($"Attractions : {string.Join(", ", profile.TopAttractions)}");

Sample output:

Destination : Reykjavik
Category : Adventure
Best Season : Summer (June–August)
Daily Cost : $180
Attractions : Blue Lagoon, Hallgrímskirkja, Golden Circle

Lists of Structured Items

You can request a List<T> just as easily:

public record PackingItem(
string Item,
string Reason,
bool Essential
);

var tripDescription = """
We are going on a week-long winter trek through the Scottish Highlands.
Expect rain, muddy trails, near-freezing temperatures at night,
and limited access to shops once we leave Fort William.
""";

var packingResult = await chatClient.GetResponseAsync<List<PackingItem>>(
$"Generate a packing list for this trip: {tripDescription}");

foreach (var item in packingResult.Result)
{
var tag = item.Essential ? "ESSENTIAL" : "optional";
Console.WriteLine($"[{tag}] {item.Item} — {item.Reason}");
}

Sample output:

[ESSENTIAL] Waterproof jacket — Rain is near-constant in the Highlands
[ESSENTIAL] Insulated sleeping bag — Temperatures drop close to 0 °C
[optional] Trekking poles — Helpful on muddy, uneven terrain

Part 3 — Combining Streaming and Structured Output

For the best of both worlds: stream a conversational response for real-time feedback, then make a second structured-output call to extract machine-readable data.

// 1. Stream the travel recommendation
StringBuilder streamed = new();
await foreach (var update in chatClient.GetStreamingResponseAsync(
"Recommend a 5-day road trip route through Portugal."))
{
Console.Write(update.Text);
streamed.Append(update.Text);
}

// 2. Extract structured stops from the streamed text
var stopsResult = await chatClient.GetResponseAsync<List<DestinationProfile>>(
$"Extract each city/stop as a DestinationProfile: {streamed}");

foreach (var stop in stopsResult.Result)
{
Console.WriteLine($" {stop.DestinationName} ({stop.Category}) — " +
$"{string.Join(", ", stop.TopAttractions)}");
}

ChatOptions — Fine-Tuning Responses

Both streaming and structured output accept a ChatOptions parameter for additional control:

var options = new ChatOptions
{
Temperature = 0.2f, // Lower = more deterministic
MaxOutputTokens = 150
};

await foreach (var update in chatClient.GetStreamingResponseAsync(
"Give three safety tips for solo travelers in South America.", options))
{
Console.Write(update.Text);
}
PropertyPurpose
TemperatureControls randomness (0–2). Lower = more focused.
MaxOutputTokensLimits response length.
TopPAlternative to temperature for controlling randomness.
ModelIdOverride the default model.

Summary

ConceptKey Takeaway
StreamingDisplay tokens as they arrive for a responsive UX.
IAsyncEnumerableC# pattern for async streams of data.
ToChatResponseConvert stream updates into a complete response object.
Structured OutputGet strongly-typed objects instead of plain text.
Record TypesIdeal for defining output schemas the model must follow.

Full Example

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

namespace MicrosoftAgentFrameworkLesson.ConsoleApp;

// ── Structured-output types ─────────────────────────────

public enum TripType
{
Beach,
Mountain,
City,
Cultural,
Adventure
}

public record DestinationProfile(
string DestinationName,
TripType Category,
string BestSeason,
int EstimatedDailyCostUsd,
string[] TopAttractions
);

public record PackingItem(
string Item,
string Reason,
bool Essential
);

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

public static class StreamingAndStructuredOutputDemo
{
public static async Task RunAsync()
{
// 1. Retrieve API key from environment
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
IChatClient chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4o-mini")
.AsIChatClient();

// ══════════════════════════════════════════════
// PART 1 — Streaming Responses
// ══════════════════════════════════════════════
Console.WriteLine("═══ Part 1: Streaming a Travel Itinerary ═══\n");

await foreach (ChatResponseUpdate update in
chatClient.GetStreamingResponseAsync(
"Create a 3-day travel itinerary for someone visiting Kyoto, Japan for the first time. Be concise."))
{
Console.Write(update.Text);
}
Console.WriteLine("\n");

// Streaming with conversation history
Console.WriteLine("═══ Part 1b: Streaming Chat with History ═══\n");

List<ChatMessage> travelChat =
[
new(ChatRole.System,
"You are a seasoned travel guide. Give brief, practical advice. " +
"Limit answers to three sentences maximum.")
];

string[] travelerQuestions =
[
"What is the best way to get around Istanbul on a budget?",
"Any hidden gems the tourists usually miss there?"
];

foreach (var question in travelerQuestions)
{
Console.WriteLine($"Traveler: {question}");
travelChat.Add(new(ChatRole.User, question));

List<ChatResponseUpdate> updates = [];
Console.Write("Guide: ");
await foreach (var update in chatClient.GetStreamingResponseAsync(travelChat))
{
Console.Write(update.Text);
updates.Add(update);
}
Console.WriteLine("\n");

travelChat.AddMessages(updates);
}

// Converting stream updates to a full ChatResponse
Console.WriteLine("═══ Part 1c: ToChatResponse ═══\n");

List<ChatResponseUpdate> allUpdates = [];
await foreach (var update in chatClient.GetStreamingResponseAsync(
"Name five must-try street foods in Bangkok, Thailand."))
{
Console.Write(update.Text);
allUpdates.Add(update);
}

ChatResponse fullResponse = allUpdates.ToChatResponse();
Console.WriteLine($"\n\n→ Collected {fullResponse.Messages.Count} message(s) from the stream.\n");

// ══════════════════════════════════════════════
// PART 2 — Structured Output
// ══════════════════════════════════════════════

// 2a — Enum classification
Console.WriteLine("═══ Part 2a: Trip Type Classification ═══\n");

string[] destinations =
[
"Bali — famous for its beaches, surf spots and ocean sunsets.",
"Zurich — a walkable European city with museums, trams and lakeside cafés.",
"Machu Picchu — ancient Inca ruins perched high in the Andes mountains.",
"Marrakech — vibrant souks, riads, and centuries of Moroccan heritage.",
"Queenstown — bungee jumping, skydiving and jet-boat rides in New Zealand."
];

foreach (var dest in destinations)
{
var result = await chatClient.GetResponseAsync<TripType>(
$"Classify this destination into a trip type: {dest}");
Console.WriteLine($"{dest.Split('—')[0].Trim(),-16} → {result.Result}");
}

// 2b — Complex record
Console.WriteLine("\n═══ Part 2b: Destination Profile ═══\n");

var profileResult = await chatClient.GetResponseAsync<DestinationProfile>(
"Create a travel profile for Reykjavik, Iceland. " +
"Include destination name, category, best season, estimated daily cost in USD, and top 3 attractions.");

var profile = profileResult.Result;
Console.WriteLine($"Destination : {profile.DestinationName}");
Console.WriteLine($"Category : {profile.Category}");
Console.WriteLine($"Best Season : {profile.BestSeason}");
Console.WriteLine($"Daily Cost : ${profile.EstimatedDailyCostUsd}");
Console.WriteLine($"Attractions : {string.Join(", ", profile.TopAttractions)}");

// 2c — List of structured items
Console.WriteLine("\n═══ Part 2c: Packing List Extraction ═══\n");

var tripDescription = """
We are going on a week-long winter trek through the Scottish Highlands.
Expect rain, muddy trails, near-freezing temperatures at night,
and limited access to shops once we leave Fort William.
""";

var packingResult = await chatClient.GetResponseAsync<List<PackingItem>>(
$"Generate a packing list for this trip: {tripDescription}");

foreach (var item in packingResult.Result)
{
var tag = item.Essential ? "ESSENTIAL" : "optional";
Console.WriteLine($"[{tag,-9}] {item.Item,-28} — {item.Reason}");
}

// ══════════════════════════════════════════════
// PART 3 — Combining Streaming + Structured Output
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Part 3: Stream then Structure ═══\n");

Console.WriteLine("Streaming a travel recommendation...\n");
StringBuilder streamed = new();
await foreach (var update in chatClient.GetStreamingResponseAsync(
"Recommend a 5-day road trip route through Portugal. Mention cities, driving times and highlights."))
{
Console.Write(update.Text);
streamed.Append(update.Text);
}

Console.WriteLine("\n\nExtracting structured stops...\n");

var stopsResult = await chatClient.GetResponseAsync<List<DestinationProfile>>(
$"Extract each city/stop from this road trip as a DestinationProfile: {streamed}");

foreach (var stop in stopsResult.Result)
{
Console.WriteLine($" {stop.DestinationName} ({stop.Category}) — {string.Join(", ", stop.TopAttractions)}");
}

// ══════════════════════════════════════════════
// ChatOptions — Fine-tuning
// ══════════════════════════════════════════════
Console.WriteLine("\n═══ Bonus: ChatOptions ═══\n");

var options = new ChatOptions
{
Temperature = 0.2f,
MaxOutputTokens = 150
};

await foreach (var update in chatClient.GetStreamingResponseAsync(
"Give three safety tips for solo travelers in South America.", options))
{
Console.Write(update.Text);
}
Console.WriteLine();
}

Share this lesson: