
Learn how to fine-tune AI models and call them from C# with OpenAI and Azure OpenAI. Step-by-step code examples inside — start building today.
Why Fine-Tuning AI Models Matters for C# Developers
Fine-tuning AI models is one of the most searched-for skills in modern software development — and for good reason. While base models like GPT-4o and Claude are impressively general, they don't know your company's tone of voice, your domain-specific terminology, or your internal classification rules. Fine-tuning solves this: you take a pre-trained model and continue training it on your own examples, producing a custom model that responds exactly the way your business needs. In this guide, you'll learn how fine-tuning works, when to use it (and when not to), how to prepare training data, how to run a fine-tuning job, and — most importantly for us — how to call your fine-tuned model from a C# application using .NET 8/9.
Here's the WHY before the HOW: a fine-tuned smaller model (like GPT-4o-mini) can often match or beat a larger base model on a narrow task, at a fraction of the cost and latency. If you're classifying support tickets, extracting structured data from invoices, or generating responses in a strict format, fine-tuning frequently pays for itself within weeks.
Fine-Tuning vs. RAG vs. Prompt Engineering: Choose Wisely
Before you fine-tune anything, understand the decision tree. This is the single biggest mistake teams make — fine-tuning is not always the answer.
- Prompt engineering — Start here. If a well-crafted system prompt with a few examples (few-shot prompting) solves your problem, you're done. Zero training cost.
- RAG (Retrieval-Augmented Generation) — Use when the model needs access to knowledge: documents, product catalogs, policies. Fine-tuning is bad at injecting new facts; RAG is built for it.
- Fine-tuning — Use when you need to change the model's behavior: consistent output format, specific tone, domain jargon, classification accuracy, or reducing prompt length (baking instructions into the model itself).
A common pattern in production systems is combining them: a fine-tuned model for behavior, plus RAG for facts.
Step 1: Prepare Your Training Data (JSONL Format)
Fine-tuning AI models starts with data quality — this determines 90% of your results. OpenAI and Azure OpenAI both expect training data in JSONL format, where each line is a complete chat conversation:
{"messages": [{"role": "system", "content": "You classify support tickets into: Billing, Technical, Account, Other."}, {"role": "user", "content": "I was charged twice this month"}, {"role": "assistant", "content": "Billing"}]}
{"messages": [{"role": "system", "content": "You classify support tickets into: Billing, Technical, Account, Other."}, {"role": "user", "content": "The app crashes when I upload a file"}, {"role": "assistant", "content": "Technical"}]}
Practical guidelines that matter:
- 50–100 high-quality examples is a solid starting point. OpenAI's minimum is 10, but real gains typically appear from 50+.
- Consistency beats volume. If your examples contradict each other, the model learns to be inconsistent.
- Include the same system prompt you'll use in production. Mismatched prompts between training and inference degrade results.
- Hold back 10–20% as a validation set so you can measure whether the model actually improved.
Here's a C# helper to generate JSONL from your existing labeled data — a task you'll do repeatedly, so automate it:
using System.Text.Json;
public record ChatMessage(string Role, string Content);
public record TrainingExample(List<ChatMessage> Messages);
public static class JsonlBuilder
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static async Task WriteAsync(string path, IEnumerable<(string Input, string Label)> data)
{
const string systemPrompt =
"You classify support tickets into: Billing, Technical, Account, Other.";
await using var writer = new StreamWriter(path);
foreach (var (input, label) in data)
{
var example = new TrainingExample(
[
new ChatMessage("system", systemPrompt),
new ChatMessage("user", input),
new ChatMessage("assistant", label)
]);
await writer.WriteLineAsync(JsonSerializer.Serialize(example, Options));
}
}
}
Step 2: Upload Data and Start the Fine-Tuning Job From C#
You can run fine-tuning jobs from the OpenAI dashboard, but doing it programmatically from C# lets you build repeatable MLOps pipelines. Install the official OpenAI .NET SDK:
dotnet add package OpenAI
Then upload your training file and create the job:
using OpenAI;
using OpenAI.Files;
using OpenAI.FineTuning;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
?? throw new InvalidOperationException("Set OPENAI_API_KEY first.");
var client = new OpenAIClient(apiKey);
// 1. Upload the training file
var fileClient = client.GetOpenAIFileClient();
var uploadedFile = await fileClient.UploadFileAsync(
"training-data.jsonl",
FileUploadPurpose.FineTune);
Console.WriteLine($"Uploaded file: {uploadedFile.Value.Id}");
// 2. Create the fine-tuning job on a base model
var ftClient = client.GetFineTuningClient();
var job = await ftClient.FineTuneAsync(
baseModel: "gpt-4o-mini-2024-07-18",
trainingFileId: uploadedFile.Value.Id,
waitUntilCompleted: false);
Console.WriteLine($"Job started: {job.Value.JobId}");
Fine-tuning jobs take anywhere from a few minutes to a few hours depending on dataset size. Poll for completion rather than blocking:
FineTuningJob status;
do
{
await Task.Delay(TimeSpan.FromSeconds(30));
status = (await ftClient.GetJobAsync(job.Value.JobId)).Value;
Console.WriteLine($"Status: {status.Status}");
}
while (status.Status is "running" or "queued" or "validating_files");
if (status.Status == "succeeded")
Console.WriteLine($"Fine-tuned model ready: {status.FineTunedModel}");
else
Console.WriteLine($"Job ended with status: {status.Status}");
When the job succeeds, you get a model ID like ft:gpt-4o-mini-2024-07-18:your-org:ticket-classifier:AbCd1234. That's your custom model.
Step 3: Call Your Fine-Tuned Model From a C# Application
Calling a fine-tuned model is identical to calling a base model — you just pass your custom model ID. Here's a production-ready service class using dependency injection, the pattern you'd use in ASP.NET Core:
using OpenAI.Chat;
public interface ITicketClassifier
{
Task<string> ClassifyAsync(string ticketText, CancellationToken ct = default);
}
public class TicketClassifier : ITicketClassifier
{
private readonly ChatClient _chat;
// Register in Program.cs:
// builder.Services.AddSingleton<ITicketClassifier>(_ =>
// new TicketClassifier(config["OpenAI:ApiKey"]!, config["OpenAI:FineTunedModel"]!));
public TicketClassifier(string apiKey, string fineTunedModelId)
{
_chat = new ChatClient(fineTunedModelId, apiKey);
}
public async Task<string> ClassifyAsync(string ticketText, CancellationToken ct = default)
{
var messages = new ChatMessage[]
{
new SystemChatMessage(
"You classify support tickets into: Billing, Technical, Account, Other."),
new UserChatMessage(ticketText)
};
var options = new ChatCompletionOptions
{
Temperature = 0f, // deterministic output for classification
MaxOutputTokenCount = 10 // labels are short — cap cost and latency
};
ChatCompletion completion = await _chat.CompleteChatAsync(messages, options, ct);
return completion.Content[0].Text.Trim();
}
}
And using it in a minimal API endpoint:
app.MapPost("/api/classify", async (TicketRequest req, ITicketClassifier classifier) =>
{
var category = await classifier.ClassifyAsync(req.Text);
return Results.Ok(new { category });
});
public record TicketRequest(string Text);
Notice Temperature = 0: for classification and extraction tasks you want deterministic behavior, not creativity. This is a small setting with a big impact on consistency.
Azure OpenAI Fine-Tuning for Enterprise C# Apps
If you're in an enterprise environment (especially in the USA, UK, or Australia where data residency matters), Azure OpenAI offers the same fine-tuning capability with your data staying inside your Azure tenant. The C# code barely changes:
using Azure.AI.OpenAI;
using Azure.Identity;
using OpenAI.Chat;
// Uses Entra ID (managed identity) — no API keys in config, a security best practice
var azureClient = new AzureOpenAIClient(
new Uri("https://your-resource.openai.azure.com/"),
new DefaultAzureCredential());
// The deployment name of your fine-tuned model in Azure
ChatClient chat = azureClient.GetChatClient("ticket-classifier-deployment");
ChatCompletion result = await chat.CompleteChatAsync(
[
new SystemChatMessage("You classify support tickets into: Billing, Technical, Account, Other."),
new UserChatMessage("My subscription renewed but I wanted to cancel")
]);
Console.WriteLine(result.Content[0].Text); // "Billing"
Key Azure difference: after fine-tuning completes, you must deploy the resulting model to get an endpoint, and deployed fine-tuned models incur an hourly hosting fee even when idle. Factor this into your cost analysis — for low-traffic workloads, OpenAI's per-token pricing may be cheaper.
Best Practices for Fine-Tuning AI Models in Production
- Measure before and after. Build an evaluation set with expected outputs, run it against the base model with prompt engineering, then against the fine-tuned model. If accuracy doesn't improve meaningfully, don't ship the fine-tune.
- Version everything. Store the model ID in configuration (
appsettings.jsonor Azure App Configuration), never hardcode it. You'll retrain regularly, and swapping models should be a config change, not a deployment. - Handle transient failures. Wrap API calls with retry policies (Polly's
AddStandardResilienceHandler()works well) — rate limits (HTTP 429) are a fact of life with AI APIs. - Cap output tokens. Set
MaxOutputTokenCountappropriate to your task. Unbounded output is unbounded cost. - Log inputs and outputs (respecting privacy rules) — this becomes your next round of training data. Production fine-tuning is a flywheel: deploy, collect, correct, retrain.
- Start with the smallest capable model. A fine-tuned gpt-4o-mini often outperforms a prompted GPT-4o on narrow tasks at 10–20x lower cost.
Common Pitfalls to Avoid
- Fine-tuning for knowledge injection. The model won't reliably memorize your product catalog from 100 examples — use RAG for facts.
- Training/inference prompt mismatch. If you trained with one system prompt and call with another, quality drops silently.
- Overfitting on tiny datasets. With very few examples and many epochs, the model parrots training data and generalizes poorly. Watch the validation loss curve in the dashboard.
- Ignoring data leakage. Never let validation examples appear in training data — your metrics will lie to you.
- Blocking threads. Never call
.Resultor.Wait()on these API calls in ASP.NET Core — alwaysawait. AI calls are slow (hundreds of milliseconds to seconds), making thread starvation a real risk.
Conclusion: Key Takeaways on Fine-Tuning AI Models With C#
Fine-tuning AI models and calling them from C# is far more accessible in 2026 than most .NET developers realize — the entire pipeline, from JSONL preparation to training to inference, can live inside your C# codebase. The essentials to remember:
- Choose the right tool: prompt engineering first, RAG for knowledge, fine-tuning for behavior — format, tone, and classification consistency.
- Data quality drives everything: 50–100 clean, consistent examples beat thousands of noisy ones.
- The official OpenAI .NET SDK makes uploading data, launching jobs, and calling fine-tuned models feel like any other C# API integration.
- Azure OpenAI gives you the same workflow with enterprise data residency — but remember the hourly deployment cost for fine-tuned models.
- Treat fine-tuning as a flywheel: evaluate, deploy, collect production data, and retrain.
Start small: pick one narrow, high-volume task in your application — ticket routing, data extraction, response drafting — build a 50-example dataset this week, and run your first fine-tuning job. Once you see a fine-tuned gpt-4o-mini hitting 95%+ accuracy on a task that previously needed a hand-tuned mega-prompt, you'll understand why this technique has become a core skill for professional C# developers.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
This article is part of the AI in .NET guide (LLMs, agents, RAG, ML.NET and AI coding tools).
Comments
Post a Comment