DOTNET EXPERT BLOG

Structured AI Output in C# with IChatClient and ASP.NET Core | Day 6

10/1/2026 11:33:39 AM Noor All Safaet Loading... 0


So far, our AI application has mainly returned text.

For example:

Current stock is below the reorder level.
You should consider ordering approximately 20 more units.

That is useful when a human is reading the answer.

But what if our C# application needs to use the AI response as data?

Suppose an inventory system needs:

Stock Status
Risk Level
Recommended Reorder Quantity
Explanation

We could ask the AI to return text and try to extract those values.

But that quickly becomes fragile.

A much better approach is structured output.

Instead of receiving unpredictable natural-language text, we can ask the model for data that maps to a C# type.

In Day 6 of our C# AI tutorial series, we will build a practical inventory analysis feature using structured AI output.

We will learn how to:

  • create a strongly typed AI response model

  • use GetResponseAsync<T>()

  • work with ChatResponse<T>

  • access typed results through Result

  • use enums in structured output

  • validate AI-generated data

  • understand JSON schema-based responses

  • use ChatResponseFormat.ForJsonSchema<T>()

  • avoid fragile manual JSON parsing

  • keep AI output separate from trusted business decisions

By the end, our application will move beyond an AI chat interface and start using AI as part of application logic.


Previously in This Series

We have been building one AI application step by step.

Day 1: Build Your First AI Application with C# and .NET

We connected C# to an AI model.

Day 2: Understanding IChatClient

We introduced the common AI abstraction provided by Microsoft.Extensions.AI.

Day 3: Build a Reusable AI Chat Service in ASP.NET Core

We created an ASP.NET Core API with dependency injection.

Day 4: Add Conversation Memory

We introduced conversation IDs and chat history.

Day 5: Stream AI Responses

We streamed generated content from the model through ASP.NET Core to the browser.

Until now, the main output of our AI system has been:

string

Today we want:

AI Response
     ↓
C# Object
     ↓
Validation
     ↓
Application Logic

That is a major architectural step.


What Is Structured AI Output?

Consider this prompt:

Analyze this inventory item.

Product: Wireless Mouse
Current Stock: 4
Reorder Level: 10

Tell me the stock status, risk level,
recommended reorder quantity, and explanation.

A normal text response might be:

The Wireless Mouse currently has low stock.
Because only four units remain and the reorder
level is ten, I would classify the risk as high.
You may want to order around twenty units.

A human can understand that easily.

C# code cannot reliably treat that paragraph as structured business data.

We would rather receive something conceptually equivalent to:

{
  "productName": "Wireless Mouse",
  "stockStatus": "Low",
  "riskLevel": "High",
  "recommendedReorderQuantity": 20,
  "explanation": "Current stock is below the reorder level."
}

Now our application can work with:

result.ProductName
result.StockStatus
result.RiskLevel
result.RecommendedReorderQuantity
result.Explanation

instead of trying to interpret arbitrary prose.


Why Parsing Free-Form AI Text Is Fragile

Imagine writing:

string response =
    aiResponse.Text;

string[] parts =
    response.Split(',');

and expecting the model always to return:

Low, High, 20, Reorder immediately

It may instead return:

Stock Status: Low
Risk: High
Recommended Quantity: 20 units

or:

I would classify this product as low stock...

Natural language is flexible.

Application contracts should be predictable.

Structured output gives us a much cleaner boundary:

AI
 ↓
Defined Structure
 ↓
C# Type

Our Day 6 Example

We will build an inventory-analysis endpoint.

The client sends:

{
  "productName": "Wireless Mouse",
  "currentStock": 4,
  "reorderLevel": 10
}

The AI analyzes the situation.

Our ASP.NET Core application receives a strongly typed result similar to:

{
  "productName": "Wireless Mouse",
  "stockStatus": "Low",
  "riskLevel": "High",
  "recommendedReorderQuantity": 20,
  "explanation": "Current stock is below the configured reorder level."
}

The important difference is that our C# application receives meaningful fields rather than an unstructured paragraph.


Step 1: Create the Request Model

Create:

Models/InventoryAnalysisRequest.cs

Add:

using System.ComponentModel.DataAnnotations;

public sealed class InventoryAnalysisRequest
{
    [Required]
    [StringLength(200, MinimumLength = 1)]
    public string ProductName { get; set; } = string.Empty;

    [Range(0, int.MaxValue)]
    public int CurrentStock { get; set; }

    [Range(0, int.MaxValue)]
    public int ReorderLevel { get; set; }
}

ASP.NET Core can validate this request before we send anything to the model.

That is important.

AI does not replace normal application validation.


Step 2: Define the Stock Status

Create:

Models/StockStatus.cs

Add:

public enum StockStatus
{
    Healthy,
    Low,
    OutOfStock
}

Now our application has a known set of possible stock-status values.

Instead of accepting arbitrary text such as:

Pretty Low
Critical-ish
Maybe Fine

we want the AI response to map to one of our defined values.


Step 3: Define the Risk Level

Create:

Models/InventoryRiskLevel.cs

Add:

public enum InventoryRiskLevel
{
    Low,
    Medium,
    High
}

Again, we are defining the shape our application expects.


Step 4: Create the Structured Result

Create:

Models/InventoryAnalysisResult.cs

Add:

using System.ComponentModel.DataAnnotations;

public sealed class InventoryAnalysisResult
{
    [Required]
    public string ProductName { get; set; } = string.Empty;

    public StockStatus StockStatus { get; set; }

    public InventoryRiskLevel RiskLevel { get; set; }

    [Range(0, int.MaxValue)]
    public int RecommendedReorderQuantity { get; set; }

    [Required]
    [StringLength(500)]
    public string Explanation { get; set; } = string.Empty;
}

This type becomes the contract for the AI result.

Our application expects:

InventoryAnalysisResult
├── ProductName
├── StockStatus
├── RiskLevel
├── RecommendedReorderQuantity
└── Explanation

That is much easier to work with than arbitrary generated text.


Step 5: Create an Inventory AI Service

Our existing IAiChatService handles general chat.

Inventory analysis is a different application capability.

Instead of adding every AI feature to:

IAiChatService

create a dedicated service.

Create:

Services/IInventoryAiService.cs

Add:

public interface IInventoryAiService
{
    Task<InventoryAnalysisResult> AnalyzeAsync(
        InventoryAnalysisRequest request,
        CancellationToken cancellationToken = default);
}

This is an important design decision.

Our application is starting to contain different AI use cases:

General Chat
Conversation Memory
Streaming
Inventory Analysis

They should not all become one giant service.


Step 6: Implement the Structured AI Service

Create:

Services/InventoryAiService.cs

Add:

using Microsoft.Extensions.AI;

public sealed class InventoryAiService
    : IInventoryAiService
{
    private readonly IChatClient _chatClient;
    private readonly ILogger<InventoryAiService> _logger;

    public InventoryAiService(
        IChatClient chatClient,
        ILogger<InventoryAiService> logger)
    {
        _chatClient = chatClient;
        _logger = logger;
    }

    public async Task<InventoryAnalysisResult> AnalyzeAsync(
        InventoryAnalysisRequest request,
        CancellationToken cancellationToken = default)
    {
        string prompt =
            $"""
            Analyze the following inventory item.

            Product name: {request.ProductName}
            Current stock: {request.CurrentStock}
            Reorder level: {request.ReorderLevel}

            Determine:
            - stock status
            - inventory risk level
            - recommended reorder quantity
            - a short explanation

            Base your analysis only on the information provided.
            Do not invent sales history, supplier lead time,
            demand forecasts, or other unavailable information.
            """;

        _logger.LogInformation(
            "Analyzing inventory status for product {ProductName}.",
            request.ProductName);

        ChatResponse<InventoryAnalysisResult> response =
            await _chatClient
                .GetResponseAsync<InventoryAnalysisResult>(
                    prompt,
                    cancellationToken: cancellationToken);

        return response.Result;
    }
}

This is the key Day 6 code.

Notice:

GetResponseAsync<InventoryAnalysisResult>

The generic type tells the structured-output API what type we expect.

The response becomes:

ChatResponse<InventoryAnalysisResult>

and the typed value is available through:

response.Result

No manual:

JsonSerializer.Deserialize(...)

is required in this version.


What GetResponseAsync<T>() Changes

Previously we used:

ChatResponse response =
    await _chatClient.GetResponseAsync(
        prompt);

and consumed:

response.Text

Now we use:

ChatResponse<InventoryAnalysisResult> response =
    await _chatClient
        .GetResponseAsync<InventoryAnalysisResult>(
            prompt);

and consume:

InventoryAnalysisResult result =
    response.Result;

Conceptually:

Prompt
  ↓
IChatClient
  ↓
AI Provider
  ↓
Structured Response
  ↓
ChatResponse<InventoryAnalysisResult>
  ↓
InventoryAnalysisResult

Our application is no longer dependent on interpreting prose.


Step 7: Register the Service

In Program.cs:

builder.Services.AddScoped<
    IInventoryAiService,
    InventoryAiService>();

Our existing IChatClient registration remains unchanged.

We now have:

IChatClient
   ↑
   ├── AiChatService
   └── InventoryAiService

The same AI abstraction can support different application features.


Step 8: Create the API Controller

Create:

Controllers/InventoryAiController.cs

Add:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/inventory-ai")]
public sealed class InventoryAiController
    : ControllerBase
{
    private readonly IInventoryAiService
        _inventoryAiService;

    public InventoryAiController(
        IInventoryAiService inventoryAiService)
    {
        _inventoryAiService =
            inventoryAiService;
    }

    [HttpPost("analyze")]
    public async Task<
        ActionResult<InventoryAnalysisResult>>
        AnalyzeAsync(
            [FromBody]
            InventoryAnalysisRequest request,
            CancellationToken cancellationToken)
    {
        InventoryAnalysisResult result =
            await _inventoryAiService.AnalyzeAsync(
                request,
                cancellationToken);

        return Ok(result);
    }
}

The endpoint is:

POST /api/inventory-ai/analyze

ASP.NET Core receives normal application data.

The service sends that information to the model.

The controller returns the typed result as JSON.


Step 9: Test the Endpoint

Send:

{
  "productName": "Wireless Mouse",
  "currentStock": 4,
  "reorderLevel": 10
}

to:

POST /api/inventory-ai/analyze

A possible result is:

{
  "productName": "Wireless Mouse",
  "stockStatus": "low",
  "riskLevel": "high",
  "recommendedReorderQuantity": 20,
  "explanation": "Current stock is below the configured reorder level."
}

The exact analysis may vary.

The important point is that our application receives data that maps to:

InventoryAnalysisResult

instead of having to extract values from a paragraph.


Step 10: Use the Result in C#

Because the response is typed, normal C# code can use it:

InventoryAnalysisResult result =
    await _inventoryAiService.AnalyzeAsync(
        request,
        cancellationToken);

if (result.RiskLevel ==
    InventoryRiskLevel.High)
{
    // Application logic
}

We could also display:

result.StockStatus

or:

result.RecommendedReorderQuantity

in a dashboard.

This is where structured output becomes much more valuable than normal chat text.


But Do Not Trust the Result Automatically

This is one of the most important lessons in Day 6.

A structured response is easier to consume.

It does not automatically make the AI's reasoning correct.

Suppose the AI returns:

{
  "recommendedReorderQuantity": 100000
}

The JSON may be perfectly valid.

The value may still be inappropriate for the business.

Think of structured output as:

Correct Shape
     ≠
Correct Business Decision

Your application must still validate important values.


Step 11: Add Business Validation

Create a helper:

private static void ValidateResult(
    InventoryAnalysisRequest request,
    InventoryAnalysisResult result)
{
    if (result.RecommendedReorderQuantity < 0)
    {
        throw new InvalidOperationException(
            "AI returned an invalid reorder quantity.");
    }

    if (string.IsNullOrWhiteSpace(
        result.Explanation))
    {
        throw new InvalidOperationException(
            "AI returned an empty explanation.");
    }
}

Then:

ChatResponse<InventoryAnalysisResult> response =
    await _chatClient
        .GetResponseAsync<InventoryAnalysisResult>(
            prompt,
            cancellationToken: cancellationToken);

InventoryAnalysisResult result =
    response.Result;

ValidateResult(
    request,
    result);

return result;

The application validates the AI output before accepting it.


DataAnnotations Do Not Automatically Validate Every AI Object

We added attributes such as:

[Range(0, int.MaxValue)]

to our result model.

But there is an important distinction.

ASP.NET Core automatically validates incoming controller models when [ApiController] is used.

An object created from an AI response does not automatically pass through that same controller model-validation pipeline.

If you want DataAnnotations validation for generated results, validate them explicitly.

For example:

using System.ComponentModel.DataAnnotations;

private static void ValidateStructuredResult(
    InventoryAnalysisResult result)
{
    var context =
        new ValidationContext(result);

    Validator.ValidateObject(
        result,
        context,
        validateAllProperties: true);
}

Then:

InventoryAnalysisResult result =
    response.Result;

ValidateStructuredResult(result);

return result;

Now our attributes are enforced in application code.


A Better Final InventoryAiService

Let's combine the pieces.

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

public sealed class InventoryAiService
    : IInventoryAiService
{
    private readonly IChatClient _chatClient;
    private readonly ILogger<InventoryAiService> _logger;

    public InventoryAiService(
        IChatClient chatClient,
        ILogger<InventoryAiService> logger)
    {
        _chatClient = chatClient;
        _logger = logger;
    }

    public async Task<InventoryAnalysisResult> AnalyzeAsync(
        InventoryAnalysisRequest request,
        CancellationToken cancellationToken = default)
    {
        string prompt =
            $"""
            Analyze the following inventory item.

            Product name: {request.ProductName}
            Current stock: {request.CurrentStock}
            Reorder level: {request.ReorderLevel}

            Determine:
            - stock status
            - inventory risk level
            - recommended reorder quantity
            - a short explanation

            Base your analysis only on the supplied data.

            Do not invent:
            - sales history
            - demand forecasts
            - supplier lead times
            - purchase history
            - unavailable business information
            """;

        _logger.LogInformation(
            "Starting AI inventory analysis for {ProductName}.",
            request.ProductName);

        ChatResponse<InventoryAnalysisResult> response =
            await _chatClient
                .GetResponseAsync<InventoryAnalysisResult>(
                    prompt,
                    cancellationToken: cancellationToken);

        InventoryAnalysisResult result =
            response.Result;

        ValidateStructuredResult(result);

        ValidateBusinessRules(
            request,
            result);

        return result;
    }

    private static void ValidateStructuredResult(
        InventoryAnalysisResult result)
    {
        var validationContext =
            new ValidationContext(result);

        Validator.ValidateObject(
            result,
            validationContext,
            validateAllProperties: true);
    }

    private static void ValidateBusinessRules(
        InventoryAnalysisRequest request,
        InventoryAnalysisResult result)
    {
        if (result.RecommendedReorderQuantity < 0)
        {
            throw new InvalidOperationException(
                "AI returned an invalid reorder quantity.");
        }

        if (!string.Equals(
            result.ProductName,
            request.ProductName,
            StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
                "AI returned an unexpected product.");
        }
    }
}

Now our flow is stronger:

Request Validation
       ↓
AI Structured Output
       ↓
C# Type
       ↓
Data Validation
       ↓
Business Validation
       ↓
Application

Schema Validation and Business Validation Are Different

This distinction is critical.

Schema validation asks:

Is RecommendedReorderQuantity an integer?

Is RiskLevel a known enum?

Is Explanation a string?

Business validation asks:

Does this reorder quantity make sense?

Is the user authorized to reorder?

Does the supplier allow this quantity?

Would this exceed the purchasing budget?

Is this product active?

Structured output can help with the first category.

Your application remains responsible for the second.


What Is Happening Behind GetResponseAsync<T>()?

Conceptually, the structured-output API knows the target type:

InventoryAnalysisResult

and uses that type to guide structured generation and materialize the result.

The target structure is approximately:

InventoryAnalysisResult
├── ProductName : string
├── StockStatus : StockStatus
├── RiskLevel : InventoryRiskLevel
├── RecommendedReorderQuantity : int
└── Explanation : string

The AI provider receives a structured-output request when the underlying client and model support that capability.

The returned structured data is then exposed as:

response.Result

This saves us from writing repetitive serialization code for common strongly typed scenarios.


The Lower-Level ResponseFormat Approach

There are situations where you may want more control over the response format.

ChatOptions exposes:

ResponseFormat

For example:

var options = new ChatOptions
{
    ResponseFormat =
        ChatResponseFormat
            .ForJsonSchema<InventoryAnalysisResult>()
};

Then:

ChatResponse response =
    await _chatClient.GetResponseAsync(
        prompt,
        options,
        cancellationToken);

The response text can then contain structured JSON according to the requested format.

This approach is useful when:

  • you need direct control over ChatOptions

  • you are working with a schema dynamically

  • the target type is determined at runtime

  • you want access to the raw structured JSON

  • you are building infrastructure around schemas

For our Day 6 application, however:

GetResponseAsync<InventoryAnalysisResult>()

is cleaner.


ChatResponseFormat.Json vs ForJsonSchema<T>()

There is another important distinction.

You can request generic JSON:

var options = new ChatOptions
{
    ResponseFormat =
        ChatResponseFormat.Json
};

This says, conceptually:

Return JSON.

But it does not define our exact business structure.

Compare that with:

var options = new ChatOptions
{
    ResponseFormat =
        ChatResponseFormat
            .ForJsonSchema<InventoryAnalysisResult>()
};

Now the requested structure is tied to our schema.

Conceptually:

ChatResponseFormat.Json

"Give me JSON."

versus:

ForJsonSchema<InventoryAnalysisResult>()

"Give me JSON shaped like this contract."

When your application expects a specific structure, schema-based output is usually more useful.


Provider Support Still Matters

IChatClient gives us a common .NET abstraction.

But not every AI provider or model necessarily supports every structured-output capability in exactly the same way.

Your application may request:

ChatResponseFormat
    .ForJsonSchema<InventoryAnalysisResult>()

but the underlying implementation ultimately determines how that request is handled.

Therefore, test structured output with the actual:

Provider
+
Model
+
Package Version

used by your application.

Do not assume that successful plain-text chat automatically means every structured-output feature is supported.


Why Prompt Instructions Still Matter

If we already have a schema, do we still need a good prompt?

Yes.

The schema defines shape.

The prompt defines meaning.

Our C# type tells the model that we need:

StockStatus
RiskLevel
RecommendedReorderQuantity
Explanation

But the prompt explains what those fields should represent.

For example:

Base your analysis only on supplied data.
Do not invent supplier lead times.

reduces the chance that the model introduces unsupported business assumptions.

Think of it as:

Schema
  ↓
What structure should be returned?

Prompt
  ↓
What should the values mean?

Both matter.


Do Not Ask AI to Calculate Deterministic Rules You Already Know

Here is another important architectural lesson.

Suppose your business rule is:

if (currentStock <= reorderLevel)
{
    status = StockStatus.Low;
}

You do not need an AI model to perform that deterministic comparison.

C# can do it faster, cheaper, and predictably.

For example:

StockStatus status =
    request.CurrentStock == 0
        ? StockStatus.OutOfStock
        : request.CurrentStock <=
          request.ReorderLevel
            ? StockStatus.Low
            : StockStatus.Healthy;

AI is more useful for tasks involving interpretation, explanation, summarization, classification, or recommendations where deterministic code alone is insufficient.

A better architecture might therefore be:

C# Business Rules
       ↓
Known Facts
       ↓
AI
       ↓
Explanation / Recommendation

rather than:

AI
 ↓
Everything

This principle becomes increasingly important as AI enters real business applications.


A More Realistic Inventory Design

Suppose our application already calculates:

Stock Status = Low
Stock Deficit = 6
Average Daily Sales = 3
Supplier Lead Time = 5 days

These facts can come from normal application logic and database queries.

Then AI can analyze:

Explain the inventory risk and suggest
a purchasing strategy based on these facts.

Structured output might return:

{
  "riskLevel": "high",
  "recommendedReorderQuantity": 25,
  "explanation": "Current inventory may not cover expected demand during the supplier lead time."
}

That division of responsibility is stronger:

Database
   ↓
Deterministic C# Calculations
   ↓
Verified Business Facts
   ↓
AI Analysis
   ↓
Structured Recommendation
   ↓
Validation

Structured Output Is Still Untrusted Input

This principle should guide every AI-powered business application:

AI-generated structured data should be treated as untrusted input until your application validates it.

The response may be valid JSON.

It may match your C# type.

It may still contain an inappropriate value.

Never directly use AI output to perform sensitive actions such as:

Create Purchase Order
Delete Record
Transfer Money
Issue Refund
Change User Permission
Send Customer Notification

without appropriate application checks.

A safer architecture is:

AI Suggests
    ↓
Application Validates
    ↓
Authorization Checked
    ↓
Business Rules Checked
    ↓
Approved Code Executes

We will return to this idea when we introduce function calling and AI agents later in the series.


What About Null Values?

Structured responses should still be designed defensively.

For example:

public string Explanation { get; set; }

could create nullable-reference warnings or ambiguity.

We used:

public string Explanation { get; set; }
    = string.Empty;

and validation:

[Required]
[StringLength(500)]

But remember:

C# defaults and DataAnnotations are application-side concepts.

Always validate generated results before relying on them.


Should You Use Classes or Records?

Both can work well.

For example:

public sealed record InventoryAnalysisResult(
    string ProductName,
    StockStatus StockStatus,
    InventoryRiskLevel RiskLevel,
    int RecommendedReorderQuantity,
    string Explanation);

is concise.

A class may be more convenient when you want:

  • DataAnnotations

  • mutable properties

  • validation frameworks

  • conventional DTO patterns

For our tutorial, a class makes validation easier to demonstrate.


Should You Use Enums?

Enums are useful when a value should come from a controlled set.

Instead of:

public string RiskLevel { get; set; }

we use:

public InventoryRiskLevel RiskLevel
{
    get;
    set;
}

That gives our application a stronger contract.

But the enum values should represent categories that actually make sense in your domain.

Do not create an enum merely because structured output supports it.


Top-Level Objects Are a Safe Default

When designing schema-based structured output, a top-level object is generally the safest pattern.

For example:

public sealed class ProductRecommendations
{
    public List<ProductRecommendation> Items
    {
        get;
        set;
    } = [];
}

is preferable to assuming every provider will accept a top-level:

List<ProductRecommendation>

directly.

A wrapper object also gives you room to add future metadata:

Items
TotalCount
Summary
Warnings

without changing the fundamental response shape.


Structured Output vs Function Calling

These concepts are related but solve different problems.

Structured output asks:

What structured data should the model return?

Function calling asks:

What application capability should the model request?

For example:

Structured Output

Analyze inventory
      ↓
{
  "riskLevel": "High"
}

versus:

Function Calling

User:
Show current stock for Product A

AI:
Call GetProductStock("Product A")

Structured output returns data.

Function calling connects the model to application operations.

We will implement function calling later in this series.


Structured Output vs RAG

These also solve different problems.

RAG answers:

What external knowledge should the model receive?

Structured output answers:

In what structure should the model return the result?

They can work together:

User Request
     ↓
Retrieve Relevant Data
     ↓
AI Model
     ↓
Structured Result
     ↓
C# Application

For example:

PostgreSQL Inventory Data
        ↓
Relevant Products
        ↓
AI Analysis
        ↓
InventoryAnalysisResult

That becomes very powerful for business applications.


Structured Output and Conversation Memory

Day 4 introduced conversation memory.

Do all structured-output calls need conversation history?

No.

Our inventory analysis request is a self-contained operation:

Product
Current Stock
Reorder Level

It does not need previous chat messages.

Keeping it stateless makes the feature easier to:

  • test

  • retry

  • cache

  • monitor

  • reason about

Use conversation history only when previous conversation context is genuinely required.

Do not automatically attach an entire chat history to every AI feature.


Structured Output and Streaming

Day 5 introduced streaming.

Should we stream structured JSON and deserialize it while chunks are still arriving?

Usually, not for a simple business operation like this.

Partial JSON might look like:

{
  "productName": "Wireless

That is not yet a complete object.

For structured application data, it is often simpler to wait for the complete structured response:

Request
   ↓
Complete Structured Result
   ↓
Validate
   ↓
Use

Streaming remains excellent for human-readable chat responses.

Structured output is excellent when code needs a predictable result.

Use each technique where it fits.


Handling Structured Output Failures

Structured AI calls can fail for several reasons:

  • provider unavailable

  • model unavailable

  • request timeout

  • unsupported response format

  • schema incompatibility

  • invalid generated result

  • cancellation

  • rate limiting

Do not expose raw provider exceptions directly to clients.

For example:

try
{
    return await AnalyzeInternalAsync(
        request,
        cancellationToken);
}
catch (ValidationException ex)
{
    _logger.LogWarning(
        ex,
        "AI returned invalid structured inventory data.");

    throw;
}
catch (OperationCanceledException)
{
    throw;
}
catch (Exception ex)
{
    _logger.LogError(
        ex,
        "Inventory AI analysis failed.");

    throw;
}

Your API can then map failures into appropriate application-level responses.


Retry Carefully

If structured generation fails, retrying may sometimes help.

But do not blindly retry every exception.

For example:

Temporary provider error
        ↓
Retry may help

while:

Invalid API key
        ↓
Retry will not help

or:

Unsupported model feature
        ↓
Retry will not help

Retries should be based on known transient conditions rather than:

catch everything
→ try again repeatedly

This matters for both reliability and AI cost.


Do Not Log Sensitive Structured Data Blindly

It may be tempting to log:

_logger.LogInformation(
    "AI result: {@Result}",
    result);

But structured output can contain:

  • customer information

  • pricing

  • financial data

  • internal business data

  • personal information

Prefer operational metadata unless the application has a clear policy allowing content logging.

For example:

_logger.LogInformation(
    "Inventory AI analysis completed for {ProductName}.",
    request.ProductName);

Logging policies should be intentional.


Suggested Project Structure After Day 6

Our application can now look like:

CSharpAiApi
│
├── Controllers
│   ├── AiController.cs
│   └── InventoryAiController.cs
│
├── Models
│   ├── AiChatResult.cs
│   ├── AiConversation.cs
│   ├── AiStreamContext.cs
│   ├── ChatRequest.cs
│   ├── ChatResponse.cs
│   ├── InventoryAnalysisRequest.cs
│   ├── InventoryAnalysisResult.cs
│   ├── InventoryRiskLevel.cs
│   └── StockStatus.cs
│
├── Services
│   ├── IAiChatService.cs
│   ├── AiChatService.cs
│   ├── IConversationStore.cs
│   ├── InMemoryConversationStore.cs
│   ├── IInventoryAiService.cs
│   └── InventoryAiService.cs
│
├── wwwroot
│   ├── index.html
│   └── chat.js
│
├── Program.cs
└── appsettings.json

The application now has two clear AI use cases:

AiChatService
     ↓
Human Conversation

InventoryAiService
     ↓
Structured Business Analysis

That separation will become increasingly valuable as the project grows.


Common Error: GetResponseAsync<T>() Is Not Found

Make sure:

using Microsoft.Extensions.AI;

is present.

Also verify the installed Microsoft.Extensions.AI package version.

The .NET AI ecosystem is evolving, and structured-output APIs available in current versions may not exist in an older package.


Common Error: The Provider Rejects the Schema

Not every model or provider supports every response format.

Check:

  • model capability

  • provider documentation

  • package version

  • generated schema shape

If the provider does not support schema-constrained structured output, requesting it may fail or behave differently.


Common Error: Valid JSON but Wrong Business Value

This is perhaps the most important failure mode.

The model returns:

{
  "recommendedReorderQuantity": 50000
}

The response may be structurally valid.

That does not mean your application should create a purchase order for 50,000 units.

Always distinguish:

Schema Valid

from:

Business Valid

Common Error: Using AI for Simple Calculations

Do not ask the model:

Is 4 less than 10?

when C# can determine:

4 < 10

directly.

Use deterministic code for deterministic rules.

Use AI where interpretation adds value.

This keeps applications:

  • faster

  • cheaper

  • easier to test

  • more predictable


Common Error: Assuming Structured Means Safe

Structured output improves predictability.

It does not make generated data trusted.

The safe flow remains:

AI Generates
    ↓
Application Validates
    ↓
Business Rules
    ↓
Authorization
    ↓
Action

Never remove those boundaries merely because the AI returned a strongly typed object.


Production Checklist for Structured AI Output

Before using structured output in a production application, review:

  • input validation

  • output validation

  • schema design

  • provider support

  • model support

  • nullable fields

  • enum design

  • business-rule validation

  • authorization

  • retry policy

  • timeout handling

  • cancellation

  • logging policy

  • sensitive-data handling

  • AI cost

  • fallback behavior

Structured output makes AI easier to integrate with code, but your application still owns correctness.


What We Built Today

Before Day 6, our main AI flow looked like:

User
 ↓
AI
 ↓
Text
 ↓
Human

Today we introduced:

Application Data
       ↓
AI
       ↓
Structured Result
       ↓
C# Object
       ↓
Validation
       ↓
Application Logic

We added:

  • InventoryAnalysisRequest

  • InventoryAnalysisResult

  • StockStatus

  • InventoryRiskLevel

  • IInventoryAiService

  • InventoryAiService

  • GetResponseAsync<T>()

  • ChatResponse<T>

  • typed Result

  • DataAnnotations validation

  • business-rule validation

  • schema-based response concepts

This is an important transition.

Our project is no longer only building an AI chatbot.

We are beginning to build AI-powered application features.


Day 6 Checklist

Before moving to Day 7, make sure you understand:

  • what structured AI output means

  • why parsing free-form AI text is fragile

  • how to define a C# output model

  • how GetResponseAsync<T>() works

  • what ChatResponse<T> represents

  • how to access response.Result

  • why enums can strengthen output contracts

  • why generated results still require validation

  • the difference between schema validation and business validation

  • what ChatResponseFormat.Json means

  • what ChatResponseFormat.ForJsonSchema<T>() provides

  • why provider and model support matter

  • why deterministic rules should remain in C#

  • how structured output differs from RAG

  • how structured output differs from function calling

If those concepts are clear, we are ready to give our AI application access to actual C# capabilities.


What's Next: Day 7

Today, the model analyzed data we supplied.

But what if the model needs information that is not already in the prompt?

Suppose the user asks:

How many Wireless Mouse units do we have in stock?

The model should not guess.

Instead, we want it to request a real C# function:

GetProductStock("Wireless Mouse")

Our application executes that function against trusted business data and returns the result to the model.

The architecture becomes:

User
  ↓
AI Model
  ↓
Function Request
  ↓
C# Application
  ↓
Database / Service
  ↓
Function Result
  ↓
AI Model
  ↓
Final Response

In Day 7: Function Calling in C# with IChatClient and ASP.NET Core, we will explore:

  • AIFunction

  • AIFunctionFactory

  • exposing C# methods as AI tools

  • ChatOptions.Tools

  • function arguments

  • automatic function invocation

  • dependency injection

  • safe tool design

  • authorization

  • preventing AI from directly controlling business operations

That is where our project begins moving from an AI that can only respond to an AI that can safely use application capabilities.


Final Thoughts

Structured output changes the relationship between AI and application code.

Before Day 6, we mostly asked:

What should the AI say?

Now we can ask:

What data should the AI return to our application?

That creates new possibilities:

Classification
Analysis
Extraction
Recommendations
Document Processing
Business Insights
Workflow Decisions

But it also creates an important responsibility.

A strongly typed AI response is still AI-generated data.

The safest mental model is:

AI Output
   ↓
Untrusted Structured Input
   ↓
Validation
   ↓
Business Rules
   ↓
Application

Use C# for rules you already know.

Use AI for interpretation where AI adds value.

And never confuse a valid schema with a valid business decision.

Day 1 connected C# to an AI model.

Day 2 introduced IChatClient.

Day 3 built a reusable ASP.NET Core architecture.

Day 4 added conversation memory.

Day 5 added response streaming.

Day 6 turned AI responses into strongly typed application data.

Next, we will let the model safely request real C# functions through function calling.

Comments 0