If your .NET application needs more than one independent conversation, an implicit current-session shortcut is no longer enough. With Aspose.LLM, you can create explicit chat sessions with StartNewChatAsync, address each conversation by its session ID, and keep the histories separate.
This pattern is useful for multi-user applications, multi-stage workflows, and longer conversations where you need explicit control over session identity and KV-cache cleanup.
This guide walks through the whole path in order: install the package, apply a license, create the API, start a session, exchange messages, run several conversations side by side, and trim the cache when the conversation gets long. Follow the steps from top to bottom, and you will finish with a working console application.
When to Use Explicit Multi-Turn Sessions
Use explicit sessions when your application needs:
- Multiple users, each with an independent conversation in the same process.
- Multi-stage workflows where each stage has its own dialogue history.
- Long conversations where you need to manage the KV cache.
- Explicit control over which conversation receives a message.
If your application only needs one short conversation at a time, the simpler SendMessageAsync pattern is usually enough. Use explicit sessions when conversation identity itself becomes part of your application logic.
Prerequisites
Before you start, make sure you have:
- A .NET 8 or .NET 10 project. A console application is enough for this guide.
- An Aspose.LLM license. Inference does not run in evaluation mode; a free temporary license works for evaluation.
- Enough RAM for the model.
Qwen25Presetneeds roughly 8–12 GB. - Internet access on the first run. The first
AsposeLLMApi.Create(preset)call downloads nativellama.cppbinaries (100–500 MB) and the model file (~4 GB forQwen25PresetQ4_K_M). Budget several minutes; later runs use the local caches.
Step 1: Install the Aspose.LLM NuGet Package
A single package contains the full SDK. From your project directory:
dotnet add package Aspose.LLM
Or, in the Visual Studio Package Manager Console:
Install-Package Aspose.LLM
The package installs one managed assembly, Aspose.LLM.dll. Native binaries are not bundled — they are fetched on first use, which is why the first run is slower than the rest.
Verify the package restored correctly with a compile-only check:
using Aspose.LLM;
using Aspose.LLM.Abstractions.Parameters.Presets;
_ = typeof(AsposeLLMApi);
_ = typeof(Qwen25Preset);
Step 2: Apply Your License
Apply the license once, before you construct the API. Aspose.LLM does not run chat operations without it.
using Aspose.LLM;
var license = new Aspose.LLM.License();
license.SetLicense("Aspose.LLM.lic");
Keep Aspose.LLM.lic next to the executable, or pass a full path. If you skip this step, the first inference call fails with Not licensed for this method.
Step 3: Create the API Instance
Create one AsposeLLMApi from a preset. This is also where you set the system prompt that every session in this guide will inherit.
using Aspose.LLM.Abstractions.Parameters.Presets;
var preset = new Qwen25Preset();
preset.ChatParameters.SystemPrompt =
"You are a concise travel assistant. Answer in one or two sentences.";
using var api = AsposeLLMApi.Create(preset);
Two things to keep in mind:
- One instance per process.
AsposeLLMApi.Createenforces this internally. A second call while the first instance is alive throwsInvalidOperationException. Create the instance once and reuse it. - The
usingpattern disposes the API automatically. Disposal unloads the model and releases the single-instance guard.
Step 4: Start an Explicit Chat Session
Create an explicit session with StartNewChatAsync:
string sessionId = await api.StartNewChatAsync();
The method returns the session ID. Keep that ID with the conversation and use it when sending subsequent messages.
You can also customize the session when it starts:
string sessionId = await api.StartNewChatAsync(
preset: myPreset,
sessionId: "user-42-conv-1");
This lets you use a different preset for that session or provide your own meaningful identifier. When you supply your own ID, uniqueness is your responsibility.
A newly started session also becomes the current session — the one that a plain SendMessageAsync call would target. That matters in Step 8.
Step 5: Send Messages to the Session
Once you have a session ID, use SendMessageToSessionAsync:
string reply1 = await api.SendMessageToSessionAsync(
sessionId,
"I want to plan a trip to Lisbon in May.");
string reply2 = await api.SendMessageToSessionAsync(
sessionId,
"Suggest three neighborhoods to stay in.");
string reply3 = await api.SendMessageToSessionAsync(
sessionId,
"Which one is closest to the beach?");
Each message belongs to the specified session. The model sees the session’s previous history, so the third question can refer to the earlier discussion.
Every message also adds to the session’s KV cache. This is what allows the conversation to remain in context across multiple turns.
For reference, these are the signatures you will work with:
public Task<string> StartNewChatAsync(
PresetCoreBase? preset = null,
string? sessionId = null);
public Task<string> SendMessageToSessionAsync(
string sessionId,
string message,
IEnumerable<byte[]>? media = null,
CancellationToken cancellationToken = default);
Step 6: Run the Complete Multi-Turn Chat
Put Steps 2 through 5 together into a single program:
using Aspose.LLM;
using Aspose.LLM.Abstractions.Parameters.Presets;
var license = new Aspose.LLM.License();
license.SetLicense("Aspose.LLM.lic");
var preset = new Qwen25Preset();
preset.ChatParameters.SystemPrompt =
"You are a concise travel assistant. Answer in one or two sentences.";
using var api = AsposeLLMApi.Create(preset);
string sessionId = await api.StartNewChatAsync();
string[] userMessages =
{
"I want to plan a trip to Lisbon in May.",
"Suggest three neighborhoods to stay in.",
"Which one is closest to the beach?",
"How long does it take to walk from there to the historic center?",
"Any recommendations for lunch on the way?",
};
foreach (string message in userMessages)
{
Console.WriteLine($"> {message}");
string reply = await api.SendMessageToSessionAsync(
sessionId,
message);
Console.WriteLine(reply);
Console.WriteLine();
}
Save this as Program.cs, make sure Aspose.LLM.lic sits next to the executable, and run:
dotnet run
The important pieces are:
- The license and preset are configured once.
AsposeLLMApiis created once.StartNewChatAsynccreates the explicit conversation.- The returned
sessionIdidentifies that conversation. - Every call to
SendMessageToSessionAsynctargets the same session. - Each follow-up can use the previous turns as context.
Step 7: Host Multiple Concurrent Conversations
A single AsposeLLMApi instance can host multiple sessions. Each session has its own conversation history and KV region, and the loaded model is shared across all of them.
string sessionA = await api.StartNewChatAsync(sessionId: "conv-A");
string sessionB = await api.StartNewChatAsync(sessionId: "conv-B");
await api.SendMessageToSessionAsync(
sessionA,
"Let us talk about ancient Rome.");
await api.SendMessageToSessionAsync(
sessionB,
"Let us talk about modern cooking.");
string replyA = await api.SendMessageToSessionAsync(
sessionA,
"Name three emperors.");
string replyB = await api.SendMessageToSessionAsync(
sessionB,
"Name three essential knives.");
Console.WriteLine($"A: {replyA}");
Console.WriteLine($"B: {replyB}");
The two topics remain separate: sessionA sees only the Rome conversation, while sessionB sees only the cooking conversation.
Do not call SendMessageToSessionAsync concurrently on the same session ID. Serialize calls per session. Across sessions, serialize at the application level because the native model and KV pool are shared and a single inference call holds native resources. If you need to handle concurrent requests, queue them or route them through a single worker.
Step 8: Manage the KV Cache in Long Sessions
Long conversations accumulate context in the session’s KV cache. As a session approaches the preset’s ContextParameters.ContextSize, the engine automatically trims the cache according to ChatParameters.CacheCleanupStrategy. The default strategy is RemoveOldestMessages.
You can also trigger cleanup explicitly at a natural topic boundary:
api.ForceCacheCleanup(
CacheCleanupStrategy.KeepSystemPromptOnly);
ForceCacheCleanup operates on the current session, and throws InvalidOperationException if no session is active. If you need to trim another session, make that session current first — for example, by calling SendMessageToSessionAsync on it — then perform the cleanup.
Five strategies are available:
| Strategy | Keeps in cache | When to use |
|---|---|---|
RemoveOldestMessages (default) | System prompt + most recent turns | General-purpose; used for automatic trimming. |
KeepSystemPromptOnly | System prompt only | Hard reset before starting a new topic in the same session. |
KeepSystemPromptAndHalf | System prompt + last half of the history | Balanced recall and room for new tokens. |
KeepSystemPromptAndFirstUserMessage | System prompt + first user turn | Recall-heavy tasks where the original ask matters. |
KeepSystemPromptAndLastUserMessage | System prompt + most recent user turn | Focus on the latest question, drop middle context. |
Explicit cleanup is useful when your application knows that an earlier part of the conversation is no longer important.
Step 9: Choose a Session ID Strategy
You have two straightforward options.
Let Aspose.LLM Generate the ID
string sessionId = await api.StartNewChatAsync();
The engine generates the identifier. Store the returned value in your application or database so you can address the conversation later.
Provide Your Own ID
string sessionId = await api.StartNewChatAsync(
sessionId: "user-42-conv-1");
Meaningful IDs can make logs and persisted session files easier to inspect. When you generate the ID yourself, make sure it is unique within the process.
Common Issues
| Problem | What to check |
|---|---|
Not licensed for this method | Apply the Aspose.LLM license before starting a session (Step 2). |
Only one AsposeLLMApi instance can be created at a time | A previous instance was not disposed. Keep one instance for the process lifetime. |
First Create call takes a long time | It is downloading native binaries and the model. On a slow network, budget 5–15 minutes. |
| Context overflow or degraded context in a long session | Review ChatParameters.CacheCleanupStrategy and consider explicit cleanup at topic boundaries. |
| Messages appear in the wrong conversation | Check that every SendMessageToSessionAsync call receives the intended sessionId. |
Multi-session code uses SendMessageAsync unexpectedly | SendMessageAsync targets the current session; use SendMessageToSessionAsync when the application needs a specific conversation. |
| Calls overlap on the same session | Serialize SendMessageToSessionAsync calls for each session ID. |
A particularly important distinction is between SendMessageAsync and SendMessageToSessionAsync: the former uses the current session, while the latter explicitly addresses the session you provide.
Simple Chat vs. Explicit Multi-Turn Sessions
| If you need… | Use |
|---|---|
| One short conversation | SendMessageAsync |
| A simple CLI or prototype | SendMessageAsync |
| Multiple independent conversations | Explicit sessions |
| Multiple users on one process | Explicit sessions |
| A multi-stage workflow with separate histories | Explicit sessions |
| Explicit session IDs | StartNewChatAsync + SendMessageToSessionAsync |
| Long conversations with cache-management needs | Explicit sessions |
Start with implicit messaging when your application has only one active conversation. Move to explicit sessions when you need to identify, route, isolate, or manage conversations independently.
Conclusion
Explicit sessions give your .NET application control over conversation identity without requiring you to build the conversation-history mechanism yourself.
Working through the steps above, you installed the package, applied a license, created a single AsposeLLMApi instance, started a session with StartNewChatAsync, kept its returned ID, and exchanged messages with SendMessageToSessionAsync. From there you can host multiple independent conversations on one instance, choose your own session IDs when useful, and manage KV-cache cleanup for long-running sessions.
For a single short conversation, implicit SendMessageAsync remains the simpler choice. For multi-user chat, multi-stage workflows, and applications where conversation identity matters, explicit sessions provide the control you need.
