Chapter 1: Introduction to Semantic Kernel (SK) for .NET
Theoretical Foundations
At its heart, Microsoft Semantic Kernel (SK) is an orchestration framework designed to solve the impedance mismatch between the probabilistic, non-deterministic nature of Large Language Models (LLMs) and the deterministic, strictly typed world of native .NET code. It is not merely a wrapper around an API call; it is a runtime that manages the lifecycle of AI interactions, enforces security boundaries, and provides the structural integrity required for enterprise-grade software.
To understand SK, one must first understand the fundamental problem of modern AI engineering: LLMs are brilliant at semantic understanding and generation, but they are stateless, lack native access to external tools, and cannot maintain complex workflows independently. Conversely, .NET is exceptional at type safety, concurrency, and business logic execution but lacks inherent semantic reasoning capabilities. SK acts as the diplomat and translator between these two worlds, allowing them to communicate via a shared, standardized protocol.
The Core Architecture: The Kernel as the Operating System
Imagine a modern operating system (OS). The OS itself does not perform specific tasks like editing photos or calculating spreadsheets. Instead, it provides a kernel that manages resources, schedules processes, and offers a standardized interface (API) through which applications can run. If an application needs to access the hard drive, it calls the file system driver; if it needs to display graphics, it calls the video driver.
The Kernel (The OS): The Kernel instance is the central execution engine. It holds the state of the AI interaction, manages the memory, and routes requests to the appropriate plugins. Just as an OS abstracts hardware complexity, the Kernel abstracts the complexity of LLM providers (OpenAI, Azure OpenAI, Hugging Face, etc.).
2. Plugins (The Drivers/Applications): Plugins are collections of native functions that the LLM can invoke. These are the "apps" that extend the OS's capabilities. Without plugins, the LLM is a closed system—smart but isolated. With plugins, the LLM can query databases, send emails, or perform calculations.
3. Planners (The Scheduler): While the OS knows how to run an app, it often needs a scheduler to decide which apps to run and in what order to achieve a complex goal. The Planner is an AI-driven component that analyzes a user's intent, reviews the available plugins, and constructs a dynamic execution plan (a chain of functions) to fulfill that intent.
The "Glue" Analogy
Consider building a house. The LLM is the architect who understands the concept of a house (the prompt: "I want a warm, modern living room"). The native .NET code is the construction crew and materials (concrete, wood, wiring). The architect cannot lay bricks, and the crew cannot design the aesthetic. Semantic Kernel is the project manager and the blueprint glue. It translates the architect's high-level design into specific, sequential instructions for the crew (e.g., "Pour foundation," "Frame walls," "Install wiring"), ensuring the final structure matches the intent.
The Role of Modern C# Features in AI Orchestration
Semantic Kernel is deeply integrated with the modern .NET ecosystem, leveraging specific C# features to create a fluent, type-safe, and asynchronous development experience. This is not incidental; these features are critical for managing the latency and complexity of AI operations.
1. Dependency Injection (DI) and Interfaces
In previous chapters of this series, we discussed the importance of Inversion of Control (IoC) for testability and modularity. In the context of AI, this is paramount. You rarely want to hardcode a specific LLM provider (e.g., OpenAIChatCompletion) directly into your business logic. Why? Because model APIs change, costs fluctuate, and you may need to switch from a cloud model to a local model for privacy reasons.
SK utilizes the standard Microsoft.Extensions.DependencyInjection library. By programming against interfaces like IChatCompletionService, you decouple your orchestration logic from the underlying model implementation.
// The abstraction that allows swapping models without changing business logic
public interface IChatCompletionService
{
Task<ChatMessageContent> GetChatMessageContentsAsync(
ChatHistory chatHistory,
PromptExecutionSettings? executionSettings = null,
Kernel? kernel = null,
CancellationToken cancellationToken = default);
}
Why this matters: If you build an agentic pattern relying on GPT-4 today, and tomorrow GPT-5 releases with a different API signature but the same interface, your application remains stable. The DI container resolves the concrete implementation at runtime, allowing you to pivot architectures instantly.
2. IAsyncEnumerable<T> and Streaming
LLMs are token-streaming machines, not monolithic block retrievers. When you ask a model a question, it generates tokens one by one. Traditional synchronous Task<string> calls mask this latency, forcing the user to wait for the entire response before seeing anything.
Modern C# introduces IAsyncEnumerable<T>, which allows you to iterate over a sequence of data as it becomes available. SK leverages this natively for streaming scenarios. This is crucial for user experience (UX) in AI applications; seeing text appear in real-time creates the illusion of "thinking" and reduces perceived latency.
// Conceptual usage of streaming in SK
await foreach (var contentChunk in kernel.InvokeStreamingAsync<StreamingChatMessageContent>(pluginFunction))
{
Console.Write(contentChunk.Content);
}
Architectural Implication: This feature transforms AI applications from request/response systems into real-time communication channels, essential for chat interfaces or live data analysis.
3. Func<T, TResult> and Native Functions
SK treats C# methods as first-class citizens. It allows you to wrap native .NET methods into KernelFunction objects. This is where the "semantic" meets the "native." By using lambda expressions and delegates, SK can execute strict, deterministic code alongside probabilistic LLM generations.
For example, a function to calculate tax rates must be 100% accurate. You cannot rely on an LLM to do math. You define a native C# method, register it as a plugin, and the Planner can call it when math is required.
4. Attributes for Metadata ([Description], [KernelFunction])
Modern C# attributes allow us to attach metadata to code. In SK, this is the bridge between code and the LLM's understanding. The LLM does not read C# code; it reads descriptions.
public class WeatherPlugin
{
[KernelFunction, Description("Gets the current weather for a location")]
public string GetWeather([Description("The city and state")] string location)
{
// Deterministic logic here
return "72°F and Sunny";
}
}
The [Description] attribute is injected into the system prompt sent to the LLM. The LLM uses this description to decide when to call the function. This is a critical concept: The code is not just executed; it is semantically indexed.
Plugins: The Modular Capabilities
Native Functions: Standard C# methods that perform deterministic operations (e.g., Math.Sqrt, Database.Query).
2. Semantic Functions: Prompts (text templates) that are sent to the LLM to perform reasoning or generation.
The Analogy of the Swiss Army Knife:
Think of a Plugin as a single tool on a Swiss Army Knife. One blade is for cutting (Native Function: Calculate), another is a screwdriver (Native Function: SendEmail), and another is a pair of scissors (Semantic Function: SummarizeText). The Kernel holds the knife handle. When a user asks, "Summarize this text and email it to Bob," the Planner extracts the "Summarize" intent (Semantic) and the "Email" intent (Native), assembling the tools required to complete the job.
Planners: The Dynamic Orchestrator
This is the most advanced concept in SK. A Planner is an AI component itself. It takes a high-level goal (e.g., "Plan a trip to Paris") and breaks it down into steps.
Goal Analysis: The Planner receives the user's request.
2. Context Retrieval: It looks at the available plugins registered in the Kernel. It sees functions like BookFlight, ReserveHotel, GetWeather, and ConvertCurrency.
3. Chain Generation: Using the LLM's reasoning capabilities, it constructs a sequence of function calls.
4. Execution: The Kernel executes the chain.
Why this is revolutionary:
In traditional programming, the developer hardcodes the workflow: if (userWantsToTravel) { CallFlightAPI(); CallHotelAPI(); }. In agentic patterns with SK, the developer only provides the tools (plugins). The Planner dynamically discovers the workflow.
Edge Case Consideration: What if the Planner chooses the wrong tool? This is where the "Human in the Loop" pattern (discussed in later chapters) becomes vital. SK allows for "Plan Creation" vs. "Plan Execution." You can generate a plan, inspect it, approve it, and then execute it. This prevents the "hallucination" of calling non-existent functions or chaining them incorrectly.
The Kernel Initialization and Execution Flow
Builder Pattern: SK uses the KernelBuilder pattern (fluent API) to construct the kernel. This allows for a clean, readable configuration of services and plugins.
2. Service Registration: You register the LLM connector (e.g., AddOpenAIChatCompletion). This tells the Kernel where to send semantic requests.
3. Plugin Import: You import plugins (e.g., ImportPluginFromType<MathPlugin>()). This tells the Kernel what native capabilities it possesses.
The Execution Lifecycle:
Prompt Templating: The input is processed. If it contains variables (e.g., {{$input}}), they are injected.
2. Function Selection: If a specific function is invoked, the Kernel prepares the execution settings. If it's a semantic function, the text template is rendered.
3. AI Request (if Semantic): The rendered prompt is sent to the LLM via the registered connector.
4. Native Execution (if Native): The C# method is invoked directly via reflection or compiled delegates.
5. Result Post-Processing: The result is captured, potentially serialized, and returned to the caller or passed to the next function in a chain.
Visualizing the Architecture
The following diagram illustrates the flow of data and control within the Semantic Kernel architecture. Note how the Kernel sits centrally, bridging the gap between the external LLM and the internal .NET ecosystem.
Theoretical Foundations
The theoretical foundation of Semantic Kernel rests on the principle of separation of concerns and modularity. By treating the LLM as a service provider rather than the application core, SK enables robust software engineering practices—dependency injection, interface-driven design, and asynchronous processing—to apply to AI development.
It transforms AI from a static prompt-response interaction into a dynamic, agentic system where the application logic (C#) and the reasoning logic (LLM) collaborate seamlessly. The use of modern C# features ensures that these systems are not only powerful but also maintainable, testable, and scalable.
Basic Code Example
Here is a simple 'Hello World' level code example demonstrating the core concept of Microsoft Semantic Kernel: orchestrating a Large Language Model (LLM) using native .NET code via Plugins and Planners.
The Scenario
Imagine you are building a customer support chatbot for a streaming service. A user asks: "I want to watch a mystery movie tonight."
Ground the LLM: Provide a real list of movies from native .NET code (a Plugin). 2. Orchestrate: Use the LLM to understand the user's intent (filtering for "mystery") and select the correct tool. 3. Execute: Run the native code to retrieve the movie details.
This example simulates that flow without needing an external API or database.
The Code Example
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;
using System.Text.Json;
// 1. Setup and Configuration
// ---------------------------------------------------------
// In a real app, load this from appsettings.json or environment variables.
// For this example, we assume a local Ollama instance running 'phi3' or 'mistral'.
// If you don't have Ollama, replace Endpoint with Azure OpenAI or OpenAI details.
var builder = Kernel.CreateBuilder();
builder.AddOllamaChatCompletion(
modelId: "phi3",
endpoint: new Uri("http://localhost:11434")
);
// Build the Kernel instance. This is the central orchestrator.
Kernel kernel = builder.Build();
// 2. Define the Native Plugin (The "Grounding" Layer)
// ---------------------------------------------------------
// This class represents native .NET code that the LLM can invoke.
// The [Description] attribute tells the LLM what the function does.
public class MoviePlugin
{
// A hardcoded list of movies (simulating a database query).
private readonly List<Movie> _movies = new()
{
new Movie("Inception", "Sci-Fi", 8.8),
new Movie("The Shawshank Redemption", "Drama", 9.3),
new Movie("Se7en", "Mystery", 8.6),
new Movie("The Dark Knight", "Action", 9.0)
};
[KernelFunction, Description("Retrieves a list of movies based on a specific genre.")]
public string GetMoviesByGenre([Description("The genre of the movie (e.g., Mystery, Drama)")] string genre)
{
var filteredMovies = _movies
.Where(m => m.Genre.Equals(genre, StringComparison.OrdinalIgnoreCase))
.ToList();
if (!filteredMovies.Any())
return $"No movies found for genre: {genre}";
// Return a serialized JSON string so the LLM can parse it easily.
return JsonSerializer.Serialize(filteredMovies, new JsonSerializerOptions { WriteIndented = true });
}
}
// Simple record to structure our data
public record Movie(string Title, string Genre, double Rating);
// 3. Register the Plugin with the Kernel
// ---------------------------------------------------------
// We add our C# class to the kernel. The kernel now knows these functions exist.
kernel.Plugins.AddFromType<MoviePlugin>("MovieLibrary");
// 4. Define the Execution Plan (The "Orchestrator")
// ---------------------------------------------------------
// We use a Prompt Execution Settings object to tell the LLM it CAN use tools.
// This is the modern replacement for the older "Planner" class in SK.
var executionSettings = new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions // Automatically triggers the C# code if needed
};
// 5. The Interaction (User -> Kernel -> LLM -> Plugin)
// ---------------------------------------------------------
Console.WriteLine("User: I want to watch a mystery movie tonight.");
Console.WriteLine("System: Processing request...\n");
// The prompt sent to the LLM.
// We instruct the LLM to act as an assistant and use the tools provided.
string prompt = "Suggest a mystery movie from the provided library.";
// Execute the kernel.
// Behind the scenes:
// 1. The LLM receives the prompt + the description of 'MovieLibrary'.
// 2. The LLM decides 'GetMoviesByGenre' is needed with argument "Mystery".
// 3. Semantic Kernel invokes the C# method 'GetMoviesByGenre'.
// 4. The result (JSON data) is sent back to the LLM.
// 5. The LLM formats the JSON into a natural language response.
var result = await kernel.InvokePromptAsync(prompt, executionSettings);
// 6. Output
// ---------------------------------------------------------
Console.WriteLine($"Assistant: {result}");
Detailed Line-by-Line Explanation
Kernel.CreateBuilder(): This initializes a builder pattern, which is the standard modern .NET approach for configuring dependency injection and services. It creates an empty Kernel instance.
* builder.AddOllamaChatCompletion(...): We are adding a chat completion service to the kernel. In this example, we use Ollama (a local LLM runner) for privacy and speed. However, Semantic Kernel is provider-agnostic; you could swap this line for AddAzureOpenAIChatCompletion or AddOpenAIChatCompletion without changing the rest of the logic.
* kernel = builder.Build(): This finalizes the configuration. The Kernel object is now the "brain" capable of orchestrating AI requests.
public class MoviePlugin: This is a standard C# class. It represents the "Native Code" side of the SK architecture. It does not inherit from any special base class, allowing you to wrap existing business logic easily.
* [KernelFunction]: This attribute is crucial. It marks the method as callable by the LLM. Without this, the kernel ignores the method.
* [Description("...")]: The LLM does not see your actual C# code; it sees metadata. The description explains what the function does so the LLM can decide when to use it. The description of the parameter ([Description("The genre...")]) helps the LLM generate the correct argument.
* public string GetMoviesByGenre(...): The method signature is simple C#. We return a string (JSON) because LLMs handle text-based data structures very well, though you can return complex objects too.
* _movies.Where(...): This is standard LINQ. The LLM has no access to this list unless it calls the function. This demonstrates how SK acts as a firewall/gateway between the LLM and your data.
kernel.Plugins.AddFromType<MoviePlugin>("MovieLibrary"): We explicitly tell the kernel about our class. We give it a logical name ("MovieLibrary") which helps the LLM namespace its tools. If you have multiple plugins, this prevents naming collisions.
OpenAIPromptExecutionSettings: This class configures the specific parameters for the API call.
* ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions: This is the "magic" setting.
* Without this: The LLM would just hallucinate an answer like "You might like 'Gone Girl'."
* With this: The LLM analyzes the prompt, sees it has access to MovieLibrary, and decides it must call GetMoviesByGenre("Mystery") to answer accurately. Semantic Kernel handles the networking and execution of that C# method automatically.
kernel.InvokePromptAsync(prompt, executionSettings): This is the single line that triggers the complex chain of events.
* Step A: The prompt and available functions are sent to the LLM.
* Step B: The LLM responds with a "Tool Call Request" (a structured JSON asking to run GetMoviesByGenre).
* Step C: SK detects this request, pauses the LLM call, and executes the C# method GetMoviesByGenre("Mystery").
* Step D: The C# method returns the JSON string of "Se7en".
* Step E: SK sends the JSON back to the LLM as a new message: "Function result: [...]".
* Step F: The LLM processes the result and generates the final natural language response.
6. Output
The final output will look something like this:
Assistant: Based on the movies in your library, I recommend Se7en. It is a mystery movie with a rating of 8.6.
Visualizing the Architecture
The following diagram illustrates the flow of data between the User, the Semantic Kernel, the LLM, and the Native Plugin.
Common Pitfalls
The Mistake: Defining a public method in a plugin class but forgetting to decorate it with [KernelFunction].
* The Consequence: The LLM will "know" the function exists (because you registered the class), but when it tries to call it, Semantic Kernel will throw an exception or simply fail to invoke it. The LLM might hallucinate that it performed the action, but your code never ran.
* The Fix: Always explicitly mark executable methods with [KernelFunction].
The Mistake: Writing [Description("Gets movies")] or leaving parameters undocumented.
* The Consequence: LLMs are probabilistic. If the description is vague, the LLM might hallucinate parameters or call the function in the wrong context (e.g., calling GetMoviesByGenre when the user asks for a specific actor, because it doesn't understand the parameter constraints).
* The Fix: Be extremely descriptive. Explain the input, the output, and the specific scenario where the function should be used.
The Mistake: Setting ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions blindly in a production environment without validation.
* The Consequence: Auto-invoke allows the LLM to execute code immediately. If the LLM is tricked (via prompt injection) into calling a destructive function (e.g., DeleteDatabase()), it will execute it without human approval.
* The Fix: For sensitive operations, use ToolCallBehavior.RequiredFunction or manually handle the FunctionCall objects returned by the LLM to add a confirmation step (e.g., "Do you want me to delete this?").
The Mistake: Using .Result or .Wait() on InvokePromptAsync in a UI application (like Blazor or ASP.NET Core).
* The Consequence: Deadlocks. Because the LLM call involves network I/O and potentially nested function calls, blocking the thread can freeze the application.
* The Fix: Always use await properly in modern C# async/await patterns. Ensure your entry point is async Task Main or an async controller action.
The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon
Loading knowledge check...
Code License: All code examples are released under the MIT License. Github repo.
Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.
All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.