
Learn Microsoft Semantic Kernel in C# with this step-by-step tutorial. Build AI agents, plugins, and copilots in .NET with runnable code. Start today.
If you want to add real AI capabilities to a .NET application, Semantic Kernel C# is the fastest, most production-ready route available today. Microsoft Semantic Kernel (SK) is an open-source SDK that lets you plug large language models like GPT-4o, Claude, or local models into ordinary C# code, give them tools (called plugins), and let them plan and execute multi-step tasks. In this Semantic Kernel tutorial you will build an AI agent step by step: from a "hello world" chat completion, through function calling with your own C# methods, to a stateful copilot that can answer questions about your own data.
Every example targets .NET 8 or later and uses the stable Microsoft.SemanticKernel package. You can copy the code straight into a console app and run it.
What Is Microsoft Semantic Kernel and Why Use It in C#?
At its core, Semantic Kernel is an orchestration layer. It sits between your application and one or more AI models and solves three problems that every AI integration eventually runs into:
- Provider abstraction. Swap OpenAI for Azure OpenAI, Ollama, or another connector without rewriting business logic.
- Function calling done right. Expose normal C# methods as tools the model can invoke, with type-safe arguments and automatic JSON schema generation.
- Agents, memory, and planning. Manage chat history, maintain state, and let the model decide which functions to call in which order.
Compared to calling the raw HTTP API yourself, SK gives you dependency injection, logging, telemetry via OpenTelemetry, retry filters, and a consistent mental model. Compared to Python frameworks like LangChain, it is idiomatic .NET: async/await, IServiceCollection, ILogger, and strong typing everywhere. That is why teams building enterprise copilots in C# choose it.
Step 1: Install Semantic Kernel and Create the Kernel
Create a new console project and add the package. If you use OpenAI directly, the core package includes the connector; for Azure OpenAI, the same package works with a different builder method.
dotnet new console -n SkAgentDemo
cd SkAgentDemo
dotnet add package Microsoft.SemanticKernel
dotnet add package Microsoft.Extensions.Logging.Console
The Kernel object is the dependency-injection container for everything AI-related. Build it once and reuse it.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
?? throw new InvalidOperationException("Set OPENAI_API_KEY first.");
var builder = Kernel.CreateBuilder();
// Register a chat model. For Azure: builder.AddAzureOpenAIChatCompletion(deployment, endpoint, key);
builder.AddOpenAIChatCompletion(modelId: "gpt-4o-mini", apiKey: apiKey);
// Optional but recommended: see exactly what the model is doing.
builder.Services.AddLogging(l => l.AddConsole().SetMinimumLevel(LogLevel.Information));
Kernel kernel = builder.Build();
// Simplest possible call.
var reply = await kernel.InvokePromptAsync("Explain dependency injection in one sentence.");
Console.WriteLine(reply);
Never hard-code API keys. Use environment variables locally and a secret store such as Azure Key Vault in production.
Step 2: Give Your AI Agent Tools with Semantic Kernel Plugins
A model on its own can only talk. To make it act, you expose C# methods as Semantic Kernel plugins. Decorate a method with [KernelFunction] and a [Description]; SK generates the JSON schema the model uses to decide when and how to call it. The descriptions are not decoration — they are the prompt the model reads, so write them clearly.
using System.ComponentModel;
using Microsoft.SemanticKernel;
public sealed class OrderPlugin
{
// In real code this would hit a database or an HTTP API.
private static readonly Dictionary<string, (string Status, decimal Total)> Orders = new()
{
["A-1001"] = ("Shipped", 129.99m),
["A-1002"] = ("Processing", 49.50m),
};
[KernelFunction("get_order_status")]
[Description("Returns the shipping status and total for a customer order ID such as A-1001.")]
public string GetOrderStatus(
[Description("The order ID, e.g. A-1001")] string orderId)
{
return Orders.TryGetValue(orderId, out var o)
? $"Order {orderId} is {o.Status}. Total: ${o.Total}."
: $"No order found with ID {orderId}.";
}
[KernelFunction("get_current_date")]
[Description("Returns today's date in ISO 8601 format.")]
public string GetCurrentDate() => DateTime.UtcNow.ToString("yyyy-MM-dd");
}
Register the plugin and turn on automatic function calling. This single setting is what transforms a chatbot into an agent: the model can now request a tool call, SK executes your C# method, feeds the result back, and the model continues until it has an answer.
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
kernel.Plugins.AddFromType<OrderPlugin>("Orders");
var settings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var chat = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory("You are a helpful support assistant for an online store.");
history.AddUserMessage("Where is my order A-1001, and what day is it today?");
var response = await chat.GetChatMessageContentAsync(history, settings, kernel);
Console.WriteLine(response.Content);
// -> "Today is 2026-08-24. Your order A-1001 has shipped, with a total of $129.99."
Notice you never told the model which function to call or in which order. It planned two calls, executed them, and synthesized a reply. That is the core loop of every AI agent in C#.
Step 3: Build a Stateful Copilot with Chat History
A copilot must remember the conversation. ChatHistory holds system, user, assistant, and tool messages; you append to it on every turn. Here is a complete interactive loop with streaming output, which noticeably improves perceived latency.
var chat = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory(
"You are OrderBot, a concise support copilot. Use tools when you need order data. " +
"If you don't know something, say so instead of guessing.");
while (true)
{
Console.Write("\nYou: ");
var input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("exit", StringComparison.OrdinalIgnoreCase))
break;
history.AddUserMessage(input);
Console.Write("OrderBot: ");
var fullReply = new System.Text.StringBuilder();
await foreach (var chunk in chat.GetStreamingChatMessageContentsAsync(history, settings, kernel))
{
Console.Write(chunk.Content);
fullReply.Append(chunk.Content);
}
history.AddAssistantMessage(fullReply.ToString());
}
Why this matters: because history is just a list you own, you can persist it to a database per user, trim old messages to control token cost, or redact sensitive content before it ever reaches the model.
Step 4: Use the Agent Framework for Multi-Agent Workflows
For more structured scenarios, Semantic Kernel ships an Agent Framework (Microsoft.SemanticKernel.Agents.Core). A ChatCompletionAgent bundles a name, instructions, a kernel, and execution settings into one reusable object, which makes it easy to run several specialized agents side by side — for example a "Researcher" and a "Reviewer".
dotnet add package Microsoft.SemanticKernel.Agents.Core
using Microsoft.SemanticKernel.Agents;
var supportAgent = new ChatCompletionAgent
{
Name = "SupportAgent",
Instructions = "Answer customer order questions using the Orders tools. Be brief and friendly.",
Kernel = kernel,
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
})
};
var thread = new ChatHistoryAgentThread();
await foreach (var message in supportAgent.InvokeAsync(
"Is order A-1002 on its way yet?", thread))
{
Console.WriteLine($"{message.Message.AuthorName}: {message.Message.Content}");
}
Agents share the same plugins and kernel, so a tool you wrote once works everywhere. The framework also supports group chats where agents hand off to each other based on a termination strategy — useful for code review bots, content pipelines, and approval workflows.
Step 5: Add Your Own Data (RAG) to the Agent
Most real copilots need to answer questions about private documents. The pattern is Retrieval-Augmented Generation: embed your documents, find the most relevant chunks for a query, and inject them into the prompt. A minimal in-memory version looks like this; in production you would use the same API with Azure AI Search, Qdrant, or Postgres pgvector connectors.
dotnet add package Microsoft.SemanticKernel.Connectors.InMemory --prerelease
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Embeddings;
public sealed class DocChunk
{
[VectorStoreKey] public string Id { get; set; } = "";
[VectorStoreData] public string Text { get; set; } = "";
[VectorStoreVector(1536)] public ReadOnlyMemory<float> Embedding { get; set; }
}
// Register an embedding model alongside the chat model.
builder.AddOpenAITextEmbeddingGeneration("text-embedding-3-small", apiKey);
var embedder = kernel.GetRequiredService<ITextEmbeddingGenerationService>();
var store = new InMemoryVectorStore();
var collection = store.GetCollection<string, DocChunk>("policies");
await collection.CreateCollectionIfNotExistsAsync();
string[] docs =
{
"Returns are accepted within 30 days of delivery with the original receipt.",
"Standard shipping takes 3-5 business days; express shipping takes 1-2 days.",
};
foreach (var (text, i) in docs.Select((t, i) => (t, i)))
{
await collection.UpsertAsync(new DocChunk
{
Id = $"doc-{i}",
Text = text,
Embedding = await embedder.GenerateEmbeddingAsync(text)
});
}
// Retrieve and answer.
var question = "How long do I have to return an item?";
var qVector = await embedder.GenerateEmbeddingAsync(question);
var results = collection.SearchEmbeddingAsync(qVector, top: 2);
var context = string.Join("\n", await results.Select(r => r.Record.Text).ToListAsync());
var answer = await kernel.InvokePromptAsync(
"Answer using only this context:\n{{$context}}\n\nQuestion: {{$question}}",
new KernelArguments { ["context"] = context, ["question"] = question });
Console.WriteLine(answer);
Wrap that retrieval in a [KernelFunction] called search_policies and your agent will call it automatically whenever a user asks a policy question — combining RAG with function calling in one agent.
Semantic Kernel Best Practices for Production
- Use dependency injection. In ASP.NET Core, call
services.AddKernel().AddOpenAIChatCompletion(...)and injectKernelinto controllers or services. Kernel is lightweight; register it as transient or scoped and share the singletonHttpClientunderneath. - Add filters for safety and observability. Implement
IFunctionInvocationFilterto log every tool call, enforce permissions, or block dangerous arguments before your method runs. - Constrain function choice when you can.
FunctionChoiceBehavior.Required()forces a tool call;Auto(functions: [...])limits the model to a subset. Fewer visible tools mean cheaper prompts and fewer mistakes. - Trim chat history. Use
ChatHistoryTruncationReducerorChatHistorySummarizationReducerto keep token usage bounded on long sessions. - Enable OpenTelemetry. SK emits traces and metrics (token counts, latency, function durations) that flow straight into Application Insights or any OTLP collector.
- Pin versions. The core SDK is stable, but agent and vector-store packages evolve quickly. Pin exact versions and read release notes before upgrading.
Common Pitfalls When Building a C# AI Agent
- Vague function descriptions. "Gets data" tells the model nothing. Describe what the function does, when to use it, and what the parameters look like, with examples.
- Forgetting to pass the kernel.
GetChatMessageContentAsync(history, settings)without thekernelargument silently disables function calling. Always pass it. - Letting the model call anything. A function that deletes records or sends email should require confirmation. Use a filter to intercept it, or keep destructive operations out of the plugin entirely.
- Blocking on async calls. Never use
.Resultor.Wait()on SK calls in ASP.NET; it can deadlock and wastes threads during long model responses. - Trusting output blindly. Models hallucinate. Validate anything that feeds into business logic, and instruct the agent to say "I don't know" rather than guess.
- Ignoring cost. Automatic function calling can trigger several model round-trips per user message. Log token usage from
response.Metadata["Usage"]and set a maximum auto-invoke count.
Conclusion: Key Takeaways
Semantic Kernel C# turns AI integration from a pile of HTTP calls into a clean, testable, idiomatic .NET architecture. You have seen the full progression: create a kernel, expose C# methods as plugins, enable automatic function calling to get a working agent, add chat history to make it a copilot, scale up with the Agent Framework, and ground it in your own data with RAG.
- The
Kernelis your AI DI container; build it once withKernel.CreateBuilder(). [KernelFunction]plus a good[Description]is all it takes to give an agent a tool.FunctionChoiceBehavior.Auto()plus passing the kernel enables agentic planning.ChatHistoryis state you own — persist, trim, and redact it deliberately.- Filters, telemetry, and constrained tool sets are what separate a demo from production.
Start with the order-status example above, replace the dictionary with your real service, and you will have a genuine AI agent running in C# within an afternoon. From there, Microsoft Semantic Kernel gives you a clear path to multi-agent copilots without leaving the .NET ecosystem you already know.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment