← All Quick Lessons
ASP.NET Core and AI12 min read · 20 August 2026

Structured Outputs and Tool Calling in ASP.NET Core: The Model Requests, Your Code Decides

Understand how typed structured outputs and tool calling connect an AI model to an ASP.NET Core application without surrendering authorization, validation or business control.

A model is naturally good at producing language. An ASP.NET Core application needs something stricter: predictable data it can validate, and controlled ways to retrieve information or perform actions.

The BuildEstate Pro scenario

BuildEstate Pro is the public property-development platform used throughout this website as a practical enterprise case study. Its ASP.NET Core and Angular architecture follows the development lifecycle from opportunity and due diligence through planning, construction, sales and operations, with permissions and workflow rules around business activity.

The examples below imagine a carefully controlled AI capability being added to that platform. They explain a realistic extension; they do not claim that the repository already contains this AI feature. A user might ask the application to analyse an authorised planning report, retrieve the current status of a project or prepare a risk summary for Angular to display.

Why free-form text is not enough

If a model says that drainage approval may delay a development, a human can understand the paragraph. Angular cannot reliably turn changing prose into separate risk cards containing category, severity, evidence and next action. A parser based on headings or wording will eventually break.

csharp
public enum RiskSeverity { Low, Medium, High }

public sealed record PlanningRisk(
    string Category,
    RiskSeverity Severity,
    string Explanation,
    string Evidence,
    string RecommendedAction);

public sealed record PlanningRiskAssessment(
    IReadOnlyList<PlanningRisk> Risks,
    IReadOnlyList<string> MissingEvidence);

This is the contract the C# service and Angular interface actually need. The result can be validated, logged safely, stored where appropriate, filtered by severity and presented consistently.

JSON mode and Structured Outputs are different

Asking for JSON, or using a JSON mode, aims to produce valid JSON. Valid JSON can still have the wrong fields, missing properties or unexpected values. Structured Outputs go further by constraining the response to a supported JSON Schema. That schema describes required properties, types, arrays, permitted values and whether extra properties are allowed.

json
{ "message": "No risks found" }

That object is valid JSON but wrong for an application expecting a risks array and missingEvidence array. Correct syntax is not the same as the correct contract.

Request a typed result in .NET

Microsoft.Extensions.AI provides the provider-neutral IChatClient abstraction and typed structured-output helpers. Where the selected model and provider support schema-constrained output, a service can request its C# result type directly.

csharp
public sealed class PlanningRiskService(IChatClient chatClient)
{
    public async Task<PlanningRiskAssessment> AnalyseAsync(
        string authorisedReport, CancellationToken cancellationToken)
    {
        var prompt = $$"""
            Analyse only the authorised planning report below.
            Every risk must quote supporting evidence.
            Put absent information in MissingEvidence; do not invent it.

            AUTHORISED_REPORT:
            {{authorisedReport}}
            """;

        var response = await chatClient
            .GetResponseAsync<PlanningRiskAssessment>(
                prompt, cancellationToken: cancellationToken);

        return response.Result;
    }
}

Exact capabilities vary by model and provider, so check the current SDK documentation and test the deployed model. Even when the API enforces the shape, treat the returned object like any other external input and apply application and domain validation.

Ground the answer in authorised evidence, require citations, check business invariants, evaluate representative documents and retain human review for decisions with material legal, financial or safety consequences. A perfectly shaped unsupported answer is still unsupported.

Tool calling solves a different problem

Suppose a user asks for the current status of a BuildEstate Pro project. The model does not know today's SQL Server data. Your application can describe a narrow tool that retrieves an authorised status. The model may request that tool, but it does not open the database or execute the C# method itself.

1. ASP.NET Core sends question + permitted tool descriptions
2. Model returns a tool name + structured arguments
3. ASP.NET Core validates, authorises and executes
4. ASP.NET Core returns the safe tool result
5. Model uses that result to prepare the final answer

Bind trusted state in code

If the authenticated user is already viewing project 152, the route, claims and authorization context know that project. Do not ask the model to invent a tenant ID, user ID or unrestricted project ID. Bind trusted identifiers inside the tool instance and let the model provide only values that genuinely require language interpretation.

csharp
public sealed class ProjectStatusTools(
    int authorisedProjectId,
    IProjectQueries projects)
{
    public Task<ProjectStatusDto?> GetCurrentStatusAsync(
        CancellationToken cancellationToken)
        => projects.GetStatusAsync(authorisedProjectId, cancellationToken);
}

The tool has no projectId argument for the model to alter. Its instance is scoped to a project the endpoint has already authorized. Microsoft.Extensions.AI can describe a .NET delegate as an AIFunction, including a JSON Schema for arguments inferred from its parameters.

csharp
var projectTools = new ProjectStatusTools(
    authorisedProject.Id, projectQueries);

AIFunction getStatus = AIFunctionFactory.Create(
    projectTools.GetCurrentStatusAsync,
    name: "get_current_project_status",
    description: "Gets the latest status of the authorised project.");

var options = new ChatOptions
{
    Tools = [getStatus]
};

A FunctionInvokingChatClient can manage the repeated model-call, function-call and result loop. Automatic invocation is convenient for low-risk read tools, but it does not remove the need to authorize the request, validate arguments, limit iterations, handle cancellation and restrict which tools are offered. Sensitive operations may require a deliberately manual workflow.

Tool descriptions are API design

A tool called ProcessData gives the model little help. Prefer a narrow name such as get_current_project_status and describe exactly what it returns and when it applies. Define only necessary arguments, describe their formats, use enums where the domain has a closed set and reject unexpected properties where strict schema support is available.

Code-review question

Could another developer understand when this tool should be used, what it can affect and which values it accepts without reading its implementation?

Schema-valid arguments still need validation

A schema may prove that projectId is an integer. It cannot prove that the project exists, belongs to the current tenant, is in the correct workflow state or may be changed by this user. This is the same distinction as an ASP.NET Core request DTO: successful deserialization is followed by authorization, validation and domain rules.

  • Allowlist the exact tools available for this request.
  • Validate every model-supplied argument and reject unknown operations.
  • Enforce authentication, tenant isolation, permissions and workflow rules in code.
  • Use timeouts, cancellation, rate limits and maximum tool-call iterations.
  • Return safe error codes rather than stack traces, SQL or secrets.
  • Record an audit trail suitable for the operation without logging sensitive prompt data.

Read tools and action tools are not equally risky

Reading a project status is different from approving a payment, changing a planning status, sending an email or deleting a document. For consequential actions, add explicit confirmation or human approval, role-based authorization, transaction boundaries, audit records, value limits and a preview of the proposed change. The model's confidence is never permission.

Design actions for retries

A timeout can leave the caller unsure whether an action completed, and an agent may request the same call again. Use an idempotency key for action tools so the same logical request cannot accidentally send two emails, create two approvals or issue two payments. This is ordinary distributed-systems engineering, not an AI-specific exception.

csharp
public sealed record ProposeStatusChange(
    int TargetStatusId,
    string Reason,
    Guid IdempotencyKey);

// The command handler checks authorization and the current workflow state,
// then returns the earlier result if this key was already processed.

One request can use both concepts

User asks for a project risk summary
              ↓
Tool calls retrieve authorised status and planning evidence
              ↓
ASP.NET Core validates and returns safe tool results
              ↓
Structured Output produces PlanningRiskAssessment
              ↓
Angular renders predictable risk cards

Tool calling answers which approved external capability the model needs. Structured output answers what shape the final response must take. They often work together, but neither grants authority to bypass application rules.

Expect zero, one or several calls—and failures

A model may need no tool, one tool, several independent calls or another call after seeing the first result. Do not assume exactly one. Preserve call identifiers, decide whether parallel execution is safe and cap the loop. Handle refusals, token limits, unavailable dependencies, invalid arguments, authorization failures, timeouts, empty results and repeated requests as normal production states.

The implementation checklist

  • Use Structured Outputs when application code must consume a predictable final result.
  • Use tools only when the model needs live information or an external capability.
  • Keep trusted identifiers and identity outside model control.
  • Prefer narrow, descriptive tools over a large general-purpose operation.
  • Validate structure, meaning, authorization and domain rules separately.
  • Require confirmation or approval for consequential actions.
  • Make retried actions idempotent and auditable.
  • Evaluate factual grounding as well as schema compliance.
  • Test failure, injection and cross-tenant scenarios before release.

The lesson to remember

In a BuildEstate Pro-style system, ASP.NET Core still owns identity, authorization, validation, workflows, transactions and auditability. The model helps interpret language and select from permitted capabilities; your application decides what is allowed and what actually happens.

ASP.NET Core structured output.NET tool callingfunction calling C#Microsoft.Extensions.AIJSON SchemaAzure OpenAI toolsAI application securityBuildEstate ProAngular AI integration

Want to go deeper?

Continue exploring the practical guides, projects and Quick Lessons across the website.