🔥 Pragmatic .NET Code Rules Course is on Presale - 50% off!BUY NOW
AI ROADMAP COURSE · TRACK A

Use AI to build .NET faster

The fastest wins in the whole roadmap: make Claude Code write your .NET, then turn it into a specialist with skills and agents. Real code, no theory.


PART 1 · LESSON 1 OF 5

Make Claude Code write your .NET

By the end of this lesson you'll have an AI assistant that writes .NET the way you write it - matching your stack, architecture, and conventions on the first try. Most developers get this wrong.

The problem

You ask Claude Code for one endpoint. You get back a repository interface, an AutoMapper profile, DateTime.Now, and a folder structure from someone else's project. The code isn't wrong - it's just not your code.

The key insight: it's not that the AI doesn't know .NET. It starts every session knowing nothing about your repo, so it fills the gap with the most common .NET code it has seen. The fix isn't a better prompt every time - it's giving the AI context, once.

Step 1 - Install Claude Code

bash
# install, then run inside any project folder
npm install -g @anthropic-ai/claude-code
claude

This lesson uses Claude Code, but the CLAUDE.md idea maps to Cursor's rules and Copilot's instructions too. Open it inside a real solution - you learn this on code you care about.

Step 2 - The one file that changes everything: CLAUDE.md

CLAUDE.md is a markdown file at your repo root. Claude Code loads it automatically at the start of every session, before your first prompt. Think of it as the briefing you'd give a new senior dev joining your team. It answers four questions:

  1. What's the stack? (exact versions)
  2. How is the project structured? (architecture + folders)
  3. What are the conventions? (the patterns you use)
  4. What should it never do? (the patterns you don't use)
md
# Project: Orders API

## Stack
- .NET 10 / C# 14 · ASP.NET Core Minimal APIs
- EF Core 10 + PostgreSQL · FluentValidation · xUnit + Testcontainers

## Architecture
- Vertical Slice: everything for one feature in Features/[Feature]/.

## Conventions
- Result pattern, not exceptions for control flow.
- Inject TimeProvider - never DateTime.Now.
- TypedResults; DTOs are records; never expose EF entities.
- CancellationToken on every async method.

## Never suggest
- AutoMapper - write explicit mappings.
- Repository/UnitOfWork over EF Core - DbContext already is one.
- Exceptions for expected outcomes (not-found, validation).
- In-memory database in tests - use Testcontainers.
💡 Don't write it by hand
Use the free CLAUDE.md Generator - pick your stack and it builds this file for you. But understand why each line is there first.
⛔ The "Never suggest" section is the secret
"Never wrap EF Core in a repository." "Never use AutoMapper." These block the wrong default before Claude walks down that path - far cheaper than reviewing and reverting it afterward. This one section saves more review time than anything else in the course.

Step 3 - Prompt for real work

  • Describe the outcome, not the steps. Not "make a controller + service + repository" - but "add a create-product endpoint, return a Result, follow the existing features."
  • Ask for a plan before edits on anything non-trivial: "give me a short plan and wait for my OK." Catches wrong assumptions before they become wrong code.
  • Tell it to leave good code alone: "Don't invent problems."

Worked example

✗ Without CLAUDE.md
C#
public class OrdersController : ControllerBase
{
  private readonly IOrderRepository _repo;
  [HttpPost] public async Task<IActionResult>
  Create(CreateOrderDto dto){
    var o = new Order{ CreatedAt = DateTime.Now };
    await _repo.AddAsync(o);
    return Ok(o); // leaks the entity
  }
}
✓ With CLAUDE.md
C#
public sealed class CreateOrderEndpoint : IEndpointGroup
{
  public void Map(IEndpointRouteBuilder app) =>
    app.MapPost("/api/orders", Handle)
       .Produces<OrderResponse>(201)
       .ProducesValidationProblem();

  static async Task<Results<Created<OrderResponse>,
    ValidationProblem>> Handle(CreateOrderRequest req,
    IOrderService svc, TimeProvider time,
    CancellationToken ct) => ...
}

Same prompt. Different universe. That's the CLAUDE.md at work.

Where CLAUDE.md lives (and its friends)

They stack - so you can set global preferences once and override per project:

FileScopeUse it for
~/.claude/CLAUDE.mdYou, every projectYour personal defaults (e.g. "always explain your plan first")
CLAUDE.md (repo root)The whole teamStack, architecture, conventions - commit it so everyone shares it
CLAUDE.local.mdJust you, this repoPersonal notes for this project - add it to .gitignore

Use Plan Mode for anything risky

Claude Code has a plan mode: it proposes a full plan and waits for your approval before touching a single file. Use it for migrations, refactors, or anything that spans several files. It's the difference between "AI did something to my repo" and "AI did exactly what I approved."

Prompting cheat sheet

Instead of…Say…
"Make this better""Optimize this for readability and remove the N+1 - don't change behavior"
"Add auth""Add JWT bearer auth with policy-based authorization; secrets in config"
"Fix the bug""Here's the exception + stack trace. Find the root cause and give the minimal fix - no try/catch that hides it"
"Write tests""Write integration tests with WebApplicationFactory + Testcontainers: happy path, validation, not-found"
"Refactor this""Refactor to the Result pattern; leave anything already idiomatic alone"

The pattern: state the outcome, name the constraints, and say what to avoid.

The bad defaults → what they should become

These are the patterns your CLAUDE.md should steer toward - and the raw material for a sharp "Never suggest" section.

AI's common defaultWhat you want insteadWhy
DateTime.NowTimeProviderTestable, deterministic time
Repository over EF CoreDbContext directlyDbContext is already a UoW + repository
AutoMapperExplicit mapping / projectionNo hidden reflection; project in EF queries
Exceptions for not-foundResult pattern + ProblemDetailsExpected outcomes aren't exceptional
new HttpClient()IHttpClientFactoryAvoids socket exhaustion
Manual cache serializeHybridCacheL1+L2, tag invalidation, stampede protection
Results.Ok(...)TypedResultsTyped, testable, better OpenAPI
UseInMemoryDatabase in testsTestcontainersReal engine catches real bugs
⚠️ Common mistakes
  • No CLAUDE.md at all → generic output → you blame the AI. The #1 mistake.
  • A CLAUDE.md that's docs for humans, not instructions for the AI. Keep it concise and imperative.
  • Accepting code you don't understand - you're still responsible.
  • Skipping the "Never suggest" section - the highest-value part.
✅ Your exercise
  1. Add a CLAUDE.md to a real project.
  2. Fill in "Never suggest" with the 3 patterns you're tired of correcting.
  3. Do one backlog task fully with Claude Code - plan first, then edits.
  4. Post what changed in the community feed.
Recap
Claude starts every session blind to your repo - CLAUDE.md fixes that. Cover stack, architecture, conventions, and (most importantly) what to never suggest. Prompt for outcomes, ask for a plan, leave good code alone.

Next → Part 2: make the assistant a specialist with skills and agents.

PART 2 · LESSON 2 OF 5

Turn the assistant into a .NET specialist

In Part 1 you gave Claude context. Now, instead of re-explaining a task every time, you give it reusable capabilities it loads automatically. That's what skills and agents are.

Skill vs. agent - the 20-second version

  • A skill is a focused capability Claude loads automatically when your request matches ("optimize this EF query", "scaffold an endpoint"). You don't call it - you describe what you want.
  • An agent explores your codebase on its own and produces a report ("audit the security of this API"). You invoke it by asking for what it does.
💡 Rule of thumb
Skill = "do this specific thing." Agent = "look at my repo and tell me what's wrong."

Step 1 - Install a set of skills

bash
/plugin marketplace add StefanTheCode/dotnet-ai-toolkit
/plugin install dotnet-ai-toolkit@thecodeman-ai-toolkit

Then just talk to Claude Code normally and the matching skill kicks in - no command to remember:

bash
> This EF query is slow, optimize it
> Scaffold a products endpoint
> Write integration tests for the checkout flow
> Review this PR for .NET antipatterns

Each skill carries a fixed checklist, so output is consistent instead of depending on how you phrased the prompt. Start with the 5 you'll use daily - don't memorize 44.

Where skills & agents live

LocationAvailable in
~/.claude/skills/ · ~/.claude/agents/Every project (personal)
.claude/skills/ · .claude/agents/ (in the repo)Just this project - commit to share with the team
A plugin marketplace (like the ToolKit)Every project, and you get updates with one command

Reopen the session after adding a skill so it loads. Update marketplace skills anytime with /plugin marketplace update thecodeman-ai-toolkit.

Step 2 - How a skill works (so you can write one)

A skill is a folder with a SKILL.md: YAML frontmatter (the trigger) + instructions (what to do).

md
---
name: ef-core-query-optimizer
description: Optimize EF Core queries. Use whenever the user shares
  EF Core / LINQ code or mentions N+1, AsNoTracking, projections...
---

# EF Core Query Optimizer
Run this checklist in order:
1. Projection - .Select() to a DTO if only some columns are needed
2. Tracking - .AsNoTracking() for read-only queries
3. N+1 - detect lazy loading in loops
4. Async + CancellationToken on every query
Output: the rewritten query + one line per change. Leave optimal code alone.
  • The description is the trigger. Make it specific and slightly pushy about when to fire. Vague description → skill never triggers.
  • The body is a checklist, not an essay. Concrete steps + BAD/GOOD examples + a fixed output format = consistent results.
Anatomy of a description that actually triggers
Weak (rarely fires)Strong (fires reliably)
"Helps with EF Core.""Use whenever the user shares EF Core / LINQ code, a DbContext, or a slow query, or mentions N+1, AsNoTracking, projections, cartesian explosion. Always use this for EF performance instead of answering from memory."

Name the triggers (the words and situations), list synonyms, and be a little pushy ("Always use this for…"). A references/ folder can hold extra material that loads only when the skill is active - keeping your main context light.

Step 3 - Write your own skill

Pick one task you repeat weekly. Create .claude/skills/my-endpoint/SKILL.md:

md
---
name: my-endpoint
description: Scaffold a new Minimal API endpoint in our style. Use when
  the user wants to add an endpoint, a route, or a new feature slice.
---

# New Endpoint
Create a full vertical slice in Features/[Feature]/:
- Request/Response records (never expose EF entities)
- FluentValidation validator + endpoint filter
- Handler returning Result<T>
- IEndpointGroup with TypedResults + OpenAPI metadata
- CancellationToken threaded through
- One integration test (WebApplicationFactory + Testcontainers)

Reopen the session, type "add a create-customer endpoint" - your skill fires. You just encoded your team's standard once, forever.

Step 4 - Use an agent

When you want a review of the whole codebase, ask for what the agent does:

bash
> Audit the security of this API
> Review this PR like a senior .NET engineer
> Find the architecture problems in this solution

The agent explores your code on its own and returns a ranked report - Critical / Should-fix / Nit - with a fix for each. The fastest second pair of eyes you'll ever have.

An agent is a single .md file with frontmatter that defines its role, the tools it may use, and (optionally) the model:

md
---
name: aspnetcore-security-auditor
description: Audits an ASP.NET Core codebase against the OWASP Top 10 and
  .NET-specific risks. Use for a security review or "is my API secure".
tools: Read, Glob, Grep, Bash
model: inherit
---

# ASP.NET Core Security Auditor
Walk the endpoints. Check authorization, injection, secrets in source,
CORS, mass assignment, vulnerable dependencies.
Output a ranked report (Critical / Should-fix / Nit) with a fix for each.

Level up - slash commands & hooks

Slash commands wrap a whole workflow behind one command. Instead of describing the steps, you type /scaffold and it runs the right skills and agents in order. A command is just a markdown file in .claude/commands/:

md
# .claude/commands/scaffold.md
Scaffold a complete vertical-slice feature: endpoint, validation,
Result handling, OpenAPI metadata, CancellationToken, and one
integration test. Match the existing features. Then run the tests.

Handy commands to build: /scaffold, /verify, /code-review, /security-scan.

Hooks run automatically around tool use - shell commands wired to events:

HookWhat it does
Post-edit on *.csRuns dotnet format - every file stays clean automatically
Pre-commitBlocks DateTime.Now, async void, new HttpClient() in staged files
Pre-bash guardBlocks destructive git ops (force push, reset --hard)

Hooks turn your conventions from "things you hope the AI follows" into "things the tooling enforces."

🔧 Troubleshooting: "my skill won't trigger"
  • Fix the description first - 90% of the time that's it.
  • Reopen the session after adding a skill - it loads at start.
  • Name it explicitly in your prompt ("optimize this EF Core query").
  • Check the folder - a skill is skills/<name>/SKILL.md, not a loose .md.
✅ Your exercise
  1. Install the ToolKit skills and use 5 on a real repo.
  2. Write one custom skill for a task you repeat weekly.
  3. Run one agent (security or code review) and act on its top finding.
  4. Share your custom skill (or what the agent caught) in the feed.
Recap
Skills = reusable, auto-triggered capabilities (the description is the trigger, the body is a checklist). Agents = specialists that review your whole codebase. Together they turn a general assistant into your .NET specialist.

Next → Part 3: MCP - give the AI real tools in C#, so it can do things in your systems, not just write code.

PART 3 · LESSON 3 OF 5

Give the AI real tools: MCP in C#

So far the AI only writes code. In this lesson you give it tools - the ability to actually do things in your systems: query a database, call your API, check a ticket. You build the tool once, in C#, and any AI client can call it.

What MCP is (in one paragraph)

MCP - the Model Context Protocol - is a standard way for AI clients (Claude Code, Copilot, Cursor) to call external tools. You write an MCP server: a small program that exposes some operations as tools. The client tells the model which tools exist, the model picks one when it needs it, your C# runs, and the result flows back into the conversation. Skills tell the AI how to write code; MCP tools let it do something.

🧭 Skill vs. agent vs. MCP tool
Skill = "write this the way we do." Agent = "review my repo and report back." MCP tool = "go touch a real system and bring back real data." The first two shape text; MCP takes action.

How a tool call actually flows

  1. Your server tells the client which tools exist - name, description, parameters.
  2. You ask a question; the model decides a tool would help and picks one.
  3. The client calls your C# method with the arguments the model filled in.
  4. Your code runs - a real query, a real API call - and returns a result.
  5. The model reads that result and answers, grounded in what your tool returned.

You never wire the call up by hand - the model decides, the client routes, your C# runs. Your whole job is to write good tools and describe them well.

When you actually want a tool

You want the AI to…Reach for
Write code your wayCLAUDE.md + skills (Parts 1-2)
Review the whole repo and report backAn agent (Part 2)
Do something in your systems - query a DB, call your API, check a ticketAn MCP tool (this lesson)

Step 1 - Create the server

bash
dotnet new console -n OrdersMcp
cd OrdersMcp
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.Hosting

The C# SDK is ModelContextProtocol. It still ships under --prerelease - drop the flag once your version is stable.

C#
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;

var builder = Host.CreateApplicationBuilder(args);

builder.Services
  .AddMcpServer()               // register the server
  .WithStdioServerTransport()   // the client launches it, talks over stdio
  .WithToolsFromAssembly();     // auto-discover [McpServerTool] methods

// your own services are available to tools via DI
builder.Services.AddSingleton<IOrderService, OrderService>();

await builder.Build().RunAsync();

Three lines do the work: AddMcpServer registers it, WithStdioServerTransport lets the client launch it, and WithToolsFromAssembly auto-discovers your tools.

Step 2 - Write a tool

A tool is just a method with two attributes. The [Description] is what the model reads to decide when to call it - treat it like a skill's trigger. Services are injected from DI, so your tool can use the same IOrderService your app already has.

C#
using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class OrderTools
{
  // The model reads this Description to decide WHEN to call the tool.
  [McpServerTool, Description("Get the current status of an order by its id.")]
  public static async Task<string> GetOrderStatus(
    IOrderService orders,                                  // injected from DI
    [Description("The order id, e.g. 1024")] int orderId,
    CancellationToken ct)
  {
    var order = await orders.FindAsync(orderId, ct);
    return order is null
      ? $"No order {orderId} found."
      : $"Order {orderId}: {order.Status}, total {order.Total:C}.";
  }
}

Step 3 - Connect it to Claude Code

bash
# Claude Code: register the server (run from any project)
claude mcp add orders -- dotnet run --project ./OrdersMcp

# then just ask - Claude calls the tool itself
> what's the status of order 1024?

Prefer config in the repo? Drop a .mcp.json so the whole team gets the same tools:

md
{
  "mcpServers": {
    "orders": {
      "command": "dotnet",
      "args": ["run", "--project", "./OrdersMcp"]
    }
  }
}

stdio vs. HTTP - which transport?

TransportUse it whenHow it runs
stdioLocal dev - the client launches your serverThe client starts your process and talks over stdin/stdout
Streamable HTTPA shared or remote server many clients useYou host it (ASP.NET Core) and clients connect over HTTP

Start with stdio. Move to HTTP once a server needs to be shared, hosted, or consumed by more than a command-line client.

See it in a real project: Performance Lab

Here's a full MCP server I built so you can read real code, not just snippets. Performance Lab lets you ask Copilot or Claude - in plain English - to load-test a .NET API, spot ThreadPool starvation, GC pressure, or a high error rate, and suggest the fix. It ships with a sample API full of intentionally broken endpoints and a Blazor dashboard to visualise the runs.

Because a Blazor dashboard and AI clients both talk to it, the server uses the HTTP transport - the exact case the table above calls out:

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services
  .AddMcpServer()
  .WithHttpTransport()          // remote server - clients connect over HTTP
  .WithToolsFromAssembly();

var app = builder.Build();
app.MapMcp("/mcp");             // the MCP endpoint Copilot/Claude connect to
app.Run();
md
// .vscode/mcp.json  (GitHub Copilot)
{
  "servers": {
    "performance-lab": {
      "type": "http",
      "url": "http://localhost:5200/mcp"
    }
  }
}

The tools are a class with a primary constructor - your services (LoadTestRunner, ResultAnalyzer, a store, a logger) are injected straight in, exactly like anywhere else in ASP.NET Core:

C#
[McpServerToolType]
public sealed class PerformanceTools(
  LoadTestRunner runner,
  ResultAnalyzer analyzer,
  IResultStore   store,
  ILogger<PerformanceTools> logger)          // all injected from DI
{
  [McpServerTool(Name = "run_load_test")]
  [Description(
    "Run a load test against an API endpoint. Fires concurrent HTTP " +
    "requests and returns throughput, latency percentiles, and error " +
    "rate. Returns a result ID for analyze_results or generate_report.")]
  public async Task<string> RunLoadTest(
    [Description("Full URL, e.g. http://localhost:5100/fast")] string url,
    [Description("Seconds to run. Default: 10")] int durationSeconds = 10,
    [Description("Concurrent virtual users. Default: 10")] int concurrentUsers = 10,
    CancellationToken ct = default)
  {
    var result   = await runner.RunAsync(
      new LoadTestRequest { Url = url, DurationSeconds = durationSeconds,
                            ConcurrentUsers = concurrentUsers }, ct);
    var analysis = analyzer.Analyze(result);   // ThreadPool starvation? GC pressure?
    store.Add(result, analysis);
    return FormatLoadTestResult(result, analysis);   // a tight, model-readable summary
  }
}
📦 Clone it and run it
Full solution - API, MCP server, dashboard, tests - MCP Server - API Performance Analysis. Run the three projects, connect Copilot, and ask it to compare /slow vs /fast. You'll watch the AI diagnose your API from real numbers.
⛔ A tool runs with your permissions
An MCP tool executes real code against real systems. Validate every input, scope credentials to the minimum, and never expose a destructive operation (delete, refund, deploy) without a confirmation step. The model will call what you give it.
⚠️ Common mistakes
  • A vague [Description] → the model never calls the tool (same rule as skills).
  • Returning a giant blob - return a tight, readable result the model can use.
  • Logging to stdout on a stdio server - it corrupts the protocol. Log to stderr.
  • Read/write tools with no guardrails. Start read-only.
✅ Your exercise
  1. Build a one-tool MCP server over stdio (start read-only - a lookup).
  2. Register it with claude mcp add and call it from a prompt.
  3. Inject a real service so it returns real data from your app.
  4. Share what tool you exposed in the community feed.
Recap
MCP turns the AI from a code writer into something that can act in your systems. Expose an operation as a tool with [McpServerTool] + a sharp [Description], run it over stdio, and register it with one command. Keep tools scoped and safe - they run for real.

Next → Part 4: stop using other people's AI - build your own AI feature in C#.

PART 4 · LESSON 4 OF 5

Build your first AI feature

Parts 1-3 were about using AI to build .NET. Now you flip sides: you put AI inside your app. The good news - it's just another service you inject. One interface, IChatClient, and any provider behind it.

The one abstraction: IChatClient

Microsoft.Extensions.AI gives .NET a single interface for talking to any LLM - IChatClient. You register one provider at startup; the rest of your code depends on the interface, not on OpenAI or Azure. Swap providers by changing one line.

bash
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
C#
using Microsoft.Extensions.AI;
using OpenAI.Chat;

// One registration. The rest of your app depends on IChatClient, not OpenAI.
builder.Services.AddChatClient(
  new ChatClient("gpt-4o-mini", builder.Configuration["OpenAI:Key"])
    .AsIChatClient());

The model id lives on the ChatClient; .AsIChatClient() adapts it to the standard interface. (The exact adapter name tracks the package version.)

Step 1 - Call the model

Inject IChatClient like any other service and ask:

C#
public sealed class SummaryService(IChatClient chat)
{
  public async Task<string> SummarizeAsync(string text, CancellationToken ct)
  {
    var response = await chat.GetResponseAsync(
      $"Summarize this in exactly two sentences:\n\n{text}",
      cancellationToken: ct);

    return response.Text;
  }
}

Step 2 - Stream the response

For anything user-facing, stream tokens as they come instead of waiting for the whole answer:

C#
// Stream tokens as they arrive - perfect for chat UIs.
await foreach (var update in
  chat.GetStreamingResponseAsync(prompt, cancellationToken: ct))
{
  Console.Write(update.Text);
}

Step 3 - Get structured output (the real unlock)

Free text is hard to use in code. Ask for a typed result and the library builds the JSON schema, tells the model, and parses the response into your record:

C#
public record SupportTicket(string Title, string Priority, string[] Tags);

// Ask for a typed result - the library builds the schema and parses the JSON.
var response = await chat.GetResponseAsync<SupportTicket>(
  $"Turn this email into a support ticket:\n\n{email}",
  cancellationToken: ct);

SupportTicket ticket = response.Result;   // strongly typed, ready to save

This is what turns an LLM from a chatbot into a feature: classify a ticket, extract fields from an email, tag content - all as real C# objects.

Give it a role - and memory

A single prompt is stateless. Pass a list of messages instead - a System message sets the role and rules, and you keep appending turns so the model remembers the conversation:

C#
List<ChatMessage> chat =
[
  new(ChatRole.System, "You are a terse .NET assistant. Answer in one paragraph."),
  new(ChatRole.User,   userQuestion),
];

var response = await chat_client.GetResponseAsync(chat, cancellationToken: ct);
chat.AddMessages(response);   // keep the reply so the next turn has context

The System message is where you put guardrails - tone, format, "only answer from the context I give you." It's the CLAUDE.md of your feature.

Let the model call your code (tools)

Same idea as MCP from Part 3 - but in-process. Hand the model a C# method and it decides when to call it, so your feature can pull real data mid-answer instead of guessing:

C#
// Expose a plain C# method as a tool the model can call.
[Description("Get the current stock count for a product SKU.")]
static int GetStock(string sku) => Inventory.CountFor(sku);

var options = new ChatOptions
{
  Tools = [AIFunctionFactory.Create(GetStock)]
};

// With .UseFunctionInvocation() on the pipeline, the library runs the
// call for you and feeds the result back to the model - automatically.
var response = await chat.GetResponseAsync(
  "How many units of SKU-42 are left?", options, cancellationToken: ct);

MCP tools do this across processes for any client; AIFunctionFactory does it inside one app. This is the seed of an AI agent (Step 7 in the roadmap).

Swap providers without touching your code

ProviderRegister withGood for
OpenAInew ChatClient("gpt-4o-mini", key).AsIChatClient()Default - cheap and capable
Azure OpenAInew AzureOpenAIClient(...).GetChatClient(deployment).AsIChatClient()Your Azure tenant / enterprise
Ollama (local)new OllamaApiClient(uri, "llama3.1")Free local dev - no data leaves your machine

Develop locally against Ollama (free, private), ship on OpenAI or Azure. Your feature code never changes.

Production niceties, almost for free

IChatClient is a pipeline - wrap it with middleware the same way you'd wrap an HTTP client:

C#
builder.Services
  .AddChatClient(new ChatClient("gpt-4o-mini", key).AsIChatClient())
  .UseFunctionInvocation()   // let the model call your C# tools
  .UseOpenTelemetry()        // traces + token metrics
  .UseLogging();             // every call logged
Add to the pipelineWhat you get
.UseFunctionInvocation()The model can call your C# functions (tools)
.UseOpenTelemetry()Traces + token metrics in your observability stack
.UseDistributedCache()Identical prompts served from cache - saves tokens
.UseLogging()Every prompt and response logged
⚠️ Tokens are money - and latency
  • Use the smallest model that works (like gpt-4o-mini) - upgrade only when quality demands it.
  • Cache identical prompts with .UseDistributedCache().
  • Keep prompts tight and cap the output length - you pay for both directions.
  • Never put a secret key in source - use configuration / user-secrets.
⛔ The model can be wrong - and confident
Treat every response as untrusted input. Validate structured output before you save it, never run model text as code or SQL, and don't surface raw answers where correctness is critical without a check. You're still the engineer.
✅ Your exercise
  1. Register an IChatClient and call it from one endpoint.
  2. Return a typed result with GetResponseAsync<T>.
  3. Point it at Ollama locally, then at OpenAI - same code.
  4. Add .UseLogging() and look at what a call actually costs.
Recap
An AI feature in .NET is one interface: IChatClient. Register a provider once, call GetResponseAsync, stream for UIs, and use structured output to get typed objects instead of text. Mind tokens, and treat every answer as untrusted.

Next → Part 5: chain everything into one repeatable workflow.

PART 5 · LESSON 5 OF 5

Tie it together: one repeatable workflow

You now have four pieces - context, skills & agents, MCP tools, and AI features. The last skill is orchestration: wiring them into one repeatable loop so shipping a feature is a single command, not ten manual steps.

The loop every task should follow

Plan → Build → Verify → Review → you ship. Each stage is powered by something you already built:

StageWhat runs itFrom
ContextCLAUDE.mdPart 1
ScaffoldA skillPart 2
Touch real systemsAn MCP toolPart 3
VerifyHooks + dotnet testPart 2
ReviewAn agentPart 2
OrchestrateA slash commandThis lesson
Approve & shipYouAlways

Step 1 - Wrap the loop in a slash command

A slash command turns the whole sequence into one word. It calls your skills, runs your tests, and invokes your agent - in order, every time:

md
# .claude/commands/ship.md
Ship a feature end to end. Do NOT commit - I do that.

1. Give me a short plan and wait for my OK.
2. Scaffold the vertical slice with the my-endpoint skill.
3. Write integration tests (WebApplicationFactory + Testcontainers).
4. Run `dotnet build && dotnet test`. Fix failures, repeat until green.
5. Run the aspnetcore-security-auditor agent on the new code.
6. Summarize the diff + the agent's top findings, then stop.
bash
> /ship a create-invoice endpoint

# Claude plans -> scaffolds (skill) -> tests -> reviews (agent)
# -> hands you the diff. You read it and commit.

Same steps, same quality bar - 9am or a Friday deploy. That's the point of a workflow: consistency you don't have to remember.

Step 2 - Keep the gates in place

  • Plan mode for anything risky - it proposes, you approve, then it touches files.
  • Hooks enforce your rules automatically (format on save, block bad patterns pre-commit).
  • An agent is the second pair of eyes before you commit.
  • You read the diff and press commit. The AI never ships on its own.
💡 The mindset
The AI executes; you decide. A good workflow moves the boring steps to the machine and keeps every real decision - and the commit - with you.

Step 3 - Let it reach further with MCP

Because your tools from Part 3 are in the loop, the workflow isn't limited to writing code. Wire the Performance Lab MCP server into a /triage command and the AI can measure a slow endpoint, name the cause from real numbers, propose the fix, and prove it - one pass, grounded in your systems:

md
# .claude/commands/triage.md
Triage a slow endpoint end to end:
1. Run the performance-lab MCP tool (run_load_test) on the endpoint.
2. From the numbers, name the likely cause - ThreadPool starvation? GC? N+1?
3. Propose the minimal .NET fix and show the diff. Do NOT apply yet.
4. On my OK, apply it and re-run the test to prove it's faster.

That's the payoff of the whole track: context (Part 1) + a skill's checklist (Part 2) + a real tool (Part 3) + a model call (Part 4), orchestrated into one command - and you still approve every change.

⚠️ Don't automate away your judgment
  • No auto-commit, no auto-deploy from a command. Always end with a human gate.
  • Keep commands small and composable - /scaffold, /verify, /ship - not one mega-command.
  • If you can't explain what a step did, don't ship it.
✅ Your exercise
  1. Write a /ship command that scaffolds, tests, and reviews - then stops for you.
  2. Add one hook (format on save, or a pre-commit guard).
  3. Run a full feature through the loop without leaving Claude Code.
  4. Share your command file in the community feed.
Recap
A workflow chains your context, skills, agents, MCP tools, and tests into one repeatable loop behind a slash command - with plan mode, hooks, and you as the gates. That's the whole of Track A: the AI does the work, you stay the engineer.

That's Track A. Next → Track B: build AI into your apps at depth - embeddings & semantic search, RAG, and agents. Grab a runnable project to start from, or see the full AI Roadmap for .NET.

QUICK REFERENCE

The one-screen cheat sheet

Install

bash
# Claude Code
npm install -g @anthropic-ai/claude-code

# The .NET AI ToolKit (skills + agents)
/plugin marketplace add StefanTheCode/dotnet-ai-toolkit
/plugin install dotnet-ai-toolkit@thecodeman-ai-toolkit
/plugin marketplace update thecodeman-ai-toolkit   # get new skills

# Build an MCP server in C# (Part 3)
dotnet add package ModelContextProtocol --prerelease

# Call an LLM from .NET (Part 4)
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI

Where things live

PathWhat
CLAUDE.md (repo root)Project context - commit it
~/.claude/CLAUDE.mdYour personal defaults, every project
.claude/skills/<name>/SKILL.mdA skill
.claude/agents/<name>.mdAn agent
.claude/commands/<name>.mdA slash command
.mcp.json (repo root)MCP servers (tools) the AI can call - Part 3

Prompts that pull their weight

bash
> Give me a short plan first, then wait for my OK
> This EF query is slow, optimize it
> Refactor this to the Result pattern; leave idiomatic code alone
> Write integration tests with WebApplicationFactory + Testcontainers
> Review this PR like a senior .NET engineer
> Audit the security of this API
> Don't invent problems - leave correct code alone
GLOSSARY

The words, in plain English

TermWhat it means
CLAUDE.mdA context file at your repo root the AI reads at the start of every session - your stack, conventions, and what to never do.
SkillA reusable, auto-triggered capability (one SKILL.md) that does one focused thing well.
AgentA specialist that explores your codebase on its own and returns a report. You invoke it by asking for what it does.
Slash commandA saved workflow you trigger with /name - it orchestrates skills and agents.
HookA shell command that runs automatically around tool use (e.g. format on every edit).
MCPModel Context Protocol - a standard that lets AI clients call external tools/data. You can build an MCP server in C#.
LLMLarge Language Model - the model behind Claude/GPT that generates text.
EmbeddingA vector representation of text, so you can search by meaning.
RAGRetrieval-Augmented Generation - retrieve your relevant data, then let the LLM answer from it (covered in Track B).
IChatClientThe Microsoft.Extensions.AI abstraction for calling any LLM provider from .NET.
MCP serverA small C# app that exposes your operations as tools any AI client can call - built with the ModelContextProtocol SDK.
Tool callingWhen the LLM decides to call one of your functions (or MCP tools), then uses the result in its answer.
Structured outputAsking the LLM for a typed result (GetResponseAsync<T>) instead of free text, so you get a parsed object back.
Plan modeA Claude Code mode where it proposes a full plan and waits for your approval before touching any file.
FAQ

Frequently asked

Does this only work with Claude Code?

The lessons use Claude Code, but the ideas map across tools: CLAUDE.md maps to Cursor rules / Copilot instructions, and skills/agents/MCP are increasingly supported everywhere.

Is the AI going to replace me?

No. It replaces the developer who refuses to use it. You stay the engineer - you review everything, and you're still responsible for what ships.

Do I need to know AI/ML math for this?

No. This is applied engineering, not data science. You won't derive a transformer. You'll ship features.

How many skills should I install?

Install a set, but reach for the 5 that match your daily work. A library of 44 you never open helps no one.

My skill isn't triggering - what's wrong?

Almost always the description. Make it specific about when to fire, add synonyms, and reopen the session.

Is it safe to let the AI edit my repo?

Use plan mode for risky changes, keep your work in git, and review diffs. Add a pre-commit hook to block bad patterns. You approve; it executes.

This is Track A. Track B is where it gets rare.

The full course - Claude Code, skills, MCP in C#, then building real AI features (LLMs, RAG, agents) into your .NET apps - is inside the community, with new video clips as I record them.

Join the community → Free newsletter (20k+)

Built by Stefan Đokić - TheCodeMan · Microsoft MVP