Module 7 — Separation of concerns: where AI touches your architecture
Prompt & Tool Design for .NET Teams · Part 2 — Semantic Kernel in .NET · Module 7 of 12
This is the module that decides whether you enjoy this codebase in a year. Left unchecked, AI code spreads: a model ID hard-coded in a controller here, a prompt concatenated into a service there, and suddenly “swap the model” is a three-day refactor. The fix is boring and effective: put the boundaries in on purpose, now, while the codebase is still small enough to argue about.
Objective
Define the architectural boundaries so AI code stays in its lane instead of seeping through the whole solution.
Read (~12 min)
- Microsoft.Extensions.AI — the
IChatClientabstraction Semantic Kernel (SK) itself is converging on. - Semantic Kernel filters, for cross-cutting concerns like logging, personally identifiable information (PII) redaction, and approval gates.
The layering to adopt
Discuss this as a team. The disagreements are the useful part.
- Domain — owns business logic and the inner services your plugins wrap. Never holds prompts, model IDs, or SK types.
- AI orchestration — owns kernel setup, plugins (thin adapters over domain services), prompt templates as embedded resources, and filters. Never holds business rules.
- Infrastructure — owns model IDs, region, credentials, and guardrail Amazon Resource Names (ARNs); it is all configuration, and hard-codes nothing.
Lab (~18 min)
- prompts move out of string literals into
/Prompts/*.mdembedded resources; - the model ID moves into
appsettings; - add one
IFunctionInvocationFilterthat logs every tool call with its arguments and duration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
public sealed class LoggingFilter(ILogger<LoggingFilter> logger)
: IFunctionInvocationFilter
{
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
var sw = Stopwatch.StartNew();
logger.LogInformation("Invoking {Plugin}.{Function}",
context.Function.PluginName, context.Function.Name);
await next(context); // the function actually runs here
logger.LogInformation("Invoked {Plugin}.{Function} in {Elapsed}ms",
context.Function.PluginName, context.Function.Name, sw.ElapsedMilliseconds);
}
}
Register it through dependency injection (DI) on the kernel builder:
1
kernelBuilder.Services.AddSingleton<IFunctionInvocationFilter, LoggingFilter>();
Then write a one-page Architecture Decision Record (ADR) — “How AI components integrate with our solution” — and commit it as module-07/adr-001-ai-boundaries.md. Future hires will read this instead of asking you.
Done when
Changing model or provider is a config change, and deleting the AI orchestration project doesn’t break the domain layer’s compile. If the domain project won’t build without the AI project, the boundary is on paper only.