I'm always excited to take on new projects and collaborate with innovative minds.
Most AI applications can answer questions, but useful AI applications perform actions. Learn how to build Function Calling in ASP.NET Core using OpenAI GPT-4o, tool registries, validation, authorization, structured logging, and business service integration to safely connect AI with real-world operations.
Most AI applications can answer questions.
Useful AI applications perform actions.
Examples include:
This is where many developers misunderstand the role of an LLM.
The model should not execute business logic.
The model should decide when business logic should be executed.
Your existing services, APIs, validation rules, authorization checks, and business workflows should remain the source of truth.
Function Calling bridges these two worlds.
In this article, we'll build a production-ready Function Calling architecture in ASP.NET Core that allows GPT-4o to safely invoke business capabilities without giving the model direct access to databases, services, or internal systems.
The architecture looks like this:
User
│
"Create an invoice"
│
GPT-4o
│
Decides Which Tool To Call
│
InvoiceService.Create()
│
Business Logic Executes
│
Result Returned to LLM
│
Final Natural Response
The key idea is simple.
The model decides.
The backend validates.
The backend executes.
The model responds.
This separation keeps AI useful without making it dangerous.
Without Function Calling, an AI model can only generate text.
That often leads to hallucinations.
Example:
User:
What is my current account balance?
AI:
Your balance is $12,500.
The model has no way of knowing whether that answer is true.
It guessed.
Now consider Function Calling.
User:
What is my current account balance?
AI
↓
Calls Customer API
↓
Receives Real Data
↓
Responds
Now the answer comes from an actual system.
This dramatically improves reliability.
Function Calling enables AI to interact with existing systems.
Examples include:
The AI becomes an orchestration layer instead of a guessing machine.
One of the biggest mistakes developers make is creating tools that do too much.
Avoid this:
DoEverythingTool
A good tool should:
Think of tools as APIs.
Small and focused beats large and complicated.
Every tool should implement a common contract.
public interface IAITool
{
string Name { get; }
string Description { get; }
Task<string> ExecuteAsync(string arguments);
}
This abstraction provides several benefits.
First, the AI system doesn't care how a tool works internally.
Second, new capabilities can be added without changing existing code.
Third, tools become easy to test and maintain.
A weather tool, invoice tool, or customer lookup tool all behave the same way from the perspective of the orchestration layer.
Let's look at a few practical examples.
Responsible for:
CreateInvoice()
Example request:
{
"customerId": 123,
"amount": 500
}
The tool delegates to an InvoiceService.
It does not contain business rules itself.
Responsible for:
GetCustomer()
Example response:
{
"id":123,
"name":"John Smith",
"email":"john@example.com"
}
Responsible for:
GetWeather()
Useful for demonstrating external API integration.
Responsible for:
SearchProducts()
Returns structured product information.
Again, one responsibility per tool.
Hardcoding tool execution quickly becomes messy.
Instead, introduce a Tool Registry.
AI
↓
Tool Registry
↓
Invoice
Weather
Customer
Products
The registry discovers and resolves tools dynamically.
Benefits include:
Adding a new tool should require registration, not modification.
ASP.NET Core already provides a powerful mechanism for managing dependencies.
Register every tool using Dependency Injection.
builder.Services.AddScoped<IAITool, InvoiceTool>();
builder.Services.AddScoped<IAITool, CustomerTool>();
builder.Services.AddScoped<IAITool, WeatherTool>();
builder.Services.AddScoped<IAITool, ProductSearchTool>();
Now the registry can automatically discover all available tools.
This follows the Open/Closed Principle.
Open for extension.
Closed for modification.
This orchestration layer is the most important part of the system.
The flow looks like this:
User Question
↓
LLM
↓
Function Request
↓
Tool Validation
↓
Tool Execution
↓
Result
↓
LLM Final Response
Let's break it down.
Example:
Create an invoice for customer 123
for $500.
The model determines:
Tool:
CreateInvoice
and generates arguments.
{
"customerId":123,
"amount":500
}
Before execution:
The model's suggestion is not automatically trusted.
The selected tool invokes the business service.
InvoiceTool
↓
InvoiceService
↓
Database
Example:
{
"invoiceId":4567,
"status":"Created"
}
The model converts the structured result into natural language.
Example:
Invoice #4567 was successfully
created for customer 123
with an amount of $500.
This is where Function Calling feels magical.
But the magic comes from orchestration, not AI.
One of the most dangerous assumptions is:
The model already validated the request.
No.
Always validate.
Reject unknown tools.
DeleteEntireDatabase()
should never execute.
Validate:
Example:
Amount > 0
Example:
Customer Exists
before creating an invoice.
Example:
Guest User
↓
CreateInvoice
↓
Denied
AI suggestions should never bypass business security.
Function Calling introduces a powerful capability.
That means security becomes even more important.
Only registered tools should be executable.
The AI must never invoke arbitrary methods.
Every tool should enforce authorization policies.
Example:
Admin
↓
CreateInvoice
↓
Allowed
Guest
↓
CreateInvoice
↓
Denied
Treat AI-generated arguments exactly like user input.
Validate everything.
Track:
This is critical for compliance and troubleshooting.
Some tools may trigger expensive operations.
Protect them appropriately.
Tools fail.
Systems fail.
Networks fail.
Design for failure.
Examples:
{
"success":false,
"error":"Tool not found"
}
{
"success":false,
"error":"Invalid amount"
}
{
"success":false,
"error":"Unable to create invoice"
}
Weather APIs and third-party services can fail.
Handle failures gracefully.
The AI should receive structured error information so it can explain the problem appropriately.
Function Calling adds a new execution layer.
Without observability, debugging becomes difficult.
Track:
Tool Selected
Execution Time
Success
Failure
Correlation ID
User ID
Example log:
Tool=CreateInvoice
Duration=132ms
Status=Success
CorrelationId=abc123
Avoid logging:
Logs should be useful, not dangerous.
A common anti-pattern looks like this:
Tool
↓
Business Logic
↓
Database
Instead:
Tool
↓
Business Service
↓
Database
Example:
InvoiceTool
↓
InvoiceService
↓
Repository
Tools orchestrate.
Services execute business logic.
This separation keeps the system maintainable.
These mistakes appear frequently in production systems.
Extremely risky.
Creates a massive attack surface.
AI should never bypass security controls.
Large tool catalogs increase token usage and confusion.
The model is not a security boundary.
Creates tightly coupled systems.
Creates compliance and privacy risks.
The reference implementation includes:
The goal is not simply to call functions.
The goal is to create a safe, maintainable orchestration layer between AI and business systems.
Include the following visuals.
Show the complete Function Calling workflow.
Visualize:
AI
↓
Tool
↓
Service
↓
Database
Display:
POST /api/chat
Show generated tool arguments.
Display:
Tool
Duration
Status
CorrelationId
These visuals help readers understand how orchestration works behind the scenes.
Function Calling transforms AI from a system that only generates text into a system that can safely interact with business operations.
The key is maintaining clear boundaries.
The model decides which capability it needs.
The backend validates the request.
Business services execute the operation.
The model then communicates the result in natural language.
By introducing tool abstractions, validation, authorization, observability, and structured execution, you can integrate AI into existing systems without compromising security or maintainability.
Function Calling gives AI the ability to interact with your existing systems, but every request still starts from scratch.
In the next article, we'll explore how to build persistent AI memory so applications can maintain context across conversations without sending the entire chat history every time.
Your email address will not be published. Required fields are marked *