Chapter 03 · Architecture

The Big Picture

Before you read any single file, build a mental map of where things live. This codebase follows Clean Architecture — a deliberate set of dependency rules that keeps business logic independent of frameworks. We'll cover what each layer owns, the rules that govern them, and the "where does X live?" lookup table you'll come back to often.

Clean Architecture in one paragraph

The codebase is divided into four concentric layers. Domain at the center knows nothing about frameworks, databases, or HTTP. Application wraps Domain with use-cases (commands, queries, interfaces) but still imports nothing concrete. Infrastructure provides concrete implementations of Application interfaces — Azure OpenAI clients, MCP servers, file system access. Presentation is the entry point — console UI, SignalR hub, REST API, MCP server — that wires everything together. Dependencies point only inward.

Why this matters

When the business rule "an agent must check the budget before calling a tool" lives in Domain, it survives any change to the LLM provider, the UI, or the database. You can swap Azure OpenAI for Claude or Console for SignalR without touching it. The opposite — business logic tangled into a Razor view or a SQL stored procedure — is the shape of code that becomes legacy.

The most important distinction: .Common vs .Core

Before we get to the layers, there's one naming convention you have to internalize early or you'll spend weeks confused. Every layer has projects whose names end in .Common (and sometimes .AI.Common) — and at least one project that is something else, like Application.Core. They mean very different things.

The dog-walker example, made concrete

Imagine you forked this template to build "PupWalk" — an AI-powered dog-walking scheduler. The pieces would shake out like this:

  • Domain.Common, Domain.AI, all the *.Common projects — unchanged. Same harness.
  • Application.Core — you'd add (or replace with) commands like ScheduleWalkCommand, CancelBookingCommand, FindAvailableWalkersQuery.
  • You'd add a new Domain.PupWalk project for dog/walker/booking entities.
  • You'd add a new Infrastructure.PupWalk for the booking-database EF DbContext and Stripe payment client.
  • Presentation.ConsoleUI stays (great for ops & debugging) plus you add a Presentation.PupWalkApi for the customer-facing REST endpoints.
  • Your skills go in skills/ at the repo root: skills/walk-coordinator/SKILL.md, skills/customer-support/SKILL.md.

Throughout this guide, when we say "you'll edit this" we usually mean a .Core project, an infrastructure.<your-domain> project, or a Markdown file in skills/ or agents/. When we say "the harness does X" we mean code in a .Common project that you should treat as a black box — understand it, but don't fork it unless you have a very good reason.

A useful rule of thumb

Before editing anything in a .Common project, ask: "would another team forking this template also benefit from my change?" If yes, edit it (and add a test). If no — if the change is specific to your application — it belongs in .Core or a new domain-specific project. Keeping that line clean is what lets the harness keep being a useful template.

The four layers

Presentation
Presentation.ConsoleUI · Presentation.AgentHub (SignalR) · Presentation.ExecutionApi (REST) · Presentation.Dashboard · Presentation.WebUI · Presentation.EvalRunner · Presentation.FoundryHost · Presentation.LoggerUI · Presentation.Common · Infrastructure.AI.MCPServer (WebAPI host)
Infrastructure
Infrastructure.AI · Infrastructure.AI.MCP · Infrastructure.AI.Connectors · Infrastructure.AI.RAG · Infrastructure.AI.Evaluation · Infrastructure.AI.Governance · Infrastructure.AI.KnowledgeGraph · Infrastructure.Common · Infrastructure.APIAccess · Infrastructure.Observability
Application
Application.Common · Application.AI.Common · Application.Core (CQRS commands)
Domain
Domain.Common · Domain.AI

Domain

Pure C#. No NuGet dependencies beyond System.*. Defines the language of the problem: AgentManifest, SkillDefinition, ToolDeclaration, WorkflowState, AgentCard, Result<T>, all the strongly-typed config classes. If you can describe a concept on a whiteboard without mentioning a vendor or framework, it lives here.

Application

The "what should happen" layer. CQRS commands like ExecuteAgentTurnCommand, interfaces like IAgentFactory and ISkillMetadataRegistry, FluentValidation validators, MediatR pipeline behaviors, the AIToolConverter that bridges internal tools to Microsoft.Extensions.AI. Application can depend on Domain, but it must never depend on Infrastructure — it only knows interfaces.

The litmus test

If a file imports anything beyond System.*, Microsoft.Extensions.*, MediatR, or other Application/Domain types, it doesn't belong in Application. Push it to Infrastructure.

Infrastructure

The "how it actually happens" layer. ChatClientFactory talks to Azure OpenAI. FileSystemService reads disk under sandbox rules. RagPipeline coordinates ingestion and retrieval. McpClient connects to external MCP servers. Each Infrastructure project implements one or more interfaces declared in Application.

Presentation

Where execution starts. A "Presentation host" is just a runnable program with a Program.cs. The harness ships several — the full set is in the layer diagram above — and each is a different way of getting a request into the same engine. The four you are most likely to run first:

  • Presentation.ConsoleUI — you type at a terminal menu. The simplest entry point, and the one to reach for when debugging.
  • Presentation.AgentHub — a SignalR (WebSocket) server, so a browser can stream a conversation token-by-token.
  • Presentation.ExecutionApi — a plain REST API. Another system uploads a zipped agent and runs it over HTTP, with no UI involved. See Chapter 17.
  • Infrastructure.AI.MCPServer — exposes the harness's own tools to other MCP clients over HTTP. (It lives in an Infrastructure folder for historical reasons, but it behaves as a Presentation host.)

Every one of these runs the same composition root — services.GetServices(...) in Presentation.Common — which calls every layer's Add*Dependencies() extension. That is why a feature you add once shows up in all of them.


The dependency rule, illustrated

Solid arrows are "depends on." The arrow always points inward:

dependency direction
Presentation ──▶ Application ──▶ Domain
     │                ▲
     │                │
     └──▶ Infrastructure ──▶ Application interfaces
                       └────▶ Domain (read-only)

Two things to internalize:

  • Infrastructure depends on Application, not the other way around. Infrastructure.AI references Application.AI.Common to get the IChatClientFactory interface — and then provides the concrete class. Application never knows the concrete exists.
  • Presentation is the only layer that touches everything. It does the wiring — calls AddDomainDependencies(), AddApplicationDependencies(), AddInfrastructureDependencies(), then builds the service provider.

Where to find anything

Use this table when you know what but not where:

If you're looking for...Look in...
A configuration class (every AppConfig.* property) Domain.Common/Config/
An agent manifest type, skill definition, tool declaration Domain.AI/Agents/, Domain.AI/Skills/, Domain.AI/Tools/
A CQRS command and its handler Application.Core/CQRS/<feature>/
An interface for an AI service Application.AI.Common/Interfaces/
A MediatR pipeline behavior Application.*.Common/MediatRBehaviors/
The Azure OpenAI / OpenAI client wiring Infrastructure.AI/Factories/ChatClientFactory.cs
A built-in tool (file system, calculator, etc.) Infrastructure.AI/Tools/
MCP server endpoint code Infrastructure.AI.MCPServer/
The REST API for running an uploaded agent bundle Presentation.ExecutionApi/ (guide: Ch. 17)
MCP client (consuming external MCP servers) Infrastructure.AI.MCP/
The RAG ingestion + retrieval pipeline Infrastructure.AI.RAG/
OpenTelemetry setup, exporters, span processors Infrastructure.Observability/
DI registration for any layer <Project>/DependencyInjection.cs
SKILL.md files (runtime skill definitions) skills/ at the repo root
AGENT.md files (agent manifests) agents/ at the repo root
Tests src/Content/Tests/<project>.Tests/ — one per source project

DI registration: the composition root

Every project has its own DependencyInjection.cs with an Add{Layer}Dependencies() extension method. The Presentation layer's services.GetServices(...) calls them in order:

C# · composition root pattern
// In Presentation.Common — every Presentation host calls this
services
    .AddDomainCommonDependencies(appConfig)
    .AddDomainAIDependencies(appConfig)
    .AddApplicationCommonDependencies(appConfig)
    .AddApplicationAICommonDependencies(appConfig)
    .AddApplicationCoreDependencies(appConfig)
    .AddInfrastructureCommonDependencies(appConfig)
    .AddInfrastructureAIDependencies(appConfig)
    .AddInfrastructureMCPDependencies(appConfig)
    .AddInfrastructureRAGDependencies(appConfig)
    .AddInfrastructureObservabilityDependencies(appConfig);

When you add a new service, you don't touch the composition root — you add the registration to the relevant layer's DependencyInjection.cs. It gets picked up the next time the host boots.

What's in each Infrastructure project

Infrastructure has the most projects. Here's a quick mental map:

Infrastructure.AI
The agent runtime — chat clients (ChatClientFactory), sandboxed file/calculation tools, state management, A2A host. Note the AgentFactory itself lives in Application.AI.Common/Factories/, not here.
Infrastructure.AI.MCP
The MCP client — discovers tools on external MCP servers and wraps them as ITool.
Infrastructure.AI.MCPServer
The MCP server — ASP.NET Core WebAPI that exposes this app's tools, prompts, and resources over MCP/HTTP.
Infrastructure.AI.RAG
Document ingestion, chunking, indexing, retrieval, reranking. The whole RAG pipeline.
Infrastructure.AI.Connectors
Adapters for GitHub, Jira, Azure DevOps, Slack — unified behind IConnectorClient.
Infrastructure.AI.Governance
Policy engine, prompt injection detection, escalation, audit logging.
Infrastructure.AI.KnowledgeGraph
Graph-backed knowledge storage — shipped with Neo4j, Kuzu, and PostgreSQL backends and Leiden community detection.
Infrastructure.Observability
OpenTelemetry pipeline, Jaeger and Prometheus exporters, LLM-aware span processor.
Infrastructure.APIAccess
HTTP resilience policies, retry/circuit-breaker for outgoing HTTP, security middleware.
Infrastructure.Common
Identity service, claim extensions — generic infrastructure not specific to AI.

Two rules to internalize

1 · Direction of the arrow

Before you add a project reference, ask: does this point inward? Application → Domain ✓. Infrastructure → Application ✓. Domain → anything ✗. Application → Infrastructure ✗.

The litmus: open .csproj, look at the <ProjectReference> lines. They should always point at a layer the same color or further left (Domain) than the current project's color in the stack diagram above.

2 · Interfaces live with the consumer

If Application.AI.Common needs a chat client, it defines IChatClientFactory in its own Interfaces/ folder. Infrastructure.AI provides the implementation. Don't put the interface in Infrastructure "near the implementation" — that breaks the dependency rule.


Where to go from here