In this section, we're going to actually be writing some code. We'll be making a basic LLM completion request from C# by building a bare-bones console application that uses the official OpenAI NuGet package. This will allow us to make a call to the OpenAI API to complete some text, similar to what we learned in previous sections.
It's important to note that this is a throwaway example. Later on, you'll be using LLMs not just from OpenAI but also from providers like Google Gemini and Anthropic, and you will implement this functionality in a much more generic way. For now, let's just get started with a basic example using OpenAI.
Before we can start writing code and calling the OpenAI API, we need to get a few prerequisites in order.
First things first, we're going to need an API key. To get one, head over to openai.com and sign up for an account. Once you're signed in, navigate to the API dashboard.
On the left-hand side, you'll find a section for API keys. Click on that, and then click "Create new secret key." You can give your key a name and assign it to a project. This will generate a new API key for you to use in our application.

Next, create a new console application. I've just created a basic .NET 10 console application in C#. At the moment, it just prints "Hello, World!" to the console.

If we run this application by hitting F5, we can confirm it works as expected.

Perfect. Now we need to get our OpenAI key into the application securely. You can find the demo code for this on GitHub, but one thing you won't find is my API key. I've stored it in a file called .env.
To do this, create a new file named .env in the root of your project. Inside this file, add a single line with your API key:
OPENAI_API_KEY=paste_your_api_key_here
To ensure you don't accidentally commit this file to source control, it's a good practice to add .env to your .gitignore file.

We'll need a couple of NuGet packages. The first is dotenv.net, which allows us to read environment variables from our .env file. The second is the OpenAI package itself.

Install these two packages using the .NET CLI:
dotnet add package dotenv.net
dotnet add package OpenAI
After installing, your project file (.csproj) should look something like this:

With the setup complete, we're ready to start coding.
Let's clear out the default "Hello, World!" code and start building our chat client.
First, we need to add our using statements and load the API key from the .env file.
using dotenv.net;
using OpenAI.Chat;
using OpenAI.Responses;
DotEnv.Load();
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
if (openAiKey == null)
{
throw new InvalidOperationException("Missing OPENAI_API_KEY");
}
We'll use DotEnv.Load() to load the variables from our file. Then, we can retrieve the key using the static Environment.GetEnvironmentVariable method. We also add a simple check to throw an InvalidOperationException if the key is missing.

Now we can create an instance of the ChatClient. This client requires a model name and the API key. We'll use gpt-5-nano, which is a small model perfect for our basic tests.
ChatClient client = new(model: "gpt-5-nano", openAiKey);

As I discussed in the last section, LLMs are stateless. They don't remember your chat history between requests. To maintain a conversation, we need to send the entire chat history with every API call.
We'll manage this using a list of ChatMessage objects. We'll start the conversation with a message from the assistant to kick things off.
List<ChatMessage> messages =
[
new AssistantChatMessage("Hello, what do you want to do today?")
];
The ChatMessage class comes from the OpenAI.Chat namespace. As we talk to the LLM, we'll be adding UserChatMessage instances for our input and new AssistantChatMessage instances for the AI's responses.

To create an interactive chat experience in the console, we'll use an infinite loop, often called an "agent loop" or "chat loop." This is one of the few times where a while (true) loop is appropriate. The loop will continuously wait for user input, send it to the AI, and display the response.
Here's the complete structure of our Program.cs file:

using dotenv.net;
using OpenAI.Chat;
using OpenAI.Responses;
DotEnv.Load();
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
if (openAiKey == null)
{
throw new InvalidOperationException("Missing OPENAI_API_KEY");
}
ChatClient client = new(model: "gpt-5-nano", openAiKey);
List<ChatMessage> messages =
[
new AssistantChatMessage("Hello, what do you want to do today?")
];
Console.WriteLine(messages[0].Content[0].Text);
while (true)
{
Console.ForegroundColor = ConsoleColor.Blue;
var input = Console.ReadLine();
if (input == null || input.ToLower() == "exit")
{
break;
}
Console.ResetColor();
messages.Add(new UserChatMessage(input));
ChatCompletion completion = client.CompleteChat(messages);
var response = completion.Content[0].Text;
messages.Add(new AssistantChatMessage(response));
Console.WriteLine(response);
}
Inside the loop, we:
Console.ReadLine.messages list as a UserChatMessage.client.CompleteChat(messages), passing the entire conversation history. This makes the API call.completion object.messages list as an AssistantChatMessage so it's included in the next request.Let's run the application and see it in action. The console will display the initial message, "Hello, what do you want to do today?", and wait for your input.
I'll start by asking it to "tell me a joke."

When I hit enter, the code sends my request to OpenAI. By inspecting the messages list in the debugger, we can see it contains both the initial assistant message and my user message.

The API call will happen synchronously (though an async version is available) and we'll get a completion object back. This object contains the response text as well as useful metadata.

The AI responds with a classic: "Why don't scientists trust atoms? Because they make up everything." Honestly, I've spent a lot of time asking LLMs to tell me jokes, and they always tell you this exact same one.
Now, let's continue the conversation. I'll say, "yes, tell me another one."

This time, when we hit the breakpoint, our messages array has four items: the initial prompt, my first question, the first joke, and my new request. The list is being built up with the entire history.
To prove that the LLM is using this history, I'll ask it: "what was the first joke you told me about?"

Because we passed in the entire conversation, it correctly answers: "The first joke was: 'Why don't scientists trust atoms? Because they make up everything.'"

Remember, the LLM hasn't learned or remembered anything. It's completely stateless. It knows the context of our conversation only because we are passing the entire history in every single request.
While debugging the response from the API, you might notice a Usage property on the completion object. This contains information about tokens.

We haven't talked about tokens a huge amount, but in a nutshell, you can think of a token as a part of a word, kind of like a syllable. For example, the word "syllable" might be three tokens.
Tokens are what OpenAI and other model providers use to measure usage. When they charge you for access to a model, they count the number of tokens in your input and the number of tokens generated in the output. When we say an LLM generates a story "word by word," it's actually generating it "token by token." It's a minor detail, but helpful to understand.
This simple console application is a really good entry point for learning how to build up conversation histories, send them off to an LLM, and get a response back. You can chat with this pretty much like you're talking to ChatGPT.
Have a play around with this code, which you can find on GitHub. Try asking it to tell you a story or answer complex questions. In the next section, we'll start to do this a little bit more properly. Thanks for reading!
© 2025 Dometrain. All rights reserved.