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.
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.
# install, then run inside any project folder
npm install -g @anthropic-ai/claude-code
claudeThis 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.
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:
# 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.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
}
}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.
They stack - so you can set global preferences once and override per project:
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."
The pattern: state the outcome, name the constraints, and say what to avoid.
These are the patterns your CLAUDE.md should steer toward - and the raw material for a sharp "Never suggest" section.
CLAUDE.md to a real project.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.
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.
/plugin marketplace add StefanTheCode/dotnet-ai-toolkit
/plugin install dotnet-ai-toolkit@thecodeman-ai-toolkitThen just talk to Claude Code normally and the matching skill kicks in - no command to remember:
> This EF query is slow, optimize it
> Scaffold a products endpoint
> Write integration tests for the checkout flow
> Review this PR for .NET antipatternsEach 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.
Reopen the session after adding a skill so it loads. Update marketplace skills anytime with /plugin marketplace update thecodeman-ai-toolkit.
A skill is a folder with a SKILL.md: YAML frontmatter (the trigger) + instructions (what to do).
---
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.description is the trigger. Make it specific and slightly pushy about when to fire. Vague description → skill never triggers.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.
Pick one task you repeat weekly. Create .claude/skills/my-endpoint/SKILL.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.
When you want a review of the whole codebase, ask for what the agent does:
> Audit the security of this API
> Review this PR like a senior .NET engineer
> Find the architecture problems in this solutionThe 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:
---
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.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/:
# .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:
Hooks turn your conventions from "things you hope the AI follows" into "things the tooling enforces."
description first - 90% of the time that's it.skills/<name>/SKILL.md, not a loose .md.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.
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.
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.
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.
dotnet new console -n OrdersMcp
cd OrdersMcp
dotnet add package ModelContextProtocol --prerelease
dotnet add package Microsoft.Extensions.HostingThe C# SDK is ModelContextProtocol. It still ships under --prerelease - drop the flag once your version is stable.
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.
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.
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}.";
}
}# 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:
{
"mcpServers": {
"orders": {
"command": "dotnet",
"args": ["run", "--project", "./OrdersMcp"]
}
}
}Start with stdio. Move to HTTP once a server needs to be shared, hosted, or consumed by more than a command-line client.
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:
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();// .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:
[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
}
}/slow vs /fast. You'll watch the AI diagnose your API from real numbers.[Description] → the model never calls the tool (same rule as skills).claude mcp add and call it from a prompt.[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#.
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.
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.
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAIusing 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.)
Inject IChatClient like any other service and ask:
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;
}
}For anything user-facing, stream tokens as they come instead of waiting for the whole answer:
// Stream tokens as they arrive - perfect for chat UIs.
await foreach (var update in
chat.GetStreamingResponseAsync(prompt, cancellationToken: ct))
{
Console.Write(update.Text);
}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:
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 saveThis 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.
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:
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 contextThe 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.
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:
// 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).
Develop locally against Ollama (free, private), ship on OpenAI or Azure. Your feature code never changes.
IChatClient is a pipeline - wrap it with middleware the same way you'd wrap an HTTP client:
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 loggedgpt-4o-mini) - upgrade only when quality demands it..UseDistributedCache().IChatClient and call it from one endpoint.GetResponseAsync<T>..UseLogging() and look at what a call actually costs.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.
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.
Plan → Build → Verify → Review → you ship. Each stage is powered by something you already built:
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:
# .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.> /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.
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:
# .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.
/scaffold, /verify, /ship - not one mega-command./ship command that scaffolds, tests, and reviews - then stops for you.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.
Each is a self-contained solution with its own README - clone it, set your own keys, run it. All on .NET 10. Read the code alongside the lessons above.
All four live in one repo: github.com/StefanTheCode/AI-in-.NET
# 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> 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 aloneThe 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.
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.
No. This is applied engineering, not data science. You won't derive a transformer. You'll ship features.
Install a set, but reach for the 5 that match your daily work. A library of 44 you never open helps no one.
Almost always the description. Make it specific about when to fire, add synonyms, and reopen the session.
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.
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