Dependency Injection in Semantic Kernel

I would assume that most of the readers of this article are aware of Dependency Injection (DI) in the .NET framework. Semantic Kernel basically builds on the same DI patterns used in ASP.NET Core which means you can register your application services, repositories, and plugins through a similar pattern.
Please note that this article is primarily focused on the intricacies of Semantic Kernel and its architecture, concepts, and inner workings. The aim is not to be a step-by-step guide on how to build a chat application or use chat-based features. Prompt engineering or end-user conversational experiences are intentionally kept out of scope so we can dive deeper into how Semantic Kernel actually works under the hood.
I have an upcoming article on how to implement a conversational experience using the example detailed in this article.
The most important object in Semantic Kernel is the Kernel object. It acts as the central execution engine. When you create a kernel using Kernel.CreateBuilder(), you get access to an IServiceCollection via builder.Services. This allows you to register dependencies such as HTTP clients, database services, configuration settings, and your own business logic classes.
Once the kernel is built, those registrations are available through kernel.Services, which acts as the service provider for resolving dependencies at runtime.
KernelBuilder is where Dependency Injection starts
The most common pattern is:
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<IMyService, MyService>();
Kernel kernel = builder.Build();
In C#.Net you can use Dependency Injection to create a kernel. This is done by creating a ServiceCollection and adding services and plugins to it which we will see later in this article. At this point the kernel is fully configured and ready to run. Any service you register can be injected into plugin constructors or resolved later using kernel.Services.
To explain this more clearly, let’s build an interactive console application where the user can change a room’s light state while the underlying Kernel Functions remain abstracted from the user.
We will use https://spectreconsole.net/ to build a console application that provides rich and interactive console experience.
The documentation is available here : https://spectreconsole.net/console
To being with, we create a new C# Console application and add the following references/NuGet packages through the following .NET CLI commands.
dotnet add package Microsoft.Extensions.DependencyInjection --version 10.0.2
dotnet add package Microsoft.SemanticKernel --version 1.70.0
dotnet add Spectre.Console --version 0.54.0
Next, we create a simple interface called ILightService that defines three asynchronous methods TurnOnAsync TurnOffAsync and IsOnAsync
ILightService.cs
public interface ILightService
{
Task TurnOnAsync(string room);
Task TurnOffAsync(string room);
Task<bool> IsOnAsync(string room);
}
Next we define a class called LightService that implements this interface
LightService.cs
using Spectre.Console;
namespace SemanticKernelDependencyInjection
{
public class LightService : ILightService
{
public Task TurnOnAsync(string room)
{
AnsiConsole.WriteLine("");
AnsiConsole.WriteLine($"Turning ON lights in {room.Split(new[] { "::" }, StringSplitOptions.None)[0]}");
Thread.Sleep(2000);
return Task.CompletedTask;
}
public Task TurnOffAsync(string room)
{
AnsiConsole.WriteLine("");
AnsiConsole.WriteLine($"Turning OFF lights in {room.Split(new[] { "::" }, StringSplitOptions.None)[0]}");
Thread.Sleep(2000);
return Task.CompletedTask;
}
public Task<bool> IsOnAsync(string room)
{
// default OFF if room not found
return Task.FromResult(
Program.rooms.Any(r => r.StartsWith(room) && r.EndsWith("ON")));
}
}
}
Here comes the most important and interesting part.
How do we create a Semantic Kernel plugin that exposes the functions that LLM can call ? Read on.
LightsPlugin.cs
using System.ComponentModel;
using Microsoft.SemanticKernel;
namespace SemanticKernelDependencyInjection
{
public class LightsPlugin(ILightService lightService)
{
[KernelFunction("lights_on")]
[Description("Turns on the lights in the specified room.Room name, e.g., 'Kitchen' or 'Bedroom'.")]
public async Task<string> LightsOn(string room)
{
await lightService.TurnOnAsync(room);
return $"Lights turned ON in {room.Split(new[] { "::" }, StringSplitOptions.None)[0]}";
}
[KernelFunction("lights_off")]
[Description("Turns off the lights in the specified room.Room name, e.g., 'Kitchen' or 'Bedroom'.")]
public async Task<string> LightsOff(string room)
{
await lightService.TurnOffAsync(room);
return $"Lights turned OFF in {room.Split(new[] { "::" }, StringSplitOptions.None)[0]}";
}
[KernelFunction("get_light_state")]
[Description("Get the state of light in a room")]
public async Task<string> GetLightState(string room)
{
bool isOn = await lightService.IsOnAsync(room);
return isOn ? $"OFF" : $"ON";
}
}
}
Lets dissect the above code line by line
public class LightsPlugin(ILightService lightService)
We create a class called LightsPlugin which acts as a Plugin and uses a constructor injection where ILightService lightService is injected by Dependency Injection. It receives an implementation of ILightService from Dependency Injection.
[KernelFunction("lights_on")]
[Description("Turns on the lights in the specified room.Room name, e.g., 'Kitchen' or 'Bedroom'.")]
public async Task<string> LightsOn(string room)
{
await lightService.TurnOnAsync(room);
return $"Lights turned ON in {room}";
}
Above we have defined a KernelFunction("lights_on") that exposes a tool named “lights_on” that accepts argument of room type “Bedroom” or “Kitchen” etc. When the LLM decides to call this tool, Semantic Kernel automatically maps the tool call to the underlying C# method LightsOn(string room). Inside that method, we delegate the actual work to the injected service by calling: lightService.TurnOnAsync(room)
[KernelFunction("lights_off")]
[Description("Turns off the lights in the specified room.Room name, e.g., 'Kitchen' or 'Bedroom'.")]
public async Task<string> LightsOff(string room)
{
await lightService.TurnOffAsync(room);
return $"Lights turned OFF in {room}";
}
Same concept, but for turning lights off. Tool name becomes lights_off
[KernelFunction("get_light_state")]
[Description("Get the state of light in a room")]
public async Task<string> GetLightState(string room)
{
bool isOn = await lightService.IsOnAsync(room);
return isOn ? $"OFF" : $"ON";
}
Above code returns the state of the lights through the GetLightState method for a given room.
Now that we have the plugins built, our next step would be to build the kernel
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<ILightService, LightService>();
builder.Services.AddSingleton<LightsPlugin>();
Kernel kernel = builder.Build();
kernel.Plugins.AddFromObject(kernel.Services.GetRequiredService<LightsPlugin>(), "Lights");
Lets dissect the above code line by line
var builder = Kernel.CreateBuilder();
We created a Kernel builder in the Semantic Kernel
builder.Services.AddSingleton<ILightService, LightService>();
We registered ILightService → LightService as a singleton. You might ask why Singleton?
This is because, the LightService is stateless in this example and does not store any per-user data. It simply performs actions like TurnOnAsync(room) ,TurnOffAsync(room) and GetLightState(room). Since there is no per-request state, creating a new instance every time is unnecessary overhead.
builder.Services is an IServiceCollection and we register LightService into it so that whenever ILightService is requested (for example by LightsPlugin via constructor injection), Dependency Injection can provide the same singleton instance.
builder.Services.AddSingleton<LightsPlugin>();
We then register LightsPlugin (singleton, with constructor injection)
Kernel kernel = builder.Build();
Here we build the Kernel
kernel.Plugins.AddFromObject(kernel.Services.GetRequiredService<LightsPlugin>(), "Lights");
The above code adds the plugin instance (resolved from DI) into kernel.Plugins.kernel.Services is the service provider that was built when we did builder.Build().
GetRequiredService<LightsPlugin>() means: Give me the LightsPlugin instance from Dependency Injection.
Since LightsPlugin has a constructor dependency:
public class LightsPlugin(ILightService lightService)
Dependency injection will automatically inject ILightService (which is LightService).
Now that we have the Dependency injection, Plugins and Functions in place lets move to functional side to see everything working together.
Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using SemanticKernelDependencyInjection;
using Spectre.Console;
internal class Program
{
public static List<string> rooms = new();
private async static Task Main(string[] args)
{
rooms.Add("Living Room :: OFF");
rooms.Add("Kitchen :: OFF");
rooms.Add("Bedroom :: OFF");
rooms.Add("Bathroom :: OFF");
rooms.Add("Dining Room :: OFF");
rooms.Add("Study :: OFF");
rooms.Add("Garage :: OFF");
rooms.Add("Balcony :: OFF");
rooms.Add("Guest Room :: OFF");
rooms.Add("Store Room :: OFF");
var builder = Kernel.CreateBuilder();
builder.Services.AddSingleton<ILightService, LightService>();
builder.Services.AddSingleton<LightsPlugin>();
Kernel kernel = builder.Build();
kernel.Plugins.AddFromObject(kernel.Services.GetRequiredService<LightsPlugin>(), "Lights");
await SetMenu(rooms, kernel);
}
public async static Task SetMenu(List<string> rooms, Kernel kernel)
{
var prompt = new SelectionPrompt<string>()
.Title("Select a [green]room[/]")
.PageSize(20)
.EnableSearch()
.SearchPlaceholderText("Type to filter...")
.AddChoices(rooms);
prompt.SearchHighlightStyle = new Style(Color.Yellow, decoration: Decoration.Underline);
var room = AnsiConsole.Prompt(prompt);
var state = await kernel.InvokeAsync("Lights", "get_light_state", new() { ["room"] = room });
AnsiConsole.MarkupLine($"The room you selected is: [blue]{room.Split(new[] { "::" }, StringSplitOptions.None)[0]}[/]");
AnsiConsole.WriteLine();
var confirmation = AnsiConsole.Prompt(
new TextPrompt<bool>($"Do you want to switch {(state.GetValue<string>() =="OFF" ? "[red]OFF[/]" : "[green]ON[/]")} the lights ?")
.AddChoice(true)
.AddChoice(false)
.DefaultValue(true)
.WithConverter(choice => choice ? "yes" : "no"));
if (confirmation == true && room.Split(new[] { "::" }, StringSplitOptions.None)[1] == " OFF")
{
var result = await kernel.InvokeAsync(pluginName: "Lights", functionName: "lights_on", arguments: new KernelArguments { ["room"] = room });
int index = rooms.FindIndex(r => r.StartsWith(room));
if (index >= 0)
{
rooms[index] = "";
rooms[index] = $"{room.Split(new[] { "::" }, StringSplitOptions.None)[0].Trim()} :: ON";
}
}
if (confirmation == true && room.Split(new[] { "::" }, StringSplitOptions.None)[1] == " ON")
{
var result = await kernel.InvokeAsync(pluginName: "Lights", functionName: "lights_off", arguments: new KernelArguments { ["room"] = room });
int index = rooms.FindIndex(r => r.StartsWith(room));
if (index >= 0)
{
rooms[index] = "";
rooms[index] = $"{room.Split(new[] { "::" }, StringSplitOptions.None)[0].Trim()} :: OFF";
}
}
AnsiConsole.Clear();
await SetMenu(rooms, kernel);
}
}
The idea above is to provide an interactive console screen where the user can change the state of lights of a particular room by invoking the relevant kernel functions.
In the following screencast, you can see an interactive console where the light status is updated based on the user’s selection. The selection invokes the corresponding kernel function.

Conclusion
In this article I have tried to highlight how dependency injection works within Semantic Kernel model, specifically how services can be registered in the kernel’s services and how they are resolved and injected into Kernel Functions during execution. The focus was more on understanding the underlying schematics of the service lifetimes, plugins and function invocations rather than on building end-to-end chat applications.
Thanks for reading !!!




