Functions in Semantic Kernel — Native (code)
🧩 Native (code) functions
Native functions are just regular methods in your host language exposed to the Kernel. Choose native when you need precise control, enterprise auth flows, or non‑LLM logic (compute, IO, transformations).
Quick recap
- Pros: Deterministic, testable, full telemetry, strong typing, zero model cost, optimal for complex business rules.
- Cons: You write the code; less flexible than prompts for reasoning; more boilerplate than OpenAPI imports.
Minimal example (C#)
public class MathPlugin
{
[KernelFunction("add")] public double Add(double x, double y) => x + y;
}
var builder = Kernel.CreateBuilder();
builder.Plugins.AddFromType<MathPlugin>();
var kernel = builder.Build();
.NET DI lifetimes that matter
| Lifetime | When to use | Common pitfalls |
|---|---|---|
| Singleton | Cross‑app shared services: logging, config, caching, typed HttpClient factories | Don’t capture scoped services inside singletons |
| Scoped | Per‑request state: EF DbContext, user context, token acquisition | Creating scopes in background services (use IServiceScopeFactory) |
| Transient | Lightweight/stateless helpers | Excess allocations if created hot in loops |
Registration example:
services.AddSingleton<IClock, SystemClock>();
services.AddScoped<IUserContext, HttpUserContext>();
services.AddTransient<IEmailFormatter, EmailFormatter>();
Background worker pattern:
public class MyWorker(IServiceScopeFactory scopeFactory, ILogger<MyWorker> log) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// work with db here
await Task.Delay(TimeSpan.FromSeconds(30), token);
}
}
}
OBO (on‑behalf‑of) auth done right
Goal: the function calls downstream APIs with the user’s delegated access.
- Use
IHttpClientFactoryfor HTTP; register typed clients for each downstream API. - Acquire tokens via a scoped service (e.g.,
ITokenAcquisitionfrom Microsoft.Identity.Web). - Inject only abstractions into native functions; avoid ambient static state.
// Typed client for a downstream API
services.AddHttpClient<CrmClient>(client =>
{
client.BaseAddress = new Uri("https://api.contoso-crm.com/");
});
// Token scopes for that API
services.AddOptions<CrmOptions>().BindConfiguration("Crm");
public class CrmClient(HttpClient http, ITokenAcquisition tokenAcq, IOptions<CrmOptions> options)
{
private readonly string[] _scopes = [ options.Value.Scope ];
public async Task<Customer?> GetCustomerAsync(string id, CancellationToken ct = default)
{
var token = await tokenAcq.GetAccessTokenForUserAsync(_scopes);
http.DefaultRequestHeaders.Authorization = new("Bearer", token);
return await http.GetFromJsonAsync<Customer>($"customers/{id}", ct);
}
}
public record CrmOptions(string Scope);
Kernel function that uses the typed client:
public class CrmPlugin(CrmClient crm)
{
[KernelFunction]
public async Task<string> GetCustomerNameAsync(string id, CancellationToken ct = default)
{
var customer = await crm.GetCustomerAsync(id, ct);
return customer is null ? "Not found" : customer.Name;
}
}
Register plugin:
builder.Plugins.AddFromType<CrmPlugin>();
Key rules:
- Don’t inject scoped services into singletons. If a long‑lived component needs scoped services, create scopes when executing.
- Prefer typed clients over raw
new HttpClient(); they integrate with DI, handlers, and resiliency policies. - Cache tokens only per request if necessary; let Microsoft.Identity.Web handle refresh.
Resiliency: retries, timeouts, and circuit breakers (Polly)
Native functions are perfect places to add resilience policies.
services.AddHttpClient<CrmClient>()
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => (int)r.StatusCode >= 500)
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(200 * attempt)))
.AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)))
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.CircuitBreakerAsync(5, TimeSpan.FromMinutes(1)));
Telemetry tips:
- Log input sizes and correlation IDs, not PII; scrub secrets.
- Emit structured logs and traces around API calls; include retry/circuit metadata.
Native vs. OpenAPI
Choose OpenAPI if:
- The API has a good spec and simple auth; you want declarative import and quick wins.
Choose Native if:
- You need OBO/mTLS/custom claims, complex error handling, transformations, or deep observability.
Hybrid pattern: Start with OpenAPI; elevate critical calls to native with typed clients and policies.
Testability
- Keep function classes thin; inject interfaces for IO; mock in unit tests.
- Favor pure methods for transformations; add integration tests around clients.