Structured AI Output in C# with IChatClient and ASP.NET Core | Day 6
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
ExplanationWe 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
Resultuse 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:
stringToday we want:
AI Response
↓
C# Object
↓
Validation
↓
Application LogicThat 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.Explanationinstead 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 immediatelyIt may instead return:
Stock Status: Low
Risk: High
Recommended Quantity: 20 unitsor:
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# TypeOur 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.csAdd:
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.csAdd:
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 Finewe want the AI response to map to one of our defined values.
Step 3: Define the Risk Level
Create:
Models/InventoryRiskLevel.csAdd:
public enum InventoryRiskLevel
{
Low,
Medium,
High
}Again, we are defining the shape our application expects.
Step 4: Create the Structured Result
Create:
Models/InventoryAnalysisResult.csAdd:
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
└── ExplanationThat 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:
IAiChatServicecreate a dedicated service.
Create:
Services/IInventoryAiService.csAdd:
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 AnalysisThey should not all become one giant service.
Step 6: Implement the Structured AI Service
Create:
Services/InventoryAiService.csAdd:
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.ResultNo manual:
JsonSerializer.Deserialize(...)is required in this version.
What GetResponseAsync<T>() Changes
Previously we used:
ChatResponse response =
await _chatClient.GetResponseAsync(
prompt);and consumed:
response.TextNow 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>
↓
InventoryAnalysisResultOur 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
└── InventoryAiServiceThe same AI abstraction can support different application features.
Step 8: Create the API Controller
Create:
Controllers/InventoryAiController.csAdd:
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/analyzeASP.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/analyzeA 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:
InventoryAnalysisResultinstead 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.StockStatusor:
result.RecommendedReorderQuantityin 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 DecisionYour 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
↓
ApplicationSchema 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:
InventoryAnalysisResultand 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 : stringThe 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.ResultThis 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:
ResponseFormatFor 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
ChatOptionsyou 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 Versionused 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
ExplanationBut 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 / Recommendationrather than:
AI
↓
EverythingThis 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 daysThese 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
↓
ValidationStructured 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 Notificationwithout appropriate application checks.
A safer architecture is:
AI Suggests
↓
Application Validates
↓
Authorization Checked
↓
Business Rules Checked
↓
Approved Code ExecutesWe 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
Warningswithout 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# ApplicationFor example:
PostgreSQL Inventory Data
↓
Relevant Products
↓
AI Analysis
↓
InventoryAnalysisResultThat 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 LevelIt 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": "WirelessThat 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
↓
UseStreaming 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 helpwhile:
Invalid API key
↓
Retry will not helpor:
Unsupported model feature
↓
Retry will not helpRetries should be based on known transient conditions rather than:
catch everything
→ try again repeatedlyThis 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.jsonThe application now has two clear AI use cases:
AiChatService
↓
Human Conversation
InventoryAiService
↓
Structured Business AnalysisThat 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 Validfrom:
Business ValidCommon Error: Using AI for Simple Calculations
Do not ask the model:
Is 4 less than 10?when C# can determine:
4 < 10directly.
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
↓
ActionNever 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
↓
HumanToday we introduced:
Application Data
↓
AI
↓
Structured Result
↓
C# Object
↓
Validation
↓
Application LogicWe added:
InventoryAnalysisRequestInventoryAnalysisResultStockStatusInventoryRiskLevelIInventoryAiServiceInventoryAiServiceGetResponseAsync<T>()ChatResponse<T>typed
ResultDataAnnotations 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>()workswhat
ChatResponse<T>representshow to access
response.Resultwhy enums can strengthen output contracts
why generated results still require validation
the difference between schema validation and business validation
what
ChatResponseFormat.Jsonmeanswhat
ChatResponseFormat.ForJsonSchema<T>()provideswhy 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 ResponseIn Day 7: Function Calling in C# with IChatClient and ASP.NET Core, we will explore:
AIFunctionAIFunctionFactoryexposing C# methods as AI tools
ChatOptions.Toolsfunction 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 DecisionsBut 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
↓
ApplicationUse 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