Semantic Kernel for .NET and Java Teams
9 min read · updated August 4, 2026
Semantic Kernel is not the .NET translation of a Python framework. It exists because a large enterprise estate already has dependency injection, typed configuration, structured logging and distributed tracing, and the cost of adopting an AI library that ignores all four is higher than the cost of the AI code itself. That is the reason to choose it and the standard to judge it by.
Why it exists
If your services are C# or Java, the practical alternative to Semantic Kernel is calling provider REST APIs directly, which is entirely viable — the request shapes are simple and the SDKs are decent. What you give up is not the HTTP call. It is the plumbing around it: a uniform way to register model clients in the container, resolve them per scope, configure them from the same options system as everything else, and have their calls appear in the same traces as your database queries.
That framing also tells you when not to use it. A single service making one prompt call does not need a kernel; it needs an HTTP client. The library earns its place when there are several AI capabilities, several teams and an existing platform to fit into.
The kernel is a service container
The central object holds registered AI services and registered functions, and resolves both at invocation time. In .NET it is built with the same builder pattern as the rest of the framework and registers into the same service collection, which is the whole point.
// C#. The builder and kernel concepts have been stable; exact
// extension-method names for registering a provider differ by package
// version, so check the ones your package exposes.
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(modelId: MODEL, apiKey: KEY);
builder.Services.AddLogging(b => b.AddConsole());
builder.Plugins.AddFromType<OrdersPlugin>();
Kernel kernel = builder.Build();
var result = await kernel.InvokePromptAsync(
"Summarise the status of order {{$orderId}} for a customer email.",
new KernelArguments { ["orderId"] = orderId });Two details generalise beyond the snippet. Prompts are templates with named arguments, and those templates can live in files rather than in code — which matters because prompts change on a different cadence from the services around them. And the kernel is registerable in the application’s own container, so a request-scoped kernel with request-scoped plugins is the natural shape rather than an afterthought.
Plugins are annotated methods
The best idea in the library. A plugin is an ordinary class; a function is an ordinary method with an attribute and a description. The framework turns the method signature into a schema the model can call, using the type system you already have rather than a hand-written JSON Schema.
public sealed class OrdersPlugin(IOrderRepository repo)
{
[KernelFunction, Description("Get the current status of a customer order.")]
public async Task<OrderStatus> GetStatusAsync(
[Description("The order id, e.g. ORD-10482")] string orderId)
=> await repo.GetStatusAsync(orderId);
}Three consequences worth naming. Dependencies arrive by constructor injection, so a plugin can use your repository, your cache and your authorisation service like any other class. The parameter descriptions are prompt text delivered through the schema, so they deserve the attention described in tool description design. And the return type is your domain type, serialised for the model — which means an object with fifty properties sends fifty properties into the context. Return a purpose-built small record, not your entity.
The authorisation point deserves emphasis because the ergonomics hide it. A plugin method is reachable by the model whenever it is registered on that kernel. Whatever identity the request is running as, the model can invoke it with arguments it chose. Check permissions inside the method against the caller’s identity, exactly as you would for a public endpoint — the reasoning is in secure tool calls.
Function calling replaced planners
Early Semantic Kernel had planners: components that took a goal and produced a plan over the available functions, either as a sequence or as generated code. They were the library’s headline feature and they are largely historical now, superseded by models that call functions natively in a loop.
This matters for two practical reasons. First, a great deal of tutorial material written about this library is about planners, and adapting it wastes time on an approach that has been deprioritised. Second, the modern shape is the ordinary agent loop: enable automatic function invocation in the execution settings, and the kernel runs the model, executes any requested functions, feeds results back and repeats. The behaviour is the same loop described in the agent loop, with the kernel supplying the plumbing.
The integration paths that matter
| Platform concern | Description |
|---|---|
| Dependency injection | Register model clients and plugins in the application's container; resolve a kernel per scope. This is what lets a plugin hold a request-scoped database context without a global. |
| Configuration | Model ids, endpoints and deployment names belong in the same options system as every other setting, bound and validated at startup. Hardcoded model strings are the thing you will most want to change during an incident. |
| Logging and telemetry | The library emits through the platform's standard logging and activity APIs, which means model calls appear as spans alongside HTTP and database calls in whatever collector you already run. This is the single largest practical advantage over rolling your own client. |
| Resilience | Retries, timeouts and circuit breaking are handled by the platform's standard HTTP resilience stack rather than by an AI-specific mechanism — one policy for all outbound calls. See circuit breakers. |
| Secrets and identity | Managed identity or the platform's secret store rather than an API key in configuration. Worth doing at the start; retrofitting it means touching every registration site. |
If those five are already solved in your estate, they are the reason to adopt this library, and none of them appear in a quickstart. The equivalent list for a Python service is why circuit breakers and observability get chosen separately there.
Testing plugins without a model
The strongest practical consequence of plugins being ordinary classes is that most of what you build is testable with the test framework you already use, at no cost and with no model call. Three layers, in increasing expense.
- Unit-test the methods directly. A kernel function is a method. Call it with arguments, assert on the result, mock its injected dependencies. No kernel, no provider, no key. This should be the overwhelming majority of your tests and it is the reason to keep business logic in the method rather than in the prompt.
- Contract-test the exposed schema. Build the kernel, enumerate the registered functions, and assert that each has a non-empty description and that every parameter has one too. A missing description is a silent quality regression — the model simply chooses worse — and it is exactly the kind of thing that slips in during a refactor. One test catches every future instance.
- Test the loop against a stub. Register a fake chat service that returns a scripted sequence: a function call, then a final answer. This exercises your wiring, your error handling and your turn limit deterministically and for nothing. The general approach is testing without the model.
- Then a small evaluation set against the real model. Nightly rather than per-commit, over inputs where you know which function should be chosen. This is the only layer that costs money and the only one that catches “the model stopped picking the right tool after we reworded a description”.
The second layer is the one teams skip and the one that pays most, because tool descriptions are prompt text living in attributes where no reviewer thinks to look at them as prose.
What to watch
Language parity is uneven. The .NET implementation leads; Java and Python implementations follow at varying distances, and a feature demonstrated in one is not necessarily available in another. Check the language you are actually shipping.
Preview surfaces move. Agent and process abstractions have been through several preview iterations. Anything marked experimental should be assumed to change; keep it behind an interface of your own if you use it at all.
Serialised return types are a context cost. Because returning a domain object is so easy, it is easy to send far more into the model’s context than intended. A function returning a list of entities can consume thousands of tokens per call without anything in the code looking wrong. Return records shaped for the model, and count what you are sending.